From 9efcc00296cc6b714d555ccd9809d7df2fa27cd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 21:27:10 +0000 Subject: [PATCH 1/8] Start fix for #376, #378 Assisted-by: Claude Code:claude-opus-5-5 From 04ecd7027a0243f0557ded2ebcc37c2a14b30f0c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 21:42:43 +0000 Subject: [PATCH 2/8] Keep unhashed requirements.txt installable A hosted or vendored scan added --hash to the patched line of a requirements file that had no hashes. pip then turns on hash-checking mode for the whole install, so every other requirement and every transitive dependency failed to install (#376). Both writers now check whether the requirements set is already in hash-checking mode. If it is, they keep writing --hash as before. If not, hosted pins the patched wheel with the url's #sha256= fragment, which pip still verifies, and vendored writes the committed wheel path without a hash. Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-cli/src/commands/get.rs | 12 +- .../src/commands/scan/discovery.rs | 6 +- crates/socket-patch-cli/src/commands/setup.rs | 3 +- .../socket-patch-cli/src/commands/update.rs | 21 ++- .../socket-patch-cli/src/commands/vendor.rs | 6 +- .../src/hosted_memory/redirect.rs | 3 +- .../socket-patch-cli/tests/apply_network.rs | 5 +- .../tests/cli_config_fallback.rs | 7 +- .../socket-patch-cli/tests/cli_parse_list.rs | 10 +- .../tests/cli_parse_rollback.rs | 6 +- .../coverage_fix_apply_silent_mute_exit.rs | 4 +- .../coverage_fix_repair_vendor_predelete.rs | 14 +- .../tests/covgap_commands_update.rs | 12 +- .../tests/covgap_commands_vex.rs | 16 +- .../tests/covgap_ecosystem_dispatch.rs | 8 +- .../socket-patch-cli/tests/covgap_output.rs | 10 +- .../tests/covgap_setup_composer_mod.rs | 5 +- .../tests/covgap_setup_gem_mod.rs | 14 +- .../tests/covgap_setup_pypi_detect.rs | 11 +- crates/socket-patch-cli/tests/e2e_cargo.rs | 6 +- crates/socket-patch-cli/tests/e2e_maven.rs | 3 +- crates/socket-patch-cli/tests/e2e_nuget.rs | 6 +- .../tests/get_edge_cases_e2e.rs | 12 +- .../tests/global_packages_e2e.rs | 5 +- .../tests/help_text_hygiene.rs | 12 +- .../tests/hosted_memory_engine.rs | 3 +- .../tests/in_process_redirect/vlt.rs | 10 +- .../tests/in_process_redirect_pdm.rs | 34 +++-- .../tests/in_process_redirect_pipenv.rs | 93 +++++++++--- .../tests/in_process_rollback_hosted/vlt.rs | 18 ++- .../tests/interactive_prompts_e2e.rs | 5 +- .../tests/rollback_duality_invariants.rs | 3 +- .../socket-patch-cli/tests/scan_vendor_e2e.rs | 6 +- .../tests/self_update_channels_e2e.rs | 5 +- .../tests/vendor_rerun_no_network_e2e.rs | 13 +- .../src/crawlers/npm_crawler.rs | 6 +- .../src/crawlers/npm_crawler/oracle.rs | 6 +- .../src/crawlers/python_crawler.rs | 6 +- .../patch/redirect/group_equivalence_tests.rs | 7 +- .../src/patch/redirect/mod.rs | 43 ++++-- .../src/patch/redirect/pdm.rs | 15 +- .../src/patch/redirect/pipenv.rs | 88 ++++++++--- .../src/patch/redirect/poetry.rs | 32 +++- .../src/patch/redirect/requirements.rs | 78 +++++++++- .../socket-patch-core/src/update/download.rs | 13 +- .../socket-patch-core/src/update/release.rs | 39 +++-- .../src/utils/group_commit.rs | 21 ++- crates/socket-patch-core/src/utils/mod.rs | 2 +- .../socket-patch-core/src/utils/pdm_lock.rs | 11 +- .../src/utils/poetry_lock.rs | 107 +++++++++++--- .../src/utils/python_script.rs | 7 +- .../src/utils/requirements.rs | 19 +++ .../src/vendor/lock_inventory/mod.rs | 2 +- .../src/vendor/lock_inventory/tests.rs | 2 +- .../src/vendor/lock_inventory/vlt.rs | 2 +- .../socket-patch-core/src/vendor/npm_dir.rs | 6 +- .../socket-patch-core/src/vendor/prestage.rs | 5 +- crates/socket-patch-core/src/vendor/pypi.rs | 31 ++-- .../src/vendor/pypi_requirements.rs | 138 ++++++++++++++---- .../src/vendor/toml_surgery.rs | 3 +- .../tests/covgap_api_blob_fetcher.rs | 5 +- .../tests/covgap_crawlers_composer_crawler.rs | 6 +- .../tests/covgap_vendor_nuget_feed.rs | 4 +- .../socket-patch-core/tests/poetry_hosted.rs | 115 ++++++++++++--- 64 files changed, 934 insertions(+), 292 deletions(-) diff --git a/crates/socket-patch-cli/src/commands/get.rs b/crates/socket-patch-cli/src/commands/get.rs index 86e0dbbb4..49901285d 100644 --- a/crates/socket-patch-cli/src/commands/get.rs +++ b/crates/socket-patch-cli/src/commands/get.rs @@ -7272,8 +7272,11 @@ mod tests { let installed = |name: &str, body: &[u8]| { let dist = site.path().join(format!("{name}-1.0.0.dist-info")); std::fs::create_dir_all(&dist).unwrap(); - std::fs::write(dist.join("METADATA"), format!("Name: {name}\nVersion: 1.0.0\n")) - .unwrap(); + std::fs::write( + dist.join("METADATA"), + format!("Name: {name}\nVersion: 1.0.0\n"), + ) + .unwrap(); std::fs::write(site.path().join(format!("{name}.py")), body).unwrap(); compute_git_sha256_from_bytes(body) }; @@ -7317,7 +7320,10 @@ mod tests { mount(uuid("bs"), "beta_sdist.py".into(), "0".repeat(64), 0).await; for n in ["gw", "gs"] { Mock::given(method("GET")) - .and(wm_path(format!("/v0/orgs/test-org/patches/view/{}", uuid(n)))) + .and(wm_path(format!( + "/v0/orgs/test-org/patches/view/{}", + uuid(n) + ))) .respond_with(ResponseTemplate::new(500)) .expect(0) .mount(&server) diff --git a/crates/socket-patch-cli/src/commands/scan/discovery.rs b/crates/socket-patch-cli/src/commands/scan/discovery.rs index 862ec33e2..19a80a48f 100644 --- a/crates/socket-patch-cli/src/commands/scan/discovery.rs +++ b/crates/socket-patch-cli/src/commands/scan/discovery.rs @@ -2094,7 +2094,11 @@ mod tests { "pkg:npm/lockonly@1.0.0", std::path::PathBuf::from("/nonexistent"), ), - crawled_pkg("alpha", "pkg:npm/alpha@1.0.0", installed("alpha", "alpha.js")), + crawled_pkg( + "alpha", + "pkg:npm/alpha@1.0.0", + installed("alpha", "alpha.js"), + ), crawled_pkg( "embedded", "pkg:npm/embedded@1.0.0", diff --git a/crates/socket-patch-cli/src/commands/setup.rs b/crates/socket-patch-cli/src/commands/setup.rs index c530616ac..5aa62226a 100644 --- a/crates/socket-patch-cli/src/commands/setup.rs +++ b/crates/socket-patch-cli/src/commands/setup.rs @@ -247,8 +247,7 @@ async fn hooked_vlt_members(found: &PackageJsonFindResult) -> Vec { } let mut hooked = Vec::new(); for loc in found.files.iter().filter(|loc| !loc.is_root) { - if let Ok(content) = socket_patch_core::utils::fs::read_regular_to_string(&loc.path).await - { + if let Ok(content) = socket_patch_core::utils::fs::read_regular_to_string(&loc.path).await { let status = is_setup_configured_str(&content); if status.postinstall_configured || status.dependencies_configured { hooked.push(loc.path.clone()); diff --git a/crates/socket-patch-cli/src/commands/update.rs b/crates/socket-patch-cli/src/commands/update.rs index fa6c384bd..be7ae1777 100644 --- a/crates/socket-patch-cli/src/commands/update.rs +++ b/crates/socket-patch-cli/src/commands/update.rs @@ -159,7 +159,11 @@ fn cancelled_message(current: &semver::Version, target: &semver::Version) -> &'s /// The result line after a successful install, naming the same action as /// [`confirm_prompt`]. -fn installed_message(current: &semver::Version, target: &semver::Version, path: &std::path::Path) -> String { +fn installed_message( + current: &semver::Version, + target: &semver::Version, + path: &std::path::Path, +) -> String { let path = path.display(); if target < current { format!("Downgraded socket-patch {current} \u{2192} {target} ({path})") @@ -500,9 +504,18 @@ mod tests { #[test] fn cancel_and_result_lines_match_the_prompt() { - assert_eq!(cancelled_message(&v("4.0.0"), &v("9.9.9")), "Update cancelled."); - assert_eq!(cancelled_message(&v("4.0.0"), &v("3.0.0")), "Downgrade cancelled."); - assert_eq!(cancelled_message(&v("4.0.0"), &v("4.0.0")), "Reinstall cancelled."); + assert_eq!( + cancelled_message(&v("4.0.0"), &v("9.9.9")), + "Update cancelled." + ); + assert_eq!( + cancelled_message(&v("4.0.0"), &v("3.0.0")), + "Downgrade cancelled." + ); + assert_eq!( + cancelled_message(&v("4.0.0"), &v("4.0.0")), + "Reinstall cancelled." + ); let p = std::path::Path::new("/opt/sp/socket-patch"); assert_eq!( installed_message(&v("4.0.0"), &v("9.9.9"), p), diff --git a/crates/socket-patch-cli/src/commands/vendor.rs b/crates/socket-patch-cli/src/commands/vendor.rs index bd1342ac7..34ada0431 100644 --- a/crates/socket-patch-cli/src/commands/vendor.rs +++ b/crates/socket-patch-cli/src/commands/vendor.rs @@ -3864,7 +3864,11 @@ mod plan_gate_tests { .unwrap(); let packages = [ ("pkg:composer/psr/cache@1.0.0", "psr/cache", UUID_A), - ("pkg:composer/psr/http-message@1.1.0", "psr/http-message", UUID_B), + ( + "pkg:composer/psr/http-message@1.1.0", + "psr/http-message", + UUID_B, + ), ("pkg:composer/psr/log@3.0.2", "psr/log", UUID_C), ]; let mut all_packages: Vec<(String, StagedSource)> = Vec::new(); diff --git a/crates/socket-patch-cli/src/hosted_memory/redirect.rs b/crates/socket-patch-cli/src/hosted_memory/redirect.rs index 4a8763415..a9567eaf5 100644 --- a/crates/socket-patch-cli/src/hosted_memory/redirect.rs +++ b/crates/socket-patch-cli/src/hosted_memory/redirect.rs @@ -19,8 +19,7 @@ use socket_patch_core::patch::redirect::npmrc::{ NPMRC_REL, }; use socket_patch_core::patch::redirect::{ - rewrite_registry_redirect_withholding_vlt, DepOverride, FileEdit, RewriteResult, - RewriteWarning, + rewrite_registry_redirect_withholding_vlt, DepOverride, FileEdit, RewriteResult, RewriteWarning, }; use socket_patch_core::utils::purl::{purl_parts, strip_purl_qualifiers}; use socket_patch_core::vendor::lock_inventory::{MemoryEntry, MemoryProject}; diff --git a/crates/socket-patch-cli/tests/apply_network.rs b/crates/socket-patch-cli/tests/apply_network.rs index 562feae43..a0dc94f38 100644 --- a/crates/socket-patch-cli/tests/apply_network.rs +++ b/crates/socket-patch-cli/tests/apply_network.rs @@ -1075,10 +1075,7 @@ async fn mismatch_blob_topup_probes_every_copy_of_a_duplicated_package() { v["summary"]["applied"], 1, "the drifted nested copy must be warn-overwritten.\nstdout={v:#}" ); - assert_eq!( - v["summary"]["failed"], 0, - "no copy may fail.\nstdout={v:#}" - ); + assert_eq!(v["summary"]["failed"], 0, "no copy may fail.\nstdout={v:#}"); // The nested copy's blob was fetched on demand… let requests = mock.received_requests().await.unwrap(); diff --git a/crates/socket-patch-cli/tests/cli_config_fallback.rs b/crates/socket-patch-cli/tests/cli_config_fallback.rs index dc09b57a6..ed24d824d 100644 --- a/crates/socket-patch-cli/tests/cli_config_fallback.rs +++ b/crates/socket-patch-cli/tests/cli_config_fallback.rs @@ -59,8 +59,7 @@ fn scan_cmd(project: &Path, data_dir: &Path) -> Command { let mut cmd = Command::new(BINARY); // Human mode: core's proxy advisory (the oracle below) is muted under // `--json`/`--silent`. - cmd.args(["scan", "-e", "npm", "--cwd"]) - .arg(project); + cmd.args(["scan", "-e", "npm", "--cwd"]).arg(project); for (key, _) in std::env::vars_os() { let name = key.to_string_lossy(); if name.starts_with("SOCKET_") { @@ -298,7 +297,9 @@ async fn corrupt_config_warns_and_keeps_json_stdout_clean() { json_cmd.arg("--json"); let json_out = run(json_cmd); assert!( - json_out.stderr.contains("could not parse socket-cli config"), + json_out + .stderr + .contains("could not parse socket-cli config"), "the parse warning must reach stderr under --json too; got:\n{}", json_out.stderr ); diff --git a/crates/socket-patch-cli/tests/cli_parse_list.rs b/crates/socket-patch-cli/tests/cli_parse_list.rs index ad6b39392..cf9a838d5 100644 --- a/crates/socket-patch-cli/tests/cli_parse_list.rs +++ b/crates/socket-patch-cli/tests/cli_parse_list.rs @@ -1202,7 +1202,10 @@ fn missing_manifest_with_corrupt_ledger_keeps_warning_in_error_envelope_via_bina assert_eq!(v["error"]["code"], "manifest_not_found", "envelope={v}"); let warnings = v["warnings"].as_array().expect("warnings[] present"); assert_eq!(warnings.len(), 1, "envelope={v}"); - assert_eq!(warnings[0]["code"], "redirect_ledger_corrupt", "envelope={v}"); + assert_eq!( + warnings[0]["code"], "redirect_ledger_corrupt", + "envelope={v}" + ); assert!( out.stderr.is_empty(), "--json must keep stderr clean: {}", @@ -1214,7 +1217,10 @@ fn missing_manifest_with_corrupt_ledger_keeps_warning_in_error_envelope_via_bina let stderr = String::from_utf8_lossy(&out.stderr); assert_eq!(out.status.code(), Some(1)); assert!(stderr.contains("Warning: "), "stderr={stderr}"); - assert!(stderr.contains("Error: Manifest not found at "), "stderr={stderr}"); + assert!( + stderr.contains("Error: Manifest not found at "), + "stderr={stderr}" + ); } #[test] diff --git a/crates/socket-patch-cli/tests/cli_parse_rollback.rs b/crates/socket-patch-cli/tests/cli_parse_rollback.rs index d46f04b8f..ba87aaaac 100644 --- a/crates/socket-patch-cli/tests/cli_parse_rollback.rs +++ b/crates/socket-patch-cli/tests/cli_parse_rollback.rs @@ -378,7 +378,11 @@ fn bare_bool_does_not_consume_next_token() { /// relied on the rejection get a test-visible flip instead of a silent one. #[test] fn multiple_targets_parse_in_order() { - let args = parse_rollback(&["pkg:npm/foo@1", "packages/api/**", "b0630680-4da6-45f9-bba8-b888e0ffd58c"]); + let args = parse_rollback(&[ + "pkg:npm/foo@1", + "packages/api/**", + "b0630680-4da6-45f9-bba8-b888e0ffd58c", + ]); assert_eq!( args.targets, vec![ diff --git a/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs b/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs index a56820e7d..bd72b2afe 100644 --- a/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs +++ b/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs @@ -150,7 +150,9 @@ fn apply_silent_online_download_failure_keeps_error_output() { ); let chatter = stderr_chatter(&stderr); assert!( - chatter.iter().any(|l| l.contains("could not be downloaded")), + chatter + .iter() + .any(|l| l.contains("could not be downloaded")), "--silent must keep the download-failure error (errors only, \ never nothing); stderr was: {stderr:?}" ); diff --git a/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs b/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs index e403c3d6e..d68e382dc 100644 --- a/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs +++ b/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs @@ -234,12 +234,9 @@ async fn repair_keeps_healthy_soft_artifact_when_rebuild_dispatch_fails() { let gemfile_wired = std::fs::read(tmp.path().join("Gemfile")).unwrap(); std::fs::remove_file(tmp.path().join(".socket/vendor/state.json")).unwrap(); - std::fs::remove_file( - tmp.path() - .join(format!( - "vendor/bundle/ruby/3.4.0/gems/{GEM_NAME}-{GEM_VERSION}/lib/padlock.rb" - )), - ) + std::fs::remove_file(tmp.path().join(format!( + "vendor/bundle/ruby/3.4.0/gems/{GEM_NAME}-{GEM_VERSION}/lib/padlock.rb" + ))) .unwrap(); mount_blob(&mock).await; @@ -280,7 +277,10 @@ async fn repair_keeps_healthy_soft_artifact_when_rebuild_dispatch_fails() { &std::fs::read_to_string(tmp.path().join(".socket/vendor/state.json")).unwrap(), ) .unwrap(); - assert_eq!(state["entries"][GEM_PURL]["uuid"], GEM_UUID, "state={state}"); + assert_eq!( + state["entries"][GEM_PURL]["uuid"], GEM_UUID, + "state={state}" + ); assert_eq!( std::fs::read(tmp.path().join("Gemfile")).unwrap(), gemfile_wired, diff --git a/crates/socket-patch-cli/tests/covgap_commands_update.rs b/crates/socket-patch-cli/tests/covgap_commands_update.rs index d0b7b7ea8..ac923fc4d 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_update.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_update.rs @@ -10,10 +10,10 @@ //! self_update_e2e.rs / interactive_prompts_e2e.rs (do not edit those //! files). -#[path = "common/pty_io.rs"] -mod pty_io; #[path = "common/mod.rs"] mod common; +#[path = "common/pty_io.rs"] +mod pty_io; #[path = "common/update_fixture.rs"] mod update_fixture; @@ -275,9 +275,8 @@ mod pty { let mut child = pair.slave.spawn_command(cmd).expect("spawn in PTY"); drop(pair.slave); - let reader_handle = crate::pty_io::PtyOutput::spawn( - pair.master.try_clone_reader().expect("clone reader"), - ); + let reader_handle = + crate::pty_io::PtyOutput::spawn(pair.master.try_clone_reader().expect("clone reader")); let mut killer = child.clone_killer(); std::thread::spawn(move || { @@ -345,7 +344,8 @@ mod pty { "a declined update exits 1 (codebase convention); got: {output}" ); assert!( - !output.contains("Updated socket-patch") && !output.contains("Reinstalled socket-patch"), + !output.contains("Updated socket-patch") + && !output.contains("Reinstalled socket-patch"), "a declined update must not report a swap; got: {output}" ); diff --git a/crates/socket-patch-cli/tests/covgap_commands_vex.rs b/crates/socket-patch-cli/tests/covgap_commands_vex.rs index d9a31ae05..1c9d787c5 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_vex.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_vex.rs @@ -544,14 +544,24 @@ fn auto_detect_multi_manifest_warning_reaches_json_envelope() { ]) .output() .expect("invoke vex"); - assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + assert!( + out.status.success(), + "{}", + String::from_utf8_lossy(&out.stderr) + ); let env: Value = serde_json::from_slice(&out.stdout).expect("envelope JSON on stdout"); let w = env["warnings"] .as_array() - .and_then(|ws| ws.iter().find(|w| w["code"] == "product_multiple_manifests")) + .and_then(|ws| { + ws.iter() + .find(|w| w["code"] == "product_multiple_manifests") + }) .unwrap_or_else(|| panic!("product_multiple_manifests warning expected: {env}")); assert!( - w["detail"].as_str().unwrap().contains("Multiple project manifests"), + w["detail"] + .as_str() + .unwrap() + .contains("Multiple project manifests"), "{w}" ); let stderr = String::from_utf8_lossy(&out.stderr); diff --git a/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs b/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs index c3655302e..dc6e63536 100644 --- a/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs +++ b/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs @@ -254,7 +254,10 @@ fn rollback_dispatch_branch_deno() { .unwrap_or_else(|e| panic!("rollback envelope must parse ({e}); stdout={stdout}")); let code = out.status.code().unwrap_or(-1); - assert_eq!(code, 0, "rollback --ecosystems=deno: expected exit 0; env={env}"); + assert_eq!( + code, 0, + "rollback --ecosystems=deno: expected exit 0; env={env}" + ); assert_eq!( env["status"], "success", "rollback --ecosystems=deno: expected success; env={env}" @@ -295,7 +298,8 @@ fn rollback_dispatch_branch_deno() { // The decisive check: the on-disk bytes are restored to ORIGINAL. let restored = std::fs::read(&verify_file).unwrap(); assert_eq!( - restored, ORIGINAL, + restored, + ORIGINAL, "rollback --ecosystems=deno: {} was not restored to its original bytes", verify_file.display() ); diff --git a/crates/socket-patch-cli/tests/covgap_output.rs b/crates/socket-patch-cli/tests/covgap_output.rs index 21121cacb..0ea5c8759 100644 --- a/crates/socket-patch-cli/tests/covgap_output.rs +++ b/crates/socket-patch-cli/tests/covgap_output.rs @@ -173,9 +173,8 @@ fn run_in_pty_inner( .expect("spawn socket-patch in PTY"); drop(pair.slave); - let reader_handle = crate::pty_io::PtyOutput::spawn( - pair.master.try_clone_reader().expect("clone reader"), - ); + let reader_handle = + crate::pty_io::PtyOutput::spawn(pair.master.try_clone_reader().expect("clone reader")); // Watchdog: detached kill after `timeout`; a no-op if the child exits // naturally first. @@ -266,7 +265,10 @@ fn remove_interactive_bare_enter_proceeds_with_default_yes() { "\n", Duration::from_secs(15), ); - assert_eq!(code, 0, "remove with bare Enter must succeed; got: {output}"); + assert_eq!( + code, 0, + "remove with bare Enter must succeed; got: {output}" + ); // The interactive confirm MUST have run — otherwise this test passes // vacuously against a regression that drops the TTY gate and // auto-proceeds. Match the distinctive prompt verbatim (the loose diff --git a/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs b/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs index 5b056b87c..a2cf92ea0 100644 --- a/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs +++ b/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs @@ -126,7 +126,10 @@ fn remove_malformed_composer_json_errors_not_silent_noop() { write(&cwd.join("composer.json"), MALFORMED_COMPOSER_JSON); let (code, v) = run_json(cwd, &["setup", "--remove", "--yes", "--json"]); - assert_eq!(code, 1, "remove on a malformed composer.json must fail: {v}"); + assert_eq!( + code, 1, + "remove on a malformed composer.json must fail: {v}" + ); assert_eq!(v["status"], "error", "{v}"); assert_eq!(v["removed"], 0, "{v}"); assert_eq!(v["errors"], 1, "{v}"); diff --git a/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs b/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs index 989cc3816..829cad6a3 100644 --- a/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs +++ b/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs @@ -71,7 +71,10 @@ fn setup_remove_clears_bundler_registration_under_bundle_app_config() { &["setup", "--yes", "--json", "--ecosystems", "gem"], &[], ); - assert_eq!(code, 0, "gem setup must succeed.\nstdout:\n{stdout}\nstderr:\n{stderr}"); + assert_eq!( + code, 0, + "gem setup must succeed.\nstdout:\n{stdout}\nstderr:\n{stderr}" + ); let v = common::parse_json_envelope(&stdout); assert_eq!(v["status"], "success", "{v}"); assert!( @@ -110,7 +113,14 @@ fn setup_remove_clears_bundler_registration_under_bundle_app_config() { // Step 3: unwire with BUNDLE_APP_CONFIG set (child-only env injection). let (code, stdout, stderr) = common::run_with_env( root, - &["setup", "--remove", "--yes", "--json", "--ecosystems", "gem"], + &[ + "setup", + "--remove", + "--yes", + "--json", + "--ecosystems", + "gem", + ], &[("BUNDLE_APP_CONFIG", "bundle-config")], ); assert_eq!( diff --git a/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs b/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs index 814a55efe..6adf98e7a 100644 --- a/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs +++ b/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs @@ -45,7 +45,10 @@ fn read(path: &Path) -> String { fn write_pm_shim(bin_dir: &Path, name: &str, log: &Path) { use std::os::unix::fs::PermissionsExt; std::fs::create_dir_all(bin_dir).expect("create shim dir"); - let body = format!("#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n", log.display()); + let body = format!( + "#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n", + log.display() + ); let p = bin_dir.join(name); std::fs::write(&p, body).expect("write shim"); std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).expect("chmod shim"); @@ -80,11 +83,7 @@ fn assert_no_pm_spawned(project: &Path, context: &str) { /// through the shared hermetic runner (the seed-then-scrub of the ambient /// `SOCKET_*` surface is load-bearing: SOCKET_DRY_RUN=true would fake every /// edit, SOCKET_ECOSYSTEMS=npm would hide the Python branch entirely). -fn run_setup_with_shims( - cwd: &Path, - bin_dir: &Path, - extra: &[&str], -) -> (i32, serde_json::Value) { +fn run_setup_with_shims(cwd: &Path, bin_dir: &Path, extra: &[&str]) -> (i32, serde_json::Value) { let path_env = format!( "{}:{}", bin_dir.display(), diff --git a/crates/socket-patch-cli/tests/e2e_cargo.rs b/crates/socket-patch-cli/tests/e2e_cargo.rs index b8770d4ca..84efdd424 100644 --- a/crates/socket-patch-cli/tests/e2e_cargo.rs +++ b/crates/socket-patch-cli/tests/e2e_cargo.rs @@ -209,8 +209,7 @@ async fn scan_discovers_fake_registry_crates() { "Expected human scan to report exactly 'Found 2 packages (2 cargo)', got:\n{combined}" ); assert!( - !combined.contains("No packages found") - && !combined.contains("No packages found"), + !combined.contains("No packages found") && !combined.contains("No packages found"), "scan reported no packages despite a populated registry:\n{combined}" ); @@ -267,8 +266,7 @@ async fn scan_discovers_vendor_crates() { "Expected human scan to report exactly 'Found 1 package (1 cargo)', got:\n{combined}" ); assert!( - !combined.contains("No packages found") - && !combined.contains("No packages found"), + !combined.contains("No packages found") && !combined.contains("No packages found"), "scan reported no packages despite a populated vendor dir:\n{combined}" ); diff --git a/crates/socket-patch-cli/tests/e2e_maven.rs b/crates/socket-patch-cli/tests/e2e_maven.rs index 40676a012..996e5e855 100644 --- a/crates/socket-patch-cli/tests/e2e_maven.rs +++ b/crates/socket-patch-cli/tests/e2e_maven.rs @@ -182,8 +182,7 @@ async fn scan_discovers_maven_artifacts() { // the word "packages", which is exactly what let the old assertion // pass when discovery was disabled. assert!( - !combined.contains("No packages found") - && !combined.contains("No packages found"), + !combined.contains("No packages found") && !combined.contains("No packages found"), "scan reported zero packages — Maven discovery did not run:\n{combined}" ); assert!( diff --git a/crates/socket-patch-cli/tests/e2e_nuget.rs b/crates/socket-patch-cli/tests/e2e_nuget.rs index e00e2e8b1..1311fcb47 100644 --- a/crates/socket-patch-cli/tests/e2e_nuget.rs +++ b/crates/socket-patch-cli/tests/e2e_nuget.rs @@ -232,7 +232,8 @@ async fn scan_discovers_global_cache_packages() { // masked. assert!( !combined.contains("No packages found") - && !combined.contains("No packages found") && !combined.contains("No global packages found"), + && !combined.contains("No packages found") + && !combined.contains("No global packages found"), "scan failed to discover the fake global cache:\n{combined}" ); // Exactly the two packages we planted (Newtonsoft.Json, System.Text.Json), @@ -291,7 +292,8 @@ async fn scan_discovers_legacy_packages() { ); assert!( !combined.contains("No packages found") - && !combined.contains("No packages found") && !combined.contains("No global packages found"), + && !combined.contains("No packages found") + && !combined.contains("No global packages found"), "scan failed to discover the legacy packages/ layout:\n{combined}" ); // Exactly the single legacy package we planted (Newtonsoft.Json.13.0.3), diff --git a/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs b/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs index e419d8c64..93ee65a11 100644 --- a/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs +++ b/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs @@ -508,8 +508,16 @@ fn get_help_lists_all_identifier_flags() { // parseable (scripts get that explicit error) but is not advertised. assert!(!stdout.contains("--one-off"), "{stdout}"); // Help text is for users: no implementation notes from the source. - for leak in ["value_parser", "parse_bool_flag", "No env binding", "locally- installed"] { - assert!(!stdout.contains(leak), "get --help leaks {leak:?}: {stdout}"); + for leak in [ + "value_parser", + "parse_bool_flag", + "No env binding", + "locally- installed", + ] { + assert!( + !stdout.contains(leak), + "get --help leaks {leak:?}: {stdout}" + ); } } diff --git a/crates/socket-patch-cli/tests/global_packages_e2e.rs b/crates/socket-patch-cli/tests/global_packages_e2e.rs index 087a9fd2a..67af0cc9e 100644 --- a/crates/socket-patch-cli/tests/global_packages_e2e.rs +++ b/crates/socket-patch-cli/tests/global_packages_e2e.rs @@ -211,7 +211,10 @@ fn assert_rollback_noop(stdout: &str) { r["skipped"], "package_not_installed", "a no-op rollback may carry only not-installed markers; envelope={v}" ); - assert!(r["path"].is_null(), "marker path must be null; envelope={v}"); + assert!( + r["path"].is_null(), + "marker path must be null; envelope={v}" + ); assert!( r.get("success").is_none() && r.get("error").is_none(), "markers carry no success/error keys; envelope={v}" diff --git a/crates/socket-patch-cli/tests/help_text_hygiene.rs b/crates/socket-patch-cli/tests/help_text_hygiene.rs index 455ac4b39..b3a7063a0 100644 --- a/crates/socket-patch-cli/tests/help_text_hygiene.rs +++ b/crates/socket-patch-cli/tests/help_text_hygiene.rs @@ -61,7 +61,11 @@ fn every_help_page_has_no_developer_notes() { names.extend(cmd.get_subcommands().map(|s| s.get_name().to_string())); let mut failures = Vec::new(); for name in &names { - let path: Vec<&str> = if name.is_empty() { vec![] } else { vec![name.as_str()] }; + let path: Vec<&str> = if name.is_empty() { + vec![] + } else { + vec![name.as_str()] + }; let text = long_help(&path); let found = leaks(&text); if !found.is_empty() { @@ -166,8 +170,10 @@ fn vendor_and_repair_summaries_read_as_one_line() { "{text}" ); assert!( - text.lines().any(|l| l - == " repair Download missing patch artifacts and clean up unused ones [aliases: gc]"), + text.lines().any(|l| { + l + == " repair Download missing patch artifacts and clean up unused ones [aliases: gc]" + }), "{text}" ); let repair = long_help(&["repair"]); diff --git a/crates/socket-patch-cli/tests/hosted_memory_engine.rs b/crates/socket-patch-cli/tests/hosted_memory_engine.rs index 8b57dfc1a..a6e5e43b0 100644 --- a/crates/socket-patch-cli/tests/hosted_memory_engine.rs +++ b/crates/socket-patch-cli/tests/hosted_memory_engine.rs @@ -963,7 +963,8 @@ async fn a_vlt_project_is_withheld_as_offline() { .and_then(|w| w["detail"].as_str()) .expect("the preflight warning is reported"); assert!( - detail.contains("/patch/npm//") && detail.contains(": offline; nothing was written"), + detail.contains("/patch/npm//") + && detail.contains(": offline; nothing was written"), "the offline refusal quotes the redacted URL" ); assert!(output.changed_files.is_empty()); diff --git a/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs b/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs index 373455bee..5da588778 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs @@ -335,11 +335,15 @@ async fn scan_redirect_vlt_artifact_fetch_error() { let detail = warning_detail(&doc, UNVERIFIABLE); let redacted = url.replace(&format!("/{TOKEN}/"), "//"); assert!( - detail.starts_with(&format!("vlt would fail to verify {redacted}: fetch error ")) - && detail.ends_with(&format!("; nothing was written for {PURL}")), + detail.starts_with(&format!( + "vlt would fail to verify {redacted}: fetch error " + )) && detail.ends_with(&format!("; nothing was written for {PURL}")), "the fetch-error refusal quotes the redacted URL" ); - assert!(!detail.contains(TOKEN), "the grant token never reaches the warning"); + assert!( + !detail.contains(TOKEN), + "the grant token never reaches the warning" + ); } async fn redirect_chain(hops: usize) -> (Value, tempfile::TempDir) { diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs b/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs index a42a8f6c9..439adfa1c 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs @@ -109,7 +109,9 @@ async fn mock_api(server: &MockServer) { .mount(server) .await; Mock::given(method("GET")) - .and(path_regex(format!("^/v0/orgs/{ORG}/patches/by-package/.+$"))) + .and(path_regex(format!( + "^/v0/orgs/{ORG}/patches/by-package/.+$" + ))) .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "patches": [{ "uuid": UUID, "purl": RECORD_PURL, @@ -337,8 +339,10 @@ async fn lock_only_pdm_project_redirects_attests_rescans_and_rolls_back() { PYPROJECT, "pyproject untouched" ); - let ledger: serde_json::Value = - serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))).unwrap(); + let ledger: serde_json::Value = serde_json::from_str(&read( + &tmp.path().join(".socket/vendor/redirect-state.json"), + )) + .unwrap(); assert!( ledger["records"][RECORD_PURL].is_object(), "ledger keyed by the artifact-qualified purl: {ledger}" @@ -363,7 +367,11 @@ async fn lock_only_pdm_project_redirects_attests_rescans_and_rolls_back() { // 2. Idempotent re-scan: no further edits, lock byte-identical. let code = run(hosted_args(tmp.path(), server.uri(), None)).await; assert_eq!(code, 0); - assert_eq!(read(&lock_path), redirected, "re-scan must not touch the lock"); + assert_eq!( + read(&lock_path), + redirected, + "re-scan must not touch the lock" + ); // 3. The committed state, manifest-less, attests (and only while wired). assert_manifestless_vex(tmp.path(), LOCK); @@ -422,8 +430,10 @@ async fn hatchling_build_backend_does_not_veto_the_pdm_lock_redirect() { pyproject, "pyproject untouched" ); - let ledger: serde_json::Value = - serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))).unwrap(); + let ledger: serde_json::Value = serde_json::from_str(&read( + &tmp.path().join(".socket/vendor/redirect-state.json"), + )) + .unwrap(); assert!( ledger["records"][RECORD_PURL].is_object(), "the pdm redirect must be confirmed and recorded despite the hatch backend: {ledger}" @@ -443,7 +453,11 @@ async fn hatchling_build_backend_does_not_veto_the_pdm_lock_redirect() { }) .await; assert_eq!(code, 0, "rollback must succeed"); - assert_eq!(read(&lock_path), LOCK, "rollback must restore the pristine lock"); + assert_eq!( + read(&lock_path), + LOCK, + "rollback must restore the pristine lock" + ); } /// The legacy `[metadata.files]` lock (lock_version 2) redirects the package @@ -461,8 +475,10 @@ async fn legacy_metadata_files_lock_redirects_both_fragments_and_warns() { assert_eq!(code, 0); let redirected = read(&lock_path); assert!(redirected.contains(HOSTED_URL), "{redirected}"); - let ledger: serde_json::Value = - serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))).unwrap(); + let ledger: serde_json::Value = serde_json::from_str(&read( + &tmp.path().join(".socket/vendor/redirect-state.json"), + )) + .unwrap(); assert_eq!( ledger["edits"].as_array().unwrap().len(), 2, diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs b/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs index 1d01bc2df..731f48dcb 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs @@ -54,7 +54,8 @@ const MAJOR_ENV: &str = socket_patch_core::utils::pipenv::MAJOR_OVERRIDE_ENV; const LOCK: &str = include_str!("../../socket-patch-core/tests/fixtures/pipenv/2026.8.0/Pipfile.lock"); -const PIPFILE: &str = include_str!("../../socket-patch-core/tests/fixtures/pipenv/2026.8.0/Pipfile"); +const PIPFILE: &str = + include_str!("../../socket-patch-core/tests/fixtures/pipenv/2026.8.0/Pipfile"); /// The upstream and patched bytes of the record's one file, so the venv /// tests can materialize a real `Ready` (upstream) install. @@ -118,7 +119,9 @@ async fn mock_api(server: &MockServer) { .mount(server) .await; Mock::given(method("GET")) - .and(path_regex(format!("^/v0/orgs/{ORG}/patches/by-package/.+$"))) + .and(path_regex(format!( + "^/v0/orgs/{ORG}/patches/by-package/.+$" + ))) .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "patches": [{ "uuid": UUID, "purl": RECORD_PURL, @@ -308,7 +311,10 @@ async fn lock_only_pipenv_project_redirects_attests_rescans_and_rolls_back() { serde_json::json!([format!("sha256:{}", sha256())]), "{redirected}" ); - assert!(entry.get("version").is_none() && entry.get("index").is_none(), "{entry}"); + assert!( + entry.get("version").is_none() && entry.get("index").is_none(), + "{entry}" + ); assert_eq!( entry["markers"], urllib3_entry(LOCK)["markers"], @@ -316,11 +322,19 @@ async fn lock_only_pipenv_project_redirects_attests_rescans_and_rolls_back() { ); let before: serde_json::Value = serde_json::from_str(LOCK).unwrap(); let after: serde_json::Value = serde_json::from_str(&redirected).unwrap(); - assert_eq!(after["_meta"], before["_meta"], "the Pipfile content hash stays"); - assert_eq!(read(&tmp.path().join("Pipfile")), PIPFILE, "Pipfile untouched"); - let ledger: serde_json::Value = - serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))) - .unwrap(); + assert_eq!( + after["_meta"], before["_meta"], + "the Pipfile content hash stays" + ); + assert_eq!( + read(&tmp.path().join("Pipfile")), + PIPFILE, + "Pipfile untouched" + ); + let ledger: serde_json::Value = serde_json::from_str(&read( + &tmp.path().join(".socket/vendor/redirect-state.json"), + )) + .unwrap(); assert!( ledger["records"][RECORD_PURL].is_object(), "ledger keyed by the artifact-qualified purl: {ledger}" @@ -340,17 +354,34 @@ async fn lock_only_pipenv_project_redirects_attests_rescans_and_rolls_back() { let vex: serde_json::Value = serde_json::from_str(&read(&vex_path)).unwrap(); let statements = vex["statements"].as_array().expect("statements"); assert_eq!(statements.len(), 1, "{vex}"); - assert_eq!(statements[0]["vulnerability"]["name"].as_str(), Some(GHSA), "{vex}"); - assert_eq!(statements[0]["status"].as_str(), Some("not_affected"), "{vex}"); + assert_eq!( + statements[0]["vulnerability"]["name"].as_str(), + Some(GHSA), + "{vex}" + ); + assert_eq!( + statements[0]["status"].as_str(), + Some("not_affected"), + "{vex}" + ); // 2. Idempotent re-scan: no further edits, lock byte-identical. let code = run(hosted_args(tmp.path(), server.uri(), None)).await; assert_eq!(code, 0); - assert_eq!(read(&lock_path), redirected, "re-scan must not touch the lock"); - let ledger: serde_json::Value = - serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))) - .unwrap(); - assert_eq!(ledger["edits"].as_array().map(Vec::len), Some(1), "one edit, not two"); + assert_eq!( + read(&lock_path), + redirected, + "re-scan must not touch the lock" + ); + let ledger: serde_json::Value = serde_json::from_str(&read( + &tmp.path().join(".socket/vendor/redirect-state.json"), + )) + .unwrap(); + assert_eq!( + ledger["edits"].as_array().map(Vec::len), + Some(1), + "one edit, not two" + ); // Manifest-less VEX over the committed state (the depscan / CI shape). manifestless_vex(tmp.path(), "pipenv lock-only", &|p: &Path| { @@ -359,7 +390,11 @@ async fn lock_only_pipenv_project_redirects_attests_rescans_and_rolls_back() { // 3. rollback unwinds the redirect and drops the record. roll_back(tmp.path(), server.uri()).await; - assert_eq!(read(&lock_path), LOCK, "rollback must restore the pristine lock byte for byte"); + assert_eq!( + read(&lock_path), + LOCK, + "rollback must restore the pristine lock byte for byte" + ); let ledger_path = tmp.path().join(".socket/vendor/redirect-state.json"); if ledger_path.exists() { let ledger: serde_json::Value = serde_json::from_str(&read(&ledger_path)).unwrap(); @@ -392,7 +427,10 @@ async fn legacy_installer_major_selects_path_references() { "Pipenv 7–11 install `path` references: {redirected}" ); assert!(entry.get("file").is_none(), "{entry}"); - assert_eq!(entry["hashes"], serde_json::json!([format!("sha256:{}", sha256())])); + assert_eq!( + entry["hashes"], + serde_json::json!([format!("sha256:{}", sha256())]) + ); // The legacy `path` reference is discovered just like `file`. manifestless_vex(tmp.path(), "pipenv legacy path", &|p: &Path| { @@ -413,7 +451,9 @@ async fn stale_pipfile_lock_does_not_veto_the_requirements_redirect() { write_project(tmp.path()); // The Pipfile.lock left behind pins a DIFFERENT package; the project // installs from requirements.txt. - let stale = LOCK.replace("\"urllib3\"", "\"six\"").replace("==1.26.18", "==1.16.0"); + let stale = LOCK + .replace("\"urllib3\"", "\"six\"") + .replace("==1.26.18", "==1.16.0"); std::fs::write(tmp.path().join("Pipfile.lock"), &stale).unwrap(); std::fs::write(tmp.path().join("requirements.txt"), "urllib3==1.26.18\n").unwrap(); @@ -461,16 +501,27 @@ async fn warm_venv_with_the_upstream_release_is_not_attested() { // attested and the embedded-VEX contract fails the command. let code = run(hosted_args(tmp.path(), server.uri(), Some(&vex_path))).await; let redirected = read(&lock_path); - assert!(redirected.contains(HOSTED_URL), "the lock is still repointed: {redirected}"); + assert!( + redirected.contains(HOSTED_URL), + "the lock is still repointed: {redirected}" + ); let attested = vex_path .exists() .then(|| serde_json::from_str::(&read(&vex_path)).unwrap()) .and_then(|v| v["statements"].as_array().map(Vec::len)) .unwrap_or(0); - assert_eq!(attested, 0, "a stale install must not be attested from the ledger"); + assert_eq!( + attested, 0, + "a stale install must not be attested from the ledger" + ); assert_ne!(code, 0, "nothing to attest fails the embedded-VEX run"); assert_eq!( - std::fs::read(site_packages(tmp.path()).join("urllib3").join("response.py")).unwrap(), + std::fs::read( + site_packages(tmp.path()) + .join("urllib3") + .join("response.py") + ) + .unwrap(), UPSTREAM, "the probe is read-only" ); diff --git a/crates/socket-patch-cli/tests/in_process_rollback_hosted/vlt.rs b/crates/socket-patch-cli/tests/in_process_rollback_hosted/vlt.rs index 7075ba262..ba5deeccd 100644 --- a/crates/socket-patch-cli/tests/in_process_rollback_hosted/vlt.rs +++ b/crates/socket-patch-cli/tests/in_process_rollback_hosted/vlt.rs @@ -509,8 +509,10 @@ async fn vlt_heal_follows_the_vlt_group_when_another_group_refuses() { let (_, doc) = scan_hosted(root, &server, &["--no-npm-allow-remote-config"], &[]); assert_eq!(redirected(&doc), 1, "the scan redirects both locks"); vlt_install_patched(root, &server); - let drifted = read(root, "package-lock.json") - .replace(&artifact_url(&server), "https://example.invalid/left-pad-1.3.0.tgz"); + let drifted = read(root, "package-lock.json").replace( + &artifact_url(&server), + "https://example.invalid/left-pad-1.3.0.tgz", + ); std::fs::write(root.join("package-lock.json"), &drifted).unwrap(); let cwd = root.to_str().unwrap().to_string(); @@ -526,10 +528,18 @@ async fn vlt_heal_follows_the_vlt_group_when_another_group_refuses() { vlt_lock(Era::V1, &[registry_node(TILDE_ID)]), "the vlt group restored the registry pins" ); - assert_eq!(read(root, "package-lock.json"), drifted, "the refused group wrote nothing"); + assert_eq!( + read(root, "package-lock.json"), + drifted, + "the refused group wrote nothing" + ); assert!( !store_dir(root, TILDE_ID).exists(), "the patched store copy is removed for the restored pins" ); - assert_eq!(advisory_details(&doc), [RESTORED], "the heal advisory is reported"); + assert_eq!( + advisory_details(&doc), + [RESTORED], + "the heal advisory is reported" + ); } diff --git a/crates/socket-patch-cli/tests/interactive_prompts_e2e.rs b/crates/socket-patch-cli/tests/interactive_prompts_e2e.rs index 83c26d676..2889c83d2 100644 --- a/crates/socket-patch-cli/tests/interactive_prompts_e2e.rs +++ b/crates/socket-patch-cli/tests/interactive_prompts_e2e.rs @@ -114,9 +114,8 @@ fn run_in_pty_bytes(args: &[&str], cwd: &Path, input: &[u8], timeout: Duration) // closed. The previous design used a chunked read+mpsc loop // because it interleaved with a try_wait poll; the simplified // design serializes wait → drop master → read_to_end joins. - let reader_handle = crate::pty_io::PtyOutput::spawn( - pair.master.try_clone_reader().expect("clone reader"), - ); + let reader_handle = + crate::pty_io::PtyOutput::spawn(pair.master.try_clone_reader().expect("clone reader")); // Watchdog: detach a thread that kills the child after `timeout`. // The cloned ChildKiller is independent of the main `child` diff --git a/crates/socket-patch-cli/tests/rollback_duality_invariants.rs b/crates/socket-patch-cli/tests/rollback_duality_invariants.rs index d4830cbd9..31f457502 100644 --- a/crates/socket-patch-cli/tests/rollback_duality_invariants.rs +++ b/crates/socket-patch-cli/tests/rollback_duality_invariants.rs @@ -533,8 +533,7 @@ fn bare_word_target_stays_identifier_error() { )], false, ); - let manifest_before = - std::fs::read(socket.join("manifest.json")).expect("read manifest bytes"); + let manifest_before = std::fs::read(socket.join("manifest.json")).expect("read manifest bytes"); let (code, stdout, stderr) = run(tmp.path(), &["--offline", "lodash"]); assert_eq!( diff --git a/crates/socket-patch-cli/tests/scan_vendor_e2e.rs b/crates/socket-patch-cli/tests/scan_vendor_e2e.rs index cba197205..b75c2420a 100644 --- a/crates/socket-patch-cli/tests/scan_vendor_e2e.rs +++ b/crates/socket-patch-cli/tests/scan_vendor_e2e.rs @@ -3078,7 +3078,11 @@ snapshots: "{v}" ); assert!(events_for(&v, CARGO_SCOPE[0].0).is_empty(), "{v}"); - assert_eq!(record_for(dl, CARGO_SCOPE[1].0)["action"], "downloaded", "{v}"); + assert_eq!( + record_for(dl, CARGO_SCOPE[1].0)["action"], + "downloaded", + "{v}" + ); assert_eq!( events_for(&v, CARGO_SCOPE[1].0), vec![("skipped", "package_not_installed")], diff --git a/crates/socket-patch-cli/tests/self_update_channels_e2e.rs b/crates/socket-patch-cli/tests/self_update_channels_e2e.rs index a13fb56f6..04310864f 100644 --- a/crates/socket-patch-cli/tests/self_update_channels_e2e.rs +++ b/crates/socket-patch-cli/tests/self_update_channels_e2e.rs @@ -55,7 +55,10 @@ async fn npm_project_local_refuses_with_local_hint() { "a project install must get the in-project upgrade command: {stderr}" ); assert!(!stderr.contains("npm update -g"), "{stderr}"); - assert!(stderr.starts_with("Error: This socket-patch binary ("), "{stderr}"); + assert!( + stderr.starts_with("Error: This socket-patch binary ("), + "{stderr}" + ); } /// An npm-bundled binary (any `node_modules` component) refuses with the diff --git a/crates/socket-patch-cli/tests/vendor_rerun_no_network_e2e.rs b/crates/socket-patch-cli/tests/vendor_rerun_no_network_e2e.rs index 10efe03bd..ac558a164 100644 --- a/crates/socket-patch-cli/tests/vendor_rerun_no_network_e2e.rs +++ b/crates/socket-patch-cli/tests/vendor_rerun_no_network_e2e.rs @@ -1175,8 +1175,7 @@ fn a_package_absent_from_the_lock_keeps_the_not_installed_skip() { b"after\n", ); let home = cargo_home.to_string_lossy().into_owned(); - let (_code, v, stderr) = - run_vendor(&root, &dead, &[], &[("CARGO_HOME", home.as_str())]); + let (_code, v, stderr) = run_vendor(&root, &dead, &[], &[("CARGO_HOME", home.as_str())]); assert_eq!( purl_events(&v, purl), vec![("skipped", "package_not_installed")], @@ -1223,7 +1222,10 @@ fn a_stale_prestage_tree_is_swept_by_the_next_wet_run_only() { let (_code, v, stderr) = run_vendor(root, &dead, &["--dry-run"], &[]); for dir in &litter { - assert!(root.join(dir).exists(), "a dry run deletes nothing: {dir}\n{v:#}\n{stderr}"); + assert!( + root.join(dir).exists(), + "a dry run deletes nothing: {dir}\n{v:#}\n{stderr}" + ); } assert!( !v.to_string().contains("socket-prestage"), @@ -1233,7 +1235,10 @@ fn a_stale_prestage_tree_is_swept_by_the_next_wet_run_only() { for extra in [&["--offline"][..], &[][..]] { let (_code, v, stderr) = run_vendor(root, &dead, extra, &[]); for dir in &litter { - assert!(!root.join(dir).exists(), "{extra:?} sweeps {dir}\n{v:#}\n{stderr}"); + assert!( + !root.join(dir).exists(), + "{extra:?} sweeps {dir}\n{v:#}\n{stderr}" + ); } assert!( !root.join(format!(".socket/vendor/composer/{OLD}")).exists(), diff --git a/crates/socket-patch-core/src/crawlers/npm_crawler.rs b/crates/socket-patch-core/src/crawlers/npm_crawler.rs index e9094e9f1..8dad88525 100644 --- a/crates/socket-patch-core/src/crawlers/npm_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/npm_crawler.rs @@ -961,7 +961,11 @@ impl NpmCrawler { /// Inside a store entry (`store_entry`) a link is a dependency edge into /// a sibling entry, whose own visit records that copy, so only a real /// directory there matches. - fn visit_resolver_dir(nm_path: PathBuf, store_entry: bool, pending: &[Target]) -> ResolverVisit { + fn visit_resolver_dir( + nm_path: PathBuf, + store_entry: bool, + pending: &[Target], + ) -> ResolverVisit { let listing = list_dir_sync(&nm_path); let probe_filter = ProbeFilter::new(&listing); let matched = pending diff --git a/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs b/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs index d71a02127..2d6e225c1 100644 --- a/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs +++ b/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs @@ -9,9 +9,9 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use super::{ - build_npm_purl, is_legacy_pnpm_store_dir_name, - is_safe_npm_component, parse_package_name, read_package_json, NpmCrawler, StoreEntry, - Target, NESTED_STORE_MAX_DEPTH, NESTED_STORE_MAX_DIRS, SKIP_DIRS, VLT_STORE_NAME, + build_npm_purl, is_legacy_pnpm_store_dir_name, is_safe_npm_component, parse_package_name, + read_package_json, NpmCrawler, StoreEntry, Target, NESTED_STORE_MAX_DEPTH, + NESTED_STORE_MAX_DIRS, SKIP_DIRS, VLT_STORE_NAME, }; use crate::crawlers::types::{CrawledPackage, CrawlerOptions}; use crate::utils::fs::is_dir; diff --git a/crates/socket-patch-core/src/crawlers/python_crawler.rs b/crates/socket-patch-core/src/crawlers/python_crawler.rs index 001ed5c17..93fee481b 100644 --- a/crates/socket-patch-core/src/crawlers/python_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/python_crawler.rs @@ -1090,10 +1090,8 @@ pub async fn get_global_python_site_packages() -> Vec { } // 1. Ask Python for site-packages (subprocesses: on the blocking pool) - let site_output = run_blocking(|| { - SITE_QUERY_MEMO.get_or_run(site_query_key(), run_site_query) - }) - .await; + let site_output = + run_blocking(|| SITE_QUERY_MEMO.get_or_run(site_query_key(), run_site_query)).await; if let Some(stdout) = site_output { for p in parse_python_site_packages_output(&stdout) { add_path(p, &mut seen, &mut results); diff --git a/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs b/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs index ce3a28798..cbce379fe 100644 --- a/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs +++ b/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs @@ -131,11 +131,12 @@ fn assert_same_with_metadata( bun_lockb_present, python_metadata, ); - let merged = merge_group_outputs(&prefix, run_groups_concurrently(&prefix, &groups)) - .map(|mut merged| { + let merged = merge_group_outputs(&prefix, run_groups_concurrently(&prefix, &groups)).map( + |mut merged| { merged.vlt_drives = vlt::vlt_drives(files, bun_lockb_present); merged - }); + }, + ); assert_eq!( merged.as_ref(), Some(&want), diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index abc6d41ce..57a34c822 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -7584,14 +7584,16 @@ mod tests { )]; let first = rewrite_registry_redirect(&files, &overrides); let out = first.files.get("requirements.txt").expect("rewritten"); + // An unhashed file pins by the url fragment (#376), so the marker + // follows it. assert_eq!( - out.matches("--hash=sha256:").count(), + out.matches("sha256").count(), 1, "exactly one hash after the first pass: {out}" ); assert!( - out.contains("; python_version >= \"3.7\" --hash="), - "marker preserved ahead of the hash: {out}" + out.contains("-none-any.whl#sha256=") && out.contains(" ; python_version >= \"3.7\"\n"), + "marker preserved after the pinned url: {out}" ); let mut again = files.clone(); @@ -7605,10 +7607,11 @@ mod tests { ); } - /// An inline comment after the marker must not swallow the appended - /// `--hash=…` (pip would then treat the hash as comment text and skip - /// enforcement). The comment is split off and re-appended AFTER the hash - /// so the pin stays active and the user's note survives. + /// An inline comment after the marker must not swallow the appended pin + /// (pip would then treat it as comment text and skip enforcement). The + /// comment is split off and re-appended AFTER the pin — the url's + /// `#sha256=` fragment in an unhashed file (#376), `--hash` in a hashed + /// one — so the pin stays active and the user's note survives. #[test] fn requirements_marker_comment_keeps_hash_active() { let original = "requests==2.28.1 ; python_version >= \"3.7\" # explanation\n"; @@ -7621,13 +7624,23 @@ mod tests { assert_eq!( output, &format!( - "requests @ {url} ; python_version >= \"3.7\" --hash=sha256:{sha256} # explanation\n" + "requests @ {url}#sha256={sha256} ; python_version >= \"3.7\" # explanation\n" ) ); let again = BTreeMap::from([("requirements.txt".to_string(), output.clone())]); let second = rewrite_registry_redirect(&again, &overrides); assert!(second.files.is_empty()); assert!(second.edits.is_empty()); + + let hashed = original.replace("# explanation", "--hash=sha256:old # explanation"); + let files = BTreeMap::from([("requirements.txt".to_string(), hashed)]); + let first = rewrite_registry_redirect(&files, &overrides); + assert_eq!( + first.files["requirements.txt"], + format!( + "requests @ {url} ; python_version >= \"3.7\" --hash=sha256:{sha256} # explanation\n" + ) + ); } const MAVEN_SUFFIXED: &str = "1.7.36-socket.aaaaaaaa"; @@ -11787,11 +11800,19 @@ mod tests { let redacted = format!( "https://patch.socket.dev/patch/npm/left-pad/1.3.0//{uuid}/left-pad-1.3.0.tgz?x=1" ); - assert_eq!(redact_grant_token(&url, &url, uuid), redacted, "the URL alone"); + assert_eq!( + redact_grant_token(&url, &url, uuid), + redacted, + "the URL alone" + ); let text = format!("vlt would fail to verify {url}: fetch error GET {url}: reset"); - let want = format!("vlt would fail to verify {redacted}: fetch error GET {redacted}: reset"); + let want = + format!("vlt would fail to verify {redacted}: fetch error GET {redacted}: reset"); assert_eq!(redact_grant_token(&text, &url, uuid), want, "every quote"); - assert!(!redact_grant_token(&text, &url, uuid).contains(token), "no token left"); + assert!( + !redact_grant_token(&text, &url, uuid).contains(token), + "no token left" + ); let registry = format!("https://patch.socket.dev/patch-registry/npm/{token}/{uuid}"); assert_eq!( redact_grant_token(®istry, ®istry, uuid), diff --git a/crates/socket-patch-core/src/patch/redirect/pdm.rs b/crates/socket-patch-core/src/patch/redirect/pdm.rs index 51cab65da..e8b441454 100644 --- a/crates/socket-patch-core/src/patch/redirect/pdm.rs +++ b/crates/socket-patch-core/src/patch/redirect/pdm.rs @@ -278,9 +278,18 @@ mod tests { #[test] fn legacy_formats_warn_stale_install_risk_once() { for (fixture, warns) in [ - (include_str!("../../../tests/fixtures/pdm-native/0.12.3.lock"), true), - (include_str!("../../../tests/fixtures/pdm-native/2.8.2.lock"), true), - (include_str!("../../../tests/fixtures/pdm-native/2.29.2.lock"), false), + ( + include_str!("../../../tests/fixtures/pdm-native/0.12.3.lock"), + true, + ), + ( + include_str!("../../../tests/fixtures/pdm-native/2.8.2.lock"), + true, + ), + ( + include_str!("../../../tests/fixtures/pdm-native/2.29.2.lock"), + false, + ), ] { let mut result = RewriteResult::default(); rewrite( diff --git a/crates/socket-patch-core/src/patch/redirect/pipenv.rs b/crates/socket-patch-core/src/patch/redirect/pipenv.rs index ef62dce7e..d593dafe1 100644 --- a/crates/socket-patch-core/src/patch/redirect/pipenv.rs +++ b/crates/socket-patch-core/src/patch/redirect/pipenv.rs @@ -555,7 +555,10 @@ mod tests { let original = serde_json::to_string(&value).unwrap(); // A live lock (Pipfile beside it): conflicts veto the siblings. let files = BTreeMap::from([ - ("Pipfile".to_string(), "[packages]\nurllib3 = \"*\"\n".to_string()), + ( + "Pipfile".to_string(), + "[packages]\nurllib3 = \"*\"\n".to_string(), + ), ("Pipfile.lock".to_string(), original), ]); let mut result = RewriteResult::default(); @@ -595,20 +598,30 @@ mod tests { for stale in &stale_locks { let files = BTreeMap::from([ ("Pipfile.lock".to_string(), stale.clone()), - ("requirements.txt".to_string(), "urllib3==1.26.18\n".to_string()), + ( + "requirements.txt".to_string(), + "urllib3==1.26.18\n".to_string(), + ), ]); - let result = super::super::rewrite_registry_redirect(&files, std::slice::from_ref(&dep)); + let result = + super::super::rewrite_registry_redirect(&files, std::slice::from_ref(&dep)); assert!( !result.refused_pipenv_uuids.contains("patch-one"), "a non-conflict must not veto: {stale}" ); assert!( - result.warnings.iter().any(|w| w.code == "redirect_pipenv_skipped"), + result + .warnings + .iter() + .any(|w| w.code == "redirect_pipenv_skipped"), "{:?}", result.warnings ); assert!( - result.files.get("requirements.txt").is_some_and(|t| t.contains("patch.socket.dev")), + result + .files + .get("requirements.txt") + .is_some_and(|t| t.contains("patch.socket.dev")), "requirements.txt must still be redirected past a stale Pipfile.lock: {result:?}" ); assert!(!result.files.contains_key("Pipfile.lock")); @@ -693,10 +706,18 @@ mod tests { ); let foreign = redirected.replacen( redirected_entry, - &format_entry(&json!({"file": "https://example.org/fork.whl"}), &redirected, 0).unwrap(), + &format_entry( + &json!({"file": "https://example.org/fork.whl"}), + &redirected, + 0, + ) + .unwrap(), 1, ); - assert!(restore(&foreign, &edits[0]).is_err(), "a foreign reference is drift"); + assert!( + restore(&foreign, &edits[0]).is_err(), + "a foreign reference is drift" + ); // Re-scan after the relock, then roll back newest-first. let (again, second) = plan(&relocked, &dep, None).unwrap(); @@ -714,7 +735,10 @@ mod tests { let files = |text: &str| BTreeMap::from([("Pipfile.lock".to_string(), text.to_string())]); assert!(lock_targets(&files(&lock()), std::slice::from_ref(&dep))); assert!(!lock_targets(&files(&lock()), std::slice::from_ref(&other))); - assert!(!lock_targets(&files("{ not json"), std::slice::from_ref(&dep))); + assert!(!lock_targets( + &files("{ not json"), + std::slice::from_ref(&dep) + )); assert!(!lock_targets(&BTreeMap::new(), std::slice::from_ref(&dep))); let mut npm = dep.clone(); npm.ecosystem = "npm".into(); @@ -739,7 +763,10 @@ mod tests { let entry: Value = serde_json::from_str(&fixed).unwrap(); assert!(entry["default"]["urllib3"].get("version").is_none()); assert!(entry["default"]["urllib3"].get("index").is_none()); - assert!(entry["default"]["urllib3"]["file"].as_str().unwrap().contains("patch-one")); + assert!(entry["default"]["urllib3"]["file"] + .as_str() + .unwrap() + .contains("patch-one")); value["default"]["urllib3"]["version"] = json!("==2.0.0"); let conflicting = serde_json::to_string(&value).unwrap(); @@ -760,7 +787,10 @@ mod tests { assert!(owned_url(public, &dep)); assert!(!owned_url("https://example.org/patch/pypi/urllib3/1.26.18/tok/patch-one/urllib3-1.26.18-py3-none-any.whl", &dep)); dep.artifact_url = "https://patches.internal.example:8443/patch/pypi/urllib3/1.26.18/tok/patch-one/urllib3-1.26.18-py3-none-any.whl".into(); - assert!(owned_url(&dep.artifact_url, &dep), "the grant's own origin is ours"); + assert!( + owned_url(&dep.artifact_url, &dep), + "the grant's own origin is ours" + ); assert!(owned_url(public, &dep), "and so is the public service"); assert!(!owned_url("https://patches.internal.example:8443/patch/pypi/urllib3/1.26.19/tok/patch-one/urllib3-1.26.19-py3-none-any.whl", &dep), "another version is not"); // Rotation on the custom origin restores through the chain. @@ -779,7 +809,12 @@ mod tests { fn restore_refuses_a_non_object_ledger_original() { let dep = dependency("urllib3", "1.26.18", "patch-one"); let (text, edits) = plan(&lock(), &dep, None).unwrap(); - for bad in ["\"just a string\"", "[1, 2]", "not json at all", "{\"a\": 1}, \"injected\": {}"] { + for bad in [ + "\"just a string\"", + "[1, 2]", + "not json at all", + "{\"a\": 1}, \"injected\": {}", + ] { let mut edit = edits[0].clone(); edit.original = Some(Value::String(bad.to_string())); assert!(restore(&text, &edit).is_err(), "{bad}"); @@ -813,18 +848,28 @@ mod tests { for edit in &first_edits { let replacement = edit.new.as_ref().unwrap().as_str().unwrap(); // A tampered reference (its `#sha256=` pin) is drift… - let drift = two.replacen(replacement, &replacement.replace("#sha256=", "#sha256=0"), 1); + let drift = two.replacen( + replacement, + &replacement.replace("#sha256=", "#sha256=0"), + 1, + ); assert!(restore(&drift, edit).is_err()); // …while a re-serialized entry that kept our reference (Pipenv // 2023+ relocking a marker-excluded entry restores the registry // `hashes` and `version` next to it) is still ours and restores. let mut value: Value = serde_json::from_str(&two).unwrap(); - let section: &str = serde_json::from_str::<[String; 2]>(edit.key.as_deref().unwrap()).unwrap()[0].clone().leak(); + let section: &str = serde_json::from_str::<[String; 2]>(edit.key.as_deref().unwrap()) + .unwrap()[0] + .clone() + .leak(); value[section]["urllib3"]["hashes"] = json!(["sha256:upstream-a", "sha256:upstream-b"]); value[section]["urllib3"]["version"] = json!("==1.26.18"); let kept = serde_json::to_string_pretty(&value).unwrap(); let restored: Value = serde_json::from_str(&restore(&kept, edit).unwrap()).unwrap(); - assert!(restored[section]["urllib3"].get("file").is_none(), "{restored}"); + assert!( + restored[section]["urllib3"].get("file").is_none(), + "{restored}" + ); let mut unsafe_edit = edit.clone(); unsafe_edit.path = "../Pipfile.lock".into(); assert!(restore(&two, &unsafe_edit).is_err()); @@ -883,7 +928,10 @@ mod compatibility_tests { assert!(!result.refused_pipenv_uuids.contains("patch-one")); assert!(result.files["requirements.txt"].contains("patch.socket.dev")); assert!(!result.files.contains_key("Pipfile.lock")); - assert!(result.warnings.iter().any(|w| w.code == "redirect_pipenv_refused" && w.detail.contains("no Pipfile"))); + assert!(result + .warnings + .iter() + .any(|w| w.code == "redirect_pipenv_refused" && w.detail.contains("no Pipfile"))); } /// Rollback survives what git and Pipenv do to the lock between the @@ -915,11 +963,17 @@ mod compatibility_tests { value["default"]["urllib3"]["version"] = json!("==1.26.18"); value["default"]["urllib3"]["index"] = json!("pypi"); let hybrid = serde_json::to_string_pretty(&value).unwrap(); - let default_edit = edits.iter().find(|e| e.key.as_deref() == Some(r#"["default","urllib3"]"#)).unwrap(); + let default_edit = edits + .iter() + .find(|e| e.key.as_deref() == Some(r#"["default","urllib3"]"#)) + .unwrap(); let restored = restore(&hybrid, default_edit).unwrap(); let value: Value = serde_json::from_str(&restored).unwrap(); assert_eq!(value["default"]["urllib3"]["version"], json!("==1.26.18")); - assert!(value["default"]["urllib3"].get("file").is_none(), "{restored}"); + assert!( + value["default"]["urllib3"].get("file").is_none(), + "{restored}" + ); // Dropped entry (`pipenv uninstall`): nothing to unwind, retires. let mut value: Value = serde_json::from_str(&redirected).unwrap(); value["default"].as_object_mut().unwrap().remove("urllib3"); diff --git a/crates/socket-patch-core/src/patch/redirect/poetry.rs b/crates/socket-patch-core/src/patch/redirect/poetry.rs index a2798cd69..eacc889a9 100644 --- a/crates/socket-patch-core/src/patch/redirect/poetry.rs +++ b/crates/socket-patch-core/src/patch/redirect/poetry.rs @@ -92,7 +92,9 @@ pub(super) fn rewrite_poetry( } } Err(detail) => { - result.refused_python_lock_uuids.insert(dep.patch_uuid.clone()); + result + .refused_python_lock_uuids + .insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_poetry_lock_unsupported".into(), detail: format!("{path}: {detail}"), @@ -100,7 +102,9 @@ pub(super) fn rewrite_poetry( continue; } } - result.confirmed_python_lock_uuids.insert(dep.patch_uuid.clone()); + result + .confirmed_python_lock_uuids + .insert(dep.patch_uuid.clone()); content = rewrite.text; if !stale_warned { if let Some(format) = @@ -136,14 +140,18 @@ pub(super) fn rewrite_poetry( } // Already redirected to this artifact (idempotent re-scan). Ok(Some(_)) => { - result.confirmed_python_lock_uuids.insert(dep.patch_uuid.clone()); + result + .confirmed_python_lock_uuids + .insert(dep.patch_uuid.clone()); } Ok(None) => result.warnings.push(RewriteWarning { code: "redirect_poetry_entry_not_found".into(), detail: format!("no {path} entry for {}@{}", dep.name, dep.version), }), Err(detail) => { - result.refused_python_lock_uuids.insert(dep.patch_uuid.clone()); + result + .refused_python_lock_uuids + .insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_poetry_lock_unsupported".into(), detail: format!("{path}: {detail}"), @@ -215,7 +223,9 @@ fn rewrite_poetry_reference( } } Err(detail) => { - result.refused_python_lock_uuids.insert(dep.patch_uuid.clone()); + result + .refused_python_lock_uuids + .insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_poetry_lock_unsupported".into(), detail: format!("{path}: {detail}"), @@ -223,7 +233,9 @@ fn rewrite_poetry_reference( continue; } } - result.confirmed_python_lock_uuids.insert(dep.patch_uuid.clone()); + result + .confirmed_python_lock_uuids + .insert(dep.patch_uuid.clone()); content = rewritten; if !stale_warned { if let Some(format) = pre_1_4_writer(&content) { @@ -257,14 +269,18 @@ fn rewrite_poetry_reference( } // Already redirected to this artifact (idempotent re-scan). Ok(Some(_)) => { - result.confirmed_python_lock_uuids.insert(dep.patch_uuid.clone()); + result + .confirmed_python_lock_uuids + .insert(dep.patch_uuid.clone()); } Ok(None) => result.warnings.push(RewriteWarning { code: "redirect_poetry_entry_not_found".into(), detail: format!("no {path} entry for {}@{}", dep.name, dep.version), }), Err(detail) => { - result.refused_python_lock_uuids.insert(dep.patch_uuid.clone()); + result + .refused_python_lock_uuids + .insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_poetry_lock_unsupported".into(), detail: format!("{path}: {detail}"), diff --git a/crates/socket-patch-core/src/patch/redirect/requirements.rs b/crates/socket-patch-core/src/patch/redirect/requirements.rs index 5e0b921c0..a033894f7 100644 --- a/crates/socket-patch-core/src/patch/redirect/requirements.rs +++ b/crates/socket-patch-core/src/patch/redirect/requirements.rs @@ -207,6 +207,10 @@ pub(super) fn rewrite( .or_default() .insert(&dep.version); } + // pip's hash-checking mode is all or nothing (#376): pin the patched + // artifact with `--hash` only when the file already carries hashes + // (the replaced pin's own included), else by the url fragment. + let hashed = crate::utils::requirements::requires_hashes(content); let mut changed = false; for dep in overrides.iter().filter(|dep| dep.ecosystem == "pypi") { let Some(sha256) = &dep.integrity.sha256 else { @@ -265,7 +269,9 @@ pub(super) fn rewrite( } } matched = true; - result.confirmed_requirements_uuids.insert(dep.patch_uuid.clone()); + result + .confirmed_requirements_uuids + .insert(dep.patch_uuid.clone()); let options = requirement_tokens(specifier) .into_iter() .skip_while(|token| !token.starts_with("--")) @@ -285,14 +291,28 @@ pub(super) fn rewrite( } else { "" }; - let mut rewritten = format!("{bom}{indent}{}{extras} @ {}", dep.name, dep.artifact_url); + // Unhashed file: the url's `#sha256=` fragment, which pip + // verifies without turning hash-checking mode on. + let location = if hashed { + dep.artifact_url.clone() + } else { + let separator = if dep.artifact_url.contains('#') { + '&' + } else { + '#' + }; + format!("{}{separator}sha256={sha256}", dep.artifact_url) + }; + let mut rewritten = format!("{bom}{indent}{}{extras} @ {location}", dep.name); for suffix in [marker.trim(), options.as_str()] { if !suffix.is_empty() { rewritten.push(' '); rewritten.push_str(suffix); } } - rewritten.push_str(&format!(" --hash=sha256:{sha256}")); + if hashed { + rewritten.push_str(&format!(" --hash=sha256:{sha256}")); + } if !comment.is_empty() { rewritten.push(' '); rewritten.push_str(comment); @@ -427,7 +447,7 @@ mod tests { let result = rewrite_registry_redirect(&input(&source), &[patch()]); assert_eq!( result.files["requirements.txt"], - format!("{prefix}# documentation \\\nrequests @ {URL} --hash=sha256:{HASH}\n") + format!("{prefix}# documentation \\\nrequests @ {URL}#sha256={HASH}\n") ); } } @@ -486,7 +506,7 @@ mod tests { other.artifact_url = URL.replace("2.28.1", "2.32.0"); other.integrity.sha256 = Some("d".repeat(64)); let expected = format!( - "requests @ {URL} ; python_version < '3.10' --hash=sha256:{HASH}\nrequests @ {} ; python_version >= '3.10' --hash=sha256:{}\n", + "requests @ {URL}#sha256={HASH} ; python_version < '3.10'\nrequests @ {}#sha256={} ; python_version >= '3.10'\n", other.artifact_url, "d".repeat(64) ); @@ -525,9 +545,53 @@ mod tests { let result = rewrite_registry_redirect(&input(source), &[patch()]); assert_eq!( result.files["requirements.txt"], - format!("requests @ {URL} --hash=sha256:{HASH}") + format!("requests @ {URL}#sha256={HASH}") + ); + } + } + + /// #376: an unhashed requirements file must stay unhashed. pip turns + /// hash-checking mode on for the WHOLE install as soon as one line has a + /// `--hash`, so pinning only the patched line breaks every other + /// requirement (and every transitive dependency). The patched sha256 + /// rides in the url's `#sha256=` fragment instead, which pip verifies + /// without turning the mode on. + #[test] + fn unhashed_file_pins_the_artifact_by_url_fragment_not_hash_option() { + let source = "requests==2.28.1\nidna==3.7\n"; + let result = rewrite_registry_redirect(&input(source), &[patch()]); + let expected = format!("requests @ {URL}#sha256={HASH}\nidna==3.7\n"); + assert_eq!(result.files["requirements.txt"], expected); + assert!(!result.files["requirements.txt"].contains("--hash")); + assert!(result.warnings.is_empty(), "{:?}", result.warnings); + // Idempotent: the rewritten line keeps the file unhashed. + let rerun = rewrite_registry_redirect(&input(&expected), &[patch()]); + assert!(rerun.files.is_empty() && rerun.edits.is_empty()); + } + + /// #376: a file the user already hashes keeps `--hash` on the patched + /// line (hash-checking mode is on either way), whether the hashes sit on + /// another requirement or the file sets `--require-hashes`. + #[test] + fn hashed_file_keeps_the_hash_option() { + for other in [ + "idna==3.7 --hash=sha256:aaaa\n", + "idna==3.7 \\\n --hash sha512:bbbb\n", + "--require-hashes\nidna==3.7\n", + ] { + let source = format!("requests==2.28.1\n{other}"); + let result = rewrite_registry_redirect(&input(&source), &[patch()]); + assert_eq!( + result.files["requirements.txt"], + format!("requests @ {URL} --hash=sha256:{HASH}\n{other}"), + "{other:?}" ); } + // A hash in a comment, or a url fragment, is not a hash option. + let source = "requests==2.28.1\n# idna==3.7 --hash=sha256:aaaa\nsix @ https://files.pythonhosted.org/six-1.16.0-py2.py3-none-any.whl#sha256=dd\n"; + let result = rewrite_registry_redirect(&input(source), &[patch()]); + assert!(result.files["requirements.txt"] + .starts_with(&format!("requests @ {URL}#sha256={HASH}\n"))); } #[test] @@ -549,7 +613,7 @@ mod tests { let result = rewrite_registry_redirect(&input("requests\n"), &[patch()]); assert_eq!( result.files["requirements.txt"], - format!("requests @ {URL} --hash=sha256:{HASH}\n") + format!("requests @ {URL}#sha256={HASH}\n") ); let mut other = patch(); other.version = "2.32.0".into(); diff --git a/crates/socket-patch-core/src/update/download.rs b/crates/socket-patch-core/src/update/download.rs index f176426ce..be1476b19 100644 --- a/crates/socket-patch-core/src/update/download.rs +++ b/crates/socket-patch-core/src/update/download.rs @@ -741,7 +741,10 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let missing = tmp.path().join("never-existed"); sweep_stale_stages(&missing); - assert!(!missing.exists(), "sweep must not create the destination dir"); + assert!( + !missing.exists(), + "sweep must not create the destination dir" + ); } /// A write failure AFTER a successful open (EFBIG here, standing in @@ -757,8 +760,7 @@ mod tests { #[test] fn stage_write_failure_cleans_up_stage_file() { const CHILD_ENV: &str = "SOCKET_PATCH_CORE_TEST_STAGE_FSIZE_CHILD"; - const TEST_NAME: &str = - "update::download::tests::stage_write_failure_cleans_up_stage_file"; + const TEST_NAME: &str = "update::download::tests::stage_write_failure_cleans_up_stage_file"; if std::env::var_os(CHILD_ENV).is_none() { let exe = std::env::current_exe().expect("test binary path must resolve"); let output = std::process::Command::new(exe) @@ -824,7 +826,10 @@ mod tests { matches!(err, UpdateError::SwapFailed(_)), "expected SwapFailed, got: {err}" ); - assert!(err.to_string().contains("error writing staged binary"), "{err}"); + assert!( + err.to_string().contains("error writing staged binary"), + "{err}" + ); let leftovers: Vec = std::fs::read_dir(tmp.path()) .unwrap() .map(|e| e.unwrap().file_name().to_string_lossy().into_owned()) diff --git a/crates/socket-patch-core/src/update/release.rs b/crates/socket-patch-core/src/update/release.rs index 5b2869012..7c3b04c29 100644 --- a/crates/socket-patch-core/src/update/release.rs +++ b/crates/socket-patch-core/src/update/release.rs @@ -751,9 +751,11 @@ mod tests { .mount(&server) .await; - let client = - metadata_client(&short_timeouts(), follow_redirect_policy(&default_endpoints())) - .unwrap(); + let client = metadata_client( + &short_timeouts(), + follow_redirect_policy(&default_endpoints()), + ) + .unwrap(); let err = client .get(format!("{}/start", server.uri())) .send() @@ -786,9 +788,11 @@ mod tests { .mount(&server) .await; - let client = - metadata_client(&short_timeouts(), follow_redirect_policy(&default_endpoints())) - .unwrap(); + let client = metadata_client( + &short_timeouts(), + follow_redirect_policy(&default_endpoints()), + ) + .unwrap(); let err = client .get(format!("{}/start", server.uri())) .send() @@ -864,7 +868,10 @@ mod tests { .unwrap_err(); assert!(matches!(err, UpdateError::CheckFailed(_)), "{err:?}"); let msg = err.to_string(); - assert!(msg.contains("expected a redirect to the latest tag"), "{msg}"); + assert!( + msg.contains("expected a redirect to the latest tag"), + "{msg}" + ); assert!(msg.contains("API fallback:"), "{msg}"); assert!(msg.contains("returned 500"), "{msg}"); } @@ -945,8 +952,14 @@ mod tests { #[test] fn url_host_keeps_explicit_ports() { - assert_eq!(url_host("http://127.0.0.1:9/x").as_deref(), Some("127.0.0.1:9")); - assert_eq!(url_host("https://github.com/a").as_deref(), Some("github.com")); + assert_eq!( + url_host("http://127.0.0.1:9/x").as_deref(), + Some("127.0.0.1:9") + ); + assert_eq!( + url_host("https://github.com/a").as_deref(), + Some("github.com") + ); assert_eq!(url_host("not a url"), None); } @@ -959,7 +972,9 @@ mod tests { // code stays `check_failed` (stable contract). let server = MockServer::start().await; Mock::given(method("GET")) - .and(path("/SocketDev/socket-patch/releases/download/v1.2.3/SHA256SUMS")) + .and(path( + "/SocketDev/socket-patch/releases/download/v1.2.3/SHA256SUMS", + )) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; @@ -992,7 +1007,9 @@ mod tests { // silently. let server = MockServer::start().await; Mock::given(method("GET")) - .and(path("/SocketDev/socket-patch/releases/download/v1.2.3/SHA256SUMS")) + .and(path( + "/SocketDev/socket-patch/releases/download/v1.2.3/SHA256SUMS", + )) .respond_with(ResponseTemplate::new(500)) .mount(&server) .await; diff --git a/crates/socket-patch-core/src/utils/group_commit.rs b/crates/socket-patch-core/src/utils/group_commit.rs index 72cc1fad5..255b0ef5a 100644 --- a/crates/socket-patch-core/src/utils/group_commit.rs +++ b/crates/socket-patch-core/src/utils/group_commit.rs @@ -297,9 +297,9 @@ where // write the lock edits beside the pre-run ledger. Put the caller's value // back before the unwind continues — the same value a caught-and- // continued caller holds. - if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - edit(Arc::make_mut(value)) - })) { + if let Err(panic) = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| edit(Arc::make_mut(value)))) + { files.insert(key, captured(value)); drop(files); std::panic::resume_unwind(panic); @@ -1596,7 +1596,10 @@ mod tests { .unwrap(); remove_dir_after_commit(&dir).await; drop(dropped); - assert!(dir.join("config.toml").exists(), "an abandoned commit removes nothing"); + assert!( + dir.join("config.toml").exists(), + "an abandoned commit removes nothing" + ); let group = GroupCommit::begin(root); super::super::fs::remove_file(&dir.join("config.toml")) @@ -1605,7 +1608,10 @@ mod tests { remove_dir_after_commit(&dir).await; assert!(dir.join("config.toml").exists(), "captured, still on disk"); group.commit().await.unwrap(); - assert!(!dir.exists(), "the emptied directory is removed after the commit"); + assert!( + !dir.exists(), + "the emptied directory is removed after the commit" + ); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("config.toml"), b"[patch]\n").unwrap(); @@ -1617,7 +1623,10 @@ mod tests { remove_dir_after_commit(&dir).await; group.commit().await.unwrap(); assert!(!dir.join("config.toml").exists()); - assert!(dir.join("credentials.toml").exists(), "a non-empty directory is kept"); + assert!( + dir.join("credentials.toml").exists(), + "a non-empty directory is kept" + ); remove_dir_after_commit(&root.join("gone")).await; std::fs::remove_file(dir.join("credentials.toml")).unwrap(); diff --git a/crates/socket-patch-core/src/utils/mod.rs b/crates/socket-patch-core/src/utils/mod.rs index e13c251d7..a79fdc6da 100644 --- a/crates/socket-patch-core/src/utils/mod.rs +++ b/crates/socket-patch-core/src/utils/mod.rs @@ -7,9 +7,9 @@ pub mod env_compat; pub mod failpoint; pub mod fs; pub mod group_commit; -pub mod notice; pub(crate) mod http; pub(crate) mod line_endings; +pub mod notice; pub mod pdm_lock; pub mod pipenv; pub mod poetry_lock; diff --git a/crates/socket-patch-core/src/utils/pdm_lock.rs b/crates/socket-patch-core/src/utils/pdm_lock.rs index 20eee27ce..65b54065b 100644 --- a/crates/socket-patch-core/src/utils/pdm_lock.rs +++ b/crates/socket-patch-core/src/utils/pdm_lock.rs @@ -358,9 +358,11 @@ fn plan_pdm_rewrite( .filter_map(|&index| packages.get(index)?.get("version").and_then(Item::as_str)) .collect(); if locked_versions.len() > 1 { - return Err("PDM lock resolves this package at multiple versions (a marker or \ + return Err( + "PDM lock resolves this package at multiple versions (a marker or \ multi-target fork); patching one fork would leave the others unpatched" - .into()); + .into(), + ); } let mut variants = std::collections::BTreeSet::new(); let mut edits = Vec::new(); @@ -775,7 +777,10 @@ mod tests { &"a".repeat(64), ) .unwrap(); - assert!(rewired.contains(&fresh) && !rewired.contains(&stale), "{rewired}"); + assert!( + rewired.contains(&fresh) && !rewired.contains(&stale), + "{rewired}" + ); // A foreign (non-Socket) existing url is still refused. let foreign = fixture("2.29.2").replace( "name = \"urllib3\"", diff --git a/crates/socket-patch-core/src/utils/poetry_lock.rs b/crates/socket-patch-core/src/utils/poetry_lock.rs index ceb7726c4..a191e5214 100644 --- a/crates/socket-patch-core/src/utils/poetry_lock.rs +++ b/crates/socket-patch-core/src/utils/poetry_lock.rs @@ -71,7 +71,10 @@ fn lock_version_of(lock: &Table) -> Result<&str, String> { { Ok("0") } - None => Err("poetry.lock has neither a [metadata] lock-version nor a [metadata.hashes] table".into()), + None => Err( + "poetry.lock has neither a [metadata] lock-version nor a [metadata.hashes] table" + .into(), + ), } } @@ -630,7 +633,15 @@ mod tests { Ok(other) => panic!("{label}: expected a refusal, got {other:?}"), } // The vendored (file-source) spelling takes the same guarded path. - match rewrite_poetry_lock(&text, "urllib3", "1.26.18", "file", ".socket/vendor/pypi/x/urllib3-1.26.18-py2.py3-none-any.whl", WHEEL, &sha()) { + match rewrite_poetry_lock( + &text, + "urllib3", + "1.26.18", + "file", + ".socket/vendor/pypi/x/urllib3-1.26.18-py2.py3-none-any.whl", + WHEEL, + &sha(), + ) { Err(err) => assert!(!err.is_empty(), "{label}"), Ok(other) => panic!("{label}: expected a refusal, got {other:?}"), } @@ -648,7 +659,10 @@ mod tests { assert!(rewritten.contains(URL)); assert!(rewritten.contains("lock-version = \"2.2\"")); for bad in ["3.0", "2", "2.x", "1.2"] { - let lock = fixture("2.4.3").replace("lock-version = \"2.1\"", &format!("lock-version = \"{bad}\"")); + let lock = fixture("2.4.3").replace( + "lock-version = \"2.1\"", + &format!("lock-version = \"{bad}\""), + ); let err = hosted(&lock).unwrap_err(); assert!(err.contains(bad), "{bad}: {err}"); } @@ -671,28 +685,47 @@ mod tests { // Poetry 1.0 carries a `#sha256=…&` fragment; the comparison ignores it. let lock10 = fixture("1.0.10"); let first10 = hosted(&lock10).unwrap().unwrap(); - let second10 = rewrite_poetry_lock(&first10, "urllib3", "1.26.18", "url", &rotated, WHEEL, &"b".repeat(64)) - .unwrap() - .unwrap(); + let second10 = rewrite_poetry_lock( + &first10, + "urllib3", + "1.26.18", + "url", + &rotated, + WHEEL, + &"b".repeat(64), + ) + .unwrap() + .unwrap(); assert!(second10.contains(&format!("{rotated}#sha256={}&", "b".repeat(64)))); // A user's own url source on another origin stays untouched. let foreign = first.replace("https://patch.socket.dev", "https://mirror.example"); - assert!(hosted(&foreign).unwrap_err().contains("existing Poetry source")); + assert!(hosted(&foreign) + .unwrap_err() + .contains("existing Poetry source")); // A vendored file source is never taken over by the hosted path here. - let vendored = rewrite_poetry_lock(&lock, "urllib3", "1.26.18", "file", ".socket/vendor/pypi/x/urllib3-1.26.18-py2.py3-none-any.whl", WHEEL, &sha()) - .unwrap() - .unwrap(); - assert!(hosted(&vendored).unwrap_err().contains("existing Poetry source")); + let vendored = rewrite_poetry_lock( + &lock, + "urllib3", + "1.26.18", + "file", + ".socket/vendor/pypi/x/urllib3-1.26.18-py2.py3-none-any.whl", + WHEEL, + &sha(), + ) + .unwrap() + .unwrap(); + assert!(hosted(&vendored) + .unwrap_err() + .contains("existing Poetry source")); } #[test] fn sha256_is_written_lowercase() { let lock = fixture("2.4.3"); let upper = "A".repeat(64); - let rewritten = - rewrite_poetry_lock(&lock, "urllib3", "1.26.18", "url", URL, WHEEL, &upper) - .unwrap() - .unwrap(); + let rewritten = rewrite_poetry_lock(&lock, "urllib3", "1.26.18", "url", URL, WHEEL, &upper) + .unwrap() + .unwrap(); assert!(rewritten.contains(&format!("sha256:{}", "a".repeat(64)))); assert!(!rewritten.contains(&upper)); } @@ -712,7 +745,10 @@ mod tests { let lock = format!("{lock}{sibling}"); let rewritten = hosted(&lock).unwrap().unwrap(); assert!(rewritten.contains(URL)); - assert!(rewritten.contains(&sibling), "sibling entry must survive verbatim"); + assert!( + rewritten.contains(&sibling), + "sibling entry must survive verbatim" + ); let edits = poetry_lock_edits(&lock, &rewritten, "urllib3").unwrap(); assert_eq!(edits.len(), 2); assert!(edits[1].0.starts_with('\n')); @@ -733,7 +769,10 @@ mod tests { "{version}: {original:?}" ); assert!(new.ends_with("[metadata]") || new.ends_with("[extras]")); - assert!(!new.contains(original.as_str()), "{version}: pristine must not be a prefix of new"); + assert!( + !new.contains(original.as_str()), + "{version}: pristine must not be a prefix of new" + ); // A relock that keeps `[package.source]` but drops the inserted // `files` line must NOT contain the pristine fragment either. let drifted: String = rewritten @@ -747,12 +786,20 @@ mod tests { // header, the second starts with it; both splice independently. let lock = fixture("2.4.3"); let mut doc: DocumentMut = lock.parse().unwrap(); - let mut second = doc["package"].as_array_of_tables().unwrap().get(0).unwrap().clone(); + let mut second = doc["package"] + .as_array_of_tables() + .unwrap() + .get(0) + .unwrap() + .clone(); second["name"] = value("six"); second["version"] = value("1.16.0"); second.set_position(None); second.remove("extras"); - doc["package"].as_array_of_tables_mut().unwrap().push(second); + doc["package"] + .as_array_of_tables_mut() + .unwrap() + .push(second); let two = doc.to_string(); let first = hosted(&two).unwrap().unwrap(); let edits = poetry_lock_edits(&two, &first, "urllib3").unwrap(); @@ -764,11 +811,29 @@ mod tests { fn absent_or_other_version_yields_none_not_error() { let lock = fixture("2.4.3"); assert_eq!( - rewrite_poetry_lock(&lock, "six", "1.16.0", "url", &URL.replace("urllib3", "six").replace("1.26.18", "1.16.0"), "six-1.16.0-py2.py3-none-any.whl", &sha()).unwrap(), + rewrite_poetry_lock( + &lock, + "six", + "1.16.0", + "url", + &URL.replace("urllib3", "six").replace("1.26.18", "1.16.0"), + "six-1.16.0-py2.py3-none-any.whl", + &sha() + ) + .unwrap(), None ); assert_eq!( - rewrite_poetry_lock(&lock, "urllib3", "1.26.17", "url", &URL.replace("1.26.18", "1.26.17"), "urllib3-1.26.17-py2.py3-none-any.whl", &sha()).unwrap(), + rewrite_poetry_lock( + &lock, + "urllib3", + "1.26.17", + "url", + &URL.replace("1.26.18", "1.26.17"), + "urllib3-1.26.17-py2.py3-none-any.whl", + &sha() + ) + .unwrap(), None ); } diff --git a/crates/socket-patch-core/src/utils/python_script.rs b/crates/socket-patch-core/src/utils/python_script.rs index 2ca51e107..5eb997005 100644 --- a/crates/socket-patch-core/src/utils/python_script.rs +++ b/crates/socket-patch-core/src/utils/python_script.rs @@ -627,7 +627,12 @@ mod rendering_tests { "{direct}" ); assert!(uv_line.ends_with('}'), "{direct}"); - assert!(direct.starts_with("[project]\nname = \"p\"\ndependencies = [\"alpha==1.0.0\"]\n\n[tool]\n"), "{direct}"); + assert!( + direct.starts_with( + "[project]\nname = \"p\"\ndependencies = [\"alpha==1.0.0\"]\n\n[tool]\n" + ), + "{direct}" + ); assert_settled(&direct); let transitive = rewrite_project_metadata( diff --git a/crates/socket-patch-core/src/utils/requirements.rs b/crates/socket-patch-core/src/utils/requirements.rs index 421cb6ef9..08c1f50fe 100644 --- a/crates/socket-patch-core/src/utils/requirements.rs +++ b/crates/socket-patch-core/src/utils/requirements.rs @@ -157,6 +157,25 @@ pub(crate) fn hash_options(code: &str) -> Vec { hashes } +/// Whether a requirements file puts pip into hash-checking mode for the whole +/// install: pip turns it on as soon as ANY requirement carries a `--hash` +/// option (of any algorithm), or the file sets `--require-hashes`. The mode +/// is all or nothing: once on, every requirement — and every transitive +/// dependency — must be `==`-pinned and hashed, so a writer must match it +/// rather than add the first `--hash` (#376) or an unhashed line (#378). +/// +/// Every line counts, socket-patch's own included: a line this writer +/// emitted keeps the mode it was written for, so a re-scan is a no-op. A +/// url's `#sha256=` fragment is not a hash option: pip verifies it without +/// turning the mode on. +pub(crate) fn requires_hashes(content: &str) -> bool { + logical_lines(content).iter().any(|line| { + strip_comment(&line.text).split_whitespace().any(|token| { + token == "--hash" || token.starts_with("--hash=") || token == "--require-hashes" + }) + }) +} + /// `(distribution, version)` a Python artifact filename names: a PEP 427 /// wheel (`dist-version-…-tags.whl`) or an sdist (`dist-version.tar.gz` / /// `.zip` / `.tar.bz2` / `.tar.xz`). Names are returned as spelled (callers diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/mod.rs b/crates/socket-patch-core/src/vendor/lock_inventory/mod.rs index 15330b99d..d7b723d91 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/mod.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/mod.rs @@ -65,8 +65,8 @@ pub(crate) mod npm_family; pub(crate) mod pnpm; pub(crate) mod pypi; pub(crate) mod recover; -pub(crate) mod vlt; pub mod view; +pub(crate) mod vlt; pub(crate) mod wired; pub(crate) mod yarn; diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/tests.rs b/crates/socket-patch-core/src/vendor/lock_inventory/tests.rs index 0e009b07e..2960fad1e 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/tests.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/tests.rs @@ -2745,7 +2745,7 @@ async fn the_vendored_requirements_writers_own_output_reinventories() { let tmp = tempfile::tempdir().unwrap(); let line = crate::vendor::pypi_requirements::vendor_line( &format!(".socket/vendor/pypi/{UUID}/requests-2.28.1-py3-none-any.whl"), - &"c".repeat(64), + Some(&"c".repeat(64)), "requests", "2.28.1", &None, diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/vlt.rs b/crates/socket-patch-core/src/vendor/lock_inventory/vlt.rs index 7034acb88..497c1b213 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/vlt.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/vlt.rs @@ -6,8 +6,8 @@ use std::path::Path; use serde_json::{Map, Value}; -use crate::constants::npm_family::VLT_LOCK; use super::view::ProjectView; +use crate::constants::npm_family::VLT_LOCK; use crate::vendor::vlt_lock_text::{ is_default_registry, sniff_lock, split_dep_id, DepId, DepIdKind, LockSniff, }; diff --git a/crates/socket-patch-core/src/vendor/npm_dir.rs b/crates/socket-patch-core/src/vendor/npm_dir.rs index 3e5c27683..c84572970 100644 --- a/crates/socket-patch-core/src/vendor/npm_dir.rs +++ b/crates/socket-patch-core/src/vendor/npm_dir.rs @@ -1249,8 +1249,10 @@ mod tests { let why = gitignore_probe(&root, &outside).await.unwrap_err(); assert!(why.contains("`git check-ignore` exited 128"), "{why}"); assert_eq!(gitignored(&root, &outside).await, None); - assert!(gitignore_unchecked_detail(".socket/vendor/npm/u/a-1.0.0", &why) - .contains("make sure no ignore rule covers .socket/")); + assert!( + gitignore_unchecked_detail(".socket/vendor/npm/u/a-1.0.0", &why) + .contains("make sure no ignore rule covers .socket/") + ); } #[cfg(unix)] diff --git a/crates/socket-patch-core/src/vendor/prestage.rs b/crates/socket-patch-core/src/vendor/prestage.rs index 47a6a75be..2b99623d4 100644 --- a/crates/socket-patch-core/src/vendor/prestage.rs +++ b/crates/socket-patch-core/src/vendor/prestage.rs @@ -464,7 +464,10 @@ mod sweep_tests { for dir in &kept { assert!(v.join(dir).exists(), "{dir} kept"); } - assert!(!v.join("gem").exists(), "the levels only the tree kept alive are pruned"); + assert!( + !v.join("gem").exists(), + "the levels only the tree kept alive are pruned" + ); assert!(!v.join(format!("composer/{u}/psr/log@3.0.2")).exists()); assert!(v.join("state.json").exists()); assert_eq!(sweep_stale(root).await, 0, "idempotent"); diff --git a/crates/socket-patch-core/src/vendor/pypi.rs b/crates/socket-patch-core/src/vendor/pypi.rs index 2c2f6c3a5..1b07f26f1 100644 --- a/crates/socket-patch-core/src/vendor/pypi.rs +++ b/crates/socket-patch-core/src/vendor/pypi.rs @@ -2324,6 +2324,20 @@ mod tests { record: PatchRecord, } + /// [`e2e_fixture`] with a hash-pinned requirements.txt: pip's + /// hash-checking mode is on, so the vendor line carries the `--hash` + /// pin the in-sync rebuild guard reads back (#376). + async fn e2e_fixture_hashed() -> E2eFixture { + let fx = e2e_fixture().await; + touch( + &fx.root, + "requirements.txt", + &format!("six==1.16.0 --hash=sha256:{}\n", "0".repeat(64)), + ) + .await; + fx + } + /// A requirements-flavor project: requirements.txt at the root, a /// six-like install in a venv-ish site-packages, and a blob store. async fn e2e_fixture() -> E2eFixture { @@ -2431,16 +2445,15 @@ mod tests { hex::encode(sha2::Sha256::digest(&wheel_bytes)) ); - // The requirements line was rewritten with that exact hash. + // The requirements line was rewritten to the wheel path. The file + // had no hashes, so neither does the line (#376): one `--hash` + // would put pip in hash-checking mode for every requirement. let req = tokio::fs::read_to_string(fx.root.join("requirements.txt")) .await .unwrap(); assert_eq!( req, - format!( - "./{wheel_rel} --hash=sha256:{} # socket-patch vendor: six==1.16.0\n", - entry.artifact.sha256 - ) + format!("./{wheel_rel} # socket-patch vendor: six==1.16.0\n") ); assert_eq!(entry.wiring.len(), 1); assert_eq!(entry.wiring[0].kind, "requirements_line"); @@ -3072,7 +3085,7 @@ wheels = [ /// `vendor_prebuilt_downloaded` advisory is emitted. #[tokio::test] async fn service_success_requirements_writes_wheel_and_wires_sha256() { - let fx = e2e_fixture().await; + let fx = e2e_fixture_hashed().await; let sources = PatchSources::blobs_only(&fx.blobs); let bytes: &[u8] = &served_wheel(b"prebuilt wheel bytes from the service"); let sri = sri_sha512(bytes); @@ -3548,7 +3561,7 @@ wheels = [ /// build that reproduces the pin, exactly as with the ledger present. #[tokio::test] async fn in_sync_ledgerless_service_rebuild_must_not_break_wired_pin() { - let fx = e2e_fixture().await; + let fx = e2e_fixture_hashed().await; let sources = PatchSources::blobs_only(&fx.blobs); let VendorOutcome::Done { result, entry, .. } = vendor_pypi( "pkg:pypi/six@1.16.0", @@ -3632,7 +3645,7 @@ wheels = [ /// pin — the wired file itself carries the pin the guard checks. #[tokio::test] async fn in_sync_ledgerless_local_rebuild_pin_mismatch_fails_loudly() { - let fx = e2e_fixture().await; + let fx = e2e_fixture_hashed().await; let sources = PatchSources::blobs_only(&fx.blobs); let bytes: &[u8] = &served_wheel(b"prebuilt wheel bytes from the service"); let sri = sri_sha512(bytes); @@ -6307,7 +6320,7 @@ wheels = [ /// `auto` in favor of the deterministic local build that reproduces it. #[tokio::test] async fn in_sync_rebuild_with_corrupt_ledger_falls_back_to_wired_pin() { - let fx = e2e_fixture().await; + let fx = e2e_fixture_hashed().await; let sources = PatchSources::blobs_only(&fx.blobs); let VendorOutcome::Done { result, entry, .. } = vendor_six(&fx, &sources, None).await else { diff --git a/crates/socket-patch-core/src/vendor/pypi_requirements.rs b/crates/socket-patch-core/src/vendor/pypi_requirements.rs index bd920202e..dc7d10cb0 100644 --- a/crates/socket-patch-core/src/vendor/pypi_requirements.rs +++ b/crates/socket-patch-core/src/vendor/pypi_requirements.rs @@ -1,13 +1,20 @@ //! requirements.txt wiring (pip & `uv pip`). //! //! The spike-verified line shape is -//! `./[ ; ] --hash=sha256: # socket-patch vendor: ==`: +//! `./[ ; ] [--hash=sha256:] # socket-patch vendor: ==`: //! both pip 26 and uv 0.11 accept the bare relative path (resolved against //! the INVOKING CWD, never the requirements-file dir — hence the documented -//! root-only constraint), enforce the `--hash` pin (implicitly: any -//! `--hash` on any line turns hash-checking on), strip the trailing comment, -//! and genuinely EVALUATE a `; marker` on a path line — so an environment -//! marker is carried over from the replaced pin instead of refused. +//! root-only constraint), enforce the `--hash` pin, strip the trailing +//! comment, and genuinely EVALUATE a `; marker` on a path line — so an +//! environment marker is carried over from the replaced pin instead of +//! refused. +//! +//! The `--hash` is written only when the requirements tree is already in +//! pip's hash-checking mode ([`requires_hashes`]): any `--hash` on any line +//! turns that mode on for the whole install, so a hashed vendor line in an +//! unhashed tree would make pip refuse every other requirement (#376). A +//! path line cannot carry a `#sha256=` fragment instead; the committed +//! wheel is the repository's own content. //! //! Logical-line model: physical lines join on a trailing `\`; comments start //! at a `#` preceded by whitespace (or column 0) outside that. The dominant @@ -19,7 +26,7 @@ use std::path::{Path, PathBuf}; use crate::crawlers::python_crawler::canonicalize_pypi_name; use crate::utils::fs::{atomic_write_bytes_preserving_mode, read_regular_to_string}; use crate::utils::requirements::{ - hash_options, logical_lines, split_comment, strip_comment, vendor_tag, + hash_options, logical_lines, requires_hashes, split_comment, strip_comment, vendor_tag, }; use super::common::{detect_eol, refuse_symlinked}; @@ -440,6 +447,9 @@ async fn plan_requirements( wheel_sha256_hex: &str, ) -> Result, (&'static str, String)> { let files = collect_requirements_files(root).await?; + // pip's hash-checking mode spans the whole install: every reachable + // file (includes too) decides whether the vendor line is hashed. + let hashed = files.iter().any(|f| requires_hashes(&f.content)); let mut planned: Vec = Vec::new(); let mut rewrote_any = false; @@ -497,7 +507,7 @@ async fn plan_requirements( for (start, count, marker, _) in spans.iter().rev() { let line = vendor_line( rel_wheel, - wheel_sha256_hex, + hashed.then_some(wheel_sha256_hex), canon_name, version, marker, @@ -542,7 +552,7 @@ async fn plan_requirements( .expect("collect_requirements_files always yields the root file first"); let line = vendor_line( rel_wheel, - wheel_sha256_hex, + hashed.then_some(wheel_sha256_hex), canon_name, version, &None, @@ -572,15 +582,17 @@ async fn plan_requirements( Ok(planned) } -/// The committed vendor line. `transitive` adds the `(transitive)` note so a -/// reader knows the line was appended (no pin was replaced). +/// The committed vendor line. `sha256_hex` is the `--hash` pin, `None` for a +/// requirements tree outside hash-checking mode (module docs). `transitive` +/// adds the `(transitive)` note so a reader knows the line was appended (no +/// pin was replaced). /// /// Visible to the rest of `vendor` so the lockfile inventory's round-trip /// test can read back exactly what this writes (the two grammars — the one /// that writes a vendored line and the one that reads it — must agree). pub(in crate::vendor) fn vendor_line( rel_wheel: &str, - sha256_hex: &str, + sha256_hex: Option<&str>, canon_name: &str, version: &str, marker: &Option, @@ -590,9 +602,12 @@ pub(in crate::vendor) fn vendor_line( .as_ref() .map(|m| format!(" ; {m}")) .unwrap_or_default(); + let hash_part = sha256_hex + .map(|hex| format!(" --hash=sha256:{hex}")) + .unwrap_or_default(); let note = if transitive { " (transitive)" } else { "" }; format!( - "./{rel_wheel}{marker_part} --hash=sha256:{sha256_hex} # socket-patch vendor: {canon_name}=={version}{note}" + "./{rel_wheel}{marker_part}{hash_part} # socket-patch vendor: {canon_name}=={version}{note}" ) } @@ -911,6 +926,11 @@ mod tests { format!("./{REL_WHEEL} --hash=sha256:{SHA} # socket-patch vendor: six==1.16.0") } + /// [`expected_line`] for a requirements tree outside hash-checking mode. + fn expected_unhashed_line() -> String { + format!("./{REL_WHEEL} # socket-patch vendor: six==1.16.0") + } + async fn write_root(content: &str) -> tempfile::TempDir { let tmp = tempfile::tempdir().unwrap(); tokio::fs::write(tmp.path().join("requirements.txt"), content) @@ -1019,7 +1039,7 @@ mod tests { .unwrap(); assert_eq!( read_root(tmp.path()).await, - format!("requests==2.31.0\n{}\n", expected_line()) + format!("requests==2.31.0\n{}\n", expected_unhashed_line()) ); assert_eq!(wiring.len(), 1); assert_eq!(wiring[0].kind, "requirements_line"); @@ -1058,6 +1078,68 @@ mod tests { assert_eq!(read_root(tmp.path()).await, original); } + /// #376: an unhashed requirements set must stay unhashed. pip turns + /// hash-checking mode on for the whole install as soon as one line has a + /// `--hash`, so a hashed vendor line makes `pip install -r` refuse every + /// other (unhashed) requirement. A bare path cannot carry a `#sha256=` + /// fragment, so the committed wheel's line goes without one. + #[tokio::test] + async fn unhashed_requirements_get_an_unhashed_vendor_line() { + for (original, wired) in [ + ( + "six==1.16.0\nidna==3.7\n", + format!("./{REL_WHEEL} # socket-patch vendor: six==1.16.0\nidna==3.7\n"), + ), + ( + "idna==3.7\n", + format!( + "idna==3.7\n./{REL_WHEEL} # socket-patch vendor: six==1.16.0 (transitive)\n" + ), + ), + ] { + let tmp = write_root(original).await; + let wiring = wire_requirements(tmp.path(), "six", "1.16.0", REL_WHEEL, SHA) + .await + .unwrap(); + assert_eq!(read_root(tmp.path()).await, wired); + // Still read back as our line for this patch. + assert!(matches!( + preflight_requirements(tmp.path(), "six", "1.16.0", UUID).await, + Ok(RequirementsTarget::InSync { pin: None }) + )); + let outcome = revert_requirements(&entry_for(wiring), tmp.path(), false).await; + assert!(outcome.success, "{:?}", outcome.error); + assert_eq!(read_root(tmp.path()).await, original); + } + } + + /// #376: hashes anywhere in the requirements tree (an `-r` include, or + /// `--require-hashes`) mean pip is in hash-checking mode, so the vendor + /// line keeps its `--hash` pin. + #[tokio::test] + async fn hashes_in_an_include_keep_the_vendor_line_hashed() { + let tmp = write_root("-r deps.txt\nsix==1.16.0\n").await; + tokio::fs::write(tmp.path().join("deps.txt"), "idna==3.7 --hash=sha256:aa\n") + .await + .unwrap(); + wire_requirements(tmp.path(), "six", "1.16.0", REL_WHEEL, SHA) + .await + .unwrap(); + assert_eq!( + read_root(tmp.path()).await, + format!("-r deps.txt\n{}\n", expected_line()) + ); + + let tmp = write_root("--require-hashes\nsix==1.16.0\n").await; + wire_requirements(tmp.path(), "six", "1.16.0", REL_WHEEL, SHA) + .await + .unwrap(); + assert_eq!( + read_root(tmp.path()).await, + format!("--require-hashes\n{}\n", expected_line()) + ); + } + #[tokio::test] async fn marker_is_carried_over_verbatim() { let tmp = write_root("six==1.16.0 ; python_version >= \"3.8\"\n").await; @@ -1067,7 +1149,7 @@ mod tests { assert_eq!( read_root(tmp.path()).await, format!( - "./{REL_WHEEL} ; python_version >= \"3.8\" --hash=sha256:{SHA} # socket-patch vendor: six==1.16.0\n" + "./{REL_WHEEL} ; python_version >= \"3.8\" # socket-patch vendor: six==1.16.0\n" ) ); } @@ -1081,7 +1163,7 @@ mod tests { assert_eq!( read_root(tmp.path()).await, format!( - "python-dateutil==2.8.2\n./{REL_WHEEL} --hash=sha256:{SHA} # socket-patch vendor: six==1.16.0 (transitive)\n" + "python-dateutil==2.8.2\n./{REL_WHEEL} # socket-patch vendor: six==1.16.0 (transitive)\n" ) ); assert_eq!(wiring[0].action, WiringAction::Added); @@ -1114,7 +1196,7 @@ mod tests { tokio::fs::read_to_string(tmp.path().join("deps/pinned.txt")) .await .unwrap(), - format!("{}\n", expected_line()) + format!("{}\n", expected_unhashed_line()) ); assert_eq!(wiring.len(), 1); assert_eq!(wiring[0].file, "deps/pinned.txt"); @@ -1224,7 +1306,7 @@ mod tests { ); assert_eq!( read_root(tmp.path()).await, - format!("# vendored from C:\\deps\\\n{}\n", expected_line()) + format!("# vendored from C:\\deps\\\n{}\n", expected_unhashed_line()) ); } @@ -1373,8 +1455,12 @@ mod tests { let wiring = wire_requirements(tmp.path(), "six", "1.16.0", REL_WHEEL, SHA) .await .unwrap(); - // Drift: the user edited the vendor line (changed the hash). - let drifted = read_root(tmp.path()).await.replace(SHA, &"0".repeat(64)); + // Drift: the user edited the vendor line (added a marker). + let drifted = read_root(tmp.path()).await.replace( + " # socket-patch vendor", + " ; python_version >= \"3\" # socket-patch vendor", + ); + assert_ne!(drifted, read_root(tmp.path()).await); tokio::fs::write(tmp.path().join("requirements.txt"), &drifted) .await .unwrap(); @@ -1457,7 +1543,7 @@ mod tests { ); assert_eq!( read_root(tmp.path()).await, - format!("{}\n", expected_line()) + format!("{}\n", expected_unhashed_line()) ); // The BOM travels inside the replaced physical line's record, so @@ -1496,7 +1582,7 @@ mod tests { tokio::fs::read_to_string(tmp.path().join("dev.txt")) .await .unwrap(), - format!("{}\n", expected_line()) + format!("{}\n", expected_unhashed_line()) ); } @@ -1603,7 +1689,7 @@ mod tests { .unwrap(); assert_eq!(wiring.len(), 2); let written = read_root(tmp.path()).await; - assert_eq!(written.matches(&expected_line()).count(), 2); + assert_eq!(written.matches(&expected_unhashed_line()).count(), 2); let outcome = revert_requirements(&entry_for(wiring), tmp.path(), false).await; assert!(outcome.success, "{:?}", outcome.error); @@ -1856,7 +1942,7 @@ mod tests { assert_eq!( read_root(tmp.path()).await, format!( - "requests==2.31.0\n./{REL_WHEEL} --hash=sha256:{SHA} # socket-patch vendor: six==1.16.0 (transitive)\n" + "requests==2.31.0\n./{REL_WHEEL} # socket-patch vendor: six==1.16.0 (transitive)\n" ) ); let outcome = revert_requirements(&entry_for(wiring), tmp.path(), false).await; @@ -1878,7 +1964,7 @@ mod tests { assert_eq!( read_root(tmp.path()).await, format!( - "requests==2.31.0\r\nzope.interface==5.0\r\n./{REL_WHEEL} --hash=sha256:{SHA} # socket-patch vendor: six==1.16.0 (transitive)\r\n" + "requests==2.31.0\r\nzope.interface==5.0\r\n./{REL_WHEEL} # socket-patch vendor: six==1.16.0 (transitive)\r\n" ) ); } @@ -1918,7 +2004,7 @@ mod tests { tokio::fs::read_to_string(tmp.path().join("deps/b.txt")) .await .unwrap(), - format!("{}\n", expected_line()) + format!("{}\n", expected_unhashed_line()) ); // No transitive duplicate at the root, and the other files are // byte-untouched — the walk really visited them. @@ -2014,7 +2100,7 @@ mod tests { tokio::fs::read_to_string(tmp.path().join("dev.txt")) .await .unwrap(), - format!("{}\n", expected_line()) + format!("{}\n", expected_unhashed_line()) ); } diff --git a/crates/socket-patch-core/src/vendor/toml_surgery.rs b/crates/socket-patch-core/src/vendor/toml_surgery.rs index 4837a61bf..b5d799b93 100644 --- a/crates/socket-patch-core/src/vendor/toml_surgery.rs +++ b/crates/socket-patch-core/src/vendor/toml_surgery.rs @@ -509,7 +509,8 @@ mod tests { // mixed-ending file, so the removal helpers must never normalize: // every byte outside the removed segment survives verbatim (the // go_mod/go_sum CRLF-churn class). - let wired = "[project]\r\nname = \"x\"\r\n\n[tool.uv.sources]\nfoo = { path = \"w.whl\" }\n"; + let wired = + "[project]\r\nname = \"x\"\r\n\n[tool.uv.sources]\nfoo = { path = \"w.whl\" }\n"; let after = remove_exact_line(wired, "foo = { path = \"w.whl\" }").unwrap(); assert_eq!(after, "[project]\r\nname = \"x\"\r\n\n[tool.uv.sources]\n"); assert_eq!( diff --git a/crates/socket-patch-core/tests/covgap_api_blob_fetcher.rs b/crates/socket-patch-core/tests/covgap_api_blob_fetcher.rs index 8d495238b..3809272d0 100644 --- a/crates/socket-patch-core/tests/covgap_api_blob_fetcher.rs +++ b/crates/socket-patch-core/tests/covgap_api_blob_fetcher.rs @@ -572,5 +572,8 @@ async fn fetch_missing_blobs_mixed_outcomes_aggregate_and_format() { // End-to-end formatter exercise with a genuinely mixed result. let rendered = format_fetch_result(&result); assert!(rendered.contains("Downloaded 1 blob\n"), "{rendered}"); - assert!(rendered.contains("Failed to download 2 blobs"), "{rendered}"); + assert!( + rendered.contains("Failed to download 2 blobs"), + "{rendered}" + ); } diff --git a/crates/socket-patch-core/tests/covgap_crawlers_composer_crawler.rs b/crates/socket-patch-core/tests/covgap_crawlers_composer_crawler.rs index 24a50d9b9..fbbda6290 100644 --- a/crates/socket-patch-core/tests/covgap_crawlers_composer_crawler.rs +++ b/crates/socket-patch-core/tests/covgap_crawlers_composer_crawler.rs @@ -95,7 +95,11 @@ async fn get_vendor_paths_global_nonexistent_composer_home_falls_back() { fn write_composer_shim(dir: &Path, echo_path: &Path) { use std::os::unix::fs::PermissionsExt; let shim = dir.join("composer"); - std::fs::write(&shim, format!("#!/bin/sh\necho '{}'\n", echo_path.display())).unwrap(); + std::fs::write( + &shim, + format!("#!/bin/sh\necho '{}'\n", echo_path.display()), + ) + .unwrap(); std::fs::set_permissions(&shim, std::fs::Permissions::from_mode(0o755)).unwrap(); } diff --git a/crates/socket-patch-core/tests/covgap_vendor_nuget_feed.rs b/crates/socket-patch-core/tests/covgap_vendor_nuget_feed.rs index 5f712da1f..7842d44bc 100644 --- a/crates/socket-patch-core/tests/covgap_vendor_nuget_feed.rs +++ b/crates/socket-patch-core/tests/covgap_vendor_nuget_feed.rs @@ -88,9 +88,7 @@ async fn stage_tempdir_creation_failure_is_reported_not_fatal() { drop(guard); match outcome { - VendorOutcome::Done { - result, entry, .. - } => { + VendorOutcome::Done { result, entry, .. } => { assert!(!result.success, "the stage failure must fail the vendor"); assert!(entry.is_none(), "no ledger entry for a failed vendor"); let err = result.error.as_deref().unwrap_or(""); diff --git a/crates/socket-patch-core/tests/poetry_hosted.rs b/crates/socket-patch-core/tests/poetry_hosted.rs index 17173e65b..a12615cc6 100644 --- a/crates/socket-patch-core/tests/poetry_hosted.rs +++ b/crates/socket-patch-core/tests/poetry_hosted.rs @@ -65,7 +65,11 @@ async fn native_lock_generations_redirect_idempotently_and_restore_every_byte() let codes: Vec<&str> = result.warnings.iter().map(|w| w.code.as_str()).collect(); assert_eq!( codes, - if pre_1_4 { vec!["redirect_poetry_stale_install_risk"] } else { vec![] }, + if pre_1_4 { + vec!["redirect_poetry_stale_install_risk"] + } else { + vec![] + }, "{version}: {:?}", result.warnings ); @@ -111,12 +115,24 @@ fn hosted_shapes_match_each_lock_generations_installer() { ) .files["poetry.lock"] .clone(); - assert!(lock10.contains(&format!("url = \"{URL}#sha256={sha}&\"")), "{lock10}"); + assert!( + lock10.contains(&format!("url = \"{URL}#sha256={sha}&\"")), + "{lock10}" + ); assert!(lock10.contains("reference = \"\""), "{lock10}"); - assert!(lock10.contains(&format!("urllib3 = [{{ file = \"{WHEEL}\", hash = \"sha256:{sha}\" }}]")), "{lock10}"); + assert!( + lock10.contains(&format!( + "urllib3 = [{{ file = \"{WHEEL}\", hash = \"sha256:{sha}\" }}]" + )), + "{lock10}" + ); // Poetry >= 1.2 consuming this 1.0 lock verifies the package `files` // entry, so it is written too (1.0 ignores the extra key). - assert_eq!(lock10.matches(&format!("sha256:{sha}")).count(), 2, "{lock10}"); + assert_eq!( + lock10.matches(&format!("sha256:{sha}")).count(), + 2, + "{lock10}" + ); let doc: toml_edit::DocumentMut = lock10.parse().unwrap(); assert!(doc["package"][0]["files"].is_array(), "{lock10}"); @@ -126,8 +142,15 @@ fn hosted_shapes_match_each_lock_generations_installer() { ) .files["poetry.lock"] .clone(); - assert_eq!(lock11.matches(&format!("sha256:{sha}")).count(), 2, "package files + metadata.files:\n{lock11}"); - assert!(lock11.contains(&format!("url = \"{URL}\"")), "no fragment on 1.1"); + assert_eq!( + lock11.matches(&format!("sha256:{sha}")).count(), + 2, + "package files + metadata.files:\n{lock11}" + ); + assert!( + lock11.contains(&format!("url = \"{URL}\"")), + "no fragment on 1.1" + ); assert!(!lock11.contains("reference"), "{lock11}"); let doc: toml_edit::DocumentMut = lock11.parse().unwrap(); assert!(doc["package"][0]["files"].is_array()); @@ -139,11 +162,19 @@ fn hosted_shapes_match_each_lock_generations_installer() { ) .files["poetry.lock"] .clone(); - assert_eq!(lock21.matches(&format!("sha256:{sha}")).count(), 1, "{lock21}"); + assert_eq!( + lock21.matches(&format!("sha256:{sha}")).count(), + 1, + "{lock21}" + ); assert!(!lock21.contains("reference")); let pristine: toml_edit::DocumentMut = original("2.4.3").parse().unwrap(); let doc: toml_edit::DocumentMut = lock21.parse().unwrap(); - assert_eq!(doc["metadata"].to_string(), pristine["metadata"].to_string(), "[metadata] untouched on 2.x"); + assert_eq!( + doc["metadata"].to_string(), + pristine["metadata"].to_string(), + "[metadata] untouched on 2.x" + ); } #[test] @@ -360,14 +391,21 @@ fn absent_entries_warn_once_and_missing_sha256_is_gated_once_per_dep() { let codes: Vec<&str> = result.warnings.iter().map(|w| w.code.as_str()).collect(); assert_eq!( codes, - vec!["redirect_poetry_entry_not_found", "redirect_poetry_entry_not_found"] + vec![ + "redirect_poetry_entry_not_found", + "redirect_poetry_entry_not_found" + ] ); let mut missing_hash = patch(); missing_hash.integrity.sha256 = None; let result = rewrite_registry_redirect(&files, &[missing_hash]); assert!(result.files.is_empty()); let codes: Vec<&str> = result.warnings.iter().map(|w| w.code.as_str()).collect(); - assert_eq!(codes, vec!["redirect_poetry_missing_sha256"], "gated once, not once per lock"); + assert_eq!( + codes, + vec!["redirect_poetry_missing_sha256"], + "gated once, not once per lock" + ); } /// A future Poetry that bumps the lock minor (2.2) is rewritten like 2.1 in @@ -381,9 +419,12 @@ async fn newer_2x_minor_redirects_and_reverts() { assert!(result.warnings.is_empty(), "{:?}", result.warnings); assert!(result.files["poetry.lock"].contains(URL)); let directory = tempfile::tempdir().unwrap(); - tokio::fs::write(directory.path().join("poetry.lock"), &result.files["poetry.lock"]) - .await - .unwrap(); + tokio::fs::write( + directory.path().join("poetry.lock"), + &result.files["poetry.lock"], + ) + .await + .unwrap(); let mut state = RedirectState { edits: result.edits, ..RedirectState::default() @@ -391,7 +432,9 @@ async fn newer_2x_minor_redirects_and_reverts() { let outcome = revert_remaining_redirect_edits(directory.path(), &mut state, false).await; assert!(outcome.fully_reverted(), "{:?}", outcome.refusals); assert_eq!( - tokio::fs::read_to_string(directory.path().join("poetry.lock")).await.unwrap(), + tokio::fs::read_to_string(directory.path().join("poetry.lock")) + .await + .unwrap(), lock ); } @@ -405,13 +448,22 @@ fn rotated_grant_token_supersedes_the_prior_hosted_url() { let first = rewrite_registry_redirect(&files, &[patch()]); let mut rotated = patch(); rotated.token = "00000000-0000-4000-8000-000000000000".into(); - rotated.artifact_url = URL.replace("7e52b8b6-53f2-4dc8-860a-1ae7ebd8be0e", "00000000-0000-4000-8000-000000000000"); + rotated.artifact_url = URL.replace( + "7e52b8b6-53f2-4dc8-860a-1ae7ebd8be0e", + "00000000-0000-4000-8000-000000000000", + ); let second = rewrite_registry_redirect(&first.files, &[rotated.clone()]); assert!(second.warnings.is_empty(), "{:?}", second.warnings); let lock = &second.files["poetry.lock"]; assert!(lock.contains(&rotated.artifact_url) && !lock.contains(URL)); assert_eq!(second.edits.len(), 1); - assert!(second.edits[0].original.as_ref().unwrap().as_str().unwrap().contains(URL)); + assert!(second.edits[0] + .original + .as_ref() + .unwrap() + .as_str() + .unwrap() + .contains(URL)); } /// A relock (or hand edit) that drops the inserted `files` line but keeps @@ -433,22 +485,35 @@ async fn dropped_files_line_with_source_kept_is_refused_not_converged() { .collect::>() .join("\n") + "\n"; - assert_ne!(drifted, *redirected, "{version}: the files line must have been removed"); + assert_ne!( + drifted, *redirected, + "{version}: the files line must have been removed" + ); assert!(drifted.contains("[package.source]")); let directory = tempfile::tempdir().unwrap(); - tokio::fs::write(directory.path().join("poetry.lock"), &drifted).await.unwrap(); + tokio::fs::write(directory.path().join("poetry.lock"), &drifted) + .await + .unwrap(); let mut state = RedirectState { edits: result.edits.clone(), ..RedirectState::default() }; let outcome = revert_remaining_redirect_edits(directory.path(), &mut state, false).await; - assert!(!outcome.fully_reverted(), "{version}: must refuse, not report success"); + assert!( + !outcome.fully_reverted(), + "{version}: must refuse, not report success" + ); assert_eq!( - tokio::fs::read_to_string(directory.path().join("poetry.lock")).await.unwrap(), + tokio::fs::read_to_string(directory.path().join("poetry.lock")) + .await + .unwrap(), drifted, "{version}: a refused revert writes nothing" ); - assert!(!state.edits.is_empty(), "{version}: the ledger keeps its edits for a re-scan"); + assert!( + !state.edits.is_empty(), + "{version}: the ledger keeps its edits for a re-scan" + ); } } @@ -463,7 +528,9 @@ async fn lock_1_0_rollback_converges_on_a_hand_restored_lock() { let result = rewrite_registry_redirect(&files, &[patch()]); assert!(!result.edits.is_empty()); let directory = tempfile::tempdir().unwrap(); - tokio::fs::write(directory.path().join("poetry.lock"), &pristine).await.unwrap(); + tokio::fs::write(directory.path().join("poetry.lock"), &pristine) + .await + .unwrap(); let mut state = RedirectState { edits: result.edits, ..RedirectState::default() @@ -472,7 +539,9 @@ async fn lock_1_0_rollback_converges_on_a_hand_restored_lock() { assert!(outcome.fully_reverted(), "{:?}", outcome.refusals); assert!(state.edits.is_empty()); assert_eq!( - tokio::fs::read_to_string(directory.path().join("poetry.lock")).await.unwrap(), + tokio::fs::read_to_string(directory.path().join("poetry.lock")) + .await + .unwrap(), pristine ); } From 7279f36463a1d88d42eccdcf99f9d7b9a3d06000 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 21:49:32 +0000 Subject: [PATCH 3/8] Refuse setup on a hash-pinned requirements.txt socket-patch setup appended an unpinned, unhashed socket-patch[hook] line to requirements.txt even when the file (or an -r include) was in pip's hash-checking mode. pip then refused to install anything from it, while setup reported success (#378). setup now reports an error and leaves such a file untouched. setup --remove also drops a hook line together with its backslash continuation lines, so no stray --hash line is left behind. Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-core/src/setup/pypi/edit.rs | 147 +++++++++++++++++- .../src/utils/requirements.rs | 22 +++ 2 files changed, 166 insertions(+), 3 deletions(-) diff --git a/crates/socket-patch-core/src/setup/pypi/edit.rs b/crates/socket-patch-core/src/setup/pypi/edit.rs index 926dbc544..b4d6c2287 100644 --- a/crates/socket-patch-core/src/setup/pypi/edit.rs +++ b/crates/socket-patch-core/src/setup/pypi/edit.rs @@ -24,6 +24,7 @@ use super::detect::{deps_contain_hook, HOOK_DEP}; // `requirements.txt` sight-unseen). use crate::utils::fs::{atomic_write_bytes_preserving_mode, read_regular_to_string}; use crate::utils::python_lock::preserve_line_endings; +use crate::utils::requirements::{logical_lines, requires_hashes}; use crate::utils::toml_edit_ext::ensure_table; use crate::vendor::common::detect_eol; @@ -108,11 +109,46 @@ pub async fn add_hook_dependency(path: &Path, kind: ManifestKind, dry_run: bool) let outcome = match kind { ManifestKind::Pyproject => pyproject_add(&content), - ManifestKind::Requirements => Ok(requirements_add(&content)), + ManifestKind::Requirements => match requirements_add(&content) { + Some(_) if requirements_tree_requires_hashes(path, &content).await => Err(format!( + "{} is in pip's hash-checking mode (its requirements carry --hash or it sets \ + --require-hashes), so pip would refuse an unpinned, unhashed `{HOOK_DEP}` line \ + and install nothing; add a pinned, hashed hook requirement yourself", + path.display() + )), + added => Ok(added), + }, }; finish(path, dry_run, outcome).await } +/// Whether pip reads this requirements file in hash-checking mode (#378): +/// its own lines, or any in-root `-r` include it pulls in, carry `--hash` or +/// `--require-hashes` (the mode spans the whole install). An include that +/// cannot be read is left to pip to report. +async fn requirements_tree_requires_hashes(path: &Path, content: &str) -> bool { + if requires_hashes(content) { + return true; + } + let Some(dir) = path.parent() else { + return false; + }; + if path.file_name() != Some(std::ffi::OsStr::new("requirements.txt")) { + return false; + } + let Ok(names) = crate::vendor::pypi_requirements::requirements_include_names(dir).await else { + return false; + }; + for rel in names.iter().filter(|rel| *rel != "requirements.txt") { + if let Ok(include) = read_regular_to_string(&dir.join(rel)).await { + if requires_hashes(&include) { + return true; + } + } + } + false +} + /// Remove the hook dependency from a manifest. Idempotent (already-absent -> /// `AlreadyConfigured`, i.e. nothing to do). pub async fn remove_hook_dependency( @@ -155,11 +191,32 @@ fn requirements_add(content: &str) -> Option { } /// Returns `Some(new_content)` if any hook line was removed, `None` otherwise. +/// +/// Works on pip's logical lines, so a hook requirement continued with `\` +/// (a pinned, `--hash`-ed hook line) goes as a whole instead of leaving its +/// `--hash` continuation lines behind (#378). fn requirements_remove(content: &str) -> Option { - let kept: Vec<&str> = content.lines().filter(|l| !deps_contain_hook(l)).collect(); - if kept.len() == content.lines().count() { + let lines = logical_lines(content); + let mut kept: Vec = Vec::new(); + let mut removed = false; + let mut bom = false; + for line in &lines { + if deps_contain_hook(&line.text) { + removed = true; + // The file's BOM is encoding, not part of the hook line. + bom |= line.start == 0 && line.physical[0].starts_with('\u{feff}'); + } else { + kept.extend(line.physical.iter().cloned()); + } + } + if !removed { return None; } + if bom { + if let Some(first) = kept.first_mut() { + first.insert(0, '\u{feff}'); + } + } let nl = detect_eol(content); let mut new = kept.join(nl); if !new.is_empty() { @@ -516,6 +573,90 @@ mod tests { assert!(requirements_remove("requests\n").is_none()); } + /// #378: a hook line with `\` continuations (the `--hash` lines of a + /// hash-pinned file) is ONE requirement; `--remove` drops all of it, not + /// just the first physical line, leaving no dangling `--hash` behind. + #[test] + fn test_requirements_remove_drops_continuation_lines() { + for nl in ["\n", "\r\n"] { + let content = format!( + "six==1.16.0 \\{nl} --hash=sha256:aa{nl}socket-patch-hook==4.0.0 \\{nl} --hash=sha256:bb \\{nl} --hash=sha256:cc{nl}idna==3.7 --hash=sha256:dd{nl}" + ); + assert_eq!( + requirements_remove(&content).unwrap(), + format!("six==1.16.0 \\{nl} --hash=sha256:aa{nl}idna==3.7 --hash=sha256:dd{nl}") + ); + } + // A first-line hook keeps the file's BOM on the next line. + assert_eq!( + requirements_remove("\u{feff}socket-patch[hook]\nsix\n").unwrap(), + "\u{feff}six\n" + ); + } + + /// #378: pip's hash-checking mode (any `--hash`, or `--require-hashes`) + /// requires every requirement to be `==`-pinned and hashed. An unhashed + /// `socket-patch[hook]` line would make `pip install -r` refuse the whole + /// file, so `setup` refuses instead of reporting success — and leaves the + /// manifest untouched. + #[tokio::test] + async fn test_add_refuses_hash_pinned_requirements() { + for original in [ + "six==1.16.0 \\\n --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254\n", + "--require-hashes\nsix==1.16.0\n", + ] { + let dir = tempfile::tempdir().unwrap(); + let req = dir.path().join("requirements.txt"); + tokio::fs::write(&req, original).await.unwrap(); + for dry_run in [true, false] { + let res = add_hook_dependency(&req, ManifestKind::Requirements, dry_run).await; + assert_eq!(res.status, PthStatus::Error, "{original:?}"); + assert!( + res.error.as_deref().unwrap_or("").contains("hash-checking mode"), + "{:?}", + res.error + ); + assert_eq!(tokio::fs::read_to_string(&req).await.unwrap(), original); + } + } + } + + /// #378: the hash-checking mode spans `-r` includes too. + #[tokio::test] + async fn test_add_refuses_when_an_include_is_hash_pinned() { + let dir = tempfile::tempdir().unwrap(); + let req = dir.path().join("requirements.txt"); + tokio::fs::write(&req, "-r base.txt\n").await.unwrap(); + tokio::fs::write( + dir.path().join("base.txt"), + "six==1.16.0 --hash=sha256:aa\n", + ) + .await + .unwrap(); + let res = add_hook_dependency(&req, ManifestKind::Requirements, false).await; + assert_eq!(res.status, PthStatus::Error); + assert_eq!( + tokio::fs::read_to_string(&req).await.unwrap(), + "-r base.txt\n" + ); + } + + /// A hash-pinned file that already declares the hook (the user wrote the + /// pinned, hashed line themselves) is configured, not refused. + #[tokio::test] + async fn test_add_hash_pinned_file_with_hook_is_configured() { + let dir = tempfile::tempdir().unwrap(); + let req = dir.path().join("requirements.txt"); + tokio::fs::write( + &req, + "six==1.16.0 --hash=sha256:aa\nsocket-patch-hook==4.0.0 --hash=sha256:bb\n", + ) + .await + .unwrap(); + let res = add_hook_dependency(&req, ManifestKind::Requirements, false).await; + assert_eq!(res.status, PthStatus::AlreadyConfigured); + } + // ── pyproject PEP 621 ──────────────────────────────────────────── #[test] diff --git a/crates/socket-patch-core/src/utils/requirements.rs b/crates/socket-patch-core/src/utils/requirements.rs index 08c1f50fe..c054b829b 100644 --- a/crates/socket-patch-core/src/utils/requirements.rs +++ b/crates/socket-patch-core/src/utils/requirements.rs @@ -212,6 +212,28 @@ pub(crate) fn url_sha256_fragment(location: &str) -> Option { mod tests { use super::*; + #[test] + fn requires_hashes_reads_pip_hash_checking_mode() { + for hashed in [ + "six==1.16.0 --hash=sha256:aa\nidna==3.7\n", + "six==1.16.0 \\\n --hash sha256:aa\n", + "six==1.16.0 --hash=sha512:aa\n", + "--require-hashes\nsix==1.16.0\n", + "\u{feff}--require-hashes\r\nsix==1.16.0\r\n", + ] { + assert!(requires_hashes(hashed), "{hashed:?}"); + } + for unhashed in [ + "", + "six==1.16.0\nidna==3.7\n", + // Comments and url fragments are not hash options. + "six==1.16.0 # --hash=sha256:aa\n# --require-hashes\n", + "six @ https://example.test/six-1.16.0-py2.py3-none-any.whl#sha256=aa\n", + ] { + assert!(!requires_hashes(unhashed), "{unhashed:?}"); + } + } + #[test] fn lexer_joins_continuations_and_strips_comments_correctly() { let lines = logical_lines("six==1.16.0 \\\n --hash=sha256:abc\nrequests\n"); From 92ec6193c67719d578d72377af0bea2307fe1d98 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 21:51:21 +0000 Subject: [PATCH 4/8] Cover a two-line unhashed file in the pip e2e Adds an 'unhashed' cell (six plus idna, no hashes) to the real-pip capstone. It asserts the wiring adds no --hash and that pip installs the patched six. Also updates the redirect fixture to the #sha256= url form written for unhashed files. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/e2e_vex_build/pip.rs | 37 ++++++++++++++----- .../requirements/basic/expected-edits.json | 2 +- .../basic/expected/requirements.txt | 2 +- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_vex_build/pip.rs b/crates/socket-patch-cli/tests/e2e_vex_build/pip.rs index 0b60020ce..c24dc476a 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_build/pip.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_build/pip.rs @@ -1,12 +1,13 @@ //! Real-pip capstone for manifest-less VEX over `requirements.txt`: for //! every pip major (latest release of each, `SOCKET_PATCH_PIP_E2E_VERSIONS` -//! overrides), HOSTED and VENDORED, three project shapes: +//! overrides), HOSTED and VENDORED, four project shapes: //! //! | cell | requirements | hosted | vendored | //! | --- | --- | --- | --- | //! | `root` | `six==1.16.0` | yes | yes | //! | `hashes` | `pip-compile --generate-hashes` style (`\` continued `--hash`, hash-checking mode) | yes | yes | //! | `include` | `-r requirements/base.txt` | root-only rewriter: stays on the registry, nothing attested | yes | +//! | `unhashed` | `six==1.16.0` + `idna==3.7`, no hashes: the wiring must not add a `--hash` (#376) | yes | yes | //! //! Each flow: //! @@ -57,6 +58,10 @@ enum Cell { Root, Hashes, Include, + /// #376: a second, unhashed requirement next to the patched one. One + /// `--hash` on the wired line would put pip in hash-checking mode for + /// the whole install and refuse `idna==3.7`. + Unhashed, } impl Cell { @@ -65,6 +70,7 @@ impl Cell { Cell::Root => "root", Cell::Hashes => "hashes", Cell::Include => "include", + Cell::Unhashed => "unhashed", } } @@ -82,6 +88,7 @@ impl Cell { ("requirements.txt", "-r requirements/base.txt\n".into()), ("requirements/base.txt", "six==1.16.0\n".into()), ], + Cell::Unhashed => vec![("requirements.txt", "six==1.16.0\nidna==3.7\n".into())], } } } @@ -221,10 +228,19 @@ fn flow(uv: &Path, major: &str, pip_version: &str, cell: Cell, mode: Mode, root: record("pip", pip_version, &row, "embedded-scan-vex", "pass"); std::fs::remove_file(&embedded).unwrap(); let wired = wiring_text(&proj, cell); + // pip's hash-checking mode is all or nothing (#376): only an already + // hashed file gets a `--hash`; the hosted url otherwise carries the + // pin as a `#sha256=` fragment pip verifies without the mode. + assert_eq!( + wired.contains("--hash="), + cell == Cell::Hashes, + "{what}: the wiring must keep the file's hash-checking mode: {wired}" + ); match mode { Mode::Hosted => assert!( - wired.contains(&api.artifact_url()) && wired.contains("--hash=sha256:"), - "{what}: requirements must point at the hosted wheel: {wired}" + wired.contains(&api.artifact_url()) + && (cell == Cell::Hashes || wired.contains(".whl#sha256=")), + "{what}: requirements must point at the pinned hosted wheel: {wired}" ), Mode::Vendored => assert!( wired.contains(&format!(".socket/vendor/pypi/{}/", mode.uuid())), @@ -239,11 +255,14 @@ fn flow(uv: &Path, major: &str, pip_version: &str, cell: Cell, mode: Mode, root: let fresh_venv = fresh.join(".venv"); pip_venv(uv, major, &fresh_venv).unwrap_or_else(|e| panic!("{what}: fresh venv: {e}")); let downloads = api.artifact_downloads(); - let out = pip( - &fresh_venv, - &fresh, - &["install", "--no-index", "-r", "requirements.txt"], - ); + // The unhashed cell's other requirement (idna) comes from PyPI; the + // patched six still can only come from the wiring. + let install: &[&str] = if cell == Cell::Unhashed { + &["install", "-r", "requirements.txt"] + } else { + &["install", "--no-index", "-r", "requirements.txt"] + }; + let out = pip(&fresh_venv, &fresh, install); assert_ok(&out, &format!("{what}: fresh `pip install --no-index -r`")); let (_, bytes, is_patched) = six_oracle(&venv_bin(&fresh_venv, "python"), &fresh) .unwrap_or_else(|| panic!("{what}: six not importable in the fresh checkout")); @@ -335,7 +354,7 @@ fn pip_every_major_hosted_and_vendored_end_in_manifest_less_vex() { continue; } }; - for cell in [Cell::Root, Cell::Hashes, Cell::Include] { + for cell in [Cell::Root, Cell::Hashes, Cell::Include, Cell::Unhashed] { for mode in [Mode::Hosted, Mode::Vendored] { let root = scratch.path().join("run"); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { diff --git a/crates/socket-patch-core/tests/fixtures/redirect/pypi/requirements/basic/expected-edits.json b/crates/socket-patch-core/tests/fixtures/redirect/pypi/requirements/basic/expected-edits.json index 1c218ec53..d83746fbd 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/pypi/requirements/basic/expected-edits.json +++ b/crates/socket-patch-core/tests/fixtures/redirect/pypi/requirements/basic/expected-edits.json @@ -5,6 +5,6 @@ "action": "rewritten", "key": "Requests", "original": "requests==2.28.1 ; python_version >= \"3.7\"", - "new": "Requests @ https://patch.socket.dev/patch/pypi/requests/2.28.1/11111111-1111-1111-1111-111111111111/33333333-3333-3333-3333-333333333333/requests-2.28.1-py3-none-any.whl ; python_version >= \"3.7\" --hash=sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + "new": "Requests @ https://patch.socket.dev/patch/pypi/requests/2.28.1/11111111-1111-1111-1111-111111111111/33333333-3333-3333-3333-333333333333/requests-2.28.1-py3-none-any.whl#sha256=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef ; python_version >= \"3.7\"" } ] diff --git a/crates/socket-patch-core/tests/fixtures/redirect/pypi/requirements/basic/expected/requirements.txt b/crates/socket-patch-core/tests/fixtures/redirect/pypi/requirements/basic/expected/requirements.txt index c7f822106..0c789d68e 100644 --- a/crates/socket-patch-core/tests/fixtures/redirect/pypi/requirements/basic/expected/requirements.txt +++ b/crates/socket-patch-core/tests/fixtures/redirect/pypi/requirements/basic/expected/requirements.txt @@ -1,2 +1,2 @@ flask==2.0.1 -Requests @ https://patch.socket.dev/patch/pypi/requests/2.28.1/11111111-1111-1111-1111-111111111111/33333333-3333-3333-3333-333333333333/requests-2.28.1-py3-none-any.whl ; python_version >= "3.7" --hash=sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef +Requests @ https://patch.socket.dev/patch/pypi/requests/2.28.1/11111111-1111-1111-1111-111111111111/33333333-3333-3333-3333-333333333333/requests-2.28.1-py3-none-any.whl#sha256=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef ; python_version >= "3.7" From 39ec2d94dd3a038b033297d54c8320c8c2e21e0e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 21:59:36 +0000 Subject: [PATCH 5/8] Expect url-fragment pins in unhashed pip e2e The production and vendored e2e legs write a one-line unhashed requirements.txt and then asserted a --hash pin, which is the #376 behavior itself. They now assert that no --hash is added. The hosted leg also asserts the #sha256= url pin, which pip and uv both verify. Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-cli/tests/e2e_hosted_production.rs | 10 +++++++--- crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs | 10 ++++++---- .../socket-patch-cli/tests/e2e_vendored_production.rs | 5 +++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_hosted_production.rs b/crates/socket-patch-cli/tests/e2e_hosted_production.rs index eacda8a85..bab54530d 100644 --- a/crates/socket-patch-cli/tests/e2e_hosted_production.rs +++ b/crates/socket-patch-cli/tests/e2e_hosted_production.rs @@ -1916,10 +1916,14 @@ fn pypi_requirements_txt_hosted_install_proof() { assert_redirected(&env_json, "requirements.txt"); let reqs = read(&proj.join("requirements.txt")); assert_hosted_pin(&reqs, PYPI_UUIDS, LEG); + // An unhashed file is pinned by the url's `#sha256=` fragment, which + // pip and uv both verify; a `--hash` would put pip in hash-checking + // mode for every other requirement (#376). assert!( - reqs.contains("--hash=sha256:"), - "{LEG}: rewritten requirements.txt carries no --hash pin, so pip/uv \ - would install the hosted wheel unverified:\n{reqs}" + reqs.contains(".whl#sha256=") && !reqs.contains("--hash"), + "{LEG}: rewritten requirements.txt must pin the hosted wheel by its \ + url fragment (and add no --hash), or pip/uv would install it \ + unverified:\n{reqs}" ); std::fs::remove_dir_all(&venv).expect("rm venv"); diff --git a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs index 43a3af7ef..dc6db4d01 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs @@ -1025,8 +1025,9 @@ fn pip_requirements_vendor_fresh_checkout_no_index_and_revert() { ); assert_vendored_applied(&parse_envelope(&stdout)); - // Artifact + the rewritten pin line (the exact spike-tested shape: - // `./ --hash=sha256: # socket-patch vendor: six==1.16.0`). + // Artifact + the rewritten pin line (the spike-tested shape: + // `./ # socket-patch vendor: six==1.16.0`; `--hash=sha256:` + // only in a file already in pip's hash-checking mode, #376). let wheel = vendored_wheel(&proj); let wheel_rel = format!( ".socket/vendor/pypi/{UUID}/{}", @@ -1044,8 +1045,9 @@ fn pip_requirements_vendor_fresh_checkout_no_index_and_revert() { "the path line must be ./-prefixed and project-relative: {vendor_line}" ); assert!( - vendor_line.contains("--hash=sha256:"), - "the path line must pin the wheel hash (hardens every install): {vendor_line}" + !requirements.contains("--hash"), + "an unhashed requirements.txt must stay unhashed, or pip's \ + hash-checking mode refuses every other requirement (#376):\n{requirements}" ); assert!( !requirements diff --git a/crates/socket-patch-cli/tests/e2e_vendored_production.rs b/crates/socket-patch-cli/tests/e2e_vendored_production.rs index 4a9bf1c03..788093a50 100644 --- a/crates/socket-patch-cli/tests/e2e_vendored_production.rs +++ b/crates/socket-patch-cli/tests/e2e_vendored_production.rs @@ -1886,8 +1886,9 @@ fn pypi_requirements_txt_vendored_install_proof() { "{LEG}: requirements.txt was not rewired to the vendored wheel:\n{reqs}" ); assert!( - reqs.contains("--hash=sha256:"), - "{LEG}: rewritten requirements.txt carries no --hash pin:\n{reqs}" + !reqs.contains("--hash"), + "{LEG}: an unhashed requirements.txt must stay unhashed, or pip's \ + hash-checking mode refuses every other requirement (#376):\n{reqs}" ); // DELIVERY PROOF: requirements.txt + .socket only, fresh venv, --no-index From 642f6092c192d6fe9b42229e64e846cd9e22f080 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 22:14:23 +0000 Subject: [PATCH 6/8] Align remaining pip tests with hash-mode wiring - The VEX discovery golden now shows the #sha256= url on the hosted requirements ref. - The hosted get test expects the url-fragment pin. - The vendor ledger parity test allows the one intended change from the base binary: no --hash in an unhashed requirements.txt. - The marker e2e installs with --require-hashes, so its input is now hash-pinned the way pip-compile writes it. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/e2e_vendor_pypi_build.rs | 7 ++++- .../tests/in_process_get_hosted_ecosystems.rs | 8 +++-- .../tests/vendor_ledger_schema_e2e.rs | 31 ++++++++++++++++++- .../vex-discover-golden/redirect-pypi.json | 2 +- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs index dc6db4d01..002f8cd6a 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs @@ -1195,7 +1195,12 @@ fn pip_vendored_requirements_evaluate_environment_markers() { "install upstream six", ); let patched = stage_patch(&project, &site_packages(&venv).join("six.py")); - let original = format!("six==1.16.0 ; {marker}\n"); + // Hash-pinned (pip-compile style), so the fresh install below can run + // `--require-hashes`: a hashed file keeps the vendor line hashed + // (#376), and the marker must survive next to the `--hash`. + let original = format!( + "six==1.16.0 ; {marker} \\\n --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254\n" + ); std::fs::write(project.join("requirements.txt"), &original).unwrap(); let (code, stdout, stderr) = run_vendored(&VendorDriver::VendorOffline, &project); assert_eq!(code, 0, "vendor failed: {stdout}\n{stderr}"); diff --git a/crates/socket-patch-cli/tests/in_process_get_hosted_ecosystems.rs b/crates/socket-patch-cli/tests/in_process_get_hosted_ecosystems.rs index c563da334..f2432e9bf 100644 --- a/crates/socket-patch-cli/tests/in_process_get_hosted_ecosystems.rs +++ b/crates/socket-patch-cli/tests/in_process_get_hosted_ecosystems.rs @@ -184,8 +184,10 @@ fn assert_no_manifest_no_blobs(cwd: &Path) { // --------------------------------------------------------------------------- /// A pip project pinning `requests==2.31.0`: the hosted grant must rewrite -/// that one line to `requests @ --hash=sha256:` (the -/// integrity pin fails closed on tampered bytes), leave the bystander line +/// that one line to `requests @ #sha256=` (the integrity +/// pin fails closed on tampered bytes; a url fragment, not `--hash`, since +/// one `--hash` would put pip in hash-checking mode for the unhashed +/// `flask` line too — #376), leave the bystander line /// byte-identical, record the ledger — and write no manifest. #[tokio::test] #[serial] @@ -221,7 +223,7 @@ async fn pypi_requirements_hosted_rewrites_pinned_line() { assert_eq!(code, 0, "get --mode hosted (pypi) should succeed"); let reqs = std::fs::read_to_string(tmp.path().join("requirements.txt")).unwrap(); - let expected_line = format!("requests @ {url} --hash=sha256:{SHA256}"); + let expected_line = format!("requests @ {url}#sha256={SHA256}"); assert!( reqs.lines().any(|l| l == expected_line), "requirements.txt must pin the hosted wheel URL + sha256; got:\n{reqs}" diff --git a/crates/socket-patch-cli/tests/vendor_ledger_schema_e2e.rs b/crates/socket-patch-cli/tests/vendor_ledger_schema_e2e.rs index 406a5b33f..3c22361eb 100644 --- a/crates/socket-patch-cli/tests/vendor_ledger_schema_e2e.rs +++ b/crates/socket-patch-cli/tests/vendor_ledger_schema_e2e.rs @@ -208,6 +208,25 @@ fn new_ledgers_compact_whole_file_snapshots_and_revert() { } } +/// `bytes` with every ` --hash=sha256:<64 hex>` option removed. +fn strip_sha256_hash_options(bytes: &[u8]) -> Vec { + const NEEDLE: &[u8] = b" --hash=sha256:"; + let mut out = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + let hex = bytes.get(i + NEEDLE.len()..i + NEEDLE.len() + 64); + if bytes[i..].starts_with(NEEDLE) + && hex.is_some_and(|h| h.iter().all(u8::is_ascii_hexdigit)) + { + i += NEEDLE.len() + 64; + } else { + out.push(bytes[i]); + i += 1; + } + } + out +} + /// Against the integrated base: for every ecosystem this binary wires /// exactly the files the base binary wired (the checked-in legacy /// fixtures), and its ledger — whatever its on-disk version — loads to the @@ -215,7 +234,17 @@ fn new_ledgers_compact_whole_file_snapshots_and_revert() { #[tokio::test] async fn this_binary_wires_what_the_base_binary_wired() { for eco in fx::ALL { - let base_wired = read_tree(&fixtures_dir().join(eco).join("wired")); + let mut base_wired = read_tree(&fixtures_dir().join(eco).join("wired")); + if *eco == "pypi-requirements" { + // The one intended difference: the base binary pinned its vendor + // lines with `--hash` even in this unhashed requirements.txt, + // which put pip in hash-checking mode for every other + // requirement (#376). This binary writes the same lines without + // it — in the file and in the ledger's recorded `new` text. + for (_, bytes) in &mut base_wired { + *bytes = strip_sha256_hash_options(bytes); + } + } let f = Fixture::new(eco); let pristine = tree(&f.root); let (code, stdout, stderr) = f.vendor(&[], &[]); diff --git a/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-pypi.json b/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-pypi.json index b601a1156..5082f40ce 100644 --- a/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-pypi.json +++ b/crates/socket-patch-core/tests/fixtures/vex-discover-golden/redirect-pypi.json @@ -9,7 +9,7 @@ "artifact_rel": null, "locked_integrity": "Sha256Hex(\"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef\")", "integrity_required": true, - "url": "https://patch.socket.dev/patch/pypi/requests/2.28.1/11111111-1111-1111-1111-111111111111/33333333-3333-3333-3333-333333333333/requests-2.28.1-py3-none-any.whl", + "url": "https://patch.socket.dev/patch/pypi/requests/2.28.1/11111111-1111-1111-1111-111111111111/33333333-3333-3333-3333-333333333333/requests-2.28.1-py3-none-any.whl#sha256=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "lockfile_basis_ok": true } ], From e0ed1aa3cdce7e414988139bed884bf07d60a763 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 23:13:54 +0000 Subject: [PATCH 7/8] Drop unrelated rustfmt churn from the fix An earlier cargo fmt --all run reformatted about 55 files that this fix doesn't otherwise touch; main isn't rustfmt-clean and CI doesn't check formatting. This restores those files to main so the PR diff only holds the hash-mode change and its tests. Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-cli/src/commands/get.rs | 12 +- .../src/commands/scan/discovery.rs | 6 +- crates/socket-patch-cli/src/commands/setup.rs | 3 +- .../socket-patch-cli/src/commands/update.rs | 21 +--- .../socket-patch-cli/src/commands/vendor.rs | 6 +- .../src/hosted_memory/redirect.rs | 3 +- .../socket-patch-cli/tests/apply_network.rs | 5 +- .../tests/cli_config_fallback.rs | 7 +- .../socket-patch-cli/tests/cli_parse_list.rs | 10 +- .../tests/cli_parse_rollback.rs | 6 +- .../coverage_fix_apply_silent_mute_exit.rs | 4 +- .../coverage_fix_repair_vendor_predelete.rs | 14 +-- .../tests/covgap_commands_update.rs | 12 +- .../tests/covgap_commands_vex.rs | 16 +-- .../tests/covgap_ecosystem_dispatch.rs | 8 +- .../socket-patch-cli/tests/covgap_output.rs | 10 +- .../tests/covgap_setup_composer_mod.rs | 5 +- .../tests/covgap_setup_gem_mod.rs | 14 +-- .../tests/covgap_setup_pypi_detect.rs | 11 +- crates/socket-patch-cli/tests/e2e_cargo.rs | 6 +- crates/socket-patch-cli/tests/e2e_maven.rs | 3 +- crates/socket-patch-cli/tests/e2e_nuget.rs | 6 +- .../tests/get_edge_cases_e2e.rs | 12 +- .../tests/global_packages_e2e.rs | 5 +- .../tests/help_text_hygiene.rs | 12 +- .../tests/hosted_memory_engine.rs | 3 +- .../tests/in_process_redirect/vlt.rs | 10 +- .../tests/in_process_redirect_pdm.rs | 34 ++---- .../tests/in_process_redirect_pipenv.rs | 93 ++++---------- .../tests/in_process_rollback_hosted/vlt.rs | 18 +-- .../tests/interactive_prompts_e2e.rs | 5 +- .../tests/rollback_duality_invariants.rs | 3 +- .../socket-patch-cli/tests/scan_vendor_e2e.rs | 6 +- .../tests/self_update_channels_e2e.rs | 5 +- .../tests/vendor_rerun_no_network_e2e.rs | 13 +- .../src/crawlers/npm_crawler.rs | 6 +- .../src/crawlers/npm_crawler/oracle.rs | 6 +- .../src/crawlers/python_crawler.rs | 6 +- .../patch/redirect/group_equivalence_tests.rs | 7 +- .../src/patch/redirect/mod.rs | 14 +-- .../src/patch/redirect/pdm.rs | 15 +-- .../src/patch/redirect/pipenv.rs | 88 +++----------- .../src/patch/redirect/poetry.rs | 32 ++--- .../socket-patch-core/src/update/download.rs | 13 +- .../socket-patch-core/src/update/release.rs | 39 ++---- .../src/utils/group_commit.rs | 21 +--- crates/socket-patch-core/src/utils/mod.rs | 2 +- .../socket-patch-core/src/utils/pdm_lock.rs | 11 +- .../src/utils/poetry_lock.rs | 107 ++++------------ .../src/utils/python_script.rs | 7 +- .../src/vendor/lock_inventory/mod.rs | 2 +- .../src/vendor/lock_inventory/vlt.rs | 2 +- .../socket-patch-core/src/vendor/npm_dir.rs | 6 +- .../socket-patch-core/src/vendor/prestage.rs | 5 +- .../src/vendor/toml_surgery.rs | 3 +- .../tests/covgap_api_blob_fetcher.rs | 5 +- .../tests/covgap_crawlers_composer_crawler.rs | 6 +- .../tests/covgap_vendor_nuget_feed.rs | 4 +- .../socket-patch-core/tests/poetry_hosted.rs | 115 ++++-------------- 59 files changed, 241 insertions(+), 688 deletions(-) diff --git a/crates/socket-patch-cli/src/commands/get.rs b/crates/socket-patch-cli/src/commands/get.rs index 49901285d..86e0dbbb4 100644 --- a/crates/socket-patch-cli/src/commands/get.rs +++ b/crates/socket-patch-cli/src/commands/get.rs @@ -7272,11 +7272,8 @@ mod tests { let installed = |name: &str, body: &[u8]| { let dist = site.path().join(format!("{name}-1.0.0.dist-info")); std::fs::create_dir_all(&dist).unwrap(); - std::fs::write( - dist.join("METADATA"), - format!("Name: {name}\nVersion: 1.0.0\n"), - ) - .unwrap(); + std::fs::write(dist.join("METADATA"), format!("Name: {name}\nVersion: 1.0.0\n")) + .unwrap(); std::fs::write(site.path().join(format!("{name}.py")), body).unwrap(); compute_git_sha256_from_bytes(body) }; @@ -7320,10 +7317,7 @@ mod tests { mount(uuid("bs"), "beta_sdist.py".into(), "0".repeat(64), 0).await; for n in ["gw", "gs"] { Mock::given(method("GET")) - .and(wm_path(format!( - "/v0/orgs/test-org/patches/view/{}", - uuid(n) - ))) + .and(wm_path(format!("/v0/orgs/test-org/patches/view/{}", uuid(n)))) .respond_with(ResponseTemplate::new(500)) .expect(0) .mount(&server) diff --git a/crates/socket-patch-cli/src/commands/scan/discovery.rs b/crates/socket-patch-cli/src/commands/scan/discovery.rs index 19a80a48f..862ec33e2 100644 --- a/crates/socket-patch-cli/src/commands/scan/discovery.rs +++ b/crates/socket-patch-cli/src/commands/scan/discovery.rs @@ -2094,11 +2094,7 @@ mod tests { "pkg:npm/lockonly@1.0.0", std::path::PathBuf::from("/nonexistent"), ), - crawled_pkg( - "alpha", - "pkg:npm/alpha@1.0.0", - installed("alpha", "alpha.js"), - ), + crawled_pkg("alpha", "pkg:npm/alpha@1.0.0", installed("alpha", "alpha.js")), crawled_pkg( "embedded", "pkg:npm/embedded@1.0.0", diff --git a/crates/socket-patch-cli/src/commands/setup.rs b/crates/socket-patch-cli/src/commands/setup.rs index 5aa62226a..c530616ac 100644 --- a/crates/socket-patch-cli/src/commands/setup.rs +++ b/crates/socket-patch-cli/src/commands/setup.rs @@ -247,7 +247,8 @@ async fn hooked_vlt_members(found: &PackageJsonFindResult) -> Vec { } let mut hooked = Vec::new(); for loc in found.files.iter().filter(|loc| !loc.is_root) { - if let Ok(content) = socket_patch_core::utils::fs::read_regular_to_string(&loc.path).await { + if let Ok(content) = socket_patch_core::utils::fs::read_regular_to_string(&loc.path).await + { let status = is_setup_configured_str(&content); if status.postinstall_configured || status.dependencies_configured { hooked.push(loc.path.clone()); diff --git a/crates/socket-patch-cli/src/commands/update.rs b/crates/socket-patch-cli/src/commands/update.rs index be7ae1777..fa6c384bd 100644 --- a/crates/socket-patch-cli/src/commands/update.rs +++ b/crates/socket-patch-cli/src/commands/update.rs @@ -159,11 +159,7 @@ fn cancelled_message(current: &semver::Version, target: &semver::Version) -> &'s /// The result line after a successful install, naming the same action as /// [`confirm_prompt`]. -fn installed_message( - current: &semver::Version, - target: &semver::Version, - path: &std::path::Path, -) -> String { +fn installed_message(current: &semver::Version, target: &semver::Version, path: &std::path::Path) -> String { let path = path.display(); if target < current { format!("Downgraded socket-patch {current} \u{2192} {target} ({path})") @@ -504,18 +500,9 @@ mod tests { #[test] fn cancel_and_result_lines_match_the_prompt() { - assert_eq!( - cancelled_message(&v("4.0.0"), &v("9.9.9")), - "Update cancelled." - ); - assert_eq!( - cancelled_message(&v("4.0.0"), &v("3.0.0")), - "Downgrade cancelled." - ); - assert_eq!( - cancelled_message(&v("4.0.0"), &v("4.0.0")), - "Reinstall cancelled." - ); + assert_eq!(cancelled_message(&v("4.0.0"), &v("9.9.9")), "Update cancelled."); + assert_eq!(cancelled_message(&v("4.0.0"), &v("3.0.0")), "Downgrade cancelled."); + assert_eq!(cancelled_message(&v("4.0.0"), &v("4.0.0")), "Reinstall cancelled."); let p = std::path::Path::new("/opt/sp/socket-patch"); assert_eq!( installed_message(&v("4.0.0"), &v("9.9.9"), p), diff --git a/crates/socket-patch-cli/src/commands/vendor.rs b/crates/socket-patch-cli/src/commands/vendor.rs index 34ada0431..bd1342ac7 100644 --- a/crates/socket-patch-cli/src/commands/vendor.rs +++ b/crates/socket-patch-cli/src/commands/vendor.rs @@ -3864,11 +3864,7 @@ mod plan_gate_tests { .unwrap(); let packages = [ ("pkg:composer/psr/cache@1.0.0", "psr/cache", UUID_A), - ( - "pkg:composer/psr/http-message@1.1.0", - "psr/http-message", - UUID_B, - ), + ("pkg:composer/psr/http-message@1.1.0", "psr/http-message", UUID_B), ("pkg:composer/psr/log@3.0.2", "psr/log", UUID_C), ]; let mut all_packages: Vec<(String, StagedSource)> = Vec::new(); diff --git a/crates/socket-patch-cli/src/hosted_memory/redirect.rs b/crates/socket-patch-cli/src/hosted_memory/redirect.rs index a9567eaf5..4a8763415 100644 --- a/crates/socket-patch-cli/src/hosted_memory/redirect.rs +++ b/crates/socket-patch-cli/src/hosted_memory/redirect.rs @@ -19,7 +19,8 @@ use socket_patch_core::patch::redirect::npmrc::{ NPMRC_REL, }; use socket_patch_core::patch::redirect::{ - rewrite_registry_redirect_withholding_vlt, DepOverride, FileEdit, RewriteResult, RewriteWarning, + rewrite_registry_redirect_withholding_vlt, DepOverride, FileEdit, RewriteResult, + RewriteWarning, }; use socket_patch_core::utils::purl::{purl_parts, strip_purl_qualifiers}; use socket_patch_core::vendor::lock_inventory::{MemoryEntry, MemoryProject}; diff --git a/crates/socket-patch-cli/tests/apply_network.rs b/crates/socket-patch-cli/tests/apply_network.rs index a0dc94f38..562feae43 100644 --- a/crates/socket-patch-cli/tests/apply_network.rs +++ b/crates/socket-patch-cli/tests/apply_network.rs @@ -1075,7 +1075,10 @@ async fn mismatch_blob_topup_probes_every_copy_of_a_duplicated_package() { v["summary"]["applied"], 1, "the drifted nested copy must be warn-overwritten.\nstdout={v:#}" ); - assert_eq!(v["summary"]["failed"], 0, "no copy may fail.\nstdout={v:#}"); + assert_eq!( + v["summary"]["failed"], 0, + "no copy may fail.\nstdout={v:#}" + ); // The nested copy's blob was fetched on demand… let requests = mock.received_requests().await.unwrap(); diff --git a/crates/socket-patch-cli/tests/cli_config_fallback.rs b/crates/socket-patch-cli/tests/cli_config_fallback.rs index ed24d824d..dc09b57a6 100644 --- a/crates/socket-patch-cli/tests/cli_config_fallback.rs +++ b/crates/socket-patch-cli/tests/cli_config_fallback.rs @@ -59,7 +59,8 @@ fn scan_cmd(project: &Path, data_dir: &Path) -> Command { let mut cmd = Command::new(BINARY); // Human mode: core's proxy advisory (the oracle below) is muted under // `--json`/`--silent`. - cmd.args(["scan", "-e", "npm", "--cwd"]).arg(project); + cmd.args(["scan", "-e", "npm", "--cwd"]) + .arg(project); for (key, _) in std::env::vars_os() { let name = key.to_string_lossy(); if name.starts_with("SOCKET_") { @@ -297,9 +298,7 @@ async fn corrupt_config_warns_and_keeps_json_stdout_clean() { json_cmd.arg("--json"); let json_out = run(json_cmd); assert!( - json_out - .stderr - .contains("could not parse socket-cli config"), + json_out.stderr.contains("could not parse socket-cli config"), "the parse warning must reach stderr under --json too; got:\n{}", json_out.stderr ); diff --git a/crates/socket-patch-cli/tests/cli_parse_list.rs b/crates/socket-patch-cli/tests/cli_parse_list.rs index cf9a838d5..ad6b39392 100644 --- a/crates/socket-patch-cli/tests/cli_parse_list.rs +++ b/crates/socket-patch-cli/tests/cli_parse_list.rs @@ -1202,10 +1202,7 @@ fn missing_manifest_with_corrupt_ledger_keeps_warning_in_error_envelope_via_bina assert_eq!(v["error"]["code"], "manifest_not_found", "envelope={v}"); let warnings = v["warnings"].as_array().expect("warnings[] present"); assert_eq!(warnings.len(), 1, "envelope={v}"); - assert_eq!( - warnings[0]["code"], "redirect_ledger_corrupt", - "envelope={v}" - ); + assert_eq!(warnings[0]["code"], "redirect_ledger_corrupt", "envelope={v}"); assert!( out.stderr.is_empty(), "--json must keep stderr clean: {}", @@ -1217,10 +1214,7 @@ fn missing_manifest_with_corrupt_ledger_keeps_warning_in_error_envelope_via_bina let stderr = String::from_utf8_lossy(&out.stderr); assert_eq!(out.status.code(), Some(1)); assert!(stderr.contains("Warning: "), "stderr={stderr}"); - assert!( - stderr.contains("Error: Manifest not found at "), - "stderr={stderr}" - ); + assert!(stderr.contains("Error: Manifest not found at "), "stderr={stderr}"); } #[test] diff --git a/crates/socket-patch-cli/tests/cli_parse_rollback.rs b/crates/socket-patch-cli/tests/cli_parse_rollback.rs index ba87aaaac..d46f04b8f 100644 --- a/crates/socket-patch-cli/tests/cli_parse_rollback.rs +++ b/crates/socket-patch-cli/tests/cli_parse_rollback.rs @@ -378,11 +378,7 @@ fn bare_bool_does_not_consume_next_token() { /// relied on the rejection get a test-visible flip instead of a silent one. #[test] fn multiple_targets_parse_in_order() { - let args = parse_rollback(&[ - "pkg:npm/foo@1", - "packages/api/**", - "b0630680-4da6-45f9-bba8-b888e0ffd58c", - ]); + let args = parse_rollback(&["pkg:npm/foo@1", "packages/api/**", "b0630680-4da6-45f9-bba8-b888e0ffd58c"]); assert_eq!( args.targets, vec![ diff --git a/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs b/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs index bd72b2afe..a56820e7d 100644 --- a/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs +++ b/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs @@ -150,9 +150,7 @@ fn apply_silent_online_download_failure_keeps_error_output() { ); let chatter = stderr_chatter(&stderr); assert!( - chatter - .iter() - .any(|l| l.contains("could not be downloaded")), + chatter.iter().any(|l| l.contains("could not be downloaded")), "--silent must keep the download-failure error (errors only, \ never nothing); stderr was: {stderr:?}" ); diff --git a/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs b/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs index d68e382dc..e403c3d6e 100644 --- a/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs +++ b/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs @@ -234,9 +234,12 @@ async fn repair_keeps_healthy_soft_artifact_when_rebuild_dispatch_fails() { let gemfile_wired = std::fs::read(tmp.path().join("Gemfile")).unwrap(); std::fs::remove_file(tmp.path().join(".socket/vendor/state.json")).unwrap(); - std::fs::remove_file(tmp.path().join(format!( - "vendor/bundle/ruby/3.4.0/gems/{GEM_NAME}-{GEM_VERSION}/lib/padlock.rb" - ))) + std::fs::remove_file( + tmp.path() + .join(format!( + "vendor/bundle/ruby/3.4.0/gems/{GEM_NAME}-{GEM_VERSION}/lib/padlock.rb" + )), + ) .unwrap(); mount_blob(&mock).await; @@ -277,10 +280,7 @@ async fn repair_keeps_healthy_soft_artifact_when_rebuild_dispatch_fails() { &std::fs::read_to_string(tmp.path().join(".socket/vendor/state.json")).unwrap(), ) .unwrap(); - assert_eq!( - state["entries"][GEM_PURL]["uuid"], GEM_UUID, - "state={state}" - ); + assert_eq!(state["entries"][GEM_PURL]["uuid"], GEM_UUID, "state={state}"); assert_eq!( std::fs::read(tmp.path().join("Gemfile")).unwrap(), gemfile_wired, diff --git a/crates/socket-patch-cli/tests/covgap_commands_update.rs b/crates/socket-patch-cli/tests/covgap_commands_update.rs index ac923fc4d..d0b7b7ea8 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_update.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_update.rs @@ -10,10 +10,10 @@ //! self_update_e2e.rs / interactive_prompts_e2e.rs (do not edit those //! files). -#[path = "common/mod.rs"] -mod common; #[path = "common/pty_io.rs"] mod pty_io; +#[path = "common/mod.rs"] +mod common; #[path = "common/update_fixture.rs"] mod update_fixture; @@ -275,8 +275,9 @@ mod pty { let mut child = pair.slave.spawn_command(cmd).expect("spawn in PTY"); drop(pair.slave); - let reader_handle = - crate::pty_io::PtyOutput::spawn(pair.master.try_clone_reader().expect("clone reader")); + let reader_handle = crate::pty_io::PtyOutput::spawn( + pair.master.try_clone_reader().expect("clone reader"), + ); let mut killer = child.clone_killer(); std::thread::spawn(move || { @@ -344,8 +345,7 @@ mod pty { "a declined update exits 1 (codebase convention); got: {output}" ); assert!( - !output.contains("Updated socket-patch") - && !output.contains("Reinstalled socket-patch"), + !output.contains("Updated socket-patch") && !output.contains("Reinstalled socket-patch"), "a declined update must not report a swap; got: {output}" ); diff --git a/crates/socket-patch-cli/tests/covgap_commands_vex.rs b/crates/socket-patch-cli/tests/covgap_commands_vex.rs index 1c9d787c5..d9a31ae05 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_vex.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_vex.rs @@ -544,24 +544,14 @@ fn auto_detect_multi_manifest_warning_reaches_json_envelope() { ]) .output() .expect("invoke vex"); - assert!( - out.status.success(), - "{}", - String::from_utf8_lossy(&out.stderr) - ); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); let env: Value = serde_json::from_slice(&out.stdout).expect("envelope JSON on stdout"); let w = env["warnings"] .as_array() - .and_then(|ws| { - ws.iter() - .find(|w| w["code"] == "product_multiple_manifests") - }) + .and_then(|ws| ws.iter().find(|w| w["code"] == "product_multiple_manifests")) .unwrap_or_else(|| panic!("product_multiple_manifests warning expected: {env}")); assert!( - w["detail"] - .as_str() - .unwrap() - .contains("Multiple project manifests"), + w["detail"].as_str().unwrap().contains("Multiple project manifests"), "{w}" ); let stderr = String::from_utf8_lossy(&out.stderr); diff --git a/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs b/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs index dc6e63536..c3655302e 100644 --- a/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs +++ b/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs @@ -254,10 +254,7 @@ fn rollback_dispatch_branch_deno() { .unwrap_or_else(|e| panic!("rollback envelope must parse ({e}); stdout={stdout}")); let code = out.status.code().unwrap_or(-1); - assert_eq!( - code, 0, - "rollback --ecosystems=deno: expected exit 0; env={env}" - ); + assert_eq!(code, 0, "rollback --ecosystems=deno: expected exit 0; env={env}"); assert_eq!( env["status"], "success", "rollback --ecosystems=deno: expected success; env={env}" @@ -298,8 +295,7 @@ fn rollback_dispatch_branch_deno() { // The decisive check: the on-disk bytes are restored to ORIGINAL. let restored = std::fs::read(&verify_file).unwrap(); assert_eq!( - restored, - ORIGINAL, + restored, ORIGINAL, "rollback --ecosystems=deno: {} was not restored to its original bytes", verify_file.display() ); diff --git a/crates/socket-patch-cli/tests/covgap_output.rs b/crates/socket-patch-cli/tests/covgap_output.rs index 0ea5c8759..21121cacb 100644 --- a/crates/socket-patch-cli/tests/covgap_output.rs +++ b/crates/socket-patch-cli/tests/covgap_output.rs @@ -173,8 +173,9 @@ fn run_in_pty_inner( .expect("spawn socket-patch in PTY"); drop(pair.slave); - let reader_handle = - crate::pty_io::PtyOutput::spawn(pair.master.try_clone_reader().expect("clone reader")); + let reader_handle = crate::pty_io::PtyOutput::spawn( + pair.master.try_clone_reader().expect("clone reader"), + ); // Watchdog: detached kill after `timeout`; a no-op if the child exits // naturally first. @@ -265,10 +266,7 @@ fn remove_interactive_bare_enter_proceeds_with_default_yes() { "\n", Duration::from_secs(15), ); - assert_eq!( - code, 0, - "remove with bare Enter must succeed; got: {output}" - ); + assert_eq!(code, 0, "remove with bare Enter must succeed; got: {output}"); // The interactive confirm MUST have run — otherwise this test passes // vacuously against a regression that drops the TTY gate and // auto-proceeds. Match the distinctive prompt verbatim (the loose diff --git a/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs b/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs index a2cf92ea0..5b056b87c 100644 --- a/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs +++ b/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs @@ -126,10 +126,7 @@ fn remove_malformed_composer_json_errors_not_silent_noop() { write(&cwd.join("composer.json"), MALFORMED_COMPOSER_JSON); let (code, v) = run_json(cwd, &["setup", "--remove", "--yes", "--json"]); - assert_eq!( - code, 1, - "remove on a malformed composer.json must fail: {v}" - ); + assert_eq!(code, 1, "remove on a malformed composer.json must fail: {v}"); assert_eq!(v["status"], "error", "{v}"); assert_eq!(v["removed"], 0, "{v}"); assert_eq!(v["errors"], 1, "{v}"); diff --git a/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs b/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs index 829cad6a3..989cc3816 100644 --- a/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs +++ b/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs @@ -71,10 +71,7 @@ fn setup_remove_clears_bundler_registration_under_bundle_app_config() { &["setup", "--yes", "--json", "--ecosystems", "gem"], &[], ); - assert_eq!( - code, 0, - "gem setup must succeed.\nstdout:\n{stdout}\nstderr:\n{stderr}" - ); + assert_eq!(code, 0, "gem setup must succeed.\nstdout:\n{stdout}\nstderr:\n{stderr}"); let v = common::parse_json_envelope(&stdout); assert_eq!(v["status"], "success", "{v}"); assert!( @@ -113,14 +110,7 @@ fn setup_remove_clears_bundler_registration_under_bundle_app_config() { // Step 3: unwire with BUNDLE_APP_CONFIG set (child-only env injection). let (code, stdout, stderr) = common::run_with_env( root, - &[ - "setup", - "--remove", - "--yes", - "--json", - "--ecosystems", - "gem", - ], + &["setup", "--remove", "--yes", "--json", "--ecosystems", "gem"], &[("BUNDLE_APP_CONFIG", "bundle-config")], ); assert_eq!( diff --git a/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs b/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs index 6adf98e7a..814a55efe 100644 --- a/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs +++ b/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs @@ -45,10 +45,7 @@ fn read(path: &Path) -> String { fn write_pm_shim(bin_dir: &Path, name: &str, log: &Path) { use std::os::unix::fs::PermissionsExt; std::fs::create_dir_all(bin_dir).expect("create shim dir"); - let body = format!( - "#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n", - log.display() - ); + let body = format!("#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n", log.display()); let p = bin_dir.join(name); std::fs::write(&p, body).expect("write shim"); std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).expect("chmod shim"); @@ -83,7 +80,11 @@ fn assert_no_pm_spawned(project: &Path, context: &str) { /// through the shared hermetic runner (the seed-then-scrub of the ambient /// `SOCKET_*` surface is load-bearing: SOCKET_DRY_RUN=true would fake every /// edit, SOCKET_ECOSYSTEMS=npm would hide the Python branch entirely). -fn run_setup_with_shims(cwd: &Path, bin_dir: &Path, extra: &[&str]) -> (i32, serde_json::Value) { +fn run_setup_with_shims( + cwd: &Path, + bin_dir: &Path, + extra: &[&str], +) -> (i32, serde_json::Value) { let path_env = format!( "{}:{}", bin_dir.display(), diff --git a/crates/socket-patch-cli/tests/e2e_cargo.rs b/crates/socket-patch-cli/tests/e2e_cargo.rs index 84efdd424..b8770d4ca 100644 --- a/crates/socket-patch-cli/tests/e2e_cargo.rs +++ b/crates/socket-patch-cli/tests/e2e_cargo.rs @@ -209,7 +209,8 @@ async fn scan_discovers_fake_registry_crates() { "Expected human scan to report exactly 'Found 2 packages (2 cargo)', got:\n{combined}" ); assert!( - !combined.contains("No packages found") && !combined.contains("No packages found"), + !combined.contains("No packages found") + && !combined.contains("No packages found"), "scan reported no packages despite a populated registry:\n{combined}" ); @@ -266,7 +267,8 @@ async fn scan_discovers_vendor_crates() { "Expected human scan to report exactly 'Found 1 package (1 cargo)', got:\n{combined}" ); assert!( - !combined.contains("No packages found") && !combined.contains("No packages found"), + !combined.contains("No packages found") + && !combined.contains("No packages found"), "scan reported no packages despite a populated vendor dir:\n{combined}" ); diff --git a/crates/socket-patch-cli/tests/e2e_maven.rs b/crates/socket-patch-cli/tests/e2e_maven.rs index 996e5e855..40676a012 100644 --- a/crates/socket-patch-cli/tests/e2e_maven.rs +++ b/crates/socket-patch-cli/tests/e2e_maven.rs @@ -182,7 +182,8 @@ async fn scan_discovers_maven_artifacts() { // the word "packages", which is exactly what let the old assertion // pass when discovery was disabled. assert!( - !combined.contains("No packages found") && !combined.contains("No packages found"), + !combined.contains("No packages found") + && !combined.contains("No packages found"), "scan reported zero packages — Maven discovery did not run:\n{combined}" ); assert!( diff --git a/crates/socket-patch-cli/tests/e2e_nuget.rs b/crates/socket-patch-cli/tests/e2e_nuget.rs index 1311fcb47..e00e2e8b1 100644 --- a/crates/socket-patch-cli/tests/e2e_nuget.rs +++ b/crates/socket-patch-cli/tests/e2e_nuget.rs @@ -232,8 +232,7 @@ async fn scan_discovers_global_cache_packages() { // masked. assert!( !combined.contains("No packages found") - && !combined.contains("No packages found") - && !combined.contains("No global packages found"), + && !combined.contains("No packages found") && !combined.contains("No global packages found"), "scan failed to discover the fake global cache:\n{combined}" ); // Exactly the two packages we planted (Newtonsoft.Json, System.Text.Json), @@ -292,8 +291,7 @@ async fn scan_discovers_legacy_packages() { ); assert!( !combined.contains("No packages found") - && !combined.contains("No packages found") - && !combined.contains("No global packages found"), + && !combined.contains("No packages found") && !combined.contains("No global packages found"), "scan failed to discover the legacy packages/ layout:\n{combined}" ); // Exactly the single legacy package we planted (Newtonsoft.Json.13.0.3), diff --git a/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs b/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs index 93ee65a11..e419d8c64 100644 --- a/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs +++ b/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs @@ -508,16 +508,8 @@ fn get_help_lists_all_identifier_flags() { // parseable (scripts get that explicit error) but is not advertised. assert!(!stdout.contains("--one-off"), "{stdout}"); // Help text is for users: no implementation notes from the source. - for leak in [ - "value_parser", - "parse_bool_flag", - "No env binding", - "locally- installed", - ] { - assert!( - !stdout.contains(leak), - "get --help leaks {leak:?}: {stdout}" - ); + for leak in ["value_parser", "parse_bool_flag", "No env binding", "locally- installed"] { + assert!(!stdout.contains(leak), "get --help leaks {leak:?}: {stdout}"); } } diff --git a/crates/socket-patch-cli/tests/global_packages_e2e.rs b/crates/socket-patch-cli/tests/global_packages_e2e.rs index 67af0cc9e..087a9fd2a 100644 --- a/crates/socket-patch-cli/tests/global_packages_e2e.rs +++ b/crates/socket-patch-cli/tests/global_packages_e2e.rs @@ -211,10 +211,7 @@ fn assert_rollback_noop(stdout: &str) { r["skipped"], "package_not_installed", "a no-op rollback may carry only not-installed markers; envelope={v}" ); - assert!( - r["path"].is_null(), - "marker path must be null; envelope={v}" - ); + assert!(r["path"].is_null(), "marker path must be null; envelope={v}"); assert!( r.get("success").is_none() && r.get("error").is_none(), "markers carry no success/error keys; envelope={v}" diff --git a/crates/socket-patch-cli/tests/help_text_hygiene.rs b/crates/socket-patch-cli/tests/help_text_hygiene.rs index b3a7063a0..455ac4b39 100644 --- a/crates/socket-patch-cli/tests/help_text_hygiene.rs +++ b/crates/socket-patch-cli/tests/help_text_hygiene.rs @@ -61,11 +61,7 @@ fn every_help_page_has_no_developer_notes() { names.extend(cmd.get_subcommands().map(|s| s.get_name().to_string())); let mut failures = Vec::new(); for name in &names { - let path: Vec<&str> = if name.is_empty() { - vec![] - } else { - vec![name.as_str()] - }; + let path: Vec<&str> = if name.is_empty() { vec![] } else { vec![name.as_str()] }; let text = long_help(&path); let found = leaks(&text); if !found.is_empty() { @@ -170,10 +166,8 @@ fn vendor_and_repair_summaries_read_as_one_line() { "{text}" ); assert!( - text.lines().any(|l| { - l - == " repair Download missing patch artifacts and clean up unused ones [aliases: gc]" - }), + text.lines().any(|l| l + == " repair Download missing patch artifacts and clean up unused ones [aliases: gc]"), "{text}" ); let repair = long_help(&["repair"]); diff --git a/crates/socket-patch-cli/tests/hosted_memory_engine.rs b/crates/socket-patch-cli/tests/hosted_memory_engine.rs index a6e5e43b0..8b57dfc1a 100644 --- a/crates/socket-patch-cli/tests/hosted_memory_engine.rs +++ b/crates/socket-patch-cli/tests/hosted_memory_engine.rs @@ -963,8 +963,7 @@ async fn a_vlt_project_is_withheld_as_offline() { .and_then(|w| w["detail"].as_str()) .expect("the preflight warning is reported"); assert!( - detail.contains("/patch/npm//") - && detail.contains(": offline; nothing was written"), + detail.contains("/patch/npm//") && detail.contains(": offline; nothing was written"), "the offline refusal quotes the redacted URL" ); assert!(output.changed_files.is_empty()); diff --git a/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs b/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs index 5da588778..373455bee 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs @@ -335,15 +335,11 @@ async fn scan_redirect_vlt_artifact_fetch_error() { let detail = warning_detail(&doc, UNVERIFIABLE); let redacted = url.replace(&format!("/{TOKEN}/"), "//"); assert!( - detail.starts_with(&format!( - "vlt would fail to verify {redacted}: fetch error " - )) && detail.ends_with(&format!("; nothing was written for {PURL}")), + detail.starts_with(&format!("vlt would fail to verify {redacted}: fetch error ")) + && detail.ends_with(&format!("; nothing was written for {PURL}")), "the fetch-error refusal quotes the redacted URL" ); - assert!( - !detail.contains(TOKEN), - "the grant token never reaches the warning" - ); + assert!(!detail.contains(TOKEN), "the grant token never reaches the warning"); } async fn redirect_chain(hops: usize) -> (Value, tempfile::TempDir) { diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs b/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs index 439adfa1c..a42a8f6c9 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs @@ -109,9 +109,7 @@ async fn mock_api(server: &MockServer) { .mount(server) .await; Mock::given(method("GET")) - .and(path_regex(format!( - "^/v0/orgs/{ORG}/patches/by-package/.+$" - ))) + .and(path_regex(format!("^/v0/orgs/{ORG}/patches/by-package/.+$"))) .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "patches": [{ "uuid": UUID, "purl": RECORD_PURL, @@ -339,10 +337,8 @@ async fn lock_only_pdm_project_redirects_attests_rescans_and_rolls_back() { PYPROJECT, "pyproject untouched" ); - let ledger: serde_json::Value = serde_json::from_str(&read( - &tmp.path().join(".socket/vendor/redirect-state.json"), - )) - .unwrap(); + let ledger: serde_json::Value = + serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))).unwrap(); assert!( ledger["records"][RECORD_PURL].is_object(), "ledger keyed by the artifact-qualified purl: {ledger}" @@ -367,11 +363,7 @@ async fn lock_only_pdm_project_redirects_attests_rescans_and_rolls_back() { // 2. Idempotent re-scan: no further edits, lock byte-identical. let code = run(hosted_args(tmp.path(), server.uri(), None)).await; assert_eq!(code, 0); - assert_eq!( - read(&lock_path), - redirected, - "re-scan must not touch the lock" - ); + assert_eq!(read(&lock_path), redirected, "re-scan must not touch the lock"); // 3. The committed state, manifest-less, attests (and only while wired). assert_manifestless_vex(tmp.path(), LOCK); @@ -430,10 +422,8 @@ async fn hatchling_build_backend_does_not_veto_the_pdm_lock_redirect() { pyproject, "pyproject untouched" ); - let ledger: serde_json::Value = serde_json::from_str(&read( - &tmp.path().join(".socket/vendor/redirect-state.json"), - )) - .unwrap(); + let ledger: serde_json::Value = + serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))).unwrap(); assert!( ledger["records"][RECORD_PURL].is_object(), "the pdm redirect must be confirmed and recorded despite the hatch backend: {ledger}" @@ -453,11 +443,7 @@ async fn hatchling_build_backend_does_not_veto_the_pdm_lock_redirect() { }) .await; assert_eq!(code, 0, "rollback must succeed"); - assert_eq!( - read(&lock_path), - LOCK, - "rollback must restore the pristine lock" - ); + assert_eq!(read(&lock_path), LOCK, "rollback must restore the pristine lock"); } /// The legacy `[metadata.files]` lock (lock_version 2) redirects the package @@ -475,10 +461,8 @@ async fn legacy_metadata_files_lock_redirects_both_fragments_and_warns() { assert_eq!(code, 0); let redirected = read(&lock_path); assert!(redirected.contains(HOSTED_URL), "{redirected}"); - let ledger: serde_json::Value = serde_json::from_str(&read( - &tmp.path().join(".socket/vendor/redirect-state.json"), - )) - .unwrap(); + let ledger: serde_json::Value = + serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))).unwrap(); assert_eq!( ledger["edits"].as_array().unwrap().len(), 2, diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs b/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs index 731f48dcb..1d01bc2df 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs @@ -54,8 +54,7 @@ const MAJOR_ENV: &str = socket_patch_core::utils::pipenv::MAJOR_OVERRIDE_ENV; const LOCK: &str = include_str!("../../socket-patch-core/tests/fixtures/pipenv/2026.8.0/Pipfile.lock"); -const PIPFILE: &str = - include_str!("../../socket-patch-core/tests/fixtures/pipenv/2026.8.0/Pipfile"); +const PIPFILE: &str = include_str!("../../socket-patch-core/tests/fixtures/pipenv/2026.8.0/Pipfile"); /// The upstream and patched bytes of the record's one file, so the venv /// tests can materialize a real `Ready` (upstream) install. @@ -119,9 +118,7 @@ async fn mock_api(server: &MockServer) { .mount(server) .await; Mock::given(method("GET")) - .and(path_regex(format!( - "^/v0/orgs/{ORG}/patches/by-package/.+$" - ))) + .and(path_regex(format!("^/v0/orgs/{ORG}/patches/by-package/.+$"))) .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "patches": [{ "uuid": UUID, "purl": RECORD_PURL, @@ -311,10 +308,7 @@ async fn lock_only_pipenv_project_redirects_attests_rescans_and_rolls_back() { serde_json::json!([format!("sha256:{}", sha256())]), "{redirected}" ); - assert!( - entry.get("version").is_none() && entry.get("index").is_none(), - "{entry}" - ); + assert!(entry.get("version").is_none() && entry.get("index").is_none(), "{entry}"); assert_eq!( entry["markers"], urllib3_entry(LOCK)["markers"], @@ -322,19 +316,11 @@ async fn lock_only_pipenv_project_redirects_attests_rescans_and_rolls_back() { ); let before: serde_json::Value = serde_json::from_str(LOCK).unwrap(); let after: serde_json::Value = serde_json::from_str(&redirected).unwrap(); - assert_eq!( - after["_meta"], before["_meta"], - "the Pipfile content hash stays" - ); - assert_eq!( - read(&tmp.path().join("Pipfile")), - PIPFILE, - "Pipfile untouched" - ); - let ledger: serde_json::Value = serde_json::from_str(&read( - &tmp.path().join(".socket/vendor/redirect-state.json"), - )) - .unwrap(); + assert_eq!(after["_meta"], before["_meta"], "the Pipfile content hash stays"); + assert_eq!(read(&tmp.path().join("Pipfile")), PIPFILE, "Pipfile untouched"); + let ledger: serde_json::Value = + serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))) + .unwrap(); assert!( ledger["records"][RECORD_PURL].is_object(), "ledger keyed by the artifact-qualified purl: {ledger}" @@ -354,34 +340,17 @@ async fn lock_only_pipenv_project_redirects_attests_rescans_and_rolls_back() { let vex: serde_json::Value = serde_json::from_str(&read(&vex_path)).unwrap(); let statements = vex["statements"].as_array().expect("statements"); assert_eq!(statements.len(), 1, "{vex}"); - assert_eq!( - statements[0]["vulnerability"]["name"].as_str(), - Some(GHSA), - "{vex}" - ); - assert_eq!( - statements[0]["status"].as_str(), - Some("not_affected"), - "{vex}" - ); + assert_eq!(statements[0]["vulnerability"]["name"].as_str(), Some(GHSA), "{vex}"); + assert_eq!(statements[0]["status"].as_str(), Some("not_affected"), "{vex}"); // 2. Idempotent re-scan: no further edits, lock byte-identical. let code = run(hosted_args(tmp.path(), server.uri(), None)).await; assert_eq!(code, 0); - assert_eq!( - read(&lock_path), - redirected, - "re-scan must not touch the lock" - ); - let ledger: serde_json::Value = serde_json::from_str(&read( - &tmp.path().join(".socket/vendor/redirect-state.json"), - )) - .unwrap(); - assert_eq!( - ledger["edits"].as_array().map(Vec::len), - Some(1), - "one edit, not two" - ); + assert_eq!(read(&lock_path), redirected, "re-scan must not touch the lock"); + let ledger: serde_json::Value = + serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))) + .unwrap(); + assert_eq!(ledger["edits"].as_array().map(Vec::len), Some(1), "one edit, not two"); // Manifest-less VEX over the committed state (the depscan / CI shape). manifestless_vex(tmp.path(), "pipenv lock-only", &|p: &Path| { @@ -390,11 +359,7 @@ async fn lock_only_pipenv_project_redirects_attests_rescans_and_rolls_back() { // 3. rollback unwinds the redirect and drops the record. roll_back(tmp.path(), server.uri()).await; - assert_eq!( - read(&lock_path), - LOCK, - "rollback must restore the pristine lock byte for byte" - ); + assert_eq!(read(&lock_path), LOCK, "rollback must restore the pristine lock byte for byte"); let ledger_path = tmp.path().join(".socket/vendor/redirect-state.json"); if ledger_path.exists() { let ledger: serde_json::Value = serde_json::from_str(&read(&ledger_path)).unwrap(); @@ -427,10 +392,7 @@ async fn legacy_installer_major_selects_path_references() { "Pipenv 7–11 install `path` references: {redirected}" ); assert!(entry.get("file").is_none(), "{entry}"); - assert_eq!( - entry["hashes"], - serde_json::json!([format!("sha256:{}", sha256())]) - ); + assert_eq!(entry["hashes"], serde_json::json!([format!("sha256:{}", sha256())])); // The legacy `path` reference is discovered just like `file`. manifestless_vex(tmp.path(), "pipenv legacy path", &|p: &Path| { @@ -451,9 +413,7 @@ async fn stale_pipfile_lock_does_not_veto_the_requirements_redirect() { write_project(tmp.path()); // The Pipfile.lock left behind pins a DIFFERENT package; the project // installs from requirements.txt. - let stale = LOCK - .replace("\"urllib3\"", "\"six\"") - .replace("==1.26.18", "==1.16.0"); + let stale = LOCK.replace("\"urllib3\"", "\"six\"").replace("==1.26.18", "==1.16.0"); std::fs::write(tmp.path().join("Pipfile.lock"), &stale).unwrap(); std::fs::write(tmp.path().join("requirements.txt"), "urllib3==1.26.18\n").unwrap(); @@ -501,27 +461,16 @@ async fn warm_venv_with_the_upstream_release_is_not_attested() { // attested and the embedded-VEX contract fails the command. let code = run(hosted_args(tmp.path(), server.uri(), Some(&vex_path))).await; let redirected = read(&lock_path); - assert!( - redirected.contains(HOSTED_URL), - "the lock is still repointed: {redirected}" - ); + assert!(redirected.contains(HOSTED_URL), "the lock is still repointed: {redirected}"); let attested = vex_path .exists() .then(|| serde_json::from_str::(&read(&vex_path)).unwrap()) .and_then(|v| v["statements"].as_array().map(Vec::len)) .unwrap_or(0); - assert_eq!( - attested, 0, - "a stale install must not be attested from the ledger" - ); + assert_eq!(attested, 0, "a stale install must not be attested from the ledger"); assert_ne!(code, 0, "nothing to attest fails the embedded-VEX run"); assert_eq!( - std::fs::read( - site_packages(tmp.path()) - .join("urllib3") - .join("response.py") - ) - .unwrap(), + std::fs::read(site_packages(tmp.path()).join("urllib3").join("response.py")).unwrap(), UPSTREAM, "the probe is read-only" ); diff --git a/crates/socket-patch-cli/tests/in_process_rollback_hosted/vlt.rs b/crates/socket-patch-cli/tests/in_process_rollback_hosted/vlt.rs index ba5deeccd..7075ba262 100644 --- a/crates/socket-patch-cli/tests/in_process_rollback_hosted/vlt.rs +++ b/crates/socket-patch-cli/tests/in_process_rollback_hosted/vlt.rs @@ -509,10 +509,8 @@ async fn vlt_heal_follows_the_vlt_group_when_another_group_refuses() { let (_, doc) = scan_hosted(root, &server, &["--no-npm-allow-remote-config"], &[]); assert_eq!(redirected(&doc), 1, "the scan redirects both locks"); vlt_install_patched(root, &server); - let drifted = read(root, "package-lock.json").replace( - &artifact_url(&server), - "https://example.invalid/left-pad-1.3.0.tgz", - ); + let drifted = read(root, "package-lock.json") + .replace(&artifact_url(&server), "https://example.invalid/left-pad-1.3.0.tgz"); std::fs::write(root.join("package-lock.json"), &drifted).unwrap(); let cwd = root.to_str().unwrap().to_string(); @@ -528,18 +526,10 @@ async fn vlt_heal_follows_the_vlt_group_when_another_group_refuses() { vlt_lock(Era::V1, &[registry_node(TILDE_ID)]), "the vlt group restored the registry pins" ); - assert_eq!( - read(root, "package-lock.json"), - drifted, - "the refused group wrote nothing" - ); + assert_eq!(read(root, "package-lock.json"), drifted, "the refused group wrote nothing"); assert!( !store_dir(root, TILDE_ID).exists(), "the patched store copy is removed for the restored pins" ); - assert_eq!( - advisory_details(&doc), - [RESTORED], - "the heal advisory is reported" - ); + assert_eq!(advisory_details(&doc), [RESTORED], "the heal advisory is reported"); } diff --git a/crates/socket-patch-cli/tests/interactive_prompts_e2e.rs b/crates/socket-patch-cli/tests/interactive_prompts_e2e.rs index 2889c83d2..83c26d676 100644 --- a/crates/socket-patch-cli/tests/interactive_prompts_e2e.rs +++ b/crates/socket-patch-cli/tests/interactive_prompts_e2e.rs @@ -114,8 +114,9 @@ fn run_in_pty_bytes(args: &[&str], cwd: &Path, input: &[u8], timeout: Duration) // closed. The previous design used a chunked read+mpsc loop // because it interleaved with a try_wait poll; the simplified // design serializes wait → drop master → read_to_end joins. - let reader_handle = - crate::pty_io::PtyOutput::spawn(pair.master.try_clone_reader().expect("clone reader")); + let reader_handle = crate::pty_io::PtyOutput::spawn( + pair.master.try_clone_reader().expect("clone reader"), + ); // Watchdog: detach a thread that kills the child after `timeout`. // The cloned ChildKiller is independent of the main `child` diff --git a/crates/socket-patch-cli/tests/rollback_duality_invariants.rs b/crates/socket-patch-cli/tests/rollback_duality_invariants.rs index 31f457502..d4830cbd9 100644 --- a/crates/socket-patch-cli/tests/rollback_duality_invariants.rs +++ b/crates/socket-patch-cli/tests/rollback_duality_invariants.rs @@ -533,7 +533,8 @@ fn bare_word_target_stays_identifier_error() { )], false, ); - let manifest_before = std::fs::read(socket.join("manifest.json")).expect("read manifest bytes"); + let manifest_before = + std::fs::read(socket.join("manifest.json")).expect("read manifest bytes"); let (code, stdout, stderr) = run(tmp.path(), &["--offline", "lodash"]); assert_eq!( diff --git a/crates/socket-patch-cli/tests/scan_vendor_e2e.rs b/crates/socket-patch-cli/tests/scan_vendor_e2e.rs index b75c2420a..cba197205 100644 --- a/crates/socket-patch-cli/tests/scan_vendor_e2e.rs +++ b/crates/socket-patch-cli/tests/scan_vendor_e2e.rs @@ -3078,11 +3078,7 @@ snapshots: "{v}" ); assert!(events_for(&v, CARGO_SCOPE[0].0).is_empty(), "{v}"); - assert_eq!( - record_for(dl, CARGO_SCOPE[1].0)["action"], - "downloaded", - "{v}" - ); + assert_eq!(record_for(dl, CARGO_SCOPE[1].0)["action"], "downloaded", "{v}"); assert_eq!( events_for(&v, CARGO_SCOPE[1].0), vec![("skipped", "package_not_installed")], diff --git a/crates/socket-patch-cli/tests/self_update_channels_e2e.rs b/crates/socket-patch-cli/tests/self_update_channels_e2e.rs index 04310864f..a13fb56f6 100644 --- a/crates/socket-patch-cli/tests/self_update_channels_e2e.rs +++ b/crates/socket-patch-cli/tests/self_update_channels_e2e.rs @@ -55,10 +55,7 @@ async fn npm_project_local_refuses_with_local_hint() { "a project install must get the in-project upgrade command: {stderr}" ); assert!(!stderr.contains("npm update -g"), "{stderr}"); - assert!( - stderr.starts_with("Error: This socket-patch binary ("), - "{stderr}" - ); + assert!(stderr.starts_with("Error: This socket-patch binary ("), "{stderr}"); } /// An npm-bundled binary (any `node_modules` component) refuses with the diff --git a/crates/socket-patch-cli/tests/vendor_rerun_no_network_e2e.rs b/crates/socket-patch-cli/tests/vendor_rerun_no_network_e2e.rs index ac558a164..10efe03bd 100644 --- a/crates/socket-patch-cli/tests/vendor_rerun_no_network_e2e.rs +++ b/crates/socket-patch-cli/tests/vendor_rerun_no_network_e2e.rs @@ -1175,7 +1175,8 @@ fn a_package_absent_from_the_lock_keeps_the_not_installed_skip() { b"after\n", ); let home = cargo_home.to_string_lossy().into_owned(); - let (_code, v, stderr) = run_vendor(&root, &dead, &[], &[("CARGO_HOME", home.as_str())]); + let (_code, v, stderr) = + run_vendor(&root, &dead, &[], &[("CARGO_HOME", home.as_str())]); assert_eq!( purl_events(&v, purl), vec![("skipped", "package_not_installed")], @@ -1222,10 +1223,7 @@ fn a_stale_prestage_tree_is_swept_by_the_next_wet_run_only() { let (_code, v, stderr) = run_vendor(root, &dead, &["--dry-run"], &[]); for dir in &litter { - assert!( - root.join(dir).exists(), - "a dry run deletes nothing: {dir}\n{v:#}\n{stderr}" - ); + assert!(root.join(dir).exists(), "a dry run deletes nothing: {dir}\n{v:#}\n{stderr}"); } assert!( !v.to_string().contains("socket-prestage"), @@ -1235,10 +1233,7 @@ fn a_stale_prestage_tree_is_swept_by_the_next_wet_run_only() { for extra in [&["--offline"][..], &[][..]] { let (_code, v, stderr) = run_vendor(root, &dead, extra, &[]); for dir in &litter { - assert!( - !root.join(dir).exists(), - "{extra:?} sweeps {dir}\n{v:#}\n{stderr}" - ); + assert!(!root.join(dir).exists(), "{extra:?} sweeps {dir}\n{v:#}\n{stderr}"); } assert!( !root.join(format!(".socket/vendor/composer/{OLD}")).exists(), diff --git a/crates/socket-patch-core/src/crawlers/npm_crawler.rs b/crates/socket-patch-core/src/crawlers/npm_crawler.rs index 8dad88525..e9094e9f1 100644 --- a/crates/socket-patch-core/src/crawlers/npm_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/npm_crawler.rs @@ -961,11 +961,7 @@ impl NpmCrawler { /// Inside a store entry (`store_entry`) a link is a dependency edge into /// a sibling entry, whose own visit records that copy, so only a real /// directory there matches. - fn visit_resolver_dir( - nm_path: PathBuf, - store_entry: bool, - pending: &[Target], - ) -> ResolverVisit { + fn visit_resolver_dir(nm_path: PathBuf, store_entry: bool, pending: &[Target]) -> ResolverVisit { let listing = list_dir_sync(&nm_path); let probe_filter = ProbeFilter::new(&listing); let matched = pending diff --git a/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs b/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs index 2d6e225c1..d71a02127 100644 --- a/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs +++ b/crates/socket-patch-core/src/crawlers/npm_crawler/oracle.rs @@ -9,9 +9,9 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use super::{ - build_npm_purl, is_legacy_pnpm_store_dir_name, is_safe_npm_component, parse_package_name, - read_package_json, NpmCrawler, StoreEntry, Target, NESTED_STORE_MAX_DEPTH, - NESTED_STORE_MAX_DIRS, SKIP_DIRS, VLT_STORE_NAME, + build_npm_purl, is_legacy_pnpm_store_dir_name, + is_safe_npm_component, parse_package_name, read_package_json, NpmCrawler, StoreEntry, + Target, NESTED_STORE_MAX_DEPTH, NESTED_STORE_MAX_DIRS, SKIP_DIRS, VLT_STORE_NAME, }; use crate::crawlers::types::{CrawledPackage, CrawlerOptions}; use crate::utils::fs::is_dir; diff --git a/crates/socket-patch-core/src/crawlers/python_crawler.rs b/crates/socket-patch-core/src/crawlers/python_crawler.rs index 93fee481b..001ed5c17 100644 --- a/crates/socket-patch-core/src/crawlers/python_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/python_crawler.rs @@ -1090,8 +1090,10 @@ pub async fn get_global_python_site_packages() -> Vec { } // 1. Ask Python for site-packages (subprocesses: on the blocking pool) - let site_output = - run_blocking(|| SITE_QUERY_MEMO.get_or_run(site_query_key(), run_site_query)).await; + let site_output = run_blocking(|| { + SITE_QUERY_MEMO.get_or_run(site_query_key(), run_site_query) + }) + .await; if let Some(stdout) = site_output { for p in parse_python_site_packages_output(&stdout) { add_path(p, &mut seen, &mut results); diff --git a/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs b/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs index cbce379fe..ce3a28798 100644 --- a/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs +++ b/crates/socket-patch-core/src/patch/redirect/group_equivalence_tests.rs @@ -131,12 +131,11 @@ fn assert_same_with_metadata( bun_lockb_present, python_metadata, ); - let merged = merge_group_outputs(&prefix, run_groups_concurrently(&prefix, &groups)).map( - |mut merged| { + let merged = merge_group_outputs(&prefix, run_groups_concurrently(&prefix, &groups)) + .map(|mut merged| { merged.vlt_drives = vlt::vlt_drives(files, bun_lockb_present); merged - }, - ); + }); assert_eq!( merged.as_ref(), Some(&want), diff --git a/crates/socket-patch-core/src/patch/redirect/mod.rs b/crates/socket-patch-core/src/patch/redirect/mod.rs index 57a34c822..ce560a891 100644 --- a/crates/socket-patch-core/src/patch/redirect/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/mod.rs @@ -11800,19 +11800,11 @@ mod tests { let redacted = format!( "https://patch.socket.dev/patch/npm/left-pad/1.3.0//{uuid}/left-pad-1.3.0.tgz?x=1" ); - assert_eq!( - redact_grant_token(&url, &url, uuid), - redacted, - "the URL alone" - ); + assert_eq!(redact_grant_token(&url, &url, uuid), redacted, "the URL alone"); let text = format!("vlt would fail to verify {url}: fetch error GET {url}: reset"); - let want = - format!("vlt would fail to verify {redacted}: fetch error GET {redacted}: reset"); + let want = format!("vlt would fail to verify {redacted}: fetch error GET {redacted}: reset"); assert_eq!(redact_grant_token(&text, &url, uuid), want, "every quote"); - assert!( - !redact_grant_token(&text, &url, uuid).contains(token), - "no token left" - ); + assert!(!redact_grant_token(&text, &url, uuid).contains(token), "no token left"); let registry = format!("https://patch.socket.dev/patch-registry/npm/{token}/{uuid}"); assert_eq!( redact_grant_token(®istry, ®istry, uuid), diff --git a/crates/socket-patch-core/src/patch/redirect/pdm.rs b/crates/socket-patch-core/src/patch/redirect/pdm.rs index e8b441454..51cab65da 100644 --- a/crates/socket-patch-core/src/patch/redirect/pdm.rs +++ b/crates/socket-patch-core/src/patch/redirect/pdm.rs @@ -278,18 +278,9 @@ mod tests { #[test] fn legacy_formats_warn_stale_install_risk_once() { for (fixture, warns) in [ - ( - include_str!("../../../tests/fixtures/pdm-native/0.12.3.lock"), - true, - ), - ( - include_str!("../../../tests/fixtures/pdm-native/2.8.2.lock"), - true, - ), - ( - include_str!("../../../tests/fixtures/pdm-native/2.29.2.lock"), - false, - ), + (include_str!("../../../tests/fixtures/pdm-native/0.12.3.lock"), true), + (include_str!("../../../tests/fixtures/pdm-native/2.8.2.lock"), true), + (include_str!("../../../tests/fixtures/pdm-native/2.29.2.lock"), false), ] { let mut result = RewriteResult::default(); rewrite( diff --git a/crates/socket-patch-core/src/patch/redirect/pipenv.rs b/crates/socket-patch-core/src/patch/redirect/pipenv.rs index d593dafe1..ef62dce7e 100644 --- a/crates/socket-patch-core/src/patch/redirect/pipenv.rs +++ b/crates/socket-patch-core/src/patch/redirect/pipenv.rs @@ -555,10 +555,7 @@ mod tests { let original = serde_json::to_string(&value).unwrap(); // A live lock (Pipfile beside it): conflicts veto the siblings. let files = BTreeMap::from([ - ( - "Pipfile".to_string(), - "[packages]\nurllib3 = \"*\"\n".to_string(), - ), + ("Pipfile".to_string(), "[packages]\nurllib3 = \"*\"\n".to_string()), ("Pipfile.lock".to_string(), original), ]); let mut result = RewriteResult::default(); @@ -598,30 +595,20 @@ mod tests { for stale in &stale_locks { let files = BTreeMap::from([ ("Pipfile.lock".to_string(), stale.clone()), - ( - "requirements.txt".to_string(), - "urllib3==1.26.18\n".to_string(), - ), + ("requirements.txt".to_string(), "urllib3==1.26.18\n".to_string()), ]); - let result = - super::super::rewrite_registry_redirect(&files, std::slice::from_ref(&dep)); + let result = super::super::rewrite_registry_redirect(&files, std::slice::from_ref(&dep)); assert!( !result.refused_pipenv_uuids.contains("patch-one"), "a non-conflict must not veto: {stale}" ); assert!( - result - .warnings - .iter() - .any(|w| w.code == "redirect_pipenv_skipped"), + result.warnings.iter().any(|w| w.code == "redirect_pipenv_skipped"), "{:?}", result.warnings ); assert!( - result - .files - .get("requirements.txt") - .is_some_and(|t| t.contains("patch.socket.dev")), + result.files.get("requirements.txt").is_some_and(|t| t.contains("patch.socket.dev")), "requirements.txt must still be redirected past a stale Pipfile.lock: {result:?}" ); assert!(!result.files.contains_key("Pipfile.lock")); @@ -706,18 +693,10 @@ mod tests { ); let foreign = redirected.replacen( redirected_entry, - &format_entry( - &json!({"file": "https://example.org/fork.whl"}), - &redirected, - 0, - ) - .unwrap(), + &format_entry(&json!({"file": "https://example.org/fork.whl"}), &redirected, 0).unwrap(), 1, ); - assert!( - restore(&foreign, &edits[0]).is_err(), - "a foreign reference is drift" - ); + assert!(restore(&foreign, &edits[0]).is_err(), "a foreign reference is drift"); // Re-scan after the relock, then roll back newest-first. let (again, second) = plan(&relocked, &dep, None).unwrap(); @@ -735,10 +714,7 @@ mod tests { let files = |text: &str| BTreeMap::from([("Pipfile.lock".to_string(), text.to_string())]); assert!(lock_targets(&files(&lock()), std::slice::from_ref(&dep))); assert!(!lock_targets(&files(&lock()), std::slice::from_ref(&other))); - assert!(!lock_targets( - &files("{ not json"), - std::slice::from_ref(&dep) - )); + assert!(!lock_targets(&files("{ not json"), std::slice::from_ref(&dep))); assert!(!lock_targets(&BTreeMap::new(), std::slice::from_ref(&dep))); let mut npm = dep.clone(); npm.ecosystem = "npm".into(); @@ -763,10 +739,7 @@ mod tests { let entry: Value = serde_json::from_str(&fixed).unwrap(); assert!(entry["default"]["urllib3"].get("version").is_none()); assert!(entry["default"]["urllib3"].get("index").is_none()); - assert!(entry["default"]["urllib3"]["file"] - .as_str() - .unwrap() - .contains("patch-one")); + assert!(entry["default"]["urllib3"]["file"].as_str().unwrap().contains("patch-one")); value["default"]["urllib3"]["version"] = json!("==2.0.0"); let conflicting = serde_json::to_string(&value).unwrap(); @@ -787,10 +760,7 @@ mod tests { assert!(owned_url(public, &dep)); assert!(!owned_url("https://example.org/patch/pypi/urllib3/1.26.18/tok/patch-one/urllib3-1.26.18-py3-none-any.whl", &dep)); dep.artifact_url = "https://patches.internal.example:8443/patch/pypi/urllib3/1.26.18/tok/patch-one/urllib3-1.26.18-py3-none-any.whl".into(); - assert!( - owned_url(&dep.artifact_url, &dep), - "the grant's own origin is ours" - ); + assert!(owned_url(&dep.artifact_url, &dep), "the grant's own origin is ours"); assert!(owned_url(public, &dep), "and so is the public service"); assert!(!owned_url("https://patches.internal.example:8443/patch/pypi/urllib3/1.26.19/tok/patch-one/urllib3-1.26.19-py3-none-any.whl", &dep), "another version is not"); // Rotation on the custom origin restores through the chain. @@ -809,12 +779,7 @@ mod tests { fn restore_refuses_a_non_object_ledger_original() { let dep = dependency("urllib3", "1.26.18", "patch-one"); let (text, edits) = plan(&lock(), &dep, None).unwrap(); - for bad in [ - "\"just a string\"", - "[1, 2]", - "not json at all", - "{\"a\": 1}, \"injected\": {}", - ] { + for bad in ["\"just a string\"", "[1, 2]", "not json at all", "{\"a\": 1}, \"injected\": {}"] { let mut edit = edits[0].clone(); edit.original = Some(Value::String(bad.to_string())); assert!(restore(&text, &edit).is_err(), "{bad}"); @@ -848,28 +813,18 @@ mod tests { for edit in &first_edits { let replacement = edit.new.as_ref().unwrap().as_str().unwrap(); // A tampered reference (its `#sha256=` pin) is drift… - let drift = two.replacen( - replacement, - &replacement.replace("#sha256=", "#sha256=0"), - 1, - ); + let drift = two.replacen(replacement, &replacement.replace("#sha256=", "#sha256=0"), 1); assert!(restore(&drift, edit).is_err()); // …while a re-serialized entry that kept our reference (Pipenv // 2023+ relocking a marker-excluded entry restores the registry // `hashes` and `version` next to it) is still ours and restores. let mut value: Value = serde_json::from_str(&two).unwrap(); - let section: &str = serde_json::from_str::<[String; 2]>(edit.key.as_deref().unwrap()) - .unwrap()[0] - .clone() - .leak(); + let section: &str = serde_json::from_str::<[String; 2]>(edit.key.as_deref().unwrap()).unwrap()[0].clone().leak(); value[section]["urllib3"]["hashes"] = json!(["sha256:upstream-a", "sha256:upstream-b"]); value[section]["urllib3"]["version"] = json!("==1.26.18"); let kept = serde_json::to_string_pretty(&value).unwrap(); let restored: Value = serde_json::from_str(&restore(&kept, edit).unwrap()).unwrap(); - assert!( - restored[section]["urllib3"].get("file").is_none(), - "{restored}" - ); + assert!(restored[section]["urllib3"].get("file").is_none(), "{restored}"); let mut unsafe_edit = edit.clone(); unsafe_edit.path = "../Pipfile.lock".into(); assert!(restore(&two, &unsafe_edit).is_err()); @@ -928,10 +883,7 @@ mod compatibility_tests { assert!(!result.refused_pipenv_uuids.contains("patch-one")); assert!(result.files["requirements.txt"].contains("patch.socket.dev")); assert!(!result.files.contains_key("Pipfile.lock")); - assert!(result - .warnings - .iter() - .any(|w| w.code == "redirect_pipenv_refused" && w.detail.contains("no Pipfile"))); + assert!(result.warnings.iter().any(|w| w.code == "redirect_pipenv_refused" && w.detail.contains("no Pipfile"))); } /// Rollback survives what git and Pipenv do to the lock between the @@ -963,17 +915,11 @@ mod compatibility_tests { value["default"]["urllib3"]["version"] = json!("==1.26.18"); value["default"]["urllib3"]["index"] = json!("pypi"); let hybrid = serde_json::to_string_pretty(&value).unwrap(); - let default_edit = edits - .iter() - .find(|e| e.key.as_deref() == Some(r#"["default","urllib3"]"#)) - .unwrap(); + let default_edit = edits.iter().find(|e| e.key.as_deref() == Some(r#"["default","urllib3"]"#)).unwrap(); let restored = restore(&hybrid, default_edit).unwrap(); let value: Value = serde_json::from_str(&restored).unwrap(); assert_eq!(value["default"]["urllib3"]["version"], json!("==1.26.18")); - assert!( - value["default"]["urllib3"].get("file").is_none(), - "{restored}" - ); + assert!(value["default"]["urllib3"].get("file").is_none(), "{restored}"); // Dropped entry (`pipenv uninstall`): nothing to unwind, retires. let mut value: Value = serde_json::from_str(&redirected).unwrap(); value["default"].as_object_mut().unwrap().remove("urllib3"); diff --git a/crates/socket-patch-core/src/patch/redirect/poetry.rs b/crates/socket-patch-core/src/patch/redirect/poetry.rs index eacc889a9..a2798cd69 100644 --- a/crates/socket-patch-core/src/patch/redirect/poetry.rs +++ b/crates/socket-patch-core/src/patch/redirect/poetry.rs @@ -92,9 +92,7 @@ pub(super) fn rewrite_poetry( } } Err(detail) => { - result - .refused_python_lock_uuids - .insert(dep.patch_uuid.clone()); + result.refused_python_lock_uuids.insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_poetry_lock_unsupported".into(), detail: format!("{path}: {detail}"), @@ -102,9 +100,7 @@ pub(super) fn rewrite_poetry( continue; } } - result - .confirmed_python_lock_uuids - .insert(dep.patch_uuid.clone()); + result.confirmed_python_lock_uuids.insert(dep.patch_uuid.clone()); content = rewrite.text; if !stale_warned { if let Some(format) = @@ -140,18 +136,14 @@ pub(super) fn rewrite_poetry( } // Already redirected to this artifact (idempotent re-scan). Ok(Some(_)) => { - result - .confirmed_python_lock_uuids - .insert(dep.patch_uuid.clone()); + result.confirmed_python_lock_uuids.insert(dep.patch_uuid.clone()); } Ok(None) => result.warnings.push(RewriteWarning { code: "redirect_poetry_entry_not_found".into(), detail: format!("no {path} entry for {}@{}", dep.name, dep.version), }), Err(detail) => { - result - .refused_python_lock_uuids - .insert(dep.patch_uuid.clone()); + result.refused_python_lock_uuids.insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_poetry_lock_unsupported".into(), detail: format!("{path}: {detail}"), @@ -223,9 +215,7 @@ fn rewrite_poetry_reference( } } Err(detail) => { - result - .refused_python_lock_uuids - .insert(dep.patch_uuid.clone()); + result.refused_python_lock_uuids.insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_poetry_lock_unsupported".into(), detail: format!("{path}: {detail}"), @@ -233,9 +223,7 @@ fn rewrite_poetry_reference( continue; } } - result - .confirmed_python_lock_uuids - .insert(dep.patch_uuid.clone()); + result.confirmed_python_lock_uuids.insert(dep.patch_uuid.clone()); content = rewritten; if !stale_warned { if let Some(format) = pre_1_4_writer(&content) { @@ -269,18 +257,14 @@ fn rewrite_poetry_reference( } // Already redirected to this artifact (idempotent re-scan). Ok(Some(_)) => { - result - .confirmed_python_lock_uuids - .insert(dep.patch_uuid.clone()); + result.confirmed_python_lock_uuids.insert(dep.patch_uuid.clone()); } Ok(None) => result.warnings.push(RewriteWarning { code: "redirect_poetry_entry_not_found".into(), detail: format!("no {path} entry for {}@{}", dep.name, dep.version), }), Err(detail) => { - result - .refused_python_lock_uuids - .insert(dep.patch_uuid.clone()); + result.refused_python_lock_uuids.insert(dep.patch_uuid.clone()); result.warnings.push(RewriteWarning { code: "redirect_poetry_lock_unsupported".into(), detail: format!("{path}: {detail}"), diff --git a/crates/socket-patch-core/src/update/download.rs b/crates/socket-patch-core/src/update/download.rs index be1476b19..f176426ce 100644 --- a/crates/socket-patch-core/src/update/download.rs +++ b/crates/socket-patch-core/src/update/download.rs @@ -741,10 +741,7 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let missing = tmp.path().join("never-existed"); sweep_stale_stages(&missing); - assert!( - !missing.exists(), - "sweep must not create the destination dir" - ); + assert!(!missing.exists(), "sweep must not create the destination dir"); } /// A write failure AFTER a successful open (EFBIG here, standing in @@ -760,7 +757,8 @@ mod tests { #[test] fn stage_write_failure_cleans_up_stage_file() { const CHILD_ENV: &str = "SOCKET_PATCH_CORE_TEST_STAGE_FSIZE_CHILD"; - const TEST_NAME: &str = "update::download::tests::stage_write_failure_cleans_up_stage_file"; + const TEST_NAME: &str = + "update::download::tests::stage_write_failure_cleans_up_stage_file"; if std::env::var_os(CHILD_ENV).is_none() { let exe = std::env::current_exe().expect("test binary path must resolve"); let output = std::process::Command::new(exe) @@ -826,10 +824,7 @@ mod tests { matches!(err, UpdateError::SwapFailed(_)), "expected SwapFailed, got: {err}" ); - assert!( - err.to_string().contains("error writing staged binary"), - "{err}" - ); + assert!(err.to_string().contains("error writing staged binary"), "{err}"); let leftovers: Vec = std::fs::read_dir(tmp.path()) .unwrap() .map(|e| e.unwrap().file_name().to_string_lossy().into_owned()) diff --git a/crates/socket-patch-core/src/update/release.rs b/crates/socket-patch-core/src/update/release.rs index 7c3b04c29..5b2869012 100644 --- a/crates/socket-patch-core/src/update/release.rs +++ b/crates/socket-patch-core/src/update/release.rs @@ -751,11 +751,9 @@ mod tests { .mount(&server) .await; - let client = metadata_client( - &short_timeouts(), - follow_redirect_policy(&default_endpoints()), - ) - .unwrap(); + let client = + metadata_client(&short_timeouts(), follow_redirect_policy(&default_endpoints())) + .unwrap(); let err = client .get(format!("{}/start", server.uri())) .send() @@ -788,11 +786,9 @@ mod tests { .mount(&server) .await; - let client = metadata_client( - &short_timeouts(), - follow_redirect_policy(&default_endpoints()), - ) - .unwrap(); + let client = + metadata_client(&short_timeouts(), follow_redirect_policy(&default_endpoints())) + .unwrap(); let err = client .get(format!("{}/start", server.uri())) .send() @@ -868,10 +864,7 @@ mod tests { .unwrap_err(); assert!(matches!(err, UpdateError::CheckFailed(_)), "{err:?}"); let msg = err.to_string(); - assert!( - msg.contains("expected a redirect to the latest tag"), - "{msg}" - ); + assert!(msg.contains("expected a redirect to the latest tag"), "{msg}"); assert!(msg.contains("API fallback:"), "{msg}"); assert!(msg.contains("returned 500"), "{msg}"); } @@ -952,14 +945,8 @@ mod tests { #[test] fn url_host_keeps_explicit_ports() { - assert_eq!( - url_host("http://127.0.0.1:9/x").as_deref(), - Some("127.0.0.1:9") - ); - assert_eq!( - url_host("https://github.com/a").as_deref(), - Some("github.com") - ); + assert_eq!(url_host("http://127.0.0.1:9/x").as_deref(), Some("127.0.0.1:9")); + assert_eq!(url_host("https://github.com/a").as_deref(), Some("github.com")); assert_eq!(url_host("not a url"), None); } @@ -972,9 +959,7 @@ mod tests { // code stays `check_failed` (stable contract). let server = MockServer::start().await; Mock::given(method("GET")) - .and(path( - "/SocketDev/socket-patch/releases/download/v1.2.3/SHA256SUMS", - )) + .and(path("/SocketDev/socket-patch/releases/download/v1.2.3/SHA256SUMS")) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; @@ -1007,9 +992,7 @@ mod tests { // silently. let server = MockServer::start().await; Mock::given(method("GET")) - .and(path( - "/SocketDev/socket-patch/releases/download/v1.2.3/SHA256SUMS", - )) + .and(path("/SocketDev/socket-patch/releases/download/v1.2.3/SHA256SUMS")) .respond_with(ResponseTemplate::new(500)) .mount(&server) .await; diff --git a/crates/socket-patch-core/src/utils/group_commit.rs b/crates/socket-patch-core/src/utils/group_commit.rs index 255b0ef5a..72cc1fad5 100644 --- a/crates/socket-patch-core/src/utils/group_commit.rs +++ b/crates/socket-patch-core/src/utils/group_commit.rs @@ -297,9 +297,9 @@ where // write the lock edits beside the pre-run ledger. Put the caller's value // back before the unwind continues — the same value a caught-and- // continued caller holds. - if let Err(panic) = - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| edit(Arc::make_mut(value)))) - { + if let Err(panic) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + edit(Arc::make_mut(value)) + })) { files.insert(key, captured(value)); drop(files); std::panic::resume_unwind(panic); @@ -1596,10 +1596,7 @@ mod tests { .unwrap(); remove_dir_after_commit(&dir).await; drop(dropped); - assert!( - dir.join("config.toml").exists(), - "an abandoned commit removes nothing" - ); + assert!(dir.join("config.toml").exists(), "an abandoned commit removes nothing"); let group = GroupCommit::begin(root); super::super::fs::remove_file(&dir.join("config.toml")) @@ -1608,10 +1605,7 @@ mod tests { remove_dir_after_commit(&dir).await; assert!(dir.join("config.toml").exists(), "captured, still on disk"); group.commit().await.unwrap(); - assert!( - !dir.exists(), - "the emptied directory is removed after the commit" - ); + assert!(!dir.exists(), "the emptied directory is removed after the commit"); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("config.toml"), b"[patch]\n").unwrap(); @@ -1623,10 +1617,7 @@ mod tests { remove_dir_after_commit(&dir).await; group.commit().await.unwrap(); assert!(!dir.join("config.toml").exists()); - assert!( - dir.join("credentials.toml").exists(), - "a non-empty directory is kept" - ); + assert!(dir.join("credentials.toml").exists(), "a non-empty directory is kept"); remove_dir_after_commit(&root.join("gone")).await; std::fs::remove_file(dir.join("credentials.toml")).unwrap(); diff --git a/crates/socket-patch-core/src/utils/mod.rs b/crates/socket-patch-core/src/utils/mod.rs index a79fdc6da..e13c251d7 100644 --- a/crates/socket-patch-core/src/utils/mod.rs +++ b/crates/socket-patch-core/src/utils/mod.rs @@ -7,9 +7,9 @@ pub mod env_compat; pub mod failpoint; pub mod fs; pub mod group_commit; +pub mod notice; pub(crate) mod http; pub(crate) mod line_endings; -pub mod notice; pub mod pdm_lock; pub mod pipenv; pub mod poetry_lock; diff --git a/crates/socket-patch-core/src/utils/pdm_lock.rs b/crates/socket-patch-core/src/utils/pdm_lock.rs index 65b54065b..20eee27ce 100644 --- a/crates/socket-patch-core/src/utils/pdm_lock.rs +++ b/crates/socket-patch-core/src/utils/pdm_lock.rs @@ -358,11 +358,9 @@ fn plan_pdm_rewrite( .filter_map(|&index| packages.get(index)?.get("version").and_then(Item::as_str)) .collect(); if locked_versions.len() > 1 { - return Err( - "PDM lock resolves this package at multiple versions (a marker or \ + return Err("PDM lock resolves this package at multiple versions (a marker or \ multi-target fork); patching one fork would leave the others unpatched" - .into(), - ); + .into()); } let mut variants = std::collections::BTreeSet::new(); let mut edits = Vec::new(); @@ -777,10 +775,7 @@ mod tests { &"a".repeat(64), ) .unwrap(); - assert!( - rewired.contains(&fresh) && !rewired.contains(&stale), - "{rewired}" - ); + assert!(rewired.contains(&fresh) && !rewired.contains(&stale), "{rewired}"); // A foreign (non-Socket) existing url is still refused. let foreign = fixture("2.29.2").replace( "name = \"urllib3\"", diff --git a/crates/socket-patch-core/src/utils/poetry_lock.rs b/crates/socket-patch-core/src/utils/poetry_lock.rs index a191e5214..ceb7726c4 100644 --- a/crates/socket-patch-core/src/utils/poetry_lock.rs +++ b/crates/socket-patch-core/src/utils/poetry_lock.rs @@ -71,10 +71,7 @@ fn lock_version_of(lock: &Table) -> Result<&str, String> { { Ok("0") } - None => Err( - "poetry.lock has neither a [metadata] lock-version nor a [metadata.hashes] table" - .into(), - ), + None => Err("poetry.lock has neither a [metadata] lock-version nor a [metadata.hashes] table".into()), } } @@ -633,15 +630,7 @@ mod tests { Ok(other) => panic!("{label}: expected a refusal, got {other:?}"), } // The vendored (file-source) spelling takes the same guarded path. - match rewrite_poetry_lock( - &text, - "urllib3", - "1.26.18", - "file", - ".socket/vendor/pypi/x/urllib3-1.26.18-py2.py3-none-any.whl", - WHEEL, - &sha(), - ) { + match rewrite_poetry_lock(&text, "urllib3", "1.26.18", "file", ".socket/vendor/pypi/x/urllib3-1.26.18-py2.py3-none-any.whl", WHEEL, &sha()) { Err(err) => assert!(!err.is_empty(), "{label}"), Ok(other) => panic!("{label}: expected a refusal, got {other:?}"), } @@ -659,10 +648,7 @@ mod tests { assert!(rewritten.contains(URL)); assert!(rewritten.contains("lock-version = \"2.2\"")); for bad in ["3.0", "2", "2.x", "1.2"] { - let lock = fixture("2.4.3").replace( - "lock-version = \"2.1\"", - &format!("lock-version = \"{bad}\""), - ); + let lock = fixture("2.4.3").replace("lock-version = \"2.1\"", &format!("lock-version = \"{bad}\"")); let err = hosted(&lock).unwrap_err(); assert!(err.contains(bad), "{bad}: {err}"); } @@ -685,47 +671,28 @@ mod tests { // Poetry 1.0 carries a `#sha256=…&` fragment; the comparison ignores it. let lock10 = fixture("1.0.10"); let first10 = hosted(&lock10).unwrap().unwrap(); - let second10 = rewrite_poetry_lock( - &first10, - "urllib3", - "1.26.18", - "url", - &rotated, - WHEEL, - &"b".repeat(64), - ) - .unwrap() - .unwrap(); + let second10 = rewrite_poetry_lock(&first10, "urllib3", "1.26.18", "url", &rotated, WHEEL, &"b".repeat(64)) + .unwrap() + .unwrap(); assert!(second10.contains(&format!("{rotated}#sha256={}&", "b".repeat(64)))); // A user's own url source on another origin stays untouched. let foreign = first.replace("https://patch.socket.dev", "https://mirror.example"); - assert!(hosted(&foreign) - .unwrap_err() - .contains("existing Poetry source")); + assert!(hosted(&foreign).unwrap_err().contains("existing Poetry source")); // A vendored file source is never taken over by the hosted path here. - let vendored = rewrite_poetry_lock( - &lock, - "urllib3", - "1.26.18", - "file", - ".socket/vendor/pypi/x/urllib3-1.26.18-py2.py3-none-any.whl", - WHEEL, - &sha(), - ) - .unwrap() - .unwrap(); - assert!(hosted(&vendored) - .unwrap_err() - .contains("existing Poetry source")); + let vendored = rewrite_poetry_lock(&lock, "urllib3", "1.26.18", "file", ".socket/vendor/pypi/x/urllib3-1.26.18-py2.py3-none-any.whl", WHEEL, &sha()) + .unwrap() + .unwrap(); + assert!(hosted(&vendored).unwrap_err().contains("existing Poetry source")); } #[test] fn sha256_is_written_lowercase() { let lock = fixture("2.4.3"); let upper = "A".repeat(64); - let rewritten = rewrite_poetry_lock(&lock, "urllib3", "1.26.18", "url", URL, WHEEL, &upper) - .unwrap() - .unwrap(); + let rewritten = + rewrite_poetry_lock(&lock, "urllib3", "1.26.18", "url", URL, WHEEL, &upper) + .unwrap() + .unwrap(); assert!(rewritten.contains(&format!("sha256:{}", "a".repeat(64)))); assert!(!rewritten.contains(&upper)); } @@ -745,10 +712,7 @@ mod tests { let lock = format!("{lock}{sibling}"); let rewritten = hosted(&lock).unwrap().unwrap(); assert!(rewritten.contains(URL)); - assert!( - rewritten.contains(&sibling), - "sibling entry must survive verbatim" - ); + assert!(rewritten.contains(&sibling), "sibling entry must survive verbatim"); let edits = poetry_lock_edits(&lock, &rewritten, "urllib3").unwrap(); assert_eq!(edits.len(), 2); assert!(edits[1].0.starts_with('\n')); @@ -769,10 +733,7 @@ mod tests { "{version}: {original:?}" ); assert!(new.ends_with("[metadata]") || new.ends_with("[extras]")); - assert!( - !new.contains(original.as_str()), - "{version}: pristine must not be a prefix of new" - ); + assert!(!new.contains(original.as_str()), "{version}: pristine must not be a prefix of new"); // A relock that keeps `[package.source]` but drops the inserted // `files` line must NOT contain the pristine fragment either. let drifted: String = rewritten @@ -786,20 +747,12 @@ mod tests { // header, the second starts with it; both splice independently. let lock = fixture("2.4.3"); let mut doc: DocumentMut = lock.parse().unwrap(); - let mut second = doc["package"] - .as_array_of_tables() - .unwrap() - .get(0) - .unwrap() - .clone(); + let mut second = doc["package"].as_array_of_tables().unwrap().get(0).unwrap().clone(); second["name"] = value("six"); second["version"] = value("1.16.0"); second.set_position(None); second.remove("extras"); - doc["package"] - .as_array_of_tables_mut() - .unwrap() - .push(second); + doc["package"].as_array_of_tables_mut().unwrap().push(second); let two = doc.to_string(); let first = hosted(&two).unwrap().unwrap(); let edits = poetry_lock_edits(&two, &first, "urllib3").unwrap(); @@ -811,29 +764,11 @@ mod tests { fn absent_or_other_version_yields_none_not_error() { let lock = fixture("2.4.3"); assert_eq!( - rewrite_poetry_lock( - &lock, - "six", - "1.16.0", - "url", - &URL.replace("urllib3", "six").replace("1.26.18", "1.16.0"), - "six-1.16.0-py2.py3-none-any.whl", - &sha() - ) - .unwrap(), + rewrite_poetry_lock(&lock, "six", "1.16.0", "url", &URL.replace("urllib3", "six").replace("1.26.18", "1.16.0"), "six-1.16.0-py2.py3-none-any.whl", &sha()).unwrap(), None ); assert_eq!( - rewrite_poetry_lock( - &lock, - "urllib3", - "1.26.17", - "url", - &URL.replace("1.26.18", "1.26.17"), - "urllib3-1.26.17-py2.py3-none-any.whl", - &sha() - ) - .unwrap(), + rewrite_poetry_lock(&lock, "urllib3", "1.26.17", "url", &URL.replace("1.26.18", "1.26.17"), "urllib3-1.26.17-py2.py3-none-any.whl", &sha()).unwrap(), None ); } diff --git a/crates/socket-patch-core/src/utils/python_script.rs b/crates/socket-patch-core/src/utils/python_script.rs index 5eb997005..2ca51e107 100644 --- a/crates/socket-patch-core/src/utils/python_script.rs +++ b/crates/socket-patch-core/src/utils/python_script.rs @@ -627,12 +627,7 @@ mod rendering_tests { "{direct}" ); assert!(uv_line.ends_with('}'), "{direct}"); - assert!( - direct.starts_with( - "[project]\nname = \"p\"\ndependencies = [\"alpha==1.0.0\"]\n\n[tool]\n" - ), - "{direct}" - ); + assert!(direct.starts_with("[project]\nname = \"p\"\ndependencies = [\"alpha==1.0.0\"]\n\n[tool]\n"), "{direct}"); assert_settled(&direct); let transitive = rewrite_project_metadata( diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/mod.rs b/crates/socket-patch-core/src/vendor/lock_inventory/mod.rs index d7b723d91..15330b99d 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/mod.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/mod.rs @@ -65,8 +65,8 @@ pub(crate) mod npm_family; pub(crate) mod pnpm; pub(crate) mod pypi; pub(crate) mod recover; -pub mod view; pub(crate) mod vlt; +pub mod view; pub(crate) mod wired; pub(crate) mod yarn; diff --git a/crates/socket-patch-core/src/vendor/lock_inventory/vlt.rs b/crates/socket-patch-core/src/vendor/lock_inventory/vlt.rs index 497c1b213..7034acb88 100644 --- a/crates/socket-patch-core/src/vendor/lock_inventory/vlt.rs +++ b/crates/socket-patch-core/src/vendor/lock_inventory/vlt.rs @@ -6,8 +6,8 @@ use std::path::Path; use serde_json::{Map, Value}; -use super::view::ProjectView; use crate::constants::npm_family::VLT_LOCK; +use super::view::ProjectView; use crate::vendor::vlt_lock_text::{ is_default_registry, sniff_lock, split_dep_id, DepId, DepIdKind, LockSniff, }; diff --git a/crates/socket-patch-core/src/vendor/npm_dir.rs b/crates/socket-patch-core/src/vendor/npm_dir.rs index c84572970..3e5c27683 100644 --- a/crates/socket-patch-core/src/vendor/npm_dir.rs +++ b/crates/socket-patch-core/src/vendor/npm_dir.rs @@ -1249,10 +1249,8 @@ mod tests { let why = gitignore_probe(&root, &outside).await.unwrap_err(); assert!(why.contains("`git check-ignore` exited 128"), "{why}"); assert_eq!(gitignored(&root, &outside).await, None); - assert!( - gitignore_unchecked_detail(".socket/vendor/npm/u/a-1.0.0", &why) - .contains("make sure no ignore rule covers .socket/") - ); + assert!(gitignore_unchecked_detail(".socket/vendor/npm/u/a-1.0.0", &why) + .contains("make sure no ignore rule covers .socket/")); } #[cfg(unix)] diff --git a/crates/socket-patch-core/src/vendor/prestage.rs b/crates/socket-patch-core/src/vendor/prestage.rs index 2b99623d4..47a6a75be 100644 --- a/crates/socket-patch-core/src/vendor/prestage.rs +++ b/crates/socket-patch-core/src/vendor/prestage.rs @@ -464,10 +464,7 @@ mod sweep_tests { for dir in &kept { assert!(v.join(dir).exists(), "{dir} kept"); } - assert!( - !v.join("gem").exists(), - "the levels only the tree kept alive are pruned" - ); + assert!(!v.join("gem").exists(), "the levels only the tree kept alive are pruned"); assert!(!v.join(format!("composer/{u}/psr/log@3.0.2")).exists()); assert!(v.join("state.json").exists()); assert_eq!(sweep_stale(root).await, 0, "idempotent"); diff --git a/crates/socket-patch-core/src/vendor/toml_surgery.rs b/crates/socket-patch-core/src/vendor/toml_surgery.rs index b5d799b93..4837a61bf 100644 --- a/crates/socket-patch-core/src/vendor/toml_surgery.rs +++ b/crates/socket-patch-core/src/vendor/toml_surgery.rs @@ -509,8 +509,7 @@ mod tests { // mixed-ending file, so the removal helpers must never normalize: // every byte outside the removed segment survives verbatim (the // go_mod/go_sum CRLF-churn class). - let wired = - "[project]\r\nname = \"x\"\r\n\n[tool.uv.sources]\nfoo = { path = \"w.whl\" }\n"; + let wired = "[project]\r\nname = \"x\"\r\n\n[tool.uv.sources]\nfoo = { path = \"w.whl\" }\n"; let after = remove_exact_line(wired, "foo = { path = \"w.whl\" }").unwrap(); assert_eq!(after, "[project]\r\nname = \"x\"\r\n\n[tool.uv.sources]\n"); assert_eq!( diff --git a/crates/socket-patch-core/tests/covgap_api_blob_fetcher.rs b/crates/socket-patch-core/tests/covgap_api_blob_fetcher.rs index 3809272d0..8d495238b 100644 --- a/crates/socket-patch-core/tests/covgap_api_blob_fetcher.rs +++ b/crates/socket-patch-core/tests/covgap_api_blob_fetcher.rs @@ -572,8 +572,5 @@ async fn fetch_missing_blobs_mixed_outcomes_aggregate_and_format() { // End-to-end formatter exercise with a genuinely mixed result. let rendered = format_fetch_result(&result); assert!(rendered.contains("Downloaded 1 blob\n"), "{rendered}"); - assert!( - rendered.contains("Failed to download 2 blobs"), - "{rendered}" - ); + assert!(rendered.contains("Failed to download 2 blobs"), "{rendered}"); } diff --git a/crates/socket-patch-core/tests/covgap_crawlers_composer_crawler.rs b/crates/socket-patch-core/tests/covgap_crawlers_composer_crawler.rs index fbbda6290..24a50d9b9 100644 --- a/crates/socket-patch-core/tests/covgap_crawlers_composer_crawler.rs +++ b/crates/socket-patch-core/tests/covgap_crawlers_composer_crawler.rs @@ -95,11 +95,7 @@ async fn get_vendor_paths_global_nonexistent_composer_home_falls_back() { fn write_composer_shim(dir: &Path, echo_path: &Path) { use std::os::unix::fs::PermissionsExt; let shim = dir.join("composer"); - std::fs::write( - &shim, - format!("#!/bin/sh\necho '{}'\n", echo_path.display()), - ) - .unwrap(); + std::fs::write(&shim, format!("#!/bin/sh\necho '{}'\n", echo_path.display())).unwrap(); std::fs::set_permissions(&shim, std::fs::Permissions::from_mode(0o755)).unwrap(); } diff --git a/crates/socket-patch-core/tests/covgap_vendor_nuget_feed.rs b/crates/socket-patch-core/tests/covgap_vendor_nuget_feed.rs index 7842d44bc..5f712da1f 100644 --- a/crates/socket-patch-core/tests/covgap_vendor_nuget_feed.rs +++ b/crates/socket-patch-core/tests/covgap_vendor_nuget_feed.rs @@ -88,7 +88,9 @@ async fn stage_tempdir_creation_failure_is_reported_not_fatal() { drop(guard); match outcome { - VendorOutcome::Done { result, entry, .. } => { + VendorOutcome::Done { + result, entry, .. + } => { assert!(!result.success, "the stage failure must fail the vendor"); assert!(entry.is_none(), "no ledger entry for a failed vendor"); let err = result.error.as_deref().unwrap_or(""); diff --git a/crates/socket-patch-core/tests/poetry_hosted.rs b/crates/socket-patch-core/tests/poetry_hosted.rs index a12615cc6..17173e65b 100644 --- a/crates/socket-patch-core/tests/poetry_hosted.rs +++ b/crates/socket-patch-core/tests/poetry_hosted.rs @@ -65,11 +65,7 @@ async fn native_lock_generations_redirect_idempotently_and_restore_every_byte() let codes: Vec<&str> = result.warnings.iter().map(|w| w.code.as_str()).collect(); assert_eq!( codes, - if pre_1_4 { - vec!["redirect_poetry_stale_install_risk"] - } else { - vec![] - }, + if pre_1_4 { vec!["redirect_poetry_stale_install_risk"] } else { vec![] }, "{version}: {:?}", result.warnings ); @@ -115,24 +111,12 @@ fn hosted_shapes_match_each_lock_generations_installer() { ) .files["poetry.lock"] .clone(); - assert!( - lock10.contains(&format!("url = \"{URL}#sha256={sha}&\"")), - "{lock10}" - ); + assert!(lock10.contains(&format!("url = \"{URL}#sha256={sha}&\"")), "{lock10}"); assert!(lock10.contains("reference = \"\""), "{lock10}"); - assert!( - lock10.contains(&format!( - "urllib3 = [{{ file = \"{WHEEL}\", hash = \"sha256:{sha}\" }}]" - )), - "{lock10}" - ); + assert!(lock10.contains(&format!("urllib3 = [{{ file = \"{WHEEL}\", hash = \"sha256:{sha}\" }}]")), "{lock10}"); // Poetry >= 1.2 consuming this 1.0 lock verifies the package `files` // entry, so it is written too (1.0 ignores the extra key). - assert_eq!( - lock10.matches(&format!("sha256:{sha}")).count(), - 2, - "{lock10}" - ); + assert_eq!(lock10.matches(&format!("sha256:{sha}")).count(), 2, "{lock10}"); let doc: toml_edit::DocumentMut = lock10.parse().unwrap(); assert!(doc["package"][0]["files"].is_array(), "{lock10}"); @@ -142,15 +126,8 @@ fn hosted_shapes_match_each_lock_generations_installer() { ) .files["poetry.lock"] .clone(); - assert_eq!( - lock11.matches(&format!("sha256:{sha}")).count(), - 2, - "package files + metadata.files:\n{lock11}" - ); - assert!( - lock11.contains(&format!("url = \"{URL}\"")), - "no fragment on 1.1" - ); + assert_eq!(lock11.matches(&format!("sha256:{sha}")).count(), 2, "package files + metadata.files:\n{lock11}"); + assert!(lock11.contains(&format!("url = \"{URL}\"")), "no fragment on 1.1"); assert!(!lock11.contains("reference"), "{lock11}"); let doc: toml_edit::DocumentMut = lock11.parse().unwrap(); assert!(doc["package"][0]["files"].is_array()); @@ -162,19 +139,11 @@ fn hosted_shapes_match_each_lock_generations_installer() { ) .files["poetry.lock"] .clone(); - assert_eq!( - lock21.matches(&format!("sha256:{sha}")).count(), - 1, - "{lock21}" - ); + assert_eq!(lock21.matches(&format!("sha256:{sha}")).count(), 1, "{lock21}"); assert!(!lock21.contains("reference")); let pristine: toml_edit::DocumentMut = original("2.4.3").parse().unwrap(); let doc: toml_edit::DocumentMut = lock21.parse().unwrap(); - assert_eq!( - doc["metadata"].to_string(), - pristine["metadata"].to_string(), - "[metadata] untouched on 2.x" - ); + assert_eq!(doc["metadata"].to_string(), pristine["metadata"].to_string(), "[metadata] untouched on 2.x"); } #[test] @@ -391,21 +360,14 @@ fn absent_entries_warn_once_and_missing_sha256_is_gated_once_per_dep() { let codes: Vec<&str> = result.warnings.iter().map(|w| w.code.as_str()).collect(); assert_eq!( codes, - vec![ - "redirect_poetry_entry_not_found", - "redirect_poetry_entry_not_found" - ] + vec!["redirect_poetry_entry_not_found", "redirect_poetry_entry_not_found"] ); let mut missing_hash = patch(); missing_hash.integrity.sha256 = None; let result = rewrite_registry_redirect(&files, &[missing_hash]); assert!(result.files.is_empty()); let codes: Vec<&str> = result.warnings.iter().map(|w| w.code.as_str()).collect(); - assert_eq!( - codes, - vec!["redirect_poetry_missing_sha256"], - "gated once, not once per lock" - ); + assert_eq!(codes, vec!["redirect_poetry_missing_sha256"], "gated once, not once per lock"); } /// A future Poetry that bumps the lock minor (2.2) is rewritten like 2.1 in @@ -419,12 +381,9 @@ async fn newer_2x_minor_redirects_and_reverts() { assert!(result.warnings.is_empty(), "{:?}", result.warnings); assert!(result.files["poetry.lock"].contains(URL)); let directory = tempfile::tempdir().unwrap(); - tokio::fs::write( - directory.path().join("poetry.lock"), - &result.files["poetry.lock"], - ) - .await - .unwrap(); + tokio::fs::write(directory.path().join("poetry.lock"), &result.files["poetry.lock"]) + .await + .unwrap(); let mut state = RedirectState { edits: result.edits, ..RedirectState::default() @@ -432,9 +391,7 @@ async fn newer_2x_minor_redirects_and_reverts() { let outcome = revert_remaining_redirect_edits(directory.path(), &mut state, false).await; assert!(outcome.fully_reverted(), "{:?}", outcome.refusals); assert_eq!( - tokio::fs::read_to_string(directory.path().join("poetry.lock")) - .await - .unwrap(), + tokio::fs::read_to_string(directory.path().join("poetry.lock")).await.unwrap(), lock ); } @@ -448,22 +405,13 @@ fn rotated_grant_token_supersedes_the_prior_hosted_url() { let first = rewrite_registry_redirect(&files, &[patch()]); let mut rotated = patch(); rotated.token = "00000000-0000-4000-8000-000000000000".into(); - rotated.artifact_url = URL.replace( - "7e52b8b6-53f2-4dc8-860a-1ae7ebd8be0e", - "00000000-0000-4000-8000-000000000000", - ); + rotated.artifact_url = URL.replace("7e52b8b6-53f2-4dc8-860a-1ae7ebd8be0e", "00000000-0000-4000-8000-000000000000"); let second = rewrite_registry_redirect(&first.files, &[rotated.clone()]); assert!(second.warnings.is_empty(), "{:?}", second.warnings); let lock = &second.files["poetry.lock"]; assert!(lock.contains(&rotated.artifact_url) && !lock.contains(URL)); assert_eq!(second.edits.len(), 1); - assert!(second.edits[0] - .original - .as_ref() - .unwrap() - .as_str() - .unwrap() - .contains(URL)); + assert!(second.edits[0].original.as_ref().unwrap().as_str().unwrap().contains(URL)); } /// A relock (or hand edit) that drops the inserted `files` line but keeps @@ -485,35 +433,22 @@ async fn dropped_files_line_with_source_kept_is_refused_not_converged() { .collect::>() .join("\n") + "\n"; - assert_ne!( - drifted, *redirected, - "{version}: the files line must have been removed" - ); + assert_ne!(drifted, *redirected, "{version}: the files line must have been removed"); assert!(drifted.contains("[package.source]")); let directory = tempfile::tempdir().unwrap(); - tokio::fs::write(directory.path().join("poetry.lock"), &drifted) - .await - .unwrap(); + tokio::fs::write(directory.path().join("poetry.lock"), &drifted).await.unwrap(); let mut state = RedirectState { edits: result.edits.clone(), ..RedirectState::default() }; let outcome = revert_remaining_redirect_edits(directory.path(), &mut state, false).await; - assert!( - !outcome.fully_reverted(), - "{version}: must refuse, not report success" - ); + assert!(!outcome.fully_reverted(), "{version}: must refuse, not report success"); assert_eq!( - tokio::fs::read_to_string(directory.path().join("poetry.lock")) - .await - .unwrap(), + tokio::fs::read_to_string(directory.path().join("poetry.lock")).await.unwrap(), drifted, "{version}: a refused revert writes nothing" ); - assert!( - !state.edits.is_empty(), - "{version}: the ledger keeps its edits for a re-scan" - ); + assert!(!state.edits.is_empty(), "{version}: the ledger keeps its edits for a re-scan"); } } @@ -528,9 +463,7 @@ async fn lock_1_0_rollback_converges_on_a_hand_restored_lock() { let result = rewrite_registry_redirect(&files, &[patch()]); assert!(!result.edits.is_empty()); let directory = tempfile::tempdir().unwrap(); - tokio::fs::write(directory.path().join("poetry.lock"), &pristine) - .await - .unwrap(); + tokio::fs::write(directory.path().join("poetry.lock"), &pristine).await.unwrap(); let mut state = RedirectState { edits: result.edits, ..RedirectState::default() @@ -539,9 +472,7 @@ async fn lock_1_0_rollback_converges_on_a_hand_restored_lock() { assert!(outcome.fully_reverted(), "{:?}", outcome.refusals); assert!(state.edits.is_empty()); assert_eq!( - tokio::fs::read_to_string(directory.path().join("poetry.lock")) - .await - .unwrap(), + tokio::fs::read_to_string(directory.path().join("poetry.lock")).await.unwrap(), pristine ); } From fcc4fcc7ff2d7b83223f9e0e3b2be7193c0c0cf3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 04:32:51 +0000 Subject: [PATCH 8/8] Keep the pin guard on hashless requirements vendor lines A requirements vendor line written into an unhashed requirements set has no --hash, so wired_pin_in returned no pin and a ledgerless in-sync rebuild skipped the guard entirely, including the path check. A rebuilt wheel at another filename would then leave the wired line pointing at a file that does not exist. wired_pin_in now pins the path of a hashless line with an empty sha256, and the guard treats an empty pinned sha256 as path-only. A hash that is present but malformed still pins nothing, as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017aQf44e9818AbFKDYnuAHZ --- crates/socket-patch-core/src/vendor/pypi.rs | 85 ++++++++++++++++++- .../src/vendor/pypi_requirements.rs | 27 ++++-- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/pypi.rs b/crates/socket-patch-core/src/vendor/pypi.rs index 18fdd7438..f557cc65e 100644 --- a/crates/socket-patch-core/src/vendor/pypi.rs +++ b/crates/socket-patch-core/src/vendor/pypi.rs @@ -985,6 +985,13 @@ async fn pypi_prelude<'p>( }) } +/// Whether a rebuilt wheel reproduces the in-sync pin. An empty pinned +/// sha256 comes from an unhashed requirements vendor line, which pins the +/// wheel path alone. +fn pin_matches(pin_path: &str, pin_sha: &str, rel_wheel: &str, sha256_hex: &str) -> bool { + pin_path == rel_wheel && (pin_sha.is_empty() || pin_sha == sha256_hex) +} + /// Whether [`vendor_pypi_with_pipenv_version`] — a wet run with the service /// enabled — asks the patch service for `record`: past every refusal it /// raises first, and answered neither by the in-sync hot path nor by the @@ -1153,7 +1160,7 @@ pub async fn vendor_pypi_with_pipenv_version<'a>( // `uv sync`, …) the moment vendor reports success. Sweep the // mismatched wheel back out and fail loudly instead. if let Some((pin_path, pin_sha)) = &expected_pin { - if *pin_path != rel_wheel || *pin_sha != artifact.sha256_hex { + if !pin_matches(pin_path, pin_sha, &rel_wheel, &artifact.sha256_hex) { let _ = tokio::fs::remove_dir_all(project_root.join(&uuid_dir_rel)).await; prune_empty_vendor_levels(&project_root.join(&uuid_dir_rel)).await; let mut result = result; @@ -1843,7 +1850,7 @@ async fn try_pypi_service_wheel( // 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_path != rel_wheel || *pin_sha != sha256_hex { + if !pin_matches(pin_path, pin_sha, &rel_wheel, &sha256_hex) { return policy.miss( warnings, "vendor_prebuilt_pin_mismatch", @@ -3434,6 +3441,80 @@ wheels = [ ); } + /// An unhashed requirements set gets a hashless vendor line, which still + /// pins the wheel PATH. With no ledger entry, a service rebuild that + /// lands at another filename would leave that line pointing at nothing, + /// so the guard refuses it; different bytes at the pinned path break no + /// hash and are accepted. + #[tokio::test] + async fn in_sync_ledgerless_rebuild_of_unhashed_line_keeps_the_wired_path() { + let fx = e2e_fixture().await; + let sources = PatchSources::blobs_only(&fx.blobs); + let vendor = |cfg: Option| { + let (fx, sources) = (&fx, &sources); + async move { + crate::vendor::test_support::vendor_pypi( + "pkg:pypi/six@1.16.0", + &fx.site_packages, + &fx.root, + &fx.record, + sources, + "2026-06-09T00:00:00Z", + false, + false, + cfg.as_ref(), + ) + .await + } + }; + let VendorOutcome::Done { result, .. } = vendor(None).await else { + panic!("first vendor must be Done"); + }; + assert!(result.success, "{:?}", result.error); + let wired = tokio::fs::read_to_string(fx.root.join("requirements.txt")) + .await + .unwrap(); + assert!(!wired.contains("--hash"), "{wired}"); + let uuid_dir = fx.root.join(format!(".socket/vendor/pypi/{UUID}")); + + // Another filename: refused, requirements.txt untouched. + tokio::fs::remove_dir_all(&uuid_dir).await.unwrap(); + let bytes = served_wheel(b"service wheel at another filename"); + let server = wiremock::MockServer::start().await; + mount_pypi_granted(&server, "six-1.16.0-py3-none-any.whl", &sri_sha512(&bytes), &bytes) + .await; + let cfg = pypi_service_cfg(&server.uri(), VendorSource::Service, false); + let error = crate::vendor::test_support::expect_failure(vendor(Some(cfg)).await); + assert!( + error.contains("does not match the wheel the lockfile still pins"), + "{error}" + ); + assert_eq!( + tokio::fs::read_to_string(fx.root.join("requirements.txt")) + .await + .unwrap(), + wired + ); + + // Same filename, different bytes: rebuilt, requirements.txt untouched. + let _ = tokio::fs::remove_dir_all(&uuid_dir).await; + let bytes = served_wheel(b"service wheel at the pinned filename"); + let server = wiremock::MockServer::start().await; + mount_pypi_granted(&server, WHEEL_NAME, &sri_sha512(&bytes), &bytes).await; + let cfg = pypi_service_cfg(&server.uri(), VendorSource::Service, false); + let VendorOutcome::Done { result, .. } = vendor(Some(cfg)).await else { + panic!("same-path rebuild must be Done"); + }; + assert!(result.success, "{:?}", result.error); + assert!(uuid_dir.join(WHEEL_NAME).is_file()); + assert_eq!( + tokio::fs::read_to_string(fx.root.join("requirements.txt")) + .await + .unwrap(), + wired + ); + } + /// The ledgerless twin of the loud local failure: a project vendored /// FROM THE SERVICE whose ledger entry AND wheel are gone must not /// "rebuild" locally into bytes the wired requirements line does not diff --git a/crates/socket-patch-core/src/vendor/pypi_requirements.rs b/crates/socket-patch-core/src/vendor/pypi_requirements.rs index a6072e797..236e25dc0 100644 --- a/crates/socket-patch-core/src/vendor/pypi_requirements.rs +++ b/crates/socket-patch-core/src/vendor/pypi_requirements.rs @@ -124,7 +124,9 @@ pub(super) enum RequirementsTarget { /// The wheel path + sha256 the wired vendor line still pins — the /// very pin `pip install --require-hashes` verifies. The in-sync /// rebuild guard falls back to it when the state.json ledger has no - /// entry left for the patch. + /// entry left for the patch. An unhashed vendor line (written into + /// an unhashed requirements set) pins its path alone: the sha256 is + /// empty and the guard checks only the path. pin: Option<(String, String)>, }, } @@ -199,16 +201,18 @@ fn vendored_uuid_for(content: &str, canon_name: &str) -> Option { /// Extract the (wheel path, sha256) pin the wired vendor line for /// `canon_name` carries — the same line shape [`vendored_uuid_for`] matches, -/// restricted to THIS patch uuid and requiring the `--hash=sha256:` pin -/// vendor always writes. Paths are returned bare (no `./` prefix), matching -/// the ledger's `artifact.path` spelling. +/// restricted to THIS patch uuid. A line with no `--hash` (vendor writes +/// none into an unhashed requirements set) still pins its path, with an +/// empty sha256; a `--hash` that is not a sha256 hex digest pins nothing. +/// Paths are returned bare (no `./` prefix), matching the ledger's +/// `artifact.path` spelling. fn wired_pin_in(content: &str, canon_name: &str, record_uuid: &str) -> Option<(String, String)> { vendor_lines(content, canon_name).find_map(|(parts, token, code)| { if parts.uuid != record_uuid { return None; } - let sha = hash_options(&code).into_iter().next()?; - if sha.len() != 64 || !sha.bytes().all(|b| b.is_ascii_hexdigit()) { + let sha = hash_options(&code).into_iter().next().unwrap_or_default(); + if !sha.is_empty() && (sha.len() != 64 || !sha.bytes().all(|b| b.is_ascii_hexdigit())) { return None; } let path = token.strip_prefix("./").unwrap_or(&token); @@ -1103,10 +1107,11 @@ mod tests { .await .unwrap(); assert_eq!(read_root(tmp.path()).await, wired); - // Still read back as our line for this patch. + // Still read back as our line for this patch, pinning the path. assert!(matches!( preflight_requirements(tmp.path(), "six", "1.16.0", UUID).await, - Ok(RequirementsTarget::InSync { pin: None }) + Ok(RequirementsTarget::InSync { pin: Some((path, sha)) }) + if path == REL_WHEEL && sha.is_empty() )); let outcome = revert_requirements(&entry_for(wiring), tmp.path(), false).await; assert!(outcome.success, "{:?}", outcome.error); @@ -1761,6 +1766,12 @@ mod tests { assert_eq!(wired_pin_in(&content, "six", "not-the-uuid"), None); let short = content.replace(&hex, "abc"); assert_eq!(wired_pin_in(&short, "six", UUID), None); + // An unhashed vendor line still pins its path, with no sha256. + let unhashed = format!("{wheel} # socket-patch vendor: six==1.16.0\nattrs==23.1.0\n"); + assert_eq!( + wired_pin_in(&unhashed, "six", UUID), + Some((wheel.trim_start_matches("./").to_string(), String::new())) + ); } /// Multi-package coexistence: a root already carrying ANOTHER package's