From 02e4c3777bd401541d1c64fbb3dcb1781c08b27b Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 14 Aug 2026 06:49:48 -0700 Subject: [PATCH] fix(vendor): match bun tuples on version, keep CRLF The bun backend's already-vendored classification matched on package name alone, never the vendored leaf's version. With patches for two versions of one package in the same bun.lock (a root instance plus a nested one), the second vendor pass classified the first pass's fresh tuple as its own stale edit and rewrote it to the OTHER version's tarball: bun then silently installed the wrong version everywhere, state.json claimed both patches applied, and a later revert left the entry pointing at a deleted tarball (frozen installs hard-fail with ENOENT). The same name-only match let the pre-flight accept a lock whose only same-name entries were vendored tuples of another version. The Ours arm now also requires the parsed vendor path's leaf to equal the target's uuid-independent `-.tgz` leaf, so other-version vendored tuples classify as foreign and are never touched; re-vendoring the same version under a new patch uuid still rewrites as before. Also preserve a trailing `\r` on rewritten entry lines: the grammar tolerantly trimmed it while parsing a CRLF bun.lock but the rebuilt line re-emitted LF only, leaving one mixed-ending line in an otherwise CRLF file (noisy diffs and autocrlf churn on Windows checkouts). Regression tests pin both: a two-version vendor + revert round-trip, a pre-flight refusal when only another version's vendored tuples exist, and a CRLF lock that stays CRLF through vendor, re-run, and revert. Co-authored-by: Claude Fable 5 --- .../socket-patch-core/src/vendor/bun_lock.rs | 207 +++++++++++++++++- 1 file changed, 200 insertions(+), 7 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/bun_lock.rs b/crates/socket-patch-core/src/vendor/bun_lock.rs index 7acb22ac..294377a4 100644 --- a/crates/socket-patch-core/src/vendor/bun_lock.rs +++ b/crates/socket-patch-core/src/vendor/bun_lock.rs @@ -41,7 +41,9 @@ use crate::vendor::bun_lock_text::{ }; use super::common::{already_patched_result, refused}; -use super::npm_common::{done_failure, guard_coordinates, guard_revert_uuid_dir, stage_patch_pack}; +use super::npm_common::{ + done_failure, guard_coordinates, guard_revert_uuid_dir, stage_patch_pack, tgz_rel_leaf, +}; use super::path::parse_vendor_path; use super::state::{ write_marker, VendorArtifact, VendorEntry, VendorMarker, WiringAction, WiringRecord, @@ -107,9 +109,10 @@ pub(crate) async fn vendor_bun( // ── 3. Pre-flight: at least one rewritable instance ────────────────── let target_spec = format!("{name}@{version}"); + let target_leaf = tgz_rel_leaf(name, version); let has_match = entries .iter() - .any(|e| classify(e, &target_spec, name).is_some()); + .any(|e| classify(e, &target_spec, name, &target_leaf).is_some()); if !has_match { return refused( "vendor_lock_entry_not_found", @@ -166,7 +169,7 @@ pub(crate) async fn vendor_bun( let mut wiring: Vec = Vec::new(); let mut changed = false; for entry in &entries { - let Some(shape) = classify(entry, &target_spec, name) else { + let Some(shape) = classify(entry, &target_spec, name, &target_leaf) else { continue; }; let (deps_verbatim, was_ours) = match shape { @@ -181,8 +184,16 @@ pub(crate) async fn vendor_bun( } }; let original_line = lines[entry.line_idx].clone(); + // Lines come from a bare `split('\n')`, so a CRLF lock's lines carry + // a trailing `\r` (the grammar trims it away when parsing). Re-emit + // it verbatim: the surgery must never mix line endings. + let cr = if original_line.ends_with('\r') { + "\r" + } else { + "" + }; let new_line = format!( - "{indent}{key}: [\"{name}@{rel_tgz}\", {deps}, \"{integrity}\"]{comma}", + "{indent}{key}: [\"{name}@{rel_tgz}\", {deps}, \"{integrity}\"]{comma}{cr}", indent = entry.indent, key = entry.key_raw, deps = deps_verbatim, @@ -422,8 +433,17 @@ enum TupleShape { /// Classify an entry against the target: `Some(Registry)` for the exact /// `name@version` registry tuple, `Some(Ours{..})` for one of our own -/// `.socket/vendor/npm/` tuples for the same package, `None` otherwise. -fn classify(entry: &BunEntry, target_spec: &str, name: &str) -> Option { +/// `.socket/vendor/npm/` tuples for the same `name@version` (any uuid), +/// `None` otherwise. The Ours arm matches on the uuid-independent tarball +/// leaf, NOT the name alone: a vendored tuple for ANOTHER version of the +/// same package is someone else's edit (two patched versions can coexist in +/// one lock — nested instances) and must never be cross-clobbered. +fn classify( + entry: &BunEntry, + target_spec: &str, + name: &str, + target_leaf: &str, +) -> Option { let spec = decode_json_string(entry.elems.first()?)?; match entry.elems.len() { 4 if spec == target_spec @@ -439,7 +459,7 @@ fn classify(entry: &BunEntry, target_spec: &str, name: &str) -> Option