1use std::{
2 collections::BTreeMap,
3 env, fs,
4 io::{Read, Seek, Write},
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use miden_assembly_syntax::Report;
10use miden_core::{
11 serde::{Deserializable, Serializable},
12 utils::DisplayHex,
13};
14use miden_mast_package::Package as MastPackage;
15use miden_package_registry::{
16 InMemoryPackageRegistry, PackageCache, PackageId, PackageIndex, PackageProvider, PackageRecord,
17 PackageRegistry, PackageStore, PackageVersions, Version, VersionRequirement,
18};
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, thiserror::Error)]
23pub enum LocalRegistryError {
24 #[error("missing required environment variable '{var}'")]
25 MissingEnv { var: &'static str },
26 #[error("failed to read registry index: {0}")]
27 IndexRead(#[source] std::io::Error),
28 #[error("failed to seek in registry index stream: {0}")]
29 IndexSeek(#[source] std::io::Error),
30 #[error("failed to lock registry index for reading: {0}")]
31 IndexReadLock(#[source] fs::TryLockError),
32 #[error("failed to write registry index: {0}")]
33 IndexWrite(#[source] std::io::Error),
34 #[error("failed to lock registry index for writing: {0}")]
35 IndexWriteLock(#[source] fs::TryLockError),
36 #[error(
37 "failed to write registry index: the index was modified by another process, please try again"
38 )]
39 WriteToStaleIndex,
40 #[error("failed to parse registry index: {0}")]
41 IndexParse(#[from] toml::de::Error),
42 #[error("failed to serialize registry index: {0}")]
43 IndexSerialize(#[from] toml::ser::Error),
44 #[error("failed to decode package artifact '{path}': {error}")]
45 PackageDecode { path: PathBuf, error: String },
46 #[error("package artifact '{path}' is missing semantic version metadata")]
47 MissingPackageVersion { path: PathBuf },
48 #[error("package '{package}' version '{version}' is already registered")]
49 DuplicateSemanticVersion {
50 package: PackageId,
51 version: miden_package_registry::SemVer,
52 },
53 #[error("package '{package}' with version '{version}' is not present in the registry")]
54 MissingPackage { package: PackageId, version: Version },
55 #[error("package '{package}' version '{version}' has no artifact digest")]
56 MissingArtifactDigest { package: PackageId, version: Version },
57 #[error("package artifact for '{package}' version '{version}' was not found at '{path}'")]
58 MissingArtifact {
59 package: PackageId,
60 version: Version,
61 path: PathBuf,
62 },
63 #[error(
64 "package artifact at '{path}' does not match requested package '{expected_package}' version '{expected_version}' (found '{actual_package}' version '{actual_version}')"
65 )]
66 ArtifactMismatch {
67 path: PathBuf,
68 expected_package: PackageId,
69 expected_version: Box<Version>,
70 actual_package: PackageId,
71 actual_version: Box<Version>,
72 },
73 #[error(
74 "package '{package}' depends on unpublished package '{dependency}' with version '{version}'"
75 )]
76 MissingDependency {
77 package: PackageId,
78 dependency: PackageId,
79 version: Version,
80 },
81}
82
83pub struct LocalPackageRegistry {
96 index_path: PathBuf,
97 artifact_dir: PathBuf,
98 index: InMemoryPackageRegistry,
99 index_checksum: [u8; 32],
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
104pub struct PackageSummary {
105 pub name: PackageId,
107 pub version: Version,
109 pub description: Option<Arc<str>>,
111 pub dependencies: BTreeMap<PackageId, VersionRequirement>,
113 pub artifact_path: Option<PathBuf>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct PublishedPackage {
122 pub name: PackageId,
123 pub version: Version,
124 pub artifact_path: PathBuf,
125}
126
127#[derive(Default, Serialize, Deserialize)]
129struct PersistedIndex {
130 #[serde(default)]
131 packages: BTreeMap<PackageId, PackageVersions>,
132}
133
134impl Default for LocalPackageRegistry {
135 fn default() -> Self {
136 Self::load_from_env().expect("could not create a default instance of the package registry")
137 }
138}
139
140impl LocalPackageRegistry {
141 pub fn load_from_env() -> Result<Self, LocalRegistryError> {
146 let sysroot = PathBuf::from(
147 env::var_os("MIDEN_SYSROOT")
148 .ok_or(LocalRegistryError::MissingEnv { var: "MIDEN_SYSROOT" })?,
149 );
150
151 let index_path = sysroot.join("etc").join("registry").join("index.toml");
152 let artifact_dir = sysroot.join("lib");
153 Self::load(index_path, artifact_dir)
154 }
155
156 pub fn load(index_path: PathBuf, artifact_dir: PathBuf) -> Result<Self, LocalRegistryError> {
166 if let Some(parent) = index_path.parent() {
167 fs::create_dir_all(parent).map_err(LocalRegistryError::IndexWrite)?;
168 }
169 fs::create_dir_all(&artifact_dir).map_err(LocalRegistryError::IndexWrite)?;
170
171 let index_checksum: [u8; 32];
172 let index = if index_path.exists() {
173 let mut contents = String::with_capacity(4 * 1024);
174 #[allow(clippy::verbose_file_reads)]
175 {
176 let mut file =
177 fs::File::open(&index_path).map_err(LocalRegistryError::IndexRead)?;
178 file.try_lock_shared().map_err(LocalRegistryError::IndexReadLock)?;
187 file.read_to_string(&mut contents).map_err(LocalRegistryError::IndexRead)?;
188 }
189 let contents = contents.trim();
190 index_checksum =
191 *miden_core::crypto::hash::Sha256::hash(contents.as_bytes()).as_bytes();
192 if contents.is_empty() {
193 InMemoryPackageRegistry::default()
194 } else {
195 let persisted = toml::from_str::<PersistedIndex>(contents)?;
196 InMemoryPackageRegistry::from_packages(persisted.packages)
197 }
198 } else {
199 index_checksum = *miden_core::crypto::hash::Sha256::hash(&[]).as_bytes();
200 InMemoryPackageRegistry::default()
201 };
202
203 Ok(Self {
204 index_path,
205 artifact_dir,
206 index,
207 index_checksum,
208 })
209 }
210
211 pub fn publish(
219 &mut self,
220 package_path: impl AsRef<Path>,
221 ) -> Result<PublishedPackage, LocalRegistryError> {
222 let package_path = package_path.as_ref();
223 let bytes = fs::read(package_path).map_err(LocalRegistryError::IndexRead)?;
224 let package = MastPackage::read_from_bytes(&bytes).map_err(|error| {
225 LocalRegistryError::PackageDecode {
226 path: package_path.to_path_buf(),
227 error: error.to_string(),
228 }
229 })?;
230
231 self.publish_package_with_bytes(Arc::new(package), bytes)
232 }
233
234 pub fn list(&self) -> Vec<PackageSummary> {
236 self.index
237 .packages()
238 .iter()
239 .flat_map(|(name, versions)| {
240 versions.values().map(|record| {
241 self.package_summary(name.clone(), record.version().clone(), record)
242 })
243 })
244 .collect()
245 }
246
247 pub fn show(&self, package: &PackageId, version: Option<&Version>) -> Option<PackageSummary> {
253 let (version, record) = match version {
254 Some(version) => self
255 .index
256 .get_by_version(package, version)
257 .map(|record| (record.version().clone(), record))?,
258 None => self
259 .index
260 .available_versions(package)?
261 .values()
262 .next_back()
263 .map(|record| (record.version().clone(), record))?,
264 };
265
266 Some(self.package_summary(package.clone(), version, record))
267 }
268
269 pub fn artifact_path(&self, package: &PackageId, version: &Version) -> Option<PathBuf> {
271 self.artifact_path_for_summary(package, version)
272 }
273
274 fn artifact_path_for_summary(&self, package: &PackageId, version: &Version) -> Option<PathBuf> {
275 let digest = *version.digest.as_ref()?;
276 let artifact_path = self.artifact_path_for_package(package, &version.version, digest);
277 if artifact_path.exists() {
278 return Some(artifact_path);
279 }
280
281 let legacy_path = self.legacy_artifact_path_for_digest(digest);
282 if self.legacy_artifact_matches(package, version, &legacy_path) {
283 Some(legacy_path)
284 } else {
285 Some(artifact_path)
286 }
287 }
288
289 fn legacy_artifact_matches(&self, package: &PackageId, version: &Version, path: &Path) -> bool {
290 let Ok(bytes) = fs::read(path) else {
291 return false;
292 };
293 let Ok(loaded) = MastPackage::read_from_bytes(&bytes) else {
294 return false;
295 };
296 let actual_version = Version::new(loaded.version.clone(), loaded.digest());
297 loaded.name == *package && actual_version == *version
298 }
299
300 pub fn load_package(
304 &self,
305 package: &PackageId,
306 version: &Version,
307 ) -> Result<Arc<MastPackage>, LocalRegistryError> {
308 self.index.get_by_version(package, version).ok_or_else(|| {
309 LocalRegistryError::MissingPackage {
310 package: package.clone(),
311 version: version.clone(),
312 }
313 })?;
314
315 let digest = version.digest.ok_or_else(|| LocalRegistryError::MissingArtifactDigest {
316 package: package.clone(),
317 version: version.clone(),
318 })?;
319 let path = self.artifact_path_for_package(package, &version.version, digest);
320 let path = if path.exists() {
321 path
322 } else {
323 let legacy_path = self.legacy_artifact_path_for_digest(digest);
324 if legacy_path.exists() {
325 legacy_path
326 } else {
327 return Err(LocalRegistryError::MissingArtifact {
328 package: package.clone(),
329 version: version.clone(),
330 path,
331 });
332 }
333 };
334
335 let bytes = fs::read(&path).map_err(LocalRegistryError::IndexRead)?;
336 let loaded = MastPackage::read_from_bytes(&bytes).map_err(|error| {
337 LocalRegistryError::PackageDecode {
338 path: path.clone(),
339 error: error.to_string(),
340 }
341 })?;
342
343 let actual_version = Version::new(loaded.version.clone(), loaded.digest());
344 if loaded.name != *package || actual_version != *version {
345 return Err(LocalRegistryError::ArtifactMismatch {
346 path,
347 expected_package: package.clone(),
348 expected_version: Box::new(version.clone()),
349 actual_package: loaded.name,
350 actual_version: Box::new(actual_version),
351 });
352 }
353
354 Ok(Arc::new(loaded))
355 }
356
357 fn save_with_locked_operation(
358 &mut self,
359 operation: impl FnOnce() -> Result<(), LocalRegistryError>,
360 ) -> Result<(), LocalRegistryError> {
361 let persisted = PersistedIndex { packages: self.index.packages().clone() };
362 let contents = toml::to_string_pretty(&persisted)?;
363 let mut file = fs::File::options()
364 .read(true)
365 .write(true)
366 .truncate(false)
367 .create(true)
368 .open(&self.index_path)
369 .map_err(LocalRegistryError::IndexWrite)?;
370 file.try_lock().map_err(LocalRegistryError::IndexWriteLock)?;
378
379 #[allow(clippy::verbose_file_reads)]
382 {
383 let mut prev_contents = Vec::with_capacity(1024);
384 file.read_to_end(&mut prev_contents).map_err(LocalRegistryError::IndexRead)?;
385 let checksum = miden_core::crypto::hash::Sha256::hash(prev_contents.trim_ascii());
386 if &self.index_checksum != checksum.as_bytes() {
387 return Err(LocalRegistryError::WriteToStaleIndex);
388 }
389 }
390
391 operation()?;
392
393 let new_checksum = miden_core::crypto::hash::Sha256::hash(contents.as_bytes().trim_ascii());
396
397 file.rewind().map_err(LocalRegistryError::IndexSeek)?;
400 file.set_len(0).map_err(LocalRegistryError::IndexWrite)?;
401 file.write_all(contents.as_bytes()).map_err(LocalRegistryError::IndexWrite)?;
402
403 self.index_checksum = *new_checksum.as_bytes();
405
406 Ok(())
407 }
408
409 fn register_and_save_with_locked_operation(
410 &mut self,
411 name: PackageId,
412 record: PackageRecord,
413 operation: impl FnOnce() -> Result<(), LocalRegistryError>,
414 ) -> Result<(), LocalRegistryError> {
415 let previous_packages = self.index.packages().clone();
416 self.register(name, record)?;
417 match self.save_with_locked_operation(operation) {
418 Ok(()) => Ok(()),
419 Err(error) => {
420 self.index = InMemoryPackageRegistry::from_packages(previous_packages);
421 Err(error)
422 },
423 }
424 }
425
426 fn write_cache_artifact_from_legacy_or_bytes(
427 artifact_path: &Path,
428 legacy_path: &Path,
429 package: &MastPackage,
430 version: &Version,
431 bytes: &[u8],
432 ) -> Result<(), LocalRegistryError> {
433 match fs::read(legacy_path) {
434 Ok(existing_bytes) => match MastPackage::read_from_bytes(&existing_bytes) {
435 Ok(existing_package) => {
436 let existing_version =
437 Version::new(existing_package.version.clone(), existing_package.digest());
438 if existing_package.name == package.name && existing_version == *version {
439 if &existing_package == package {
440 fs::write(artifact_path, existing_bytes)
441 .map_err(LocalRegistryError::IndexWrite)
442 } else {
443 Err(LocalRegistryError::DuplicateSemanticVersion {
444 package: package.name.clone(),
445 version: package.version.clone(),
446 })
447 }
448 } else {
449 fs::write(artifact_path, bytes).map_err(LocalRegistryError::IndexWrite)
450 }
451 },
452 Err(_) => fs::write(artifact_path, bytes).map_err(LocalRegistryError::IndexWrite),
453 },
454 Err(_) => fs::write(artifact_path, bytes).map_err(LocalRegistryError::IndexWrite),
455 }
456 }
457
458 fn repair_cache_artifact(
459 artifact_path: &Path,
460 legacy_path: &Path,
461 package: &MastPackage,
462 version: &Version,
463 bytes: &[u8],
464 ) -> Result<(), LocalRegistryError> {
465 match fs::read(artifact_path) {
466 Ok(existing_bytes) => match MastPackage::read_from_bytes(&existing_bytes) {
467 Ok(existing_package) if &existing_package == package => Ok(()),
468 Ok(_) => Err(LocalRegistryError::DuplicateSemanticVersion {
469 package: package.name.clone(),
470 version: package.version.clone(),
471 }),
472 Err(_) => Self::write_cache_artifact_from_legacy_or_bytes(
473 artifact_path,
474 legacy_path,
475 package,
476 version,
477 bytes,
478 ),
479 },
480 Err(_) => Self::write_cache_artifact_from_legacy_or_bytes(
481 artifact_path,
482 legacy_path,
483 package,
484 version,
485 bytes,
486 ),
487 }
488 }
489
490 fn artifact_path_for_package(
495 &self,
496 package: &PackageId,
497 version: &miden_package_registry::SemVer,
498 digest: miden_core::Word,
499 ) -> PathBuf {
500 let package_id = DisplayHex(package.as_bytes());
501 let semantic_version = version.to_string();
502 let semantic_version = DisplayHex(semantic_version.as_bytes());
503 let digest_bytes = digest.as_bytes();
504 let digest = DisplayHex::new(&digest_bytes);
505 let filename = format!("{package_id}-{semantic_version}-{digest:#}.masp");
506 self.artifact_dir.join(filename)
507 }
508
509 fn legacy_artifact_path_for_digest(&self, digest: miden_core::Word) -> PathBuf {
511 let filename = format!("{:#}.masp", DisplayHex::new(&digest.as_bytes()));
512 self.artifact_dir.join(filename)
513 }
514
515 fn package_summary(
517 &self,
518 name: PackageId,
519 version: Version,
520 record: &PackageRecord,
521 ) -> PackageSummary {
522 PackageSummary {
523 artifact_path: self.artifact_path_for_summary(&name, &version),
524 dependencies: record
525 .dependencies()
526 .iter()
527 .map(|(dependency, requirement)| (dependency.clone(), requirement.clone()))
528 .collect(),
529 description: record.description().cloned(),
530 name,
531 version,
532 }
533 }
534
535 fn record_for_package(
536 package: &MastPackage,
537 version: Version,
538 ) -> (PackageRecord, Vec<(PackageId, Version, VersionRequirement)>) {
539 let dependencies = package
540 .manifest
541 .dependencies()
542 .map(|dependency| {
543 let dependency_name = dependency.id().clone();
544 let dependency_version =
545 Version::new(dependency.version.clone(), dependency.digest);
546 (
547 dependency_name,
548 dependency_version.clone(),
549 VersionRequirement::Exact(dependency_version),
550 )
551 })
552 .collect::<Vec<_>>();
553
554 let record = match package.description.clone() {
555 Some(description) => PackageRecord::new(
556 version,
557 dependencies
558 .iter()
559 .map(|(name, _version, requirement)| (name.clone(), requirement.clone())),
560 )
561 .with_description(description),
562 None => PackageRecord::new(
563 version,
564 dependencies
565 .iter()
566 .map(|(name, _version, requirement)| (name.clone(), requirement.clone())),
567 ),
568 };
569
570 (record, dependencies)
571 }
572
573 fn cache_package_with_bytes(
574 &mut self,
575 package: Arc<MastPackage>,
576 bytes: Vec<u8>,
577 ) -> Result<PublishedPackage, LocalRegistryError> {
578 let digest = package.digest();
579 let version = Version::new(package.version.clone(), digest);
580 let (record, _dependencies) = Self::record_for_package(&package, version.clone());
581 let artifact_path = self.artifact_path_for_package(&package.name, &package.version, digest);
582
583 if let Some(existing) = self.index.get_by_semver(&package.name, &package.version) {
584 if existing.version() != &version || existing != &record {
585 return Err(LocalRegistryError::DuplicateSemanticVersion {
586 package: package.name.clone(),
587 version: package.version.clone(),
588 });
589 }
590
591 let legacy_path = self.legacy_artifact_path_for_digest(digest);
592 self.save_with_locked_operation(|| {
593 Self::repair_cache_artifact(
594 &artifact_path,
595 &legacy_path,
596 package.as_ref(),
597 &version,
598 &bytes,
599 )
600 })?;
601 return Ok(PublishedPackage {
602 name: package.name.clone(),
603 version,
604 artifact_path,
605 });
606 }
607
608 self.register_and_save_with_locked_operation(package.name.clone(), record, || {
609 fs::write(&artifact_path, bytes).map_err(LocalRegistryError::IndexWrite)
610 })?;
611
612 Ok(PublishedPackage {
613 name: package.name.clone(),
614 version,
615 artifact_path,
616 })
617 }
618
619 fn publish_package_with_bytes(
624 &mut self,
625 package: Arc<MastPackage>,
626 bytes: Vec<u8>,
627 ) -> Result<PublishedPackage, LocalRegistryError> {
628 let digest = package.digest();
629 let version = Version::new(package.version.clone(), digest);
630 let (record, dependencies) = Self::record_for_package(&package, version.clone());
631
632 for (dependency_name, dependency_version, _requirement) in dependencies.iter() {
633 if self.index.get_exact_version(dependency_name, dependency_version).is_none() {
634 return Err(LocalRegistryError::MissingDependency {
635 package: package.name.clone(),
636 dependency: dependency_name.clone(),
637 version: dependency_version.clone(),
638 });
639 }
640 }
641
642 let artifact_path = self.artifact_path_for_package(&package.name, &package.version, digest);
644 if self.index.get_by_semver(&package.name, &package.version).is_some() {
645 return Err(LocalRegistryError::DuplicateSemanticVersion {
646 package: package.name.clone(),
647 version: package.version.clone(),
648 });
649 }
650
651 self.register_and_save_with_locked_operation(package.name.clone(), record, || {
653 fs::write(&artifact_path, bytes).map_err(LocalRegistryError::IndexWrite)
654 })?;
655
656 Ok(PublishedPackage {
657 name: package.name.clone(),
658 version,
659 artifact_path,
660 })
661 }
662}
663
664impl PackageRegistry for LocalPackageRegistry {
665 fn available_versions(&self, package: &PackageId) -> Option<&PackageVersions> {
666 self.index.available_versions(package)
667 }
668}
669
670impl PackageIndex for LocalPackageRegistry {
671 type Error = LocalRegistryError;
672
673 fn register(&mut self, name: PackageId, record: PackageRecord) -> Result<(), Self::Error> {
674 let semver = record.semantic_version().clone();
675 self.index.insert_record(name.clone(), record).map_err(|_error| {
676 LocalRegistryError::DuplicateSemanticVersion { package: name, version: semver }
677 })
678 }
679}
680
681impl PackageProvider for LocalPackageRegistry {
682 fn load_package(
683 &self,
684 package: &PackageId,
685 version: &Version,
686 ) -> Result<Arc<MastPackage>, Report> {
687 Self::load_package(self, package, version).map_err(|error| Report::msg(error.to_string()))
688 }
689}
690
691impl PackageCache for LocalPackageRegistry {
692 type Error = LocalRegistryError;
693
694 fn cache_package(&mut self, package: Arc<MastPackage>) -> Result<Version, Self::Error> {
695 let bytes = package.to_bytes();
696 self.cache_package_with_bytes(package, bytes).map(|published| published.version)
697 }
698}
699
700impl PackageStore for LocalPackageRegistry {
701 fn publish_package(&mut self, package: Arc<MastPackage>) -> Result<Version, Self::Error> {
702 let bytes = package.to_bytes();
703 self.publish_package_with_bytes(package, bytes)
704 .map(|published| published.version)
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use miden_mast_package::{Dependency, Package, Section, SectionId, TargetType};
711 use tempfile::TempDir;
712
713 use super::*;
714
715 fn build_package<'a>(
716 name: &str,
717 version: &str,
718 dependencies: impl IntoIterator<Item = (&'a str, &'a str, TargetType, miden_core::Word)>,
719 ) -> Box<Package> {
720 Package::generate(
721 name.into(),
722 version.parse().unwrap(),
723 TargetType::Library,
724 dependencies.into_iter().map(|(name, version, kind, digest)| Dependency {
725 name: name.into(),
726 version: version.parse().unwrap(),
727 kind,
728 digest,
729 }),
730 )
731 }
732
733 fn load_registry(tempdir: &TempDir) -> LocalPackageRegistry {
734 let index_path = tempdir.path().join("midenup").join("registry").join("index.toml");
735 let artifact_dir = tempdir.path().join("sysroot").join("lib");
736 LocalPackageRegistry::load(index_path, artifact_dir).expect("failed to load registry")
737 }
738
739 #[test]
740 fn publish_persists_artifact_and_index() {
741 let tempdir = TempDir::new().unwrap();
742 let mut registry = load_registry(&tempdir);
743
744 let dep_path = tempdir.path().join("dep.masp");
745 let dep = build_package("dep", "1.0.0", []);
746 dep.write_to_file(&dep_path).unwrap();
747 registry.publish(&dep_path).expect("failed to publish dependency");
748
749 let package_path = tempdir.path().join("pkg.masp");
750 let package =
751 build_package("pkg", "2.0.0", [("dep", "1.0.0", TargetType::Library, dep.digest())]);
752 package.write_to_file(&package_path).unwrap();
753
754 let published = registry.publish(&package_path).expect("failed to publish package");
755 let artifact = &published.artifact_path;
756 assert!(artifact.exists());
757
758 let reloaded = load_registry(&tempdir);
759 let listed = reloaded.list();
760 assert_eq!(listed.len(), 2);
761
762 let shown = reloaded.show(&PackageId::from("pkg"), None).expect("missing package");
763 assert_eq!(shown.version.version, "2.0.0".parse().unwrap());
764 assert_eq!(shown.dependencies.len(), 1);
765 assert_eq!(shown.dependencies.keys().next().unwrap(), &PackageId::from("dep"));
766 assert_eq!(
767 shown.dependencies.values().next().unwrap().to_string(),
768 format!("1.0.0#{}", dep.digest())
769 );
770 }
771
772 #[test]
773 fn publish_rejects_missing_dependencies() {
774 let tempdir = TempDir::new().unwrap();
775 let mut registry = load_registry(&tempdir);
776
777 let package_path = tempdir.path().join("pkg.masp");
778 let package = build_package(
779 "pkg",
780 "1.0.0",
781 [(
782 "dep",
783 "1.0.0",
784 TargetType::Library,
785 miden_core::utils::hash_string_to_word("dep"),
786 )],
787 );
788 package.write_to_file(&package_path).unwrap();
789
790 let error = registry.publish(&package_path).expect_err("publish should fail");
791 assert!(matches!(error, LocalRegistryError::MissingDependency { .. }));
792 }
793
794 #[test]
795 fn cache_persists_packages_with_missing_dependencies() {
796 let tempdir = TempDir::new().unwrap();
797 let mut registry = load_registry(&tempdir);
798
799 let dependency_digest = miden_core::utils::hash_string_to_word("dep");
800 let package = build_package(
801 "pkg",
802 "1.0.0",
803 [("dep", "1.0.0", TargetType::Library, dependency_digest)],
804 );
805 let version = registry
806 .cache_package(Arc::from(package))
807 .expect("cache should accept unresolved dependencies");
808
809 let reloaded = load_registry(&tempdir);
810 let shown = reloaded
811 .show(&PackageId::from("pkg"), Some(&version))
812 .expect("cached package should be indexed");
813 assert_eq!(shown.dependencies.len(), 1);
814 assert!(reloaded.load_package(&PackageId::from("pkg"), &version).is_ok());
815 }
816
817 #[test]
818 fn cache_rejects_different_artifact_for_existing_exact_version() {
819 let tempdir = TempDir::new().unwrap();
820 let mut registry = load_registry(&tempdir);
821
822 let package_path = tempdir.path().join("pkg.masp");
823 build_package("pkg", "1.0.0", []).write_to_file(&package_path).unwrap();
824 let published = registry.publish(&package_path).unwrap();
825
826 let mut conflicting_package = build_package("pkg", "1.0.0", []);
827 conflicting_package
828 .sections
829 .push(Section::new(SectionId::custom("cache-test").unwrap(), Vec::from([1, 2, 3])));
830 assert_eq!(Some(conflicting_package.digest()), published.version.digest);
831
832 let error = registry
833 .cache_package(Arc::from(conflicting_package))
834 .expect_err("cache should reject conflicting package artifacts");
835 assert!(matches!(error, LocalRegistryError::DuplicateSemanticVersion { .. }));
836
837 let loaded = registry.load_package(&PackageId::from("pkg"), &published.version).unwrap();
838 assert_eq!(loaded.manifest.dependencies().count(), 0);
839 assert!(loaded.sections.is_empty());
840 }
841
842 #[test]
843 fn stale_cache_does_not_overwrite_artifact() {
844 let tempdir = TempDir::new().unwrap();
845 let mut stale_registry = load_registry(&tempdir);
846 let mut current_registry = load_registry(&tempdir);
847
848 let package_path = tempdir.path().join("pkg.masp");
849 build_package("pkg", "1.0.0", []).write_to_file(&package_path).unwrap();
850 let published = current_registry.publish(&package_path).unwrap();
851 let original_bytes = fs::read(&published.artifact_path).unwrap();
852
853 let mut conflicting_package = build_package("pkg", "1.0.0", []);
854 conflicting_package.sections.push(Section::new(
855 SectionId::custom("stale-cache-test").unwrap(),
856 Vec::from([1, 2, 3]),
857 ));
858 assert_eq!(Some(conflicting_package.digest()), published.version.digest);
859
860 let error = stale_registry
861 .cache_package(conflicting_package.into())
862 .expect_err("stale cache should fail before writing artifact bytes");
863 assert!(matches!(error, LocalRegistryError::WriteToStaleIndex));
864 assert_eq!(fs::read(&published.artifact_path).unwrap(), original_bytes);
865 }
866
867 #[test]
868 fn concurrent_cache_repair_does_not_overwrite_existing_artifact() {
869 let tempdir = TempDir::new().unwrap();
870 let mut registry = load_registry(&tempdir);
871
872 let package_path = tempdir.path().join("pkg.masp");
873 build_package("pkg", "1.0.0", []).write_to_file(&package_path).unwrap();
874 let published = registry.publish(&package_path).unwrap();
875 fs::remove_file(&published.artifact_path).unwrap();
876
877 let mut first_registry = load_registry(&tempdir);
878 let mut second_registry = load_registry(&tempdir);
879 let repaired_package = build_package("pkg", "1.0.0", []);
880 let repaired_bytes = repaired_package.to_bytes();
881 first_registry.cache_package(repaired_package.into()).unwrap();
882
883 let mut conflicting_package = build_package("pkg", "1.0.0", []);
884 conflicting_package.sections.push(Section::new(
885 SectionId::custom("cache-repair-race-test").unwrap(),
886 Vec::from([1, 2, 3]),
887 ));
888 assert_eq!(Some(conflicting_package.digest()), published.version.digest);
889 let error = second_registry
890 .cache_package(conflicting_package.into())
891 .expect_err("cache repair should revalidate the artifact under the index lock");
892 assert!(matches!(error, LocalRegistryError::DuplicateSemanticVersion { .. }));
893 assert_eq!(fs::read(&published.artifact_path).unwrap(), repaired_bytes);
894 }
895
896 #[test]
897 fn cache_repair_checks_legacy_artifact_before_writing_qualified_artifact() {
898 let tempdir = TempDir::new().unwrap();
899 let mut registry = load_registry(&tempdir);
900
901 let package_path = tempdir.path().join("pkg.masp");
902 build_package("pkg", "1.0.0", []).write_to_file(&package_path).unwrap();
903 let published = registry.publish(&package_path).unwrap();
904 let original_bytes = fs::read(&published.artifact_path).unwrap();
905 let legacy_path =
906 registry.legacy_artifact_path_for_digest(published.version.digest.unwrap());
907 fs::rename(&published.artifact_path, &legacy_path).unwrap();
908
909 let mut conflicting_package = build_package("pkg", "1.0.0", []);
910 conflicting_package.sections.push(Section::new(
911 SectionId::custom("legacy-cache-repair-test").unwrap(),
912 Vec::from([1, 2, 3]),
913 ));
914 assert_eq!(Some(conflicting_package.digest()), published.version.digest);
915
916 let error = registry
917 .cache_package(conflicting_package.into())
918 .expect_err("cache repair should reject conflicts with the legacy artifact");
919 assert!(matches!(error, LocalRegistryError::DuplicateSemanticVersion { .. }));
920 assert!(!published.artifact_path.exists());
921 assert_eq!(fs::read(&legacy_path).unwrap(), original_bytes);
922
923 let loaded = registry.load_package(&PackageId::from("pkg"), &published.version).unwrap();
924 assert!(loaded.sections.is_empty());
925 }
926
927 #[test]
928 fn cache_repair_checks_legacy_artifact_before_replacing_corrupt_qualified_artifact() {
929 let tempdir = TempDir::new().unwrap();
930 let mut registry = load_registry(&tempdir);
931
932 let package_path = tempdir.path().join("pkg.masp");
933 build_package("pkg", "1.0.0", []).write_to_file(&package_path).unwrap();
934 let published = registry.publish(&package_path).unwrap();
935 let original_bytes = fs::read(&published.artifact_path).unwrap();
936 let legacy_path =
937 registry.legacy_artifact_path_for_digest(published.version.digest.unwrap());
938 fs::write(&legacy_path, &original_bytes).unwrap();
939 fs::write(&published.artifact_path, b"not a package").unwrap();
940
941 let mut conflicting_package = build_package("pkg", "1.0.0", []);
942 conflicting_package.sections.push(Section::new(
943 SectionId::custom("legacy-cache-corrupt-qualified-test").unwrap(),
944 Vec::from([1, 2, 3]),
945 ));
946 assert_eq!(Some(conflicting_package.digest()), published.version.digest);
947
948 let error = registry
949 .cache_package(conflicting_package.into())
950 .expect_err("cache repair should reject conflicts with the legacy artifact");
951 assert!(matches!(error, LocalRegistryError::DuplicateSemanticVersion { .. }));
952 assert_eq!(fs::read(&legacy_path).unwrap(), original_bytes);
953 assert_eq!(fs::read(&published.artifact_path).unwrap(), b"not a package");
954 }
955
956 #[test]
957 fn list_and_show_include_multiple_versions() {
958 let tempdir = TempDir::new().unwrap();
959 let mut registry = load_registry(&tempdir);
960
961 let first_path = tempdir.path().join("pkg-1.masp");
962 let second_path = tempdir.path().join("pkg-2.masp");
963 build_package("pkg", "1.0.0", []).write_to_file(&first_path).unwrap();
964 build_package("pkg", "2.0.0", []).write_to_file(&second_path).unwrap();
965
966 registry.publish(&first_path).unwrap();
967 registry.publish(&second_path).unwrap();
968
969 let listed = registry.list();
970 assert_eq!(listed.len(), 2);
971
972 let latest = registry.show(&PackageId::from("pkg"), None).unwrap();
973 assert_eq!(latest.version.version, "2.0.0".parse().unwrap());
974
975 let exact =
976 registry.show(&PackageId::from("pkg"), Some(&"1.0.0".parse().unwrap())).unwrap();
977 assert_eq!(exact.version.version, "1.0.0".parse().unwrap());
978 }
979
980 #[test]
981 fn publish_rejects_duplicate_semantic_versions() {
982 let tempdir = TempDir::new().unwrap();
983 let mut registry = load_registry(&tempdir);
984
985 let first_path = tempdir.path().join("pkg-1.masp");
986 let second_path = tempdir.path().join("pkg-2.masp");
987 build_package("pkg", "1.0.0", []).write_to_file(&first_path).unwrap();
988 build_package("pkg", "1.0.0", []).write_to_file(&second_path).unwrap();
989
990 registry.publish(&first_path).unwrap();
991 let error = registry.publish(&second_path).expect_err("duplicate semver should fail");
992 assert!(matches!(error, LocalRegistryError::DuplicateSemanticVersion { .. }));
993 }
994
995 #[test]
996 fn publish_rejects_duplicate_semantic_versions_for_identical_bytes() {
997 let tempdir = TempDir::new().unwrap();
998 let mut registry = load_registry(&tempdir);
999
1000 let package_path = tempdir.path().join("pkg.masp");
1001 build_package("pkg", "1.0.0", []).write_to_file(&package_path).unwrap();
1002
1003 registry.publish(&package_path).unwrap();
1004 let error = registry
1005 .publish(&package_path)
1006 .expect_err("duplicate semver should fail even for identical bytes");
1007 assert!(matches!(error, LocalRegistryError::DuplicateSemanticVersion { .. }));
1008 }
1009
1010 #[test]
1011 fn publish_duplicate_semver_does_not_overwrite_artifact() {
1012 let tempdir = TempDir::new().unwrap();
1013 let mut registry = load_registry(&tempdir);
1014
1015 let package_path = tempdir.path().join("pkg.masp");
1016 build_package("pkg", "1.0.0", []).write_to_file(&package_path).unwrap();
1017 let published = registry.publish(&package_path).unwrap();
1018 let original_bytes = fs::read(&published.artifact_path).unwrap();
1019
1020 let mut conflicting_package = build_package("pkg", "1.0.0", []);
1021 conflicting_package
1022 .sections
1023 .push(Section::new(SectionId::custom("publish-test").unwrap(), Vec::from([1, 2, 3])));
1024 assert_eq!(Some(conflicting_package.digest()), published.version.digest);
1025 let conflicting_path = tempdir.path().join("pkg-conflicting.masp");
1026 conflicting_package.write_to_file(&conflicting_path).unwrap();
1027
1028 let error = registry
1029 .publish(&conflicting_path)
1030 .expect_err("duplicate semver should fail before writing artifact bytes");
1031 assert!(matches!(error, LocalRegistryError::DuplicateSemanticVersion { .. }));
1032 assert_eq!(fs::read(&published.artifact_path).unwrap(), original_bytes);
1033
1034 let loaded = registry.load_package(&PackageId::from("pkg"), &published.version).unwrap();
1035 assert!(loaded.sections.is_empty());
1036 }
1037
1038 #[test]
1039 fn stale_publish_duplicate_semver_does_not_overwrite_artifact() {
1040 let tempdir = TempDir::new().unwrap();
1041 let mut stale_registry = load_registry(&tempdir);
1042 let mut current_registry = load_registry(&tempdir);
1043
1044 let package_path = tempdir.path().join("pkg.masp");
1045 build_package("pkg", "1.0.0", []).write_to_file(&package_path).unwrap();
1046 let published = current_registry.publish(&package_path).unwrap();
1047 let original_bytes = fs::read(&published.artifact_path).unwrap();
1048
1049 let mut conflicting_package = build_package("pkg", "1.0.0", []);
1050 conflicting_package.sections.push(Section::new(
1051 SectionId::custom("stale-publish-test").unwrap(),
1052 Vec::from([1, 2, 3]),
1053 ));
1054 assert_eq!(Some(conflicting_package.digest()), published.version.digest);
1055 let conflicting_path = tempdir.path().join("pkg-conflicting.masp");
1056 conflicting_package.write_to_file(&conflicting_path).unwrap();
1057
1058 let error = stale_registry
1059 .publish(&conflicting_path)
1060 .expect_err("stale publish should fail before writing artifact bytes");
1061 assert!(matches!(error, LocalRegistryError::WriteToStaleIndex));
1062 assert_eq!(fs::read(&published.artifact_path).unwrap(), original_bytes);
1063 }
1064
1065 #[test]
1066 fn failed_publish_artifact_write_does_not_persist_index_on_later_save() {
1067 let tempdir = TempDir::new().unwrap();
1068 let mut registry = load_registry(&tempdir);
1069
1070 let package = build_package("pkg", "1.0.0", []);
1071 let package_path = tempdir.path().join("pkg.masp");
1072 package.write_to_file(&package_path).unwrap();
1073 let artifact_path =
1074 registry.artifact_path_for_package(&package.name, &package.version, package.digest());
1075 fs::create_dir(&artifact_path).unwrap();
1076
1077 let error = registry
1078 .publish(&package_path)
1079 .expect_err("artifact write should fail while the artifact path is a directory");
1080 assert!(matches!(error, LocalRegistryError::IndexWrite(_)));
1081 fs::remove_dir(&artifact_path).unwrap();
1082
1083 let other_path = tempdir.path().join("other.masp");
1084 build_package("other", "1.0.0", []).write_to_file(&other_path).unwrap();
1085 registry.publish(&other_path).expect("later publish should succeed");
1086
1087 let reloaded = load_registry(&tempdir);
1088 assert!(reloaded.show(&PackageId::from("pkg"), None).is_none());
1089 assert!(reloaded.show(&PackageId::from("other"), None).is_some());
1090 }
1091
1092 #[test]
1093 #[should_panic = "stale registry write failed"]
1094 fn publish_rejects_writes_to_a_stale_index() {
1095 let tempdir = TempDir::new().unwrap();
1096 let mut first_registry = load_registry(&tempdir);
1097 let mut stale_registry = load_registry(&tempdir);
1098
1099 let first_path = tempdir.path().join("a.masp");
1100 let second_path = tempdir.path().join("longer-package-name.masp");
1101 build_package("a", "1.0.0", []).write_to_file(&first_path).unwrap();
1102 build_package("longer-package-name", "1.0.0", [])
1103 .write_to_file(&second_path)
1104 .unwrap();
1105
1106 first_registry.publish(&first_path).unwrap();
1109 stale_registry.publish(&second_path).expect("stale registry write failed");
1111
1112 let reloaded = load_registry(&tempdir);
1113 assert!(reloaded.show(&PackageId::from("a"), None).is_some());
1114 assert!(reloaded.show(&PackageId::from("longer-package-name"), None).is_some());
1115 }
1116
1117 #[test]
1118 fn load_package_rejects_artifact_that_does_not_match_requested_identity() {
1119 let tempdir = TempDir::new().unwrap();
1120 let mut registry = load_registry(&tempdir);
1121
1122 let package_path = tempdir.path().join("pkg.masp");
1123 build_package("pkg", "1.0.0", []).write_to_file(&package_path).unwrap();
1124 let published = registry.publish(&package_path).unwrap();
1125
1126 build_package("other", "1.0.0", [])
1127 .write_to_file(&published.artifact_path)
1128 .unwrap();
1129
1130 let error = registry
1131 .load_package(&PackageId::from("pkg"), &published.version)
1132 .expect_err("artifact mismatch should be rejected");
1133 assert!(matches!(error, LocalRegistryError::ArtifactMismatch { .. }));
1134 }
1135
1136 #[test]
1137 fn load_package_accepts_legacy_digest_only_artifact_path() {
1138 let tempdir = TempDir::new().unwrap();
1139 let mut registry = load_registry(&tempdir);
1140
1141 let package = build_package("pkg", "1.0.0", []);
1142 let package_path = tempdir.path().join("pkg.masp");
1143 package.write_to_file(&package_path).unwrap();
1144 let published = registry.publish(&package_path).unwrap();
1145 let legacy_path =
1146 registry.legacy_artifact_path_for_digest(published.version.digest.unwrap());
1147 assert_ne!(published.artifact_path, legacy_path);
1148 fs::rename(&published.artifact_path, &legacy_path).unwrap();
1149
1150 let shown = registry.show(&PackageId::from("pkg"), Some(&published.version)).unwrap();
1151 assert_eq!(shown.artifact_path.as_ref(), Some(&legacy_path));
1152 assert_eq!(
1153 registry.artifact_path(&PackageId::from("pkg"), &published.version),
1154 Some(legacy_path.clone())
1155 );
1156 let listed = registry.list();
1157 assert_eq!(listed.len(), 1);
1158 assert_eq!(listed[0].artifact_path.as_ref(), Some(&legacy_path));
1159
1160 let loaded = registry.load_package(&PackageId::from("pkg"), &published.version).unwrap();
1161 assert_eq!(loaded.as_ref(), package.as_ref());
1162 }
1163
1164 #[test]
1165 fn summaries_do_not_report_legacy_artifact_for_different_package_identity() {
1166 let tempdir = TempDir::new().unwrap();
1167 let mut registry = load_registry(&tempdir);
1168
1169 let first = build_package("first", "1.0.0", []);
1170 let second = build_package("second", "1.0.0", []);
1171 assert_eq!(first.digest(), second.digest());
1172
1173 let first_path = tempdir.path().join("first.masp");
1174 let second_path = tempdir.path().join("second.masp");
1175 first.write_to_file(&first_path).unwrap();
1176 second.write_to_file(&second_path).unwrap();
1177
1178 let first = registry.publish(&first_path).unwrap();
1179 let second = registry.publish(&second_path).unwrap();
1180 let legacy_path = registry.legacy_artifact_path_for_digest(first.version.digest.unwrap());
1181 fs::rename(&first.artifact_path, &legacy_path).unwrap();
1182 fs::remove_file(&second.artifact_path).unwrap();
1183
1184 let first_summary = registry.show(&PackageId::from("first"), Some(&first.version)).unwrap();
1185 assert_eq!(first_summary.artifact_path.as_ref(), Some(&legacy_path));
1186 let second_summary =
1187 registry.show(&PackageId::from("second"), Some(&second.version)).unwrap();
1188 assert_eq!(second_summary.artifact_path.as_ref(), Some(&second.artifact_path));
1189 }
1190
1191 #[test]
1192 fn artifact_paths_distinguish_packages_with_same_mast_digest() {
1193 let tempdir = TempDir::new().unwrap();
1194 let mut registry = load_registry(&tempdir);
1195
1196 let first = build_package("first", "1.0.0", []);
1197 let second = build_package("second", "1.0.0", []);
1198 assert_eq!(first.digest(), second.digest());
1199
1200 let first_path = tempdir.path().join("first.masp");
1201 let second_path = tempdir.path().join("second.masp");
1202 first.write_to_file(&first_path).unwrap();
1203 second.write_to_file(&second_path).unwrap();
1204
1205 let first = registry.publish(&first_path).unwrap();
1206 let second = registry.publish(&second_path).unwrap();
1207 assert_ne!(first.artifact_path, second.artifact_path);
1208
1209 let first_loaded =
1210 registry.load_package(&PackageId::from("first"), &first.version).unwrap();
1211 let second_loaded =
1212 registry.load_package(&PackageId::from("second"), &second.version).unwrap();
1213 assert_eq!(first_loaded.name, PackageId::from("first"));
1214 assert_eq!(second_loaded.name, PackageId::from("second"));
1215 }
1216}