Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 96 additions & 10 deletions crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ fn absolutizes_file_overrides(pm: &str) -> bool {
matches!(parts.as_slice(), [9, 0, patch] if *patch <= 4)
}

/// pnpm 8.0.0-8.1.0 refuse their OWN lock for a scoped `file:` tarball
/// override under `--frozen-lockfile` (ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY
/// on the `file:` key they just wrote; measured 2026-10-06: 8.1.0 refuses,
/// 8.1.1 accepts), so no vendored scoped lock can pass there.
fn refuses_own_scoped_file_override(pm: &str) -> bool {
let Some(v) = pm.strip_prefix("pnpm@") else {
return false;
};
let parts: Vec<u32> = v.split('.').filter_map(|p| p.parse().ok()).collect();
matches!(parts.as_slice(), [8, 0, _] | [8, 1, 0])
}

fn has_corepack_pm(pm: &str) -> bool {
// Isolated too: this probe is what actually downloads the package manager
// the first time, and corepack stores it under `COREPACK_HOME`.
Expand Down Expand Up @@ -888,9 +900,13 @@ fn assert_manifestless_vendored_vex(
let state = fresh.join(".socket/vendor/state.json");
let lock_wired = std::fs::read(&lock).expect("fresh checkout lock");
let pkg_wired = std::fs::read(fresh.join("package.json")).unwrap();
let (dep, version) = purl
.strip_prefix("pkg:npm/")
.and_then(|nv| nv.rsplit_once('@'))
.expect("an npm purl");
assert!(
fresh
.join(format!(".socket/vendor/npm/{UUID}/{DEP}-{DEP_VERSION}.tgz"))
.join(format!(".socket/vendor/npm/{UUID}/{dep}-{version}.tgz"))
.is_file(),
"[{tag}] the committed tarball must travel with the checkout"
);
Expand Down Expand Up @@ -1025,7 +1041,7 @@ fn assert_manifestless_vendored_vex(
);
assert!(plain.status.success(), "[{tag}] plain install: {plain:?}");
assert_eq!(
std::fs::read(fresh.join("node_modules").join(DEP).join("index.js")).unwrap(),
std::fs::read(fresh.join("node_modules").join(dep).join("index.js")).unwrap(),
patched,
"[{tag}] a plain install must re-apply the overrides (vendored bytes)"
);
Expand Down Expand Up @@ -1062,8 +1078,30 @@ async fn pnpm_pinned_matrix_vendored_lifecycle_and_manifestless_vex() {
run_pnpm_capstone(&pm, VendorDriver::VendorCli).await;
run_pnpm_capstone(&pm, VendorDriver::GetUuid).await;
}
7 => off_runtime(|| run_legacy_capstone(&pm, "lockfileVersion: 5.4", "proj")),
8 => off_runtime(|| run_legacy_capstone(&pm, "lockfileVersion: '6.0'", "proj")),
7 => off_runtime(|| {
run_legacy_capstone(&pm, "lockfileVersion: 5.4", "proj");
run_legacy_capstone_for(
&pm,
"lockfileVersion: 5.4",
"proj",
SCOPED_DEP,
SCOPED_DEP_VERSION,
);
}),
8 => off_runtime(|| {
run_legacy_capstone(&pm, "lockfileVersion: '6.0'", "proj");
if refuses_own_scoped_file_override(&pm) {
println!("SKIP scoped legacy leg ({pm}): pnpm refuses its own scoped file: lock");
return;
}
run_legacy_capstone_for(
&pm,
"lockfileVersion: '6.0'",
"proj",
SCOPED_DEP,
SCOPED_DEP_VERSION,
);
}),
_ => off_runtime(|| run_unsupported_lock_refusal(&pm)),
}
}
Expand Down Expand Up @@ -1637,6 +1675,43 @@ fn pnpm8_real_lifecycle_under_yaml_indicator_paths() {
}
}

/// #956: a scoped package's rekeyed packages entry must keep the lock
/// loadable. An unquoted `name: @scope/pkg` is invalid YAML, so every
/// frozen install failed with ERR_PNPM_BROKEN_LOCKFILE after a successful
/// vendor.
const SCOPED_DEP: &str = "@isaacs/string-locale-compare";
const SCOPED_DEP_VERSION: &str = "1.1.0";

