1#![forbid(unsafe_code)]
2
3use std::collections::BTreeSet;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::time::Duration;
7
8use anyhow::{Context, Result, anyhow};
9use clap::ValueEnum;
10use greentic_distributor_client::{DistClient, DistOptions};
11use greentic_pack::{PackLoad, SigningPolicy, open_pack};
12use greentic_types::pack_manifest::{ExtensionInline, PackManifest};
13use greentic_types::provider::PROVIDER_EXTENSION_ID;
14use greentic_types::validate::{Diagnostic, Severity};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use wasmtime::component::{Component, Linker};
18use wasmtime::{Config, Engine, Store};
19use wasmtime_wasi::p2::add_to_linker_sync;
20use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
21
22use crate::runtime::{NetworkPolicy, RuntimeContext};
23
24const PACK_VALIDATOR_WORLDS: [&str; 2] = [
25 "greentic:pack-validate@0.1.0/pack-validator",
26 "greentic:pack-validate/pack-validator@0.1.0",
27];
28pub const DEFAULT_VALIDATOR_ALLOW: &str = "oci://ghcr.io/greentic-ai/validators/";
29const DEFAULT_TIMEOUT_SECS: u64 = 2;
30const DEFAULT_MAX_MEMORY_BYTES: usize = 64 * 1024 * 1024;
31
32mod bindings {
33 wasmtime::component::bindgen!({
34 inline: r#"
35 package greentic:pack-validate@0.1.0;
36
37 interface validator {
38 record diagnostic {
39 severity: string,
40 code: string,
41 message: string,
42 path: option<string>,
43 hint: option<string>,
44 }
45
46 record pack-inputs {
47 manifest-cbor: list<u8>,
48 sbom-json: string,
49 file-index: list<string>,
50 }
51
52 applies: func(inputs: pack-inputs) -> bool;
53 validate: func(inputs: pack-inputs) -> list<diagnostic>;
54 }
55
56 world pack-validator {
57 export validator;
58 }
59 "#,
60 });
61}
62
63use bindings::PackValidator;
64use bindings::exports::greentic::pack_validate::validator::{
65 Diagnostic as WasmDiagnostic, PackInputs,
66};
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
69pub enum ValidatorPolicy {
70 Required,
71 Optional,
72}
73
74impl ValidatorPolicy {
75 pub fn is_required(self) -> bool {
76 matches!(self, ValidatorPolicy::Required)
77 }
78}
79
80#[derive(Clone, Debug)]
81pub struct ValidatorConfig {
82 pub validators_root: PathBuf,
83 pub validator_packs: Vec<String>,
84 pub validator_allow: Vec<String>,
85 pub validator_cache_dir: PathBuf,
86 pub policy: ValidatorPolicy,
87 pub local_validators: Vec<LocalValidator>,
88}
89
90#[derive(Clone, Debug)]
91pub struct LocalValidator {
92 pub component_id: String,
93 pub path: PathBuf,
94}
95
96#[derive(Clone, Debug, Serialize)]
97pub struct ValidatorSourceReport {
98 pub reference: String,
99 pub origin: String,
100 pub status: String,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub message: Option<String>,
103}
104
105#[derive(Clone, Debug)]
106struct ValidatorRef {
107 reference: String,
108 digest: Option<String>,
109 origin: String,
110}
111
112#[derive(Clone, Debug)]
113struct ValidatorComponent {
114 component_id: String,
115 wasm: Vec<u8>,
116}
117
118#[derive(Clone, Debug, Default)]
119pub struct ValidatorRunResult {
120 pub diagnostics: Vec<Diagnostic>,
121 pub sources: Vec<ValidatorSourceReport>,
122 pub missing_required: bool,
123}
124
125pub async fn run_wasm_validators(
126 load: &PackLoad,
127 config: &ValidatorConfig,
128 runtime: &RuntimeContext,
129) -> Result<ValidatorRunResult> {
130 let inputs = build_pack_inputs(load)?;
131
132 let mut result = ValidatorRunResult::default();
133 let mut components = Vec::new();
134
135 for local in &config.local_validators {
136 let reference = format!("local:{}", local.path.display());
137 match fs::read(&local.path) {
138 Ok(bytes) => {
139 components.push(ValidatorComponent {
140 component_id: local.component_id.clone(),
141 wasm: bytes,
142 });
143 result.sources.push(ValidatorSourceReport {
144 reference: reference.clone(),
145 origin: "local".to_string(),
146 status: "loaded".to_string(),
147 message: None,
148 });
149 }
150 Err(err) => {
151 let is_required = config.policy.is_required();
152 if is_required {
153 result.missing_required = true;
154 result.diagnostics.push(Diagnostic {
155 severity: Severity::Error,
156 code: "PACK_VALIDATOR_REQUIRED".to_string(),
157 message: format!(
158 "Validator {} is required but could not be loaded.",
159 reference
160 ),
161 path: None,
162 hint: Some(err.to_string()),
163 data: Value::Null,
164 });
165 } else {
166 result.diagnostics.push(Diagnostic {
167 severity: Severity::Warn,
168 code: "PACK_VALIDATOR_UNAVAILABLE".to_string(),
169 message: format!("Validator {} could not be loaded; skipping.", reference),
170 path: None,
171 hint: Some(err.to_string()),
172 data: Value::Null,
173 });
174 }
175 result.sources.push(ValidatorSourceReport {
176 reference,
177 origin: "local".to_string(),
178 status: "failed".to_string(),
179 message: Some(err.to_string()),
180 });
181 }
182 }
183 }
184
185 let refs = collect_validator_refs(load, config);
186 if refs.is_empty() && components.is_empty() {
187 return Ok(result);
188 }
189
190 for validator_ref in refs {
191 match load_validator_components(&validator_ref, config, runtime).await {
192 Ok(mut loaded) => {
193 components.append(&mut loaded);
194 result.sources.push(ValidatorSourceReport {
195 reference: validator_ref.reference.clone(),
196 origin: validator_ref.origin.clone(),
197 status: "loaded".to_string(),
198 message: None,
199 });
200 }
201 Err(err) => {
202 let is_required = config.policy.is_required();
203 if is_required {
204 result.missing_required = true;
205 result.diagnostics.push(Diagnostic {
206 severity: Severity::Error,
207 code: "PACK_VALIDATOR_REQUIRED".to_string(),
208 message: format!(
209 "Validator {} is required but could not be loaded.",
210 validator_ref.reference
211 ),
212 path: None,
213 hint: Some(err.to_string()),
214 data: Value::Null,
215 });
216 }
217 result.sources.push(ValidatorSourceReport {
218 reference: validator_ref.reference.clone(),
219 origin: validator_ref.origin.clone(),
220 status: "failed".to_string(),
221 message: Some(err.to_string()),
222 });
223 if !is_required {
224 result.diagnostics.push(Diagnostic {
225 severity: Severity::Warn,
226 code: "PACK_VALIDATOR_UNAVAILABLE".to_string(),
227 message: format!(
228 "Validator {} could not be loaded; skipping.",
229 validator_ref.reference
230 ),
231 path: None,
232 hint: Some(err.to_string()),
233 data: Value::Null,
234 });
235 }
236 }
237 }
238 }
239
240 if components.is_empty() {
241 return Ok(result);
242 }
243
244 let engine = build_engine()?;
245 let mut linker = Linker::new(&engine);
246 add_to_linker_sync(&mut linker)?;
247
248 for component in components {
249 let validator_result = run_component_validator(&engine, &mut linker, &component, &inputs);
250 match validator_result {
251 Ok(mut diags) => result.diagnostics.append(&mut diags),
252 Err(err) => {
253 result.diagnostics.push(Diagnostic {
254 severity: Severity::Warn,
255 code: "PACK_VALIDATOR_FAILED".to_string(),
256 message: format!(
257 "Validator component {} failed to execute.",
258 component.component_id
259 ),
260 path: None,
261 hint: Some(err.to_string()),
262 data: Value::Null,
263 });
264 }
265 }
266 }
267
268 Ok(result)
269}
270
271fn build_engine() -> Result<Engine> {
272 let mut config = Config::new();
273 config.wasm_component_model(true);
274 config.epoch_interruption(true);
275 Engine::new(&config)
276}
277
278fn run_component_validator(
279 engine: &Engine,
280 linker: &mut Linker<ValidatorCtx>,
281 component: &ValidatorComponent,
282 inputs: &PackInputs,
283) -> Result<Vec<Diagnostic>> {
284 let component = Component::from_binary(engine, &component.wasm)
285 .context("failed to load validator component")?;
286
287 let mut store = Store::new(engine, ValidatorCtx::new());
288 store.limiter(|ctx| &mut ctx.limits);
289 store.set_epoch_deadline(1);
290
291 let validator = PackValidator::instantiate(&mut store, &component, linker)
292 .context("failed to instantiate validator component")?;
293 let guest = validator.greentic_pack_validate_validator();
294
295 let engine = engine.clone();
296 let timeout = Duration::from_secs(DEFAULT_TIMEOUT_SECS);
297 std::thread::spawn(move || {
298 std::thread::sleep(timeout);
299 engine.increment_epoch();
300 });
301
302 let applies = guest
303 .call_applies(&mut store, inputs)
304 .context("validator applies call failed")?;
305 if !applies {
306 return Ok(Vec::new());
307 }
308
309 let diags = guest
310 .call_validate(&mut store, inputs)
311 .context("validator validate call failed")?;
312 Ok(convert_diagnostics(diags))
313}
314
315fn convert_diagnostics(diags: Vec<WasmDiagnostic>) -> Vec<Diagnostic> {
316 diags
317 .into_iter()
318 .map(|diag| Diagnostic {
319 severity: match diag.severity.as_str() {
320 "info" => Severity::Info,
321 "warn" => Severity::Warn,
322 "error" => Severity::Error,
323 _ => Severity::Warn,
324 },
325 code: diag.code,
326 message: diag.message,
327 path: diag.path,
328 hint: diag.hint,
329 data: Value::Null,
330 })
331 .collect()
332}
333
334fn build_pack_inputs(load: &PackLoad) -> Result<PackInputs> {
335 let manifest_bytes = load.files.get("manifest.cbor").cloned().unwrap_or_default();
336
337 let sbom_json = if let Some(bytes) = load.files.get("sbom.json") {
338 String::from_utf8_lossy(bytes).to_string()
339 } else if let Some(bytes) = load.files.get("sbom.cbor") {
340 let value: Value = serde_cbor::from_slice(bytes).context("sbom.cbor is not valid CBOR")?;
341 serde_json::to_string(&value).context("failed to serialize sbom json")?
342 } else {
343 serde_json::to_string(&serde_json::json!({"files": load.sbom}))
344 .context("failed to serialize sbom json")?
345 };
346
347 let file_index = load.files.keys().cloned().collect();
348
349 Ok(PackInputs {
350 manifest_cbor: manifest_bytes,
351 sbom_json,
352 file_index,
353 })
354}
355
356fn collect_validator_refs(load: &PackLoad, config: &ValidatorConfig) -> Vec<ValidatorRef> {
357 let mut refs = Vec::new();
358
359 for reference in &config.validator_packs {
360 refs.push(ValidatorRef {
361 reference: reference.clone(),
362 digest: None,
363 origin: "cli".to_string(),
364 });
365 }
366
367 if config.validators_root.exists()
368 && let Ok(entries) = std::fs::read_dir(&config.validators_root)
369 {
370 for entry in entries.flatten() {
371 let path = entry.path();
372 if path.extension().and_then(|ext| ext.to_str()) == Some("gtpack") {
373 refs.push(ValidatorRef {
374 reference: path.to_string_lossy().to_string(),
375 digest: None,
376 origin: "validators-root".to_string(),
377 });
378 }
379 }
380 }
381
382 if let Some(manifest) = load.gpack_manifest.as_ref() {
383 refs.extend(validator_refs_from_manifest(manifest));
384 }
385 refs.extend(validator_refs_from_annotations(load));
386
387 let mut seen = BTreeSet::new();
388 refs.retain(|r| seen.insert((r.reference.clone(), r.digest.clone())));
389 refs
390}
391
392fn validator_refs_from_manifest(manifest: &PackManifest) -> Vec<ValidatorRef> {
393 let mut refs = Vec::new();
394 let Some(extensions) = manifest.extensions.as_ref() else {
395 return refs;
396 };
397 let Some(extension) = extensions.get(PROVIDER_EXTENSION_ID) else {
398 return refs;
399 };
400 let Some(inline) = extension.inline.as_ref() else {
401 return refs;
402 };
403
404 let value = match inline {
405 ExtensionInline::Other(value) => value.clone(),
406 _ => serde_json::to_value(inline).unwrap_or(Value::Null),
407 };
408
409 if let Some(reference) = value.get("validator_ref").and_then(Value::as_str) {
410 let digest = value
411 .get("validator_digest")
412 .and_then(Value::as_str)
413 .map(|s| s.to_string());
414 refs.push(ValidatorRef {
415 reference: reference.to_string(),
416 digest,
417 origin: "provider-extension".to_string(),
418 });
419 }
420
421 if let Some(values) = value.get("validator_refs").and_then(Value::as_array) {
422 for entry in values {
423 if let Some(reference) = entry.as_str() {
424 refs.push(ValidatorRef {
425 reference: reference.to_string(),
426 digest: None,
427 origin: "provider-extension".to_string(),
428 });
429 }
430 }
431 }
432
433 if let Some(providers) = value.get("providers").and_then(Value::as_array) {
434 for provider in providers {
435 if let Some(reference) = provider.get("validator_ref").and_then(Value::as_str) {
436 let digest = provider
437 .get("validator_digest")
438 .and_then(Value::as_str)
439 .map(|s| s.to_string());
440 refs.push(ValidatorRef {
441 reference: reference.to_string(),
442 digest,
443 origin: "provider-extension".to_string(),
444 });
445 }
446 }
447 }
448
449 refs
450}
451
452fn validator_refs_from_annotations(load: &PackLoad) -> Vec<ValidatorRef> {
453 let mut refs = Vec::new();
454 if let Some(value) = load.manifest.meta.annotations.get("greentic.validators") {
455 match value {
456 Value::String(reference) => refs.push(ValidatorRef {
457 reference: reference.clone(),
458 digest: None,
459 origin: "annotations".to_string(),
460 }),
461 Value::Array(items) => {
462 for item in items {
463 if let Some(reference) = item.as_str() {
464 refs.push(ValidatorRef {
465 reference: reference.to_string(),
466 digest: None,
467 origin: "annotations".to_string(),
468 });
469 } else if let Some(reference) = item.get("ref").and_then(Value::as_str) {
470 let digest = item
471 .get("digest")
472 .and_then(Value::as_str)
473 .map(|s| s.to_string());
474 refs.push(ValidatorRef {
475 reference: reference.to_string(),
476 digest,
477 origin: "annotations".to_string(),
478 });
479 }
480 }
481 }
482 _ => {}
483 }
484 }
485 refs
486}
487
488async fn load_validator_components(
489 validator_ref: &ValidatorRef,
490 config: &ValidatorConfig,
491 runtime: &RuntimeContext,
492) -> Result<Vec<ValidatorComponent>> {
493 let reference = validator_ref.reference.as_str();
494 if reference.starts_with("oci://") {
495 return load_validator_components_from_oci(validator_ref, config, runtime).await;
496 }
497
498 let path = Path::new(reference);
499 if path.exists() {
500 if path.is_dir() {
501 return load_validator_components_from_dir(path);
502 }
503 if path.extension().and_then(|ext| ext.to_str()) == Some("gtpack") {
504 return load_validator_components_from_pack(path);
505 }
506 if path.extension().and_then(|ext| ext.to_str()) == Some("wasm") {
507 let wasm = std::fs::read(path).with_context(|| {
508 format!("failed to read validator component {}", path.display())
509 })?;
510 return Ok(vec![ValidatorComponent {
511 component_id: path
512 .file_stem()
513 .and_then(|name| name.to_str())
514 .unwrap_or("validator")
515 .to_string(),
516 wasm,
517 }]);
518 }
519 }
520
521 Err(anyhow!(
522 "validator reference {} could not be resolved",
523 reference
524 ))
525}
526
527fn load_validator_components_from_dir(path: &Path) -> Result<Vec<ValidatorComponent>> {
528 let mut components = Vec::new();
529 for entry in std::fs::read_dir(path)
530 .with_context(|| format!("failed to read validators root {}", path.display()))?
531 {
532 let entry = entry?;
533 let path = entry.path();
534 if path.extension().and_then(|ext| ext.to_str()) == Some("gtpack") {
535 components.extend(load_validator_components_from_pack(&path)?);
536 }
537 }
538 Ok(components)
539}
540
541fn load_validator_components_from_pack(path: &Path) -> Result<Vec<ValidatorComponent>> {
542 let load = open_pack(path, SigningPolicy::DevOk)
543 .map_err(|err| anyhow!(err.message))
544 .with_context(|| format!("failed to open validator pack {}", path.display()))?;
545 let mut components = Vec::new();
546
547 if let Some(manifest) = load.gpack_manifest.as_ref() {
548 for component in &manifest.components {
549 if !PACK_VALIDATOR_WORLDS
550 .iter()
551 .any(|world| world == &component.world)
552 {
553 continue;
554 }
555 let wasm_paths = [
556 format!(
557 "components/{}@{}/component.wasm",
558 component.id.as_str(),
559 component.version
560 ),
561 format!("components/{}.wasm", component.id.as_str()),
562 ];
563 let wasm = wasm_paths
564 .iter()
565 .find_map(|path| load.files.get(path).cloned())
566 .ok_or_else(|| {
567 anyhow!(
568 "validator pack missing {} for component {}",
569 wasm_paths.join(" or "),
570 component.id.as_str()
571 )
572 })?;
573 components.push(ValidatorComponent {
574 component_id: component.id.as_str().to_string(),
575 wasm,
576 });
577 }
578 } else {
579 for component in &load.manifest.components {
580 let Some(world) = component.world.as_deref() else {
581 continue;
582 };
583 if !PACK_VALIDATOR_WORLDS.iter().any(|item| item == &world) {
584 continue;
585 }
586 let Some(wasm) = load.files.get(&component.file_wasm).cloned() else {
587 return Err(anyhow!(
588 "validator pack missing {} for component {}",
589 component.file_wasm,
590 component.name
591 ));
592 };
593 components.push(ValidatorComponent {
594 component_id: component.name.clone(),
595 wasm,
596 });
597 }
598 }
599
600 if components.is_empty() {
601 return Err(anyhow!(
602 "validator pack {} contains no pack-validator components",
603 path.display()
604 ));
605 }
606
607 Ok(components)
608}
609
610async fn load_validator_components_from_oci(
611 validator_ref: &ValidatorRef,
612 config: &ValidatorConfig,
613 runtime: &RuntimeContext,
614) -> Result<Vec<ValidatorComponent>> {
615 let allowed = if config.validator_allow.is_empty() {
616 vec![DEFAULT_VALIDATOR_ALLOW.to_string()]
617 } else {
618 config.validator_allow.clone()
619 };
620 if !allowed
621 .iter()
622 .any(|prefix| validator_ref.reference.starts_with(prefix))
623 {
624 return Err(anyhow!(
625 "validator ref {} is not in allowlist",
626 validator_ref.reference
627 ));
628 }
629
630 let dist = DistClient::new(DistOptions {
631 cache_dir: config.validator_cache_dir.clone(),
632 allow_tags: true,
633 offline: runtime.network_policy() == NetworkPolicy::Offline,
634 allow_insecure_local_http: false,
635 });
636
637 let resolved = if runtime.network_policy() == NetworkPolicy::Offline {
638 dist.ensure_cached(&validator_ref.reference)
639 .await
640 .context("validator ref not cached")?
641 } else {
642 dist.resolve_ref(&validator_ref.reference)
643 .await
644 .context("failed to fetch validator ref")?
645 };
646
647 if let Some(expected) = validator_ref.digest.as_ref()
648 && resolved.digest != *expected
649 {
650 return Err(anyhow!(
651 "validator digest mismatch (expected {}, got {})",
652 expected,
653 resolved.digest
654 ));
655 }
656
657 let cache_path = resolved
658 .cache_path
659 .as_ref()
660 .ok_or_else(|| anyhow!("validator ref resolved without cache path"))?;
661 let bytes = std::fs::read(cache_path)
662 .with_context(|| format!("failed to read validator cache {}", cache_path.display()))?;
663
664 if is_zip_archive(&bytes) {
665 let temp = tempfile::NamedTempFile::new()?;
666 std::fs::write(temp.path(), &bytes)?;
667 return load_validator_components_from_pack(temp.path());
668 }
669
670 Ok(vec![ValidatorComponent {
671 component_id: "validator".to_string(),
672 wasm: bytes,
673 }])
674}
675
676fn is_zip_archive(bytes: &[u8]) -> bool {
677 bytes.len() >= 4 && bytes[0] == 0x50 && bytes[1] == 0x4b && bytes[2] == 0x03 && bytes[3] == 0x04
678}
679
680struct ValidatorCtx {
681 table: ResourceTable,
682 wasi: WasiCtx,
683 limits: wasmtime::StoreLimits,
684}
685
686impl ValidatorCtx {
687 fn new() -> Self {
688 let limits = wasmtime::StoreLimitsBuilder::new()
689 .memory_size(DEFAULT_MAX_MEMORY_BYTES)
690 .build();
691 let wasi = WasiCtxBuilder::new()
692 .inherit_stdout()
693 .inherit_stderr()
694 .build();
695 Self {
696 table: ResourceTable::new(),
697 wasi,
698 limits,
699 }
700 }
701}
702
703impl WasiView for ValidatorCtx {
704 fn ctx(&mut self) -> WasiCtxView<'_> {
705 WasiCtxView {
706 table: &mut self.table,
707 ctx: &mut self.wasi,
708 }
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use greentic_pack::builder::SbomEntry;
716 use greentic_types::{
717 ComponentCapabilities, ComponentId, ComponentManifest, ComponentProfiles, PackId, PackKind,
718 PackManifest, PackSignatures, ResourceHints, encode_pack_manifest,
719 };
720 use semver::Version;
721 use serde::Serialize;
722 use std::collections::BTreeMap;
723 use std::fs::File;
724 use std::io::Write;
725 use tempfile::tempdir;
726 use zip::write::FileOptions;
727 use zip::{CompressionMethod, ZipWriter};
728
729 #[derive(Serialize)]
730 struct SbomDocument {
731 format: String,
732 files: Vec<SbomEntry>,
733 }
734
735 #[test]
736 fn validator_pack_accepts_id_wasm_from_pack_manifest() {
737 let temp = tempdir().expect("temp dir");
738 let pack_path = temp.path().join("validator.gtpack");
739 let component_id = ComponentId::new("messaging-validator").expect("component id");
740 let component_version = Version::parse("0.1.0").expect("component version");
741 let component = ComponentManifest {
742 id: component_id.clone(),
743 version: component_version.clone(),
744 supports: Vec::new(),
745 world: PACK_VALIDATOR_WORLDS[0].to_string(),
746 profiles: ComponentProfiles::default(),
747 capabilities: ComponentCapabilities::default(),
748 configurators: None,
749 operations: Vec::new(),
750 config_schema: None,
751 resources: ResourceHints::default(),
752 dev_flows: BTreeMap::new(),
753 };
754 let manifest = PackManifest {
755 schema_version: "pack-v1".to_string(),
756 pack_id: PackId::new("dev.local.validator").expect("pack id"),
757 name: None,
758 version: component_version,
759 kind: PackKind::Provider,
760 publisher: "test".to_string(),
761 components: vec![component],
762 flows: Vec::new(),
763 dependencies: Vec::new(),
764 capabilities: Vec::new(),
765 secret_requirements: Vec::new(),
766 signatures: PackSignatures::default(),
767 bootstrap: None,
768 extensions: None,
769 };
770 let manifest_cbor = encode_pack_manifest(&manifest).expect("encode manifest");
771 let wasm_bytes = b"validator wasm";
772 let wasm_path = format!("components/{}.wasm", component_id.as_str());
773 let sbom_entries = vec![
774 SbomEntry {
775 path: "manifest.cbor".to_string(),
776 size: manifest_cbor.len() as u64,
777 hash_blake3: blake3::hash(&manifest_cbor).to_hex().to_string(),
778 media_type: "application/cbor".to_string(),
779 },
780 SbomEntry {
781 path: wasm_path.clone(),
782 size: wasm_bytes.len() as u64,
783 hash_blake3: blake3::hash(wasm_bytes).to_hex().to_string(),
784 media_type: "application/wasm".to_string(),
785 },
786 ];
787 let sbom = SbomDocument {
788 format: "greentic-sbom-v1".to_string(),
789 files: sbom_entries,
790 };
791 let sbom_cbor = serde_cbor::to_vec(&sbom).expect("encode sbom");
792
793 let file = File::create(&pack_path).expect("create pack");
794 let mut writer = ZipWriter::new(file);
795 let options = FileOptions::<()>::default().compression_method(CompressionMethod::Stored);
796 writer
797 .start_file("manifest.cbor", options)
798 .expect("start manifest");
799 writer.write_all(&manifest_cbor).expect("write manifest");
800 writer.start_file(&wasm_path, options).expect("start wasm");
801 writer.write_all(wasm_bytes).expect("write wasm");
802 writer.start_file("sbom.cbor", options).expect("start sbom");
803 writer.write_all(&sbom_cbor).expect("write sbom");
804 writer.finish().expect("finish pack");
805
806 let components =
807 load_validator_components_from_pack(&pack_path).expect("load validator components");
808 assert_eq!(components.len(), 1);
809 assert_eq!(components[0].component_id, "messaging-validator");
810 assert_eq!(components[0].wasm, wasm_bytes);
811 }
812}