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 | |
| 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')
| -rw-r--r-- | crates/mozart-registry/src/repository/inline_package_repo.rs | 63 | ||||
| -rw-r--r-- | crates/mozart-registry/src/repository/mod.rs | 319 | ||||
| -rw-r--r-- | crates/mozart-registry/src/repository/packagist_repo.rs | 121 | ||||
| -rw-r--r-- | crates/mozart-registry/src/repository/vcs_repo.rs | 63 |
4 files changed, 0 insertions, 566 deletions
diff --git a/crates/mozart-registry/src/repository/inline_package_repo.rs b/crates/mozart-registry/src/repository/inline_package_repo.rs deleted file mode 100644 index 1043559..0000000 --- a/crates/mozart-registry/src/repository/inline_package_repo.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! [`Repository`] for inline `type: package` repositories. -//! -//! Wraps [`crate::inline_package::collect_inline_packages`]. The data is -//! embedded in `composer.json` so there's no I/O — the repo just filters -//! its in-memory list by queried name. -//! -//! Mirrors `Composer\Repository\PackageRepository` (which extends -//! `ArrayRepository`). Only the package's own `name` is matched against -//! queries — `replace`/`provide` targets are NOT advertised here, exactly -//! like Composer's `ArrayRepository::loadPackages` checks `getName()` only. -//! Replacement satisfaction happens later in the solver once the replacing -//! package is loaded transitively. - -use super::{LoadResult, NamedPackagistVersion, PackageQuery, Repository}; -use crate::inline_package::{InlinePackage, collect_inline_packages}; -use mozart_core::package::RawRepository; - -pub struct InlinePackageRepository { - id: String, - packages: Vec<InlinePackage>, -} - -impl InlinePackageRepository { - /// Build from the raw `repositories` array of a `composer.json`. Non- - /// `package` entries are ignored. - pub fn from_repositories(repositories: &[RawRepository]) -> Self { - Self { - id: "package".to_string(), - packages: collect_inline_packages(repositories), - } - } - - pub fn package_count(&self) -> usize { - self.packages.len() - } -} - -#[async_trait::async_trait] -impl Repository for InlinePackageRepository { - fn id(&self) -> &str { - &self.id - } - - async fn load_packages(&self, queries: &[PackageQuery<'_>]) -> anyhow::Result<LoadResult> { - let mut result = LoadResult::default(); - for query in queries { - let mut found_any = false; - for ipkg in &self.packages { - if ipkg.name == query.name { - found_any = true; - result.packages.push(NamedPackagistVersion { - name: ipkg.name.clone(), - version: ipkg.version.clone(), - }); - } - } - if found_any { - result.names_found.push(query.name.to_string()); - } - } - Ok(result) - } -} 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 -} diff --git a/crates/mozart-registry/src/repository/packagist_repo.rs b/crates/mozart-registry/src/repository/packagist_repo.rs deleted file mode 100644 index fa656b7..0000000 --- a/crates/mozart-registry/src/repository/packagist_repo.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! [`Repository`] backed by the live Packagist HTTP API. -//! -//! Wraps the existing [`crate::packagist::fetch_package_versions`] so the -//! resolver sees the same data either through this trait or via the legacy -//! direct call. Construction takes ownership of the [`Cache`] handle so -//! callers no longer thread it through `ResolveRequest` / `LockFileGenerationRequest`. - -use super::{LoadResult, NamedPackagistVersion, PackageQuery, Repository, SearchMode}; -use crate::cache::Cache; -use crate::packagist; -use crate::packagist::SearchResult; - -pub struct PackagistRepository { - id: String, - cache: Cache, -} - -impl PackagistRepository { - pub fn new(cache: Cache) -> Self { - Self { - id: "packagist.org".to_string(), - cache, - } - } -} - -#[async_trait::async_trait] -impl Repository for PackagistRepository { - fn id(&self) -> &str { - &self.id - } - - async fn load_packages(&self, queries: &[PackageQuery<'_>]) -> anyhow::Result<LoadResult> { - let mut result = LoadResult::default(); - for query in queries { - // Errors propagate to the caller. Composer's - // `ComposerRepository::loadAsyncPackages` distinguishes 404 - // (empty result, no error) from transport failures (exception); - // Mozart's underlying `fetch_package_versions` doesn't yet make - // that distinction, so for now both surface as `Err` and the - // caller decides whether the loop wants to continue (transitive - // exploration) or abort (seed-time fetch failure). - let versions = packagist::fetch_package_versions(query.name, &self.cache).await?; - // A successful fetch counts as "this repo authoritatively knows - // the name", even if the version list is empty — mirrors - // Composer's `ArrayRepository::loadPackages` which adds the - // name to `namesFound` regardless of constraint match. - result.names_found.push(query.name.to_string()); - for version in versions { - result.packages.push(NamedPackagistVersion { - name: query.name.to_string(), - version, - }); - } - } - Ok(result) - } - - async fn search( - &self, - query: &str, - mode: SearchMode, - package_type: Option<&str>, - ) -> anyhow::Result<Vec<SearchResult>> { - match mode { - SearchMode::Fulltext => { - let (results, _total) = packagist::search_packages(query, package_type).await?; - Ok(results) - } - SearchMode::Name => { - let pattern = build_name_regex(query)?; - let names = packagist::fetch_package_names(package_type, &self.cache).await?; - Ok(names - .into_iter() - .filter(|name| pattern.is_match(name)) - .map(empty_search_result) - .collect()) - } - SearchMode::Vendor => { - let pattern = build_name_regex(query)?; - let vendors = packagist::fetch_vendor_names(&self.cache).await?; - Ok(vendors - .into_iter() - .filter(|name| pattern.is_match(name)) - .map(empty_search_result) - .collect()) - } - } - } -} - -/// Build the case-insensitive `(?:t1|t2|...)` regex from whitespace-split -/// tokens, mirroring Composer's `'{(?:'.implode('|', $matches).')}i'`. -/// -/// Tokens are joined as-is — callers are expected to have already escaped -/// regex metacharacters (`SearchCommand` calls `preg_quote`; Mozart calls -/// `regex::escape` before reaching this point). -fn build_name_regex(query: &str) -> anyhow::Result<regex::Regex> { - let tokens: Vec<&str> = query.split_whitespace().collect(); - let body = if tokens.is_empty() { - String::new() - } else { - tokens.join("|") - }; - Ok(regex::Regex::new(&format!("(?i)(?:{body})"))?) -} - -/// Build a [`SearchResult`] with only `name` populated, mirroring the shape -/// Composer returns for `SEARCH_NAME` / `SEARCH_VENDOR` modes -/// (`['name' => $name]`, all other fields `null`). -fn empty_search_result(name: String) -> SearchResult { - SearchResult { - name, - description: String::new(), - url: String::new(), - repository: None, - downloads: 0, - favers: 0, - abandoned: None, - } -} diff --git a/crates/mozart-registry/src/repository/vcs_repo.rs b/crates/mozart-registry/src/repository/vcs_repo.rs deleted file mode 100644 index fff5f6f..0000000 --- a/crates/mozart-registry/src/repository/vcs_repo.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! [`Repository`] for VCS-type repositories. -//! -//! Wraps [`crate::vcs_bridge::scan_vcs_repositories`] + [`crate::vcs_bridge::vcs_to_packagist_version`]. -//! Scanning is expensive (clones / fetches), so we do it once at construction -//! and serve subsequent queries from the in-memory cache. Mirrors -//! `Composer\Repository\Vcs\VcsRepository`'s lazy-then-memoized behavior. - -use super::{LoadResult, NamedPackagistVersion, PackageQuery, Repository}; -use crate::packagist::PackagistVersion; -use crate::vcs_bridge::{scan_vcs_repositories, vcs_to_packagist_version}; -use mozart_core::package::RawRepository; - -pub struct VcsRepository { - id: String, - versions: Vec<(String, PackagistVersion)>, -} - -impl VcsRepository { - /// Scan every VCS-type entry in `repositories` and cache the resulting - /// versions. Non-VCS entries are ignored. This performs network I/O. - pub async fn from_repositories(repositories: &[RawRepository]) -> Self { - let scanned = scan_vcs_repositories(repositories).await; - let versions = scanned - .iter() - .map(|v| (v.name.clone(), vcs_to_packagist_version(v))) - .collect(); - Self { - id: "vcs".to_string(), - versions, - } - } - - pub fn version_count(&self) -> usize { - self.versions.len() - } -} - -#[async_trait::async_trait] -impl Repository for VcsRepository { - fn id(&self) -> &str { - &self.id - } - - async fn load_packages(&self, queries: &[PackageQuery<'_>]) -> anyhow::Result<LoadResult> { - let mut result = LoadResult::default(); - for query in queries { - let mut found_any = false; - for (name, version) in &self.versions { - if name == query.name { - found_any = true; - result.packages.push(NamedPackagistVersion { - name: name.clone(), - version: version.clone(), - }); - } - } - if found_any { - result.names_found.push(query.name.to_string()); - } - } - Ok(result) - } -} |
