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
1 change: 1 addition & 0 deletions crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,7 @@ Every `--json` invocation emits a single JSON object that follows the **unified
| `redirect_bun_workspace_unsupported` | `redirect.warnings[]` (warning) | scan/get `--mode hosted` (bun): a lockfileVersion-0 lock (Bun 1.1.39–1.1.45 `--save-text-lockfile`) holds `workspace:` packages; frozen installs of that grammar cannot keep the hosted tuple. Detail: "Bun version-0 workspace locks cannot preserve hosted tarballs on frozen installs; delete bun.lock and re-run `bun install` with Bun >= 1.2 (which writes lockfileVersion 1, accepted by hosted mode) — a plain in-place `bun install` bumps the version only when a workspace depends on another workspace (e.g. root -> member); otherwise it keeps version 0 or fails to resolve" (measured: Bun 1.2.0 keeps 0, 1.2.23–1.4.2 exit 1 "failed to resolve" on a root that does not depend on its members). Version-1/2 workspace locks are rewritten. Exit 0. |
| `redirect_bun_lockb_invalid` | `redirect.warnings[]` (warning) | scan/get `--mode hosted`: the native binary lock is malformed, unreadable, unsupported or cannot be rewritten safely. No installer is spawned and no binary or sibling npm lock edit or takeover occurs; dry-run reports the same format error. Exit 0, `redirected: 0`. |
| `redirect_bun_entry_not_found` / `redirect_bun_missing_sha512` | `redirect.warnings[]` (warning) | scan/get `--mode hosted` (bun): the lock has no rewritable entry at the granted version (re-resolved, or occupied by an unowned URL/file spec) / the grant carries no sha512 integrity. Per-dep; nothing rewritten for it; exit 0. NOT emitted for the digest-less 2-tuple Bun 1.1.39–1.3.9 re-save our URL tuple as — that entry counts as redirected and is healed. |
| `redirect_bun_patched_dependency_skipped` | `redirect.warnings[]` (warning) | scan/get `--mode hosted` (bun, `bun.lock` and `bun.lockb`): the project patches the granted `name@version` itself with `bun patch` (a `patchedDependencies` key for `name@version`, or the bare name, in the root `package.json` or mirrored in `bun.lock`). Bun applies that patch only to the registry resolution, so the entry is left on its registry tuple instead of silently losing the user's patch (#367). Per-dep; the detail names the key and the remedy (fold the Socket fix into the user's patch, or drop the `patchedDependencies` entry and re-run); the in-run VEX never assumes the uuid applied. Vendored mode refuses the same package `vendor_lock_entry_unsupported` before any write or download. Exit 0. |
| `redirect_vlt_lock_unsupported` | `redirect.warnings[]` (warning) | scan/get `--mode hosted` (vlt): `vlt-lock.json` has a `lockfileVersion` other than absent, `0` or `1` (decided on the raw JSON token), is not a JSON object, starts with a UTF-8 BOM, or its `nodes` section is not vlt's one-node-per-line layout. Nothing rewritten; also refuses a vendored → hosted takeover of a `flavor: "vlt"` entry before its revert (`redirect.skipped[].reason`). Exit 0. |
| `redirect_requirements_takeover_unreachable` | `redirect.warnings[]` (warning) | scan/get `--mode hosted` (pypi / requirements.txt): a vendored → hosted takeover of a package that vendored mode wired through a pin in a `-r` include, or through a `(transitive)` line it appended to the root `requirements.txt`. Hosted mode only rewrites an existing pin in the root `requirements.txt`, so the takeover is refused before the revert (wet and `--dry-run`): the vendored wiring, ledger entry and wheel stay byte-identical, the purl is skipped with this code as `redirect.skipped[].reason`, and nothing is redirected for it. Exit 0. The detail names the remedy and its reach: run `socket-patch vendor --revert` (it reverts EVERY vendored package in the project, not just this one), move the pin from the include into the root `requirements.txt` and delete it from the include (or, for a `(transitive)` line, add an exact `==` pin to the root file), then re-run `scan --mode hosted`. |
| `redirect_vlt_missing_sha512` / `redirect_vlt_entry_not_found` / `redirect_vlt_entry_vendored` / `redirect_vlt_unsupported_lock_key` | `redirect.warnings[]` (warning) | scan/get `--mode hosted` (vlt): the grant has no sha512 / the lock has no default-registry node for `name@version` / the only match is a vendored `file` node under `.socket/vendor/npm/<uuid>/` / a default-registry instance is outside vlt's node-line grammar or still unpatched after the splice. Per dep; none of the dep's instances is written. `redirect_vlt_missing_sha512` and `redirect_vlt_unsupported_lock_key` refuse the dep: it is never confirmed, whichever lock drives (a sibling lock may still carry its rewritten URL). `redirect_vlt_entry_not_found` and `redirect_vlt_entry_vendored` only say `vlt-lock.json` does not wire it: while vlt drives it is not confirmed; otherwise a sibling lock's rules may confirm it. Exit 0. |
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
183 changes: 181 additions & 2 deletions crates/socket-patch-core/src/hosted/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,18 @@ pub async fn read_candidate_files(
}
}
}
// The root manifest's `patchedDependencies` names the packages the
// project patches itself with `bun patch`, which the bun rewriters
// must leave on their registry tuple (#367). Read beside either bun
// lock, advisory too: the member walk above reaches the root only
// through a `workspaces` section in bun's emitted shape.
if !out.files.contains_key("package.json")
&& (out.files.contains_key("bun.lock") || super::vlt::bun_lockb_present(view))
{
if let Some(text) = read_advisory(view, unreadable, "package.json").await {
out.files.insert("package.json".to_string(), text);
}
}
Comment thread
mikolalysenko marked this conversation as resolved.
}

