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/greenticai/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 Ok(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 .map_err(|err| anyhow!("failed to load validator component: {err}"))?;
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 .map_err(|err| anyhow!("failed to instantiate validator component: {err}"))?;
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 .map_err(|err| anyhow!("validator applies call failed: {err}"))?;
305 if !applies {
306 return Ok(Vec::new());
307 }
308
309 let diags = guest
310 .call_validate(&mut store, inputs)
311 .map_err(|err| anyhow!("validator validate call failed: {err}"))?;
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 ..DistOptions::default()
636 });
637
638 let offline = runtime.network_policy() == NetworkPolicy::Offline;
639 let source = dist
640 .parse_source(&validator_ref.reference)
641 .context("failed to parse validator ref")?;
642 let descriptor = if offline {
643 dist.resolve(source, greentic_distributor_client::ResolvePolicy)
644 .await
645 .context("validator ref not cached")?
646 } else {
647 dist.resolve(source, greentic_distributor_client::ResolvePolicy)
648 .await
649 .context("failed to fetch validator ref")?
650 };
651 let resolved = if offline {
652 dist.fetch(&descriptor, greentic_distributor_client::CachePolicy)
653 .await
654 .context("validator ref not cached")?
655 } else {
656 dist.fetch(&descriptor, greentic_distributor_client::CachePolicy)
657 .await
658 .context("failed to fetch validator ref")?
659 };
660
661 if let Some(expected) = validator_ref.digest.as_ref()
662 && resolved.digest != *expected
663 {
664 return Err(anyhow!(
665 "validator digest mismatch (expected {}, got {})",
666 expected,
667 resolved.digest
668 ));
669 }
670
671 let cache_path = resolved
672 .cache_path
673 .as_ref()
674 .ok_or_else(|| anyhow!("validator ref resolved without cache path"))?;
675 let bytes = std::fs::read(cache_path)
676 .with_context(|| format!("failed to read validator cache {}", cache_path.display()))?;
677
678 if is_zip_archive(&bytes) {
679 let temp = tempfile::NamedTempFile::new()?;
680 std::fs::write(temp.path(), &bytes)?;
681 return load_validator_components_from_pack(temp.path());
682 }
683
684 Ok(vec![ValidatorComponent {
685 component_id: "validator".to_string(),
686 wasm: bytes,
687 }])
688}
689
690fn is_zip_archive(bytes: &[u8]) -> bool {
691 bytes.len() >= 4 && bytes[0] == 0x50 && bytes[1] == 0x4b && bytes[2] == 0x03 && bytes[3] == 0x04
692}
693
694struct ValidatorCtx {
695 table: ResourceTable,
696 wasi: WasiCtx,
697 limits: wasmtime::StoreLimits,
698}
699
700impl ValidatorCtx {
701 fn new() -> Self {
702 let limits = wasmtime::StoreLimitsBuilder::new()
703 .memory_size(DEFAULT_MAX_MEMORY_BYTES)
704 .build();
705 let wasi = WasiCtxBuilder::new()
706 .inherit_stdout()
707 .inherit_stderr()
708 .build();
709 Self {
710 table: ResourceTable::new(),
711 wasi,
712 limits,
713 }
714 }
715}
716
717impl WasiView for ValidatorCtx {
718 fn ctx(&mut self) -> WasiCtxView<'_> {
719 WasiCtxView {
720 table: &mut self.table,
721 ctx: &mut self.wasi,
722 }
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use greentic_pack::builder::SbomEntry;
730 use greentic_types::{
731 ComponentCapabilities, ComponentId, ComponentManifest, ComponentProfiles, PackId, PackKind,
732 PackManifest, PackSignatures, ResourceHints, encode_pack_manifest,
733 };
734 use semver::Version;
735 use serde::Serialize;
736 use std::collections::BTreeMap;
737 use std::fs::File;
738 use std::io::Write;
739 use tempfile::tempdir;
740 use zip::write::FileOptions;
741 use zip::{CompressionMethod, ZipWriter};
742
743 #[derive(Serialize)]
744 struct SbomDocument {
745 format: String,
746 files: Vec<SbomEntry>,
747 }
748
749 #[test]
750 fn validator_pack_accepts_id_wasm_from_pack_manifest() {
751 let temp = tempdir().expect("temp dir");
752 let pack_path = temp.path().join("validator.gtpack");
753 let component_id = ComponentId::new("messaging-validator").expect("component id");
754 let component_version = Version::parse("0.1.0").expect("component version");
755 let component = ComponentManifest {
756 id: component_id.clone(),
757 version: component_version.clone(),
758 supports: Vec::new(),
759 world: PACK_VALIDATOR_WORLDS[0].to_string(),
760 profiles: ComponentProfiles::default(),
761 capabilities: ComponentCapabilities::default(),
762 configurators: None,
763 operations: Vec::new(),
764 config_schema: None,
765 resources: ResourceHints::default(),
766 dev_flows: BTreeMap::new(),
767 };
768 let manifest = PackManifest {
769 schema_version: "pack-v1".to_string(),
770 pack_id: PackId::new("dev.local.validator").expect("pack id"),
771 name: None,
772 version: component_version,
773 kind: PackKind::Provider,
774 publisher: "test".to_string(),
775 components: vec![component],
776 flows: Vec::new(),
777 dependencies: Vec::new(),
778 capabilities: Vec::new(),
779 secret_requirements: Vec::new(),
780 signatures: PackSignatures::default(),
781 bootstrap: None,
782 extensions: None,
783 };
784 let manifest_cbor = encode_pack_manifest(&manifest).expect("encode manifest");
785 let wasm_bytes = b"validator wasm";
786 let wasm_path = format!("components/{}.wasm", component_id.as_str());
787 let sbom_entries = vec![
788 SbomEntry {
789 path: "manifest.cbor".to_string(),
790 size: manifest_cbor.len() as u64,
791 hash_blake3: blake3::hash(&manifest_cbor).to_hex().to_string(),
792 media_type: "application/cbor".to_string(),
793 },
794 SbomEntry {
795 path: wasm_path.clone(),
796 size: wasm_bytes.len() as u64,
797 hash_blake3: blake3::hash(wasm_bytes).to_hex().to_string(),
798 media_type: "application/wasm".to_string(),
799 },
800 ];
801 let sbom = SbomDocument {
802 format: "greentic-sbom-v1".to_string(),
803 files: sbom_entries,
804 };
805 let sbom_cbor = serde_cbor::to_vec(&sbom).expect("encode sbom");
806
807 let file = File::create(&pack_path).expect("create pack");
808 let mut writer = ZipWriter::new(file);
809 let options = FileOptions::<()>::default().compression_method(CompressionMethod::Stored);
810 writer
811 .start_file("manifest.cbor", options)
812 .expect("start manifest");
813 writer.write_all(&manifest_cbor).expect("write manifest");
814 writer.start_file(&wasm_path, options).expect("start wasm");
815 writer.write_all(wasm_bytes).expect("write wasm");
816 writer.start_file("sbom.cbor", options).expect("start sbom");
817 writer.write_all(&sbom_cbor).expect("write sbom");
818 writer.finish().expect("finish pack");
819
820 let components =
821 load_validator_components_from_pack(&pack_path).expect("load validator components");
822 assert_eq!(components.len(), 1);
823 assert_eq!(components[0].component_id, "messaging-validator");
824 assert_eq!(components[0].wasm, wasm_bytes);
825 }
826}