Skip to main content

hara_native/
compiled_product.rs

1//! Immutable identities for compiled Hara products.
2//!
3//! The product layer deliberately contains no evaluator or filesystem policy.
4//! It gives compiler targets, runtimes, and browser hosts one deterministic
5//! description of the bytes they exchange.
6
7use sha2::{Digest, Sha256};
8use std::collections::HashMap;
9
10pub const COMPILED_PRODUCT_MANIFEST_SCHEMA: &str = "hara.compiled-product.manifest/0-alpha";
11
12#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
13pub enum CompiledProductKind {
14    HbcModule,
15    HbcPackage,
16    WholeWasm,
17}
18
19impl CompiledProductKind {
20    pub const fn as_str(self) -> &'static str {
21        match self {
22            Self::HbcModule => "hbc-module",
23            Self::HbcPackage => "hbc-package",
24            Self::WholeWasm => "whole-wasm",
25        }
26    }
27
28    pub const fn format(self) -> &'static str {
29        match self {
30            Self::HbcModule => "HBC0",
31            Self::HbcPackage => "HBX0",
32            Self::WholeWasm => "HNW0",
33        }
34    }
35}
36
37#[derive(Clone, Debug, Hash, PartialEq, Eq)]
38pub struct ProductCacheKey {
39    pub kind: CompiledProductKind,
40    pub source_digest: String,
41    pub module_digests: Vec<String>,
42    pub compiler_id: String,
43    pub abi_version: String,
44    pub options_digest: String,
45}
46
47impl ProductCacheKey {
48    pub fn new(
49        kind: CompiledProductKind,
50        source_digest: impl Into<String>,
51        compiler_id: impl Into<String>,
52        abi_version: impl Into<String>,
53        options: impl AsRef<[u8]>,
54    ) -> Self {
55        Self::with_module_digests(
56            kind,
57            source_digest,
58            compiler_id,
59            abi_version,
60            options,
61            Vec::new(),
62        )
63    }
64
65    pub fn with_module_digests(
66        kind: CompiledProductKind,
67        source_digest: impl Into<String>,
68        compiler_id: impl Into<String>,
69        abi_version: impl Into<String>,
70        options: impl AsRef<[u8]>,
71        module_digests: Vec<String>,
72    ) -> Self {
73        Self {
74            kind,
75            source_digest: source_digest.into(),
76            module_digests,
77            compiler_id: compiler_id.into(),
78            abi_version: abi_version.into(),
79            options_digest: sha256_hex(options.as_ref()),
80        }
81    }
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct CompiledProductManifest {
86    pub schema: String,
87    pub product: CompiledProductKind,
88    pub format: String,
89    pub abi_version: String,
90    pub compiler_id: String,
91    pub source_digest: String,
92    pub module_digests: Vec<String>,
93    pub options_digest: String,
94    pub artifact_digest: String,
95    pub artifact_bytes: usize,
96}
97
98impl CompiledProductManifest {
99    pub fn to_json(&self) -> serde_json::Value {
100        let mut manifest = serde_json::json!({
101            "schema": self.schema,
102            "product": self.product.as_str(),
103            "format": self.format,
104            "abi-version": self.abi_version,
105            "compiler-id": self.compiler_id,
106            "source-digest": self.source_digest,
107            "module-digests": self.module_digests,
108            "options-digest": self.options_digest,
109            "artifact-digest": self.artifact_digest,
110            "artifact-bytes": self.artifact_bytes,
111        });
112        if self.product == CompiledProductKind::WholeWasm {
113            manifest["entrypoint"] = serde_json::json!("hara_entry");
114            manifest["error-global"] = serde_json::json!("hara_error");
115            manifest["heap-global"] = serde_json::json!("hara_heap");
116            manifest["import-module"] = serde_json::json!("hara");
117        }
118        manifest
119    }
120
121    pub fn cache_key(&self) -> ProductCacheKey {
122        ProductCacheKey {
123            kind: self.product,
124            source_digest: self.source_digest.clone(),
125            module_digests: self.module_digests.clone(),
126            compiler_id: self.compiler_id.clone(),
127            abi_version: self.abi_version.clone(),
128            options_digest: self.options_digest.clone(),
129        }
130    }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct CompiledProduct {
135    pub manifest: CompiledProductManifest,
136    pub bytes: Vec<u8>,
137}
138
139impl CompiledProduct {
140    pub fn new(
141        product: CompiledProductKind,
142        source_digest: impl Into<String>,
143        module_digests: Vec<String>,
144        compiler_id: impl Into<String>,
145        abi_version: impl Into<String>,
146        options: impl AsRef<[u8]>,
147        bytes: Vec<u8>,
148    ) -> Self {
149        let compiler_id = compiler_id.into();
150        let abi_version = abi_version.into();
151        let options_digest = sha256_hex(options.as_ref());
152        let manifest = CompiledProductManifest {
153            schema: COMPILED_PRODUCT_MANIFEST_SCHEMA.into(),
154            product,
155            format: product.format().into(),
156            abi_version,
157            compiler_id,
158            source_digest: source_digest.into(),
159            module_digests,
160            options_digest,
161            artifact_digest: sha256_hex(&bytes),
162            artifact_bytes: bytes.len(),
163        };
164        Self { manifest, bytes }
165    }
166
167    pub fn cache_key(&self) -> ProductCacheKey {
168        self.manifest.cache_key()
169    }
170
171    pub fn verify(&self) -> Result<(), String> {
172        if self.manifest.artifact_bytes != self.bytes.len() {
173            return Err("compiled product manifest byte length mismatch".into());
174        }
175        if self.manifest.artifact_digest != sha256_hex(&self.bytes) {
176            return Err("compiled product manifest digest mismatch".into());
177        }
178        Ok(())
179    }
180}
181
182#[derive(Default)]
183pub struct InMemoryProductCache {
184    products: HashMap<ProductCacheKey, CompiledProduct>,
185}
186
187impl InMemoryProductCache {
188    pub fn get(&self, key: &ProductCacheKey) -> Option<&CompiledProduct> {
189        self.products.get(key)
190    }
191
192    pub fn insert(&mut self, product: CompiledProduct) -> Result<ProductCacheKey, String> {
193        product.verify()?;
194        let key = product.cache_key();
195        self.products.insert(key.clone(), product);
196        Ok(key)
197    }
198
199    pub fn remove(&mut self, key: &ProductCacheKey) -> Option<CompiledProduct> {
200        self.products.remove(key)
201    }
202
203    pub fn len(&self) -> usize {
204        self.products.len()
205    }
206
207    pub fn is_empty(&self) -> bool {
208        self.products.is_empty()
209    }
210
211    pub fn clear(&mut self) {
212        self.products.clear();
213    }
214}
215
216pub fn sha256_hex(bytes: &[u8]) -> String {
217    let digest = Sha256::digest(bytes);
218    digest.iter().map(|byte| format!("{byte:02x}")).collect()
219}
220
221#[cfg(test)]
222mod tests {
223    use super::{CompiledProduct, CompiledProductKind, InMemoryProductCache};
224
225    fn product(bytes: &[u8]) -> CompiledProduct {
226        CompiledProduct::new(
227            CompiledProductKind::HbcModule,
228            "source-digest",
229            vec!["module-digest".into()],
230            "hara-test",
231            "1",
232            "{}",
233            bytes.to_vec(),
234        )
235    }
236
237    #[test]
238    fn manifest_is_self_verifying_and_json_stable() {
239        let product = product(b"HBC0");
240        product.verify().unwrap();
241        assert_eq!(product.manifest.format, "HBC0");
242        assert_eq!(product.manifest.artifact_bytes, 4);
243        assert_eq!(product.manifest.to_json()["product"], "hbc-module");
244    }
245
246    #[test]
247    fn cache_reuses_exact_products_and_separates_target_keys() {
248        let mut cache = InMemoryProductCache::default();
249        let first = product(b"first");
250        let key = cache.insert(first.clone()).unwrap();
251        assert_eq!(cache.get(&key), Some(&first));
252        assert_eq!(cache.len(), 1);
253
254        let other = CompiledProduct::new(
255            CompiledProductKind::WholeWasm,
256            "source-digest",
257            vec!["module-digest".into()],
258            "hara-test",
259            "1",
260            "{}",
261            b"first".to_vec(),
262        );
263        let other_key = cache.insert(other).unwrap();
264        assert_ne!(key, other_key);
265        assert_eq!(cache.len(), 2);
266        cache.clear();
267        assert!(cache.is_empty());
268    }
269
270    #[test]
271    fn cache_keys_include_module_dependencies() {
272        let first = CompiledProduct::new(
273            CompiledProductKind::HbcModule,
274            "source-digest",
275            vec!["module-a".into()],
276            "hara-test",
277            "1",
278            "{}",
279            b"first".to_vec(),
280        );
281        let second = CompiledProduct::new(
282            CompiledProductKind::HbcModule,
283            "source-digest",
284            vec!["module-b".into()],
285            "hara-test",
286            "1",
287            "{}",
288            b"first".to_vec(),
289        );
290
291        assert_ne!(first.cache_key(), second.cache_key());
292    }
293
294    #[test]
295    fn cache_rejects_tampered_products() {
296        let mut product = product(b"valid");
297        product.bytes.push(0);
298        let mut cache = InMemoryProductCache::default();
299        let error = cache.insert(product).unwrap_err();
300        assert_eq!(error, "compiled product manifest byte length mismatch");
301    }
302}