1use serde_json::{Value, json};
9use sha2::{Digest, Sha256};
10use std::fmt::Write as _;
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::time::{SystemTime, UNIX_EPOCH};
14use traverse_contracts::parse_contract;
15use traverse_registry::{
16 PublicRegistryCapabilityRecord, RegistryReference, ResolvedRegistryComponent,
17 SyncedPublicRegistryState,
18};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum RegistryCacheErrorCode {
23 RegistrySyncMissing,
25 RegistryVersionNotFound,
27 RegistryDependencyYanked,
29 RegistryPrepareFailed,
31 RegistryArtifactDigestMismatch,
33 RegistryCacheEntryMissing,
35 RegistryMetadataCacheInvalid,
37}
38
39impl RegistryCacheErrorCode {
40 #[must_use]
42 pub fn as_str(self) -> &'static str {
43 match self {
44 Self::RegistrySyncMissing => "registry_sync_missing",
45 Self::RegistryVersionNotFound => "registry_version_not_found",
46 Self::RegistryDependencyYanked => "registry_dependency_yanked",
47 Self::RegistryPrepareFailed => "registry_prepare_failed",
48 Self::RegistryArtifactDigestMismatch => "registry_artifact_digest_mismatch",
49 Self::RegistryCacheEntryMissing => "registry_cache_entry_missing",
50 Self::RegistryMetadataCacheInvalid => "registry_metadata_cache_invalid",
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct RegistryCacheError {
58 pub code: RegistryCacheErrorCode,
60 pub message: String,
62}
63
64impl RegistryCacheError {
65 fn new(code: RegistryCacheErrorCode, message: impl Into<String>) -> Self {
66 Self {
67 code,
68 message: message.into(),
69 }
70 }
71}
72
73pub trait RegistryArtifactFetcher {
75 fn fetch(&self, url: &str) -> Result<Vec<u8>, String>;
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct HostRegistryCache {
86 root: PathBuf,
87}
88
89impl HostRegistryCache {
90 #[must_use]
95 pub fn new(root: impl Into<PathBuf>) -> Self {
96 Self { root: root.into() }
97 }
98
99 #[must_use]
101 pub fn root(&self) -> &Path {
102 &self.root
103 }
104
105 pub fn evict(&self, artifact_digest: &str) -> Result<(), RegistryCacheError> {
112 let digest = normalize_digest(artifact_digest).ok_or_else(|| {
113 RegistryCacheError::new(
114 RegistryCacheErrorCode::RegistryPrepareFailed,
115 "artifact digest must be sha256: followed by 64 hex characters",
116 )
117 })?;
118 let artifact = self.artifact_path(&digest);
119 let meta = self.meta_path(&digest);
120 if artifact.exists() {
121 fs::remove_file(&artifact).map_err(|error| {
122 RegistryCacheError::new(
123 RegistryCacheErrorCode::RegistryPrepareFailed,
124 format!("failed to evict verified registry artifact: {error}"),
125 )
126 })?;
127 }
128 if meta.exists() {
129 fs::remove_file(&meta).map_err(|error| {
130 RegistryCacheError::new(
131 RegistryCacheErrorCode::RegistryPrepareFailed,
132 format!("failed to evict registry cache metadata: {error}"),
133 )
134 })?;
135 }
136 Ok(())
137 }
138
139 pub fn evict_all(&self) -> Result<(), RegistryCacheError> {
146 for sub in ["sha256", "meta", "refs"] {
147 let path = self.root.join(sub);
148 if path.exists() {
149 fs::remove_dir_all(&path).map_err(|error| {
150 RegistryCacheError::new(
151 RegistryCacheErrorCode::RegistryPrepareFailed,
152 format!("failed to clear host registry cache: {error}"),
153 )
154 })?;
155 }
156 }
157 Ok(())
158 }
159
160 fn artifact_path(&self, digest_hex: &str) -> PathBuf {
161 self.root.join("sha256").join(digest_hex)
162 }
163
164 fn meta_path(&self, digest_hex: &str) -> PathBuf {
165 self.root.join("meta").join(format!("{digest_hex}.json"))
166 }
167
168 fn ref_path(&self, reference: &RegistryReference) -> PathBuf {
169 let key = sha256_hex(
170 format!(
171 "{}:{}:{}",
172 reference.namespace, reference.id, reference.version_range
173 )
174 .as_bytes(),
175 );
176 self.root.join("refs").join(format!("{key}.json"))
177 }
178
179 fn public_metadata_path(&self) -> PathBuf {
180 self.root.join("public-metadata").join("current.json")
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
186pub struct PublicCapabilityMetadata {
187 pub namespace: String,
188 pub id: String,
189 pub version: String,
190 pub artifact_digest: String,
191 pub source_release: String,
192 pub index_digest: String,
193 pub summary: String,
194 pub description: String,
195 pub scenarios: Vec<String>,
196 pub service_type: String,
197 pub permitted_targets: Vec<String>,
198 pub lifecycle: String,
199 pub provenance: Option<Value>,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
207pub struct PublicMetadataRead {
208 pub records: Vec<PublicCapabilityMetadata>,
209 pub stale: bool,
210 pub source_release: String,
211 pub index_digest: String,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
215struct PublicMetadataGeneration {
216 schema_version: u32,
217 stale: bool,
218 source_release: String,
219 index_digest: String,
220 records: Vec<PublicCapabilityMetadata>,
221}
222
223pub fn publish_public_metadata(
231 cache: &HostRegistryCache,
232 snapshot: &SyncedPublicRegistryState,
233 stale: bool,
234) -> Result<(), RegistryCacheError> {
235 if snapshot.capabilities.is_empty() {
236 return Err(RegistryCacheError::new(
237 RegistryCacheErrorCode::RegistrySyncMissing,
238 "synced registry index snapshot contains no capabilities",
239 ));
240 }
241 let index_digest = index_snapshot_digest(snapshot);
242 let records = snapshot
243 .capabilities
244 .iter()
245 .map(|record| PublicCapabilityMetadata {
246 namespace: record.namespace.clone(),
247 id: record.id.clone(),
248 version: record.version.clone(),
249 artifact_digest: record.digest.clone(),
250 source_release: snapshot.release_tag.clone(),
251 index_digest: index_digest.clone(),
252 summary: record.summary.clone(),
253 description: record.description.clone(),
254 scenarios: record
255 .use_cases
256 .iter()
257 .map(|use_case| use_case.scenario.clone())
258 .collect(),
259 service_type: record.service_type.clone(),
260 permitted_targets: record.permitted_targets.clone(),
261 lifecycle: record.lifecycle.clone(),
262 provenance: record.provenance.clone(),
263 })
264 .collect();
265 write_json_atomic(
266 &cache.public_metadata_path(),
267 &PublicMetadataGeneration {
268 schema_version: 1,
269 stale,
270 source_release: snapshot.release_tag.clone(),
271 index_digest,
272 records,
273 },
274 )
275}
276
277pub fn read_public_metadata(
284 cache: &HostRegistryCache,
285) -> Result<PublicMetadataRead, RegistryCacheError> {
286 let bytes = fs::read(cache.public_metadata_path()).map_err(|_| {
287 RegistryCacheError::new(
288 RegistryCacheErrorCode::RegistrySyncMissing,
289 "public metadata cache generation is missing",
290 )
291 })?;
292 let generation: PublicMetadataGeneration = serde_json::from_slice(&bytes).map_err(|_| {
293 RegistryCacheError::new(
294 RegistryCacheErrorCode::RegistryMetadataCacheInvalid,
295 "public metadata cache generation is malformed",
296 )
297 })?;
298 if generation.schema_version != 1
299 || generation.source_release.is_empty()
300 || normalize_digest(&generation.index_digest).is_none()
301 || generation.records.iter().any(|record| {
302 record.namespace.is_empty()
303 || record.id.is_empty()
304 || record.version.is_empty()
305 || normalize_digest(&record.artifact_digest).is_none()
306 || record.source_release != generation.source_release
307 || record.index_digest != generation.index_digest
308 || record.service_type.is_empty()
309 || record.lifecycle.is_empty()
310 })
311 {
312 return Err(RegistryCacheError::new(
313 RegistryCacheErrorCode::RegistryMetadataCacheInvalid,
314 "public metadata cache generation has invalid verification bindings",
315 ));
316 }
317 Ok(PublicMetadataRead {
318 records: generation.records,
319 stale: generation.stale,
320 source_release: generation.source_release,
321 index_digest: generation.index_digest,
322 })
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct RegistryPrepareEvidence {
328 pub namespace: String,
330 pub id: String,
332 pub selected_version: String,
334 pub version_range: String,
336 pub source_release: String,
338 pub index_digest: String,
340 pub artifact_digest: String,
342 pub verified_at: u64,
344 pub outcome: &'static str,
346}
347
348impl RegistryPrepareEvidence {
349 #[must_use]
351 pub fn as_value(&self) -> Value {
352 json!({
353 "namespace": self.namespace,
354 "id": self.id,
355 "selected_version": self.selected_version,
356 "version_range": self.version_range,
357 "source_release": self.source_release,
358 "index_digest": self.index_digest,
359 "artifact_digest": self.artifact_digest,
360 "verified_at": self.verified_at,
361 "outcome": self.outcome,
362 })
363 }
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
367struct CacheEntryMeta {
368 namespace: String,
369 id: String,
370 selected_version: String,
371 version_range: String,
372 source_release: String,
373 index_digest: String,
374 artifact_digest: String,
375 contract_digest: String,
376 verified_at: u64,
377}
378
379#[derive(Debug, Clone, PartialEq, Eq)]
381pub struct VerifiedRegistryDependency {
382 pub wasm_binary_path: PathBuf,
384 pub contract_path: PathBuf,
386 pub contract_bytes: Vec<u8>,
388 pub wasm_digest: String,
390 pub evidence: RegistryPrepareEvidence,
392}
393
394pub fn prepare(
404 cache: &HostRegistryCache,
405 snapshot: &SyncedPublicRegistryState,
406 reference: &RegistryReference,
407 fetcher: &dyn RegistryArtifactFetcher,
408) -> Result<RegistryPrepareEvidence, RegistryCacheError> {
409 if snapshot.capabilities.is_empty() {
410 return Err(RegistryCacheError::new(
411 RegistryCacheErrorCode::RegistrySyncMissing,
412 "synced registry index snapshot contains no capabilities",
413 ));
414 }
415 let record = select_highest_active(snapshot, reference)?;
416 let index_digest = index_snapshot_digest(snapshot);
417 let artifact_bytes = fetcher.fetch(&record.artifact_url).map_err(|message| {
418 RegistryCacheError::new(
419 RegistryCacheErrorCode::RegistryPrepareFailed,
420 format!("host registry artifact fetch failed: {message}"),
421 )
422 })?;
423 let artifact_hex = verify_digest(&record.digest, &artifact_bytes)?;
424 let contract_bytes = fetcher.fetch(&record.contract_url).map_err(|message| {
425 RegistryCacheError::new(
426 RegistryCacheErrorCode::RegistryPrepareFailed,
427 format!("host registry contract fetch failed: {message}"),
428 )
429 })?;
430 let contract_hex = verify_digest(&record.contract_digest, &contract_bytes)?;
431
432 write_verified_bytes(cache, &artifact_hex, &artifact_bytes)?;
433 write_verified_bytes(cache, &contract_hex, &contract_bytes)?;
434
435 let verified_at = unix_seconds();
436 let meta = CacheEntryMeta {
437 namespace: record.namespace.clone(),
438 id: record.id.clone(),
439 selected_version: record.version.clone(),
440 version_range: reference.version_range.clone(),
441 source_release: snapshot.release_tag.clone(),
442 index_digest: index_digest.clone(),
443 artifact_digest: record.digest.clone(),
444 contract_digest: record.contract_digest.clone(),
445 verified_at,
446 };
447 write_json_atomic(&cache.meta_path(&artifact_hex), &meta)?;
448 write_json_atomic(
449 &cache.ref_path(reference),
450 &json!({
451 "artifact_digest": record.digest,
452 "contract_digest": record.contract_digest,
453 }),
454 )?;
455
456 Ok(RegistryPrepareEvidence {
457 namespace: record.namespace,
458 id: record.id,
459 selected_version: record.version,
460 version_range: reference.version_range.clone(),
461 source_release: snapshot.release_tag.clone(),
462 index_digest,
463 artifact_digest: record.digest,
464 verified_at,
465 outcome: "prepared",
466 })
467}
468
469pub fn resolve_offline(
477 cache: &HostRegistryCache,
478 reference: &RegistryReference,
479) -> Result<VerifiedRegistryDependency, RegistryCacheError> {
480 let ref_path = cache.ref_path(reference);
481 let ref_bytes = fs::read(&ref_path).map_err(|_| {
482 RegistryCacheError::new(
483 RegistryCacheErrorCode::RegistryCacheEntryMissing,
484 "verified registry cache entry is missing for registry_ref",
485 )
486 })?;
487 let pointer: Value = serde_json::from_slice(&ref_bytes).map_err(|_| {
488 RegistryCacheError::new(
489 RegistryCacheErrorCode::RegistryCacheEntryMissing,
490 "verified registry cache entry is missing for registry_ref",
491 )
492 })?;
493 let artifact_digest = pointer
494 .get("artifact_digest")
495 .and_then(Value::as_str)
496 .ok_or_else(|| {
497 RegistryCacheError::new(
498 RegistryCacheErrorCode::RegistryCacheEntryMissing,
499 "verified registry cache entry is missing for registry_ref",
500 )
501 })?;
502 let contract_digest = pointer
503 .get("contract_digest")
504 .and_then(Value::as_str)
505 .ok_or_else(|| {
506 RegistryCacheError::new(
507 RegistryCacheErrorCode::RegistryCacheEntryMissing,
508 "verified registry cache entry is missing for registry_ref",
509 )
510 })?;
511 let artifact_hex = normalize_digest(artifact_digest).ok_or_else(|| {
512 RegistryCacheError::new(
513 RegistryCacheErrorCode::RegistryCacheEntryMissing,
514 "verified registry cache entry is missing for registry_ref",
515 )
516 })?;
517 let contract_hex = normalize_digest(contract_digest).ok_or_else(|| {
518 RegistryCacheError::new(
519 RegistryCacheErrorCode::RegistryCacheEntryMissing,
520 "verified registry cache entry is missing for registry_ref",
521 )
522 })?;
523 let (wasm_binary_path, _) = read_verified(cache, &artifact_hex, artifact_digest)?;
524 let (contract_path, contract_bytes) = read_verified(cache, &contract_hex, contract_digest)?;
525 let meta_bytes = fs::read(cache.meta_path(&artifact_hex)).map_err(|_| {
526 RegistryCacheError::new(
527 RegistryCacheErrorCode::RegistryCacheEntryMissing,
528 "verified registry cache entry is missing for registry_ref",
529 )
530 })?;
531 let meta: CacheEntryMeta = serde_json::from_slice(&meta_bytes).map_err(|_| {
532 RegistryCacheError::new(
533 RegistryCacheErrorCode::RegistryCacheEntryMissing,
534 "verified registry cache entry is missing for registry_ref",
535 )
536 })?;
537 Ok(VerifiedRegistryDependency {
538 wasm_binary_path,
539 contract_path,
540 contract_bytes,
541 wasm_digest: artifact_digest.to_string(),
542 evidence: RegistryPrepareEvidence {
543 namespace: meta.namespace,
544 id: meta.id,
545 selected_version: meta.selected_version,
546 version_range: meta.version_range,
547 source_release: meta.source_release,
548 index_digest: meta.index_digest,
549 artifact_digest: meta.artifact_digest,
550 verified_at: meta.verified_at,
551 outcome: "resolved",
552 },
553 })
554}
555
556pub fn resolve_component(
563 cache: &HostRegistryCache,
564 reference: &RegistryReference,
565) -> Result<ResolvedRegistryComponent, RegistryCacheError> {
566 let resolved = resolve_offline(cache, reference)?;
567 let contract_text = String::from_utf8(resolved.contract_bytes).map_err(|_| {
568 RegistryCacheError::new(
569 RegistryCacheErrorCode::RegistryPrepareFailed,
570 "verified registry contract is invalid: not utf-8",
571 )
572 })?;
573 let contract = parse_contract(&contract_text).map_err(|failure| {
574 RegistryCacheError::new(
575 RegistryCacheErrorCode::RegistryPrepareFailed,
576 format!(
577 "verified registry contract is invalid: {}",
578 failure
579 .errors
580 .first()
581 .map_or("parse failed", |error| error.message.as_str())
582 ),
583 )
584 })?;
585 Ok(ResolvedRegistryComponent {
586 contract_path: resolved.contract_path,
587 contract,
588 wasm_binary_path: resolved.wasm_binary_path,
589 wasm_digest: resolved.wasm_digest,
590 })
591}
592
593fn select_highest_active(
594 snapshot: &SyncedPublicRegistryState,
595 reference: &RegistryReference,
596) -> Result<PublicRegistryCapabilityRecord, RegistryCacheError> {
597 let requirement = semver::VersionReq::parse(&reference.version_range).map_err(|error| {
598 RegistryCacheError::new(
599 RegistryCacheErrorCode::RegistryVersionNotFound,
600 format!(
601 "invalid registry_ref version_range {}: {error}",
602 reference.version_range
603 ),
604 )
605 })?;
606 let matching = snapshot
607 .capabilities
608 .iter()
609 .filter_map(|record| {
610 if record.namespace != reference.namespace || record.id != reference.id {
611 return None;
612 }
613 semver::Version::parse(&record.version)
614 .ok()
615 .filter(|version| requirement.matches(version))
616 .map(|version| (version, record.clone()))
617 })
618 .collect::<Vec<_>>();
619 let mut active = matching
620 .iter()
621 .filter(|(_, record)| !record.deprecated)
622 .cloned()
623 .collect::<Vec<_>>();
624 active.sort_by(|left, right| right.0.cmp(&left.0));
625 if let Some((_, record)) = active.into_iter().next() {
626 return Ok(record);
627 }
628 if matching.is_empty() {
629 Err(RegistryCacheError::new(
630 RegistryCacheErrorCode::RegistryVersionNotFound,
631 format!(
632 "no synced public registry version for {}:{} satisfies {}",
633 reference.namespace, reference.id, reference.version_range
634 ),
635 ))
636 } else {
637 Err(RegistryCacheError::new(
638 RegistryCacheErrorCode::RegistryDependencyYanked,
639 format!(
640 "only yanked public registry versions for {}:{} satisfy {}",
641 reference.namespace, reference.id, reference.version_range
642 ),
643 ))
644 }
645}
646
647fn index_snapshot_digest(snapshot: &SyncedPublicRegistryState) -> String {
648 let encoded = serde_json::to_vec(snapshot).unwrap_or_default();
649 format!("sha256:{}", sha256_hex(&encoded))
650}
651
652fn verify_digest(declared: &str, bytes: &[u8]) -> Result<String, RegistryCacheError> {
653 let expected = normalize_digest(declared).ok_or_else(|| {
654 RegistryCacheError::new(
655 RegistryCacheErrorCode::RegistryArtifactDigestMismatch,
656 "registry digest must be sha256: followed by 64 hex characters",
657 )
658 })?;
659 if sha256_hex(bytes) == expected {
660 Ok(expected)
661 } else {
662 Err(RegistryCacheError::new(
663 RegistryCacheErrorCode::RegistryArtifactDigestMismatch,
664 "registry artifact bytes do not match the published digest",
665 ))
666 }
667}
668
669fn write_verified_bytes(
670 cache: &HostRegistryCache,
671 digest_hex: &str,
672 bytes: &[u8],
673) -> Result<PathBuf, RegistryCacheError> {
674 let path = cache.artifact_path(digest_hex);
675 if path.exists() {
676 let cached = fs::read(&path).map_err(|error| {
677 RegistryCacheError::new(
678 RegistryCacheErrorCode::RegistryPrepareFailed,
679 format!("failed to read existing verified cache entry: {error}"),
680 )
681 })?;
682 if sha256_hex(&cached) != digest_hex {
683 return Err(RegistryCacheError::new(
684 RegistryCacheErrorCode::RegistryArtifactDigestMismatch,
685 "existing registry cache entry digest mismatch",
686 ));
687 }
688 return Ok(path);
689 }
690 let parent = cache.root.join("sha256");
691 fs::create_dir_all(&parent).map_err(|error| {
692 RegistryCacheError::new(
693 RegistryCacheErrorCode::RegistryPrepareFailed,
694 format!("failed to create host registry cache directory: {error}"),
695 )
696 })?;
697 let temporary = path.with_extension("tmp");
698 fs::write(&temporary, bytes).map_err(|error| {
699 let _ = fs::remove_file(&temporary);
700 RegistryCacheError::new(
701 RegistryCacheErrorCode::RegistryPrepareFailed,
702 format!("failed to write registry cache entry: {error}"),
703 )
704 })?;
705 fs::rename(&temporary, &path).map_err(|error| {
706 let _ = fs::remove_file(&temporary);
707 RegistryCacheError::new(
708 RegistryCacheErrorCode::RegistryPrepareFailed,
709 format!("failed to commit registry cache entry: {error}"),
710 )
711 })?;
712 Ok(path)
713}
714
715fn read_verified(
716 cache: &HostRegistryCache,
717 digest_hex: &str,
718 declared: &str,
719) -> Result<(PathBuf, Vec<u8>), RegistryCacheError> {
720 let path = cache.artifact_path(digest_hex);
721 let bytes = fs::read(&path).map_err(|_| {
722 RegistryCacheError::new(
723 RegistryCacheErrorCode::RegistryCacheEntryMissing,
724 "verified registry cache entry is missing for registry_ref",
725 )
726 })?;
727 verify_digest(declared, &bytes)?;
728 Ok((path, bytes))
729}
730
731fn write_json_atomic(path: &Path, value: &impl serde::Serialize) -> Result<(), RegistryCacheError> {
732 let parent = path.parent().ok_or_else(|| {
733 RegistryCacheError::new(
734 RegistryCacheErrorCode::RegistryPrepareFailed,
735 "registry cache metadata path has no parent directory",
736 )
737 })?;
738 fs::create_dir_all(parent).map_err(|error| {
739 RegistryCacheError::new(
740 RegistryCacheErrorCode::RegistryPrepareFailed,
741 format!("failed to create host registry cache directory: {error}"),
742 )
743 })?;
744 let bytes = serde_json::to_vec(value).map_err(|error| {
745 RegistryCacheError::new(
746 RegistryCacheErrorCode::RegistryPrepareFailed,
747 format!("failed to encode registry cache metadata: {error}"),
748 )
749 })?;
750 let temporary = path.with_extension("tmp");
751 fs::write(&temporary, bytes).map_err(|error| {
752 let _ = fs::remove_file(&temporary);
753 RegistryCacheError::new(
754 RegistryCacheErrorCode::RegistryPrepareFailed,
755 format!("failed to write registry cache metadata: {error}"),
756 )
757 })?;
758 fs::rename(&temporary, path).map_err(|error| {
759 let _ = fs::remove_file(&temporary);
760 RegistryCacheError::new(
761 RegistryCacheErrorCode::RegistryPrepareFailed,
762 format!("failed to commit registry cache metadata: {error}"),
763 )
764 })?;
765 Ok(())
766}
767
768fn normalize_digest(digest: &str) -> Option<String> {
769 let digest = digest.strip_prefix("sha256:")?;
770 if digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
771 Some(digest.to_ascii_lowercase())
772 } else {
773 None
774 }
775}
776
777fn sha256_hex(bytes: &[u8]) -> String {
778 let digest = Sha256::digest(bytes);
779 let mut value = String::with_capacity(digest.len() * 2);
780 for byte in digest {
781 let _ = write!(value, "{byte:02x}");
782 }
783 value
784}
785
786fn unix_seconds() -> u64 {
787 SystemTime::now()
788 .duration_since(UNIX_EPOCH)
789 .map_or(0, |duration| duration.as_secs())
790}
791
792#[cfg(test)]
793mod tests {
794 #![allow(clippy::expect_used, clippy::unwrap_used)]
795
796 use super::*;
797 use std::collections::HashMap;
798 use std::sync::atomic::{AtomicU64, Ordering};
799
800 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
801
802 struct MapFetcher {
803 assets: HashMap<String, Vec<u8>>,
804 }
805
806 impl RegistryArtifactFetcher for MapFetcher {
807 fn fetch(&self, url: &str) -> Result<Vec<u8>, String> {
808 self.assets
809 .get(url)
810 .cloned()
811 .ok_or_else(|| "missing asset".to_string())
812 }
813 }
814
815 fn digest_for(bytes: &[u8]) -> String {
816 format!("sha256:{}", sha256_hex(bytes))
817 }
818
819 fn sample_snapshot(
820 deprecated: bool,
821 ) -> (SyncedPublicRegistryState, MapFetcher, RegistryReference) {
822 let artifact = b"wasm-bytes".to_vec();
823 let contract = br#"{"kind":"capability_contract"}"#.to_vec();
824 let artifact_digest = digest_for(&artifact);
825 let contract_digest = digest_for(&contract);
826 let record = PublicRegistryCapabilityRecord {
827 namespace: "demo".to_string(),
828 id: "greet".to_string(),
829 version: "1.2.0".to_string(),
830 digest: artifact_digest,
831 artifact_url: "https://example.test/greet.wasm".to_string(),
832 contract_digest,
833 contract_url: "https://example.test/greet.json".to_string(),
834 deprecated,
835 summary: "Greeting".to_string(),
836 description: "Greets a person".to_string(),
837 use_cases: Vec::new(),
838 service_type: "stateless".to_string(),
839 permitted_targets: Vec::new(),
840 lifecycle: "active".to_string(),
841 provenance: None,
842 };
843 let older = PublicRegistryCapabilityRecord {
844 namespace: "demo".to_string(),
845 id: "greet".to_string(),
846 version: "1.0.0".to_string(),
847 digest: digest_for(b"older"),
848 artifact_url: "https://example.test/older.wasm".to_string(),
849 contract_digest: digest_for(b"older-contract"),
850 contract_url: "https://example.test/older.json".to_string(),
851 deprecated: false,
852 summary: String::new(),
853 description: String::new(),
854 use_cases: Vec::new(),
855 service_type: "stateless".to_string(),
856 permitted_targets: Vec::new(),
857 lifecycle: "active".to_string(),
858 provenance: None,
859 };
860 let snapshot = SyncedPublicRegistryState {
861 schema_version: "1".to_string(),
862 workspace_id: "ws".to_string(),
863 state_scope: "public".to_string(),
864 source_repo: "traverse-framework/registry".to_string(),
865 release_tag: "index-v9".to_string(),
866 index_version: 9,
867 generated_at: "2026-07-29T00:00:00Z".to_string(),
868 source_commit: None,
869 synced_at: "2026-07-29T00:00:00Z".to_string(),
870 record_count: 2,
871 validation_status: "valid".to_string(),
872 governing_spec: "055-registry-sync".to_string(),
873 capabilities: vec![older, record.clone()],
874 events: Vec::new(),
875 };
876 let mut assets = HashMap::new();
877 assets.insert(record.artifact_url.clone(), artifact);
878 assets.insert(record.contract_url.clone(), contract);
879 let fetcher = MapFetcher { assets };
880 let reference = RegistryReference {
881 namespace: "demo".to_string(),
882 id: "greet".to_string(),
883 version_range: "^1.0.0".to_string(),
884 };
885 (snapshot, fetcher, reference)
886 }
887
888 fn unique_cache() -> HostRegistryCache {
889 let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
890 let nanos = SystemTime::now()
891 .duration_since(UNIX_EPOCH)
892 .expect("clock")
893 .as_nanos();
894 let root = std::env::temp_dir().join(format!("traverse-embedder-cache-{nanos}-{counter}"));
895 fs::create_dir_all(&root).expect("temp");
896 HostRegistryCache::new(root)
897 }
898
899 #[test]
900 fn prepare_then_offline_resolve_round_trip() {
901 let cache = unique_cache();
902 let (snapshot, fetcher, reference) = sample_snapshot(false);
903 let evidence = prepare(&cache, &snapshot, &reference, &fetcher).expect("prepare");
904 assert_eq!(evidence.selected_version, "1.2.0");
905 assert_eq!(evidence.source_release, "index-v9");
906 assert_eq!(evidence.outcome, "prepared");
907 let resolved = resolve_offline(&cache, &reference).expect("offline");
908 assert_eq!(resolved.evidence.selected_version, "1.2.0");
909 assert_eq!(
910 fs::read(&resolved.wasm_binary_path).expect("wasm"),
911 b"wasm-bytes"
912 );
913 }
914
915 #[test]
916 fn offline_resolve_without_prepare_is_missing() {
917 let cache = unique_cache();
918 let reference = RegistryReference {
919 namespace: "demo".to_string(),
920 id: "greet".to_string(),
921 version_range: "^1.0.0".to_string(),
922 };
923 let failure = resolve_offline(&cache, &reference).expect_err("missing");
924 assert_eq!(
925 failure.code,
926 RegistryCacheErrorCode::RegistryCacheEntryMissing
927 );
928 }
929
930 #[test]
931 fn yanked_only_range_fails_closed() {
932 let cache = unique_cache();
933 let (mut snapshot, fetcher, reference) = sample_snapshot(true);
934 snapshot
935 .capabilities
936 .retain(|record| record.version == "1.2.0");
937 let failure = prepare(&cache, &snapshot, &reference, &fetcher).expect_err("yanked");
938 assert_eq!(
939 failure.code,
940 RegistryCacheErrorCode::RegistryDependencyYanked
941 );
942 assert!(
943 cache
944 .root
945 .join("sha256")
946 .read_dir()
947 .ok()
948 .is_none_or(|mut d| d.next().is_none())
949 );
950 }
951
952 #[test]
953 fn digest_mismatch_leaves_no_usable_entry() {
954 let cache = unique_cache();
955 let (snapshot, mut fetcher, reference) = sample_snapshot(false);
956 fetcher.assets.insert(
957 "https://example.test/greet.wasm".to_string(),
958 b"tampered".to_vec(),
959 );
960 let failure = prepare(&cache, &snapshot, &reference, &fetcher).expect_err("mismatch");
961 assert_eq!(
962 failure.code,
963 RegistryCacheErrorCode::RegistryArtifactDigestMismatch
964 );
965 let missing = resolve_offline(&cache, &reference).expect_err("no entry");
966 assert_eq!(
967 missing.code,
968 RegistryCacheErrorCode::RegistryCacheEntryMissing
969 );
970 }
971
972 #[test]
973 fn empty_snapshot_is_sync_missing() {
974 let cache = unique_cache();
975 let (mut snapshot, fetcher, reference) = sample_snapshot(false);
976 snapshot.capabilities.clear();
977 let failure = prepare(&cache, &snapshot, &reference, &fetcher).expect_err("empty");
978 assert_eq!(failure.code, RegistryCacheErrorCode::RegistrySyncMissing);
979 }
980
981 #[test]
982 fn evict_removes_prepared_entry() {
983 let cache = unique_cache();
984 let (snapshot, fetcher, reference) = sample_snapshot(false);
985 let evidence = prepare(&cache, &snapshot, &reference, &fetcher).expect("prepare");
986 cache.evict(&evidence.artifact_digest).expect("evict");
987 let missing = resolve_offline(&cache, &reference).expect_err("gone");
988 assert!(matches!(
989 missing.code,
990 RegistryCacheErrorCode::RegistryCacheEntryMissing
991 | RegistryCacheErrorCode::RegistryArtifactDigestMismatch
992 ));
993 }
994
995 #[test]
996 fn error_codes_and_evidence_project_secret_free_values() {
997 for (code, wire) in [
998 (
999 RegistryCacheErrorCode::RegistrySyncMissing,
1000 "registry_sync_missing",
1001 ),
1002 (
1003 RegistryCacheErrorCode::RegistryVersionNotFound,
1004 "registry_version_not_found",
1005 ),
1006 (
1007 RegistryCacheErrorCode::RegistryDependencyYanked,
1008 "registry_dependency_yanked",
1009 ),
1010 (
1011 RegistryCacheErrorCode::RegistryPrepareFailed,
1012 "registry_prepare_failed",
1013 ),
1014 (
1015 RegistryCacheErrorCode::RegistryArtifactDigestMismatch,
1016 "registry_artifact_digest_mismatch",
1017 ),
1018 (
1019 RegistryCacheErrorCode::RegistryCacheEntryMissing,
1020 "registry_cache_entry_missing",
1021 ),
1022 ] {
1023 assert_eq!(code.as_str(), wire);
1024 }
1025 let cache = unique_cache();
1026 assert!(cache.root().exists());
1027 let (snapshot, fetcher, reference) = sample_snapshot(false);
1028 let evidence = prepare(&cache, &snapshot, &reference, &fetcher).expect("prepare");
1029 let value = evidence.as_value();
1030 assert_eq!(value["outcome"], "prepared");
1031 assert_eq!(value["selected_version"], "1.2.0");
1032 cache.evict_all().expect("evict_all");
1033 assert!(
1034 resolve_offline(&cache, &reference)
1035 .expect_err("cleared")
1036 .code
1037 == RegistryCacheErrorCode::RegistryCacheEntryMissing
1038 );
1039 let invalid = cache.evict("not-a-digest").expect_err("invalid");
1040 assert_eq!(invalid.code, RegistryCacheErrorCode::RegistryPrepareFailed);
1041 }
1042
1043 #[test]
1044 fn prepare_reports_fetch_and_version_selection_failures() {
1045 let cache = unique_cache();
1046 let (snapshot, fetcher, mut reference) = sample_snapshot(false);
1047 reference.version_range = "not a range!!!".to_string();
1048 let invalid_range = prepare(&cache, &snapshot, &reference, &fetcher).expect_err("range");
1049 assert_eq!(
1050 invalid_range.code,
1051 RegistryCacheErrorCode::RegistryVersionNotFound
1052 );
1053
1054 let (snapshot, fetcher, mut reference) = sample_snapshot(false);
1055 reference.version_range = "^9.0.0".to_string();
1056 let missing = prepare(&cache, &snapshot, &reference, &fetcher).expect_err("missing");
1057 assert_eq!(
1058 missing.code,
1059 RegistryCacheErrorCode::RegistryVersionNotFound
1060 );
1061
1062 let (snapshot, fetcher, reference) = sample_snapshot(false);
1063 let empty_fetcher = MapFetcher {
1064 assets: HashMap::new(),
1065 };
1066 let fetch_failed =
1067 prepare(&cache, &snapshot, &reference, &empty_fetcher).expect_err("fetch");
1068 assert_eq!(
1069 fetch_failed.code,
1070 RegistryCacheErrorCode::RegistryPrepareFailed
1071 );
1072 let _ = fetcher;
1073 }
1074
1075 #[test]
1076 fn prepare_rejects_invalid_declared_digests_and_reuses_verified_hits() {
1077 let cache = unique_cache();
1078 let (mut snapshot, fetcher, reference) = sample_snapshot(false);
1079 snapshot.capabilities[1].digest = "sha256:short".to_string();
1080 let invalid = prepare(&cache, &snapshot, &reference, &fetcher).expect_err("digest");
1081 assert_eq!(
1082 invalid.code,
1083 RegistryCacheErrorCode::RegistryArtifactDigestMismatch
1084 );
1085
1086 let (snapshot, fetcher, reference) = sample_snapshot(false);
1087 let first = prepare(&cache, &snapshot, &reference, &fetcher).expect("first");
1088 let second = prepare(&cache, &snapshot, &reference, &fetcher).expect("reuse");
1089 assert_eq!(first.artifact_digest, second.artifact_digest);
1090
1091 let artifact_hex = normalize_digest(&first.artifact_digest).expect("hex");
1092 let path = cache.artifact_path(&artifact_hex);
1093 fs::write(&path, b"tampered").expect("tamper");
1094 let tampered = prepare(&cache, &snapshot, &reference, &fetcher).expect_err("tampered");
1095 assert_eq!(
1096 tampered.code,
1097 RegistryCacheErrorCode::RegistryArtifactDigestMismatch
1098 );
1099 }
1100 #[test]
1101 fn offline_resolve_rejects_corrupt_pointers_and_missing_meta() {
1102 let cache = unique_cache();
1103 let reference = RegistryReference {
1104 namespace: "demo".to_string(),
1105 id: "greet".to_string(),
1106 version_range: "^1.0.0".to_string(),
1107 };
1108 let ref_path = cache.ref_path(&reference);
1109 fs::create_dir_all(ref_path.parent().expect("parent")).expect("dirs");
1110 fs::write(&ref_path, b"{not-json").expect("corrupt");
1111 let corrupt = resolve_offline(&cache, &reference).expect_err("corrupt");
1112 assert_eq!(
1113 corrupt.code,
1114 RegistryCacheErrorCode::RegistryCacheEntryMissing
1115 );
1116
1117 fs::write(
1118 &ref_path,
1119 br#"{"contract_digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}"#,
1120 )
1121 .expect("partial artifact");
1122 let missing_artifact = resolve_offline(&cache, &reference).expect_err("artifact");
1123 assert_eq!(
1124 missing_artifact.code,
1125 RegistryCacheErrorCode::RegistryCacheEntryMissing
1126 );
1127
1128 fs::write(
1129 &ref_path,
1130 br#"{"artifact_digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","contract_digest":"bad"}"#,
1131 )
1132 .expect("bad contract digest");
1133 let bad_contract = resolve_offline(&cache, &reference).expect_err("contract dig");
1134 assert_eq!(
1135 bad_contract.code,
1136 RegistryCacheErrorCode::RegistryCacheEntryMissing
1137 );
1138
1139 fs::write(
1140 &ref_path,
1141 br#"{"artifact_digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}"#,
1142 )
1143 .expect("partial");
1144 let partial = resolve_offline(&cache, &reference).expect_err("partial");
1145 assert_eq!(
1146 partial.code,
1147 RegistryCacheErrorCode::RegistryCacheEntryMissing
1148 );
1149
1150 let (snapshot, fetcher, reference) = sample_snapshot(false);
1151 let evidence = prepare(&cache, &snapshot, &reference, &fetcher).expect("prepare");
1152 let artifact_hex = normalize_digest(&evidence.artifact_digest).expect("hex");
1153 fs::remove_file(cache.meta_path(&artifact_hex)).expect("remove meta");
1154 let missing_meta = resolve_offline(&cache, &reference).expect_err("meta");
1155 assert_eq!(
1156 missing_meta.code,
1157 RegistryCacheErrorCode::RegistryCacheEntryMissing
1158 );
1159 }
1160
1161 #[test]
1162 fn contract_fetch_failure_and_invalid_contract_digest_fail_closed() {
1163 let cache = unique_cache();
1164 let (snapshot, mut fetcher, reference) = sample_snapshot(false);
1165 fetcher.assets.remove("https://example.test/greet.json");
1166 let failure = prepare(&cache, &snapshot, &reference, &fetcher).expect_err("contract fetch");
1167 assert_eq!(failure.code, RegistryCacheErrorCode::RegistryPrepareFailed);
1168
1169 let (snapshot, mut fetcher, reference) = sample_snapshot(false);
1170 fetcher.assets.insert(
1171 "https://example.test/greet.json".to_string(),
1172 b"wrong-contract".to_vec(),
1173 );
1174 let mismatch = prepare(&cache, &snapshot, &reference, &fetcher).expect_err("contract dig");
1175 assert_eq!(
1176 mismatch.code,
1177 RegistryCacheErrorCode::RegistryArtifactDigestMismatch
1178 );
1179 }
1180
1181 #[test]
1182 fn evict_all_and_missing_evict_paths_are_idempotent() {
1183 let cache = unique_cache();
1184 cache.evict_all().expect("empty evict_all");
1185 let digest = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
1186 cache.evict(digest).expect("missing evict ok");
1187 let (snapshot, fetcher, reference) = sample_snapshot(false);
1188 let evidence = prepare(&cache, &snapshot, &reference, &fetcher).expect("prepare");
1189 cache.evict_all().expect("clear");
1190 cache.evict(&evidence.artifact_digest).expect("again");
1191 }
1192
1193 #[test]
1194 fn write_verified_bytes_reports_directory_collision_failures() {
1195 let cache = unique_cache();
1196 let bytes = b"content";
1197 let digest = digest_for(bytes);
1198 let hex = normalize_digest(&digest).expect("hex");
1199 let path = cache.artifact_path(&hex);
1200 fs::create_dir_all(&path).expect("block as directory");
1201 let failure = write_verified_bytes(&cache, &hex, bytes).expect_err("dir");
1202 assert_eq!(failure.code, RegistryCacheErrorCode::RegistryPrepareFailed);
1203 }
1204
1205 #[test]
1206 fn evict_and_write_json_report_filesystem_failures() {
1207 let cache_art = unique_cache();
1209 let (snapshot, fetcher, reference) = sample_snapshot(false);
1210 let evidence = prepare(&cache_art, &snapshot, &reference, &fetcher).expect("prepare");
1211 let hex = normalize_digest(&evidence.artifact_digest).expect("hex");
1212 let artifact = cache_art.artifact_path(&hex);
1213 fs::remove_file(&artifact).expect("remove");
1214 fs::create_dir_all(&artifact).expect("dir");
1215 fs::write(artifact.join("nested"), b"x").expect("nested");
1216 let art_evict = cache_art
1217 .evict(&evidence.artifact_digest)
1218 .expect_err("artifact evict");
1219 assert_eq!(
1220 art_evict.code,
1221 RegistryCacheErrorCode::RegistryPrepareFailed
1222 );
1223
1224 let cache = unique_cache();
1225 let (snapshot, fetcher, reference) = sample_snapshot(false);
1226 let evidence = prepare(&cache, &snapshot, &reference, &fetcher).expect("prepare");
1227 let hex = normalize_digest(&evidence.artifact_digest).expect("hex");
1228
1229 let meta = cache.meta_path(&hex);
1231 fs::remove_file(&meta).expect("remove meta file");
1232 fs::create_dir_all(&meta).expect("meta dir");
1233 fs::write(meta.join("nested"), b"x").expect("nested");
1234 let meta_evict = cache
1235 .evict(&evidence.artifact_digest)
1236 .expect_err("meta evict");
1237 assert_eq!(
1238 meta_evict.code,
1239 RegistryCacheErrorCode::RegistryPrepareFailed
1240 );
1241
1242 let cache2 = unique_cache();
1243 fs::write(cache2.root.join("sha256"), b"not-dir").expect("file");
1244 let clear_fail = cache2.evict_all().expect_err("evict_all");
1245 assert_eq!(
1246 clear_fail.code,
1247 RegistryCacheErrorCode::RegistryPrepareFailed
1248 );
1249
1250 let cache3 = unique_cache();
1251 let meta_parent = cache3.root.join("meta");
1252 fs::write(&meta_parent, b"not-dir").expect("block meta dir");
1253 let meta_fail = write_json_atomic(
1254 &cache3.meta_path("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"),
1255 &json!({"ok": true}),
1256 )
1257 .expect_err("meta");
1258 assert_eq!(
1259 meta_fail.code,
1260 RegistryCacheErrorCode::RegistryPrepareFailed
1261 );
1262
1263 let cache4 = unique_cache();
1265 fs::write(cache4.root.join("sha256"), b"not-dir").expect("block");
1266 let bytes = b"fresh";
1267 let digest = digest_for(bytes);
1268 let hex = normalize_digest(&digest).expect("hex");
1269 let create_fail = write_verified_bytes(&cache4, &hex, bytes).expect_err("create");
1270 assert_eq!(
1271 create_fail.code,
1272 RegistryCacheErrorCode::RegistryPrepareFailed
1273 );
1274 }
1275
1276 #[test]
1277 fn write_helpers_report_tmp_rename_and_existing_entry_failures() {
1278 let cache5 = unique_cache();
1280 let digest = digest_for(b"another");
1281 let hex = normalize_digest(&digest).expect("hex");
1282 let path = cache5.artifact_path(&hex);
1283 fs::create_dir_all(path.parent().expect("parent")).expect("parent");
1284 fs::create_dir_all(path.with_extension("tmp")).expect("tmp dir");
1285 let write_fail = write_verified_bytes(&cache5, &hex, b"another").expect_err("write");
1286 assert_eq!(
1287 write_fail.code,
1288 RegistryCacheErrorCode::RegistryPrepareFailed
1289 );
1290
1291 let cache6 = unique_cache();
1293 let digest = digest_for(b"rename-me");
1294 let hex = normalize_digest(&digest).expect("hex");
1295 let path = cache6.artifact_path(&hex);
1296 fs::create_dir_all(path.parent().expect("parent")).expect("parent");
1297 fs::create_dir_all(&path).expect("dest dir");
1298 let rename_fail = write_verified_bytes(&cache6, &hex, b"rename-me").expect_err("rename");
1299 assert_eq!(
1300 rename_fail.code,
1301 RegistryCacheErrorCode::RegistryPrepareFailed
1302 );
1303
1304 let cache7 = unique_cache();
1306 let target = cache7.root.join("refs").join("blocked.json");
1307 fs::create_dir_all(target.parent().expect("parent")).expect("parent");
1308 fs::create_dir_all(&target).expect("dest dir");
1309 let json_rename = write_json_atomic(&target, &json!({"ok": true})).expect_err("rename");
1310 assert_eq!(
1311 json_rename.code,
1312 RegistryCacheErrorCode::RegistryPrepareFailed
1313 );
1314
1315 let cache8 = unique_cache();
1317 let target = cache8.root.join("refs").join("writeme.json");
1318 fs::create_dir_all(target.parent().expect("parent")).expect("parent");
1319 fs::create_dir_all(target.with_extension("tmp")).expect("tmp dir");
1320 let json_write = write_json_atomic(&target, &json!({"ok": true})).expect_err("write");
1321 assert_eq!(
1322 json_write.code,
1323 RegistryCacheErrorCode::RegistryPrepareFailed
1324 );
1325
1326 let cache9 = unique_cache();
1328 let digest = digest_for(b"readable");
1329 let hex = normalize_digest(&digest).expect("hex");
1330 let path = cache9.artifact_path(&hex);
1331 fs::create_dir_all(&path).expect("dir as entry");
1332 let read_existing = write_verified_bytes(&cache9, &hex, b"readable").expect_err("read");
1333 assert_eq!(
1334 read_existing.code,
1335 RegistryCacheErrorCode::RegistryPrepareFailed
1336 );
1337 }
1338
1339 #[test]
1340 fn prepare_fails_when_refs_root_is_blocked_and_meta_can_be_corrupt() {
1341 let cache = unique_cache();
1342 fs::write(cache.root.join("refs"), b"not-a-directory").expect("block refs");
1343 let (snapshot, fetcher, reference) = sample_snapshot(false);
1344 let failure = prepare(&cache, &snapshot, &reference, &fetcher).expect_err("refs");
1345 assert_eq!(failure.code, RegistryCacheErrorCode::RegistryPrepareFailed);
1346
1347 let cache2 = unique_cache();
1348 let (snapshot, fetcher, reference) = sample_snapshot(false);
1349 let evidence = prepare(&cache2, &snapshot, &reference, &fetcher).expect("prepare");
1350 let hex = normalize_digest(&evidence.artifact_digest).expect("hex");
1351 fs::write(cache2.meta_path(&hex), b"{not-json").expect("corrupt meta");
1352 let corrupt_meta = resolve_offline(&cache2, &reference).expect_err("meta");
1353 assert_eq!(
1354 corrupt_meta.code,
1355 RegistryCacheErrorCode::RegistryCacheEntryMissing
1356 );
1357 }
1358
1359 #[test]
1360 fn version_selection_skips_unrelated_namespace_records() {
1361 let cache = unique_cache();
1362 let (mut snapshot, fetcher, reference) = sample_snapshot(false);
1363 snapshot.capabilities.insert(
1364 0,
1365 PublicRegistryCapabilityRecord {
1366 namespace: "other".to_string(),
1367 id: "thing".to_string(),
1368 version: "9.9.9".to_string(),
1369 digest: digest_for(b"other"),
1370 artifact_url: "https://example.test/other.wasm".to_string(),
1371 contract_digest: digest_for(b"other-contract"),
1372 contract_url: "https://example.test/other.json".to_string(),
1373 deprecated: false,
1374 summary: String::new(),
1375 description: String::new(),
1376 use_cases: Vec::new(),
1377 service_type: String::new(),
1378 permitted_targets: Vec::new(),
1379 lifecycle: String::new(),
1380 provenance: None,
1381 },
1382 );
1383 let evidence = prepare(&cache, &snapshot, &reference, &fetcher).expect("prepare");
1384 assert_eq!(evidence.selected_version, "1.2.0");
1385 }
1386
1387 #[test]
1388 fn write_json_atomic_reports_serialize_failures() {
1389 #[derive(Debug)]
1390 struct Boom;
1391 impl serde::Serialize for Boom {
1392 fn serialize<S: serde::Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
1393 Err(serde::ser::Error::custom("boom"))
1394 }
1395 }
1396 let cache = unique_cache();
1397 let path = cache.root.join("refs").join("boom.json");
1398 let failure = write_json_atomic(&path, &Boom).expect_err("serialize");
1399 assert_eq!(failure.code, RegistryCacheErrorCode::RegistryPrepareFailed);
1400 }
1401
1402 #[test]
1403 fn write_json_atomic_rejects_paths_without_parent() {
1404 let failure = write_json_atomic(Path::new("/"), &serde_json::json!({"ok": true}))
1405 .expect_err("root path");
1406 assert_eq!(failure.code, RegistryCacheErrorCode::RegistryPrepareFailed);
1407 assert!(failure.message.contains("no parent directory"));
1408 }
1409
1410 #[cfg(unix)]
1411 #[test]
1412 fn write_verified_bytes_reports_rename_failures_on_readonly_parent() {
1413 use std::os::unix::fs::PermissionsExt;
1414 let cache = unique_cache();
1415 let bytes = b"rename-readonly";
1416 let digest = digest_for(bytes);
1417 let hex = normalize_digest(&digest).expect("hex");
1418 let path = cache.artifact_path(&hex);
1419 let parent = path.parent().expect("parent").to_path_buf();
1420 fs::create_dir_all(&parent).expect("parent");
1421 let temporary = path.with_extension("tmp");
1422 fs::write(&temporary, bytes).expect("tmp");
1423 let mut perms = fs::metadata(&parent).expect("meta").permissions();
1424 perms.set_mode(0o555);
1425 fs::set_permissions(&parent, perms).expect("readonly");
1426 let failure = write_verified_bytes(&cache, &hex, bytes).expect_err("rename");
1427 let mut perms = fs::metadata(&parent).expect("meta").permissions();
1428 perms.set_mode(0o755);
1429 fs::set_permissions(&parent, perms).expect("restore");
1430 assert_eq!(failure.code, RegistryCacheErrorCode::RegistryPrepareFailed);
1431 }
1432
1433 #[test]
1434 fn offline_resolve_rejects_invalid_digest_shapes_in_pointer() {
1435 let cache = unique_cache();
1436 let reference = RegistryReference {
1437 namespace: "demo".to_string(),
1438 id: "greet".to_string(),
1439 version_range: "^1.0.0".to_string(),
1440 };
1441 let ref_path = cache.ref_path(&reference);
1442 fs::create_dir_all(ref_path.parent().expect("parent")).expect("dirs");
1443 fs::write(
1444 &ref_path,
1445 br#"{"artifact_digest":"bad","contract_digest":"bad"}"#,
1446 )
1447 .expect("write");
1448 let failure = resolve_offline(&cache, &reference).expect_err("bad digests");
1449 assert_eq!(
1450 failure.code,
1451 RegistryCacheErrorCode::RegistryCacheEntryMissing
1452 );
1453 }
1454
1455 #[test]
1456 fn resolve_component_rejects_non_contract_json_with_matching_digest() {
1457 let cache = unique_cache();
1458 let bogus = br#"{"kind":"not-a-capability-contract"}"#.to_vec();
1459 let artifact = b"wasm-bytes".to_vec();
1460 let artifact_digest = digest_for(&artifact);
1461 let contract_digest = digest_for(&bogus);
1462 let record = PublicRegistryCapabilityRecord {
1463 namespace: "demo".to_string(),
1464 id: "greet".to_string(),
1465 version: "1.0.0".to_string(),
1466 digest: artifact_digest,
1467 artifact_url: "https://example.test/greet.wasm".to_string(),
1468 contract_digest,
1469 contract_url: "https://example.test/greet.json".to_string(),
1470 deprecated: false,
1471 summary: String::new(),
1472 description: String::new(),
1473 use_cases: Vec::new(),
1474 service_type: String::new(),
1475 permitted_targets: Vec::new(),
1476 lifecycle: String::new(),
1477 provenance: None,
1478 };
1479 let snapshot = SyncedPublicRegistryState {
1480 schema_version: "1".to_string(),
1481 workspace_id: "ws".to_string(),
1482 state_scope: "public".to_string(),
1483 source_repo: "traverse-framework/registry".to_string(),
1484 release_tag: "index-v9".to_string(),
1485 index_version: 9,
1486 generated_at: "2026-07-29T00:00:00Z".to_string(),
1487 source_commit: None,
1488 synced_at: "2026-07-29T00:00:00Z".to_string(),
1489 record_count: 1,
1490 validation_status: "valid".to_string(),
1491 governing_spec: "055-registry-sync".to_string(),
1492 capabilities: vec![record.clone()],
1493 events: Vec::new(),
1494 };
1495 let mut assets = HashMap::new();
1496 assets.insert(record.artifact_url.clone(), artifact);
1497 assets.insert(record.contract_url.clone(), bogus);
1498 let fetcher = MapFetcher { assets };
1499 let reference = RegistryReference {
1500 namespace: "demo".to_string(),
1501 id: "greet".to_string(),
1502 version_range: "1.0.0".to_string(),
1503 };
1504 prepare(&cache, &snapshot, &reference, &fetcher).expect("prepare");
1505 let failure = resolve_component(&cache, &reference).expect_err("parse");
1506 assert_eq!(failure.code, RegistryCacheErrorCode::RegistryPrepareFailed);
1507 }
1508
1509 #[test]
1510 fn resolve_component_rejects_non_utf8_contract_bytes() {
1511 let cache = unique_cache();
1512 let bogus = vec![0xff, 0xfe, 0xfd];
1513 let artifact = b"wasm-bytes".to_vec();
1514 let artifact_digest = digest_for(&artifact);
1515 let contract_digest = digest_for(&bogus);
1516 let record = PublicRegistryCapabilityRecord {
1517 namespace: "demo".to_string(),
1518 id: "greet".to_string(),
1519 version: "1.0.0".to_string(),
1520 digest: artifact_digest,
1521 artifact_url: "https://example.test/greet.wasm".to_string(),
1522 contract_digest,
1523 contract_url: "https://example.test/greet.json".to_string(),
1524 deprecated: false,
1525 summary: String::new(),
1526 description: String::new(),
1527 use_cases: Vec::new(),
1528 service_type: String::new(),
1529 permitted_targets: Vec::new(),
1530 lifecycle: String::new(),
1531 provenance: None,
1532 };
1533 let snapshot = SyncedPublicRegistryState {
1534 schema_version: "1".to_string(),
1535 workspace_id: "ws".to_string(),
1536 state_scope: "public".to_string(),
1537 source_repo: "traverse-framework/registry".to_string(),
1538 release_tag: "index-v9".to_string(),
1539 index_version: 9,
1540 generated_at: "2026-07-29T00:00:00Z".to_string(),
1541 source_commit: None,
1542 synced_at: "2026-07-29T00:00:00Z".to_string(),
1543 record_count: 1,
1544 validation_status: "valid".to_string(),
1545 governing_spec: "055-registry-sync".to_string(),
1546 capabilities: vec![record.clone()],
1547 events: Vec::new(),
1548 };
1549 let mut assets = HashMap::new();
1550 assets.insert(record.artifact_url.clone(), artifact);
1551 assets.insert(record.contract_url.clone(), bogus);
1552 let fetcher = MapFetcher { assets };
1553 let reference = RegistryReference {
1554 namespace: "demo".to_string(),
1555 id: "greet".to_string(),
1556 version_range: "1.0.0".to_string(),
1557 };
1558 prepare(&cache, &snapshot, &reference, &fetcher).expect("prepare");
1559 let failure = resolve_component(&cache, &reference).expect_err("utf8");
1560 assert_eq!(failure.code, RegistryCacheErrorCode::RegistryPrepareFailed);
1561 }
1562
1563 #[test]
1564 fn public_metadata_round_trip_is_sanitized_and_can_be_stale() {
1565 let cache = unique_cache();
1566 let (mut snapshot, _, _) = sample_snapshot(false);
1567 let record = snapshot.capabilities.last_mut().expect("record");
1568 record.summary = "Greeting capability".to_string();
1569 record.description = "Greets a public user".to_string();
1570 record.use_cases = vec![traverse_registry::PublicUseCaseSummary {
1571 scenario: "Greet a new user".to_string(),
1572 }];
1573 publish_public_metadata(&cache, &snapshot, true).expect("publish");
1574 let generation = read_public_metadata(&cache).expect("read");
1575 assert!(generation.stale);
1576 assert_eq!(generation.source_release, snapshot.release_tag);
1577 assert!(!generation.index_digest.is_empty());
1578 assert!(
1579 generation
1580 .records
1581 .iter()
1582 .any(|record| record.description == "Greets a public user"
1583 && record.scenarios == ["Greet a new user"])
1584 );
1585 let encoded = fs::read(cache.public_metadata_path()).expect("generation");
1586 let text = String::from_utf8(encoded).expect("utf8");
1587 assert!(!text.contains("input_example"));
1588 assert!(!text.contains("output_example"));
1589 }
1590
1591 #[test]
1592 fn public_metadata_rejects_tampered_generation_binding() {
1593 let cache = unique_cache();
1594 let (snapshot, _, _) = sample_snapshot(false);
1595 publish_public_metadata(&cache, &snapshot, false).expect("publish");
1596 let path = cache.public_metadata_path();
1597 let mut generation: Value =
1598 serde_json::from_slice(&fs::read(&path).expect("read")).expect("json");
1599 generation["records"][0]["index_digest"] =
1600 json!("sha256:0000000000000000000000000000000000000000000000000000000000000000");
1601 write_json_atomic(&path, &generation).expect("tamper");
1602 assert_eq!(
1603 read_public_metadata(&cache).expect_err("invalid").code,
1604 RegistryCacheErrorCode::RegistryMetadataCacheInvalid
1605 );
1606 }
1607
1608 #[test]
1609 fn public_metadata_rejects_empty_snapshots_and_has_stable_error_code() {
1610 let cache = unique_cache();
1611 let (mut snapshot, _, _) = sample_snapshot(false);
1612 snapshot.capabilities.clear();
1613 assert_eq!(
1614 publish_public_metadata(&cache, &snapshot, false)
1615 .expect_err("empty")
1616 .code,
1617 RegistryCacheErrorCode::RegistrySyncMissing
1618 );
1619 assert_eq!(
1620 RegistryCacheErrorCode::RegistryMetadataCacheInvalid.as_str(),
1621 "registry_metadata_cache_invalid"
1622 );
1623 }
1624
1625 #[test]
1626 fn public_metadata_fails_closed_for_missing_and_malformed_generations() {
1627 let cache = unique_cache();
1628 assert_eq!(
1629 read_public_metadata(&cache).expect_err("missing").code,
1630 RegistryCacheErrorCode::RegistrySyncMissing
1631 );
1632 let path = cache.public_metadata_path();
1633 fs::create_dir_all(path.parent().expect("parent")).expect("directory");
1634 fs::write(&path, b"not json").expect("malformed generation");
1635 assert_eq!(
1636 read_public_metadata(&cache).expect_err("malformed").code,
1637 RegistryCacheErrorCode::RegistryMetadataCacheInvalid
1638 );
1639 }
1640}