Skip to main content

shape_runtime/
module_manifest.rs

1//! Module manifest for content-addressed module distribution.
2//!
3//! A `ModuleManifest` describes a distributable module by mapping its exported
4//! functions and type schemas to content-addressed hashes. This allows the
5//! loader to fetch only the blobs it needs from a `BlobStore`.
6
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use std::collections::HashMap;
10
11/// Manifest for a content-addressed module distribution.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ModuleManifest {
14    pub name: String,
15    pub version: String,
16    /// Exported function names mapped to their content hashes.
17    pub exports: HashMap<String, [u8; 32]>,
18    /// Type schema names mapped to their content hashes.
19    pub type_schemas: HashMap<String, [u8; 32]>,
20    /// Permissions required by this module.
21    pub required_permission_bits: u64,
22    /// Transitive dependency closure for each export: export hash → list of dependency hashes.
23    #[serde(default)]
24    pub dependency_closure: HashMap<[u8; 32], Vec<[u8; 32]>>,
25    /// SHA-256 hash of this manifest (excluding this field and signature).
26    pub manifest_hash: [u8; 32],
27    /// Optional cryptographic signature.
28    pub signature: Option<ModuleSignature>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ModuleSignature {
33    pub author_key: [u8; 32],
34    /// Ed25519 signature (64 bytes). Uses `Vec<u8>` because serde does not
35    /// support `[u8; 64]` out of the box.
36    pub signature: Vec<u8>,
37    pub signed_at: u64,
38}
39
40/// Helper struct for deterministic manifest hashing.
41/// We hash only the fields that define the manifest's identity,
42/// excluding `manifest_hash` and `signature`.
43#[derive(Serialize)]
44struct ManifestHashInput<'a> {
45    name: &'a str,
46    version: &'a str,
47    exports: Vec<(&'a String, &'a [u8; 32])>,
48    type_schemas: Vec<(&'a String, &'a [u8; 32])>,
49    required_permission_bits: u64,
50    dependency_closure: Vec<(&'a [u8; 32], &'a Vec<[u8; 32]>)>,
51}
52
53impl ModuleManifest {
54    pub fn new(name: String, version: String) -> Self {
55        Self {
56            name,
57            version,
58            exports: HashMap::new(),
59            type_schemas: HashMap::new(),
60            required_permission_bits: 0,
61            dependency_closure: HashMap::new(),
62            manifest_hash: [0u8; 32],
63            signature: None,
64        }
65    }
66
67    pub fn add_export(&mut self, name: String, hash: [u8; 32]) {
68        self.exports.insert(name, hash);
69    }
70
71    pub fn add_type_schema(&mut self, name: String, hash: [u8; 32]) {
72        self.type_schemas.insert(name, hash);
73    }
74
75    /// Compute `manifest_hash` from the identity fields.
76    ///
77    /// Exports and type schemas are sorted by key for deterministic hashing.
78    pub fn finalize(&mut self) {
79        let mut exports: Vec<_> = self.exports.iter().collect();
80        exports.sort_by_key(|(k, _)| *k);
81
82        let mut type_schemas: Vec<_> = self.type_schemas.iter().collect();
83        type_schemas.sort_by_key(|(k, _)| *k);
84
85        let mut dep_closure: Vec<_> = self.dependency_closure.iter().collect();
86        dep_closure.sort_by_key(|(k, _)| *k);
87
88        let input = ManifestHashInput {
89            name: &self.name,
90            version: &self.version,
91            exports,
92            type_schemas,
93            required_permission_bits: self.required_permission_bits,
94            dependency_closure: dep_closure,
95        };
96
97        let bytes = rmp_serde::encode::to_vec(&input)
98            .expect("ManifestHashInput serialization should not fail");
99        let digest = Sha256::digest(&bytes);
100        self.manifest_hash.copy_from_slice(&digest);
101    }
102
103    /// Verify that `manifest_hash` matches the current content.
104    pub fn verify_integrity(&self) -> bool {
105        let mut exports: Vec<_> = self.exports.iter().collect();
106        exports.sort_by_key(|(k, _)| *k);
107
108        let mut type_schemas: Vec<_> = self.type_schemas.iter().collect();
109        type_schemas.sort_by_key(|(k, _)| *k);
110
111        let mut dep_closure: Vec<_> = self.dependency_closure.iter().collect();
112        dep_closure.sort_by_key(|(k, _)| *k);
113
114        let input = ManifestHashInput {
115            name: &self.name,
116            version: &self.version,
117            exports,
118            type_schemas,
119            required_permission_bits: self.required_permission_bits,
120            dependency_closure: dep_closure,
121        };
122
123        let bytes = rmp_serde::encode::to_vec(&input)
124            .expect("ManifestHashInput serialization should not fail");
125        let digest = Sha256::digest(&bytes);
126        let mut expected = [0u8; 32];
127        expected.copy_from_slice(&digest);
128        self.manifest_hash == expected
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_new_manifest_has_zero_hash() {
138        let m = ModuleManifest::new("test".into(), "0.1.0".into());
139        assert_eq!(m.manifest_hash, [0u8; 32]);
140        assert!(m.exports.is_empty());
141        assert!(m.type_schemas.is_empty());
142    }
143
144    #[test]
145    fn test_finalize_produces_nonzero_hash() {
146        let mut m = ModuleManifest::new("mymod".into(), "1.0.0".into());
147        m.add_export("greet".into(), [1u8; 32]);
148        m.finalize();
149        assert_ne!(m.manifest_hash, [0u8; 32]);
150    }
151
152    #[test]
153    fn test_verify_integrity_passes_after_finalize() {
154        let mut m = ModuleManifest::new("mymod".into(), "1.0.0".into());
155        m.add_export("greet".into(), [1u8; 32]);
156        m.add_type_schema("MyType".into(), [2u8; 32]);
157        m.required_permission_bits = 0x03;
158        m.finalize();
159        assert!(m.verify_integrity());
160    }
161
162    #[test]
163    fn test_verify_integrity_fails_after_mutation() {
164        let mut m = ModuleManifest::new("mymod".into(), "1.0.0".into());
165        m.add_export("greet".into(), [1u8; 32]);
166        m.finalize();
167        assert!(m.verify_integrity());
168
169        m.add_export("farewell".into(), [3u8; 32]);
170        assert!(!m.verify_integrity());
171    }
172
173    #[test]
174    fn test_deterministic_hash() {
175        let build = || {
176            let mut m = ModuleManifest::new("det".into(), "0.0.1".into());
177            m.add_export("b_fn".into(), [10u8; 32]);
178            m.add_export("a_fn".into(), [20u8; 32]);
179            m.add_type_schema("Z".into(), [30u8; 32]);
180            m.add_type_schema("A".into(), [40u8; 32]);
181            m.finalize();
182            m.manifest_hash
183        };
184        assert_eq!(build(), build());
185    }
186
187    #[test]
188    fn test_serde_roundtrip() {
189        let mut m = ModuleManifest::new("serde_test".into(), "2.0.0".into());
190        m.add_export("run".into(), [7u8; 32]);
191        m.required_permission_bits = 0xFF;
192        m.finalize();
193
194        let json = serde_json::to_string(&m).expect("serialize");
195        let restored: ModuleManifest = serde_json::from_str(&json).expect("deserialize");
196
197        assert_eq!(restored.name, "serde_test");
198        assert_eq!(restored.version, "2.0.0");
199        assert_eq!(restored.exports.get("run"), Some(&[7u8; 32]));
200        assert_eq!(restored.required_permission_bits, 0xFF);
201        assert!(restored.verify_integrity());
202    }
203}