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/path_repository.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/path_repository.rs')
| -rw-r--r-- | crates/mozart-registry/src/path_repository.rs | 243 |
1 files changed, 0 insertions, 243 deletions
diff --git a/crates/mozart-registry/src/path_repository.rs b/crates/mozart-registry/src/path_repository.rs deleted file mode 100644 index bf71315..0000000 --- a/crates/mozart-registry/src/path_repository.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! Support for `type: path` repositories. -//! -//! Mirrors `Composer\Repository\PathRepository`: a path repo points at a -//! local directory containing a `composer.json`, and the resolver loads the -//! package from that file directly. Mozart does not yet support glob URLs or -//! the `versions` / `reference: none` options — only the bare -//! `{ type: path, url: ... }` form the installer fixtures exercise. -//! -//! Resolution model: a path repo is expanded into a synthetic -//! `type: package` [`RawRepository`] whose payload is the loaded composer.json -//! plus a `dist` block. After this expansion the rest of the registry treats -//! the package the same as any inline `type: package` entry — that is the -//! whole point of doing the work here rather than threading a new repo type -//! through the resolver / lockfile. -//! -//! `dist.reference` matches Composer's `hash('sha1', $json . serialize($options))` -//! where `$options` carries the auto-detected `relative` flag (true when the -//! original URL was not absolute). The same SHA-1 ends up in the lockfile, so -//! consumers comparing references against Composer-produced lockfiles see -//! byte-identical values. - -use std::path::{Path, PathBuf}; - -use mozart_core::package::RawRepository; -use mozart_php_serialize::{Value as PhpValue, serialize as php_serialize}; -use sha1::{Digest, Sha1}; - -/// Translate path repos in `repositories` into synthetic `type: package` -/// entries. Non-path entries are returned unchanged in original order. -/// -/// `base_dir` is the directory used to resolve relative `url` values -/// (Composer's PHP code resolves these against the process cwd; in production -/// that equals the project root, in tests it equals the fixtures anchor). -/// -/// Failures (missing directory, unreadable composer.json, missing -/// `name`/`version`) drop the offending entry silently — the rest of the -/// repository list still applies. This mirrors Composer's lenient -/// PathRepository, which logs a warning and moves on rather than aborting the -/// whole resolve. -pub fn expand_path_repositories( - repositories: &[RawRepository], - base_dir: &Path, -) -> Vec<RawRepository> { - let mut out = Vec::with_capacity(repositories.len()); - for repo in repositories { - if repo.repo_type != "path" { - out.push(repo.clone()); - continue; - } - let Some(url) = repo.url.as_deref() else { - continue; - }; - let Some(synthetic) = load_path_package(url, base_dir) else { - continue; - }; - out.push(synthetic); - } - out -} - -/// Read one path repo's `composer.json` and synthesize the inline-package -/// form. Returns `None` for any I/O or parse failure (Composer behaves the -/// same — `PathRepository::initialize` skips entries whose `composer.json` -/// is missing). -fn load_path_package(url: &str, base_dir: &Path) -> Option<RawRepository> { - let resolved = resolve_path(url, base_dir); - let composer_json_path = resolved.join("composer.json"); - let json = std::fs::read_to_string(&composer_json_path).ok()?; - let mut package: serde_json::Value = serde_json::from_str(&json).ok()?; - let obj = package.as_object_mut()?; - - // `version` is mandatory in the inline-package representation: without it - // the resolver would skip the package. Composer's PathRepository falls - // back to `dev-main` when no version is declared and no VCS is present; - // mirror that so a path repo whose composer.json omits `version` still - // produces a usable entry. - if !obj.contains_key("version") { - obj.insert( - "version".to_string(), - serde_json::Value::String("dev-main".to_string()), - ); - } - - let is_relative = !Path::new(url).is_absolute(); - let reference = compute_path_reference(json.as_bytes(), is_relative); - - obj.insert( - "dist".to_string(), - serde_json::json!({ - "type": "path", - "url": url, - "reference": reference, - }), - ); - // Composer copies `symlink`/`relative` from `options` into - // `transport-options`. We have no `options` to forward today but emit an - // empty object so consumers reading the package see the same shape. - obj.entry("transport-options") - .or_insert_with(|| serde_json::json!({})); - - Some(RawRepository { - repo_type: "package".to_string(), - url: None, - package: Some(serde_json::Value::Array(vec![package])), - only: None, - exclude: None, - canonical: None, - security_advisories: None, - }) -} - -fn resolve_path(url: &str, base_dir: &Path) -> PathBuf { - let p = Path::new(url); - if p.is_absolute() { - p.to_path_buf() - } else { - base_dir.join(p) - } -} - -/// Compose the SHA-1 reference Composer uses for path repos: -/// `sha1($json . serialize(['relative' => $isRelative]))`. The `relative` -/// flag is the only option Composer's auto-detection populates when the user -/// supplied no `options` block. -fn compute_path_reference(json_bytes: &[u8], is_relative: bool) -> String { - let options = PhpValue::Array(vec![( - PhpValue::String("relative".to_string()), - PhpValue::Bool(is_relative), - )]); - let serialized = php_serialize(&options); - let mut hasher = Sha1::new(); - hasher.update(json_bytes); - hasher.update(serialized.as_bytes()); - let bytes = hasher.finalize(); - let mut hex = String::with_capacity(bytes.len() * 2); - for b in bytes { - use std::fmt::Write; - let _ = write!(&mut hex, "{:02x}", b); - } - hex -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn computes_known_reference_for_plugin_a_fixture() { - // Fixture used by partial-update-loads-root-aliases-for-path-repos.test. - // Expected reference (`b133081...`) is what PHP's - // `hash('sha1', file_get_contents($composerJson) . serialize(['relative' => true]))` - // produces for this file — pin it here so reference computation - // changes can't drift silently from Composer. - let composer_json_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../composer/tests/Composer/Test/Fixtures/functional/installed-versions/plugin-a/composer.json"); - let bytes = std::fs::read(&composer_json_path).expect("fixture composer.json must exist"); - let reference = compute_path_reference(&bytes, true); - assert!( - reference.starts_with("b133081"), - "unexpected reference: {reference}" - ); - } - - #[test] - fn relative_url_resolves_against_base_dir_and_emits_synthetic_package_repo() { - let temp = tempfile::tempdir().unwrap(); - std::fs::create_dir_all(temp.path().join("pkg-dir")).unwrap(); - std::fs::write( - temp.path().join("pkg-dir").join("composer.json"), - r#"{"name": "vendor/pkg", "version": "1.2.3"}"#, - ) - .unwrap(); - - let input = vec![RawRepository { - repo_type: "path".to_string(), - url: Some("pkg-dir".to_string()), - package: None, - only: None, - exclude: None, - canonical: None, - security_advisories: None, - }]; - let expanded = expand_path_repositories(&input, temp.path()); - assert_eq!(expanded.len(), 1); - assert_eq!(expanded[0].repo_type, "package"); - - let pkgs = expanded[0] - .package - .as_ref() - .expect("expanded entry must carry a package payload") - .as_array() - .expect("payload should be an array"); - assert_eq!(pkgs.len(), 1); - let pkg = &pkgs[0]; - assert_eq!(pkg["name"], "vendor/pkg"); - assert_eq!(pkg["version"], "1.2.3"); - assert_eq!(pkg["dist"]["type"], "path"); - assert_eq!(pkg["dist"]["url"], "pkg-dir"); - assert!( - pkg["dist"]["reference"] - .as_str() - .map(|s| s.len() == 40) - .unwrap_or(false), - "reference should be a 40-char SHA-1" - ); - } - - #[test] - fn missing_composer_json_drops_the_entry() { - let temp = tempfile::tempdir().unwrap(); - let input = vec![RawRepository { - repo_type: "path".to_string(), - url: Some("does-not-exist".to_string()), - package: None, - only: None, - exclude: None, - canonical: None, - security_advisories: None, - }]; - let expanded = expand_path_repositories(&input, temp.path()); - assert!(expanded.is_empty()); - } - - #[test] - fn non_path_repos_pass_through_unchanged() { - let input = vec![RawRepository { - repo_type: "vcs".to_string(), - url: Some("https://example.com/repo.git".to_string()), - package: None, - only: None, - exclude: None, - canonical: None, - security_advisories: None, - }]; - let expanded = expand_path_repositories(&input, Path::new("/tmp")); - assert_eq!(expanded.len(), 1); - assert_eq!(expanded[0].repo_type, "vcs"); - assert_eq!( - expanded[0].url.as_deref(), - Some("https://example.com/repo.git") - ); - } -} |
