File size: 5,104 Bytes
3374e90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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<u8>,
}

fn sha256(data: &[u8]) -> [u8; 32] {
    let mut hasher = Sha256::new();
    hasher.update(data);
    hasher.finalize().into()
}

pub fn read_manifest(data: &[u8]) -> Result<Manifest, BexError> {
    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<BexPackage, BexError> {
    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<Vec<u8>, 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());
    }
}