1use 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}