1use std::fs;
2use std::path::{Path, PathBuf};
3use std::time::UNIX_EPOCH;
4
5use dashmap::DashMap;
6use once_cell::sync::Lazy;
7
8#[cfg(test)]
9use std::sync::atomic::{AtomicUsize, Ordering};
10
11use crate::abi;
12use crate::capabilities::Capabilities;
13use crate::describe::{self, DescribePayload};
14use crate::error::ComponentError;
15use crate::lifecycle::Lifecycle;
16use crate::limits::Limits;
17use crate::loader;
18use crate::manifest::ComponentManifest;
19use crate::schema::{self, JsonPath};
20use crate::signing::{SigningError, compute_wasm_hash};
21use crate::telemetry::TelemetrySpec;
22
23#[derive(Debug, Clone)]
24pub struct PreparedComponent {
25 pub manifest: ComponentManifest,
26 pub manifest_path: PathBuf,
27 pub wasm_path: PathBuf,
28 pub root: PathBuf,
29 pub wasm_hash: String,
30 pub describe: DescribePayload,
31 pub lifecycle: Lifecycle,
32 pub redactions: Vec<JsonPath>,
33 pub defaults: Vec<String>,
34 pub hash_verified: bool,
35 pub world_ok: bool,
36}
37
38static ABI_CACHE: Lazy<DashMap<(PathBuf, String), FileStamp>> = Lazy::new(DashMap::new);
39static DESCRIBE_CACHE: Lazy<DashMap<PathBuf, DescribeCacheEntry>> = Lazy::new(DashMap::new);
40
41#[cfg(test)]
42static ABI_MISSES: Lazy<AtomicUsize> = Lazy::new(|| AtomicUsize::new(0));
43#[cfg(test)]
44static DESCRIBE_MISSES: Lazy<AtomicUsize> = Lazy::new(|| AtomicUsize::new(0));
45
46pub fn prepare_component(path_or_id: &str) -> Result<PreparedComponent, ComponentError> {
47 let handle = loader::discover(path_or_id)?;
48 let manifest = handle.manifest.clone();
49 let manifest_path = handle.manifest_path.clone();
50 let root = handle.root.clone();
51 let wasm_path = handle.wasm_path.clone();
52
53 let computed_hash = compute_wasm_hash(&wasm_path)?;
54 if computed_hash != manifest.hashes.component_wasm.as_str() {
55 return Err(SigningError::HashMismatch {
56 expected: manifest.hashes.component_wasm.as_str().to_string(),
57 found: computed_hash,
58 }
59 .into());
60 }
61
62 cached_world_check(&wasm_path, manifest.world.as_str())?;
63 let lifecycle = abi::has_lifecycle(&wasm_path)?;
64 let describe_payload = cached_describe(&wasm_path, &manifest)?;
65 let mut redactions = Vec::new();
66 let mut defaults = Vec::new();
67 for version in &describe_payload.versions {
68 let schema_str = serde_json::to_string(&version.schema)
69 .expect("describe schema serialization never fails");
70 let mut hits = schema::try_collect_redactions(&schema_str)?;
71 redactions.append(&mut hits);
72 let defaults_hits = schema::collect_default_annotations(&schema_str)?;
73 defaults.extend(
74 defaults_hits
75 .into_iter()
76 .map(|(path, applied)| format!("{}={}", path.as_str(), applied)),
77 );
78 }
79
80 Ok(PreparedComponent {
81 manifest,
82 manifest_path,
83 wasm_path,
84 root,
85 wasm_hash: computed_hash,
86 describe: describe_payload,
87 lifecycle,
88 redactions,
89 defaults,
90 hash_verified: true,
91 world_ok: true,
92 })
93}
94
95fn cached_world_check(path: &Path, expected: &str) -> Result<(), ComponentError> {
96 let stamp = file_stamp(path)?;
97 let key = (path.to_path_buf(), expected.to_string());
98 if let Some(entry) = ABI_CACHE.get(&key)
99 && *entry == stamp
100 {
101 return Ok(());
102 }
103
104 abi::check_world(path, expected)?;
105 #[cfg(test)]
106 {
107 ABI_MISSES.fetch_add(1, Ordering::SeqCst);
108 }
109 ABI_CACHE.insert(key, stamp);
110 Ok(())
111}
112
113fn cached_describe(
114 path: &Path,
115 manifest: &ComponentManifest,
116) -> Result<DescribePayload, ComponentError> {
117 let stamp = file_stamp(path)?;
118 if let Some(entry) = DESCRIBE_CACHE.get(path)
119 && entry.stamp == stamp
120 && entry.export == manifest.describe_export.as_str()
121 {
122 return Ok(entry.payload.clone());
123 }
124
125 let payload = describe::load(path, manifest)?;
126 #[cfg(test)]
127 {
128 DESCRIBE_MISSES.fetch_add(1, Ordering::SeqCst);
129 }
130 DESCRIBE_CACHE.insert(
131 path.to_path_buf(),
132 DescribeCacheEntry {
133 stamp,
134 export: manifest.describe_export.as_str().to_string(),
135 payload: payload.clone(),
136 },
137 );
138 Ok(payload)
139}
140
141fn file_stamp(path: &Path) -> Result<FileStamp, ComponentError> {
142 let meta = fs::metadata(path)?;
143 let len = meta.len();
144 let modified = meta
145 .modified()
146 .ok()
147 .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
148 .map(|dur| dur.as_nanos())
149 .unwrap_or(0);
150 Ok(FileStamp { len, modified })
151}
152
153#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
154struct FileStamp {
155 len: u64,
156 modified: u128,
157}
158
159#[derive(Clone)]
160struct DescribeCacheEntry {
161 stamp: FileStamp,
162 export: String,
163 payload: DescribePayload,
164}
165
166pub fn clear_cache_for(path: &Path) {
167 let path_buf = path.to_path_buf();
168 ABI_CACHE.retain(|(p, _), _| p != &path_buf);
169 DESCRIBE_CACHE.remove(path);
170}
171
172#[derive(Debug, Clone)]
173pub struct RunnerConfig {
174 pub wasm_path: PathBuf,
175 pub world: String,
176 pub capabilities: Capabilities,
177 pub limits: Option<Limits>,
178 pub telemetry: Option<TelemetrySpec>,
179 pub redactions: Vec<JsonPath>,
180 pub defaults: Vec<String>,
181 pub describe: DescribePayload,
182}
183
184#[derive(Debug, Clone)]
185pub struct PackEntry {
186 pub manifest_json: String,
187 pub describe_schema: Option<String>,
188 pub wasm_hash: String,
189 pub world: String,
190}
191
192impl PreparedComponent {
193 pub fn redaction_paths(&self) -> &[JsonPath] {
194 &self.redactions
195 }
196
197 pub fn defaults_applied(&self) -> &[String] {
198 &self.defaults
199 }
200
201 pub fn to_runner_config(&self) -> RunnerConfig {
202 RunnerConfig {
203 wasm_path: self.wasm_path.clone(),
204 world: self.manifest.world.as_str().to_string(),
205 capabilities: self.manifest.capabilities.clone(),
206 limits: self.manifest.limits.clone(),
207 telemetry: self.manifest.telemetry.clone(),
208 redactions: self.redactions.clone(),
209 defaults: self.defaults.clone(),
210 describe: self.describe.clone(),
211 }
212 }
213
214 pub fn to_pack_entry(&self) -> Result<PackEntry, ComponentError> {
215 let manifest_json = fs::read_to_string(&self.manifest_path)?;
216 let describe_schema = self.describe.versions.first().map(|version| {
217 serde_json::to_string(&version.schema).expect("describe schema serialization")
218 });
219 Ok(PackEntry {
220 manifest_json,
221 describe_schema,
222 wasm_hash: self.wasm_hash.clone(),
223 world: self.manifest.world.as_str().to_string(),
224 })
225 }
226}
227
228#[cfg(test)]
229pub(crate) fn cache_stats() -> (usize, usize) {
230 (
231 ABI_MISSES.load(Ordering::SeqCst),
232 DESCRIBE_MISSES.load(Ordering::SeqCst),
233 )
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use blake3::Hasher;
240 use tempfile::TempDir;
241 use wasm_encoder::{
242 CodeSection, CustomSection, ExportKind, ExportSection, Function, FunctionSection,
243 Instruction, Module, TypeSection,
244 };
245 use wit_component::{StringEncoding, metadata};
246 use wit_parser::{Resolve, WorldId};
247
248 const TEST_WIT: &str = r#"
249package greentic:component@0.1.0;
250world node {
251 export describe: func();
252}
253"#;
254
255 #[test]
256 fn caches_results() {
257 ABI_MISSES.store(0, Ordering::SeqCst);
258 DESCRIBE_MISSES.store(0, Ordering::SeqCst);
259 let fixture = TestFixture::new(TEST_WIT, &["describe"]);
260 prepare_component(fixture.manifest_path.to_str().unwrap()).unwrap();
261 let first = cache_stats();
262 prepare_component(fixture.manifest_path.to_str().unwrap()).unwrap();
263 assert_eq!(first, cache_stats());
264 }
265
266 struct TestFixture {
267 _temp: TempDir,
268 manifest_path: PathBuf,
269 }
270
271 impl TestFixture {
272 fn new(world_src: &str, funcs: &[&str]) -> Self {
273 let temp = TempDir::new().expect("tempdir");
274 let (wasm, manifest) = build_component(world_src, funcs);
275 fs::write(temp.path().join("component.wasm"), &wasm).unwrap();
276 let manifest_path = temp.path().join("component.manifest.json");
277 fs::write(&manifest_path, manifest).unwrap();
278 Self {
279 _temp: temp,
280 manifest_path,
281 }
282 }
283 }
284
285 fn build_component(world_src: &str, funcs: &[&str]) -> (Vec<u8>, String) {
286 let mut resolve = Resolve::default();
287 let pkg = resolve.push_str("test.wit", world_src).unwrap();
288 let world = resolve.select_world(&[pkg], Some("node")).unwrap();
289 let metadata = metadata::encode(&resolve, world, StringEncoding::UTF8, None).unwrap();
290
291 let mut module = Module::new();
292 let mut types = TypeSection::new();
293 types.ty().function([], []);
294 module.section(&types);
295
296 let mut funcs_section = FunctionSection::new();
297 for _ in funcs {
298 funcs_section.function(0);
299 }
300 module.section(&funcs_section);
301
302 let mut exports = ExportSection::new();
303 for (idx, name) in funcs.iter().enumerate() {
304 exports.export(name, ExportKind::Func, idx as u32);
305 }
306 module.section(&exports);
307
308 let mut code = CodeSection::new();
309 for _ in funcs {
310 let mut body = Function::new([]);
311 body.instruction(&Instruction::End);
312 code.function(&body);
313 }
314 module.section(&code);
315
316 module.section(&CustomSection {
317 name: "component-type".into(),
318 data: std::borrow::Cow::Borrowed(&metadata),
319 });
320 module.section(&CustomSection {
321 name: "producers".into(),
322 data: std::borrow::Cow::Borrowed(b"wasm32-wasip2"),
323 });
324
325 let wasm_bytes = module.finish();
326 let observed_world = detect_world(&wasm_bytes).unwrap_or_else(|| "root:root/root".into());
327 let mut hasher = Hasher::new();
328 hasher.update(&wasm_bytes);
329 let digest = hasher.finalize();
330 let hash = format!("blake3:{}", hex::encode(digest.as_bytes()));
331
332 let manifest = serde_json::json!({
333 "id": "com.greentic.test.component",
334 "name": "Test",
335 "version": "0.1.0",
336 "world": observed_world,
337 "describe_export": "describe",
338 "supports": ["messaging"],
339 "profiles": {
340 "default": "stateless",
341 "supported": ["stateless"]
342 },
343 "capabilities": {
344 "wasi": {
345 "filesystem": {
346 "mode": "none",
347 "mounts": []
348 },
349 "random": true,
350 "clocks": true
351 },
352 "host": {
353 "messaging": {
354 "inbound": true,
355 "outbound": true
356 }
357 }
358 },
359 "limits": {"memory_mb": 64, "wall_time_ms": 1000},
360 "telemetry": {"span_prefix": "test.component"},
361 "artifacts": {"component_wasm": "component.wasm"},
362 "hashes": {"component_wasm": hash},
363 });
364
365 (wasm_bytes, serde_json::to_string_pretty(&manifest).unwrap())
366 }
367
368 fn detect_world(bytes: &[u8]) -> Option<String> {
369 let decoded = crate::wasm::decode_world(bytes).ok()?;
370 Some(world_label(&decoded.resolve, decoded.world))
371 }
372
373 fn world_label(resolve: &Resolve, world_id: WorldId) -> String {
374 let world = &resolve.worlds[world_id];
375 if let Some(pkg_id) = world.package {
376 let pkg = &resolve.packages[pkg_id];
377 if let Some(version) = &pkg.name.version {
378 format!(
379 "{}:{}/{}@{}",
380 pkg.name.namespace, pkg.name.name, world.name, version
381 )
382 } else {
383 format!("{}:{}/{}", pkg.name.namespace, pkg.name.name, world.name)
384 }
385 } else {
386 world.name.clone()
387 }
388 }
389}