#[test]
fn pnpm7_real_lifecycle_scoped_package() {
if !has_corepack_pm(PNPM_LEGACY_7) {
println!("SKIP: `corepack {PNPM_LEGACY_7}` unavailable");
return;
}
run_legacy_capstone_for(
PNPM_LEGACY_7,
"lockfileVersion: 5.4",
"proj",
SCOPED_DEP,
SCOPED_DEP_VERSION,
);
}

#[test]
fn pnpm8_real_lifecycle_scoped_package() {
if !has_corepack_pm(PNPM_LEGACY_8) {
println!("SKIP: `corepack {PNPM_LEGACY_8}` unavailable");
return;
}
run_legacy_capstone_for(
PNPM_LEGACY_8,
"lockfileVersion: '6.0'",
"proj",
SCOPED_DEP,
SCOPED_DEP_VERSION,
);
}

/// Full lifecycle against the REAL pinned legacy pnpm, spike-proven flags:
///
/// 1. online fixture install (skip when the registry is unreachable);
Expand All @@ -1653,14 +1728,19 @@ fn pnpm8_real_lifecycle_under_yaml_indicator_paths() {
/// 5. idempotent re-vendor (byte-stable, already_vendored);
/// 6. revert restores both files byte-identical and removes .socket/vendor.
fn run_legacy_capstone(pm: &str, lock_head: &str, proj_dir: &str) {
run_legacy_capstone_for(pm, lock_head, proj_dir, DEP, DEP_VERSION);
}

/// [`run_legacy_capstone`] for any registry package `dep@version`.
fn run_legacy_capstone_for(pm: &str, lock_head: &str, proj_dir: &str, dep: &str, version: &str) {
let tmp = tempfile::tempdir().unwrap();
let proj = tmp.path().join(proj_dir);
std::fs::create_dir_all(&proj).unwrap();
let pkg_doc = serde_json::json!({
"name": "pnpm-legacy-capstone",
"version": "0.0.0",
"private": true,
"dependencies": { DEP: DEP_VERSION },
"dependencies": { dep: version },
});
std::fs::write(
proj.join("package.json"),
Expand Down Expand Up @@ -1688,10 +1768,10 @@ fn run_legacy_capstone(pm: &str, lock_head: &str, proj_dir: &str) {
return;
}

let installed_index = proj.join("node_modules").join(DEP).join("index.js");
let installed_index = proj.join("node_modules").join(dep).join("index.js");
let orig = std::fs::read(&installed_index).expect("installed index.js");
let patched: Vec<u8> = [MARKER.as_bytes(), orig.as_slice()].concat();
let purl = format!("pkg:npm/{DEP}@{DEP_VERSION}");
let purl = format!("pkg:npm/{dep}@{version}");
stage_patch(&proj, &purl, "package/index.js", &orig, &patched);

let lock_path = proj.join("pnpm-lock.yaml");
Expand Down Expand Up @@ -1722,18 +1802,24 @@ fn run_legacy_capstone(pm: &str, lock_head: &str, proj_dir: &str) {
let env = parse_envelope(&stdout);
assert_eq!(env["status"], "success", "envelope: {env}");
assert_eq!(env["summary"]["applied"], 1, "{env}");
let tgz_rel = format!(".socket/vendor/npm/{UUID}/{DEP}-{DEP_VERSION}.tgz");
let tgz_rel = format!(".socket/vendor/npm/{UUID}/{dep}-{version}.tgz");
assert!(proj.join(&tgz_rel).is_file());
assert!(
!proj.join("pnpm-workspace.yaml").exists(),
"legacy wiring must not create pnpm-workspace.yaml ({pm})"
);
let lock_after = std::fs::read_to_string(&lock_path).unwrap();
// pnpm single-quotes an `@`-leading (scoped) key.
let override_key = if dep.starts_with('@') {
format!("'{dep}@{version}'")
} else {
format!("{dep}@{version}")
};
let abs = socket_patch_core::vendor::pnpm_lock_legacy::normalize_canonical_root(
&std::fs::canonicalize(&proj).unwrap().display().to_string(),
);
assert!(
lock_after.contains(&format!("{DEP}@{DEP_VERSION}: file:{tgz_rel}")),
lock_after.contains(&format!("{override_key}: file:{tgz_rel}")),
"lock overrides must point at the vendored tarball ({pm}):\n{lock_after}"
);
assert!(
Expand Down Expand Up @@ -1857,7 +1943,7 @@ fn run_legacy_capstone(pm: &str, lock_head: &str, proj_dir: &str) {
String::from_utf8_lossy(&plain.stderr),
);
let fresh_installed =
std::fs::read(fresh.join("node_modules").join(DEP).join("index.js")).unwrap();
std::fs::read(fresh.join("node_modules").join(dep).join("index.js")).unwrap();
assert_eq!(
fresh_installed, patched,
"moved-checkout install must land the patched bytes ({pm})"
Expand Down
9 changes: 4 additions & 5 deletions crates/socket-patch-core/src/crawlers/gradle_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ pub fn hash_eq(dir_name: &str, sha1_hex: &str) -> bool {
/// Whether `bytes` are the pristine download Gradle stored in the hash
/// directory `dir_name` (their sha1 names it).
pub fn pristine(dir_name: &str, bytes: &[u8]) -> bool {
use sha1::{Digest, Sha1};
hash_eq(dir_name, &hex::encode(Sha1::digest(bytes)))
hash_eq(dir_name, &crate::utils::digest::sha1_hex_of(bytes))
}

/// Whether `path` is a version directory of a `files-2.1` tree
Expand Down Expand Up @@ -432,8 +431,6 @@ impl DerivedIndex {
/// The [`DerivedCopies`] of the jar `jar_leaf` whose pristine bytes
/// hash to `pristine_sha1`.
pub fn query(&self, jar_leaf: &str, pristine_sha1: &str) -> DerivedCopies {
use sha1::{Digest, Sha1};

let instrumented = format!("instrumented-{jar_leaf}");
let mut out = DerivedCopies {
incomplete: self.incomplete,
Expand All @@ -460,7 +457,9 @@ impl DerivedIndex {
out.stale.push(path.clone());
} else if name == jar_leaf || name == instrumented {
match crate::utils::fs::read_regular_to_bytes_sync(path) {
Ok(bytes) if hash_eq(&hex::encode(Sha1::digest(&bytes)), pristine_sha1) => {
Ok(bytes)
if hash_eq(&crate::utils::digest::sha1_hex_of(&bytes), pristine_sha1) =>
{
out.stale.push(path.clone())
}
Ok(_) => out.unknown.push(path.clone()),
Expand Down
7 changes: 2 additions & 5 deletions crates/socket-patch-core/src/patch/jvm_jar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use sha1::Digest as _;

use crate::crawlers::gradle_cache;
use crate::hash::git_sha256::compute_git_sha256_from_bytes;
use crate::manifest::schema::PatchFileInfo;
Expand Down Expand Up @@ -353,12 +351,11 @@ fn unpatched_members(
}

fn sha256_hex(bytes: &[u8]) -> String {
use sha2::Digest as _;
hex::encode(sha2::Sha256::digest(bytes))
crate::utils::digest::sha256_hex_of(bytes)
}

fn sha1_hex(bytes: &[u8]) -> String {
hex::encode(sha1::Sha1::digest(bytes))
crate::utils::digest::sha1_hex_of(bytes)
}

/// `<socket_dir>/jvm-originals/<sha256>.jar`.
Expand Down
4 changes: 1 addition & 3 deletions crates/socket-patch-core/src/patch/sidecars/maven.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

use std::path::{Path, PathBuf};

use sha1::Digest as _;

use super::{
SidecarAdvisory, SidecarAdvisoryCode, SidecarError, SidecarFile, SidecarFileAction,
SidecarPayload, SidecarSeverity,
Expand All @@ -44,7 +42,7 @@ impl Algo {

fn digest(self, bytes: &[u8]) -> String {
match self {
Algo::Sha1 => hex::encode(sha1::Sha1::digest(bytes)),
Algo::Sha1 => crate::utils::digest::sha1_hex_of(bytes),
Algo::Md5 => hex::encode(md5(bytes)),
}
}
Expand Down
Loading
Loading