diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
| commit | 8cc1ba8a02c0318b65658f1634de378c780392b9 (patch) | |
| tree | fdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-registry/src/repository/mod.rs | |
| parent | 72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff) | |
| download | php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.gz php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.zst php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.zip | |
refactor(workspace): consolidate crates into mozart-core
Merged mozart-archiver, mozart-autoload, mozart-registry,
mozart-sat-resolver, and mozart-vcs into mozart-core to align
the source layout with Composer's structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry/src/repository/mod.rs')
| -rw-r--r-- | crates/mozart-registry/src/repository/mod.rs | 319 |
1 files changed, 0 insertions, 319 deletions
diff --git a/crates/mozart-registry/src/repository/mod.rs b/crates/mozart-registry/src/repository/mod.rs deleted file mode 100644 index 46f62f0..0000000 --- a/crates/mozart-registry/src/repository/mod.rs +++ /dev/null @@ -1,319 +0,0 @@ -//! Repository abstraction over package metadata sources. -//! -//! Mirrors Composer's `Composer\Repository\RepositoryInterface::loadPackages` -//! and `Composer\Repository\RepositoryManager`. The resolver and lockfile -//! generator query a [`RepositorySet`] instead of calling Packagist directly, -//! so test code can substitute a set without `PackagistRepository` (mirroring -//! Composer's `FactoryMock` injecting `repositories: ['packagist' => false]`). -//! -//! Concrete implementations live in sibling modules: [`packagist_repo`] for -//! the live Packagist HTTP repo, [`inline_package_repo`] for `type: package` -//! entries embedded in `composer.json`, and [`vcs_repo`] for VCS repositories. - -use std::collections::BTreeMap; - -use crate::advisory::{MatchedAdvisory, PackageInfo}; -use crate::packagist::{PackagistVersion, SearchResult}; - -pub mod inline_package_repo; -pub mod packagist_repo; -pub mod vcs_repo; - -/// Search modes for [`Repository::search`]. -/// -/// Mirrors Composer's `RepositoryInterface::SEARCH_FULLTEXT|SEARCH_NAME|SEARCH_VENDOR` -/// constants (`composer/src/Composer/Repository/RepositoryInterface.php`). -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum SearchMode { - /// Full-text search over name, description, and keywords (Packagist's - /// `search.json` API). - Fulltext, - /// Match the regex against package names. Tokens are split on whitespace - /// and joined as `(?:t1|t2|...)`; callers must pre-quote regex metachars. - Name, - /// Match the regex against vendor names. Result rows have only `name` - /// populated (the vendor part). - Vendor, -} - -/// One name-keyed lookup against a repository. -/// -/// Matches the `$packageNameMap` argument of Composer's `loadPackages`. The -/// constraint is informational — repositories may use it to skip versions -/// that obviously can't match (an optimization), but the resolver still -/// re-checks every returned version when generating rules. -#[derive(Debug, Clone)] -pub struct PackageQuery<'a> { - pub name: &'a str, - /// Raw constraint string from `composer.json`, e.g. `"^1.2"`. `None` - /// when the caller wants every version (transitive exploration). - pub constraint: Option<&'a str>, -} - -/// Result of a single [`Repository::load_packages`] call. -/// -/// Mirrors Composer's `['packages' => ..., 'namesFound' => ...]` tuple. -/// `names_found` lets [`RepositorySet`] short-circuit lower-priority repos -/// once an upstream repo has authoritatively answered for a name (Composer's -/// "first repo wins" semantics). -#[derive(Debug, Default)] -pub struct LoadResult { - pub packages: Vec<NamedPackagistVersion>, - pub names_found: Vec<String>, -} - -/// A `PackagistVersion` paired with the canonical package name it answers -/// for. Inline `type: package` repos can return packages whose own `name` -/// field differs from the queried name when they declare `replace`/`provide`, -/// so callers need both. -#[derive(Debug, Clone)] -pub struct NamedPackagistVersion { - pub name: String, - pub version: PackagistVersion, -} - -/// A source of package metadata. Mirrors Composer's `RepositoryInterface`. -/// -/// Implementations should return an empty [`LoadResult`] (not an error) when -/// they simply don't know a queried name — [`RepositorySet`] uses that to -/// fall through to the next repo. Reserve `Err` for genuine I/O failures -/// the caller cannot route around. -#[async_trait::async_trait] -pub trait Repository: Send + Sync { - /// Identifier for diagnostics (`"packagist.org"`, `"package"`, `"vcs:<url>"`). - fn id(&self) -> &str; - - /// Look up every version of every queried name this repo knows about. - async fn load_packages(&self, queries: &[PackageQuery<'_>]) -> anyhow::Result<LoadResult>; - - /// Search this repository. - /// - /// The default returns an empty result so repositories that don't - /// participate in search (e.g. inline / VCS repos that only resolve - /// known names) can opt out. Mirrors Composer's - /// `RepositoryInterface::search` whose default behavior on - /// `ArrayRepository` walks the in-memory list. - async fn search( - &self, - _query: &str, - _mode: SearchMode, - _package_type: Option<&str>, - ) -> anyhow::Result<Vec<SearchResult>> { - Ok(Vec::new()) - } -} - -/// Ordered list of repositories. Mirrors `Composer\Repository\RepositoryManager`. -/// -/// `load_packages` queries each repo in order. Once a repo authoritatively -/// answers for a name (i.e. lists it in `names_found`), later repos are not -/// asked about that name — matching Composer's first-repo-wins priority. -pub struct RepositorySet { - repos: Vec<Box<dyn Repository>>, -} - -impl RepositorySet { - pub fn new(repos: Vec<Box<dyn Repository>>) -> Self { - Self { repos } - } - - /// Production default: a single [`packagist_repo::PackagistRepository`] - /// backed by the given on-disk cache. Mirrors what Composer does when - /// no `'packagist' => false` entry appears in the merged config. - pub fn with_packagist(repo_cache: crate::cache::Cache) -> Self { - Self::new(vec![Box::new(packagist_repo::PackagistRepository::new( - repo_cache, - ))]) - } - - /// An empty set. Mirrors Composer's `'packagist' => false` test config: - /// resolution proceeds entirely from packages already in the pool - /// (eager VCS scan, inline `type: package` repos, the locked repository). - pub fn empty() -> Self { - Self::new(Vec::new()) - } - - pub fn is_empty(&self) -> bool { - self.repos.is_empty() - } - - pub fn len(&self) -> usize { - self.repos.len() - } - - /// Iterate over repositories in priority order. - pub fn repos(&self) -> impl Iterator<Item = &dyn Repository> { - self.repos.iter().map(|b| b.as_ref()) - } - - /// Query every repo, accumulating packages and tracking which names have - /// been authoritatively answered. Names already covered by an earlier - /// repo are dropped from the query passed to later repos. - pub async fn load_packages( - &self, - queries: &[PackageQuery<'_>], - ) -> anyhow::Result<Vec<NamedPackagistVersion>> { - use indexmap::IndexSet; - - let mut packages: Vec<NamedPackagistVersion> = Vec::new(); - let mut answered: IndexSet<String> = IndexSet::new(); - - for repo in &self.repos { - let pending: Vec<PackageQuery<'_>> = queries - .iter() - .filter(|q| !answered.contains(q.name)) - .cloned() - .collect(); - if pending.is_empty() { - break; - } - let result = repo.load_packages(&pending).await?; - for name in result.names_found { - answered.insert(name); - } - packages.extend(result.packages); - } - - Ok(packages) - } - - /// Fan-out search across every repository, concatenating results in - /// priority order. Mirrors Composer's - /// `CompositeRepository::search` which `array_merge`s per-repo results - /// without de-duplication. - pub async fn search( - &self, - query: &str, - mode: SearchMode, - package_type: Option<&str>, - ) -> anyhow::Result<Vec<SearchResult>> { - let mut all = Vec::new(); - for repo in &self.repos { - let mut hits = repo.search(query, mode, package_type).await?; - all.append(&mut hits); - } - Ok(all) - } - - /// Fetch security advisories matching the installed packages, with version filtering. - /// - /// Mirrors `Composer\Repository\RepositorySet::getMatchingSecurityAdvisories()`. - /// Returns the matched advisories (already filtered by installed version) and a list - /// of unreachable repository URLs. When `ignore_unreachable` is false and a repository - /// is unreachable, the error is propagated instead. - pub async fn get_matching_security_advisories( - &self, - packages: &[PackageInfo], - _allow_partial: bool, - ignore_unreachable: bool, - ) -> anyhow::Result<(BTreeMap<String, Vec<MatchedAdvisory>>, Vec<String>)> { - let names: Vec<&str> = packages.iter().map(|p| p.name.as_str()).collect(); - - let (raw_advisories, unreachable_repos) = - match crate::packagist::fetch_security_advisories(&names).await { - Ok(a) => (a, vec![]), - Err(e) if ignore_unreachable => { - tracing::warn!("Packagist advisory fetch failed (ignored): {e}"); - let unreachable = vec!["https://packagist.org".to_string()]; - (BTreeMap::new(), unreachable) - } - Err(e) => return Err(e), - }; - - let matched = version_filter_advisories(&raw_advisories, packages); - - Ok((matched, unreachable_repos)) - } -} - -/// Normalize single-pipe OR separators (`|`) in a version constraint string to -/// double-pipe (`||`) so the constraint parser can handle both forms. -/// -/// The Packagist security advisories API may return constraints with single `|` -/// as the OR separator (e.g. `>=1.0,<1.5|>=2.0,<2.3`), but Mozart's -/// `VersionConstraint::parse` expects `||`. -/// -/// TODO: fix `mozart_semver::VersionConstraint::parse` to accept single `|` and remove this. -fn normalize_or_separator(constraint: &str) -> String { - let bytes = constraint.as_bytes(); - let mut result = String::with_capacity(constraint.len() + 4); - let mut i = 0; - while i < bytes.len() { - if bytes[i] == b'|' { - if i + 1 < bytes.len() && bytes[i + 1] == b'|' { - result.push_str("||"); - i += 2; - } else { - result.push_str("||"); - i += 1; - } - } else { - result.push(bytes[i] as char); - i += 1; - } - } - result -} - -/// Filter raw advisories by installed package versions. -/// -/// Mirrors the version-matching step inside Composer's repository advisory fetch. -fn version_filter_advisories( - all_advisories: &BTreeMap<String, Vec<crate::packagist::SecurityAdvisory>>, - packages: &[PackageInfo], -) -> BTreeMap<String, Vec<MatchedAdvisory>> { - let mut result: BTreeMap<String, Vec<MatchedAdvisory>> = BTreeMap::new(); - - for pkg in packages { - let Some(advisories) = all_advisories.get(&pkg.name) else { - continue; - }; - - let version_str = pkg - .version_normalized - .as_deref() - .unwrap_or(pkg.version.as_str()); - - let installed_ver = match mozart_semver::Version::parse(version_str) { - Ok(v) => v, - Err(_) => { - tracing::warn!( - "Could not parse version {:?} for package {:?}, skipping advisory matching", - version_str, - pkg.name - ); - continue; - } - }; - - let mut matched: Vec<MatchedAdvisory> = Vec::new(); - - for advisory in advisories { - let normalized = normalize_or_separator(&advisory.affected_versions); - let constraint = match mozart_semver::VersionConstraint::parse(&normalized) { - Ok(c) => c, - Err(_) => { - tracing::warn!( - "Could not parse affected versions {:?} for advisory {:?}, skipping", - advisory.affected_versions, - advisory.advisory_id - ); - continue; - } - }; - - if constraint.matches(&installed_ver) { - matched.push(MatchedAdvisory { - advisory: advisory.clone(), - installed_version: pkg.version.clone(), - }); - } - } - - if !matched.is_empty() { - result.insert(pkg.name.clone(), matched); - } - } - - result -} |
