use bex_types::{BexError, Manifest}; use sha2::{Sha256, Digest}; pub const MAGIC: &[u8; 4] = b"BEX\x01"; pub const CONTAINER_VERSION: u16 = 1; pub const HEADER_LEN: usize = 96; pub struct BexPackage { pub manifest: Manifest, pub wasm: Vec, } fn sha256(data: &[u8]) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(data); hasher.finalize().into() } pub fn read_manifest(data: &[u8]) -> Result { if data.len() < HEADER_LEN { return Err(BexError::ManifestInvalid("too short".into())); } if &data[0..4] != MAGIC { return Err(BexError::ManifestInvalid("bad magic".into())); } let expected_crc = u32::from_le_bytes(data[92..96].try_into().unwrap()); let actual_crc = crc32fast::hash(&data[0..92]); if expected_crc != actual_crc { return Err(BexError::ManifestInvalid("header CRC mismatch".into())); } let manifest_len = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize; if HEADER_LEN.checked_add(manifest_len).map_or(true, |end| end > data.len()) { return Err(BexError::ManifestInvalid( format!("manifest_len {} exceeds package size {}", manifest_len, data.len()) )); } let manifest_bytes = &data[HEADER_LEN..HEADER_LEN + manifest_len]; let expected_hash = &data[60..92]; let actual_hash = sha256(manifest_bytes); if expected_hash != actual_hash { return Err(BexError::ManifestInvalid("manifest hash mismatch".into())); } serde_yaml::from_slice(manifest_bytes).map_err(|e| BexError::ManifestInvalid(format!("yaml: {e}"))) } pub fn unpack(data: &[u8]) -> Result { let manifest = read_manifest(data)?; let flags = u16::from_le_bytes(data[6..8].try_into().unwrap()); let manifest_len = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize; if HEADER_LEN.checked_add(manifest_len).map_or(true, |end| end > data.len()) { return Err(BexError::ManifestInvalid( format!("manifest_len {} exceeds package size {}", manifest_len, data.len()) )); } let wasm_original_len = u64::from_le_bytes(data[20..28].try_into().unwrap()) as usize; let wasm_start = HEADER_LEN + manifest_len; let wasm = if flags & 1 != 0 { let mut out = Vec::with_capacity(wasm_original_len); zstd::stream::copy_decode(&data[wasm_start..], &mut out) .map_err(|e| BexError::Internal(format!("zstd: {e}")))?; out } else { data[wasm_start..].to_vec() }; let expected = &data[28..60]; let actual = sha256(&wasm); if expected != actual { return Err(BexError::HashMismatch { plugin_id: manifest.id.clone() }); } Ok(BexPackage { manifest, wasm }) } pub fn pack(manifest: &Manifest, wasm: &[u8]) -> Result, BexError> { let yaml = serde_yaml::to_string(manifest).map_err(|e| BexError::Internal(e.to_string()))?; let yaml_bytes = yaml.as_bytes(); let compressed = zstd::bulk::compress(wasm, 9).map_err(|e| BexError::Internal(e.to_string()))?; let mut out = Vec::with_capacity(HEADER_LEN + yaml_bytes.len() + compressed.len()); out.extend_from_slice(MAGIC); out.extend_from_slice(&CONTAINER_VERSION.to_le_bytes()); out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&(yaml_bytes.len() as u32).to_le_bytes()); out.extend_from_slice(&(compressed.len() as u64).to_le_bytes()); out.extend_from_slice(&(wasm.len() as u64).to_le_bytes()); out.extend_from_slice(&sha256(wasm)); out.extend_from_slice(&sha256(yaml_bytes)); let crc = crc32fast::hash(&out[0..92]); out.extend_from_slice(&crc.to_le_bytes()); out.extend_from_slice(yaml_bytes); out.extend_from_slice(&compressed); Ok(out) } #[cfg(test)] mod tests { use super::*; use bex_types::manifest::*; fn test_manifest() -> Manifest { Manifest { schema: 1, id: "com.test.plugin".into(), name: "Test".into(), version: "1.0.0".into(), authors: vec!["dev".into()], abi: ">=1.0.0,<2.0.0".into(), provides: ProvidesSpec { search: true, info: true, ..Default::default() }, network: NetworkSpec { hosts: vec!["*".into()], concurrent: 8 }, storage: false, secrets: vec![], allow_js: false, allow_js_fetch: false, display: DisplaySpec { description: Some("test".into()), ..Default::default() }, } } #[test] fn round_trip() { let m = test_manifest(); let wasm = b"\x00asm\x01\x00\x00\x00"; let packed = pack(&m, wasm).unwrap(); let unpacked = unpack(&packed).unwrap(); assert_eq!(unpacked.manifest.id, "com.test.plugin"); assert_eq!(unpacked.wasm, wasm.to_vec()); } #[test] fn read_manifest_from_packed() { let m = test_manifest(); let packed = pack(&m, b"test").unwrap(); let read = read_manifest(&packed).unwrap(); assert_eq!(read.id, "com.test.plugin"); } #[test] fn bad_magic() { let data = vec![0u8; 96]; let r = read_manifest(&data); assert!(r.is_err()); } }