Skip to main content

miden_package_registry_local/
lib.rs

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/// The error raised when operations on a [LocalPackageRegistry] fail
22#[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
83/// A [PackageRegistry] implementation that uses the local filesystem for storage of:
84///
85/// * The package index, as a TOML manifest, written to `$MIDEN_SYSROOT/etc/registry/index.toml`
86/// * The package artifacts (i.e. `.masp` files) of registered packages, stored under
87///   `$MIDEN_SYSROOT/lib`.
88///
89/// The index is associated with a specific toolchain for now, as it makes integration into
90/// `midenup` easier, and we're still at a stage where having a clean slate when switching
91/// to a new toolchain prevents confusing errors.
92///
93/// TODO(pauls): In the future, we should move the registry to a toolchain-agnostic location, and
94/// make registry operations global.
95pub struct LocalPackageRegistry {
96    index_path: PathBuf,
97    artifact_dir: PathBuf,
98    index: InMemoryPackageRegistry,
99    index_checksum: [u8; 32],
100}
101
102/// The metadata about a package produced when listing or describing a package in the index
103#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
104pub struct PackageSummary {
105    /// The package identifier/name
106    pub name: PackageId,
107    /// The package semantic version and digest
108    pub version: Version,
109    /// The package description
110    pub description: Option<Arc<str>>,
111    /// The version requirements for dependencies of this package
112    pub dependencies: BTreeMap<PackageId, VersionRequirement>,
113    /// The location of the assembled artifact on disk.
114    ///
115    /// If `None`, the package has been registered virtually, and so has no location on disk.
116    pub artifact_path: Option<PathBuf>,
117}
118
119/// A succinct summary of a package, produced when it is published to the registry.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct PublishedPackage {
122    pub name: PackageId,
123    pub version: Version,
124    pub artifact_path: PathBuf,
125}
126
127/// The representation of the on-disk package index
128#[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    /// Create a new [LocalPackageRegistry], by deriving the index and artifact storage locations
142    /// from the `$MIDEN_SYSROOT` environment variable.
143    ///
144    /// This produces an error if the environment variable is unset, or the index fails to load.
145    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    /// Create a new [LocalPackageRegistry], specifying the locations of the registry index, and
157    /// artifact storage.
158    ///
159    /// Requirements:
160    ///
161    /// * The `index_path` must be a file path to the index manifest, which is a TOML file.
162    /// * The `artifact_dir` must be a directory path.
163    ///
164    /// This produces an error if the paths are invalid, or the index fails to load.
165    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                // Acquire a non-exclusive lock on the file for reading, but return an error if
179                // there is an outstanding exclusive lock on the file already.
180                //
181                // This will fail if a handle to the index file has an exclusive lock on it for
182                // writing, see `save` for details.
183                //
184                // Multiple readers can hold this type of lock simultaneously, but a shared lock
185                // cannot be acquired in the presence of an exclusive lock, and vice versa.
186                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    /// Publish the Miden package found at `package_path`.
212    ///
213    /// The provided path must be a path to a `.masp` file (or at least a file containing a valid
214    /// Miden package).
215    ///
216    /// Returns an error if the package cannot be found, is invalid, or cannot be written to the
217    /// artifact store of the registry.
218    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    /// List all of the packages indexed by this registry
235    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    /// Get the package summary for the given package and version.
248    ///
249    /// If no version is specified, the latest version will be returned.
250    ///
251    /// If the package is not indexed, or the specified version cannot be found, this returns `None`
252    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    /// Get the path in the artifact store for `package` at `version`
270    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    /// Loads the given version of `package` from the artifact store.
301    ///
302    /// Returns an error if the artifact cannot be loaded, or is unknown to the registry.
303    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        // Acquire an exclusive lock for writing the index file, and return an error if we cannot
371        // obtain one due to any other outstanding lock on the file.
372        //
373        // This will fail if another write is being performed on the same file, or if the index is
374        // currently being loaded by another process.
375        //
376        // See `load` for the non-exclusive lock obtained for reads
377        file.try_lock().map_err(LocalRegistryError::IndexWriteLock)?;
378
379        // Validate that the contents of the persisted index have not changed under us, by
380        // recomputing the checksum of its contents and comparing to when we last loaded the index.
381        #[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        // Compute the new checksum of the updated index contents before we write, but do not
394        // update the in-memory state until we've successfully persisted the index
395        let new_checksum = miden_core::crypto::hash::Sha256::hash(contents.as_bytes().trim_ascii());
396
397        // Truncate the file to ensure that if the new index is smaller than the old one, that
398        // we don't end up with a corrupted index.
399        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        // Update the index checksum for the next write
404        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    /// Derive the path in the artifact store for a package with `digest`
491    ///
492    /// The file name includes the package name and semantic version to avoid collisions between
493    /// package identities that share the same underlying MAST digest.
494    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    /// Derive the artifact path used before filenames included package identity.
510    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    /// Construct the [PackageSummary] for the given package version
516    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    /// Publish `package`, with `bytes` representing the serialized form of `package` which
620    /// determines its provenance, i.e. if we deserialized `package` from `bytes`, then `bytes`
621    /// is those exact bytes, not the bytes we would get by serializing `package`, which might
622    /// differ from the original bytes.
623    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        // Write the package artifact to the registry
643        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        // Persist the updated registry index and artifact under the index write lock.
652        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        // This write succeeds because the index is not yet stale, but this write will make it
1107        // stale
1108        first_registry.publish(&first_path).unwrap();
1109        // This write will fail because it's view of the index is now stale
1110        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}