// Cargo workspace members (and in-root path dependencies) declare
Expand Down Expand Up @@ -1075,7 +1087,26 @@ pub async fn rewrite(
.retain(|w| w.code != "redirect_npm_no_lockfile");
match content {
Ok(bytes) => {
crate::patch::redirect::rewrite_bun_binary(&bytes, &overrides, &mut rewrite)
// A package the project patches itself (`bun patch`) keeps
// its registry record, loudly (#367).
let user_patched = crate::vendor::bun_lock_text::patched_dependency_keys(
files.get("package.json").map(String::as_str),
None,
);
let binary_overrides: Vec<DepOverride> = overrides
.iter()
.filter(|o| {
o.ecosystem != "npm"
|| !crate::patch::redirect::skip_bun_user_patched(
&user_patched,
&crate::patch::redirect::full_name(o),
o,
&mut rewrite,
)
})
.cloned()
.collect();
crate::patch::redirect::rewrite_bun_binary(&bytes, &binary_overrides, &mut rewrite)
}
Err(warning) => rewrite.warnings.push(warning),
}
Expand Down Expand Up @@ -1562,7 +1593,8 @@ fn confirm(
let uuid = c.dep.patch_uuid.as_str();
// vlt decides before the binary-bun rule, so `bun.lockb` beside
// a vlt-driven `vlt-lock.json` never confirms an npm purl.
if rewrite.refused_vlt_uuids.contains(uuid) {
if rewrite.refused_vlt_uuids.contains(uuid) || rewrite.refused_bun_uuids.contains(uuid)
{
return ProbeStep::Decided(false);
}
if rewrite.vlt_drives && purl.starts_with("pkg:npm/") {
Expand Down Expand Up @@ -2170,6 +2202,153 @@ mod tests {
assert!(redirected(&done), "{:?}", done.rewrite.warnings);
}

/// REGRESSION (#367), binary lock: a `bun.lockb`-only project's root
/// manifest is read for its `patchedDependencies`, and a package the
/// project patches itself with `bun patch` keeps its registry record,
/// loudly, and is never assumed patched by the in-run VEX.
#[tokio::test]
async fn issue_367_bun_lockb_keeps_a_user_patched_package_on_the_registry() {
use crate::patch::redirect::Integrity;
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/bun-lockb-bundled/both");
let candidates = vec![Candidate {
purl: "pkg:npm/is-number@7.0.0".into(),
dep: DepOverride {
ecosystem: "npm".into(),
name: "is-number".into(),
namespace: None,
version: "7.0.0".into(),
token: "tok".into(),
patch_uuid: "uuid".into(),
artifact_url: "https://patch.test/is-number-7.0.0.tgz".into(),
registry_override: None,
integrity: Integrity {
sha512: Some(format!("sha512-{}==", "A".repeat(86))),
..Default::default()
},
},
}];
let manifest = r#"{"name":"p","version":"1.0.0","dependencies":{"@bh/bund":"1.0.0","is-number":"7.0.0"}}"#;
let patched_manifest = manifest.replacen(
"}}",
r#"},"patchedDependencies":{"is-number@7.0.0":"patches/is-number@7.0.0.patch"}}"#,
1,
);
for user_patched in [false, true] {
let tmp = tempfile::tempdir().unwrap();
std::fs::copy(fixture.join("bun.lockb"), tmp.path().join("bun.lockb")).unwrap();
std::fs::write(
tmp.path().join("package.json"),
if user_patched {
&patched_manifest
} else {
manifest
},
)
.unwrap();
let view = ProjectView::Disk(tmp.path());
let outer = OuterAllowRemote::default;
let options = RewriteOptions {
dry_run: false,
targets_pipenv_lock: false,
pipenv_major: None,
pipenv_unknown_detail: String::new(),
trust_lockfile_config: true,
npm_allow_remote_config: true,
npm_outer: &outer,
blocking: false,
};
let read = read_candidate_files(&view, &BTreeSet::new(), &candidates).await;
assert!(read.files.contains_key("package.json"));
let done = rewrite(
&view,
read,
&candidates,
BTreeMap::new(),
&BTreeSet::new(),
&[],
options,
)
.await;
let skipped = done
.rewrite
.warnings
.iter()
.find(|w| w.code == "redirect_bun_patched_dependency_skipped");
if user_patched {
assert!(
!done.rewrite.binary_files.contains_key("bun.lockb"),
"the user-patched record is left alone"
);
let skipped = skipped.expect("the skip is reported");
assert!(
skipped.detail.contains("is-number@7.0.0"),
"{}",
skipped.detail
);
assert!(done.rewrite.bundled_skipped_uuids.contains("uuid"));
} else {
assert!(
done.rewrite.binary_files.contains_key("bun.lockb"),
"{:?}",
done.rewrite.warnings
);
assert!(skipped.is_none(), "{:?}", done.rewrite.warnings);
}
assert!(!done.rewrite.files.contains_key("package.json"));
}
}

/// REGRESSION (#367), text lock: the root manifest is read beside a
/// `bun.lock` even when the lock has no `workspaces` section to reach it
/// through, and a package the project patches itself is never
/// confirmed, not even when a sibling `package-lock.json` takes the
/// hosted URL: Bun keeps installing the registry bytes.
#[tokio::test]
async fn issue_367_bun_lock_user_patched_package_is_never_confirmed() {
let bun_lock = "{\n \"lockfileVersion\": 1,\n \"packages\": {\n \"left-pad\": \
[\"left-pad@1.3.0\", \"\", {}, \"sha512-UPSTREAM==\"],\n }\n}\n";
let npm_lock = r#"{
"name": "app",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": { "name": "app", "dependencies": { "left-pad": "1.3.0" } },
"node_modules/left-pad": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
"integrity": "sha512-UPSTREAM=="
}
}
}
"#;
let manifest = r#"{"name":"app","dependencies":{"left-pad":"1.3.0"},"patchedDependencies":{"left-pad@1.3.0":"patches/left-pad@1.3.0.patch"}}"#;
// Without the sibling npm lock nothing else reads the manifest.
for with_npm_lock in [false, true] {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("bun.lock"), bun_lock).unwrap();
if with_npm_lock {
std::fs::write(tmp.path().join("package-lock.json"), npm_lock).unwrap();
}
std::fs::write(tmp.path().join("package.json"), manifest).unwrap();
let (read, done) = npm_rewrite(&ProjectView::Disk(tmp.path()), &BTreeSet::new()).await;
assert!(read.files.contains_key("package.json"));
assert!(
!done.rewrite.files.contains_key("bun.lock"),
"the user-patched entry keeps its registry tuple"
);
assert!(
done.rewrite
.warnings
.iter()
.any(|w| w.code == "redirect_bun_patched_dependency_skipped"),
"{:?}",
done.rewrite.warnings
);
assert!(done.confirmed.is_empty(), "{:?}", done.confirmed);
}
}

fn gem_candidate() -> Candidate {
use crate::patch::redirect::{Integrity, RegistryOverride, RegistryOverrideIdentifiers};
Candidate {
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
Loading
Loading