diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index a1a7b20f1..a11cd1b47 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -1475,7 +1475,7 @@ command's integration tests check its output. | `vendor_would_revert_redirect` / `vendor_takeover_reverted_redirect` | `skipped` (advisory event) | vendor / scan / get `--mode vendored` over a hosted pin (every ecosystem, v5.0): dry run — the upstream restore was resolved (registry lookups included) and would succeed (for bun, only after the Bun vendored preflight accepted the lock; a refused lock is previewed as the wet run's `failed ` instead) / wet run — the pin's lock entries were restored to their upstream registry entry before vendoring (mode takeover; detail ` was hosted; restored its upstream registry entry () before vendoring (mode takeover)`), so `vendor --revert` later returns to upstream. Fires on the run that takes over, not on re-runs, and not for a purl whose takeover was rolled back because the backend refused it (see "Takeover reconciliation"). | | `redirect_revert_failed` | `failed` | vendor / scan / get `--mode vendored` (dry and wet): the upstream restore of a hosted pin was refused (`--offline`, a registry that does not answer, a lock shape the restore refuses — for `bun.lockb`, a record the codec cannot rebuild) — detail `cannot vendor over the live hosted pin: cannot restore to its upstream registry entry: ; restore it from version control instead (`git checkout -- `)` (for `bun.lock` / `bun.lockb` the detail also adds `, then run \`bun install --force\` (a plain \`bun install\` keeps the patched copy)`); nothing vendored for the purl, hosted wiring left in place, exit 1 `partialFailure`. | | `patch_fetch_failed` (eject) | `failed` | vendor eject (v5.0): a hosted pin's patch record could not be fetched from `…/patches/view/`; the whole eject is refused (`eject_refused`), nothing touched, exit 1. | -| `redirect_pnpm_lockfile_elsewhere` / `redirect_workspace_lockfile_elsewhere` / `cargo_manifest_not_workspace_root` (hosted) | top-level `error.code` (`status: "error"`) | scan / get `--mode hosted` (v5.0): the project directory is a workspace member whose lock lives in another directory, so the rewriters, which read only the project directory, would pin nothing (pnpm: no npm-family lock here, and the nearest ancestor `pnpm-workspace.yaml` or the project's `lockfile-dir` (`.npmrc`) / `lockfileDir` (`pnpm-workspace.yaml`) puts `pnpm-lock.yaml` elsewhere; npm / yarn / Bun, `redirect_workspace_lockfile_elsewhere`: no npm-family lock here, and the nearest ancestor `package.json` whose `workspaces` (array, or the object form's `packages`) matches the directory holds `package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock`, `bun.lock` or `bun.lockb`; a matching root with none of them that is itself listed by an outer root's `workspaces` hands the check to that root; vlt, same code: the nearest ancestor `vlt.json` whose `workspaces` (a string, an array, or an object of groups) matches the directory holds `vlt-lock.json`, or, as vlt falls back to it when `vlt.json` has no `workspaces` field, the `package.json` `workspaces` root above holds `vlt-lock.json`, and the nearer of a `vlt.json` and a `package.json` root is named; `workspaces` patterns use the glob grammar the package managers share: `*`, `?`, `**`, brace sets and sequences (`{a,b}`, `{1..3}`) and character classes (`[a-c]`, `[!a]`), read the way the root's lock's package manager reads them (#1097): extglobs (`@(a|b)`, `?(…)`, `*(…)`, `+(…)`, `!(…)`) match for npm and yarn but not Bun or vlt; yarn berry (picomatch) also reads regex groups (`packages/(a|b)`; a group without `|` only in a pattern fast-glob deems dynamic, or in a `!` pattern), the regex quantifiers `?` / `+` after a group, `+` after a class and `+` inside a group, POSIX classes (`[[:alpha:]]`) and `..` segments in a static pattern or after a static prefix (`packages/a/../b` lists `packages/b`; literal in a `!` pattern); a group alternative holding `/` (`(packages/ui|apps/web)`) is matched across components, and a `?` or `+` quantifying such a group fails closed (the pattern lists every directory, and excludes none as a `!` pattern) (#1490); a `!` pattern excludes what earlier patterns matched for npm, Bun 1.3+ (a `bun.lock` with a top-level `configVersion`) and vlt, excludes whatever the order for yarn berry (a `yarn.lock` with `__metadata`, where a leading `!(` is an extglob), and is ignored by yarn classic and older Bun (a `bun.lock` without `configVersion`, or `bun.lockb` alone); only the root's locks whose package manager lists the directory govern it (and are named), and a lockless root lists it when any of them would; when a pnpm workspace also governs the directory, the nearer root is named and a tie goes to `redirect_pnpm_lockfile_elsewhere`; uv (pypi, #1138), `redirect_workspace_lockfile_elsewhere`: the directory holds a `pyproject.toml`, and the nearest ancestor `pyproject.toml` declaring `[tool.uv.workspace]` lists it in `members` (and not in `exclude`), with no standalone project (`[project]`, no workspace) in between, so uv installs it from that root's `uv.lock` (a `uv.lock`, `poetry.lock`, `pdm.lock`, `Pipfile.lock` or pylock left in the member is never read and does not exempt it); the message says uv workspaces are not patched from their root yet either, and vendored refuses the same layout with `pypi_uv_workspace_unsupported` instead of rewriting a member with Hatch configuration as a lockless Hatch project; a directory whose own locks are all ones its manager never reads inside a workspace member is refused the same way, naming the ignored locks: `package-lock.json` / `npm-shrinkwrap.json` when its `package.json` `workspaces` root holds `package-lock.json` or `npm-shrinkwrap.json` (npm, #1094), `bun.lock` / `bun.lockb` when that root holds `bun.lock` or `bun.lockb` (Bun, #1101), and `vlt-lock.json` in a directory with no `vlt.json` of its own when its vlt workspace root (as above) holds `vlt-lock.json` (vlt, #1134); vendored refuses it with `vendor_lockfile_missing`, and `vex` reads the ignored lock as absent, with one `patched_ref_unattributable` warning naming it when it holds Socket references) or rewrite the member as a lockless project (cargo: the vendored workspace-root check). Refused before any takeover or write, `--dry-run` included; the message names the directory to run from; exit 1. Disk runs only (an in-memory project has no ancestors). | +| `redirect_pnpm_lockfile_elsewhere` / `redirect_workspace_lockfile_elsewhere` / `cargo_manifest_not_workspace_root` (hosted) | top-level `error.code` (`status: "error"`) | scan / get `--mode hosted` (v5.0): the project directory is a workspace member whose lock lives in another directory, so the rewriters, which read only the project directory, would pin nothing (pnpm: no npm-family lock here, and the nearest ancestor `pnpm-workspace.yaml` or the project's `lockfile-dir` (`.npmrc`) / `lockfileDir` (`pnpm-workspace.yaml`) puts `pnpm-lock.yaml` elsewhere; npm / yarn / Bun, `redirect_workspace_lockfile_elsewhere`: no npm-family lock here, and the nearest ancestor `package.json` whose `workspaces` (array, or the object form's `packages`) matches the directory holds `package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock`, `bun.lock` or `bun.lockb`; a matching root with none of them that is itself listed by an outer root's `workspaces` hands the check to that root; vlt, same code: the nearest ancestor `vlt.json` whose `workspaces` (a string, an array, or an object of groups) matches the directory holds `vlt-lock.json`, or, as vlt falls back to it when `vlt.json` has no `workspaces` field, the `package.json` `workspaces` root above holds `vlt-lock.json`, and the nearer of a `vlt.json` and a `package.json` root is named; `workspaces` patterns use the glob grammar the package managers share: `*`, `?`, `**`, brace sets and sequences (`{a,b}`, `{1..3}`) and character classes (`[a-c]`, `[!a]`), read the way the root's lock's package manager reads them (#1097): extglobs (`@(a|b)`, `?(…)`, `*(…)`, `+(…)`, `!(…)`) match for npm and yarn but not Bun or vlt; yarn berry (picomatch) also reads regex groups (`packages/(a|b)`; a group without `|` only in a pattern fast-glob deems dynamic, or in a `!` pattern), the regex quantifiers `?` / `+` after a group, `+` after a class and `+` inside a group, POSIX classes (`[[:alpha:]]`) and `..` segments in a static pattern or after a static prefix (`packages/a/../b` lists `packages/b`; literal in a `!` pattern); a group alternative holding `/` (`(packages/ui|apps/web)`) is matched across components, and a `?` or `+` quantifying such a group fails closed (the pattern lists every directory, and excludes none as a `!` pattern) (#1490); a `!` pattern excludes what earlier patterns matched for npm, Bun 1.3+ (a `bun.lock` with a top-level `configVersion`) and vlt, excludes whatever the order for yarn berry (a `yarn.lock` with `__metadata`, where a leading `!(` is an extglob), and is ignored by yarn classic and older Bun (a `bun.lock` without `configVersion`, or a `bun.lockb` alone without the trailing `cNfGvRsN` config-version extension, its binary twin, #1492); only the root's locks whose package manager lists the directory govern it (and are named), and a lockless root lists it when any of them would; when a pnpm workspace also governs the directory, the nearer root is named and a tie goes to `redirect_pnpm_lockfile_elsewhere`; uv (pypi, #1138), `redirect_workspace_lockfile_elsewhere`: the directory holds a `pyproject.toml`, and the nearest ancestor `pyproject.toml` declaring `[tool.uv.workspace]` lists it in `members` (and not in `exclude`), with no standalone project (`[project]`, no workspace) in between, so uv installs it from that root's `uv.lock` (a `uv.lock`, `poetry.lock`, `pdm.lock`, `Pipfile.lock` or pylock left in the member is never read and does not exempt it); the message says uv workspaces are not patched from their root yet either, and vendored refuses the same layout with `pypi_uv_workspace_unsupported` instead of rewriting a member with Hatch configuration as a lockless Hatch project; a directory whose own locks are all ones its manager never reads inside a workspace member is refused the same way, naming the ignored locks: `package-lock.json` / `npm-shrinkwrap.json` when its `package.json` `workspaces` root holds `package-lock.json` or `npm-shrinkwrap.json` (npm, #1094), `bun.lock` / `bun.lockb` when that root holds `bun.lock` or `bun.lockb` (Bun, #1101), and `vlt-lock.json` in a directory with no `vlt.json` of its own when its vlt workspace root (as above) holds `vlt-lock.json` (vlt, #1134); vendored refuses it with `vendor_lockfile_missing`, and `vex` reads the ignored lock as absent, with one `patched_ref_unattributable` warning naming it when it holds Socket references) or rewrite the member as a lockless project (cargo: the vendored workspace-root check). Refused before any takeover or write, `--dry-run` included; the message names the directory to run from; exit 1. Disk runs only (an in-memory project has no ancestors). | | `redirect_pnpm_settings_elsewhere` | top-level `error.code` (`status: "error"`) | scan / get `--mode hosted`: the project directory is a pnpm workspace member (listed by the `packages:` globs of the nearest ancestor `pnpm-workspace.yaml`) with its own v9 `pnpm-lock.yaml` (`sharedWorkspaceLockfile: false`) and no `pnpm-workspace.yaml` of its own, so its pnpm settings come from that ancestor file, which pnpm reads alone (a member's own file is ignored). A directory those globs do not list (no `packages:`, an empty list, a non-matching or `!`-excluded path) is a standalone project on pnpm 11.28+/12 that reads only its own file: it is pinned and gets its own `pnpm-workspace.yaml` like any single project. A root file that does not parse, or whose patterns use braces, classes or extglobs, counts as listing the project. When that file neither carries `trustLockfile: true` nor explicitly sets another value, the trust auto-config has nowhere to go: refused before any takeover or write, `--dry-run` included; the message names the root file to add `trustLockfile: true` to (or `--no-trust-lockfile-config` pins without it); exit 1. Once the root file trusts the lock (or opts out), the member is pinned and no nested `pnpm-workspace.yaml` is created; the `redirect_pnpm_trust_lockfile` warning names the root file. In memory, a member whose lock is demoted into its workspace root (#492) is never refused; one whose lock is not (the workspace root's files do not confirm it pins or ignores that lock, or socket.yml leaves the root out) is refused with this code as its project error, nothing written for it, whenever its lock is v9, the trust auto-config is on and that file may list it (listed, unreadable, or not readable as globs), whatever it says about `trustLockfile`. | | `eject_refused` | top-level `error.code` (`status: "error"`) | vendor eject (v5.0): a record fetch failed or a pin's upstream restore was refused while planning; nothing was changed, exit 1. | | `eject_planned` | `applied` (reason) | vendor eject `--dry-run` (v5.0): the pin would be restored upstream and vendored; nothing written. | diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs b/crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs index c231a4969..6c719c56f 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs @@ -1837,7 +1837,8 @@ async fn hosted_scan_from_brace_or_class_glob_workspace_member_refuses() { /// #1097: the member matcher read every root's `workspaces` the npm way. /// yarn classic ignores `!` patterns (`packages/a` stays a member of /// `["packages/*", "!packages/a"]`), as does Bun before 1.3 (a `bun.lock` -/// with no `configVersion`, or a binary `bun.lockb`), and npm and yarn +/// with no `configVersion`, or a binary `bun.lockb` without its +/// `cNfGvRsN` twin, #1492), and npm and yarn /// match extglobs (`packages/@(a|b)`). The matcher dropped those members, /// so a hosted `scan` / `get` from one pinned nothing and exited 0. #[tokio::test] diff --git a/crates/socket-patch-core/src/hosted/governing_root.rs b/crates/socket-patch-core/src/hosted/governing_root.rs index 79ecb9cda..360816407 100644 --- a/crates/socket-patch-core/src/hosted/governing_root.rs +++ b/crates/socket-patch-core/src/hosted/governing_root.rs @@ -27,9 +27,12 @@ use crate::formats::text::strip_bom; use crate::formats::yarn::berry_gates::YARN_LOCK; use crate::formats::yarn::is_berry_lock; use crate::patch::redirect::npmrc::npmrc_top_level_value; -use crate::utils::fs::{open_regular_file, read_regular_to_string, read_regular_to_string_sync}; +use crate::utils::fs::{ + open_regular_file, open_regular_file_sync, read_regular_to_string, read_regular_to_string_sync, +}; use crate::utils::pnpm_workspace::governing_workspace_file; use crate::utils::workspace_globs::{workspaces_include, workspaces_include_as, WorkspacesDialect}; +use crate::vendor::bun_lockb; use crate::vendor::cargo::NOT_WORKSPACE_ROOT; use crate::vendor::cargo_manifest; use crate::vendor::lock_inventory::ProjectView; @@ -547,10 +550,11 @@ async fn package_json_workspace_root(root: &Path) -> Option<(PathBuf, Vec<&'stat /// reads `lock` at `root` (#1097). yarn classic and berry tell apart by /// berry's `__metadata` key ([`is_berry_lock`]; an empty or unreadable /// `yarn.lock` reads as classic, the wider set), and Bun 1.3+ by the -/// top-level `configVersion` it writes into `bun.lock` (a lock without -/// it, or a binary `bun.lockb` with no `bun.lock` beside it, reads as -/// older Bun, the wider set). Only the head of a lock is read: both keys -/// come first. +/// top-level `configVersion` it writes into `bun.lock`, or the trailing +/// `cNfGvRsN` extension that carries it in a binary `bun.lockb` (#1492; +/// a lock without it, as Bun 1.3.0 and earlier write, reads as older Bun, +/// the wider set). Only the head of a text lock is read: both keys come +/// first. async fn lock_dialect(root: &Path, lock: &str) -> WorkspacesDialect { match lock { YARN_LOCK => { @@ -573,12 +577,34 @@ async fn lock_dialect(root: &Path, lock: &str) -> WorkspacesDialect { } // Bun reads the text lock when both exist. BUN_LOCKB if root.join(BUN_LOCK).is_file() => Box::pin(lock_dialect(root, BUN_LOCK)).await, - BUN_LOCKB => WorkspacesDialect::BUN_PRE_1_3, + // Bun marks a binary lock with its config version too (#1492). + BUN_LOCKB => { + if bun_lockb_config_version(&root.join(lock)).await.is_some() { + WorkspacesDialect::BUN + } else { + WorkspacesDialect::BUN_PRE_1_3 + } + } VLT_LOCK => WorkspacesDialect::VLT, _ => WorkspacesDialect::NPM, } } +/// The `configVersion` a binary `bun.lockb` carries in its `cNfGvRsN` +/// extension (#1492), read through the FIFO-safe opener on the blocking +/// pool. `None` when the lock is unreadable, not a binary lock, or +/// untagged (written before Bun wrote `configVersion`). +async fn bun_lockb_config_version(path: &Path) -> Option { + let path = path.to_path_buf(); + tokio::task::spawn_blocking(move || { + let (mut file, _) = open_regular_file_sync(&path).ok()?; + bun_lockb::read_config_version(&mut file) + }) + .await + .ok() + .flatten() +} + /// How much of a lock [`lock_dialect`] reads: the yarn berry and Bun keys /// it looks for sit in the first lines. const LOCK_HEAD_BYTES: u64 = 4096; @@ -1370,6 +1396,59 @@ mod tests { ); } + /// #1492: Bun 1.2+ still writes a binary `bun.lockb` under + /// `saveTextLockfile = false`, and Bun 1.3.x+ marks it with a + /// `cNfGvRsN` config-version tag (the twin of the text lock's + /// `configVersion`). A tagged lone `bun.lockb` honours `!`, so the + /// excluded member's own lock is the one Bun reads; an untagged one + /// (Bun 1.2.23 here) or one that is not a binary lock keeps the wider + /// pre-1.3 reading. + #[tokio::test] + async fn bun_lockb_with_a_config_version_tag_honours_negated_workspaces() { + const BUN_1_3_14: &[u8] = include_bytes!("../../tests/fixtures/bun-lockb/1.3.14/bun.lockb"); + const BUN_1_2_23: &[u8] = include_bytes!("../../tests/fixtures/bun-lockb/1.2.23/bun.lockb"); + // A workspace root's lock: the tag follows its workspace and + // catalog extensions. + const BUN_1_4_2_WORKSPACE: &[u8] = + include_bytes!("../../tests/fixtures/bun-lockb/1.4.2-extensions/bun.lockb"); + // Scoped overrides: Bun writes their extension after the tag. + const BUN_1_4_2_SCOPED: &[u8] = + include_bytes!("../../tests/fixtures/bun-lockb/1.4.2-scoped-overrides/bun.lockb"); + let refused = Some(WORKSPACE_LOCKFILE_ELSEWHERE); + for (lock, expected) in [ + (BUN_1_3_14, None), + (BUN_1_4_2_WORKSPACE, None), + (BUN_1_4_2_SCOPED, None), + (BUN_1_2_23, refused), + (&b"\0"[..], refused), + ] { + let tmp = tempfile::tempdir().unwrap(); + write( + tmp.path(), + "package.json", + r#"{"private":true,"workspaces":["packages/*","!packages/b"]}"#, + ); + std::fs::write(tmp.path().join("bun.lockb"), lock).unwrap(); + write(tmp.path(), "packages/b/package.json", "{}"); + let member = tmp.path().join("packages/b"); + assert_eq!( + code(&member, "npm").await.as_deref(), + expected, + "bun.lockb of {} bytes", + lock.len() + ); + // The member's own binary lock is a stray only when the + // root's Bun lists the member. + std::fs::write(member.join("bun.lockb"), lock).unwrap(); + assert_eq!( + member_stray_lock(&member).await.is_some(), + expected.is_some(), + "bun.lockb of {} bytes", + lock.len() + ); + } + } + /// #1097: a lockless intermediate root reads its `workspaces` in the /// widest dialect, so a member it lists only through an extglob, or /// excludes only with `!`, still reaches the outer root's lock. diff --git a/crates/socket-patch-core/src/vendor/bun_lockb.rs b/crates/socket-patch-core/src/vendor/bun_lockb.rs index 7cb80debf..6d051150c 100644 --- a/crates/socket-patch-core/src/vendor/bun_lockb.rs +++ b/crates/socket-patch-core/src/vendor/bun_lockb.rs @@ -15,6 +15,7 @@ use serde_json::{json, Value}; use sha2::{Digest, Sha512_256}; use std::cmp::Ordering; use std::collections::VecDeque; +use std::io::{Read, Seek, SeekFrom}; use std::ops::Range; const HEADER: &[u8] = b"#!/usr/bin/env bun\nbun-lockfile-format-v0\n"; @@ -49,6 +50,91 @@ pub(crate) const NORMALIZED_FORMAT_1: u8 = 1; /// Workspace dependency behaviors were normalized (not invertible). pub(crate) const NORMALIZED_WORKSPACE: u8 = 2; +/// Bun's `configVersion` from a binary lock's `cNfGvRsN` extension (the +/// twin of the text lock's top-level key, #1492), or `None` for a lock +/// that is not binary, is untagged (Bun 1.3.0 and earlier) or does not +/// hold together. Walks only the structure (the header, the six buffer +/// descriptors and the extension tags), seeking past every array, so +/// neither package records nor the order of the extensions matters (Bun +/// writes `sCoPdOvR` after the tag), and a large lock costs a few small +/// reads. Like [`BunLockb::parse`], it stops at an unknown tag. +pub(crate) fn read_config_version(lock: &mut R) -> Option { + let mut head = [0; PACKAGES_AT + 40]; + lock.read_exact(&mut head).ok()?; + if !head.starts_with(HEADER) { + return None; + } + let total = usize_at(&head, TOTAL_AT).ok()?; + let mut pos = usize_at(&head, PACKAGES_AT + 32).ok()?; + for _ in 0..6 { + pos = skip_array(lock, pos, total)?.end; + } + if read_u64(lock, pos, total)? != 0 { + return None; + } + pos += 8; + while pos.checked_add(8).is_some_and(|end| end <= total) { + let mut tag = [0; 8]; + read_at(lock, pos, &mut tag, total)?; + let n = match &tag { + b"wOrKsPaC" => 4, + b"tRuStEDd" => 1, + b"eMpTrUsT" => 0, + b"oVeRriDs" | b"pAtChEdD" => 2, + b"sCoPdOvR" => 3, + b"cNfGvRsN" => return read_u64(lock, pos + 8, total), + b"cAtAlOgS" => { + pos += 8; + let mut groups = 0; + for _ in 0..3 { + let array = skip_array(lock, pos, total)?; + groups = array.len() / 8; + pos = array.end; + } + for _ in 0..groups * 2 { + pos = skip_array(lock, pos, total)?.end; + } + continue; + } + _ => return None, + }; + pos += 8; + for _ in 0..n { + pos = skip_array(lock, pos, total)?.end; + } + } + None +} + +fn read_at(lock: &mut R, at: usize, buf: &mut [u8], total: usize) -> Option<()> { + if at.checked_add(buf.len())? > total { + return None; + } + lock.seek(SeekFrom::Start(at as u64)).ok()?; + lock.read_exact(buf).ok() +} + +fn read_u64(lock: &mut R, at: usize, total: usize) -> Option { + let mut bytes = [0; 8]; + read_at(lock, at, &mut bytes, total)?; + Some(u64::from_le_bytes(bytes)) +} + +/// The data range of the array whose descriptor is at `descriptor`, with +/// [`read_array`]'s checks; each array starts past its descriptor, so a +/// walk over them always moves forward. +fn skip_array( + lock: &mut R, + descriptor: usize, + total: usize, +) -> Option> { + let mut bytes = [0; 16]; + read_at(lock, descriptor, &mut bytes, total)?; + let start = usize_at(&bytes, 0).ok()?; + let end = usize_at(&bytes, 8).ok()?; + (start >= descriptor + 16 && start <= end && end <= total).then_some(start..end) +} + #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct BinaryPackage { pub(crate) id: usize, @@ -2280,6 +2366,41 @@ mod tests { format!("sha512-{}", STANDARD.encode([42; 64])) } + fn config_version_of(lock: &[u8]) -> Option { + read_config_version(&mut std::io::Cursor::new(lock)) + } + + /// #1492: Bun writes a `cNfGvRsN` config-version extension (the twin + /// of the text lock's `configVersion`) since a 1.3 patch release, after + /// the workspace and catalog extensions of a workspace root and before + /// the scoped-overrides one; 1.3.0 and earlier writers do not. + #[test] + fn config_version_is_read_from_tagged_locks_only() { + let extra = [ + "prerelease-pair-1.3.14", + "1.4.2-extensions", + "1.4.2-scoped-overrides", + "1.2.23-extensions", + ]; + for version in VERSIONS.iter().chain(&extra) { + let lock = fixture(version); + let tagged = version.contains("1.3.14") || version.starts_with("1.4.2"); + assert_eq!(config_version_of(&lock), tagged.then_some(1), "{version}"); + } + for name in ["1.3.9-ws-deps-late.lockb", "1.4.2-ws-adder.lockb"] { + let lock = std::fs::read( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/bun-lockb/late-dependent") + .join(name), + ) + .unwrap(); + assert_eq!(config_version_of(&lock), Some(1), "{name}"); + } + // Not a binary lock, or a header with nothing after it. + assert_eq!(config_version_of(b"\0"), None); + assert_eq!(config_version_of(HEADER), None); + } + #[test] fn every_historical_binary_layout_parses_losslessly() { for version in VERSIONS { diff --git a/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/bun.lockb b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/bun.lockb new file mode 100644 index 000000000..fea0d06ee Binary files /dev/null and b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/bun.lockb differ diff --git a/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/bunfig.toml b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/bunfig.toml new file mode 100644 index 000000000..30e63e0e1 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/bunfig.toml @@ -0,0 +1,2 @@ +[install] +saveTextLockfile = false diff --git a/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/package.json b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/package.json new file mode 100644 index 000000000..78fe588b2 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/package.json @@ -0,0 +1,15 @@ +{ + "name": "binary-scoped-overrides-fixture", + "version": "1.0.0", + "private": true, + "workspaces": ["packages/*", "!packages/b"], + "dependencies": { + "minimist": "1.2.2", + "is-number": "7.0.0" + }, + "overrides": { + "is-number": { + "left-pad": "1.3.0" + } + } +} diff --git a/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/packages/a/package.json b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/packages/a/package.json new file mode 100644 index 000000000..008d7d1fb --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/packages/a/package.json @@ -0,0 +1 @@ +{"name":"a","version":"1.0.0"} diff --git a/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/packages/b/package.json b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/packages/b/package.json new file mode 100644 index 000000000..363a869fd --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/packages/b/package.json @@ -0,0 +1 @@ +{"name":"b","version":"1.0.0"} diff --git a/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/provenance.json b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/provenance.json new file mode 100644 index 000000000..4b4b4fdd9 --- /dev/null +++ b/crates/socket-patch-core/tests/fixtures/bun-lockb/1.4.2-scoped-overrides/provenance.json @@ -0,0 +1,7 @@ +{ + "bun": "1.4.2", + "os": "linux", + "arch": "x86_64", + "command": "bun install --ignore-scripts", + "sha256": "b5b1bcb3da1c54bc439014d0e1cbe3d214af5cbb7412a9060a46d7e7404655a6" +} diff --git a/crates/socket-patch-core/tests/fixtures/bun-lockb/README.md b/crates/socket-patch-core/tests/fixtures/bun-lockb/README.md index ff3556d89..0caf38abe 100644 --- a/crates/socket-patch-core/tests/fixtures/bun-lockb/README.md +++ b/crates/socket-patch-core/tests/fixtures/bun-lockb/README.md @@ -14,6 +14,10 @@ and an empty `BUN_INSTALL_CACHE_DIR` to regenerate. Bun 1.2+ fixtures include - 1.3.14 / 1.4.2: current optional configuration extensions. - `*-extensions`: root and workspace scripts, a GitHub resolution, and catalogs on 1.2.23 and 1.4.2. +- `1.4.2-scoped-overrides`: a nested `overrides` entry (unused by the + installed packages), whose `sCoPdOvR` + extension Bun writes after `cNfGvRsN`, and a `!` workspace pattern + (#1492); written on Linux x64. - `0.8.1-production` / `1.0.0-production`: a production dependency and a different version aliased in dev dependencies, pinning production-filter string-pool reconstruction on early binary readers. diff --git a/crates/socket-patch-core/tests/fixtures/vex-discover-golden/bun-lockb.json b/crates/socket-patch-core/tests/fixtures/vex-discover-golden/bun-lockb.json index 50fd530f2..c1e41b0b7 100644 --- a/crates/socket-patch-core/tests/fixtures/vex-discover-golden/bun-lockb.json +++ b/crates/socket-patch-core/tests/fixtures/vex-discover-golden/bun-lockb.json @@ -438,6 +438,39 @@ "elsewhere": [], "live_claims": [] }, + "bun-lockb/1.4.2-scoped-overrides": { + "refs": [], + "diagnostics": [], + "recognized": [], + "unlocked_pins": [], + "elsewhere": [ + { + "purl": "pkg:npm/is-number@7.0.0", + "file": "bun.lockb" + }, + { + "purl": "pkg:npm/minimist@1.2.2", + "file": "bun.lockb" + } + ], + "live_claims": [] + }, + "bun-lockb/1.4.2-scoped-overrides/packages/a": { + "refs": [], + "diagnostics": [], + "recognized": [], + "unlocked_pins": [], + "elsewhere": [], + "live_claims": [] + }, + "bun-lockb/1.4.2-scoped-overrides/packages/b": { + "refs": [], + "diagnostics": [], + "recognized": [], + "unlocked_pins": [], + "elsewhere": [], + "live_claims": [] + }, "bun-lockb/late-dependent": { "refs": [], "diagnostics": [],