diff --git a/crates/socket-patch-bench/src/fixtures/mod.rs b/crates/socket-patch-bench/src/fixtures/mod.rs index d02863f64..9539a602a 100644 --- a/crates/socket-patch-bench/src/fixtures/mod.rs +++ b/crates/socket-patch-bench/src/fixtures/mod.rs @@ -80,6 +80,28 @@ pub struct Fixture { pub expect: Expect, } +fn fixture( + scanned: usize, + patches: Vec, + rewritten: &[&str], + warnings: &[&'static str], +) -> Fixture { + Fixture { + project: "project", + expect: Expect { + scanned, + redirected: patches.len(), + rewritten: rewritten.iter().map(|s| s.to_string()).collect(), + allowed_warnings: warnings.to_vec(), + ..Expect::default() + }, + patches, + files: Vec::new(), + env_paths: Vec::new(), + env: Vec::new(), + } +} + /// The artifact host's base URL inside fixtures. The CLI is pointed at the /// mock with `SOCKET_PATCH_SERVER_URL`; references carry absolute URLs, so /// a fixture writes this placeholder and the engine rewrites it to the diff --git a/crates/socket-patch-bench/src/fixtures/npm.rs b/crates/socket-patch-bench/src/fixtures/npm.rs index bc5bd44c2..6fdf552ac 100644 --- a/crates/socket-patch-bench/src/fixtures/npm.rs +++ b/crates/socket-patch-bench/src/fixtures/npm.rs @@ -8,7 +8,7 @@ use std::fmt::Write as _; use serde_json::{json, Value}; use super::gen::{self, Rng, Tree}; -use super::{Expect, Fixture, Size, PATCH_HOST}; +use super::{fixture, Fixture, Size, PATCH_HOST}; use crate::mock::PatchSpec; /// One installed npm package. @@ -318,30 +318,6 @@ pub fn view_json(uuid: &str, purl: &str, file: &str) -> Value { }) } -fn fixture( - g: &Graph, - patches: Vec, - rewritten: &[&str], - warnings: &[&'static str], -) -> Fixture { - Fixture { - project: "project", - expect: Expect { - scanned: g.pkgs.len(), - lockfile_only: 0, - redirected: patches.len(), - rewritten: rewritten.iter().map(|s| s.to_string()).collect(), - allowed_warnings: warnings.to_vec(), - rescan_lockfile_only: 0, - rescan_extra_scanned: 0, - }, - patches, - files: Vec::new(), - env_paths: Vec::new(), - env: Vec::new(), - } -} - // ── npm ──────────────────────────────────────────────────────────────── /// `package-lock.json` (lockfileVersion 3). @@ -389,7 +365,7 @@ pub fn build_npm(t: &mut Tree, size: Size) -> std::io::Result { g.install_hoisted(t, "project/")?; t.mkdir("home")?; Ok(fixture( - &g, + g.pkgs.len(), g.patches(false), &["package-lock.json", ".npmrc"], &["redirect_npm_allow_remote"], @@ -487,7 +463,7 @@ pub fn build_pnpm(t: &mut Tree, size: Size) -> std::io::Result { t.write("project/node_modules/.pnpm/lock.yaml", pnpm_lock(&g))?; t.mkdir("home")?; Ok(fixture( - &g, + g.pkgs.len(), g.patches(false), &["pnpm-lock.yaml", "pnpm-workspace.yaml"], &["redirect_pnpm_trust_lockfile"], @@ -557,7 +533,7 @@ pub fn build_yarn_classic(t: &mut Tree, size: Size) -> std::io::Result )?; g.install_hoisted(t, "project/")?; t.mkdir("home")?; - Ok(fixture(&g, g.patches(false), &["yarn.lock"], &[])) + Ok(fixture(g.pkgs.len(), g.patches(false), &["yarn.lock"], &[])) } // ── yarn berry ───────────────────────────────────────────────────────── @@ -637,7 +613,7 @@ pub fn build_yarn_berry(t: &mut Tree, size: Size) -> std::io::Result { t.mkdir("home")?; // Hosted Berry pins both descriptor resolutions and their lock entries. Ok(fixture( - &g, + g.pkgs.len(), g.patches(true), &["package.json", "yarn.lock"], &[], @@ -698,7 +674,7 @@ pub fn build_bun(t: &mut Tree, size: Size) -> std::io::Result { t.write("project/bun.lock", bun_lock(&g))?; g.install_hoisted(t, "project/")?; t.mkdir("home")?; - Ok(fixture(&g, g.patches(false), &["bun.lock"], &[])) + Ok(fixture(g.pkgs.len(), g.patches(false), &["bun.lock"], &[])) } /// Bun's isolated linker (the default since Bun 1.3.2): the same text @@ -739,7 +715,7 @@ pub fn build_bun_isolated(t: &mut Tree, size: Size) -> std::io::Result )?; } t.mkdir("home")?; - Ok(fixture(&g, g.patches(false), &["bun.lock"], &[])) + Ok(fixture(g.pkgs.len(), g.patches(false), &["bun.lock"], &[])) } // ── vlt ──────────────────────────────────────────────────────────────── @@ -829,7 +805,7 @@ pub fn build_vlt(t: &mut Tree, size: Size) -> std::io::Result { t.mkdir("home")?; let patches = g.patches(false); let mut f = fixture( - &g, + g.pkgs.len(), patches, &["vlt-lock.json"], &["redirect_vlt_reinstall_required"], diff --git a/crates/socket-patch-bench/src/fixtures/other.rs b/crates/socket-patch-bench/src/fixtures/other.rs index 3d5a526ac..aadeb6376 100644 --- a/crates/socket-patch-bench/src/fixtures/other.rs +++ b/crates/socket-patch-bench/src/fixtures/other.rs @@ -12,7 +12,7 @@ use serde_json::json; use super::gen::{self, Rng, Tree}; use super::npm::view_json; use super::pypi::pretty4; -use super::{Expect, Fixture, Size, PATCH_HOST}; +use super::{fixture, Fixture, Size, PATCH_HOST}; use crate::mock::PatchSpec; /// A generic package of a non-npm ecosystem. @@ -86,30 +86,6 @@ fn spec(purl: String, uuid: String, view_file: &str, reference: serde_json::Valu } } -fn fixture( - scanned: usize, - patches: Vec, - rewritten: &[&str], - warnings: &[&'static str], -) -> Fixture { - Fixture { - project: "project", - expect: Expect { - scanned, - lockfile_only: 0, - redirected: patches.len(), - rewritten: rewritten.iter().map(|s| s.to_string()).collect(), - allowed_warnings: warnings.to_vec(), - rescan_lockfile_only: 0, - rescan_extra_scanned: 0, - }, - patches, - files: Vec::new(), - env_paths: Vec::new(), - env: Vec::new(), - } -} - // ── RubyGems (bundler) ───────────────────────────────────────────────── pub fn build_gem(t: &mut Tree, size: Size) -> std::io::Result { diff --git a/crates/socket-patch-bench/src/fixtures/pypi.rs b/crates/socket-patch-bench/src/fixtures/pypi.rs index c293fd92a..f5e38a9ef 100644 --- a/crates/socket-patch-bench/src/fixtures/pypi.rs +++ b/crates/socket-patch-bench/src/fixtures/pypi.rs @@ -12,7 +12,7 @@ use std::io::Write as _; use serde_json::json; use super::gen::{self, Rng, Tree}; -use super::{Expect, Fixture, Served, Size, PATCH_HOST}; +use super::{Fixture, Served, Size, PATCH_HOST}; use crate::mock::PatchSpec; const SITE: &str = "project/.venv/lib/python3.12/site-packages"; @@ -197,23 +197,14 @@ pub fn patches(ds: &[Dist]) -> (Vec, Served) { fn fixture(ds: &[Dist], rewritten: &[&str], warnings: &[&'static str]) -> Fixture { let (patches, files) = patches(ds); - Fixture { - project: "project", - expect: Expect { - scanned: ds.len(), - lockfile_only: 0, - redirected: patches.len(), - rewritten: rewritten.iter().map(|s| s.to_string()).collect(), - // The venv still holds the unpatched files after a hosted scan - // (a reinstall picks the patch up), and the wet run says so. - allowed_warnings: [&["redirect_pypi_stale_install"][..], warnings].concat(), - ..Expect::default() - }, - patches, - files, - env_paths: Vec::new(), - env: Vec::new(), - } + let mut fixture = super::fixture(ds.len(), patches, rewritten, warnings); + fixture.files = files; + // The venv still holds the unpatched files until the next install. + fixture + .expect + .allowed_warnings + .insert(0, "redirect_pypi_stale_install"); + fixture } fn pyproject(ds: &[Dist], extra: &str) -> String { diff --git a/crates/socket-patch-cli/src/commands/get.rs b/crates/socket-patch-cli/src/commands/get.rs index dbbc2c863..e9535191c 100644 --- a/crates/socket-patch-cli/src/commands/get.rs +++ b/crates/socket-patch-cli/src/commands/get.rs @@ -1635,13 +1635,11 @@ pub async fn run(args: GetArgs) -> i32 { print!("{}", format_selected_patches(&selected, color)); } - // Agent-mode dry run: preview against the manifest, write nothing. - // (Hosted/vendored dry runs are handled inside their engines.) The - // per-release variant narrowing the wet run applies inside the - // download engine runs here too, so the preview names only the - // variants a wet run would fetch. - if args.common.dry_run && mode == super::scan::ScanMode::Agent { - let (selected, variant_warnings, _views) = filter_to_installed_releases( + // Agent wet runs and vendored runs narrow variants in their download + // engines. Hosted runs and agent previews need the same narrowing here. + let agent_preview = args.common.dry_run && mode == super::scan::ScanMode::Agent; + let selected = if agent_preview || mode == super::scan::ScanMode::Hosted { + let (selected, variant_warnings, _) = filter_to_installed_releases( &selected, args.all_releases, &args.common.crawler_options(), @@ -1649,19 +1647,23 @@ pub async fn run(args: GetArgs) -> i32 { &api_client, ) .await; - let mut narrow_warnings = narrow_warnings; narrow_warnings.extend( variant_warnings .into_iter() .map(|w| ("release_narrowing".to_string(), w)), ); + selected + } else { + selected + }; + if agent_preview { return agent_dry_run(&args, &selected, &narrow_skips, &narrow_warnings).await; } // Agent mode confirms before acting (default YES). Dry runs skip the // prompt: nothing mutates, so nothing to confirm. Hosted and vendored // runs never prompt (v5.0), like `scan`. - if mode == super::scan::ScanMode::Agent && !args.common.dry_run { + if mode == super::scan::ScanMode::Agent { let prompt = format_confirm_prompt(args.save_only, selected.len()); if !crate::ui::confirm(&prompt, true, &args.common) { if !quiet { @@ -1673,30 +1675,6 @@ pub async fn run(args: GetArgs) -> i32 { match mode { super::scan::ScanMode::Hosted => { - // Per-release VARIANT narrowing (the finer layer under the - // coarse version narrowing above). Agent/vendored runs get it - // inside the download engines; hosted never downloads, so run - // it here — otherwise every PyPI wheel/sdist, gem platform, and - // Maven classifier variant of the installed version would be - // granted and rewritten, not just the installed distribution. - // Same fallbacks as everywhere else: uninstalled/unmatched - // bases keep all variants with a warning; --all-releases - // passes through. (The views it fetched are not needed here: - // hosted never downloads.) - let (selected, variant_warnings, _views) = filter_to_installed_releases( - &selected, - args.all_releases, - &args.common.crawler_options(), - quiet, - &api_client, - ) - .await; - let mut narrow_warnings = narrow_warnings; - narrow_warnings.extend( - variant_warnings - .into_iter() - .map(|w| ("release_narrowing".to_string(), w)), - ); return run_get_hosted( &args, &api_client, diff --git a/crates/socket-patch-cli/src/commands/scan/discovery.rs b/crates/socket-patch-cli/src/commands/scan/discovery.rs index 6c8d415e4..4fda59f40 100644 --- a/crates/socket-patch-cli/src/commands/scan/discovery.rs +++ b/crates/socket-patch-cli/src/commands/scan/discovery.rs @@ -2146,28 +2146,22 @@ mod tests { tier: "free".into(), }; let entry = |uuid: &str, detached: bool| VendorEntry { - ecosystem: "npm".into(), - base_purl: "pkg:npm/insync@1.0.0".into(), - uuid: uuid.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/npm/{uuid}/insync-1.0.0.tgz"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, detached, record: Some(record.clone()), - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "npm".into(), + "pkg:npm/insync@1.0.0".into(), + uuid.into(), + VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/npm/{uuid}/insync-1.0.0.tgz"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) }; // In sync: judged from the record, zero fetches, nothing cached. @@ -2299,20 +2293,6 @@ mod tests { let ledger = HashMap::from([( "pkg:npm/embedded@1.0.0".to_string(), VendorEntry { - ecosystem: "npm".into(), - base_purl: "pkg:npm/embedded@1.0.0".into(), - uuid: "u-embedded".into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: ".socket/vendor/npm/u-embedded/embedded-1.0.0.tgz".into(), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, detached: true, record: Some(PatchRecord { uuid: "u-embedded".into(), @@ -2329,12 +2309,20 @@ mod tests { license: "MIT".into(), tier: "free".into(), }), - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "npm".into(), + "pkg:npm/embedded@1.0.0".into(), + "u-embedded".into(), + VendorArtifact { + yarn_berry10c0: None, + path: ".socket/vendor/npm/u-embedded/embedded-1.0.0.tgz".into(), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) }, )]); diff --git a/crates/socket-patch-cli/src/commands/scan/gc.rs b/crates/socket-patch-cli/src/commands/scan/gc.rs index 38c102440..cdf6b1b0e 100644 --- a/crates/socket-patch-cli/src/commands/scan/gc.rs +++ b/crates/socket-patch-cli/src/commands/scan/gc.rs @@ -1310,28 +1310,21 @@ mod tests { state.entries.insert( PURL.to_string(), socket_patch_core::vendor::VendorEntry { - ecosystem: "npm".into(), - base_purl: PURL.into(), - uuid: UUID.into(), - artifact: socket_patch_core::vendor::state::VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/npm/{UUID}/gone-1.0.0.tgz"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some("package-lock".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..socket_patch_core::vendor::VendorEntry::new( + "npm".into(), + PURL.into(), + UUID.into(), + socket_patch_core::vendor::state::VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/npm/{UUID}/gone-1.0.0.tgz"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) }, ); socket_patch_core::vendor::save_state(tmp.path(), &state) @@ -1431,42 +1424,35 @@ mod tests { state.entries.insert( PURL.to_string(), socket_patch_core::vendor::VendorEntry { - ecosystem: "npm".into(), - base_purl: PURL.into(), - uuid: UUID.into(), - artifact: socket_patch_core::vendor::state::VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/npm/{UUID}/gone-1.0.0.tgz"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: vec![WiringRecord { - file: "package-lock.json".into(), - kind: "npm_lock_entry".into(), - action: WiringAction::Rewritten, - key: Some("node_modules/gone".into()), - original: Some(serde_json::json!({ - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gone/-/gone-1.0.0.tgz", - })), - new: Some(serde_json::json!({ - "version": "1.0.0", - "resolved": - format!("file:.socket/vendor/npm/{UUID}/gone-1.0.0.tgz"), - })), - }], - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some("package-lock".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..socket_patch_core::vendor::VendorEntry::new( + "npm".into(), + PURL.into(), + UUID.into(), + socket_patch_core::vendor::state::VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/npm/{UUID}/gone-1.0.0.tgz"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + vec![WiringRecord { + file: "package-lock.json".into(), + kind: "npm_lock_entry".into(), + action: WiringAction::Rewritten, + key: Some("node_modules/gone".into()), + original: Some(serde_json::json!({ + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gone/-/gone-1.0.0.tgz", + })), + new: Some(serde_json::json!({ + "version": "1.0.0", + "resolved": + format!("file:.socket/vendor/npm/{UUID}/gone-1.0.0.tgz"), + })), + }], + ) }, ); socket_patch_core::vendor::save_state(tmp.path(), &state) diff --git a/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs b/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs index 5ba93c8c2..17e989b5e 100644 --- a/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs +++ b/crates/socket-patch-cli/src/commands/scan/vendor_flow.rs @@ -1079,28 +1079,23 @@ mod migration_tests { fn entry(uuid: &str, detached: bool, record: Option) -> VendorEntry { VendorEntry { - ecosystem: "npm".into(), - base_purl: PURL.into(), - uuid: uuid.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/npm/{uuid}/left-pad-1.3.0.tgz"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, detached, record, flavor: Some("package-lock".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "npm".into(), + PURL.into(), + uuid.into(), + VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/npm/{uuid}/left-pad-1.3.0.tgz"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) } } diff --git a/crates/socket-patch-cli/src/commands/vendor.rs b/crates/socket-patch-cli/src/commands/vendor.rs index f522c439f..522caa770 100644 --- a/crates/socket-patch-cli/src/commands/vendor.rs +++ b/crates/socket-patch-cli/src/commands/vendor.rs @@ -304,8 +304,7 @@ async fn takeover_dry_refusal( /// Dispatch one purl to its ecosystem backend. `pkg_path` is the crawler's /// installed location (site-packages root for pypi, the package dir -/// otherwise), or a fetched artifact the backend materialises only if it -/// reaches a branch that reads it. Returns `None` for purls with no vendor +/// otherwise), when available. Returns `None` for purls with no vendor /// backend in this build. #[allow(clippy::too_many_arguments)] pub(crate) async fn dispatch_vendor_one( @@ -336,24 +335,22 @@ pub(crate) async fn dispatch_vendor_one( const SERVICE_ECOSYSTEMS: &[&str] = &[ "npm", "pypi", "cargo", "golang", "composer", "gem", "nuget", "maven", ]; - if let Some(cfg) = service { - if cfg.source.requires_service() && !SERVICE_ECOSYSTEMS.contains(&eco) { - return Some(VendorOutcome::Refused { - code: "vendor_service_unsupported_ecosystem", - detail: format!( - "--vendor-source=service is not supported for `{eco}` \ + if service.is_some() && !SERVICE_ECOSYSTEMS.contains(&eco) { + return Some(VendorOutcome::Refused { + code: "vendor_service_unsupported_ecosystem", + detail: format!( + "--vendor-source=service is not supported for `{eco}` \ (prebuilt downloads cover npm, pypi, cargo, golang, composer, \ gem, nuget, and maven)" - ), - }); - } + ), + }); } // Every backend takes the identical 9-argument tuple. macro_rules! vend { - ($backend:path) => { + ($backend:path, $source:expr) => { $backend( purl, - pkg_path, + $source, project_root, record, sources, @@ -365,35 +362,10 @@ pub(crate) async fn dispatch_vendor_one( .await }; } - // Maven and NuGet have no registry-fetch rung — `fetch_and_stage` serves - // no fetcher for either and `stage_local_artifact` is npm-only — so their - // source is the crawler's own directory. A ledger-driven maven re-run on - // a cold cache gets a deferred hint instead: the committed tree answers - // an in-sync re-run, and anything else refuses for the missing jar. - macro_rules! vend_installed { - ($backend:path) => {{ - debug_assert!( - eco == "maven" || matches!(pkg_path, PackageSource::Installed(_)), - "{eco} has no fetch rung; a pending source would need materialising" - ); - $backend( - purl, - pkg_path.path(), - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service, - ) - .await - }}; - } Some(match eco { // The flavor router probes the project's lockfile (package-lock / // yarn / pnpm / bun) and dispatches or refuses per flavor. - "npm" => vend!(vendor::npm_flavor::vendor_npm_any), + "npm" => vend!(vendor::npm_flavor::vendor_npm_any, pkg_path), "pypi" => { vendor::pypi::vendor_pypi_with_pipenv_version( purl, @@ -410,12 +382,12 @@ pub(crate) async fn dispatch_vendor_one( ) .await } - "gem" => vend!(vendor::gem::vendor_gem), - "cargo" => vend!(vendor::cargo::vendor_cargo_crate), - "golang" => vend!(vendor::golang::vendor_go_module), - "composer" => vend!(vendor::composer_lock::vendor_composer), - "nuget" => vend_installed!(vendor::nuget_feed::vendor_nuget), - "maven" => vend_installed!(vendor::maven_repo::vendor_maven), + "gem" => vend!(vendor::gem::vendor_gem, pkg_path), + "cargo" => vend!(vendor::cargo::vendor_cargo_crate, pkg_path), + "golang" => vend!(vendor::golang::vendor_go_module, pkg_path), + "composer" => vend!(vendor::composer_lock::vendor_composer, pkg_path), + "nuget" => vend!(vendor::nuget_feed::vendor_nuget, pkg_path.path()), + "maven" => vend!(vendor::maven_repo::vendor_maven, pkg_path.path()), _ => return None, }) } @@ -5187,28 +5159,21 @@ mod variant_probe_tests { /// A ledger entry recording the wheel variant as vendored at `uuid`. fn wheel_entry(uuid: &str) -> VendorEntry { VendorEntry { - ecosystem: "pypi".into(), - base_purl: BASE.into(), - uuid: uuid.into(), - artifact: socket_patch_core::vendor::state::VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/pypi/{uuid}/foo-1.0.0-py3-none-any.whl"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some("requirements".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "pypi".into(), + BASE.into(), + uuid.into(), + socket_patch_core::vendor::state::VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/pypi/{uuid}/foo-1.0.0-py3-none-any.whl"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) } } @@ -5446,28 +5411,22 @@ mod gc_tests { fn entry(detached: bool) -> VendorEntry { VendorEntry { - ecosystem: "npm".into(), - base_purl: PURL.into(), - uuid: UUID.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/npm/{UUID}/left-pad-1.3.0.tgz"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, detached, - record: None, flavor: Some("package-lock".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "npm".into(), + PURL.into(), + UUID.into(), + VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/npm/{UUID}/left-pad-1.3.0.tgz"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) } } @@ -6631,11 +6590,11 @@ mod revert_dispatch_tests { const UUID: &str = "9f6b2c4e-1d3a-4f6b-8c2d-7e5a9b1c3d5f"; fn entry_for(eco: &str, base_purl: &str) -> VendorEntry { - VendorEntry { - ecosystem: eco.into(), - base_purl: base_purl.into(), - uuid: UUID.into(), - artifact: VendorArtifact { + VendorEntry::new( + eco.into(), + base_purl.into(), + UUID.into(), + VendorArtifact { yarn_berry10c0: None, path: format!(".socket/vendor/{eco}/{UUID}/artifact"), sha256: String::new(), @@ -6643,18 +6602,8 @@ mod revert_dispatch_tests { platform_locked: None, file_inventory: None, }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + Vec::new(), + ) } /// The nuget and maven revert arms must route to their real backends — @@ -6742,28 +6691,21 @@ mod persist_tests { fn npm_entry(base_purl: &str, uuid: &str) -> VendorEntry { VendorEntry { - ecosystem: "npm".into(), - base_purl: base_purl.into(), - uuid: uuid.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/npm/{uuid}/pkg.tgz"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some("package-lock".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "npm".into(), + base_purl.into(), + uuid.into(), + VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/npm/{uuid}/pkg.tgz"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) } } @@ -7366,11 +7308,11 @@ mod unused_vendored_manifest_keys_tests { /// A ledger entry whose base purl is `base_purl`; only the purl /// matters to the manifest-key relation. fn entry(ecosystem: &str, base_purl: &str) -> VendorEntry { - VendorEntry { - ecosystem: ecosystem.into(), - base_purl: base_purl.into(), - uuid: "11111111-1111-4111-8111-111111111111".into(), - artifact: VendorArtifact { + VendorEntry::new( + ecosystem.into(), + base_purl.into(), + "11111111-1111-4111-8111-111111111111".into(), + VendorArtifact { yarn_berry10c0: None, path: String::new(), sha256: String::new(), @@ -7378,18 +7320,8 @@ mod unused_vendored_manifest_keys_tests { platform_locked: None, file_inventory: None, }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + Vec::new(), + ) } /// The keys an unused ledger entry `key` (base purl = the key) owns. diff --git a/crates/socket-patch-cli/src/commands/vex_sources.rs b/crates/socket-patch-cli/src/commands/vex_sources.rs index 2ce582171..6fd53646c 100644 --- a/crates/socket-patch-cli/src/commands/vex_sources.rs +++ b/crates/socket-patch-cli/src/commands/vex_sources.rs @@ -846,20 +846,6 @@ fn vendored_entry_for(cand: &Cand, vref: &PatchedRef) -> VendorEntry { let eco = vendor_ref(wired).map(|v| v.eco).unwrap_or_default(); let source = vref.source_file.to_string_lossy(); VendorEntry { - ecosystem: eco, - base_purl: strip_purl_qualifiers(&cand.key).to_string(), - uuid: vref.uuid.clone(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: wired.to_string(), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, // Bun's workspace-mirror integrity check and the vlt package-dir // verifier (structure rule, manifest exemption by the local blob, // since there is no inventory pin) key off the flavor. @@ -868,13 +854,21 @@ fn vendored_entry_for(cand: &Cand, vref: &PatchedRef) -> VendorEntry { "vlt-lock.json" => Some("vlt".to_string()), _ => None, }, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, detached: true, - record: None, + ..VendorEntry::new( + eco, + strip_purl_qualifiers(&cand.key).to_string(), + vref.uuid.clone(), + VendorArtifact { + yarn_berry10c0: None, + path: wired.to_string(), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) } } @@ -1502,28 +1496,22 @@ mod tests { vendor.entries.insert( key.into(), VendorEntry { - ecosystem: "npm".into(), - base_purl: key.into(), - uuid: U1.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/npm/{U1}/x-1.0.0.tgz"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, detached: true, record: Some(record(U1)), + ..VendorEntry::new( + "npm".into(), + key.into(), + U1.into(), + VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/npm/{U1}/x-1.0.0.tgz"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) }, ); let mut redirect = RedirectState::new(); @@ -1582,35 +1570,29 @@ mod tests { vendor.entries.insert( "pkg:npm/x@1.0.0".into(), VendorEntry { - ecosystem: "npm".into(), - base_purl: "pkg:npm/x@1.0.0".into(), - uuid: U1.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: rel.clone(), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: vec![WiringRecord { - file: "yarn.lock".into(), - kind: "yarn_lock_entry".into(), - action: WiringAction::Rewritten, - key: None, - original: None, - new: None, - }], - lock: None, - took_over_go_patches: false, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, detached: true, record: Some(record(U1)), + ..VendorEntry::new( + "npm".into(), + "pkg:npm/x@1.0.0".into(), + U1.into(), + VendorArtifact { + yarn_berry10c0: None, + path: rel.clone(), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + vec![WiringRecord { + file: "yarn.lock".into(), + kind: "yarn_lock_entry".into(), + action: WiringAction::Rewritten, + key: None, + original: None, + new: None, + }], + ) }, ); let mut redirect = RedirectState::new(); diff --git a/crates/socket-patch-cli/tests/covgap_commands_vendor.rs b/crates/socket-patch-cli/tests/covgap_commands_vendor.rs index 2c7b2c61e..a18e4450b 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_vendor.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_vendor.rs @@ -253,11 +253,11 @@ async fn write_ledger_entry(root: &Path, eco: &str) { let mut state = VendorState::default(); state.entries.insert( PURL.to_string(), - VendorEntry { - ecosystem: eco.into(), - base_purl: PURL.into(), - uuid: UUID.into(), - artifact: VendorArtifact { + VendorEntry::new( + eco.into(), + PURL.into(), + UUID.into(), + VendorArtifact { yarn_berry10c0: None, path: format!(".socket/vendor/{eco}/{UUID}/left-pad-1.3.0.tgz"), sha256: String::new(), @@ -265,18 +265,8 @@ async fn write_ledger_entry(root: &Path, eco: &str) { platform_locked: None, file_inventory: None, }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }, + Vec::new(), + ), ); save_state(root, &state).await.unwrap(); } diff --git a/crates/socket-patch-cli/tests/covgap_commands_vex.rs b/crates/socket-patch-cli/tests/covgap_commands_vex.rs index 17d378531..869528c8e 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_vex.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_vex.rs @@ -748,11 +748,11 @@ fn write_golang_vendor_state(cwd: &Path, purl: &str, rel_path: &str) { let mut state = VendorState::new(); state.entries.insert( purl.to_string(), - VendorEntry { - ecosystem: "golang".to_string(), - base_purl: purl.to_string(), - uuid: UUID.to_string(), - artifact: VendorArtifact { + VendorEntry::new( + "golang".to_string(), + purl.to_string(), + UUID.to_string(), + VendorArtifact { yarn_berry10c0: None, path: rel_path.to_string(), sha256: String::new(), @@ -760,7 +760,7 @@ fn write_golang_vendor_state(cwd: &Path, purl: &str, rel_path: &str) { platform_locked: None, file_inventory: None, }, - wiring: vec![WiringRecord { + vec![WiringRecord { file: "go.mod".to_string(), kind: "go_replace".to_string(), action: WiringAction::Added, @@ -768,17 +768,7 @@ fn write_golang_vendor_state(cwd: &Path, purl: &str, rel_path: &str) { original: None, new: None, }], - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }, + ), ); let dir = cwd.join(".socket/vendor"); std::fs::create_dir_all(&dir).unwrap(); diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/composer.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/composer.rs index 797892aad..76a461c06 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/composer.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/composer.rs @@ -403,40 +403,29 @@ fn write_redirect_ledger(cwd: &Path, key: &str, rec: PatchRecord) { /// embedded), with the composer backend's lock wiring record. fn write_vendor_ledger(cwd: &Path, key: &str, rec: PatchRecord) { let mut state = VendorState::new(); - state.entries.insert( + let mut entry = VendorEntry::new( + "composer".to_string(), key.to_string(), - VendorEntry { - ecosystem: "composer".to_string(), - base_purl: key.to_string(), - uuid: rec.uuid.clone(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: artifact_rel(&rec.uuid), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: vec![WiringRecord { - file: "composer.lock".to_string(), - kind: "composer_lock_package".to_string(), - action: WiringAction::Rewritten, - key: Some("acme/vexprobe".to_string()), - original: None, - new: None, - }], - lock: None, - took_over_go_patches: false, - detached: false, - record: Some(rec), - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + rec.uuid.clone(), + VendorArtifact { + yarn_berry10c0: None, + path: artifact_rel(&rec.uuid), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, }, + vec![WiringRecord { + file: "composer.lock".to_string(), + kind: "composer_lock_package".to_string(), + action: WiringAction::Rewritten, + key: Some("acme/vexprobe".to_string()), + original: None, + new: None, + }], ); + entry.record = Some(rec); + state.entries.insert(key.to_string(), entry); let dir = cwd.join(".socket/vendor"); std::fs::create_dir_all(&dir).unwrap(); std::fs::write( diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/deno.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/deno.rs index 645670fa5..c466b72aa 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/deno.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/deno.rs @@ -280,35 +280,29 @@ fn deno_ledger_claims_are_dead() { state.entries.insert( JSR_PURL.to_string(), VendorEntry { - ecosystem: "jsr".to_string(), - base_purl: JSR_PURL.to_string(), - uuid: UUID.to_string(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: rel.clone(), - sha256: "0".repeat(64), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: vec![WiringRecord { - file: "deno.lock".to_string(), - kind: "deno_lock_entry".to_string(), - action: WiringAction::Rewritten, - key: None, - original: None, - new: None, - }], - lock: None, - took_over_go_patches: false, detached: true, record: Some(record(UUID)), - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "jsr".to_string(), + JSR_PURL.to_string(), + UUID.to_string(), + VendorArtifact { + yarn_berry10c0: None, + path: rel.clone(), + sha256: "0".repeat(64), + size: None, + platform_locked: None, + file_inventory: None, + }, + vec![WiringRecord { + file: "deno.lock".to_string(), + kind: "deno_lock_entry".to_string(), + action: WiringAction::Rewritten, + key: None, + original: None, + new: None, + }], + ) }, ); fx.put( diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/maven.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/maven.rs index 8fcd4c679..09d21ea79 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/maven.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/maven.rs @@ -382,35 +382,29 @@ fn write_vendor_ledger( state.entries.insert( purl.to_string(), VendorEntry { - ecosystem: eco.to_string(), - base_purl: purl.to_string(), - uuid: uuid.to_string(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: artifact_rel.to_string(), - sha256, - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: vec![WiringRecord { - file: wiring.0.to_string(), - kind: wiring.1.to_string(), - action: WiringAction::Rewritten, - key: None, - original: None, - new: None, - }], - lock: None, - took_over_go_patches: false, detached: true, record: Some(rec), - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + eco.to_string(), + purl.to_string(), + uuid.to_string(), + VendorArtifact { + yarn_berry10c0: None, + path: artifact_rel.to_string(), + sha256, + size: None, + platform_locked: None, + file_inventory: None, + }, + vec![WiringRecord { + file: wiring.0.to_string(), + kind: wiring.1.to_string(), + action: WiringAction::Rewritten, + key: None, + original: None, + new: None, + }], + ) }, ); fx.put( diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/nuget.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/nuget.rs index 0dae5f934..e4ea8e88d 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/nuget.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/nuget.rs @@ -361,35 +361,29 @@ fn write_vendor_ledger( state.entries.insert( purl.to_string(), VendorEntry { - ecosystem: eco.to_string(), - base_purl: purl.to_string(), - uuid: uuid.to_string(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: artifact_rel.to_string(), - sha256, - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: vec![WiringRecord { - file: wiring.0.to_string(), - kind: wiring.1.to_string(), - action: WiringAction::Rewritten, - key: None, - original: None, - new: None, - }], - lock: None, - took_over_go_patches: false, detached: true, record: Some(rec), - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + eco.to_string(), + purl.to_string(), + uuid.to_string(), + VendorArtifact { + yarn_berry10c0: None, + path: artifact_rel.to_string(), + sha256, + size: None, + platform_locked: None, + file_inventory: None, + }, + vec![WiringRecord { + file: wiring.0.to_string(), + kind: wiring.1.to_string(), + action: WiringAction::Rewritten, + key: None, + original: None, + new: None, + }], + ) }, ); fx.put( diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/poetry.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/poetry.rs index 6d36df83d..6a4110406 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/poetry.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/poetry.rs @@ -442,13 +442,13 @@ fn write_redirect_ledger(p: &Proj, ledger_purl: &str, record: PatchRecord) { /// (the shape every current vendor writer persists). fn write_vendor_ledger(p: &Proj, sha: &str, record: PatchRecord) { let mut state = VendorState::new(); - state.entries.insert( - api_purl(), - VendorEntry { - ecosystem: "pypi".to_string(), - base_purl: purl(), - uuid: record.uuid.clone(), - artifact: VendorArtifact { + let mut entry = VendorEntry { + flavor: Some("poetry".to_string()), + ..VendorEntry::new( + "pypi".to_string(), + purl(), + record.uuid.clone(), + VendorArtifact { yarn_berry10c0: None, path: vendored_rel(&record.uuid), sha256: sha.to_string(), @@ -456,7 +456,7 @@ fn write_vendor_ledger(p: &Proj, sha: &str, record: PatchRecord) { platform_locked: None, file_inventory: None, }, - wiring: vec![WiringRecord { + vec![WiringRecord { file: "poetry.lock".to_string(), kind: "poetry_lock_package".to_string(), action: WiringAction::Rewritten, @@ -464,18 +464,10 @@ fn write_vendor_ledger(p: &Proj, sha: &str, record: PatchRecord) { original: None, new: None, }], - lock: None, - took_over_go_patches: false, - detached: false, - record: Some(record), - flavor: Some("poetry".to_string()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }, - ); + ) + }; + entry.record = Some(record); + state.entries.insert(api_purl(), entry); p.write( ".socket/vendor/state.json", serde_json::to_string_pretty(&state).unwrap(), diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/uv.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/uv.rs index 5c33c952c..bcdd2d7db 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/uv.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/uv.rs @@ -550,13 +550,13 @@ fn write_redirect_ledger(p: &Proj, ledger_purl: &str, record: PatchRecord, files /// shape every current vendor writer persists). fn write_vendor_ledger(p: &Proj, flavor: Flavor, rel: &str, sha: &str, record: PatchRecord) { let mut state = VendorState::new(); - state.entries.insert( - flavor.api_purl(), - VendorEntry { - ecosystem: "pypi".to_string(), - base_purl: flavor.purl(), - uuid: record.uuid.clone(), - artifact: VendorArtifact { + let mut entry = VendorEntry { + flavor: Some(flavor.ledger_flavor().to_string()), + ..VendorEntry::new( + "pypi".to_string(), + flavor.purl(), + record.uuid.clone(), + VendorArtifact { yarn_berry10c0: None, path: rel.to_string(), sha256: sha.to_string(), @@ -564,7 +564,7 @@ fn write_vendor_ledger(p: &Proj, flavor: Flavor, rel: &str, sha: &str, record: P platform_locked: None, file_inventory: None, }, - wiring: flavor + flavor .native_files() .iter() .map(|(file, _)| WiringRecord { @@ -576,18 +576,10 @@ fn write_vendor_ledger(p: &Proj, flavor: Flavor, rel: &str, sha: &str, record: P new: None, }) .collect(), - lock: None, - took_over_go_patches: false, - detached: false, - record: Some(record), - flavor: Some(flavor.ledger_flavor().to_string()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }, - ); + ) + }; + entry.record = Some(record); + state.entries.insert(flavor.api_purl(), entry); p.write( ".socket/vendor/state.json", serde_json::to_string_pretty(&state).unwrap(), diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/yarn.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/yarn.rs index ba46ca696..55b365309 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/yarn.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/yarn.rs @@ -480,13 +480,13 @@ fn write_vendor_ledger(cwd: &Path, flavor: Flavor, rel: &str, rec: PatchRecord) vec![wiring_record("yarn.lock", "yarn_lock_block")] }; let mut state = VendorState::new(); - state.entries.insert( - PURL.to_string(), - VendorEntry { - ecosystem: "npm".to_string(), - base_purl: PURL.to_string(), - uuid: rec.uuid.clone(), - artifact: VendorArtifact { + let mut entry = VendorEntry { + flavor: Some(flavor.ledger_flavor().to_string()), + ..VendorEntry::new( + "npm".to_string(), + PURL.to_string(), + rec.uuid.clone(), + VendorArtifact { yarn_berry10c0: None, path: rel.to_string(), sha256: String::new(), @@ -495,18 +495,10 @@ fn write_vendor_ledger(cwd: &Path, flavor: Flavor, rel: &str, rec: PatchRecord) file_inventory: None, }, wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: Some(rec), - flavor: Some(flavor.ledger_flavor().to_string()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }, - ); + ) + }; + entry.record = Some(rec); + state.entries.insert(PURL.to_string(), entry); put( cwd, ".socket/vendor/state.json", diff --git a/crates/socket-patch-cli/tests/e2e_vex_vendor.rs b/crates/socket-patch-cli/tests/e2e_vex_vendor.rs index 20b0b611c..4c270e405 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_vendor.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_vendor.rs @@ -149,11 +149,11 @@ fn write_vendor_state(cwd: &Path, purl: &str, rel_path: &str) { let mut state = VendorState::new(); state.entries.insert( purl.to_string(), - VendorEntry { - ecosystem: "cargo".to_string(), - base_purl: purl.to_string(), - uuid: UUID.to_string(), - artifact: VendorArtifact { + VendorEntry::new( + "cargo".to_string(), + purl.to_string(), + UUID.to_string(), + VendorArtifact { yarn_berry10c0: None, path: rel_path.to_string(), sha256: String::new(), @@ -161,18 +161,8 @@ fn write_vendor_state(cwd: &Path, purl: &str, rel_path: &str) { platform_locked: None, file_inventory: None, }, - wiring: vec![wiring], - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }, + vec![wiring], + ), ); let dir = cwd.join(".socket/vendor"); std::fs::create_dir_all(&dir).unwrap(); @@ -606,28 +596,22 @@ fn write_detached_vendor_state(cwd: &Path, purl: &str, rel_path: &str, record: P state.entries.insert( purl.to_string(), VendorEntry { - ecosystem: "cargo".to_string(), - base_purl: purl.to_string(), - uuid: UUID.to_string(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: rel_path.to_string(), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: vec![wiring], - lock: None, - took_over_go_patches: false, detached: true, record: Some(record), - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "cargo".to_string(), + purl.to_string(), + UUID.to_string(), + VendorArtifact { + yarn_berry10c0: None, + path: rel_path.to_string(), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + vec![wiring], + ) }, ); let dir = cwd.join(".socket/vendor"); @@ -984,28 +968,22 @@ fn detached_matrix_entry( wiring: WiringRecord, ) -> VendorEntry { VendorEntry { - ecosystem: eco.to_string(), - base_purl: purl.to_string(), - uuid: uuid.to_string(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: rel_path.to_string(), - sha256, - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: vec![wiring], - lock: None, - took_over_go_patches: false, detached: true, record: Some(record), - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + eco.to_string(), + purl.to_string(), + uuid.to_string(), + VendorArtifact { + yarn_berry10c0: None, + path: rel_path.to_string(), + sha256, + size: None, + platform_locked: None, + file_inventory: None, + }, + vec![wiring], + ) } } @@ -2621,21 +2599,6 @@ fn reconstructed_ledger_entry_without_wiring_attests_from_the_root_lock() { state.entries.insert( purl.to_string(), VendorEntry { - ecosystem: "npm".to_string(), - base_purl: purl.to_string(), - uuid: uuid.to_string(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: rel.clone(), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - // What `repair`'s reconstruction records for npm. - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, detached: true, record: Some(make_record( uuid, @@ -2645,11 +2608,20 @@ fn reconstructed_ledger_entry_without_wiring_attests_from_the_root_lock() { &["CVE-2026-31"], )), flavor: Some("pnpm".to_string()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "npm".to_string(), + purl.to_string(), + uuid.to_string(), + VendorArtifact { + yarn_berry10c0: None, + path: rel.clone(), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) }, ); std::fs::create_dir_all(cwd.join(".socket/vendor")).unwrap(); @@ -2742,20 +2714,6 @@ fn pnpm_bundled_copy_blocks_vex_but_not_vendor_check() { state.entries.insert( purl.to_string(), VendorEntry { - ecosystem: "npm".to_string(), - base_purl: purl.to_string(), - uuid: uuid.to_string(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: rel.clone(), - sha256, - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, detached: true, record: Some(make_record( uuid, @@ -2765,11 +2723,20 @@ fn pnpm_bundled_copy_blocks_vex_but_not_vendor_check() { &["CVE-2026-1033"], )), flavor: Some("pnpm".to_string()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "npm".to_string(), + purl.to_string(), + uuid.to_string(), + VendorArtifact { + yarn_berry10c0: None, + path: rel.clone(), + sha256, + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) }, ); std::fs::create_dir_all(cwd.join(".socket/vendor")).unwrap(); diff --git a/crates/socket-patch-cli/tests/in_process_vendor.rs b/crates/socket-patch-cli/tests/in_process_vendor.rs index 45bdc69ee..6fc2867f0 100644 --- a/crates/socket-patch-cli/tests/in_process_vendor.rs +++ b/crates/socket-patch-cli/tests/in_process_vendor.rs @@ -2648,11 +2648,11 @@ async fn vendored_golang_purl_skipped_by_apply() { let mut state = VendorState::new(); state.entries.insert( purl.clone(), - VendorEntry { - ecosystem: "golang".to_string(), - base_purl: purl.clone(), - uuid: UUID.to_string(), - artifact: VendorArtifact { + VendorEntry::new( + "golang".to_string(), + purl.clone(), + UUID.to_string(), + VendorArtifact { yarn_berry10c0: None, path: format!(".socket/vendor/golang/{UUID}/{MODULE}@{VERSION}"), sha256: String::new(), @@ -2660,18 +2660,8 @@ async fn vendored_golang_purl_skipped_by_apply() { platform_locked: None, file_inventory: None, }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }, + Vec::new(), + ), ); socket_patch_core::vendor::save_state(root, &state) .await diff --git a/crates/socket-patch-core/src/crawlers/ivy_cache.rs b/crates/socket-patch-core/src/crawlers/ivy_cache.rs index 5ae6fbcca..09bd294e7 100644 --- a/crates/socket-patch-core/src/crawlers/ivy_cache.rs +++ b/crates/socket-patch-core/src/crawlers/ivy_cache.rs @@ -21,6 +21,7 @@ use std::path::{Path, PathBuf}; use super::coursier_cache::{existing_dedup, jvm_option_values, log_source, TargetOs}; use super::maven_crawler::parse_pom_group_artifact_version; use super::types::CrawledPackage; +use crate::formats::xml::attr as attribute; use crate::utils::fs::{open_regular_file_sync, read_regular_to_bytes_sync}; use crate::vendor::jvm::layout::is_path_safe; @@ -268,30 +269,6 @@ fn info_agrees(ivy: &Path, org: &str, module: &str, rev: &str) -> bool { .all(|(attr, want)| attribute(tag, attr).is_none_or(|got| got == *want)) } -/// The value of `name="…"` (or `'…'`) inside one tag's text. -fn attribute<'a>(tag: &'a str, name: &str) -> Option<&'a str> { - let mut rest = tag; - while let Some(at) = rest.find(name) { - let before = rest[..at].chars().last(); - let after = rest[at + name.len()..].trim_start(); - rest = &rest[at + name.len()..]; - if !before.is_some_and(char::is_whitespace) { - continue; - } - let Some(after) = after.strip_prefix('=') else { - continue; - }; - let after = after.trim_start(); - let quote = after.chars().next()?; - if quote != '"' && quote != '\'' { - return None; - } - let body = &after[1..]; - return body.find(quote).map(|end| &body[..end]); - } - None -} - /// Whether `text`'s root element is ` bool { diff --git a/crates/socket-patch-core/src/crawlers/types.rs b/crates/socket-patch-core/src/crawlers/types.rs index 283fb5e98..020624833 100644 --- a/crates/socket-patch-core/src/crawlers/types.rs +++ b/crates/socket-patch-core/src/crawlers/types.rs @@ -38,34 +38,18 @@ impl Ecosystem { /// Match a PURL string to its ecosystem. pub fn from_purl(purl: &str) -> Option { - if purl.starts_with("pkg:cargo/") { - return Some(Ecosystem::Cargo); - } - if purl.starts_with("pkg:gem/") { - return Some(Ecosystem::Gem); - } - if purl.starts_with("pkg:golang/") { - return Some(Ecosystem::Golang); - } - if purl.starts_with("pkg:maven/") { - return Some(Ecosystem::Maven); - } - if purl.starts_with("pkg:composer/") { - return Some(Ecosystem::Composer); - } - if purl.starts_with("pkg:nuget/") { - return Some(Ecosystem::Nuget); - } - if purl.starts_with("pkg:jsr/") { - return Some(Ecosystem::Deno); - } - if purl.starts_with("pkg:npm/") { - Some(Ecosystem::Npm) - } else if purl.starts_with("pkg:pypi/") { - Some(Ecosystem::Pypi) - } else { - None - } + Some(match purl.strip_prefix("pkg:")?.split_once('/')?.0 { + "npm" => Self::Npm, + "pypi" => Self::Pypi, + "cargo" => Self::Cargo, + "gem" => Self::Gem, + "golang" => Self::Golang, + "maven" => Self::Maven, + "composer" => Self::Composer, + "nuget" => Self::Nuget, + "jsr" => Self::Deno, + _ => return None, + }) } /// Name used in the `--ecosystems` CLI flag (e.g. `"npm"`, `"pypi"`, `"cargo"`). @@ -105,15 +89,11 @@ impl Ecosystem { /// Human-readable name for user-facing messages. pub fn display_name(&self) -> &'static str { match self { - Ecosystem::Npm => "npm", Ecosystem::Pypi => "python", - Ecosystem::Cargo => "cargo", Ecosystem::Gem => "ruby", Ecosystem::Golang => "go", - Ecosystem::Maven => "maven", Ecosystem::Composer => "php", - Ecosystem::Nuget => "nuget", - Ecosystem::Deno => "deno", + _ => self.cli_name(), } } } diff --git a/crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs b/crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs index 032c503e5..2aeeeef06 100644 --- a/crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs +++ b/crates/socket-patch-core/src/patch/redirect/pypi_takeover.rs @@ -126,28 +126,21 @@ mod tests { fn entry(flavor: &str, wiring: Vec) -> VendorEntry { VendorEntry { - ecosystem: "pypi".into(), - base_purl: "pkg:pypi/six@1.16.0".into(), - uuid: "u".into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: ".socket/vendor/pypi/u/six-1.16.0-py2.py3-none-any.whl".into(), - sha256: "0".repeat(64), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some(flavor.into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "pypi".into(), + "pkg:pypi/six@1.16.0".into(), + "u".into(), + VendorArtifact { + yarn_berry10c0: None, + path: ".socket/vendor/pypi/u/six-1.16.0-py2.py3-none-any.whl".into(), + sha256: "0".repeat(64), + size: None, + platform_locked: None, + file_inventory: None, + }, + wiring, + ) } } diff --git a/crates/socket-patch-core/src/patch/redirect/requirements.rs b/crates/socket-patch-core/src/patch/redirect/requirements.rs index 0b2589702..507a8134d 100644 --- a/crates/socket-patch-core/src/patch/redirect/requirements.rs +++ b/crates/socket-patch-core/src/patch/redirect/requirements.rs @@ -460,10 +460,8 @@ mod takeover_reach_tests { fn entry(flavor: &str, wiring: Vec) -> VendorEntry { VendorEntry { - ecosystem: "pypi".into(), - base_purl: "pkg:pypi/six@1.16.0".into(), - uuid: "5c3e1a2b-7d4f-4e6a-9b8c-1d2e3f4a5b6c".into(), - artifact: VendorArtifact { +flavor: Some(flavor.into()), +..VendorEntry::new("pypi".into(), "pkg:pypi/six@1.16.0".into(), "5c3e1a2b-7d4f-4e6a-9b8c-1d2e3f4a5b6c".into(), VendorArtifact { yarn_berry10c0: None, path: ".socket/vendor/pypi/5c3e1a2b-7d4f-4e6a-9b8c-1d2e3f4a5b6c/six-1.16.0-py3-none-any.whl" .into(), @@ -471,19 +469,8 @@ mod takeover_reach_tests { size: None, platform_locked: None, file_inventory: None, - }, - wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: Some(flavor.into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + }, wiring) +} } #[test] diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/bun_lockb.rs b/crates/socket-patch-core/src/patch/redirect/upstream/bun_lockb.rs index 3d12f9384..88876d272 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/bun_lockb.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/bun_lockb.rs @@ -17,10 +17,8 @@ use std::collections::BTreeSet; -use super::npm::{ - bun_tarball_url, by_uuid, fetch_dists_on, refuse_all_in, BunConfigOrder, BunRegistrySettings, -}; -use super::{Ctx, FormatResult, HostedPin, View}; +use super::npm::{bun_tarball_url, fetch_dists_on, BunConfigOrder, BunRegistrySettings}; +use super::{by_uuid, refuse_all_in, Ctx, FormatResult, HostedPin, View}; use crate::vendor::bun_lockb::{BunLockb, NORMALIZED_FORMAT_1, NORMALIZED_WORKSPACE}; pub(super) async fn restore( diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/maven.rs b/crates/socket-patch-core/src/patch/redirect/upstream/maven.rs index 5b8e13488..df9a89276 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/maven.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/maven.rs @@ -23,8 +23,7 @@ use regex::Regex; -use super::npm::{by_uuid, read_or_refuse, refuse_all_in}; -use super::{Ctx, FormatResult, HostedPin, View}; +use super::{by_uuid, read_or_refuse, refuse_all_in, Ctx, FormatResult, HostedPin, View}; use crate::patch::redirect::{ generation, maven_repositories_with_id, maven_tag_inner_range, maven_tag_text_in, remove_maven_repository, MAVEN_DEPENDENCY_BLOCK_RE, MVN_CHECKSUMS, MVN_CONFIG, MVN_CONFIG_ARGS, diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs b/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs index 8c2a0d793..d63e42e3e 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs @@ -666,6 +666,39 @@ impl FormatResult { } } +fn by_uuid<'p>(pins: &[&'p HostedPin]) -> BTreeMap<&'p str, &'p HostedPin> { + pins.iter().map(|p| (p.uuid.as_str(), *p)).collect() +} + +/// A missing or unreadable file refuses every pin discovery found in it. +async fn read_or_refuse( + view: &mut View<'_>, + rel: &str, + pins: &BTreeMap<&str, &HostedPin>, + result: &mut FormatResult, +) -> Option { + let why = match view.read(rel).await { + Ok(Some(text)) => return Some(text), + Ok(None) => format!("{rel} no longer exists"), + Err(e) => e, + }; + refuse_all_in(pins, rel, result, why); + None +} + +fn refuse_all_in( + pins: &BTreeMap<&str, &HostedPin>, + rel: &str, + result: &mut FormatResult, + why: String, +) { + for pin in pins.values() { + if pin.files.iter().any(|f| f == rel) { + result.refuse(&pin.uuid, why.clone()); + } + } +} + /// Shared context handed to every format restorer. pub(crate) struct Ctx<'a> { pub client: &'a UpstreamClient, diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs b/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs index 4fda8b0f6..6df7efbac 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/npm.rs @@ -13,14 +13,9 @@ use std::collections::{BTreeMap, BTreeSet}; use serde_json::Value; use super::client::NpmDist; -use super::{Ctx, FormatResult, HostedPin, View}; +use super::{by_uuid, read_or_refuse, refuse_all_in, Ctx, FormatResult, HostedPin, View}; use crate::vendor::lock_inventory::{npm_lock_entries, NpmLockEntry}; -/// The pins by uuid. -pub(super) fn by_uuid<'p>(pins: &[&'p HostedPin]) -> BTreeMap<&'p str, &'p HostedPin> { - pins.iter().map(|p| (p.uuid.as_str(), *p)).collect() -} - /// Resolve the dist of every `(uuid, name, version)` wanted from the /// default registry, concurrently. A failed lookup refuses its pin. pub(super) async fn fetch_dists( @@ -274,40 +269,6 @@ pub(crate) async fn restore_npm_locks( result } -/// Read `rel` through the view; a missing or unreadable file refuses every -/// pin discovery found in it. -pub(super) async fn read_or_refuse( - view: &mut View<'_>, - rel: &str, - pins: &BTreeMap<&str, &HostedPin>, - result: &mut FormatResult, -) -> Option { - match view.read(rel).await { - Ok(Some(text)) => Some(text), - Ok(None) => { - refuse_all_in(pins, rel, result, format!("{rel} no longer exists")); - None - } - Err(e) => { - refuse_all_in(pins, rel, result, e); - None - } - } -} - -pub(super) fn refuse_all_in( - pins: &BTreeMap<&str, &HostedPin>, - rel: &str, - result: &mut FormatResult, - why: String, -) { - for pin in pins.values() { - if pin.files.iter().any(|f| f == rel) { - result.refuse(&pin.uuid, why.clone()); - } - } -} - // ── yarn.lock ──────────────────────────────────────────────────────────────── /// yarn v1's default registry host, used for a classic lock's `resolved` diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/nuget.rs b/crates/socket-patch-core/src/patch/redirect/upstream/nuget.rs index 37d370c33..d83b44925 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/nuget.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/nuget.rs @@ -21,8 +21,7 @@ use regex::Regex; use serde_json::Value; -use super::npm::{by_uuid, read_or_refuse, refuse_all_in}; -use super::{Ctx, FormatResult, HostedPin, View}; +use super::{by_uuid, read_or_refuse, refuse_all_in, Ctx, FormatResult, HostedPin, View}; use crate::formats::nuget::{parse_config, NugetConfig}; use crate::vendor::nuget_feed::normalize_nuget_version; diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/pypi.rs b/crates/socket-patch-core/src/patch/redirect/upstream/pypi.rs index 2e3d176a1..7a4c1b6d6 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/pypi.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/pypi.rs @@ -18,16 +18,11 @@ use serde_json::{json, Value}; use toml_edit::{DocumentMut, Item}; use super::client::PypiFile; -use super::{Ctx, FormatResult, HostedPin, View}; +use super::{by_uuid, read_or_refuse, refuse_all_in, Ctx, FormatResult, HostedPin, View}; use crate::crawlers::python_crawler::canonicalize_pypi_name; use crate::utils::python_lock::preserve_line_endings; use crate::vendor::common::pep508_name; -/// The pins by uuid. -pub(super) fn by_uuid<'p>(pins: &[&'p HostedPin]) -> BTreeMap<&'p str, &'p HostedPin> { - pins.iter().map(|p| (p.uuid.as_str(), *p)).collect() -} - /// The in-scope pin a hosted `location` names. pub(super) fn pin_of<'p>( location: &str, @@ -49,40 +44,6 @@ pub(super) fn pin_coords(pin: &HostedPin, result: &mut FormatResult) -> Option<( } } -/// Read `rel` through the view; a missing or unreadable file refuses every -/// pin discovery found in it. -pub(super) async fn read_or_refuse( - view: &mut View<'_>, - rel: &str, - pins: &BTreeMap<&str, &HostedPin>, - result: &mut FormatResult, -) -> Option { - match view.read(rel).await { - Ok(Some(text)) => Some(text), - Ok(None) => { - refuse_all_in(pins, rel, result, format!("{rel} no longer exists")); - None - } - Err(e) => { - refuse_all_in(pins, rel, result, e); - None - } - } -} - -pub(super) fn refuse_all_in( - pins: &BTreeMap<&str, &HostedPin>, - rel: &str, - result: &mut FormatResult, - why: String, -) { - for pin in pins.values() { - if pin.files.iter().any(|f| f == rel) { - result.refuse(&pin.uuid, why.clone()); - } - } -} - /// The release files of every `(uuid, name, version)` wanted, fetched /// concurrently and keyed by `(name, version)`. A failed lookup refuses its /// pin. diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/pypi_locks.rs b/crates/socket-patch-core/src/patch/redirect/upstream/pypi_locks.rs index 8530ddf48..6d14fdc94 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/pypi_locks.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/pypi_locks.rs @@ -34,10 +34,9 @@ use toml_edit::{DocumentMut, Item}; use super::client::PypiFile; use super::pypi::{ - by_uuid, fetch_release_files, multiline_toml_array, pin_of, read_or_refuse, refuse_all_in, - toml_quote, toml_value, universal_release, + fetch_release_files, multiline_toml_array, pin_of, toml_quote, toml_value, universal_release, }; -use super::{Ctx, FormatResult, HostedPin, View}; +use super::{by_uuid, read_or_refuse, refuse_all_in, Ctx, FormatResult, HostedPin, View}; use crate::crawlers::python_crawler::canonicalize_pypi_name; use crate::utils::poetry_lock::is_multiline_array; use crate::utils::python_lock::preserve_line_endings; diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/uv.rs b/crates/socket-patch-core/src/patch/redirect/upstream/uv.rs index a3bfcff4b..b78632a04 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/uv.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/uv.rs @@ -57,10 +57,9 @@ use toml_edit::{DocumentMut, Item, TableLike, Value}; use super::client::PypiFile; use super::pypi::{ - by_uuid, fetch_release_files, is_pypi_simple, pin_of, read_or_refuse, refuse_all_in, - toml_quote, toml_value, universal_release, + fetch_release_files, is_pypi_simple, pin_of, toml_quote, toml_value, universal_release, }; -use super::{Ctx, FormatResult, HostedPin, View}; +use super::{by_uuid, read_or_refuse, refuse_all_in, Ctx, FormatResult, HostedPin, View}; use crate::crawlers::python_crawler::canonicalize_pypi_name; use crate::utils::python_lock::{paired_metadata_rel, preserve_line_endings, UvSource}; use crate::vendor::common::pep508_name; diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs b/crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs index 44cc28d24..4bf2a7a4f 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs @@ -27,8 +27,8 @@ use std::collections::BTreeMap; use serde_json::{Map, Value}; -use super::npm::{by_uuid, fetch_dists_on, read_or_refuse, refuse_all_in}; -use super::{Ctx, FormatResult, HostedPin, View}; +use super::npm::fetch_dists_on; +use super::{by_uuid, read_or_refuse, refuse_all_in, Ctx, FormatResult, HostedPin, View}; use crate::vendor::vlt_lock_text::{ brotli_for_slot3, default_registry_alias, entry_text, is_default_registry, nodes_block, parse_node_line, registry_base, render_entry_line, render_tuple_with_slots, sniff_lock, diff --git a/crates/socket-patch-core/src/vendor/bun_binary.rs b/crates/socket-patch-core/src/vendor/bun_binary.rs index 3697e7a47..e235315b5 100644 --- a/crates/socket-patch-core/src/vendor/bun_binary.rs +++ b/crates/socket-patch-core/src/vendor/bun_binary.rs @@ -105,26 +105,23 @@ fn merge_duplicates( #[allow(clippy::too_many_arguments)] pub(crate) async fn vendor( purl: &str, - installed_dir: super::source::PackageSource<'_>, + _installed_dir: super::source::PackageSource<'_>, root: &Path, record: &PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, + _sources: &crate::patch::apply::PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, + _force: bool, service: Option<&super::VendorServiceConfig>, ) -> VendorOutcome { super::npm_common::vendor_npm_family( &BunBinaryBackend, super::npm_common::NpmVendorRequest { purl, - installed_dir, project_root: root, record, - sources, vendored_at, dry_run, - force, service, }, ) diff --git a/crates/socket-patch-core/src/vendor/bun_lock.rs b/crates/socket-patch-core/src/vendor/bun_lock.rs index e9706f914..f864c1e5f 100644 --- a/crates/socket-patch-core/src/vendor/bun_lock.rs +++ b/crates/socket-patch-core/src/vendor/bun_lock.rs @@ -320,24 +320,21 @@ pub async fn binary_vendor_paths(project_root: &Path) -> Result, Str #[allow(clippy::too_many_arguments)] pub(crate) async fn vendor_bun<'a>( purl: &str, - installed_dir: impl Into>, + _installed_dir: impl Into>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, + _sources: &PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, + _force: bool, service: Option<&super::VendorServiceConfig>, ) -> VendorOutcome { let req = NpmVendorRequest { purl, - installed_dir: installed_dir.into(), project_root, record, - sources, vendored_at, dry_run, - force, service, }; if binary_lock_drives(project_root) { diff --git a/crates/socket-patch-core/src/vendor/cargo.rs b/crates/socket-patch-core/src/vendor/cargo.rs index 322397ba2..d12b010ce 100644 --- a/crates/socket-patch-core/src/vendor/cargo.rs +++ b/crates/socket-patch-core/src/vendor/cargo.rs @@ -38,16 +38,14 @@ use super::cargo_lock::{self, LockEditError}; use super::cargo_manifest; use super::cargo_tag; use super::common::{ - already_patched_result, copy_matches_after_hashes, done, inventory_or_warn, - prune_empty_vendor_levels, refuse_symlinked, refused, service_offline_conflict, stage_dir_for, - swap_stage_into_place, synthesized_result, + already_patched_result, cleanup_failed_stage, copy_matches_after_hashes, done, + inventory_or_warn, prune_empty_vendor_levels, refuse_symlinked, refused, + service_offline_conflict, stage_dir_for, swap_stage_into_place, synthesized_result, }; use super::parse_memo::ParseMemo; use super::path::vendor_uuid_dir_rel; use super::registry_fetch::{extract_on_blocking_pool, extract_tgz}; -use super::service_fetch::{ - claim_prestaged, fetch_verified_archive, ServiceAttempt, ServicePolicy, ServiceTerminal, -}; +use super::service_fetch::{claim_prestaged, fetch_verified_archive, ServicePolicy}; use super::source::PackageSource; use super::state::{ write_marker_or_warn, CargoLockOriginal, VendorArtifact, VendorEntry, VendorMarker, @@ -264,22 +262,6 @@ async fn hosted_redirect_residue(project_root: &Path, name: &str, version: &str) None } -/// Failure cleanup for a staged (re)build: always remove the stage, then -/// either unwind the whole `/` dir (`unwind_uuid_dir` — a fresh vendor -/// with no pre-existing state worth keeping) or leave existing state -/// untouched; either way prune any empty-husk dirs left behind. -async fn cleanup_failed_stage(stage: &Path, uuid_dir: &Path, unwind_uuid_dir: bool) { - let _ = remove_tree(stage).await; - if unwind_uuid_dir { - let _ = remove_tree(uuid_dir).await; - } - prune_empty_vendor_levels(uuid_dir).await; -} - -/// Outcome of attempting to materialise the cargo copy from the patch service -/// (`Used`: the prebuilt crate was extracted into `copy_dir`). -type CargoServiceCopy = ServiceAttempt<()>; - /// Download the prebuilt `.crate`, integrity-verify it, and extract it into /// `copy_dir` (a path-dep copy must carry no `.cargo-checksum.json`). The extracted /// crate is the patched package built by the server. @@ -291,20 +273,14 @@ pub(super) async fn cargo_service_copy( copy_dir: &Path, uuid_dir: &Path, warnings: &mut Vec, -) -> CargoServiceCopy { - let Some(cfg) = service else { - return CargoServiceCopy::HardFail(Box::new(super::service_fetch::required())); - }; - if !cfg.service_enabled() { - return CargoServiceCopy::HardFail(Box::new(super::service_fetch::required())); - } - let policy = ServicePolicy::new(cfg, ServiceTerminal::Refused); +) -> Result<(), Box> { + let cfg = service + .filter(|cfg| cfg.service_enabled()) + .ok_or_else(|| Box::new(super::service_fetch::required()))?; + let policy = ServicePolicy::Refused; let fetched = fetch_verified_archive(cfg, &record.uuid).await; let subject = format!("crate for {name}"); - let mut archive = match policy.settle(fetched, "crate", &subject, warnings) { - Ok(archive) => archive, - Err(attempt) => return attempt, - }; + let mut archive = policy.settle(fetched, "crate", &subject)?; // Extract the `.crate` (tar.gz; strip its single `{name}-{version}/` // top-level dir) into a STAGE sibling and swap it into the copy dir only // once fully verified — a failure then leaves any pre-existing copy @@ -317,51 +293,45 @@ pub(super) async fn cargo_service_copy( let _ = remove_tree(&stage).await; if let Err(e) = tokio::fs::create_dir_all(&stage).await { cleanup_failed_stage(&stage, uuid_dir, false).await; - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot create {}: {e}", stage.display()), - ); + )); } let crate_bytes = std::mem::take(&mut archive.bytes); if let Err(e) = extract_on_blocking_pool(crate_bytes, &stage, extract_tgz).await { cleanup_failed_stage(&stage, uuid_dir, false).await; - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_extract_failed", format!("cannot extract the prebuilt crate: {e}"), - ); + )); } } let _ = tokio::fs::remove_file(stage.join(".cargo-checksum.json")).await; if !copy_matches_after_hashes(&stage, &record.files).await { cleanup_failed_stage(&stage, uuid_dir, false).await; - return policy.miss( - warnings, - "vendor_prebuilt_layout_mismatch", - format!( - "prebuilt crate for {name} extracted to an unexpected \ + return Err(policy.miss(format!( + "prebuilt crate for {name} extracted to an unexpected \ layout (patched files absent at their recorded paths)" - ), - ); + ))); } // The copy's version carries the patch uuid tag, written in the stage so // a swapped-in copy is never untagged. if let Err(e) = cargo_tag::tag_copy_manifest(&stage, version, &record.uuid).await { cleanup_failed_stage(&stage, uuid_dir, false).await; - return policy.miss( - warnings, - "vendor_prebuilt_layout_mismatch", - format!("prebuilt crate for {name}: cannot tag its version ({e})"), - ); + return Err(policy.miss(format!( + "prebuilt crate for {name}: cannot tag its version ({e})" + ))); } if let Err(e) = swap_stage_into_place(&stage, copy_dir).await { cleanup_failed_stage(&stage, uuid_dir, false).await; - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot move the extracted crate into place: {e}"), - ); + )); } warnings.push(archive.downloaded_warning(name)); - CargoServiceCopy::Used(()) + Ok(()) } /// Everything [`vendor_cargo_crate`] decides before its dry-run branch: the @@ -860,10 +830,8 @@ pub async fn vendor_cargo_crate<'a>( ) .await { - CargoServiceCopy::Used(()) => { - already_patched_result(purl, ©_dir, &record.files) - } - CargoServiceCopy::HardFail(outcome) => return *outcome, + Ok(()) => already_patched_result(purl, ©_dir, &record.files), + Err(outcome) => return *outcome, }; warnings.push(VendorWarning::new( "vendor_artifact_rebuilt", @@ -988,12 +956,12 @@ pub async fn vendor_cargo_crate<'a>( ) .await { - CargoServiceCopy::Used(()) => { + Ok(()) => { // The service crate is the patched package; trust its verified // integrity (every file reads as AlreadyPatched). already_patched_result(purl, ©_dir, &record.files) } - CargoServiceCopy::HardFail(outcome) => return *outcome, + Err(outcome) => return *outcome, }; let file_inventory = @@ -1178,28 +1146,21 @@ fn cargo_entry( }); } VendorEntry { - ecosystem: "cargo".to_string(), - base_purl, - uuid: record.uuid.clone(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: copy_rel.to_string(), - sha256: String::new(), // dir-shaped: integrity is per-file afterHashes - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring, lock: lock_original, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "cargo".to_string(), + base_purl, + record.uuid.clone(), + VendorArtifact { + yarn_berry10c0: None, + path: copy_rel.to_string(), + sha256: String::new(), // dir-shaped: integrity is per-file afterHashes + size: None, + platform_locked: None, + file_inventory: None, + }, + wiring, + ) } } @@ -3957,11 +3918,11 @@ mod tests { } fn ledger_entry_for(uuid: &str) -> VendorEntry { - VendorEntry { - ecosystem: "cargo".into(), - base_purl: PURL.into(), - uuid: uuid.into(), - artifact: VendorArtifact { + VendorEntry::new( + "cargo".into(), + PURL.into(), + uuid.into(), + VendorArtifact { yarn_berry10c0: None, path: format!(".socket/vendor/cargo/{uuid}/cfg-if-1.0.4"), sha256: String::new(), @@ -3969,18 +3930,8 @@ mod tests { platform_locked: None, file_inventory: None, }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + Vec::new(), + ) } /// The lockfile-in-use probe for cargo (GC/prune reclaim): detached lock diff --git a/crates/socket-patch-core/src/vendor/common.rs b/crates/socket-patch-core/src/vendor/common.rs index 87a429ea7..485fd1770 100644 --- a/crates/socket-patch-core/src/vendor/common.rs +++ b/crates/socket-patch-core/src/vendor/common.rs @@ -105,19 +105,11 @@ pub(crate) fn done( } } -/// Shared helper the vendor backends delegate to: the fail-closed refusals -/// for a `--vendor-source=service` run that cannot reach the service — -/// combined with `--offline`, or with no API client configured — checked -/// before any service consultation. Every backend's service helper treats -/// `!service_enabled()` as "build locally", so this is the one gate that -/// keeps `service` mode from silently building. +/// Refuse network acquisition when offline or without an API client. pub(crate) fn service_offline_conflict( service: Option<&VendorServiceConfig>, ) -> Option { let cfg = service?; - if !cfg.source.requires_service() { - return None; - } if cfg.offline { return Some(refused( "vendor_service_offline_conflict", @@ -352,6 +344,16 @@ pub(crate) fn backup_dir_for(copy_dir: &Path) -> std::path::PathBuf { swap_sibling_for(copy_dir, ".socket-old") } +/// Remove a failed stage, optionally unwind a fresh UUID directory, and prune +/// empty parents. Existing wired copies must survive failed rebuilds. +pub(crate) async fn cleanup_failed_stage(stage: &Path, uuid_dir: &Path, unwind_uuid_dir: bool) { + let _ = remove_tree(stage).await; + if unwind_uuid_dir { + let _ = remove_tree(uuid_dir).await; + } + prune_empty_vendor_levels(uuid_dir).await; +} + /// Swap a fully-built stage into place without a destructive window: park the /// old copy (if any) at `.socket-old` with a same-dir rename, rename the /// stage over the now-vacant copy path, and only then delete the backup. Every diff --git a/crates/socket-patch-core/src/vendor/composer_lock.rs b/crates/socket-patch-core/src/vendor/composer_lock.rs index e28b635d5..63e319d43 100644 --- a/crates/socket-patch-core/src/vendor/composer_lock.rs +++ b/crates/socket-patch-core/src/vendor/composer_lock.rs @@ -62,9 +62,7 @@ use super::parse_memo::ParseMemo; use super::path::{parse_vendor_path, vendor_uuid_dir_rel}; use super::registry_fetch::{extract_on_blocking_pool, extract_zip}; use super::revert::{self, KeepPolicy}; -use super::service_fetch::{ - claim_prestaged, fetch_verified_archive, ServiceAttempt, ServicePolicy, ServiceTerminal, -}; +use super::service_fetch::{claim_prestaged, fetch_verified_archive, ServicePolicy}; use super::source::PackageSource; use super::state::{ write_marker_or_warn, VendorArtifact, VendorEntry, VendorMarker, WiringAction, WiringRecord, @@ -331,10 +329,8 @@ pub async fn vendor_composer<'a>( ) .await { - ComposerServiceCopy::Used(()) => { - already_patched_result(purl, ©_dir, &record.files) - } - ComposerServiceCopy::HardFail(outcome) => return *outcome, + Ok(()) => already_patched_result(purl, ©_dir, &record.files), + Err(outcome) => return *outcome, }; mirror_filters::heal_or_warn(©_dir, record, &pkg, &mut warnings).await; warnings.push(VendorWarning::new( @@ -374,8 +370,8 @@ pub async fn vendor_composer<'a>( match composer_service_copy(service, record, &pkg, ©_dir, &uuid_dir, &mut warnings) .await { - ComposerServiceCopy::Used(()) => already_patched_result(purl, ©_dir, &record.files), - ComposerServiceCopy::HardFail(outcome) => return *outcome, + Ok(()) => already_patched_result(purl, ©_dir, &record.files), + Err(outcome) => return *outcome, }; if let Err(detail) = mirror_filters::neutralize_or_conflict(©_dir, record, &pkg, &mut warnings).await @@ -439,11 +435,11 @@ pub async fn vendor_composer<'a>( let marker = VendorMarker::new("composer", &base_purl, record, vendored_at); write_marker_or_warn(&uuid_dir, &marker, &mut warnings).await; - let entry = VendorEntry { - ecosystem: "composer".to_string(), + let entry = VendorEntry::new( + "composer".to_string(), base_purl, - uuid: record.uuid.clone(), - artifact: VendorArtifact { + record.uuid.clone(), + VendorArtifact { yarn_berry10c0: None, path: copy_rel, sha256: String::new(), // Directory integrity uses the complete inventory. @@ -451,7 +447,7 @@ pub async fn vendor_composer<'a>( platform_locked: None, file_inventory, }, - wiring: vec![WiringRecord { + vec![WiringRecord { file: COMPOSER_LOCK.to_string(), kind: WIRING_KIND.to_string(), action: WiringAction::Rewritten, @@ -459,17 +455,7 @@ pub async fn vendor_composer<'a>( original: (!was_vendored).then_some(original_entry), new: Some(Value::Object(rewritten)), }], - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }; + ); done(result, Some(entry), warnings) } @@ -648,10 +634,6 @@ async fn cleanup_failed_stage(stage: &Path, uuid_dir: &Path, unwind_uuid_dir: bo prune_empty_vendor_dirs(stage).await; } -/// Outcome of attempting to materialise the composer copy from the patch -/// service (`Used`: the prebuilt dist zip was extracted into `copy_dir`). -type ComposerServiceCopy = ServiceAttempt<()>; - /// Download the prebuilt dist zip, integrity-verify it, and extract it into /// `copy_dir` (dropping the zip's variable top-level dir). Maps each service /// outcome onto the `auto` / `service` fallback policy. The extracted zip IS @@ -663,20 +645,14 @@ pub(super) async fn composer_service_copy( copy_dir: &Path, uuid_dir: &Path, warnings: &mut Vec, -) -> ComposerServiceCopy { - let Some(cfg) = service else { - return ComposerServiceCopy::HardFail(Box::new(super::service_fetch::required())); - }; - if !cfg.service_enabled() { - return ComposerServiceCopy::HardFail(Box::new(super::service_fetch::required())); - } - let policy = ServicePolicy::new(cfg, ServiceTerminal::Refused); +) -> Result<(), Box> { + let cfg = service + .filter(|cfg| cfg.service_enabled()) + .ok_or_else(|| Box::new(super::service_fetch::required()))?; + let policy = ServicePolicy::Refused; let fetched = fetch_verified_archive(cfg, &record.uuid).await; let subject = format!("dist zip for {pkg}"); - let mut archive = match policy.settle(fetched, "dist zip", &subject, warnings) { - Ok(archive) => archive, - Err(attempt) => return attempt, - }; + let mut archive = policy.settle(fetched, "dist zip", &subject)?; // Extract into a STAGE sibling and swap it into the copy dir only // once fully verified — a failure then leaves any pre-existing // (possibly live-wired) copy and its marker untouched and no husk @@ -689,19 +665,19 @@ pub(super) async fn composer_service_copy( let _ = remove_tree(&stage).await; if let Err(e) = tokio::fs::create_dir_all(&stage).await { cleanup_failed_stage(&stage, uuid_dir, false).await; - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot create {}: {e}", stage.display()), - ); + )); } // composer dist zips carry a single variable top-level dir. let zip_bytes = std::mem::take(&mut archive.bytes); if let Err(e) = extract_on_blocking_pool(zip_bytes, &stage, extract_dist_zip).await { cleanup_failed_stage(&stage, uuid_dir, false).await; - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_extract_failed", format!("cannot extract the prebuilt dist zip: {e}"), - ); + )); } } // Verify the EXTRACTED TREE, not just the archive bytes. The @@ -717,25 +693,21 @@ pub(super) async fn composer_service_copy( // build. if !copy_matches_after_hashes(&stage, &record.files).await { cleanup_failed_stage(&stage, uuid_dir, false).await; - return policy.miss( - warnings, - "vendor_prebuilt_layout_mismatch", - format!( - "prebuilt dist zip for {pkg} extracted to an \ + return Err(policy.miss(format!( + "prebuilt dist zip for {pkg} extracted to an \ unexpected layout (patched files absent at their \ recorded paths)" - ), - ); + ))); } if let Err(e) = swap_stage_into_place(&stage, copy_dir).await { cleanup_failed_stage(&stage, uuid_dir, false).await; - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot move the extracted dist into place: {e}"), - ); + )); } warnings.push(archive.downloaded_warning(pkg)); - ComposerServiceCopy::Used(()) + Ok(()) } /// Locate the package's entry: `packages[]` first, then `packages-dev[]`. diff --git a/crates/socket-patch-core/src/vendor/gem.rs b/crates/socket-patch-core/src/vendor/gem.rs index 353b636d8..1849d69d2 100644 --- a/crates/socket-patch-core/src/vendor/gem.rs +++ b/crates/socket-patch-core/src/vendor/gem.rs @@ -65,8 +65,8 @@ use crate::utils::fs::{atomic_write_bytes_preserving_mode, read_regular_to_strin use crate::utils::purl::{build_gem_purl, parse_gem_purl, purl_qualifier}; use super::common::{ - already_patched_result, copy_matches_after_hashes, done, failed_result, inventory_or_warn, - prune_empty_vendor_levels, refused, service_offline_conflict, stage_dir_for, + already_patched_result, cleanup_failed_stage, copy_matches_after_hashes, done, failed_result, + inventory_or_warn, prune_empty_vendor_levels, refused, service_offline_conflict, stage_dir_for, swap_stage_into_place, synthesized_result, }; use super::path::{parse_vendor_path, vendor_uuid_dir_rel}; @@ -74,7 +74,7 @@ use super::registry_fetch::{extract_gem_data, extract_on_blocking_pool}; use super::revert::{self, KeepPolicy}; use super::service_fetch::{ claim_prestaged, fetch_verified_archive, fetch_verified_secondary, SecondaryArtifactResult, - ServiceAttempt, ServicePolicy, ServiceTerminal, + ServicePolicy, }; use super::source::PackageSource; use super::state::{ @@ -131,7 +131,6 @@ struct GemPrelude { gemfile_text: String, lock_path: PathBuf, lock_text: String, - local_stub: Option<(PathBuf, String)>, /// Gemfile and Gemfile.lock already wire this uuid's copy. lock_wired: bool, /// ...and the committed copy is intact (the in-sync hot path, which @@ -381,14 +380,14 @@ async fn gem_prelude( } }; - let local_stub: Option<(PathBuf, String)> = { + let local_stub = { let spec_src = installed_path .parent() .filter(|gems| gems.file_name().is_some_and(|n| n == "gems")) .and_then(Path::parent) .map(|home| home.join("specifications").join(format!("{leaf}.gemspec"))); match spec_src { - Some(p) => read_regular_to_string(&p).await.ok().map(|t| (p, t)), + Some(p) => read_regular_to_string(&p).await.ok(), None => None, } }; @@ -398,7 +397,7 @@ async fn gem_prelude( // Only the local stub is checked here (when present); the service stub is // re-checked in `gem_service_copy`, and a native gem emits no service stub // at all (the converter refuses it), so the service path also misses. - if let Some((_, text)) = &local_stub { + if let Some(text) = &local_stub { if gemspec_declares_extensions(text) { return Err(refused( "native_extensions_unsupported", @@ -461,7 +460,6 @@ async fn gem_prelude( gemfile_text, lock_path, lock_text, - local_stub, lock_wired, copy_ok, }) @@ -544,10 +542,10 @@ pub async fn vendor_gem<'a>( installed_dir: impl Into>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, + _sources: &PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, + _force: bool, service: Option<&VendorServiceConfig>, ) -> VendorOutcome { let installed_dir = installed_dir.into(); @@ -565,7 +563,6 @@ pub async fn vendor_gem<'a>( gemfile_text, lock_path, lock_text: _, - local_stub, lock_wired, copy_ok, } = &prelude; @@ -603,15 +600,10 @@ pub async fn vendor_gem<'a>( let mut warnings: Vec = Vec::new(); let result = match materialise_patched_copy( purl, - installed_dir, copy_dir, uuid_dir, name, - version, - local_stub.as_ref().map(|(p, t)| (p.as_path(), t.as_str())), record, - sources, - force, false, // live-wired: never unwind the uuid dir on failure service, &mut warnings, @@ -679,15 +671,10 @@ pub async fn vendor_gem<'a>( } let mut result = match materialise_patched_copy( purl, - installed_dir, copy_dir, uuid_dir, name, - version, - local_stub.as_ref().map(|(p, t)| (p.as_path(), t.as_str())), record, - sources, - force, true, // fresh vendor: nothing pre-existing worth keeping service, &mut warnings, @@ -858,11 +845,11 @@ fn gem_entry( file_inventory: Option>, wiring: Vec, ) -> VendorEntry { - VendorEntry { - ecosystem: "gem".to_string(), + VendorEntry::new( + "gem".to_string(), base_purl, - uuid: record.uuid.clone(), - artifact: VendorArtifact { + record.uuid.clone(), + VendorArtifact { yarn_berry10c0: None, path: copy_rel, sha256: String::new(), // dir-shaped: whole-tree integrity is the inventory @@ -871,31 +858,7 @@ fn gem_entry( file_inventory, }, wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } -} - -/// Failure cleanup for a staged (re)build: always remove the stage, then -/// either unwind the whole `/` dir (`unwind_uuid_dir` — a fresh vendor -/// with no pre-existing state worth keeping) or leave existing state -/// untouched — a live-wired rebuild must never delete the copy the Gemfile -/// `path:` and the lock's PATH `remote:` still point at; either way prune any -/// empty-husk dirs left behind. -async fn cleanup_failed_stage(stage: &Path, uuid_dir: &Path, unwind_uuid_dir: bool) { - let _ = remove_tree(stage).await; - if unwind_uuid_dir { - let _ = remove_tree(uuid_dir).await; - } - prune_empty_vendor_levels(uuid_dir).await; + ) } /// The path-source stub gemspec served as the gem's SECOND artifact, alongside @@ -904,31 +867,8 @@ async fn cleanup_failed_stage(stage: &Path, uuid_dir: &Path, unwind_uuid_dir: bo /// eval-able Ruby form a bundler path source loads. pub(crate) const GEM_STUB_ARTIFACT_KIND: &str = "gem-stub-gemspec"; -/// Outcome of attempting to materialise the gem copy from the patch service. -pub(super) enum GemServiceCopy { - /// The prebuilt `.gem` was extracted into `copy_dir` and the verified stub - /// gemspec written as `.gemspec`. - Used, - /// Bubble this terminal outcome (boxed — `VendorOutcome` is large). - HardFail(Box), -} - -/// Download the prebuilt `.gem` + its `gem-stub-gemspec` secondary artifact, -/// integrity-verify both, extract the `.gem`'s `data.tar.gz` into `copy_dir`, -/// and write the stub as `.gemspec`. The extracted `.gem` IS the patched -/// package the converter built, so it needs no local install — the point of -/// the service path. Maps each service outcome onto the `auto` / `service` -/// fallback policy. -/// -/// A MISSING stub artifact is a terminal miss (fall back under `auto`, refuse -/// under `service`): it means either a native-extension gem (the converter -/// emits no stub — bundler can't build extensions for a path source) or a gem -/// patch built before the stub rollout (the invalidation migration rebuilds -/// those). The downloaded stub is re-checked for native extensions as defense -/// in depth, and an INVALID stub — one missing the rubygems-required -/// `summary`/`authors` assignments — follows -/// the same miss policy under its own `vendor_prebuilt_stub_invalid` code -/// (always loud, even under `auto`). +/// Download and verify the `.gem` and its stub gemspec, then atomically +/// replace the copy. Missing, invalid, or native-extension stubs are refused. pub(super) async fn gem_service_copy( service: Option<&VendorServiceConfig>, record: &PatchRecord, @@ -937,81 +877,37 @@ pub(super) async fn gem_service_copy( uuid_dir: &Path, unwind_uuid_dir: bool, warnings: &mut Vec, -) -> GemServiceCopy { - let Some(cfg) = service else { - return GemServiceCopy::HardFail(Box::new(super::service_fetch::required())); - }; - if !cfg.service_enabled() { - return GemServiceCopy::HardFail(Box::new(super::service_fetch::required())); - } - fn hard(code: &'static str, detail: String) -> GemServiceCopy { - GemServiceCopy::HardFail(Box::new(refused(code, detail))) - } - // One policy for every service miss: explicit `service` refuses (the - // `refusal` tuple names the terminal code and an optional remedy sentence - // for its detail), `auto` warns under `code` and falls back to the local - // build. `is_stub_defect` marks the misses where the service DID serve a - // stub that failed validation — the reason then rides the `FallBack` - // payload (see [`GemServiceCopy::FallBack`]). - let miss = |_warnings: &mut Vec, - _code: &'static str, - refusal: (&'static str, &str), - reason: String, - _is_stub_defect: bool| { - let (code, remedy) = refusal; - hard( - code, - if remedy.is_empty() { - reason - } else { - format!("{reason}. {remedy}") - }, - ) - }; - +) -> Result<(), Box> { + let cfg = service + .filter(|cfg| cfg.service_enabled()) + .ok_or_else(|| Box::new(super::service_fetch::required()))?; // Step 1: the prebuilt `.gem` (sha512-verified against the reference). let fetched = fetch_verified_archive(cfg, &record.uuid).await; let subject = format!(".gem for {name}"); - let policy = ServicePolicy::new(cfg, ServiceTerminal::Refused); - let mut archive = match policy.settle::<()>(fetched, ".gem", &subject, warnings) { - Ok(archive) => archive, - Err(ServiceAttempt::HardFail(outcome)) => return GemServiceCopy::HardFail(outcome), - Err(ServiceAttempt::Used(())) => { - return GemServiceCopy::HardFail(Box::new(super::service_fetch::required())); - } - }; + let policy = ServicePolicy::Refused; + let mut archive = policy.settle(fetched, ".gem", &subject)?; // Step 2: the stub gemspec the converter generated alongside the `.gem`. let stub = match fetch_verified_secondary(cfg, &archive, GEM_STUB_ARTIFACT_KIND).await { SecondaryArtifactResult::Ready(bytes) => bytes, SecondaryArtifactResult::Absent => { - return miss( - warnings, - "vendor_prebuilt_stub_missing", - ("vendor_prebuilt_required", ""), + return Err(policy.miss( "the patch service served no stub gemspec for this gem (a native-extension \ gem, or a patch built before the stub rollout)" .to_string(), - false, - ); + )); } SecondaryArtifactResult::IntegrityMismatch(reason) => { - return hard( + return Err(policy.hard( "vendor_prebuilt_integrity_mismatch", format!( "prebuilt stub gemspec for {name} failed integrity verification ({reason}); \ refusing to fall back to a local build on tampered bytes" ), - ); + )); } SecondaryArtifactResult::Failed(reason) => { - return miss( - warnings, - "vendor_prebuilt_unavailable", - ("vendor_prebuilt_required", ""), - format!("could not fetch the stub gemspec ({reason})"), - false, - ); + return Err(policy.miss(format!("could not fetch the stub gemspec ({reason})"))); } }; let stub_text = String::from_utf8_lossy(&stub); @@ -1020,24 +916,17 @@ pub(super) async fn gem_service_copy( // refuse one here too — bundler silently skips extension builds for path // sources, so a native gem would install and then fail at `require` time. if gemspec_declares_extensions(&stub_text) { - return hard( + return Err(policy.hard( "native_extensions_unsupported", format!( "the served stub gemspec for {name} declares native extensions; bundler does \ not build extensions for path-sourced gems" ), - ); + )); } - // Defense in depth: a served stub may omit the rubygems-required - // `summary`/`authors`, and every bundler major validates path-source - // gemspecs — writing such a stub - // verbatim makes every later `bundle install` exit 1 (`missing value for - // attribute summary`). An INVALID stub follows the MISSING-stub policy - // (fall back under `auto`, refuse under `service`) but under its own - // `vendor_prebuilt_stub_invalid` code, and always loudly — the served - // artifact is defective, not merely absent. Nothing has been written yet, - // so the refusal leaves no partial artifacts. + // Bundler rejects path-source gemspecs without summary/authors. Refuse + // before writing anything, so invalid stubs leave no partial artifact. let missing_attrs = gemspec_missing_required_attrs(&stub_text); if !missing_attrs.is_empty() { let licenses_note = if gemspec_assigns_attr(&stub_text, &["licenses", "license"]) { @@ -1052,16 +941,13 @@ pub(super) async fn gem_service_copy( `bundle install` fail", missing_attrs.join(", "), ); - return miss( - warnings, + return Err(policy.hard( "vendor_prebuilt_stub_invalid", - ( - "vendor_prebuilt_stub_invalid", - "Retry after the patch service publishes a corrected artifact", + format!( + "{}. Retry after the patch service publishes a corrected artifact", + reason ), - reason, - true, - ); + )); } // Extract the patched `.gem`'s data.tar.gz into a STAGE sibling, add the @@ -1077,82 +963,58 @@ pub(super) async fn gem_service_copy( let _ = remove_tree(&stage).await; if let Err(e) = tokio::fs::create_dir_all(&stage).await { cleanup_failed_stage(&stage, uuid_dir, unwind_uuid_dir).await; - return hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot create {}: {e}", stage.display()), - ); + )); } let gem_bytes = std::mem::take(&mut archive.bytes); if let Err(e) = extract_on_blocking_pool(gem_bytes, &stage, extract_gem_data).await { cleanup_failed_stage(&stage, uuid_dir, unwind_uuid_dir).await; - return hard( + return Err(policy.hard( "vendor_prebuilt_extract_failed", format!("cannot extract the prebuilt .gem: {e}"), - ); + )); } } if let Err(e) = tokio::fs::write(stage.join(format!("{name}.gemspec")), &stub).await { cleanup_failed_stage(&stage, uuid_dir, unwind_uuid_dir).await; - return hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot write the stub gemspec into the vendored dir: {e}"), - ); + )); } if !copy_matches_after_hashes(&stage, &record.files).await { cleanup_failed_stage(&stage, uuid_dir, unwind_uuid_dir).await; - return miss( - warnings, - "vendor_prebuilt_layout_mismatch", - ("vendor_prebuilt_required", ""), - format!( - "prebuilt .gem for {name} extracted to an unexpected layout \ + return Err(policy.miss(format!( + "prebuilt .gem for {name} extracted to an unexpected layout \ (patched files absent at their recorded paths)" - ), - false, - ); + ))); } if let Err(e) = swap_stage_into_place(&stage, copy_dir).await { cleanup_failed_stage(&stage, uuid_dir, unwind_uuid_dir).await; - return hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot move the extracted .gem into place: {e}"), - ); + )); } warnings.push(archive.downloaded_warning(name)); - GemServiceCopy::Used + Ok(()) } -/// Materialise the patched copy at `copy_dir` plus its `.gemspec` stub, -/// service-download first (see [`gem_service_copy`]) and local copy+stub+apply -/// as the fallback. Returns the verify [`ApplyResult`] (a synthesized -/// `AlreadyPatched` on the service path), or a terminal [`VendorOutcome`] to -/// bubble. A non-fatal copy/stub/patch failure is surfaced as an UN-successful -/// `ApplyResult` (the caller returns it as a `Done` with no ledger entry). -/// -/// Either build is staged (see [`swap_stage_into_place`]) and swapped into -/// `copy_dir` only on success, so a failure never destroys a pre-existing -/// copy: with `unwind_uuid_dir` (a fresh vendor — nothing pre-existing to -/// keep) the whole uuid dir is removed on failure, without it (the wired -/// hot-path rebuild, where the Gemfile `path:` and the lock's PATH `remote:` -/// still point at the copy) the previous copy, marker, and wiring are left -/// exactly as they were. +/// Acquire a verified service copy without replacing the prior copy on failure. #[allow(clippy::too_many_arguments)] async fn materialise_patched_copy( purl: &str, - _installed_dir: PackageSource<'_>, copy_dir: &Path, uuid_dir: &Path, name: &str, - _version: &str, - _local_stub: Option<(&Path, &str)>, record: &PatchRecord, - _sources: &PatchSources<'_>, - _force: bool, unwind_uuid_dir: bool, service: Option<&VendorServiceConfig>, warnings: &mut Vec, ) -> Result> { - match gem_service_copy( + gem_service_copy( service, record, name, @@ -1161,15 +1023,8 @@ async fn materialise_patched_copy( unwind_uuid_dir, warnings, ) - .await - { - GemServiceCopy::Used => { - // The service `.gem` is the patched package; trust its verified - // integrity (every file reads as AlreadyPatched). - Ok(already_patched_result(purl, copy_dir, &record.files)) - } - GemServiceCopy::HardFail(outcome) => Err(outcome), - } + .await?; + Ok(already_patched_result(purl, copy_dir, &record.files)) } /// Revert a gem vendor entry: restore the Gemfile line / delete the managed diff --git a/crates/socket-patch-core/src/vendor/golang.rs b/crates/socket-patch-core/src/vendor/golang.rs index 16369b3b2..ee546a84f 100644 --- a/crates/socket-patch-core/src/vendor/golang.rs +++ b/crates/socket-patch-core/src/vendor/golang.rs @@ -36,9 +36,7 @@ use super::common::{ }; use super::path::vendor_uuid_dir_rel; use super::registry_fetch::{extract_on_blocking_pool, extract_zip_with_prefix}; -use super::service_fetch::{ - claim_prestaged, fetch_verified_archive, ServiceAttempt, ServicePolicy, ServiceTerminal, -}; +use super::service_fetch::{claim_prestaged, fetch_verified_archive, ServicePolicy}; use super::source::PackageSource; use super::state::{ write_marker_or_warn, VendorArtifact, VendorEntry, VendorMarker, WiringAction, WiringRecord, @@ -272,11 +270,11 @@ pub async fn vendor_go_module<'a>( ) .await { - GoServiceRedirect::Used(inventory) => ( + Ok(inventory) => ( already_patched_result(purl, ©_dir, &record.files), inventory, ), - GoServiceRedirect::HardFail(outcome) => return *outcome, + Err(outcome) => return *outcome, }; if dry_run { @@ -383,58 +381,47 @@ pub async fn vendor_go_module<'a>( write_marker_or_warn(&project_root.join(&base_rel), &marker, &mut warnings).await; let entry = VendorEntry { - ecosystem: "golang".to_string(), - base_purl, - uuid: record.uuid.clone(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: format!("{base_rel}/{module}@{version}"), - sha256: String::new(), // dir-shaped: integrity is per-file afterHashes - size: None, - platform_locked: None, - file_inventory, - }, - wiring: vec![WiringRecord { - file: "go.mod".to_string(), - kind: "go_replace".to_string(), - // Rewritten whenever ANY socket-owned directive pre-existed (a - // go-patches or hosted takeover, or a re-vendor refreshing an - // older uuid). - action: if prior_target.is_some() { - WiringAction::Rewritten - } else { - WiringAction::Added - }, - key: Some(module.to_string()), - original: prior_target.map(serde_json::Value::from), - new: Some(serde_json::Value::from(replace_target_path( - &base_rel, module, version, - ))), - }], - lock: None, took_over_go_patches: takeover, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "golang".to_string(), + base_purl, + record.uuid.clone(), + VendorArtifact { + yarn_berry10c0: None, + path: format!("{base_rel}/{module}@{version}"), + sha256: String::new(), // dir-shaped: integrity is per-file afterHashes + size: None, + platform_locked: None, + file_inventory, + }, + vec![WiringRecord { + file: "go.mod".to_string(), + kind: "go_replace".to_string(), + // Rewritten whenever ANY socket-owned directive pre-existed (a + // go-patches or hosted takeover, or a re-vendor refreshing an + // older uuid). + action: if prior_target.is_some() { + WiringAction::Rewritten + } else { + WiringAction::Added + }, + key: Some(module.to_string()), + original: prior_target.map(serde_json::Value::from), + new: Some(serde_json::Value::from(replace_target_path( + &base_rel, module, version, + ))), + }], + ) }; done(result, Some(entry), warnings) } -/// Outcome of attempting to materialise the go copy from the patch service -/// (`Used`: the prebuilt module zip was extracted and the `replace` wired). -type GoServiceRedirect = ServiceAttempt>>; - /// Download the prebuilt module zip, verify it (sha512 + the `h1:` dirhash, /// done by `fetch_verified_archive`), extract it into `copy_dir` (stripping its /// `{module}@{version}/` prefix), ensure a `go.mod`, and wire the `replace` /// directive — the same end state `apply_go_redirect` produces, minus the copy -/// + local apply. Maps each service outcome onto the `auto` / `service` policy. +/// + local apply. /// /// `wired` — the run started with the vendor `replace` already pointing at /// THIS uuid's copy; a failure leg must then also drop that directive with the @@ -453,40 +440,34 @@ async fn go_service_redirect( copy_was_ok: bool, wired: bool, warnings: &mut Vec, -) -> GoServiceRedirect { +) -> Result>, Box> { if record.files.is_empty() || copy_was_ok { if let Err(e) = go_mod_edit::ensure_replace_entry(project_root, module, version, base_rel, dry_run) .await { - return GoServiceRedirect::HardFail(Box::new(refused( + return Err(Box::new(refused( "vendor_prebuilt_wire_failed", e.to_string(), ))); } - return GoServiceRedirect::Used(None); + return Ok(None); } if dry_run { let prefix = format!("{module}@{version}/"); - return match super::service_fetch::preview_service(service, record, move |bytes, dest| { + return super::service_fetch::preview_service(service, record, move |bytes, dest| { extract_zip_with_prefix(bytes, dest, &prefix) }) .await - { - Ok(()) => GoServiceRedirect::Used(None), - Err(outcome) => GoServiceRedirect::HardFail(outcome), - }; + .map(|()| None); } let Some(cfg) = service.filter(|cfg| cfg.service_enabled()) else { - return GoServiceRedirect::HardFail(Box::new(super::service_fetch::required())); + return Err(Box::new(super::service_fetch::required())); }; - let policy = ServicePolicy::new(cfg, ServiceTerminal::Refused); + let policy = ServicePolicy::Refused; let fetched = fetch_verified_archive(cfg, &record.uuid).await; let subject = format!("module zip for {module}"); - let mut archive = match policy.settle(fetched, "module zip", &subject, warnings) { - Ok(archive) => archive, - Err(attempt) => return attempt, - }; + let mut archive = policy.settle(fetched, "module zip", &subject)?; // Extract the module zip (strip its literal `{module}@{version}/` // prefix) into a STAGE sibling of the copy dir and swap it into // place only once verified — the cargo / composer / gem shape: a @@ -502,10 +483,10 @@ async fn go_service_redirect( if let Err(e) = tokio::fs::create_dir_all(&stage).await { cleanup_failed_service_stage(&stage, project_root, base_rel, copy_dir, module, wired) .await; - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot create {}: {e}", stage.display()), - ); + )); } let zip_bytes = std::mem::take(&mut archive.bytes); let prefix_owned = prefix.clone(); @@ -516,40 +497,36 @@ async fn go_service_redirect( { cleanup_failed_service_stage(&stage, project_root, base_rel, copy_dir, module, wired) .await; - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_extract_failed", format!("cannot extract the prebuilt module zip: {e}"), - ); + )); } } // A `replace` target needs a go.mod declaring the module path; // pre-modules zips may lack one — synthesize the minimal form. if let Err(e) = ensure_module_go_mod(&stage, module).await { cleanup_failed_service_stage(&stage, project_root, base_rel, copy_dir, module, wired).await; - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot synthesize go.mod for the copy: {e}"), - ); + )); } if !copy_matches_after_hashes(&stage, &record.files).await { cleanup_failed_service_stage(&stage, project_root, base_rel, copy_dir, module, wired).await; - return policy.miss( - warnings, - "vendor_prebuilt_layout_mismatch", - format!( - "prebuilt module zip for {module} extracted to an \ + return Err(policy.miss(format!( + "prebuilt module zip for {module} extracted to an \ unexpected layout (patched files absent at their \ recorded paths)" - ), - ); + ))); } let file_inventory = inventory_or_warn(&stage, &format!("{module}@{version}"), warnings).await; if let Err(e) = swap_stage_into_place(&stage, copy_dir).await { cleanup_failed_service_stage(&stage, project_root, base_rel, copy_dir, module, wired).await; - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot move the extracted module into place: {e}"), - ); + )); } if let Err(e) = go_mod_edit::ensure_replace_entry(project_root, module, version, base_rel, false).await @@ -562,13 +539,13 @@ async fn go_service_redirect( if !wired { teardown_failed_service_copy(project_root, base_rel, module, false).await; } - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_wire_failed", format!("failed to update go.mod: {e}"), - ); + )); } warnings.push(archive.downloaded_warning(module)); - GoServiceRedirect::Used(file_inventory) + Ok(file_inventory) } /// Failure cleanup for the service legs (the vendor-side sibling of the diff --git a/crates/socket-patch-core/src/vendor/jvm/gradle.rs b/crates/socket-patch-core/src/vendor/jvm/gradle.rs index 3ccb053ac..95664cf9b 100644 --- a/crates/socket-patch-core/src/vendor/jvm/gradle.rs +++ b/crates/socket-patch-core/src/vendor/jvm/gradle.rs @@ -1459,15 +1459,10 @@ pub(crate) fn apply_line(t: &WiringTarget, kotlin: bool, prefix: &str) -> String /// DSL, counts (a user who reformatted the line keeps it, and a trailing /// comment such as a digest is ignored); one inside a comment does not. pub(crate) fn has_apply_line(text: &str, dsl: Dsl, t: &WiringTarget, prefix: &str) -> bool { - apply_line_token(text, dsl, t, prefix).is_some() -} - -/// The `apply` token of the first live apply line of `t`'s script. -fn apply_line_token(text: &str, dsl: Dsl, t: &WiringTarget, prefix: &str) -> Option { let path = format!("{prefix}{}", t.script_rel); let toks = dsl::tokens(text, dsl); (0..toks.len()) - .find(|&i| { + .any(|i| { if !is_ident(toks.get(i), "apply") { return false; } @@ -1479,25 +1474,6 @@ fn apply_line_token(text: &str, dsl: Dsl, t: &WiringTarget, prefix: &str) -> Opt && (is_punct(toks.get(j + 1), b':') || is_punct(toks.get(j + 1), b'=')) && matches!(toks.get(j + 2), Some(Token { tok: Tok::Str { value, .. }, .. }) if *value == path) }) - .map(|i| toks[i].start) -} - -/// `text` without the whole line holding the first live apply line of -/// `t`'s script (whatever follows it on that line, e.g. an older digest). -#[allow(dead_code)] // The hosted planner's digest rewrite. -pub(crate) fn remove_apply_line( - text: &str, - dsl: Dsl, - t: &WiringTarget, - prefix: &str, -) -> Option { - let at = apply_line_token(text, dsl, t, prefix)?; - let start = text[..at].rfind('\n').map_or(0, |i| i + 1); - if !text[start..at].trim().is_empty() { - return None; - } - let end = text[at..].find('\n').map_or(text.len(), |i| at + i + 1); - Some(format!("{}{}", &text[..start], &text[end..])) } fn append_line(text: &str, line: &str) -> String { @@ -3873,7 +3849,7 @@ mod tests { } /// The vendored defaults keep the historic line; the hosted target's - /// digest comment is ignored by the match and cut with its line. + /// digest comment is ignored by the match. #[test] fn wiring_target_spells_and_finds_apply_lines() { let vendored = WiringTarget::vendored(); @@ -3895,10 +3871,6 @@ mod tests { let newer = hosted("fedcba9876543210"); assert!(has_apply_line(&text, Dsl::Kotlin, &newer, "")); assert!(!has_apply_line(&text, Dsl::Kotlin, &vendored, "")); - assert_eq!( - remove_apply_line(&text, Dsl::Kotlin, &newer, "").as_deref(), - Some("rootProject.name = \"x\"\r\ninclude(\"a\")\r\n") - ); assert!(newer.owns_repository("socketPatchHosted_abc")); assert!(!newer.owns_repository("socketPatchHostedX")); assert_eq!(remove_line("a\n b \nc\n", "b").as_deref(), Some("a\nc\n")); diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/python_lock_union_tests.rs b/crates/socket-patch-core/src/vendor/lock_inventory/python_lock_union_tests.rs index 8b41d8b96..2169a44c2 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/python_lock_union_tests.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/python_lock_union_tests.rs @@ -122,36 +122,23 @@ async fn python_document_recovery_canonicalizes_the_purl_name() { "lock-version = '1.0'\n[[packages]]\nname = 'pyyaml'\nversion = '6.0.1'\narchive = {{ url = 'https://pypi.org/PyYAML-6.0.1-py3-none-any.whl', hashes = {{ sha256 = '{WHEEL_SHA}' }} }}\n" ); let entry = crate::vendor::state::VendorEntry { - ecosystem: "pypi".into(), - base_purl: "pkg:pypi/PyYAML@6.0.1".into(), - uuid: "11111111-1111-4111-8111-111111111111".into(), - artifact: crate::vendor::state::VendorArtifact { +flavor: Some("python-lock".into()), +..crate::vendor::state::VendorEntry::new("pypi".into(), "pkg:pypi/PyYAML@6.0.1".into(), "11111111-1111-4111-8111-111111111111".into(), crate::vendor::state::VendorArtifact { yarn_berry10c0: None, path: ".socket/vendor/pypi/11111111-1111-4111-8111-111111111111/PyYAML-6.0.1-py3-none-any.whl".into(), sha256: String::new(), size: None, platform_locked: None, file_inventory: None, - }, - wiring: vec![crate::vendor::state::WiringRecord { + }, vec![crate::vendor::state::WiringRecord { file: "pylock.toml".into(), kind: "python_lock_document".into(), action: crate::vendor::state::WiringAction::Rewritten, key: Some("pyyaml".into()), original: Some(serde_json::Value::String(lock)), new: None, - }], - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: Some("python-lock".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }; + }]) +}; let recovered = recover_lock_entry(tmp.path(), &entry).await.unwrap(); assert_eq!( recovered.resolved.as_deref(), diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/recover_tests.rs b/crates/socket-patch-core/src/vendor/lock_inventory/recover_tests.rs index ebe3cad9d..af62fbd16 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/recover_tests.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/recover_tests.rs @@ -5,11 +5,11 @@ use super::*; const UUID: &str = "9f6b2c4e-1d3a-4f6b-8c2d-7e5a9b1c3d5f"; fn entry(eco: &str, base_purl: &str, wiring: Vec) -> VendorEntry { - VendorEntry { - ecosystem: eco.into(), - base_purl: base_purl.into(), - uuid: UUID.into(), - artifact: VendorArtifact { + VendorEntry::new( + eco.into(), + base_purl.into(), + UUID.into(), + VendorArtifact { yarn_berry10c0: None, path: format!(".socket/vendor/{eco}/{UUID}/x"), sha256: String::new(), @@ -18,17 +18,7 @@ fn entry(eco: &str, base_purl: &str, wiring: Vec) -> VendorEntry { file_inventory: None, }, wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + ) } fn rec(kind: &str, original: serde_json::Value) -> WiringRecord { diff --git a/crates/socket-patch-core/src/vendor/maven_repo.rs b/crates/socket-patch-core/src/vendor/maven_repo.rs index 8615b7c5b..011d22cbe 100644 --- a/crates/socket-patch-core/src/vendor/maven_repo.rs +++ b/crates/socket-patch-core/src/vendor/maven_repo.rs @@ -31,7 +31,7 @@ use super::common::{ }; use super::path::vendor_uuid_dir_rel; use super::revert::{self, KeepPolicy}; -use super::service_fetch::{service_archive_copy, ServiceCopy}; +use super::service_fetch::service_archive_copy; use super::state::{VendorArtifact, VendorEntry, WiringAction, WiringRecord}; use super::{RevertOpts, RevertOutcome, VendorOutcome, VendorServiceConfig, VendorWarning}; @@ -163,11 +163,11 @@ fn maven_entry( jar_bytes: &[u8], wiring: Vec, ) -> VendorEntry { - VendorEntry { - ecosystem: "maven".to_string(), + VendorEntry::new( + "maven".to_string(), base_purl, - uuid: record.uuid.clone(), - artifact: VendorArtifact { + record.uuid.clone(), + VendorArtifact { yarn_berry10c0: None, // A `.jar` is a single verifiable file; record its plain sha256 for // tooling (harvest re-derives per-entry git hashes from the zip, so @@ -179,17 +179,7 @@ fn maven_entry( file_inventory: None, }, wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + ) } /// Revert a Maven vendor entry. A JVM entry goes to the planner's revert; @@ -793,11 +783,11 @@ async fn vendor_maven_jvm( match service_archive_copy(service, record, &artifact_id, ".jar", &mut warnings) .await { - ServiceCopy::Used(bytes) => ( + Ok(bytes) => ( bytes, already_patched_result(purl, &display_path, &record.files), ), - ServiceCopy::HardFail(outcome) => return *outcome, + Err(outcome) => return *outcome, }; if !result.success { return done(result, None, warnings); diff --git a/crates/socket-patch-core/src/vendor/mod.rs b/crates/socket-patch-core/src/vendor/mod.rs index f69750f01..4fcf8aad9 100644 --- a/crates/socket-patch-core/src/vendor/mod.rs +++ b/crates/socket-patch-core/src/vendor/mod.rs @@ -292,9 +292,9 @@ const ARCHIVE_PREFETCH_BYTES: usize = 128 * 1024 * 1024; impl VendorServiceConfig { /// Whether this run may actually attempt a service download right now: - /// the mode permits it, we're online, and a client is configured. + /// we're online and a client is configured. pub fn service_enabled(&self) -> bool { - self.source.may_use_service() && !self.offline && self.client.is_some() + !self.offline && self.client.is_some() } /// Whether a run through this config would prefetch service downloads diff --git a/crates/socket-patch-core/src/vendor/npm_common.rs b/crates/socket-patch-core/src/vendor/npm_common.rs index 508e6c2c1..cb743b1f0 100644 --- a/crates/socket-patch-core/src/vendor/npm_common.rs +++ b/crates/socket-patch-core/src/vendor/npm_common.rs @@ -1,24 +1,14 @@ -//! Flavor-agnostic npm vendoring pipeline: coordinate guards plus the shared -//! stage→patch→pack steps. -//! -//! Every tarball-artifact npm flavor (package-lock, yarn classic/berry, pnpm -//! incl. legacy, bun) vendors the same way up to the wiring: validate the -//! coordinates fail-closed, stage a private copy of the installed package in -//! a tempdir OUTSIDE the project, prune nested `node_modules`, refuse -//! bundled-deps packages, run the hardened apply pipeline against the stage, -//! and pack the result into a deterministic tarball under -//! `.socket/vendor/npm//`. Only the lockfile wiring differs per flavor, -//! and it always runs LAST — so a refusal or failure in this pipeline leaves -//! the project byte-untouched (a dry run stops after verification and -//! creates nothing on disk). vlt shares the coordinate guards but vendors a -//! directory artifact instead (see [`super::npm_dir`]). +//! Shared npm vendoring: validate coordinates, reuse a verified committed +//! artifact or download one from the patch service, then wire the lockfile. +//! Wiring runs last; dry runs verify without changing project files. +//! vlt shares the guards but stores a directory artifact (see [`super::npm_dir`]). use std::path::{Path, PathBuf}; use serde_json::Value; use crate::manifest::schema::PatchRecord; -use crate::patch::apply::{normalize_file_path, ApplyResult, PatchSources}; +use crate::patch::apply::{normalize_file_path, ApplyResult}; use crate::patch::copy_tree::remove_tree; use crate::patch::package::read_archive_to_map; use crate::patch::path_safety; @@ -32,10 +22,7 @@ use super::npm_dir; use super::npm_pack::PackedTarball; use super::path::vendor_uuid_dir_rel; use super::reuse; -use super::service_fetch::{ - fetch_verified_archive, ServiceArtifact, ServiceAttempt, ServicePolicy, ServiceTerminal, -}; -use super::source::PackageSource; +use super::service_fetch::{fetch_verified_archive, ServiceArtifact, ServicePolicy}; use super::state::{ write_marker_or_warn, PnpmMeta, VendorArtifact, VendorEntry, VendorMarker, WiringRecord, }; @@ -243,15 +230,11 @@ pub async fn npm_tarball_gitignore_preflight( )) } -#[allow(clippy::too_many_arguments)] pub(super) async fn stage_patch_pack( purl: &str, - installed_dir: PackageSource<'_>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, dry_run: bool, - force: bool, warnings: &mut Vec, service: Option<&VendorServiceConfig>, ) -> Result<(Option, ApplyResult), Box> { @@ -264,18 +247,8 @@ pub(super) async fn stage_patch_pack( npm_dir::gitignored_detail(&coords.uuid_dir_rel, &rules), ))); } - let (staged, result) = acquire_patch_pack( - purl, - installed_dir, - project_root, - record, - sources, - dry_run, - force, - warnings, - service, - ) - .await?; + let (staged, result) = + acquire_patch_pack(purl, project_root, record, dry_run, warnings, service).await?; if let Some(staged) = &staged { keep_pack_committable(purl, project_root, &coords, staged, warnings).await?; } @@ -339,15 +312,11 @@ async fn keep_pack_committable( } } -#[allow(clippy::too_many_arguments)] async fn acquire_patch_pack( purl: &str, - _installed_dir: PackageSource<'_>, project_root: &Path, record: &PatchRecord, - _sources: &PatchSources<'_>, dry_run: bool, - _force: bool, warnings: &mut Vec, service: Option<&VendorServiceConfig>, ) -> Result<(Option, ApplyResult), Box> { @@ -363,12 +332,9 @@ async fn acquire_patch_pack( } if let Some(cfg) = service { if cfg.service_enabled() { - match try_service_pack(purl, project_root, &coords, record, cfg, dry_run, warnings) + return try_service_pack(purl, project_root, &coords, record, cfg, dry_run, warnings) .await - { - ServicePackDecision::Used(pair) => return Ok(*pair), - ServicePackDecision::HardFail(outcome) => return Err(outcome), - } + .map(|pair| *pair); } } @@ -445,12 +411,7 @@ async fn reuse_committed_pack( // ───────────────────────── service-download path ───────────────────────── -/// Outcome of attempting the service-download fast path in [`stage_patch_pack`] -/// (`Used`: the staged pack + a synthesized success, boxed — the pair is large). -type ServicePackDecision = ServiceAttempt, ApplyResult)>>; - -/// Download + verify the prebuilt tarball and turn it into an [`NpmStagedPack`], -/// mapping each service outcome onto the `auto` / `service` fallback policy. +/// Download and verify a prebuilt tarball, then stage it for lockfile wiring. async fn try_service_pack( purl: &str, project_root: &Path, @@ -459,37 +420,30 @@ async fn try_service_pack( cfg: &VendorServiceConfig, dry_run: bool, warnings: &mut Vec, -) -> ServicePackDecision { - let policy = ServicePolicy::new(cfg, ServiceTerminal::Failure(purl)); +) -> Result, ApplyResult)>, Box> { + let policy = ServicePolicy::Failure(purl); let archive = match fetch_verified_archive(cfg, &record.uuid).await { // This backend's `service` refusal words a request failure differently. - ServiceArtifact::Failed(reason) if cfg.source.requires_service() => { - return policy.hard( + ServiceArtifact::Failed(reason) => { + return Err(policy.hard( "vendor_prebuilt_required", format!("patch service request failed: {reason}"), - ); + )); } - fetched => match policy.settle(fetched, "artifact", "artifact", warnings) { - Ok(archive) => archive, - Err(attempt) => return attempt, - }, + fetched => policy.settle(fetched, "artifact", "artifact")?, }; // The SRI proves only that the transfer is intact: require the tarball to // carry every patched file at its afterHash before reporting the package // patched and wiring the lock to it. if !tgz_bytes_match_after_hashes(&archive.bytes, record) { - return policy.miss( - warnings, - "vendor_prebuilt_layout_mismatch", - format!( - "prebuilt tarball for {}@{} does not carry the patched files at their \ + return Err(policy.miss(format!( + "prebuilt tarball for {}@{} does not carry the patched files at their \ recorded paths", - coords.name, coords.version - ), - ); + coords.name, coords.version + ))); } if dry_run { - return ServicePackDecision::Used(Box::new(( + return Ok(Box::new(( None, super::common::preview_result( purl, @@ -498,7 +452,7 @@ async fn try_service_pack( ), ))); } - match staged_pack_from_service_bytes( + let mut staged = staged_pack_from_service_bytes( purl, project_root, coords, @@ -506,22 +460,14 @@ async fn try_service_pack( &archive.bytes, &archive.integrity_sri, ) - .await - { - Ok(mut staged) => { - warnings.push( - archive.downloaded_warning(format_args!("{}@{}", coords.name, coords.version)), - ); - staged.packed.yarn_berry10c0 = archive.yarn_berry10c0; - // No local apply to verify — every patched file reads as - // `AlreadyPatched` (the tarball's members were checked against - // their afterHashes above). - let result = - already_patched_result(purl, &project_root.join(&staged.rel_tgz), &record.files); - ServicePackDecision::Used(Box::new((Some(staged), result))) - } - Err(outcome) => ServicePackDecision::HardFail(outcome), - } + .await?; + warnings.push(archive.downloaded_warning(format_args!("{}@{}", coords.name, coords.version))); + staged.packed.yarn_berry10c0 = archive.yarn_berry10c0; + // No local apply to verify — every patched file reads as + // `AlreadyPatched` (the tarball's members were checked against + // their afterHashes above). + let result = already_patched_result(purl, &project_root.join(&staged.rel_tgz), &record.files); + Ok(Box::new((Some(staged), result))) } async fn staged_pack_from_service_bytes( @@ -795,16 +741,11 @@ pub(super) async fn finish_vendored( /// receives it. pub(super) struct NpmVendorRequest<'a> { pub purl: &'a str, - /// The crawler's `node_modules/` dir (or a service-only source); - /// read-only — patching happens on a staged copy. - pub installed_dir: PackageSource<'a>, pub project_root: &'a Path, pub record: &'a PatchRecord, - pub sources: &'a PatchSources<'a>, /// RFC3339 timestamp for the informational marker. pub vendored_at: &'a str, pub dry_run: bool, - pub force: bool, pub service: Option<&'a VendorServiceConfig>, } @@ -885,13 +826,10 @@ pub(super) async fn vendor_npm_family( ) -> VendorOutcome { let NpmVendorRequest { purl, - installed_dir, project_root, record, - sources, vendored_at, dry_run, - force, service, } = req; let mut warnings: Vec = Vec::new(); @@ -910,25 +848,11 @@ pub(super) async fn vendor_npm_family( Err(outcome) => return *outcome, }; - // Stage → patch → pack: tempdir stage outside the project, nested - // node_modules prune, bundled-deps refusal, hardened apply, - // deterministic pack. - let (staged, result) = match stage_patch_pack( - purl, - installed_dir, - project_root, - record, - sources, - dry_run, - force, - &mut warnings, - service, - ) - .await - { - Ok(pair) => pair, - Err(outcome) => return *outcome, - }; + let (staged, result) = + match stage_patch_pack(purl, project_root, record, dry_run, &mut warnings, service).await { + Ok(pair) => pair, + Err(outcome) => return *outcome, + }; let Some(mut staged) = staged else { // Failed patch (wiring is last, so the project is byte-untouched) // or a dry run (stops after the verify). @@ -1283,21 +1207,8 @@ mod tests { record: &PatchRecord, service: Option<&VendorServiceConfig>, ) -> Result<(Option, ApplyResult), Box> { - let blobs = root.join(".socket/blobs"); - let sources = PatchSources::blobs_only(&blobs); let mut warnings = Vec::new(); - stage_patch_pack( - LP_PURL, - (&root.join("node_modules/left-pad")).into(), - root, - record, - &sources, - false, - false, - &mut warnings, - service, - ) - .await + stage_patch_pack(LP_PURL, root, record, false, &mut warnings, service).await } async fn build_tgz(files: &[(&str, &[u8])]) -> Vec { @@ -1529,8 +1440,7 @@ mod tests { } /// A served tarball with an intact SRI whose `index.js` is still the - /// ORIGINAL bytes is not the patched package: `service` fails the package - /// and writes nothing; `auto` warns and builds locally instead. + /// ORIGINAL bytes is not the patched package and must be refused. #[tokio::test] async fn service_tarball_failing_after_hashes_is_rejected() { let tgz = build_tgz(&[("index.js", ORIG_INDEX)]).await; @@ -1549,23 +1459,10 @@ mod tests { let (tmp, record) = local_fixture(b"{\"name\":\"left-pad\",\"version\":\"1.3.0\"}").await; let root = tmp.path(); - let blobs = root.join(".socket/blobs"); - let sources = PatchSources::blobs_only(&blobs); let mut warnings = Vec::new(); let cfg = service_cfg(&server.uri(), VendorSource::Service); let err = expect_err( - stage_patch_pack( - LP_PURL, - (&root.join("node_modules/left-pad")).into(), - root, - &record, - &sources, - false, - false, - &mut warnings, - Some(&cfg), - ) - .await, + stage_patch_pack(LP_PURL, root, &record, false, &mut warnings, Some(&cfg)).await, ); expect_done_failure(err, "does not carry the patched files"); assert!(!root.join(".socket/vendor").exists()); @@ -1690,28 +1587,21 @@ mod tests { .unwrap_or_else(|e| panic!("{e:?}")); assert!(fresh.verified_bytes.is_none()); let entry = crate::vendor::state::VendorEntry { - ecosystem: "npm".into(), - base_purl: LP_PURL.into(), - uuid: record.uuid.clone(), - artifact: crate::vendor::state::VendorArtifact { - yarn_berry10c0: None, - path: fresh.rel_tgz.clone(), - sha256: fresh.packed.sha256_hex.clone(), - size: Some(fresh.packed.size), - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, flavor: Some("yarn-berry".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - detached: false, - record: None, + ..crate::vendor::state::VendorEntry::new( + "npm".into(), + LP_PURL.into(), + record.uuid.clone(), + crate::vendor::state::VendorArtifact { + yarn_berry10c0: None, + path: fresh.rel_tgz.clone(), + sha256: fresh.packed.sha256_hex.clone(), + size: Some(fresh.packed.size), + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) }; crate::vendor::test_support::persist(tmp.path(), LP_PURL, entry).await; let coords = guard_coordinates(LP_PURL, &record).unwrap(); @@ -1830,18 +1720,13 @@ mod tests { } let server = granted_service().await; let cfg = service_cfg(&server.uri(), VendorSource::Service); - let blobs = tmp.path().join(".socket/blobs"); - let sources = PatchSources::blobs_only(&blobs); let mut warnings = Vec::new(); let err = expect_err( stage_patch_pack( LP_PURL, - (&tmp.path().join("node_modules/left-pad")).into(), tmp.path(), &patched_index_record(), - &sources, dry_run, - false, &mut warnings, Some(&cfg), ) diff --git a/crates/socket-patch-core/src/vendor/npm_dir.rs b/crates/socket-patch-core/src/vendor/npm_dir.rs index b3362ce29..ff5df4639 100644 --- a/crates/socket-patch-core/src/vendor/npm_dir.rs +++ b/crates/socket-patch-core/src/vendor/npm_dir.rs @@ -26,7 +26,7 @@ use std::path::{Path, PathBuf}; use serde_json::Value; use crate::manifest::schema::PatchRecord; -use crate::patch::apply::{normalize_file_path, ApplyResult, PatchSources}; +use crate::patch::apply::{normalize_file_path, ApplyResult}; use crate::patch::copy_tree::{fresh_copy, remove_tree}; use crate::utils::fs::atomic_write_bytes; @@ -34,10 +34,7 @@ use super::common::{already_patched_result, refused, service_offline_conflict}; use super::npm_common::{ declares_bundled_deps, done_failure, done_failure_unstage, guard_coordinates, }; -use super::service_fetch::{ - fetch_verified_archive, ServiceAttempt, ServicePolicy, ServiceTerminal, -}; -use super::source::PackageSource; +use super::service_fetch::{fetch_verified_archive, ServicePolicy}; use super::state::VENDOR_MARKER_FILE; use super::vlt_lock_text::vendored_dir_rel; use super::{VendorOutcome, VendorServiceConfig, VendorWarning}; @@ -530,20 +527,15 @@ pub(crate) fn gitignored_detail(rel: &str, rules: &str) -> String { // ── pipeline ───────────────────────────────────────────────────────────── -/// Reuse the committed dir, else build it from the patch -/// service or the installed copy, then write it into place. Same result +/// Reuse a verified committed directory, or acquire one from the patch service. Same result /// shape as [`super::npm_common::stage_patch_pack`]: `Err` is a refusal or /// a failure with the project untouched, `Ok((None, _))` a failed patch or /// a dry run, `Ok((Some(dir), _))` the artifact on disk. -#[allow(clippy::too_many_arguments)] pub(super) async fn stage_patch_dir( purl: &str, - _installed_dir: PackageSource<'_>, project_root: &Path, record: &PatchRecord, - _sources: &PatchSources<'_>, dry_run: bool, - _force: bool, warnings: &mut Vec, service: Option<&VendorServiceConfig>, ) -> Result<(Option, ApplyResult), Box> { @@ -556,7 +548,6 @@ pub(super) async fn stage_patch_dir( .keys() .any(|k| normalize_file_path(k) == "package.json"); - let reusable = false; match super::reuse::reusable_committed_dir(project_root, record, &rel_dir).await { Ok(inventory) => { if !dry_run { @@ -589,7 +580,7 @@ pub(super) async fn stage_patch_dir( Err(miss) => super::reuse::log_miss(purl, &miss), } - if let Some(refusal) = service_offline_conflict(service).filter(|_| !reusable) { + if let Some(refusal) = service_offline_conflict(service) { return Err(Box::new(refusal)); } @@ -600,33 +591,22 @@ pub(super) async fn stage_patch_dir( )) })?; let stage = stage_tmp.path().join("stage"); - let mut result = None; - if let Some(cfg) = service.filter(|cfg| cfg.service_enabled()) { - match try_service_dir( - purl, - record, - cfg, - &stage, - &coords.name, - &coords.version, - warnings, - ) - .await - { - ServiceDir::Used(()) => { - result = Some(already_patched_result(purl, &rel_abs, &record.files)); - } - ServiceDir::HardFail(outcome) => return Err(outcome), - } - } - let result = match result { - Some(result) => { - prune_staged_node_modules(purl, &stage, &coords.name, &coords.version).await?; - apply_transforms(&stage, &coords.name, &coords.version).await?; - result - } - None => return Err(Box::new(super::service_fetch::required())), - }; + let cfg = service + .filter(|cfg| cfg.service_enabled()) + .ok_or_else(|| Box::new(super::service_fetch::required()))?; + try_service_dir( + purl, + record, + cfg, + &stage, + &coords.name, + &coords.version, + warnings, + ) + .await?; + let result = already_patched_result(purl, &rel_abs, &record.files); + prune_staged_node_modules(purl, &stage, &coords.name, &coords.version).await?; + apply_transforms(&stage, &coords.name, &coords.version).await?; if dry_run { return Ok((None, result)); } @@ -853,8 +833,6 @@ async fn tree_matches_after_hashes(stage: &Path, record: &PatchRecord) -> bool { true } -type ServiceDir = ServiceAttempt<()>; - /// The service fast path: the prebuilt tarball, integrity- and /// afterHash-verified, extracted into `stage` with its first path component /// stripped whatever it is called. The fallback policy is the tarball @@ -867,13 +845,10 @@ pub(super) async fn try_service_dir( name: &str, version: &str, warnings: &mut Vec, -) -> ServiceDir { - let policy = ServicePolicy::new(cfg, ServiceTerminal::Failure(purl)); +) -> Result<(), Box> { + let policy = ServicePolicy::Failure(purl); let fetched = fetch_verified_archive(cfg, &record.uuid).await; - let archive = match policy.settle(fetched, "artifact", "artifact", warnings) { - Ok(archive) => archive, - Err(attempt) => return attempt, - }; + let archive = policy.settle(fetched, "artifact", "artifact")?; let downloaded = archive.downloaded_warning(format_args!("{name}@{version}")); let (bytes, dest) = (archive.bytes, stage.to_path_buf()); let extracted = tokio::task::spawn_blocking(move || { @@ -883,24 +858,20 @@ pub(super) async fn try_service_dir( .map_err(|e| e.to_string()) .and_then(|r| r); if let Err(e) = extracted { - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_extract_failed", format!("prebuilt tarball for {name}@{version} is unsafe: {e}"), - ); + )); } if !tree_matches_after_hashes(stage, record).await { let _ = remove_tree(stage).await; - return policy.miss( - warnings, - "vendor_prebuilt_layout_mismatch", - format!( - "prebuilt tarball for {name}@{version} does not carry the patched files \ + return Err(policy.miss(format!( + "prebuilt tarball for {name}@{version} does not carry the patched files \ at their recorded paths" - ), - ); + ))); } warnings.push(downloaded); - ServiceDir::Used(()) + Ok(()) } #[cfg(test)] diff --git a/crates/socket-patch-core/src/vendor/npm_flavor.rs b/crates/socket-patch-core/src/vendor/npm_flavor.rs index ba0860dbc..fdb0c4d73 100644 --- a/crates/socket-patch-core/src/vendor/npm_flavor.rs +++ b/crates/socket-patch-core/src/vendor/npm_flavor.rs @@ -2044,28 +2044,21 @@ mod tests { fn probe_entry(flavor: Option<&str>) -> VendorEntry { VendorEntry { - ecosystem: "npm".into(), - base_purl: "pkg:npm/left-pad@1.3.0".into(), - uuid: UUID.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/npm/{UUID}/left-pad-1.3.0.tgz"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: flavor.map(str::to_string), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "npm".into(), + "pkg:npm/left-pad@1.3.0".into(), + UUID.into(), + VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/npm/{UUID}/left-pad-1.3.0.tgz"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) } } diff --git a/crates/socket-patch-core/src/vendor/npm_lock.rs b/crates/socket-patch-core/src/vendor/npm_lock.rs index 59f11b5c4..abe9a9018 100644 --- a/crates/socket-patch-core/src/vendor/npm_lock.rs +++ b/crates/socket-patch-core/src/vendor/npm_lock.rs @@ -97,26 +97,23 @@ const DEP_MANIFEST_FIELDS: [&str; 4] = [ #[allow(clippy::too_many_arguments)] pub async fn vendor_npm<'a>( purl: &str, - installed_dir: impl Into>, + _installed_dir: impl Into>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, + _sources: &PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, + _force: bool, service: Option<&super::VendorServiceConfig>, ) -> VendorOutcome { vendor_npm_family( &PackageLockBackend, NpmVendorRequest { purl, - installed_dir: installed_dir.into(), project_root, record, - sources, vendored_at, dry_run, - force, service, }, ) @@ -5168,11 +5165,11 @@ mod tests { fx.lock_bytes ); // And revert refuses to delete through a tampered uuid too. - let entry = VendorEntry { - ecosystem: "npm".into(), - base_purl: fx.purl(), - uuid: "../../x".into(), - artifact: VendorArtifact { + let entry = VendorEntry::new( + "npm".into(), + fx.purl(), + "../../x".into(), + VendorArtifact { yarn_berry10c0: None, path: "whatever".into(), sha256: String::new(), @@ -5180,18 +5177,8 @@ mod tests { platform_locked: None, file_inventory: None, }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }; + Vec::new(), + ); let outcome = revert_npm(&entry, fx.root(), false).await; assert!(!outcome.success, "tampered uuid must fail closed"); } diff --git a/crates/socket-patch-core/src/vendor/nuget_feed.rs b/crates/socket-patch-core/src/vendor/nuget_feed.rs index 13c335489..0bd8729ad 100644 --- a/crates/socket-patch-core/src/vendor/nuget_feed.rs +++ b/crates/socket-patch-core/src/vendor/nuget_feed.rs @@ -20,7 +20,7 @@ use super::common::{ use super::parse_memo::ParseMemo; use super::path::vendor_uuid_dir_rel; use super::revert::{self, KeepPolicy}; -use super::service_fetch::{service_archive_copy, ServiceCopy}; +use super::service_fetch::service_archive_copy; use super::state::{ write_marker_or_warn, VendorArtifact, VendorEntry, VendorMarker, WiringAction, WiringRecord, }; @@ -650,11 +650,11 @@ fn nuget_entry( nupkg_bytes: &[u8], wiring: Vec, ) -> VendorEntry { - VendorEntry { - ecosystem: "nuget".to_string(), + VendorEntry::new( + "nuget".to_string(), base_purl, - uuid: record.uuid.clone(), - artifact: VendorArtifact { + record.uuid.clone(), + VendorArtifact { yarn_berry10c0: None, // A `.nupkg` is a single verifiable file; record its plain sha256 // for tooling (harvest re-derives per-entry git hashes from the @@ -666,17 +666,7 @@ fn nuget_entry( file_inventory: None, }, wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + ) } /// Revert a NuGet vendor entry: undo the lock pin, restore/delete the @@ -801,7 +791,7 @@ async fn materialise_patched_nupkg( warnings: &mut Vec, ) -> Result<(Vec, ApplyResult), Box> { match service_archive_copy(service, record, name, ".nupkg", warnings).await { - ServiceCopy::Used(bytes) => { + Ok(bytes) => { if let Err(e) = write_nupkg(uuid_dir, nupkg_path, &bytes).await { if !config_wired { let _ = remove_tree(uuid_dir).await; @@ -814,7 +804,7 @@ async fn materialise_patched_nupkg( already_patched_result(purl, nupkg_path, &record.files), )) } - ServiceCopy::HardFail(outcome) => Err(outcome), + Err(outcome) => Err(outcome), } } @@ -2762,11 +2752,11 @@ mod tests { let root = dir.path().join("proj"); tokio::fs::create_dir_all(&root).await.unwrap(); - let entry = VendorEntry { - ecosystem: "nuget".to_string(), - base_purl: PURL.to_string(), - uuid: UUID.to_string(), - artifact: VendorArtifact { + let entry = VendorEntry::new( + "nuget".to_string(), + PURL.to_string(), + UUID.to_string(), + VendorArtifact { yarn_berry10c0: None, path: copy_rel(), sha256: String::new(), @@ -2774,7 +2764,7 @@ mod tests { platform_locked: None, file_inventory: None, }, - wiring: vec![WiringRecord { + vec![WiringRecord { file: "../outside.txt".to_string(), kind: CONFIG_SOURCE_WIRING_KIND.to_string(), action: WiringAction::Rewritten, @@ -2782,17 +2772,7 @@ mod tests { original: Some(Value::String("EVIL".to_string())), new: Some(Value::String("precious".to_string())), }], - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - }; + ); let outcome = revert_nuget(&entry, &root, false).await; assert!( !outcome.success, @@ -3576,11 +3556,11 @@ mod tests { // ── revert entry validation edges ────────────────────────────────────── fn entry_with_wiring(uuid: &str, wiring: Vec) -> VendorEntry { - VendorEntry { - ecosystem: "nuget".to_string(), - base_purl: PURL.to_string(), - uuid: uuid.to_string(), - artifact: VendorArtifact { + VendorEntry::new( + "nuget".to_string(), + PURL.to_string(), + uuid.to_string(), + VendorArtifact { yarn_berry10c0: None, path: copy_rel(), sha256: String::new(), @@ -3589,17 +3569,7 @@ mod tests { file_inventory: None, }, wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + ) } /// A tampered state.json uuid must refuse the revert fail-closed before diff --git a/crates/socket-patch-core/src/vendor/pnpm_lock.rs b/crates/socket-patch-core/src/vendor/pnpm_lock.rs index c843ae50e..11c38eaba 100644 --- a/crates/socket-patch-core/src/vendor/pnpm_lock.rs +++ b/crates/socket-patch-core/src/vendor/pnpm_lock.rs @@ -127,24 +127,21 @@ const REVERT_ALLOWLIST: [&str; 3] = [PNPM_LOCK, PACKAGE_JSON, PNPM_WORKSPACE]; #[allow(clippy::too_many_arguments)] pub async fn vendor_pnpm<'a>( purl: &str, - installed_dir: impl Into>, + _installed_dir: impl Into>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, + _sources: &PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, + _force: bool, service: Option<&super::VendorServiceConfig>, ) -> VendorOutcome { vendor_pnpm_dialect( purl, - installed_dir.into(), project_root, record, - sources, vendored_at, dry_run, - force, service, PnpmDialect::V9, ) @@ -172,16 +169,12 @@ impl PnpmDialect { } } -#[allow(clippy::too_many_arguments)] pub(super) async fn vendor_pnpm_dialect( purl: &str, - installed_dir: PackageSource<'_>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, service: Option<&super::VendorServiceConfig>, dialect: PnpmDialect, ) -> VendorOutcome { @@ -189,13 +182,10 @@ pub(super) async fn vendor_pnpm_dialect( &PnpmBackend { dialect }, NpmVendorRequest { purl, - installed_dir, project_root, record, - sources, vendored_at, dry_run, - force, service, }, ) diff --git a/crates/socket-patch-core/src/vendor/pnpm_lock_legacy.rs b/crates/socket-patch-core/src/vendor/pnpm_lock_legacy.rs index 990bdfcad..c87e1f689 100644 --- a/crates/socket-patch-core/src/vendor/pnpm_lock_legacy.rs +++ b/crates/socket-patch-core/src/vendor/pnpm_lock_legacy.rs @@ -201,24 +201,21 @@ impl Ctx<'_> { #[allow(clippy::too_many_arguments)] pub async fn vendor_pnpm_legacy<'a>( purl: &str, - installed_dir: impl Into>, + _installed_dir: impl Into>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, + _sources: &PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, + _force: bool, service: Option<&super::VendorServiceConfig>, ) -> VendorOutcome { super::pnpm_lock::vendor_pnpm_dialect( purl, - installed_dir.into(), project_root, record, - sources, vendored_at, dry_run, - force, service, PnpmDialect::Legacy, ) diff --git a/crates/socket-patch-core/src/vendor/pypi.rs b/crates/socket-patch-core/src/vendor/pypi.rs index 5baad640f..958b4fd35 100644 --- a/crates/socket-patch-core/src/vendor/pypi.rs +++ b/crates/socket-patch-core/src/vendor/pypi.rs @@ -39,9 +39,7 @@ use super::pypi_uv::{ }; use super::pypi_wheel::WheelArtifact; use super::reuse; -use super::service_fetch::{ - fetch_verified_archive, ServiceArtifact, ServiceAttempt, ServicePolicy, ServiceTerminal, -}; +use super::service_fetch::{fetch_verified_archive, ServiceArtifact, ServicePolicy}; use super::source::PackageSource; use super::state::{ write_marker_or_warn, PdmMeta, PipenvMeta, PoetryMeta, UvMeta, VendorArtifact, VendorEntry, @@ -822,7 +820,6 @@ async fn stale_install_sites( /// ahead of the vendor loop ([`service_preflight`]). struct PypiPrelude<'p> { base: &'p str, - raw_name: String, version: String, canon_name: String, uuid_dir_rel: String, @@ -1150,7 +1147,11 @@ async fn pypi_prelude<'p>( ), )); return Err(done( - reuse_preview_result(base, &project_root.join(&acquired.rel_wheel), record), + super::common::preview_result( + base, + &project_root.join(&acquired.rel_wheel), + &record.files, + ), None, warnings, )); @@ -1167,7 +1168,6 @@ async fn pypi_prelude<'p>( } Ok(PypiPrelude { base, - raw_name: raw_name.to_string(), version: version.to_string(), canon_name, uuid_dir_rel, @@ -1225,25 +1225,23 @@ pub(crate) async fn service_preflight( #[allow(clippy::too_many_arguments)] pub async fn vendor_pypi_with_pipenv_version<'a>( purl: &str, - site_packages: impl Into>, + _site_packages: impl Into>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, + _sources: &PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, + _force: bool, service: Option<&VendorServiceConfig>, pipenv_version: &tokio::sync::OnceCell>, installed_sites: &InstalledSiteListings, ) -> VendorOutcome { - let site_packages = site_packages.into(); let hosted_origins: Vec = service .and_then(|s| s.patch_server_url.clone()) .into_iter() .collect(); let PypiPrelude { base, - raw_name, version, canon_name, uuid_dir_rel, @@ -1268,12 +1266,9 @@ pub async fn vendor_pypi_with_pipenv_version<'a>( Ok(prelude) => prelude, Err(outcome) => return outcome, }; - let (raw_name, version) = (raw_name.as_str(), version.as_str()); + let version = version.as_str(); let reused = reused_wheel.is_some(); - // Acquire the patched wheel: prefer the prebuilt service artifact (which - // skips needing the package installed), else build it locally. A refusal / - // hard fail bubbles as a terminal outcome. let AcquiredWheel { wheel_name, rel_wheel, @@ -1285,15 +1280,10 @@ pub async fn vendor_pypi_with_pipenv_version<'a>( Some(acquired) => acquired, None => match acquire_patched_wheel( base, - raw_name, - version, - site_packages, &uuid_dir_rel, project_root, record, - sources, dry_run, - force, service, expected_pin.as_ref(), &mut warnings, @@ -1584,28 +1574,21 @@ pub async fn vendor_pypi_with_pipenv_version<'a>( }; let mut entry = VendorEntry { - ecosystem: "pypi".to_string(), - base_purl: base.to_string(), - uuid: record.uuid.clone(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: rel_wheel, - sha256: artifact.sha256_hex, - size: Some(artifact.size), - platform_locked: platform_locked.then_some(true), - file_inventory: None, - }, - wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some(flavor.as_str().to_string()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "pypi".to_string(), + base.to_string(), + record.uuid.clone(), + VendorArtifact { + yarn_berry10c0: None, + path: rel_wheel, + sha256: artifact.sha256_hex, + size: Some(artifact.size), + platform_locked: platform_locked.then_some(true), + file_inventory: None, + }, + wiring, + ) }; match meta { MetaSlot::Uv(m) => entry.uv = Some(m), @@ -2534,52 +2517,25 @@ fn reusable_wheel_leaf(leaf: &str, canon_name: &str, version: &str) -> bool { super::pypi_distribution::matches(leaf, canon_name, version) } -/// The dry-run preview of a Fresh-path reuse: the shape a dry-run local -/// build reports (every patched file verified, ready to wire) — the CLI -/// renders it as a verified preview, as it would the build. -fn reuse_preview_result(base: &str, abs: &Path, record: &PatchRecord) -> ApplyResult { - let files_verified = record - .files - .keys() - .map(|f| crate::patch::apply::VerifyResult { - file: f.clone(), - status: crate::patch::apply::VerifyStatus::Ready, - message: None, - current_hash: None, - expected_hash: None, - target_hash: None, - }) - .collect(); - super::common::synthesized_result(base, abs, files_verified, true, None) -} - struct AcquiredWheel { wheel_name: String, rel_wheel: String, result: ApplyResult, - /// `None` on a dry run or a failed build (the caller short-circuits). + /// `None` on a dry run. artifact: Option, platform_locked: bool, /// Tag list for the `vendor_platform_locked` advisory. platform_tags_display: String, } -/// Acquire the patched wheel: prefer the prebuilt service artifact (which does -/// not require the package to be installed), else build it locally from the -/// installed dist. Returns `Err(outcome)` with the terminal `VendorOutcome` to -/// bubble (a refusal, or a `service`-mode miss). +/// Acquire a verified prebuilt Python distribution from the patch service. #[allow(clippy::too_many_arguments)] async fn acquire_patched_wheel( base: &str, - _raw_name: &str, - _version: &str, - _site_packages: PackageSource<'_>, uuid_dir_rel: &str, project_root: &Path, record: &PatchRecord, - _sources: &PatchSources<'_>, dry_run: bool, - _force: bool, service: Option<&VendorServiceConfig>, expected_pin: Option<&(String, String)>, warnings: &mut Vec, @@ -2589,7 +2545,7 @@ async fn acquire_patched_wheel( } if let Some(cfg) = service { if cfg.service_enabled() { - match try_pypi_service_wheel( + return try_pypi_service_wheel( base, uuid_dir_rel, project_root, @@ -2600,10 +2556,8 @@ async fn acquire_patched_wheel( warnings, ) .await - { - PypiServiceWheel::Used(acq) => return Ok(*acq), - PypiServiceWheel::HardFail(outcome) => return Err(*outcome), - } + .map(|acq| *acq) + .map_err(|outcome| *outcome); } } @@ -2613,10 +2567,6 @@ async fn acquire_patched_wheel( )) } -/// Outcome of attempting a pypi service download (the wheel facts boxed — -/// they are large). -type PypiServiceWheel = ServiceAttempt>; - /// Download and verify the server wheel or sdist for `record.uuid`. #[allow(clippy::too_many_arguments)] async fn try_pypi_service_wheel( @@ -2628,25 +2578,18 @@ async fn try_pypi_service_wheel( dry_run: bool, expected_pin: Option<&(String, String)>, warnings: &mut Vec, -) -> PypiServiceWheel { - let policy = ServicePolicy::new(cfg, ServiceTerminal::Refused); +) -> Result, Box> { + let policy = ServicePolicy::Refused; let fetched = fetch_verified_archive(cfg, &record.uuid).await; // The client refused a non-wheel before downloading it. if let ServiceArtifact::Unavailable(reason) = &fetched { if reason == PYPI_NOT_A_WHEEL { - return policy.miss(warnings, "vendor_prebuilt_unavailable", reason.clone()); + return Err(policy.miss(reason.clone())); } } - let archive = match policy.settle(fetched, "wheel", "wheel", warnings) { - Ok(archive) => archive, - Err(attempt) => return attempt, - }; + let archive = policy.settle(fetched, "wheel", "wheel")?; let Some(wheel_name) = wheel_filename_from_url(&archive.source_url) else { - return policy.miss( - warnings, - "vendor_prebuilt_unavailable", - PYPI_NOT_A_WHEEL.to_string(), - ); + return Err(policy.miss(PYPI_NOT_A_WHEEL.to_string())); }; // The SRI proves only that the transfer is intact. A wheel's // members are site-packages-relative (the `record.files` keys), @@ -2656,55 +2599,47 @@ async fn try_pypi_service_wheel( .and_then(|members| super::pypi_distribution::verify_members(&members, &wheel_name, record)) .is_err() { - return policy.miss( - warnings, - "vendor_prebuilt_layout_mismatch", - format!( - "prebuilt wheel for {base} does not carry the patched files at \ + return Err(policy.miss(format!( + "prebuilt wheel for {base} does not carry the patched files at \ their recorded paths" - ), - ); + ))); } let Some((name, version)) = parse_pypi_purl(base) else { - return policy.hard("unsafe_coordinates", base.to_string()); + return Err(policy.hard("unsafe_coordinates", base.to_string())); }; if !super::pypi_distribution::matches(&wheel_name, &name, &version) { - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_layout_mismatch", "Python archive filename does not match the requested package".to_string(), - ); + )); } let rel_wheel = format!("{uuid_dir_rel}/{wheel_name}"); // Digested on first ask: pypi is the only backend that pins it. let sha256_hex = archive.sha256_hex().to_string(); if let Some((pin_path, pin_sha)) = expected_pin { if !pin_matches(pin_path, pin_sha, &rel_wheel, &sha256_hex) { - return policy.miss( - warnings, - "vendor_prebuilt_pin_mismatch", - format!( - "the prebuilt wheel ({rel_wheel}, sha256 {sha256_hex}) does not \ + return Err(policy.miss(format!( + "the prebuilt wheel ({rel_wheel}, sha256 {sha256_hex}) does not \ match the wheel the lockfile still pins ({pin_path}, sha256 \ {pin_sha})" - ), - ); + ))); } } let dest = project_root.join(uuid_dir_rel).join(&wheel_name); if !dry_run { if let Some(parent) = dest.parent() { if let Err(e) = tokio::fs::create_dir_all(parent).await { - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot create {}: {e}", parent.display()), - ); + )); } } if let Err(e) = atomic_write_artifact(&dest, &archive.bytes).await { - return policy.hard( + return Err(policy.hard( "vendor_prebuilt_write_failed", format!("cannot write the vendored wheel: {e}"), - ); + )); } } let (platform_locked, platform_tags_display) = if wheel_name.ends_with(".whl") { @@ -2713,7 +2648,7 @@ async fn try_pypi_service_wheel( (false, String::new()) }; warnings.push(archive.downloaded_warning(format_args!("the wheel for {base}"))); - PypiServiceWheel::Used(Box::new(AcquiredWheel { + Ok(Box::new(AcquiredWheel { rel_wheel, result: if dry_run { super::common::preview_result(base, &dest, &record.files) @@ -4076,28 +4011,21 @@ wheels = [ async fn revert_unknown_flavor_fails_closed() { let fx = e2e_fixture().await; let entry = VendorEntry { - ecosystem: "pypi".into(), - base_purl: "pkg:pypi/six@1.16.0".into(), - uuid: UUID.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/pypi/{UUID}/x.whl"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring: vec![], - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some("mystery".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "pypi".into(), + "pkg:pypi/six@1.16.0".into(), + UUID.into(), + VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/pypi/{UUID}/x.whl"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + vec![], + ) }; let outcome = revert_pypi(&entry, &fx.root, false).await; assert!(!outcome.success); @@ -4972,28 +4900,21 @@ wheels = [ /// A pypi-flavored [`VendorEntry`] carrying just what revert reads. fn revert_entry(flavor: &str, rel_wheel: &str, wiring: Vec) -> VendorEntry { VendorEntry { - ecosystem: "pypi".into(), - base_purl: "pkg:pypi/six@1.16.0".into(), - uuid: UUID.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: rel_wheel.to_string(), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some(flavor.into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "pypi".into(), + "pkg:pypi/six@1.16.0".into(), + UUID.into(), + VendorArtifact { + yarn_berry10c0: None, + path: rel_wheel.to_string(), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + wiring, + ) } } diff --git a/crates/socket-patch-core/src/vendor/pypi_pdm.rs b/crates/socket-patch-core/src/vendor/pypi_pdm.rs index 0ea65150f..752fd17a0 100644 --- a/crates/socket-patch-core/src/vendor/pypi_pdm.rs +++ b/crates/socket-patch-core/src/vendor/pypi_pdm.rs @@ -713,28 +713,22 @@ distribution = false fn entry_for(wiring: Vec, meta: PdmMeta) -> VendorEntry { VendorEntry { - ecosystem: "pypi".into(), - base_purl: "pkg:pypi/six@1.16.0".into(), - uuid: UUID.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: REL_WHEEL.into(), - sha256: WHEEL_SHA.into(), - size: Some(11053), - platform_locked: None, - file_inventory: None, - }, - wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some("pdm".into()), - uv: None, - pnpm: None, - poetry: None, pdm: Some(meta), - pipenv: None, + ..VendorEntry::new( + "pypi".into(), + "pkg:pypi/six@1.16.0".into(), + UUID.into(), + VendorArtifact { + yarn_berry10c0: None, + path: REL_WHEEL.into(), + sha256: WHEEL_SHA.into(), + size: Some(11053), + platform_locked: None, + file_inventory: None, + }, + wiring, + ) } } diff --git a/crates/socket-patch-core/src/vendor/pypi_pipenv.rs b/crates/socket-patch-core/src/vendor/pypi_pipenv.rs index 4b3cdec79..a835ab0cf 100644 --- a/crates/socket-patch-core/src/vendor/pypi_pipenv.rs +++ b/crates/socket-patch-core/src/vendor/pypi_pipenv.rs @@ -1039,28 +1039,22 @@ mod tests { fn entry_for(wiring: Vec, meta: PipenvMeta) -> VendorEntry { VendorEntry { - ecosystem: "pypi".into(), - base_purl: "pkg:pypi/six@1.16.0".into(), - uuid: UUID.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: REL_WHEEL.into(), - sha256: WHEEL_SHA.into(), - size: Some(11053), - platform_locked: None, - file_inventory: None, - }, - wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some("pipenv".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, pipenv: Some(meta), + ..VendorEntry::new( + "pypi".into(), + "pkg:pypi/six@1.16.0".into(), + UUID.into(), + VendorArtifact { + yarn_berry10c0: None, + path: REL_WHEEL.into(), + sha256: WHEEL_SHA.into(), + size: Some(11053), + platform_locked: None, + file_inventory: None, + }, + wiring, + ) } } diff --git a/crates/socket-patch-core/src/vendor/pypi_poetry.rs b/crates/socket-patch-core/src/vendor/pypi_poetry.rs index b290c1daf..b678bf631 100644 --- a/crates/socket-patch-core/src/vendor/pypi_poetry.rs +++ b/crates/socket-patch-core/src/vendor/pypi_poetry.rs @@ -788,28 +788,22 @@ content-hash = "4b42a89b7ff7b26511b06acdc458dbd85312e5083db8f212b017482bc68cdd01 fn entry_for(wiring: Vec, meta: PoetryMeta) -> VendorEntry { VendorEntry { - ecosystem: "pypi".into(), - base_purl: "pkg:pypi/six@1.16.0".into(), - uuid: UUID.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: REL_WHEEL.into(), - sha256: WHEEL_SHA.into(), - size: Some(11053), - platform_locked: None, - file_inventory: None, - }, - wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some("poetry".into()), - uv: None, - pnpm: None, poetry: Some(meta), - pdm: None, - pipenv: None, + ..VendorEntry::new( + "pypi".into(), + "pkg:pypi/six@1.16.0".into(), + UUID.into(), + VendorArtifact { + yarn_berry10c0: None, + path: REL_WHEEL.into(), + sha256: WHEEL_SHA.into(), + size: Some(11053), + platform_locked: None, + file_inventory: None, + }, + wiring, + ) } } diff --git a/crates/socket-patch-core/src/vendor/pypi_requirements.rs b/crates/socket-patch-core/src/vendor/pypi_requirements.rs index 22d3862d3..8d1527b98 100644 --- a/crates/socket-patch-core/src/vendor/pypi_requirements.rs +++ b/crates/socket-patch-core/src/vendor/pypi_requirements.rs @@ -1371,28 +1371,21 @@ mod tests { fn entry_for(wiring: Vec) -> VendorEntry { VendorEntry { - ecosystem: "pypi".into(), - base_purl: "pkg:pypi/six@1.16.0".into(), - uuid: UUID.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: REL_WHEEL.into(), - sha256: SHA.into(), - size: Some(11053), - platform_locked: None, - file_inventory: None, - }, - wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some("requirements".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "pypi".into(), + "pkg:pypi/six@1.16.0".into(), + UUID.into(), + VendorArtifact { + yarn_berry10c0: None, + path: REL_WHEEL.into(), + sha256: SHA.into(), + size: Some(11053), + platform_locked: None, + file_inventory: None, + }, + wiring, + ) } } diff --git a/crates/socket-patch-core/src/vendor/pypi_uv.rs b/crates/socket-patch-core/src/vendor/pypi_uv.rs index 1bae31c55..51ac4e547 100644 --- a/crates/socket-patch-core/src/vendor/pypi_uv.rs +++ b/crates/socket-patch-core/src/vendor/pypi_uv.rs @@ -2451,28 +2451,22 @@ wheels = [ fn entry_for(wiring: Vec, meta: UvMeta) -> VendorEntry { VendorEntry { - ecosystem: "pypi".into(), - base_purl: "pkg:pypi/six@1.16.0".into(), - uuid: UUID.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: REL_WHEEL.into(), - sha256: WHEEL_SHA.into(), - size: Some(11053), - platform_locked: None, - file_inventory: None, - }, - wiring, - lock: None, - took_over_go_patches: false, - detached: false, - record: None, flavor: Some("uv".into()), uv: Some(meta), - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "pypi".into(), + "pkg:pypi/six@1.16.0".into(), + UUID.into(), + VendorArtifact { + yarn_berry10c0: None, + path: REL_WHEEL.into(), + sha256: WHEEL_SHA.into(), + size: Some(11053), + platform_locked: None, + file_inventory: None, + }, + wiring, + ) } } diff --git a/crates/socket-patch-core/src/vendor/redownload.rs b/crates/socket-patch-core/src/vendor/redownload.rs index 92d5172cd..a9b2bd01f 100644 --- a/crates/socket-patch-core/src/vendor/redownload.rs +++ b/crates/socket-patch-core/src/vendor/redownload.rs @@ -9,9 +9,7 @@ use crate::utils::purl::{ use super::common::{copy_matches_after_hashes, swap_stage_into_place}; use super::jvm::layout; -use super::service_fetch::{ - fetch_verified_archive, ServiceAttempt, ServicePolicy, ServiceTerminal, VerifiedArchive, -}; +use super::service_fetch::{fetch_verified_archive, ServicePolicy, VerifiedArchive}; use super::state::VendorEntry; use super::{VendorOutcome, VendorServiceConfig, VendorWarning}; @@ -24,28 +22,19 @@ fn detail(outcome: VendorOutcome) -> String { } } -fn used(attempt: ServiceAttempt) -> Result { - match attempt { - ServiceAttempt::Used(value) => Ok(value), - ServiceAttempt::HardFail(outcome) => Err(detail(*outcome)), - } -} - async fn download_archive( service: &VendorServiceConfig, record: &PatchRecord, noun: &str, subject: &str, - warnings: &mut Vec, ) -> Result { - ServicePolicy::new(service, ServiceTerminal::Refused) + ServicePolicy::Refused .settle( fetch_verified_archive(service, &record.uuid).await, noun, subject, - warnings, ) - .or_else(used) + .map_err(|outcome| detail(*outcome)) } // Without the ledger's fingerprint no download can be proven to be the @@ -139,7 +128,6 @@ pub async fn restore( record, "archive", &format!("archive for {}", entry.base_purl), - &mut warnings, ) .await?; if entry.artifact.sha256.is_empty() @@ -191,34 +179,32 @@ pub async fn restore( "cargo" => { let (name, version) = parse_cargo_purl(&entry.base_purl).ok_or("invalid cargo coordinates")?; - used( - super::cargo::cargo_service_copy( - Some(service), - record, - &name, - &version, - &stage, - uuid_dir, - &mut warnings, - ) - .await, - )?; + super::cargo::cargo_service_copy( + Some(service), + record, + &name, + &version, + &stage, + uuid_dir, + &mut warnings, + ) + .await + .map_err(|outcome| detail(*outcome))?; } "composer" => { let ((namespace, name), _) = parse_composer_purl(&entry.base_purl).ok_or("invalid composer coordinates")?; let package = format!("{namespace}/{name}"); - used( - super::composer_lock::composer_service_copy( - Some(service), - record, - &package, - &stage, - uuid_dir, - &mut warnings, - ) - .await, - )?; + super::composer_lock::composer_service_copy( + Some(service), + record, + &package, + &stage, + uuid_dir, + &mut warnings, + ) + .await + .map_err(|outcome| detail(*outcome))?; super::composer_lock::mirror_filters::neutralize_or_conflict( &stage, record, @@ -230,7 +216,7 @@ pub async fn restore( "gem" => { let (name, _) = parse_gem_purl(&entry.base_purl).ok_or("invalid gem coordinates")?; - match super::gem::gem_service_copy( + super::gem::gem_service_copy( Some(service), record, &name, @@ -240,26 +226,22 @@ pub async fn restore( &mut warnings, ) .await - { - super::gem::GemServiceCopy::Used => {} - super::gem::GemServiceCopy::HardFail(outcome) => return Err(detail(*outcome)), - } + .map_err(|outcome| detail(*outcome))?; } "npm" => { let (name, version) = super::npm_common::parse_npm_purl(&entry.base_purl) .ok_or("invalid npm coordinates")?; - used( - super::npm_dir::try_service_dir( - &entry.base_purl, - record, - service, - &stage, - &name, - &version, - &mut warnings, - ) - .await, - )?; + super::npm_dir::try_service_dir( + &entry.base_purl, + record, + service, + &stage, + &name, + &version, + &mut warnings, + ) + .await + .map_err(|outcome| detail(*outcome))?; super::npm_dir::apply_transforms(&stage, &name, &version) .await .map_err(|outcome| detail(*outcome))?; @@ -272,7 +254,6 @@ pub async fn restore( record, "module zip", &format!("module zip for {module}"), - &mut warnings, ) .await?; let prefix = format!("{module}@{version}/"); @@ -641,28 +622,23 @@ mod tests { fn entry(bytes: &[u8]) -> VendorEntry { VendorEntry { - ecosystem: "npm".into(), - base_purl: "pkg:npm/example@1.0.0".into(), - uuid: UUID.into(), - artifact: VendorArtifact { - path: format!(".socket/vendor/npm/{UUID}/example-1.0.0.tgz"), - sha256: hex::encode(Sha256::digest(bytes)), - size: Some(bytes.len() as u64), - platform_locked: None, - file_inventory: None, - yarn_berry10c0: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, detached: true, record: Some(record()), flavor: Some("npm".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "npm".into(), + "pkg:npm/example@1.0.0".into(), + UUID.into(), + VendorArtifact { + path: format!(".socket/vendor/npm/{UUID}/example-1.0.0.tgz"), + sha256: hex::encode(Sha256::digest(bytes)), + size: Some(bytes.len() as u64), + platform_locked: None, + file_inventory: None, + yarn_berry10c0: None, + }, + Vec::new(), + ) } } diff --git a/crates/socket-patch-core/src/vendor/registry_fetch.rs b/crates/socket-patch-core/src/vendor/registry_fetch.rs index cc7ff6b9a..27e4eea50 100644 --- a/crates/socket-patch-core/src/vendor/registry_fetch.rs +++ b/crates/socket-patch-core/src/vendor/registry_fetch.rs @@ -162,37 +162,12 @@ where } } -/// What an archive walk does with each entry's decompressed bytes. -/// -/// The two modes run the SAME walk in the same order over the same -/// constants: every archive-shaped refusal (entry count, the declared and -/// actual size caps, the traversal guard, the declared-vs-actual mismatch) -/// fires at the same entry with the same message in both. [`Sink::Validate`] -/// only sends the bytes to `io::sink()` instead of a file and creates -/// nothing, so a source whose extraction is deferred -/// ([`FetchedPackage::dir`]) can still be refused where the eager extraction -/// refused it. -/// -/// The `cannot create …` errors are the ones a pass that opens nothing -/// cannot raise. Most are environment-shaped — a full or unwritable -/// destination — and belong to whoever writes. The exception is a name the -/// archive itself uses as both a file and a directory: the write walk -/// refuses every such archive, so [`Sink::Validate`] models the -/// destinations, and when one shows up it hands the archive to the write -/// walk rather than answering for it (see [`DestShape::file_dir_conflict`]). -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub(crate) enum Sink { - Write, - Validate, -} - /// The destination side of an archive walk: opens each entry's file and /// creates each parent directory ONCE per walk (archives list a directory's /// files consecutively, so the per-entry `create_dir_all` re-walked the same -/// parents for every member). In [`Sink::Validate`] it opens nothing. +/// parents for every member). struct EntrySink<'a> { dest: &'a Path, - sink: Sink, /// Entries whose final path component is this are not written — the /// `fresh_copy` skip the vendor stage asks for (cargo's /// `.cargo-checksum.json`, which must never reach a path-dep copy). @@ -201,10 +176,9 @@ struct EntrySink<'a> { } impl<'a> EntrySink<'a> { - fn new(dest: &'a Path, sink: Sink) -> Self { + fn new(dest: &'a Path) -> Self { Self { dest, - sink, skip_file_name: None, made: std::collections::HashSet::new(), } @@ -238,9 +212,6 @@ impl<'a> EntrySink<'a> { /// written. The tar walk stays one pass: a tar entry is only reachable /// by reading the one before it. fn open(&mut self, rel: &Path) -> Result, String> { - if self.sink == Sink::Validate { - return Ok(None); - } let target = self.dest.join(rel); // The parent is created even for a skipped entry: what this stands // in for extracted the WHOLE archive and copied the tree out again, @@ -280,69 +251,15 @@ fn fold_key(p: &Path) -> String { p.to_string_lossy().to_ascii_lowercase() } -/// What the entries a zip plans to write look like to a FILESYSTEM, as -/// opposed to a set of `PathBuf`s. -#[derive(Default)] -struct DestShape { - /// Two planned entries may land on ONE file, or a name is used as both - /// a file and a directory. Either way the entries are not independent: - /// the pool must not spread them and each parent must be created right - /// before its own file, as a sequential in-order extraction does. - /// - /// Besides the exact repeats the plan already resolves, two spellings - /// meet on a case-insensitive volume (`LICENSE` / `license`) or a - /// normalization-insensitive one (`café` in NFC / NFD). ASCII case is - /// checked; for anything non-ASCII the walk gives up on deciding and - /// takes the one-at-a-time path. - in_order: bool, - /// A name is used as both a file and a directory — a refusal the write - /// walk raises for every such archive, decided by its entries alone. - file_dir_conflict: bool, -} - -/// What a sequential walk has put under the destination so far, by the key a -/// case-insensitive filesystem compares on — enough to answer the one -/// refusal a write-free pass cannot: a name used as both a file and a -/// directory ([`DestShape::file_dir_conflict`]), which a sequential archive -/// only reveals as it is read. -#[derive(Default)] -struct DestModel { - files: std::collections::HashSet, - dirs: std::collections::HashSet, -} - -impl DestModel { - /// Record `rel` and report whether writing it clashes with what an - /// earlier entry put there. - fn clashes(&mut self, rel: &Path) -> bool { - let rel = dest_rel(rel); - for ancestor in rel.ancestors().skip(1) { - if ancestor.as_os_str().is_empty() { - break; - } - let key = fold_key(ancestor); - if self.files.contains(&key) { - return true; - } - self.dirs.insert(key); - } - let key = fold_key(&rel); - if self.dirs.contains(&key) { - return true; - } - self.files.insert(key); - false - } -} - -/// Read the shape off the planned destinations (relative, `./`-collapsed). -fn dest_shape(rels: &[PathBuf]) -> DestShape { - let mut shape = DestShape::default(); +/// Aliasing paths and file/directory conflicts require sequential writes. +/// Non-ASCII names also use that path because volume normalization varies. +fn destinations_need_order(rels: &[PathBuf]) -> bool { + let mut in_order = false; let mut files: std::collections::HashMap = std::collections::HashMap::new(); let mut dirs: std::collections::HashSet = std::collections::HashSet::new(); let mut walked: Option<&Path> = None; for rel in rels { - shape.in_order |= !rel.as_os_str().as_encoded_bytes().is_ascii(); + in_order |= !rel.as_os_str().as_encoded_bytes().is_ascii(); // An archive lists a directory's files consecutively, so the parent // chain is nearly always the one the entry before it walked. let parent = rel.parent(); @@ -357,17 +274,14 @@ fn dest_shape(rels: &[PathBuf]) -> DestShape { } match files.insert(fold_key(rel), rel.as_path()) { // Two spellings of one file the exact-path rule cannot see. - Some(earlier) if earlier != rel.as_path() => shape.in_order = true, + Some(earlier) if earlier != rel.as_path() => in_order = true, _ => {} } } - shape.file_dir_conflict = files.keys().any(|key| dirs.contains(key)); - shape.in_order |= shape.file_dir_conflict; - shape + in_order || files.keys().any(|key| dirs.contains(key)) } -/// Copy one entry's bytes into the opened file, or count them when -/// validating. +/// Copy an entry into its file, or discard its bytes when skipped or superseded. fn drain_entry( reader: &mut R, out: Option<&mut std::fs::File>, @@ -411,60 +325,9 @@ pub(crate) fn extract_zip_skipping( strip_first: bool, skip_file_name: Option<&str>, ) -> Result<(), String> { - walk_zip(bytes, dest, strip_first, Sink::Write, None, skip_file_name).map(|_| ()) -} - -/// [`extract_zip`]'s write-free twin: every refusal, nothing created. -/// Reports whether the extraction would put `watch` at the root (see -/// [`lands_at_root`]) so the fetchers' post-extraction presence probe can -/// run without a tree. -/// -/// `dest` is where the tree WOULD go. Nothing is written there — it names -/// the destinations the refusals name, and it is where the write walk takes -/// over for the one refusal this pass cannot decide on its own (see -/// [`DestShape::file_dir_conflict`]). -#[cfg(test)] -pub(crate) fn validate_zip( - bytes: &[u8], - dest: &Path, - strip_first: bool, - watch: Option<&str>, -) -> Result { - walk_zip(bytes, dest, strip_first, Sink::Validate, watch, None) -} - -/// The zip walk, in two passes. -/// -/// Pass one reads the central directory alone — no entry is inflated — and -/// decides everything that comes from headers, in entry order over one -/// running total: the traversal guard, the per-entry and total DECLARED -/// caps, and each entry's destination (every parent directory created -/// once). It stops at the first refusal. -/// -/// Pass two inflates the planned entries on a bounded pool of threads, each -/// with its own reader over the shared bytes. Inflating is the whole cost of -/// a big dist zip and it is per-entry independent, so the only thing the -/// pass has to serialise is the ANSWER: a repeated name is written by its -/// last spelling, as an in-order extraction left it, and the refusal -/// reported is the one at the lowest entry index — which, against pass one's -/// own index, reproduces a sequential walk's verdict entry for entry. -fn walk_zip( - bytes: &[u8], - dest: &Path, - strip_first: bool, - sink: Sink, - watch: Option<&str>, - skip_file_name: Option<&str>, -) -> Result { - let plan = plan_zip(bytes, dest, strip_first, sink, watch, skip_file_name)?; - // A name used as both a file and a directory is refused by every write - // walk, but only the filesystem can say with which errno, at which - // entry — and this pass opens nothing. Hand the archive to the write - // walk, into the destination the tree would have gone to, so the fetch - // reports exactly the refusal the eager extraction reported. - if sink == Sink::Validate && plan.file_dir_conflict { - return walk_zip(bytes, dest, strip_first, Sink::Write, watch, skip_file_name); - } + // Plan headers in archive order, then inflate independent entries in parallel. + // The earliest refusal wins; a header refusal takes precedence at the same entry. + let plan = plan_zip(bytes, dest, strip_first, skip_file_name)?; let body_refusal = inflate_planned_entries(bytes, &plan.entries, plan.declared_total, plan.in_order); match (body_refusal, plan.header_refusal) { @@ -475,7 +338,7 @@ fn walk_zip( } (Some((_, body)), None) => Err(body), (None, Some((_, header))) => Err(header), - (None, None) => Ok(plan.seen_watched), + (None, None) => Ok(()), } } @@ -487,11 +350,11 @@ struct PlannedEntry { declared: u64, exec: bool, /// Where the bytes go — `None` when the entry is read but not written: - /// validating, skipped by name, or an earlier spelling of a repeated - /// name that a later entry overwrites. + /// skipped by name, or an earlier spelling of a repeated name that a + /// later entry overwrites. target: Option, /// The directory to create before writing, when the plan left that to - /// the inflate pass (see [`DestShape::in_order`]). `None` when the plan + /// the inflate pass (see [`ZipPlan::in_order`]). `None` when the plan /// created every parent itself. parent: Option, } @@ -503,20 +366,14 @@ struct ZipPlan { /// What the planned entries declare they decompress to — the pool's /// work estimate. declared_total: u64, - seen_watched: bool, - /// [`DestShape::in_order`]: the entries are not independent, so one - /// thread writes them in plan order, creating each parent as it goes. + /// Aliasing entries must be written in order, creating each parent as needed. in_order: bool, - /// [`DestShape::file_dir_conflict`]. - file_dir_conflict: bool, } fn plan_zip( bytes: &[u8], dest: &Path, strip_first: bool, - sink: Sink, - watch: Option<&str>, skip_file_name: Option<&str>, ) -> Result { let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes)) @@ -528,9 +385,7 @@ fn plan_zip( entries: Vec::new(), header_refusal: None, declared_total: 0, - seen_watched: false, in_order: false, - file_dir_conflict: false, }; // Each planned entry's destination relative to `dest`, which is what // the repeated-name rule and the aliasing check are decided on. @@ -587,7 +442,6 @@ fn plan_zip( )); break; } - plan.seen_watched |= watch.is_some_and(|name| lands_at_root(&rel, name)); plan.declared_total += declared; rels.push(dest_rel(&rel)); plan.entries.push(PlannedEntry { @@ -595,19 +449,14 @@ fn plan_zip( rel_str, declared, exec: file.unix_mode().is_some_and(|m| m & 0o111 != 0), - target: (sink == Sink::Write).then(|| dest.join(&rel)), + target: Some(dest.join(&rel)), parent: None, }); } - let shape = dest_shape(&rels); - plan.in_order = shape.in_order; - plan.file_dir_conflict = shape.file_dir_conflict; - if sink == Sink::Validate { - return Ok(plan); - } + plan.in_order = destinations_need_order(&rels); // The destination pass, in entry order. - let mut out = EntrySink::new(dest, sink).skipping(skip_file_name); + let mut out = EntrySink::new(dest).skipping(skip_file_name); if plan.in_order { // Aliasing destinations: leave the directories to the inflate pass, // which creates each one right before its own file, so a name used @@ -816,18 +665,6 @@ fn inflate_one( None } -/// Whether extracting `rel` puts `name` at the root of the destination — -/// either as the entry itself or as a directory the walk creates for it. -/// This is exactly what a `metadata(dest.join(name))` probe would answer -/// after a full extraction. -fn lands_at_root(rel: &Path, name: &str) -> bool { - // A leading `./` survives `Path::components` but not `dest.join(rel)`, - // which is what the probe this replaces ran against. - rel.components() - .find(|c| !matches!(c, std::path::Component::CurDir)) - .is_some_and(|c| c.as_os_str() == name) -} - /// PyPI's JSON API base; override with `SOCKET_PYPI_JSON_API` (tests point it /// at a mock). Used only to turn a lock's file hash into a download URL for /// locks that record hashes without URLs (poetry.lock, which records one wheel @@ -930,37 +767,6 @@ fn go_glob_match(pattern: &[u8], name: &[u8]) -> bool { /// Runs in the ecosystem-agnostic service-download path whenever the /// service reports a `dirhashH1`. pub(crate) fn go_h1_of_zip(bytes: &[u8]) -> Result { - Ok(walk_module_zip(bytes, None)?.h1) -} - -/// What one walk over a module zip learned. -#[cfg_attr(not(test), allow(dead_code))] -struct ModuleZipWalk { - /// The `h1:` dirhash of the entries. - h1: String, - /// The refusal [`extract_zip_with_prefix`] would have raised over the - /// same entries, held back — `None` when it would have extracted - /// cleanly, and always `None` when no prefix was given. - extract_refusal: Option, - /// An entry name used as both a file and a directory: the extraction - /// refuses it, but with an errno only the filesystem knows, so the - /// caller hands the archive back to the extraction (see - /// [`DestShape::file_dir_conflict`]). Always `false` without a prefix. - dest_clash: bool, -} - -/// The dirhash walk, optionally also answering what the extraction walk -/// would have said about the same entries. -/// -/// One inflate serves both the dirhash and the extraction checks: both -/// derive everything from the entry's name, its declared size and how many -/// bytes it actually decompresses to. -/// -/// Refusal ORDER matches a dirhash-then-extract sequence: a dirhash refusal -/// wins outright and returns here; the extraction refusal is recorded at -/// the lowest entry index and handed back for the caller to raise only -/// after the dirhash has been compared. -fn walk_module_zip(bytes: &[u8], validate_prefix: Option<&str>) -> Result { use std::io::Read as _; let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes)) .map_err(|e| format!("unreadable module zip: {e}"))?; @@ -969,13 +775,6 @@ fn walk_module_zip(bytes: &[u8], validate_prefix: Option<&str>) -> Result = Vec::new(); let mut total: u64 = 0; - // The extraction walk's own running total — DECLARED sizes, where the - // dirhash walk counts actual ones. - let mut declared_total: u64 = 0; - let mut extract_refusal: Option = None; - // See [`walk_tar_gz`]: the one refusal that needs a real filesystem. - let mut shape = DestModel::default(); - let mut dest_clash = false; for i in 0..archive.len() { let mut file = archive .by_index(i) @@ -1021,16 +820,6 @@ fn walk_module_zip(bytes: &[u8], validate_prefix: Option<&str>) -> Result) -> Result Option { - let Some(rel) = name.strip_prefix(prefix) else { - return Some(format!( - "module zip entry `{name}` lies outside `{prefix}` — refusing the artifact" - )); - }; - if !is_safe_relative_subpath(rel) { - return Some(format!( - "module zip entry `{name}` escapes the extraction dir — refusing the artifact" - )); - } - if declared > MAX_ENTRY_BYTES { - return Some(format!( - "module zip entry `{name}` is {declared} bytes (cap {MAX_ENTRY_BYTES})" - )); - } - *declared_total += declared; - if *declared_total > MAX_TOTAL_DECOMPRESSED_BYTES { - return Some(format!( - "module zip decompresses past the {MAX_TOTAL_DECOMPRESSED_BYTES}-byte cap" - )); - } - let copied = actual.min(declared + 1); - if copied != declared { - return Some(format!( - "module zip entry `{name}` decompresses to {copied} bytes but declares \ - {declared} — refusing the artifact" - )); - } - None + Ok(format!( + "h1:{}", + base64::engine::general_purpose::STANDARD.encode(h.finalize()) + )) } /// Verify a golang module zip's `h1:` dirhash against an expected value. @@ -1128,28 +871,6 @@ pub(crate) fn extract_zip_with_prefix_skipping( dest: &Path, prefix: &str, skip_file_name: Option<&str>, -) -> Result<(), String> { - walk_zip_with_prefix(bytes, dest, prefix, Sink::Write, skip_file_name) -} - -/// [`extract_zip_with_prefix`]'s write-free twin: every refusal, nothing -/// created. The golang fetch gets the same answer out of its dirhash walk -/// ([`walk_module_zip`]); this is the oracle that pins the two together. -#[cfg(test)] -pub(crate) fn validate_zip_with_prefix( - bytes: &[u8], - dest: &Path, - prefix: &str, -) -> Result<(), String> { - walk_zip_with_prefix(bytes, dest, prefix, Sink::Validate, None) -} - -fn walk_zip_with_prefix( - bytes: &[u8], - dest: &Path, - prefix: &str, - sink: Sink, - skip_file_name: Option<&str>, ) -> Result<(), String> { use std::io::Read as _; let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes)) @@ -1157,11 +878,8 @@ fn walk_zip_with_prefix( if archive.len() > MAX_ENTRIES { return Err(format!("module zip exceeds {MAX_ENTRIES} entries")); } - let mut out = EntrySink::new(dest, sink).skipping(skip_file_name); + let mut out = EntrySink::new(dest).skipping(skip_file_name); let mut total: u64 = 0; - // See [`walk_tar_gz`]: the one refusal a write-free pass cannot open a - // file to find out about. - let mut shape = DestModel::default(); for i in 0..archive.len() { let mut file = archive .by_index(i) @@ -1196,9 +914,6 @@ fn walk_zip_with_prefix( "module zip decompresses past the {MAX_TOTAL_DECOMPRESSED_BYTES}-byte cap" )); } - if sink == Sink::Validate && shape.clashes(Path::new(rel)) { - return walk_zip_with_prefix(bytes, dest, prefix, Sink::Write, skip_file_name); - } let mut target = out.open(Path::new(rel))?; // Hold the caps against the ACTUAL decompressed bytes too — the // declared size is header data a crafted zip can understate. @@ -1389,12 +1104,9 @@ pub(crate) fn extract_tgz_skipping( bytes, dest, /*strip_first=*/ true, - Sink::Write, - None, skip_file_name, /*strict=*/ false, ) - .map(|_| ()) } /// [`extract_tgz`] that refuses the archive instead of skipping a symlink, @@ -1402,30 +1114,7 @@ pub(crate) fn extract_tgz_skipping( /// extracted, so nothing the archive carries may be silently dropped). pub(crate) fn extract_tgz_strict(bytes: &[u8], dest: &Path) -> Result<(), String> { walk_tar_gz( - bytes, - dest, - /*strip_first=*/ true, - Sink::Write, - None, - None, - /*strict=*/ true, - ) - .map(|_| ()) -} - -/// [`extract_tgz`]'s write-free twin: every refusal, nothing created. -/// Reports whether `watch` would land at the root (see [`lands_at_root`]). -/// `dest` is where the tree WOULD go; see [`validate_zip`]. -#[cfg(test)] -pub(crate) fn validate_tgz(bytes: &[u8], dest: &Path, watch: Option<&str>) -> Result { - walk_tar_gz( - bytes, - dest, - /*strip_first=*/ true, - Sink::Validate, - watch, - None, - /*strict=*/ false, + bytes, dest, /*strip_first=*/ true, None, /*strict=*/ true, ) } @@ -1449,22 +1138,6 @@ pub(crate) fn extract_gem_data_skipping( gem_bytes: &[u8], dest: &Path, skip_file_name: Option<&str>, -) -> Result<(), String> { - walk_gem_data(gem_bytes, dest, Sink::Write, skip_file_name) -} - -/// [`extract_gem_data`]'s write-free twin: every refusal, nothing created. -/// `dest` is where the tree WOULD go; see [`validate_zip`]. -#[cfg(test)] -pub(crate) fn validate_gem_data(gem_bytes: &[u8], dest: &Path) -> Result<(), String> { - walk_gem_data(gem_bytes, dest, Sink::Validate, None) -} - -fn walk_gem_data( - gem_bytes: &[u8], - dest: &Path, - sink: Sink, - skip_file_name: Option<&str>, ) -> Result<(), String> { use std::io::Read as _; let mut archive = tar::Archive::new(gem_bytes); @@ -1490,12 +1163,9 @@ fn walk_gem_data( &buf, dest, /*strip_first=*/ false, - sink, - None, skip_file_name, /*strict=*/ false, - ) - .map(|_| ()); + ); } Err("the .gem carries no data.tar.gz".to_string()) } @@ -1504,24 +1174,14 @@ fn walk_tar_gz( bytes: &[u8], dest: &Path, strip_first: bool, - sink: Sink, - watch: Option<&str>, skip_file_name: Option<&str>, strict: bool, -) -> Result { +) -> Result<(), String> { use std::io::Read as _; let gz = flate2::read::GzDecoder::new(bytes).take(MAX_TOTAL_DECOMPRESSED_BYTES); let mut archive = tar::Archive::new(gz); - let mut out = EntrySink::new(dest, sink).skipping(skip_file_name); - let mut seen_watched = false; + let mut out = EntrySink::new(dest).skipping(skip_file_name); let mut count = 0usize; - // Validating opens nothing, so a destination the filesystem refuses - // cannot surface here. A name used as both a file and a directory is - // decidable from the entries alone, though, and the write walk always - // refuses it — so model what the entries put on disk and, when one - // shows up, let the write walk answer into the destination the tree - // would have gone to. - let mut shape = DestModel::default(); for entry in archive .entries() .map_err(|e| format!("unreadable tarball: {e}"))? @@ -1575,25 +1235,13 @@ fn walk_tar_gz( "tarball entry `{rel_str}` is {size} bytes (cap {MAX_ENTRY_BYTES})" )); } - if sink == Sink::Validate && shape.clashes(&rel) { - return walk_tar_gz( - bytes, - dest, - strip_first, - Sink::Write, - watch, - skip_file_name, - strict, - ); - } let mut target = out.open(&rel)?; let mode = entry.header().mode().unwrap_or(0o644); drain_entry(&mut entry, target.as_mut()) .map_err(|e| format!("cannot extract `{rel_str}`: {e}"))?; set_entry_mode(target.as_ref(), mode & 0o111 != 0); - seen_watched |= watch.is_some_and(|name| lands_at_root(&rel, name)); } - Ok(seen_watched) + Ok(()) } #[cfg(test)] @@ -2322,12 +1970,8 @@ mod tests { outer.into_inner().unwrap() } - /// The archives the extractors refuse, each paired with the walk that - /// reads it. Deferring the WRITE is only safe while the fetch still - /// decides everything the write decided, so the validation pass has to - /// refuse the same bytes at the same entry with the same words. #[test] - fn validation_pass_refuses_exactly_what_the_extractor_refuses() { + fn extractors_refuse_malformed_archives() { let big_content = vec![0x42u8; 4096]; // ── tar.gz ────────────────────────────────────────────────────── @@ -2362,9 +2006,7 @@ mod tests { ("tar truncated", truncated_tgz), ] { let stage = tempfile::tempdir().unwrap(); - let eager = extract_tgz(&bytes, stage.path()).unwrap_err(); - let lazy = validate_tgz(&bytes, stage.path(), Some("package.json")).unwrap_err(); - assert_eq!(lazy, eager, "{label}"); + assert!(extract_tgz(&bytes, stage.path()).is_err(), "{label}"); } // ── zip ───────────────────────────────────────────────────────── @@ -2389,10 +2031,7 @@ mod tests { ("zip truncated", truncated_zip, false), ] { let stage = tempfile::tempdir().unwrap(); - let eager = extract_zip(&bytes, stage.path(), strip).unwrap_err(); - let lazy = - validate_zip(&bytes, stage.path(), strip, Some("composer.json")).unwrap_err(); - assert_eq!(lazy, eager, "{label}"); + assert!(extract_zip(&bytes, stage.path(), strip).is_err(), "{label}"); } // ── module zip (prefix) ───────────────────────────────────────── @@ -2414,31 +2053,12 @@ mod tests { ("module zip lying declared size", module_lying), ] { let stage = tempfile::tempdir().unwrap(); - let eager = extract_zip_with_prefix(&bytes, stage.path(), prefix).unwrap_err(); - let lazy = validate_zip_with_prefix(&bytes, stage.path(), prefix).unwrap_err(); - assert_eq!(lazy, eager, "{label}"); - // And the golang fetch's fused walk, which answers the same - // question off the dirhash pass's single inflate. - match walk_module_zip(&bytes, Some(prefix)) { - // The dirhash pass guards the caps too and refuses first. - Err(dirhash_refusal) => assert!( - dirhash_refusal.contains("cap"), - "{label}: {dirhash_refusal}" - ), - Ok(walk) => assert_eq!( - walk.extract_refusal.as_deref(), - Some(eager.as_str()), - "{label}: the fused walk must hold the extraction refusal verbatim" - ), - } + assert!( + extract_zip_with_prefix(&bytes, stage.path(), prefix).is_err(), + "{label}" + ); } - // A healthy module zip: the fused walk agrees with both walks. - let healthy = make_module_zip(prefix, &[("go.mod", b"module m"), ("a/b.go", b"package b")]); - let walk = walk_module_zip(&healthy, Some(prefix)).unwrap(); - assert_eq!(walk.h1, go_h1_of_zip(&healthy).unwrap()); - assert_eq!(walk.extract_refusal, None); - // ── .gem ──────────────────────────────────────────────────────── let no_data = { let mut outer = tar::Builder::new(Vec::new()); @@ -2454,19 +2074,12 @@ mod tests { let traversing = wrap_gem(&make_traversing_tgz("../evil.rb")); for (label, bytes) in [("gem without data", no_data), ("gem traversal", traversing)] { let stage = tempfile::tempdir().unwrap(); - let eager = extract_gem_data(&bytes, stage.path()).unwrap_err(); - let lazy = validate_gem_data(&bytes, stage.path()).unwrap_err(); - assert_eq!(lazy, eager, "{label}"); + assert!(extract_gem_data(&bytes, stage.path()).is_err(), "{label}"); } } - /// The refusal a write-free pass cannot reach on its own: an archive - /// that names one path as both a file and a directory. It is decided by - /// the archive's own entries — no environment involved — so the fetch - /// has to refuse it where the eager extraction refused it, with the - /// errno the filesystem gave, at the entry it gave it for. #[test] - fn validation_pass_refuses_a_file_that_is_also_a_directory() { + fn extractors_refuse_a_file_that_is_also_a_directory() { let tgz_file_first = make_tgz(&[ ("package/package.json", b"{}", false), ("package/a", b"i am a file", false), @@ -2480,24 +2093,12 @@ mod tests { let zip_file_first = make_zip(&[("a", b"i am a file"), ("a/b", b"i am under it")]); let zip_dir_first = make_zip(&[("a/b", b"i am under it"), ("a", b"i am a file")]); - // The message names the destination, so eager and lazy each get a - // fresh one and the two are compared with it masked out. - let masked = |dest: &Path, detail: &str| { - detail.replace(&dest.to_string_lossy().into_owned(), "") - }; for (label, bytes) in [ ("tgz file first", &tgz_file_first), ("tgz dir first", &tgz_dir_first), ] { let eager_at = tempfile::tempdir().unwrap(); let eager = extract_tgz(bytes, eager_at.path()).unwrap_err(); - let lazy_at = tempfile::tempdir().unwrap(); - let lazy = validate_tgz(bytes, lazy_at.path(), Some("package.json")).unwrap_err(); - assert_eq!( - masked(lazy_at.path(), &lazy), - masked(eager_at.path(), &eager), - "{label}" - ); assert!(eager.contains("cannot create"), "{label}: {eager}"); } for (label, bytes) in [ @@ -2506,14 +2107,6 @@ mod tests { ] { let eager_at = tempfile::tempdir().unwrap(); let eager = extract_zip(bytes, eager_at.path(), /*strip_first=*/ false).unwrap_err(); - let lazy_at = tempfile::tempdir().unwrap(); - let lazy = - validate_zip(bytes, lazy_at.path(), false, Some("composer.json")).unwrap_err(); - assert_eq!( - masked(lazy_at.path(), &lazy), - masked(eager_at.path(), &eager), - "{label}" - ); assert!(eager.contains("cannot create"), "{label}: {eager}"); } @@ -2524,93 +2117,69 @@ mod tests { ])); let eager_at = tempfile::tempdir().unwrap(); let eager = extract_gem_data(&gem, eager_at.path()).unwrap_err(); - let lazy_at = tempfile::tempdir().unwrap(); - let lazy = validate_gem_data(&gem, lazy_at.path()).unwrap_err(); - assert_eq!( - masked(lazy_at.path(), &lazy), - masked(eager_at.path(), &eager) - ); + assert!(eager.contains("cannot create"), "{eager}"); let prefix = "github.com/x/y@v1.0.0/"; let module = make_module_zip(prefix, &[("a", b"file"), ("a/b", b"under")]); let eager_at = tempfile::tempdir().unwrap(); let eager = extract_zip_with_prefix(&module, eager_at.path(), prefix).unwrap_err(); - let lazy_at = tempfile::tempdir().unwrap(); - let lazy = validate_zip_with_prefix(&module, lazy_at.path(), prefix).unwrap_err(); - assert_eq!( - masked(lazy_at.path(), &lazy), - masked(eager_at.path(), &eager) - ); - // …and the golang fetch's fused walk, which decides it off the - // dirhash pass and hands the archive back to the extraction. - assert!(walk_module_zip(&module, Some(prefix)).unwrap().dest_clash); + assert!(eager.contains("cannot create"), "{eager}"); } - /// And on a healthy archive: the pass accepts it, writes nothing, and - /// answers the root-file probe without an extracted tree. #[test] - fn validation_pass_accepts_and_answers_the_root_probe() { + fn extractors_preserve_package_layout() { + let tmp = tempfile::tempdir().unwrap(); let tgz = make_tgz(&[ ("package/package.json", b"{}", false), ("package/bin/cli.js", b"#!/usr/bin/env node\n", true), ]); - let nowhere = tempfile::tempdir().unwrap(); - let nowhere = nowhere.path().join("package"); - assert!(validate_tgz(&tgz, &nowhere, Some("package.json")).unwrap()); - assert!(!validate_tgz(&tgz, &nowhere, Some("Cargo.toml")).unwrap()); + let dest = tmp.path().join("tgz"); + extract_tgz(&tgz, &dest).unwrap(); + assert_eq!(std::fs::read(dest.join("package.json")).unwrap(), b"{}"); + assert_eq!( + std::fs::read(dest.join("bin/cli.js")).unwrap(), + b"#!/usr/bin/env node\n" + ); - // A nested entry counts, exactly as the directory the extraction - // creates for it made `metadata(dir.join(name))` succeed. let nested = make_tgz(&[("package/composer.json/x", b"{}", false)]); - assert!(validate_tgz(&nested, &nowhere, Some("composer.json")).unwrap()); - - let zip_bytes = make_zip(&[("pfx/composer.json", b"{}"), ("pfx/src/a.php", b"/composer.json`, which is what the `metadata` probe this - // replaces saw — `Path::components` keeps the leading `.`, which it - // did not. A flat `composer archive`-built dist is the shape that - // reaches this (the zipball layout is stripped first). let flat = make_zip(&[("root.txt", b"x"), ("./composer.json", b"{}")]); - let extracted = tempfile::tempdir().unwrap(); - extract_zip(&flat, extracted.path(), /*strip_first=*/ false).unwrap(); - assert!( - extracted.path().join("composer.json").exists(), - "the extraction puts it at the root" - ); - assert!(validate_zip( - &flat, - &nowhere, - /*strip_first=*/ false, - Some("composer.json") - ) - .unwrap()); - // The tar twin, where the strip leaves the `./` behind. + let dest = tmp.path().join("flat"); + extract_zip(&flat, &dest, false).unwrap(); + assert_eq!(std::fs::read(dest.join("composer.json")).unwrap(), b"{}"); let dotted = make_tgz(&[("foo-1.0/./Cargo.toml", b"[package]", false)]); - assert!(validate_tgz(&dotted, &nowhere, Some("Cargo.toml")).unwrap()); + let dest = tmp.path().join("dotted"); + extract_tgz(&dotted, &dest).unwrap(); + assert_eq!( + std::fs::read(dest.join("Cargo.toml")).unwrap(), + b"[package]" + ); } /// A zip with a unix mode per entry, so the parallel walk's `fchmod` @@ -2793,15 +2362,7 @@ mod tests { let plan_for = |names: &[&str]| { let archive = plain(names); let dest = tempfile::tempdir().unwrap(); - let plan = plan_zip( - &archive, - dest.path(), - /*strip_first=*/ false, - Sink::Write, - None, - None, - ) - .unwrap(); + let plan = plan_zip(&archive, dest.path(), /*strip_first=*/ false, None).unwrap(); (plan, dest) }; for (label, names, spread) in [ @@ -2849,11 +2410,6 @@ mod tests { !dest.path().join("pkg/after0.txt").exists(), "the walk stopped at the refusal; nothing past it is planned" ); - // And the validation pass, which never writes, says the same. - assert_eq!( - validate_zip(&archive, dest.path(), false, None).unwrap_err(), - err - ); } /// Every file under `root`, relative, with its bytes and unix mode. diff --git a/crates/socket-patch-core/src/vendor/reuse.rs b/crates/socket-patch-core/src/vendor/reuse.rs index 3c6799403..776a412af 100644 --- a/crates/socket-patch-core/src/vendor/reuse.rs +++ b/crates/socket-patch-core/src/vendor/reuse.rs @@ -407,28 +407,21 @@ mod tests { fn entry_for(uuid: &str, rel: &str, bytes: &[u8]) -> VendorEntry { VendorEntry { - ecosystem: "npm".into(), - base_purl: "pkg:npm/left-pad@1.3.0".into(), - uuid: uuid.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: rel.into(), - sha256: hex::encode(Sha256::digest(bytes)), - size: Some(bytes.len() as u64), - platform_locked: None, - file_inventory: None, - }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, flavor: Some("package-lock".into()), - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - detached: false, - record: None, + ..VendorEntry::new( + "npm".into(), + "pkg:npm/left-pad@1.3.0".into(), + uuid.into(), + VendorArtifact { + yarn_berry10c0: None, + path: rel.into(), + sha256: hex::encode(Sha256::digest(bytes)), + size: Some(bytes.len() as u64), + platform_locked: None, + file_inventory: None, + }, + Vec::new(), + ) } } diff --git a/crates/socket-patch-core/src/vendor/service_fetch.rs b/crates/socket-patch-core/src/vendor/service_fetch.rs index 824caca70..b4b111ec8 100644 --- a/crates/socket-patch-core/src/vendor/service_fetch.rs +++ b/crates/socket-patch-core/src/vendor/service_fetch.rs @@ -5,8 +5,7 @@ //! then integrity-verifies the bytes BEFORE they are ever written/extracted. //! Verification is fail-closed — a byte/hash mismatch is always a hard error //! (`IntegrityMismatch`), never a silent fallback to a wrong artifact. The -//! per-ecosystem backends own the placement (Tier A: write the archive; Tier B: -//! extract it into the vendor directory) and the build-vs-service policy. +//! backends own the placement: write the archive or extract it into a directory. use crate::api::client::{SecondaryArtifact, VendorServiceOutcome}; use crate::manifest::schema::PatchRecord; @@ -36,17 +35,12 @@ pub(crate) async fn preview_service( let cfg = service .filter(|cfg| cfg.service_enabled()) .ok_or_else(|| Box::new(required()))?; - let policy = ServicePolicy::new(cfg, ServiceTerminal::Refused); - let archive = match policy.settle::<()>( + let policy = ServicePolicy::Refused; + let archive = policy.settle( fetch_verified_archive(cfg, &record.uuid).await, "archive", &record.uuid, - &mut Vec::new(), - ) { - Ok(archive) => archive, - Err(ServiceAttempt::HardFail(outcome)) => return Err(outcome), - _ => return Err(Box::new(required())), - }; + )?; let stage = tempfile::tempdir() .map_err(|e| Box::new(refused("vendor_prebuilt_extract_failed", e.to_string())))?; super::registry_fetch::extract_on_blocking_pool(archive.bytes, stage.path(), extract) @@ -198,86 +192,49 @@ pub(crate) async fn claim_prestaged( } } -/// Outcome of a backend's verified service download. -pub(crate) enum ServiceAttempt { - /// The verified service artifact was used; `T` is what the backend made - /// of it. - Used(T), - /// Bubble this terminal outcome (boxed — `VendorOutcome` is large). - HardFail(Box), -} - -/// The single-file outcome for the Tier-A backends (maven `.jar`, nuget -/// `.nupkg`): the prebuilt patched bytes, written verbatim. -pub(crate) type ServiceCopy = ServiceAttempt>; - /// How a backend reports a terminal service failure. #[derive(Clone, Copy)] -pub(crate) enum ServiceTerminal<'a> { - /// A [`VendorOutcome::Refused`] carrying the refusal code. +pub(crate) enum ServicePolicy<'a> { Refused, - /// The npm backends' failed `Done` for `purl` (the code is not reported). + /// npm reports a failed `Done` for this purl. Failure(&'a str), } -/// Every service miss is terminal; backends never construct an archive locally. -pub(crate) struct ServicePolicy<'a> { - terminal: ServiceTerminal<'a>, -} - -impl<'a> ServicePolicy<'a> { - pub(crate) fn new(_cfg: &VendorServiceConfig, terminal: ServiceTerminal<'a>) -> Self { - Self { terminal } - } - - pub(crate) fn hard(&self, code: &'static str, detail: String) -> ServiceAttempt { - ServiceAttempt::HardFail(Box::new(match self.terminal { - ServiceTerminal::Refused => refused(code, detail), - ServiceTerminal::Failure(purl) => super::npm_common::done_failure(purl, detail), - })) +impl ServicePolicy<'_> { + pub(crate) fn hard(&self, code: &'static str, detail: String) -> Box { + Box::new(match self { + Self::Refused => refused(code, detail), + Self::Failure(purl) => super::npm_common::done_failure(purl, detail), + }) } - /// Refuse a patch the service has no artifact for (`code` is - /// [`super::VENDOR_PREBUILT_PENDING`] or [`super::VENDOR_PREBUILT_UNAVAILABLE`]): - /// the same hard failure as [`Self::hard`], except that the npm backends' - /// failed `Done` also carries `code` as a warning, so the vendor loop can - /// tell "not served (yet)" from a real failure and keep an older vendored - /// patch of the same package instead of failing the run (#954). - fn unserved(&self, code: &'static str, detail: String) -> ServiceAttempt { - match self.terminal { - ServiceTerminal::Refused => self.hard("vendor_prebuilt_required", detail), - ServiceTerminal::Failure(purl) => { + /// npm must distinguish unserved patches from failures so the vendor loop + /// can keep an older vendored patch of the same package (#954). + fn unserved(&self, code: &'static str, detail: String) -> Box { + match self { + Self::Refused => self.miss(detail), + Self::Failure(purl) => { let warning = VendorWarning::new(code, detail.clone()); - ServiceAttempt::HardFail(Box::new(super::common::done( + Box::new(super::common::done( super::common::failed_result(purl, std::path::Path::new(""), detail), None, vec![warning], - ))) + )) } } } - /// Refuse an unavailable server artifact. - pub(crate) fn miss( - &self, - warnings: &mut Vec, - code: &'static str, - reason: String, - ) -> ServiceAttempt { - let _ = (warnings, code); + pub(crate) fn miss(&self, reason: String) -> Box { self.hard("vendor_prebuilt_required", reason) } - /// The verified archive of a `Ready` outcome, or every other outcome - /// mapped onto the policy. `noun` names the artifact kind in messages - /// ("crate"); `subject` names this artifact ("crate for serde"). - pub(crate) fn settle( + /// `noun` names the artifact kind; `subject` identifies it in error messages. + pub(crate) fn settle( &self, artifact: ServiceArtifact, noun: &str, subject: &str, - warnings: &mut Vec, - ) -> Result> { + ) -> Result> { match artifact { ServiceArtifact::Ready(archive) => Ok(archive), ServiceArtifact::IntegrityMismatch(reason) => Err(self.hard( @@ -291,16 +248,13 @@ impl<'a> ServicePolicy<'a> { super::VENDOR_PREBUILT_PENDING, format!("prebuilt {noun} is still building"), )), - // No artifact is available for these coordinates or entitlements. ServiceArtifact::Unavailable(reason) => Err(self.unserved( super::VENDOR_PREBUILT_UNAVAILABLE, format!("prebuilt {noun} unavailable: {reason}"), )), - ServiceArtifact::Failed(reason) => Err(self.miss( - warnings, - "vendor_prebuilt_unavailable", - format!("patch service request failed ({reason})"), - )), + ServiceArtifact::Failed(reason) => { + Err(self.miss(format!("patch service request failed ({reason})"))) + } } } } @@ -313,25 +267,19 @@ pub(crate) async fn service_archive_copy( name: &str, noun: &str, warnings: &mut Vec, -) -> ServiceCopy { +) -> Result, Box> { // The maven/nuget flows have no earlier guard, so the fail-closed // `--vendor-source=service` refusals (`--offline`, no API client) live // here (the other backends check the same helper at their entry points). if let Some(refusal) = service_offline_conflict(service) { - return ServiceCopy::HardFail(Box::new(refusal)); - } - let Some(cfg) = service else { - return ServiceCopy::HardFail(Box::new(super::service_fetch::required())); - }; - if !cfg.service_enabled() { - return ServiceCopy::HardFail(Box::new(super::service_fetch::required())); + return Err(Box::new(refusal)); } - let policy = ServicePolicy::new(cfg, ServiceTerminal::Refused); + let cfg = service + .filter(|cfg| cfg.service_enabled()) + .ok_or_else(|| Box::new(super::service_fetch::required()))?; + let policy = ServicePolicy::Refused; let fetched = fetch_verified_archive(cfg, &record.uuid).await; - let archive = match policy.settle(fetched, noun, noun, warnings) { - Ok(archive) => archive, - Err(attempt) => return attempt, - }; + let archive = policy.settle(fetched, noun, noun)?; // The SRI proves the download is intact, not that it carries the // patch: the bytes are written verbatim and reported AlreadyPatched, // so every patched member must hash to its afterHash first (the @@ -341,17 +289,13 @@ pub(crate) async fn service_archive_copy( .zip_verdict(&record.files) .unwrap_or_else(|| zip_bytes_match_after_hashes(&archive.bytes, &record.files)) { - return policy.miss( - warnings, - "vendor_prebuilt_layout_mismatch", - format!( - "prebuilt {noun} for {name} does not carry the patched files at their \ + return Err(policy.miss(format!( + "prebuilt {noun} for {name} does not carry the patched files at their \ recorded paths" - ), - ); + ))); } warnings.push(archive.downloaded_warning(name)); - ServiceCopy::Used(archive.bytes) + Ok(archive.bytes) } /// Outcome of fetching + verifying a named secondary artifact. @@ -371,9 +315,8 @@ pub(crate) enum SecondaryArtifactResult { /// `gem-stub-gemspec`) referenced by a [`VerifiedArchive`]. /// /// The bytes are verified against the artifact's own sha512 SRI, fail-closed -/// like the primary archive. Returns `Absent` when the archive referenced no -/// artifact of this kind — the caller treats that as a miss (fall back under -/// `auto`, refuse under `service`). +/// like the primary archive. Returns `Absent` when no artifact of this kind +/// was referenced. pub(crate) async fn fetch_verified_secondary( cfg: &VendorServiceConfig, archive: &VerifiedArchive, @@ -571,26 +514,23 @@ mod tests { )); } - /// IntegrityMismatch is a hard error in EVERY mode — under the - /// default `auto` the Tier-A copy must refuse, never fall back to a local - /// rebuild on tampered bytes (the enum's own contract: "never fall back"). + /// Tampered bytes must be refused before extraction. #[tokio::test] - async fn service_copy_integrity_mismatch_auto_hard_fails() { + async fn service_copy_integrity_mismatch_hard_fails() { let server = MockServer::start().await; let body = b"the real bytes"; let wrong = PackedTarball::from_bytes(b"completely different bytes").integrity; mount_granted(&server, &wrong, body).await; - let mut cfg = cfg_for(&server); - cfg.source = VendorSource::Service; + let cfg = cfg_for(&server); let mut warnings = Vec::new(); match service_archive_copy(Some(&cfg), &record(), "x", ".jar", &mut warnings).await { - ServiceCopy::HardFail(outcome) => match *outcome { + Err(outcome) => match *outcome { VendorOutcome::Refused { code, .. } => { assert_eq!(code, "vendor_prebuilt_integrity_mismatch"); } other => panic!("expected Refused, got {other:?}"), }, - ServiceCopy::Used(_) => panic!("tampered bytes must never be used"), + Ok(_) => panic!("tampered bytes must never be used"), } } @@ -601,27 +541,14 @@ mod tests { cfg.offline = true; let mut warnings = Vec::new(); match service_archive_copy(Some(&cfg), &record(), "x", ".jar", &mut warnings).await { - ServiceCopy::HardFail(outcome) => match *outcome { + Err(outcome) => match *outcome { VendorOutcome::Refused { code, .. } => { assert_eq!(code, "vendor_service_offline_conflict"); } other => panic!("expected Refused, got {other:?}"), }, - ServiceCopy::Used(_) => panic!("offline run must not download"), + Ok(_) => panic!("offline run must not download"), } - } - - #[tokio::test] - async fn service_copy_offline_refuses_without_advisory() { - let server = MockServer::start().await; - let mut cfg = cfg_for(&server); - cfg.source = VendorSource::Service; - cfg.offline = true; - let mut warnings = Vec::new(); - assert!(matches!( - service_archive_copy(Some(&cfg), &record(), "x", ".jar", &mut warnings).await, - ServiceCopy::HardFail(_) - )); assert!(warnings.is_empty(), "the refusal needs no advisory"); } @@ -680,7 +607,7 @@ mod tests { } /// Tier-A happy path: a granted, integrity-verified archive comes back as - /// `Used(bytes)` plus exactly one `vendor_prebuilt_downloaded` advisory + /// bytes plus exactly one `vendor_prebuilt_downloaded` advisory /// naming the package and the serve URL. #[tokio::test] async fn service_copy_ready_returns_used_bytes_with_downloaded_note() { @@ -698,8 +625,8 @@ mod tests { ) .await { - ServiceCopy::Used(bytes) => assert_eq!(bytes, body), - ServiceCopy::HardFail(outcome) => panic!("expected Used, got HardFail({outcome:?})"), + Ok(bytes) => assert_eq!(bytes, body), + Err(outcome) => panic!("expected archive bytes, got {outcome:?}"), } assert_eq!(warnings.len(), 1, "exactly one downloaded advisory"); assert_eq!(warnings[0].code, "vendor_prebuilt_downloaded"); @@ -717,23 +644,6 @@ mod tests { ); } - #[tokio::test] - async fn service_copy_pending_refuses_without_fallback() { - let server = MockServer::start().await; - mount_status(&server, "pending_build").await; - let mut cfg = cfg_for(&server); - cfg.source = VendorSource::Service; - let mut warnings = Vec::new(); - assert!(matches!( - service_archive_copy(Some(&cfg), &record(), "x", ".jar", &mut warnings).await, - ServiceCopy::HardFail(_) - )); - assert!( - warnings.is_empty(), - "a hard failure is not a fallback advisory" - ); - } - #[tokio::test] async fn service_copy_pending_service_hard_fails() { let server = MockServer::start().await; @@ -748,14 +658,14 @@ mod tests { ) .await { - ServiceCopy::HardFail(outcome) => match *outcome { + Err(outcome) => match *outcome { VendorOutcome::Refused { code, detail } => { assert_eq!(code, "vendor_prebuilt_required"); assert!(detail.contains("is still building"), "{detail}"); } other => panic!("expected Refused, got {other:?}"), }, - ServiceCopy::Used(_) => panic!("pending build must not yield bytes"), + Ok(_) => panic!("pending build must not yield bytes"), } assert!(warnings.is_empty(), "the hard-fail path must not warn"); } @@ -776,60 +686,18 @@ mod tests { ) .await { - ServiceCopy::HardFail(outcome) => match *outcome { + Err(outcome) => match *outcome { VendorOutcome::Refused { code, detail } => { assert_eq!(code, "vendor_prebuilt_required"); assert_eq!(detail, "prebuilt .jar unavailable: not_found"); } other => panic!("expected Refused, got {other:?}"), }, - ServiceCopy::Used(_) => panic!("unavailable archive must not yield bytes"), + Ok(_) => panic!("unavailable archive must not yield bytes"), } assert!(warnings.is_empty(), "the hard-fail path must not warn"); } - /// An unavailable artifact is a refusal, with no fallback advisory. - #[tokio::test] - async fn service_copy_unavailable_refuses_without_fallback() { - let server = MockServer::start().await; - mount_status(&server, "not_found").await; - let mut cfg = cfg_for(&server); - cfg.source = VendorSource::Service; - let mut warnings = Vec::new(); - assert!(matches!( - service_archive_copy(Some(&cfg), &record(), "x", ".jar", &mut warnings).await, - ServiceCopy::HardFail(_) - )); - assert!( - warnings.is_empty(), - "Unavailable under auto must fall back quietly, without a warning" - ); - } - - /// Transport failure under `auto`: warn and fall back. The Failed arm - /// deliberately reuses the `vendor_prebuilt_unavailable` warning code - /// (matching the golang mapping) rather than a dedicated one. - #[tokio::test] - async fn service_copy_failed_refuses_without_fallback() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v0/orgs/acme/patches/package")) - .respond_with(ResponseTemplate::new(500)) - .mount(&server) - .await; - let mut cfg = cfg_for(&server); - cfg.source = VendorSource::Service; - let mut warnings = Vec::new(); - assert!(matches!( - service_archive_copy(Some(&cfg), &record(), "x", ".jar", &mut warnings).await, - ServiceCopy::HardFail(_) - )); - assert!( - warnings.is_empty(), - "a hard failure is not a fallback advisory" - ); - } - #[tokio::test] async fn service_copy_failed_service_hard_fails() { let server = MockServer::start().await; @@ -848,7 +716,7 @@ mod tests { ) .await { - ServiceCopy::HardFail(outcome) => match *outcome { + Err(outcome) => match *outcome { VendorOutcome::Refused { code, detail } => { assert_eq!(code, "vendor_prebuilt_required"); assert!( @@ -858,7 +726,7 @@ mod tests { } other => panic!("expected Refused, got {other:?}"), }, - ServiceCopy::Used(_) => panic!("a failed request must not yield bytes"), + Ok(_) => panic!("a failed request must not yield bytes"), } assert!(warnings.is_empty(), "the hard-fail path must not warn"); } @@ -931,7 +799,7 @@ mod tests { let mut warnings = Vec::new(); let copy = service_archive_copy(Some(&cfg), &rec, "x", ".jar", &mut warnings).await; match (source, copy) { - (VendorSource::Service, ServiceCopy::HardFail(outcome)) => match *outcome { + (VendorSource::Service, Err(outcome)) => match *outcome { VendorOutcome::Refused { code, detail } => { assert_eq!(code, "vendor_prebuilt_required"); assert!( @@ -941,7 +809,7 @@ mod tests { } other => panic!("expected Refused, got {other:?}"), }, - (source, ServiceCopy::Used(_)) => { + (source, Ok(_)) => { panic!("{source:?}: unpatched service bytes were accepted") } } diff --git a/crates/socket-patch-core/src/vendor/state.rs b/crates/socket-patch-core/src/vendor/state.rs index 9217c3baa..9c84ba93b 100644 --- a/crates/socket-patch-core/src/vendor/state.rs +++ b/crates/socket-patch-core/src/vendor/state.rs @@ -312,26 +312,23 @@ pub struct VendorEntry { } impl VendorEntry { - /// The ledger entry every npm-family vendor backend records: ecosystem - /// `npm`, the wiring flavor (`None` is package-lock's pre-flavor - /// spelling) and no other ecosystem's extras. A flavor sets its one - /// meta field afterwards (`pnpm`, `artifact.yarn_berry10c0`). - pub(crate) fn npm( + /// Create an entry with no ecosystem-specific metadata or embedded record. + pub fn new( + ecosystem: String, base_purl: String, uuid: String, artifact: VendorArtifact, wiring: Vec, - flavor: Option<&str>, ) -> Self { Self { - ecosystem: "npm".to_string(), + ecosystem, base_purl, uuid, artifact, wiring, lock: None, took_over_go_patches: false, - flavor: flavor.map(str::to_string), + flavor: None, uv: None, pnpm: None, poetry: None, @@ -342,6 +339,23 @@ impl VendorEntry { } } + /// The ledger entry every npm-family vendor backend records: ecosystem + /// `npm`, the wiring flavor (`None` is package-lock's pre-flavor + /// spelling) and no other ecosystem's extras. A flavor sets its one + /// meta field afterwards (`pnpm`, `artifact.yarn_berry10c0`). + pub(crate) fn npm( + base_purl: String, + uuid: String, + artifact: VendorArtifact, + wiring: Vec, + flavor: Option<&str>, + ) -> Self { + Self { + flavor: flavor.map(str::to_string), + ..Self::new("npm".to_string(), base_purl, uuid, artifact, wiring) + } + } + /// Whether this entry's committed artifact is on disk under /// `project_root` — for a FILE artifact (wheel, tarball: a recorded /// `sha256`), only when its bytes still hash to that pin; a copy dir @@ -989,11 +1003,11 @@ mod tests { const UUID: &str = "9f6b2c4e-1d3a-4f6b-8c2d-7e5a9b1c3d5f"; fn sample_entry() -> VendorEntry { - VendorEntry { - ecosystem: "npm".into(), - base_purl: "pkg:npm/lodash@4.17.21".into(), - uuid: UUID.into(), - artifact: VendorArtifact { + VendorEntry::new( + "npm".into(), + "pkg:npm/lodash@4.17.21".into(), + UUID.into(), + VendorArtifact { yarn_berry10c0: None, path: format!(".socket/vendor/npm/{UUID}/lodash-4.17.21.tgz"), sha256: "ab".repeat(32), @@ -1001,7 +1015,7 @@ mod tests { platform_locked: None, file_inventory: None, }, - wiring: vec![WiringRecord { + vec![WiringRecord { file: "package-lock.json".into(), kind: "npm_lock_entry".into(), action: WiringAction::Rewritten, @@ -1017,17 +1031,7 @@ mod tests { "integrity": "sha512-ours" })), }], - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + ) } /// A cargo entry migrated from the pre-v5 `.cargo/config.toml` wiring to @@ -1046,28 +1050,21 @@ mod tests { }; let uuid = "9f6b2c4e-1d3a-4f6b-8c2d-7e5a9b1c3d5f"; let base = |wiring: Vec, lock: Option| VendorEntry { - ecosystem: "cargo".into(), - base_purl: "pkg:cargo/cfg-if@1.0.4".into(), - uuid: uuid.into(), - artifact: VendorArtifact { - yarn_berry10c0: None, - path: format!(".socket/vendor/cargo/{uuid}/cfg-if-1.0.4"), - sha256: String::new(), - size: None, - platform_locked: None, - file_inventory: None, - }, - wiring, lock, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, + ..VendorEntry::new( + "cargo".into(), + "pkg:cargo/cfg-if@1.0.4".into(), + uuid.into(), + VendorArtifact { + yarn_berry10c0: None, + path: format!(".socket/vendor/cargo/{uuid}/cfg-if-1.0.4"), + sha256: String::new(), + size: None, + platform_locked: None, + file_inventory: None, + }, + wiring, + ) }; let orig = CargoLockOriginal { source: "registry+https://github.com/rust-lang/crates.io-index".into(), diff --git a/crates/socket-patch-core/src/vendor/test_support.rs b/crates/socket-patch-core/src/vendor/test_support.rs index c94b74598..567ab7b10 100644 --- a/crates/socket-patch-core/src/vendor/test_support.rs +++ b/crates/socket-patch-core/src/vendor/test_support.rs @@ -508,515 +508,62 @@ async fn assert_fresh_vendor_in_use(root: &Path, outcome: &VendorOutcome, dry_ru ); } -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_pnpm<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::pnpm_lock::vendor_pnpm( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_yarn_classic<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::yarn_classic_lock::vendor_yarn_classic( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_npm<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::npm_lock::vendor_npm( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_composer<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::composer_lock::vendor_composer( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_cargo_crate<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::cargo::vendor_cargo_crate( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_vlt<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::vlt_lock::vendor_vlt( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_maven<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::maven_repo::vendor_maven( - purl, - source.path(), - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_pnpm_legacy<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::pnpm_lock_legacy::vendor_pnpm_legacy( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_nuget<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::nuget_feed::vendor_nuget( - purl, - source.path(), - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_yarn_berry<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::yarn_berry_lock::vendor_yarn_berry( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_pypi<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::pypi::vendor_pypi( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_gem<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::gem::vendor_gem( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_go_module<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::golang::vendor_go_module( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_npm_any<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::npm_flavor::vendor_npm_any( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn vendor_bun<'a>( - purl: &str, - source: impl Into>, - project_root: &Path, - record: &crate::manifest::schema::PatchRecord, - sources: &crate::patch::apply::PatchSources<'_>, - vendored_at: &str, - dry_run: bool, - force: bool, - service: Option<&VendorServiceConfig>, -) -> VendorOutcome { - let source = source.into(); - let fixture = if service.is_none() { - Some(service_fixture::Fixture::new(purl, source, record, sources).await) - } else { - None - }; - let outcome = super::bun_lock::vendor_bun( - purl, - source, - project_root, - record, - sources, - vendored_at, - dry_run, - force, - service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), - ) - .await; - assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; - outcome -} +// Keep the same fixture lifetime and post-vendor GC assertion for every backend. +macro_rules! vendor_fixtures { + ($($module:ident::$name:ident),+ $(,)?) => {$( + #[allow(clippy::too_many_arguments)] + pub(crate) async fn $name<'a>( + purl: &str, + source: impl Into>, + project_root: &Path, + record: &crate::manifest::schema::PatchRecord, + sources: &crate::patch::apply::PatchSources<'_>, + vendored_at: &str, + dry_run: bool, + force: bool, + service: Option<&VendorServiceConfig>, + ) -> VendorOutcome { + let source = source.into(); + let fixture = if service.is_none() { + Some(service_fixture::Fixture::new(purl, source, record, sources).await) + } else { + None + }; + let outcome = super::$module::$name( + purl, + source.path(), + project_root, + record, + sources, + vendored_at, + dry_run, + force, + service.or_else(|| fixture.as_ref().map(|f| &f.cfg)), + ) + .await; + assert_fresh_vendor_in_use(project_root, &outcome, dry_run).await; + outcome + } + )+}; +} + +vendor_fixtures!( + pnpm_lock::vendor_pnpm, + yarn_classic_lock::vendor_yarn_classic, + npm_lock::vendor_npm, + composer_lock::vendor_composer, + cargo::vendor_cargo_crate, + vlt_lock::vendor_vlt, + maven_repo::vendor_maven, + pnpm_lock_legacy::vendor_pnpm_legacy, + nuget_feed::vendor_nuget, + yarn_berry_lock::vendor_yarn_berry, + pypi::vendor_pypi, + gem::vendor_gem, + golang::vendor_go_module, + npm_flavor::vendor_npm_any, + bun_lock::vendor_bun, +); pub(crate) fn expect_failure(outcome: VendorOutcome) -> String { match outcome { diff --git a/crates/socket-patch-core/src/vendor/test_support/service_fixture.rs b/crates/socket-patch-core/src/vendor/test_support/service_fixture.rs index dec7f9e38..47ac3d38a 100644 --- a/crates/socket-patch-core/src/vendor/test_support/service_fixture.rs +++ b/crates/socket-patch-core/src/vendor/test_support/service_fixture.rs @@ -68,41 +68,38 @@ impl Fixture { } else { None }; - match Ok::<_, String>(source.path()) { - Ok(dir) => match archive(purl, dir, record, sources).await { - Ok((leaf, bytes, secondary)) => { - let uri = server.uri(); - let url = format!("{uri}/archive/{leaf}"); - let mut artifacts = vec![ - serde_json::json!({"kind":"tarball", "url":url, "integrity":{"sha512":super::sri(&bytes)}}), - ]; - if purl.starts_with("pkg:npm/") { - let name = crate::vendor::npm_common::parse_npm_purl(purl) - .map(|p| p.0) - .unwrap_or_default(); - if let Ok(checksum) = - crate::vendor::berry_zip::berry_cache_checksum_10c0(&bytes, &name) - { - artifacts.push(serde_json::json!({"kind":"yarn-berry-zip", "integrity":{"yarnBerry10c0":checksum}})); - } + match archive(purl, source.path(), record, sources).await { + Ok((leaf, bytes, secondary)) => { + let uri = server.uri(); + let url = format!("{uri}/archive/{leaf}"); + let mut artifacts = vec![ + serde_json::json!({"kind":"tarball", "url":url, "integrity":{"sha512":super::sri(&bytes)}}), + ]; + if purl.starts_with("pkg:npm/") { + let name = crate::vendor::npm_common::parse_npm_purl(purl) + .map(|p| p.0) + .unwrap_or_default(); + if let Ok(checksum) = + crate::vendor::berry_zip::berry_cache_checksum_10c0(&bytes, &name) + { + artifacts.push(serde_json::json!({"kind":"yarn-berry-zip", "integrity":{"yarnBerry10c0":checksum}})); } - for (kind, name, bytes) in secondary { - artifacts.push(serde_json::json!({"kind":kind,"url":format!("{uri}/archive/{name}"),"integrity":{"sha512":super::sri(&bytes)}})); - Mock::given(method("GET")) - .and(path(format!("/archive/{name}"))) - .respond_with(ResponseTemplate::new(200).set_body_bytes(bytes)) - .mount(&server) - .await; - } - Mock::given(method("POST")).and(path(super::PACKAGE_PATH)).respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"results":{ &record.uuid: {"status":"granted", "purl":purl, "url":url, "artifacts":artifacts}}}))).mount(&server).await; + } + for (kind, name, bytes) in secondary { + artifacts.push(serde_json::json!({"kind":kind,"url":format!("{uri}/archive/{name}"),"integrity":{"sha512":super::sri(&bytes)}})); Mock::given(method("GET")) - .and(path(format!("/archive/{leaf}"))) + .and(path(format!("/archive/{name}"))) .respond_with(ResponseTemplate::new(200).set_body_bytes(bytes)) .mount(&server) .await; } - Err(_) => super::mount_no_results(&server).await, - }, + Mock::given(method("POST")).and(path(super::PACKAGE_PATH)).respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"results":{ &record.uuid: {"status":"granted", "purl":purl, "url":url, "artifacts":artifacts}}}))).mount(&server).await; + Mock::given(method("GET")) + .and(path(format!("/archive/{leaf}"))) + .respond_with(ResponseTemplate::new(200).set_body_bytes(bytes)) + .mount(&server) + .await; + } Err(_) => super::mount_no_results(&server).await, } Self { diff --git a/crates/socket-patch-core/src/vendor/verify.rs b/crates/socket-patch-core/src/vendor/verify.rs index 2f972fcc2..b5ed2f2f3 100644 --- a/crates/socket-patch-core/src/vendor/verify.rs +++ b/crates/socket-patch-core/src/vendor/verify.rs @@ -852,11 +852,11 @@ mod tests { } fn entry(eco: &str, uuid: &str, rel_path: &str) -> VendorEntry { - VendorEntry { - ecosystem: eco.into(), - base_purl: "pkg:npm/x@1.0.0".into(), - uuid: uuid.into(), - artifact: VendorArtifact { + VendorEntry::new( + eco.into(), + "pkg:npm/x@1.0.0".into(), + uuid.into(), + VendorArtifact { yarn_berry10c0: None, path: rel_path.into(), sha256: String::new(), @@ -864,18 +864,8 @@ mod tests { platform_locked: None, file_inventory: None, }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + Vec::new(), + ) } fn write_tgz(dest: &Path, member: &str, bytes: &[u8]) { diff --git a/crates/socket-patch-core/src/vendor/vlt_lock.rs b/crates/socket-patch-core/src/vendor/vlt_lock.rs index f93aacd42..c211c794e 100644 --- a/crates/socket-patch-core/src/vendor/vlt_lock.rs +++ b/crates/socket-patch-core/src/vendor/vlt_lock.rs @@ -1246,16 +1246,15 @@ pub async fn keep_vlt_links( #[allow(clippy::too_many_arguments)] pub(crate) async fn vendor_vlt<'a>( purl: &str, - installed_dir: impl Into>, + _installed_dir: impl Into>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, + _sources: &PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, + _force: bool, service: Option<&super::VendorServiceConfig>, ) -> VendorOutcome { - let installed_dir = installed_dir.into(); let coords = match guard_coordinates(purl, record) { Ok(coords) => coords, Err(outcome) => return *outcome, @@ -1317,22 +1316,11 @@ pub(crate) async fn vendor_vlt<'a>( ) .await; - let (staged, result) = match stage_patch_dir( - purl, - installed_dir, - project_root, - record, - sources, - dry_run, - force, - &mut warnings, - service, - ) - .await - { - Ok(pair) => pair, - Err(outcome) => return *outcome, - }; + let (staged, result) = + match stage_patch_dir(purl, project_root, record, dry_run, &mut warnings, service).await { + Ok(pair) => pair, + Err(outcome) => return *outcome, + }; if result.success { warnings.extend(reinstall); } diff --git a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs index 13596dfb2..d2abbc6ac 100644 --- a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs +++ b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs @@ -92,26 +92,23 @@ const KIND_LOCK_ENTRY: &str = "yarn_berry_lock_entry"; #[allow(clippy::too_many_arguments)] pub async fn vendor_yarn_berry<'a>( purl: &str, - installed_dir: impl Into>, + _installed_dir: impl Into>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, + _sources: &PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, + _force: bool, service: Option<&super::VendorServiceConfig>, ) -> VendorOutcome { vendor_npm_family( &YarnBerryBackend, NpmVendorRequest { purl, - installed_dir: installed_dir.into(), project_root, record, - sources, vendored_at, dry_run, - force, service, }, ) diff --git a/crates/socket-patch-core/src/vendor/yarn_classic_lock.rs b/crates/socket-patch-core/src/vendor/yarn_classic_lock.rs index 12f28accc..66f4640d6 100644 --- a/crates/socket-patch-core/src/vendor/yarn_classic_lock.rs +++ b/crates/socket-patch-core/src/vendor/yarn_classic_lock.rs @@ -66,26 +66,23 @@ const KIND_LOCK_BLOCK: &str = "yarn_lock_block"; #[allow(clippy::too_many_arguments)] pub async fn vendor_yarn_classic<'a>( purl: &str, - installed_dir: impl Into>, + _installed_dir: impl Into>, project_root: &Path, record: &PatchRecord, - sources: &PatchSources<'_>, + _sources: &PatchSources<'_>, vendored_at: &str, dry_run: bool, - force: bool, + _force: bool, service: Option<&super::VendorServiceConfig>, ) -> VendorOutcome { vendor_npm_family( &YarnClassicBackend, NpmVendorRequest { purl, - installed_dir: installed_dir.into(), project_root, record, - sources, vendored_at, dry_run, - force, service, }, ) diff --git a/crates/socket-patch-core/src/vex/verify.rs b/crates/socket-patch-core/src/vex/verify.rs index b89530d8b..0294fea7c 100644 --- a/crates/socket-patch-core/src/vex/verify.rs +++ b/crates/socket-patch-core/src/vex/verify.rs @@ -1340,11 +1340,11 @@ mod tests { const VUUID: &str = "9f6b2c4e-1d3a-4f6b-8c2d-7e5a9b1c3d5f"; fn vendor_entry(purl: &str, rel_path: &str) -> VendorEntry { - VendorEntry { - ecosystem: "cargo".to_string(), - base_purl: purl.to_string(), - uuid: VUUID.to_string(), - artifact: VendorArtifact { + VendorEntry::new( + "cargo".to_string(), + purl.to_string(), + VUUID.to_string(), + VendorArtifact { yarn_berry10c0: None, path: rel_path.to_string(), sha256: String::new(), @@ -1352,18 +1352,8 @@ mod tests { platform_locked: None, file_inventory: None, }, - wiring: Vec::new(), - lock: None, - took_over_go_patches: false, - detached: false, - record: None, - flavor: None, - uv: None, - pnpm: None, - poetry: None, - pdm: None, - pipenv: None, - } + Vec::new(), + ) } /// `applied_patches` must be exactly `applied_patches_with_vendor(.., None)` diff --git a/crates/socket-patch-node/src/provider.rs b/crates/socket-patch-node/src/provider.rs index 13b132ce7..1215354cc 100644 --- a/crates/socket-patch-node/src/provider.rs +++ b/crates/socket-patch-node/src/provider.rs @@ -174,10 +174,6 @@ fn missing_value(method: &str) -> ApiError { )) } -fn request_json(value: serde_json::Value) -> String { - value.to_string() -} - impl PatchApi for JsPatchApi { fn uses_public_proxy(&self) -> bool { false @@ -192,7 +188,7 @@ impl PatchApi for JsPatchApi { .iter() .map(|purl| serde_json::json!({ "purl": purl })) .collect(); - let request = request_json(serde_json::json!({ "components": components })); + let request = serde_json::json!({ "components": components }).to_string(); let method = "searchPatchesBatch"; // Like the HTTP collection-route 404: "no patches" is an empty // success, so a miss is a misconfiguration, not zero patches. @@ -211,7 +207,7 @@ impl PatchApi for JsPatchApi { fn search_patches_by_package<'a>(&'a self, purl: &'a str) -> ApiFuture<'a, SearchResponse> { Box::pin(async move { let method = "searchPatchesByPackage"; - let request = request_json(serde_json::json!({ "purl": purl })); + let request = serde_json::json!({ "purl": purl }).to_string(); match call_json(method, &self.search_patches_by_package, request).await? { Reply::Value(value) => value.ok_or_else(|| missing_value(method)), Reply::NotFound(_) => Ok(SearchResponse { @@ -231,7 +227,7 @@ impl PatchApi for JsPatchApi { return Ok(HashMap::new()); } let method = "fetchRegistryReferences"; - let request = request_json(serde_json::json!({ "uuids": uuids })); + let request = serde_json::json!({ "uuids": uuids }).to_string(); match call_json::(method, &self.fetch_registry_references, request) .await? { @@ -243,7 +239,7 @@ impl PatchApi for JsPatchApi { fn fetch_patch<'a>(&'a self, uuid: &'a str) -> ApiFuture<'a, Option> { Box::pin(async move { - let request = request_json(serde_json::json!({ "uuid": uuid })); + let request = serde_json::json!({ "uuid": uuid }).to_string(); match call_json("fetchPatch", &self.fetch_patch, request).await? { Reply::Value(value) => Ok(value), Reply::NotFound(_) => Ok(None), @@ -254,7 +250,7 @@ impl PatchApi for JsPatchApi { fn download_artifact<'a>(&'a self, url: &'a str, max_bytes: u64) -> ApiFuture<'a, Vec> { Box::pin(async move { let method = "downloadArtifact"; - let request = request_json(serde_json::json!({ "url": url, "maxBytes": max_bytes })); + let request = serde_json::json!({ "url": url, "maxBytes": max_bytes }).to_string(); let promise = self .download_artifact .call_async_catch(request)