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
19 changes: 13 additions & 6 deletions crates/socket-patch-cli/src/commands/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,14 +536,21 @@ pub(crate) async fn persist_vendor_entry(
// carry it forward by wiring identity, or a
// later `--revert` can only shrug
// (`vendor_lock_entry_drifted`) instead of
// restoring the registry fragment.
// restoring the registry fragment. Identity is
// uuid-agnostic (`wiring_key_matches`): berry's
// lock key embeds the vendored path, so the
// uuid change that CAUSED the re-vendor changes
// the key too.
for rec in &mut entry.wiring {
if rec.action == vendor::state::WiringAction::Rewritten && rec.original.is_none() {
if let Some(prev_rec) = prev
.wiring
.iter()
.find(|p| p.file == rec.file && p.kind == rec.kind && p.key == rec.key)
{
if let Some(prev_rec) = prev.wiring.iter().find(|p| {
p.file == rec.file
&& p.kind == rec.kind
&& match (p.key.as_deref(), rec.key.as_deref()) {
(Some(a), Some(b)) => vendor::path::wiring_key_matches(a, b),
(a, b) => a == b,
}
}) {
rec.original = prev_rec.original.clone();
}
}
Expand Down
131 changes: 131 additions & 0 deletions crates/socket-patch-cli/tests/in_process_vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,137 @@ async fn revendor_new_uuid_cleans_stale_artifact_and_still_reverts() {
assert!(!fx.vendor_dir().exists(), "vendor tree fully pruned");
}

// ─────────────────────────────────────────────────────────────────────
// 8d. re-vendor under a new patch uuid — yarn berry
// ─────────────────────────────────────────────────────────────────────

/// The yarn-berry variant of 8c. Berry's lock wiring key is the `file:`
/// locator key, which EMBEDS the patch uuid — so the carry-forward that
/// keeps the pre-vendor registry fragment across a patch update must match
/// wiring records uuid-agnostically. Without that, the re-vendored entry
/// keeps `original: null`, and a later `--revert` deletes the artifact dir
/// while leaving the dangling `file:` lock entry in place (a permanently
/// broken `yarn install --immutable`).
#[tokio::test]
async fn revendor_new_uuid_carries_original_forward_yarn_berry() {
const UUID2: &str = "0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d";
const BERRY_PKG: &str = r#"{
"name": "berry-fixture",
"version": "1.0.0",
"private": true,
"dependencies": {
"left-pad": "1.3.0"
}
}
"#;
let berry_lock: String = format!(
"# This file is generated by running \"yarn install\" inside your project.\n\
# Manual changes might be lost - proceed with caution!\n\n\
__metadata:\n version: 8\n cacheKey: 10c0\n\n\
\"left-pad@npm:1.3.0\":\n version: 1.3.0\n \
resolution: \"left-pad@npm:1.3.0\"\n checksum: 10c0/{}\n \
languageName: node\n linkType: hard\n\n\
\"berry-fixture@workspace:.\":\n version: 0.0.0-use.local\n \
resolution: \"berry-fixture@workspace:.\"\n dependencies:\n \
left-pad: \"npm:1.3.0\"\n languageName: unknown\n linkType: soft\n",
"3".repeat(128)
);

// Berry project fixture: package.json + berry yarn.lock + .yarnrc.yml +
// installed copy + offline manifest/blob (same staging as npm_fixture).
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let pkg = root.join("node_modules/left-pad");
std::fs::create_dir_all(&pkg).unwrap();
std::fs::write(
pkg.join("package.json"),
br#"{"name":"left-pad","version":"1.3.0"}"#,
)
.unwrap();
std::fs::write(pkg.join("index.js"), ORIG_INDEX).unwrap();
std::fs::write(root.join("package.json"), BERRY_PKG).unwrap();
std::fs::write(root.join("yarn.lock"), &berry_lock).unwrap();
std::fs::write(
root.join(".yarnrc.yml"),
"nodeLinker: node-modules\nenableGlobalCache: true\n",
)
.unwrap();
let before_hash = compute_git_sha256_from_bytes(ORIG_INDEX);
let after_hash = compute_git_sha256_from_bytes(PATCHED_INDEX);
let manifest = json!({ "patches": { PURL: patch_record(&before_hash, &after_hash) } });
std::fs::create_dir_all(root.join(".socket/blobs")).unwrap();
std::fs::write(
root.join(".socket/manifest.json"),
serde_json::to_vec_pretty(&manifest).unwrap(),
)
.unwrap();
std::fs::write(root.join(".socket/blobs").join(&after_hash), PATCHED_INDEX).unwrap();

// First vendor (uuid A): the ledger's lock record carries the verbatim
// pre-vendor registry entry.
assert_eq!(vendor_run(vendor_args(root)).await, 0, "first vendor");
let state_path = root.join(".socket/vendor/state.json");
let lock_wiring = |state: &Value| -> Value {
state["entries"][PURL]["wiring"]
.as_array()
.expect("wiring array")
.iter()
.find(|w| w["kind"] == "yarn_berry_lock_entry")
.expect("berry lock wiring record")
.clone()
};
let state: Value = serde_json::from_slice(&std::fs::read(&state_path).unwrap()).unwrap();
let registry_original = lock_wiring(&state)["original"].clone();
assert_eq!(
registry_original[0], "\"left-pad@npm:1.3.0\":",
"first vendor records the registry entry: {registry_original:#}"
);

// The manifest record moves to a newer patch uuid.
let mut manifest: Value =
serde_json::from_slice(&std::fs::read(root.join(".socket/manifest.json")).unwrap())
.unwrap();
manifest["patches"][PURL]["uuid"] = json!(UUID2);
std::fs::write(
root.join(".socket/manifest.json"),
serde_json::to_vec_pretty(&manifest).unwrap(),
)
.unwrap();

let (code, env) = vendor_cli(root, &[]);
assert_eq!(code, 0, "re-vendor must succeed: {env:#}");
let state: Value = serde_json::from_slice(&std::fs::read(&state_path).unwrap()).unwrap();
assert_eq!(state["entries"][PURL]["uuid"], UUID2);
assert!(
!root.join(format!(".socket/vendor/npm/{UUID}")).exists(),
"old uuid dir swept"
);
// THE regression: the re-vendored lock record (whose key now embeds
// uuid B) must have regained the registry original recorded under the
// uuid-A key.
assert_eq!(
lock_wiring(&state)["original"],
registry_original,
"the pre-vendor registry fragment must survive the uuid change: {state:#}"
);

// And revert must restore BOTH files byte-for-byte — pre-fix it leaves
// a `file:` lock entry pointing at the deleted uuid-B artifact.
let (code, env) = vendor_cli(root, &["--revert"]);
assert_eq!(code, 0, "revert after re-vendor: {env:#}");
assert_eq!(
std::fs::read_to_string(root.join("yarn.lock")).unwrap(),
berry_lock,
"yarn.lock restored to the pristine registry entry byte-for-byte"
);
assert_eq!(
std::fs::read_to_string(root.join("package.json")).unwrap(),
BERRY_PKG,
"package.json restored (resolutions table dropped)"
);
assert!(!root.join(".socket/vendor").exists(), "vendor tree pruned");
}

// ─────────────────────────────────────────────────────────────────────
// 9. offline with no local source
// ─────────────────────────────────────────────────────────────────────
Expand Down
28 changes: 28 additions & 0 deletions crates/socket-patch-core/src/vendor/npm_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,34 @@ pub(super) fn done_failure(purl: &str, error: String) -> VendorOutcome {
done(failed_result(purl, Path::new(""), error), None, Vec::new())
}

/// [`done_failure`] for a wiring failure AFTER the shared pipeline packed
/// the artifact into `<project>/.socket/vendor/<eco>/<uuid>/`: unless the
/// uuid dir already existed before this run (a same-uuid re-vendor may still
/// be referenced by live wiring), best-effort remove it — no ledger entry is
/// ever persisted for a failed wiring, so `--revert` could never clean it up
/// and the module contract ("a failure leaves the project byte-untouched")
/// would be broken by an orphaned, possibly defective artifact dir. Empty
/// parent dirs are pruned non-recursively (a sibling artifact keeps them).
pub(super) async fn done_failure_unstage(
purl: &str,
error: String,
project_root: &Path,
uuid_dir_rel: &str,
uuid_dir_preexisted: bool,
) -> VendorOutcome {
if !uuid_dir_preexisted {
let uuid_dir = project_root.join(uuid_dir_rel);
let _ = remove_tree(&uuid_dir).await;
if let Some(eco_dir) = uuid_dir.parent() {
let _ = tokio::fs::remove_dir(eco_dir).await;
if let Some(vendor_dir) = eco_dir.parent() {
let _ = tokio::fs::remove_dir(vendor_dir).await;
}
}
}
done_failure(purl, error)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
85 changes: 85 additions & 0 deletions crates/socket-patch-core/src/vendor/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,55 @@ pub fn parse_vendor_path(s: &str) -> Option<VendorPathParts> {
})
}

/// Do two wiring-record keys name the same lockfile fragment across a patch
/// update? Byte-equal keys always match. Keys that embed a
/// `.socket/vendor/<eco>/<uuid>/` path (yarn berry's `file:` locator lock
/// key) additionally match when they are equal with every uuid level
/// normalized away — updating a patch changes the uuid, which changes the
/// embedded path, but the record still names the same lock entry. The
/// re-vendor carry-forward relies on this to keep the pre-vendor `original`
/// across patch updates; keys without a recognizable vendored path only ever
/// match byte-equal.
pub fn wiring_key_matches(a: &str, b: &str) -> bool {
if a == b {
return true;
}
match (normalize_vendor_uuids(a), normalize_vendor_uuids(b)) {
(Some(na), Some(nb)) => na == nb,
_ => false,
}
}

/// Replace every embedded `.socket/vendor/<eco>/<uuid>` uuid level with `*`;
/// `None` when the string embeds no recognizable vendored path.
fn normalize_vendor_uuids(s: &str) -> Option<String> {
let anchor = format!("{VENDOR_DIR}/");
let mut out = String::new();
let mut rest = s;
let mut replaced = false;
while let Some(idx) = rest.find(&anchor) {
let head_end = idx + anchor.len();
out.push_str(&rest[..head_end]);
rest = &rest[head_end..];
let Some((eco, tail)) = rest.split_once('/') else {
break;
};
if !ECOSYSTEM_DIRS.contains(&eco) {
continue;
}
let uuid_end = tail.find('/').unwrap_or(tail.len());
if !is_canonical_uuid(&tail[..uuid_end]) {
continue;
}
out.push_str(eco);
out.push_str("/*");
rest = &tail[uuid_end..];
replaced = true;
}
out.push_str(rest);
replaced.then_some(out)
}

/// Split a `<name>-<version>` leaf at the version boundary: the version is
/// the suffix after the LAST `-` that is immediately followed by a digit
/// (versions always start with a digit; names may contain digit-bearing
Expand Down Expand Up @@ -448,6 +497,42 @@ mod tests {
assert!(parse_vendor_path(&format!("x.socket/vendor/npm/{UUID}/y.tgz")).is_none());
}

/// The re-vendor carry-forward matches wiring keys ACROSS a patch-uuid
/// change when the key embeds the vendored path (berry's `file:` locator
/// key), and only byte-equal otherwise.
#[test]
fn wiring_key_matching_is_uuid_agnostic_for_vendored_paths() {
const UUID2: &str = "0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d";
let berry_key = |uuid: &str| {
format!(
"\"left-pad@file:./.socket/vendor/npm/{uuid}/left-pad-1.3.0.tgz\
::locator=vendor-spike%40workspace%3A.\""
)
};
// Same key always matches; uuid-only difference matches too.
assert!(wiring_key_matches(&berry_key(UUID), &berry_key(UUID)));
assert!(wiring_key_matches(&berry_key(UUID), &berry_key(UUID2)));
// A different package leaf (other name/version) never matches.
assert!(!wiring_key_matches(
&berry_key(UUID),
&format!(
"\"is-odd@file:./.socket/vendor/npm/{UUID2}/is-odd-1.0.0.tgz\
::locator=vendor-spike%40workspace%3A.\""
)
));
// Keys with no vendored path only match byte-equal (classic block
// keys, npm lock paths, bare names).
assert!(wiring_key_matches("left-pad@^1.3.0", "left-pad@^1.3.0"));
assert!(!wiring_key_matches("left-pad@^1.3.0", "left-pad@~1.3.0"));
// A vendored-path key never matches a non-vendored one.
assert!(!wiring_key_matches(&berry_key(UUID), "left-pad@^1.3.0"));
// Non-canonical uuid levels are not normalized (fail-closed).
assert!(!wiring_key_matches(
".socket/vendor/npm/not-a-uuid/x.tgz",
&format!(".socket/vendor/npm/{UUID}/x.tgz")
));
}

#[test]
fn leaf_round_trips() {
// npm, incl. scoped and digit-bearing names + prerelease versions.
Expand Down
Loading
Loading