Skip to content
Merged
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
69 changes: 62 additions & 7 deletions crates/socket-patch-core/src/patch/redirect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4837,6 +4837,17 @@ fn rewrite_bun_lock(
Some(content),
);

// Decode each entry's spec once per lock, not once per (patch, entry)
// pair. The bundled flag needs a full JSON parse of the entry's meta
// object, so it is computed lazily, only for entries whose spec matches
// some patch, and cached (#580: the eager per-patch parse made bun scans
// O(entries x patches) JSON parses).
let specs: Vec<Option<String>> = entries
.iter()
.map(|e| e.elems.first().and_then(|s| decode_json_string(s)))
.collect();
let mut bundled: Vec<Option<bool>> = vec![None; entries.len()];

let mut changed = false;
let mut pinned_any = false;
for dep in &npm {
Expand All @@ -4854,18 +4865,20 @@ fn rewrite_bun_lock(
let target_spec = format!("{fname}@{}", dep.version);
let url_spec = format!("{fname}@{}", dep.artifact_url);
let mut matched_any = false;
for entry in &entries {
let Some(spec) = entry.elems.first().and_then(|e| decode_json_string(e)) else {
for (i, entry) in entries.iter().enumerate() {
let Some(spec) = specs[i].as_deref() else {
continue;
};
// Bun unpacks a bundled copy from its PARENT's tarball and never
// reads the entry's spec (#469), so a rewrite here would count
// as redirected (and VEX-attest the patch) while the unpatched
// bundled bytes keep installing. Mirrors npm's `inBundle` guard.
if is_bundled_entry(entry)
&& (spec == target_spec
|| spec == url_spec
|| is_prior_hosted_bun_spec(&spec, &fname, &dep.artifact_url))
// The cheap spec match runs first; the bundled parse only for
// a match (#580).
if (spec == target_spec
|| spec == url_spec
|| is_prior_hosted_bun_spec(spec, &fname, &dep.artifact_url))
&& *bundled[i].get_or_insert_with(|| is_bundled_entry(entry))
{
matched_any = true;
result.bundled_skipped_uuids.insert(dep.patch_uuid.clone());
Expand Down Expand Up @@ -4909,7 +4922,7 @@ fn rewrite_bun_lock(
deps_verbatim = entry.elems[1].clone();
} else if matches!(entry.elems.len(), 2 | 3)
&& entry.elems[1].starts_with('{')
&& is_prior_hosted_bun_spec(&spec, &fname, &dep.artifact_url)
&& is_prior_hosted_bun_spec(spec, &fname, &dep.artifact_url)
{
// A URL tuple written by an EARLIER redirect whose artifact
// URL has since changed (a patch republish rotates the uuid
Expand Down Expand Up @@ -11857,6 +11870,48 @@ mod tests {
);
}

/// #580: the bundled check runs only for entries whose spec matches the
/// patch. A bundled copy of ANOTHER package (or another version) beside
/// the target never warns or keeps the patch out of VEX, while a bundled
/// copy of the target itself still does, across several patches.
#[test]
fn bun_lock_bundled_check_only_applies_to_matching_entries() {
let sha512 = format!("sha512-{}==", "A".repeat(86));
let ovr = npm_override("is-number", "7.0.0", "http://p.test/isn.tgz", &sha512);
let regular = "\"is-number\": [\"is-number@7.0.0\", \"\", {}, \"sha512-UP==\"],";
let other_bundled = "\"@bh/bund/kind-of\": [\"kind-of@6.0.3\", \"\", { \"bundled\": true }, \"sha512-KO==\"],";
let other_version_bundled = "\"@bh/bund/is-number\": [\"is-number@6.0.0\", \"\", { \"bundled\": true }, \"sha512-OV==\"],";

let lock = format!("{regular}\n {other_bundled}\n {other_version_bundled}");
let mut files = BTreeMap::new();
files.insert("bun.lock".to_string(), bun_lock_file(&lock, 1));
let mut r = RewriteResult::default();
rewrite_bun_lock(&files, std::slice::from_ref(&ovr), &mut r);
assert_eq!(r.edits.len(), 1, "{:?}", r.edits);
assert_eq!(r.edits[0].key.as_deref(), Some("is-number"));
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
assert!(r.bundled_skipped_uuids.is_empty());
let out = r.files.get("bun.lock").expect("lock rewritten");
assert!(out.contains(other_bundled) && out.contains(other_version_bundled));

// A second patch whose target IS bundled still warns, and only for
// its own uuid.
let mut kind_of = npm_override("kind-of", "6.0.3", "http://p.test/ko.tgz", &sha512);
kind_of.patch_uuid = "33333333-3333-4333-8333-333333333333".into();
let mut r = RewriteResult::default();
rewrite_bun_lock(&files, &[ovr.clone(), kind_of.clone()], &mut r);
assert_eq!(r.edits.len(), 1, "{:?}", r.edits);
assert_eq!(
warning_codes(&r),
vec!["redirect_bun_bundled_instance_skipped"],
"{:?}",
r.warnings
);
assert!(r.warnings[0].detail.contains("@bh/bund/kind-of"));
assert!(r.bundled_skipped_uuids.contains(&kind_of.patch_uuid));
assert!(!r.bundled_skipped_uuids.contains(&ovr.patch_uuid));
}

/// REGRESSION (#367): `bun patch --commit` keys the project's own patch
/// on the registry `name@version` in package.json (and bun.lock's
/// mirror). Rewiring that package to a hosted URL makes Bun drop the
Expand Down
10 changes: 5 additions & 5 deletions crates/socket-patch-core/src/vendor/bun_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1092,10 +1092,10 @@ fn classify_rewritable(
name: &str,
target_leaf: &str,
) -> Option<TupleShape> {
if is_bundled_entry(entry) {
return None;
}
classify(entry, target_spec, name, target_leaf)
// The cheap spec match runs first; the bundled parse only for a match
// (#580).
let shape = classify(entry, target_spec, name, target_leaf)?;
(!is_bundled_entry(entry)).then_some(shape)
}

/// Keys of the bundled entries that resolve the target `name@version`.
Expand All @@ -1107,7 +1107,7 @@ fn bundled_matches(
) -> Vec<String> {
entries
.iter()
.filter(|e| is_bundled_entry(e) && classify(e, target_spec, name, target_leaf).is_some())
.filter(|e| classify(e, target_spec, name, target_leaf).is_some() && is_bundled_entry(e))
.map(|e| e.key.clone())
.collect()
}
Expand Down
26 changes: 26 additions & 0 deletions crates/socket-patch-core/src/vendor/bun_lock_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ pub(crate) fn is_bundled_entry(entry: &BunEntry) -> bool {
let Some(meta) = entry.elems.iter().skip(1).find(|e| e.starts_with('{')) else {
return false;
};
// Fast path (#580): a meta that never spells `bundled` cannot carry the
// key. A backslash could hide it behind a JSON escape, so only a meta
// with neither skips the parse.
if !meta.contains("bundled") && !meta.contains('\\') {
return false;
}
match serde_json::from_str::<serde_json::Value>(meta) {
Ok(value) => value.get("bundled").and_then(serde_json::Value::as_bool) == Some(true),
Err(_) => meta.contains("\"bundled\""),
Expand Down Expand Up @@ -717,6 +723,26 @@ mod tests {
));
}

/// The substring fast path (#580) never changes the answer: a meta that
/// never spells `bundled` is not bundled, a malformed meta that does
/// still fails closed, and a JSON-escaped key still takes the parse.
#[test]
fn bundled_fast_path_matches_the_full_parse() {
let bundled = |line: &str| is_bundled_entry(&parse_entry_line(line).unwrap());
assert!(!bundled(
r#" "q": ["q@1.0.0", "", { "dependencies": { "a": "1" } }, "sha512-X=="],"#
));
assert!(!bundled(
r#" "q": ["q@1.0.0", "", { "x": , }, "sha512-X=="],"#
));
assert!(bundled(
r#" "q": ["q@1.0.0", "", { "bundled": true, "x": , }, "sha512-X=="],"#
));
assert!(bundled(
r#" "q": ["q@1.0.0", "", { "bundl\u0065d": true }, "sha512-X=="],"#
));
}

#[test]
fn line_grammar_parses_the_fixture_shapes() {
// Registry 4-tuple with deps and trailing comma.
Expand Down
Loading