diff options
Diffstat (limited to 'crates/mozart-registry/src/inline_package.rs')
| -rw-r--r-- | crates/mozart-registry/src/inline_package.rs | 277 |
1 files changed, 0 insertions, 277 deletions
diff --git a/crates/mozart-registry/src/inline_package.rs b/crates/mozart-registry/src/inline_package.rs deleted file mode 100644 index 95f842f..0000000 --- a/crates/mozart-registry/src/inline_package.rs +++ /dev/null @@ -1,277 +0,0 @@ -//! Support for inline `type: package` repositories. -//! -//! `composer.json` may embed full package metadata under -//! `repositories[].package`, mirroring `Composer\Repository\PackageRepository`. -//! These packages need no network fetch — they go straight into the resolver -//! pool and into the generated lockfile entry verbatim. - -use crate::packagist::PackagistVersion; -use crate::repository_filter::RepositoryFilter; -use indexmap::IndexSet; -use mozart_core::package::RawRepository; - -/// One package extracted from a `type: package` repository. -pub struct InlinePackage { - pub name: String, - pub version: PackagistVersion, -} - -/// Collect every package definition from `type: package` repositories. -/// -/// Each repository's `package` field may be a single object or an array of -/// objects. Entries that fail to parse (missing `name`/`version`, etc.) are -/// silently skipped so the rest of the repositories list still applies — -/// matching Composer's lenient PackageRepository constructor. -/// -/// Repositories are processed in declaration order. Once any repository -/// authoritatively answers for a package name, lower-priority `type: package` -/// repositories that list the same name are skipped — mirroring Composer's -/// first-repo-wins priority via `RepositorySet::findPackages`. -pub fn collect_inline_packages(repositories: &[RawRepository]) -> Vec<InlinePackage> { - let mut packages = Vec::new(); - let mut claimed: IndexSet<String> = IndexSet::new(); - for repo in repositories { - if repo.repo_type != "package" { - continue; - } - let Some(value) = &repo.package else { - continue; - }; - let filter = RepositoryFilter::from_repo(repo); - - let mut from_this_repo: Vec<InlinePackage> = Vec::new(); - match value { - serde_json::Value::Array(arr) => { - for entry in arr { - if let Some(pkg) = parse_inline_package(entry) { - from_this_repo.push(pkg); - } - } - } - serde_json::Value::Object(_) => { - if let Some(pkg) = parse_inline_package(value) { - from_this_repo.push(pkg); - } - } - _ => {} - } - - let mut names_this_repo: IndexSet<String> = IndexSet::new(); - for pkg in from_this_repo { - if !filter.is_allowed(&pkg.name) { - continue; - } - if claimed.contains(&pkg.name) { - continue; - } - names_this_repo.insert(pkg.name.clone()); - packages.push(pkg); - } - // canonical: false → packages enter the pool but the name is not - // claimed, so lower-priority repositories may still answer for it. - // Mirrors `FilterRepository::loadPackages`'s `namesFound = []` reset. - if filter.canonical { - claimed.extend(names_this_repo); - } - } - packages -} - -/// One advisory extracted from a repository's `security-advisories` block. -/// Carries enough to filter affected versions out of the pool when -/// `config.audit.block-insecure` is set, matching the slice of Composer's -/// `SecurityAdvisoryPoolFilter` Mozart needs for resolution-time blocking. -#[derive(Debug, Clone)] -pub struct SecurityAdvisory { - pub advisory_id: String, - pub affected_versions: String, -} - -/// Collect every `security-advisories` entry across all repositories. -/// Returned map is keyed by lowercase package name so the resolver can -/// look up affected versions in lockstep with the rest of its -/// case-insensitive name handling. Repository order is preserved within -/// each list. -pub fn collect_security_advisories( - repositories: &[RawRepository], -) -> indexmap::IndexMap<String, Vec<SecurityAdvisory>> { - let mut out: indexmap::IndexMap<String, Vec<SecurityAdvisory>> = indexmap::IndexMap::new(); - for repo in repositories { - let Some(advisories) = &repo.security_advisories else { - continue; - }; - let Some(map) = advisories.as_object() else { - continue; - }; - for (pkg_name, list) in map { - let Some(arr) = list.as_array() else { - continue; - }; - for entry in arr { - let Some(obj) = entry.as_object() else { - continue; - }; - let Some(affected) = obj - .get("affectedVersions") - .and_then(|v| v.as_str()) - .map(String::from) - else { - continue; - }; - let advisory_id = obj - .get("advisoryId") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_default(); - out.entry(pkg_name.to_lowercase()) - .or_default() - .push(SecurityAdvisory { - advisory_id, - affected_versions: affected, - }); - } - } - } - out -} - -fn parse_inline_package(value: &serde_json::Value) -> Option<InlinePackage> { - let obj = value.as_object()?; - let name = obj.get("name")?.as_str()?.to_string(); - let version_str = obj.get("version")?.as_str()?.to_string(); - - // PackagistVersion requires `version_normalized`. If the inline definition - // omits it (the common case), compute it the same way Packagist does: - // run the version through Mozart's normalizer. - // - // Mirrors Composer's `ArrayLoader::parsePackage` Composer v1 compat path: - // when `version_normalized` is exactly `9999999-dev` (the legacy default - // branch sentinel), re-normalize from the human-readable `version` field - // instead. Without this, the package's version stays as `9999999-dev` - // even though its pretty form is e.g. `dev-master`, and a root require - // for `dev-master` then can't match the loaded package. - let mut value_for_parse = value.clone(); - if let serde_json::Value::Object(ref mut map) = value_for_parse { - let needs_normalize = match map.get("version_normalized") { - None => true, - Some(serde_json::Value::String(s)) => s == "9999999-dev", - _ => false, - }; - if needs_normalize { - let normalized = mozart_semver::Version::parse(&version_str) - .map(|v| v.to_string()) - .unwrap_or_else(|_| version_str.clone()); - map.insert( - "version_normalized".to_string(), - serde_json::Value::String(normalized), - ); - } - } - - let version: PackagistVersion = serde_json::from_value(value_for_parse).ok()?; - Some(InlinePackage { name, version }) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn pkg_repo(value: serde_json::Value) -> RawRepository { - RawRepository { - repo_type: "package".to_string(), - url: None, - package: Some(value), - only: None, - exclude: None, - canonical: None, - security_advisories: None, - } - } - - #[test] - fn collects_single_inline_package_object() { - let repos = vec![pkg_repo(serde_json::json!({ - "name": "a/a", - "version": "1.0.0" - }))]; - let pkgs = collect_inline_packages(&repos); - assert_eq!(pkgs.len(), 1); - assert_eq!(pkgs[0].name, "a/a"); - assert_eq!(pkgs[0].version.version, "1.0.0"); - assert_eq!(pkgs[0].version.version_normalized, "1.0.0.0"); - } - - #[test] - fn collects_inline_package_array() { - let repos = vec![pkg_repo(serde_json::json!([ - {"name": "a/a", "version": "1.0.0"}, - {"name": "b/b", "version": "2.0.0"} - ]))]; - let pkgs = collect_inline_packages(&repos); - assert_eq!(pkgs.len(), 2); - assert_eq!(pkgs[0].name, "a/a"); - assert_eq!(pkgs[1].name, "b/b"); - } - - #[test] - fn ignores_non_package_repos() { - let repos = vec![RawRepository { - repo_type: "vcs".to_string(), - url: Some("https://example.com/foo.git".to_string()), - package: None, - only: None, - exclude: None, - canonical: None, - security_advisories: None, - }]; - assert!(collect_inline_packages(&repos).is_empty()); - } - - #[test] - fn skips_entries_missing_name_or_version() { - let repos = vec![pkg_repo(serde_json::json!([ - {"name": "a/a", "version": "1.0.0"}, - {"name": "missing/version"}, - {"version": "2.0.0"}, - {"name": "b/b", "version": "2.0.0"} - ]))]; - let pkgs = collect_inline_packages(&repos); - assert_eq!(pkgs.len(), 2); - assert_eq!(pkgs[0].name, "a/a"); - assert_eq!(pkgs[1].name, "b/b"); - } - - #[test] - fn preserves_explicit_version_normalized() { - let repos = vec![pkg_repo(serde_json::json!({ - "name": "a/a", - "version": "1.0", - "version_normalized": "1.0.0.0-explicit" - }))]; - let pkgs = collect_inline_packages(&repos); - assert_eq!(pkgs[0].version.version_normalized, "1.0.0.0-explicit"); - } - - #[test] - fn parses_full_metadata_fields() { - let repos = vec![pkg_repo(serde_json::json!({ - "name": "a/a", - "version": "1.0.0", - "type": "library", - "require": {"b/b": "^2.0"}, - "replace": {"old/x": "1.0"}, - "provide": {"some/iface": "1.0"}, - "conflict": {"bad/pkg": "*"}, - "dist": {"type": "zip", "url": "https://e.com/a.zip"} - }))]; - let pkgs = collect_inline_packages(&repos); - assert_eq!(pkgs.len(), 1); - let v = &pkgs[0].version; - assert_eq!(v.package_type.as_deref(), Some("library")); - assert_eq!(v.require.get("b/b").map(String::as_str), Some("^2.0")); - assert_eq!(v.replace.get("old/x").map(String::as_str), Some("1.0")); - assert_eq!(v.provide.get("some/iface").map(String::as_str), Some("1.0")); - assert_eq!(v.conflict.get("bad/pkg").map(String::as_str), Some("*")); - assert!(v.dist.is_some()); - } -} |
