Skip to content

Commit 88c444c

Browse files
committed
Merge release/v5-prerelease (#281) into v5/remove-setup-and-ui
Keep this branch's deletion of the setup-only package_json module (#281 had only repointed find.rs at the format registry), and take #281's registry-backed npm_family in constants.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj
2 parents e824129 + 73c0c4f commit 88c444c

62 files changed

Lines changed: 4523 additions & 3350 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/socket-patch-cli/src/commands/get.rs‎

Lines changed: 8 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use socket_patch_core::api::types::{
1111
};
1212
use socket_patch_core::crawlers::fuzzy_match::fuzzy_match_packages;
1313
use socket_patch_core::crawlers::{CrawlerOptions, Ecosystem};
14+
use socket_patch_core::formats::pnpm::PnpmLock;
1415
use socket_patch_core::manifest::operations::{read_manifest, write_manifest};
1516
use socket_patch_core::manifest::schema::{
1617
PatchFileInfo, PatchManifest, PatchRecord, VulnerabilityInfo,
@@ -1494,47 +1495,6 @@ fn purl_has_version(purl: &str) -> bool {
14941495
})
14951496
}
14961497

1497-
/// Does the raw pnpm-lock text RESOLVE `name@version`? Boundary-anchored
1498-
/// probes over the three lock grammars — a plain `contains` collides on
1499-
/// version prefixes (`left-pad@1.3.0` matches inside
1500-
/// `left-pad@1.3.0-beta.1`), name suffixes (`pad@1.3.0` inside
1501-
/// `left-pad@1.3.0`), and unscoped-inside-scoped names (`name@1.0.0` inside
1502-
/// `@scope/name@1.0.0`). The needles cover v6/v9's `name@version` and v5's
1503-
/// `/name/version` key spellings; a match counts only when the preceding
1504-
/// char cannot extend the name (start/whitespace/quote, or a `/` delimiter
1505-
/// itself preceded by such a boundary) and the following char cannot extend
1506-
/// the version (so `:`, `'`, `(`, and v5's `_peer` suffix all accept).
1507-
/// Heuristic by design: a false negative degrades to a calm skip, a false
1508-
/// positive costs one grant request the rewriter's per-dep confirmation
1509-
/// then ignores.
1510-
fn pnpm_lock_resolves(text: &str, name: &str, version: &str) -> bool {
1511-
let version_boundary = |c: char| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '+'));
1512-
let name_boundary = |c: char| matches!(c, ' ' | '\t' | '\n' | '\r' | '\'' | '"');
1513-
for needle in [format!("{name}@{version}"), format!("/{name}/{version}")] {
1514-
for (pos, _) in text.match_indices(needle.as_str()) {
1515-
let before_ok = match text[..pos].chars().next_back() {
1516-
None => true,
1517-
// v5/v6's leading key delimiter — legitimate only when the
1518-
// char before it is itself a boundary (otherwise this is a
1519-
// scoped `@scope/<name>` tail: a DIFFERENT package).
1520-
Some('/') => text[..pos - 1]
1521-
.chars()
1522-
.next_back()
1523-
.is_none_or(name_boundary),
1524-
Some(c) => name_boundary(c),
1525-
};
1526-
let after_ok = text[pos + needle.len()..]
1527-
.chars()
1528-
.next()
1529-
.is_none_or(version_boundary);
1530-
if before_ok && after_ok {
1531-
return true;
1532-
}
1533-
}
1534-
}
1535-
false
1536-
}
1537-
15381498
/// Outcome of the coarse installed-VERSION narrowing over a CVE/GHSA/PURL
15391499
/// search fan-out (see [`filter_to_installed_purls`]).
15401500
struct InstalledNarrowing {
@@ -1572,7 +1532,7 @@ struct InstalledNarrowing {
15721532
/// `yarn_pnp_unsupported`, not a false "not installed"). pnpm PnP skips
15731533
/// carry `pnpm_pnp_unsupported` in agent/vendored modes; hosted mode — the
15741534
/// refusal's own remedy — keeps the versions the raw pnpm-lock.yaml text
1575-
/// resolves ([`pnpm_lock_resolves`]), labels a judged miss
1535+
/// resolves ([`PnpmLock::resolves`]), labels a judged miss
15761536
/// `package_not_installed` like any other mode, and reserves the layout
15771537
/// code for an unreadable lock (no judgment possible).
15781538
///
@@ -1641,6 +1601,7 @@ async fn filter_to_installed_purls(
16411601
let pnpm_pnp_lock_text: Option<String> = (pnp_pnpm && mode == super::scan::ScanMode::Hosted)
16421602
.then(|| std::fs::read_to_string(common.cwd.join("pnpm-lock.yaml")).ok())
16431603
.flatten();
1604+
let pnpm_pnp_lock = pnpm_pnp_lock_text.as_deref().map(PnpmLock::parse);
16441605

16451606
let mut out = InstalledNarrowing {
16461607
kept: Vec::new(),
@@ -1670,8 +1631,8 @@ async fn filter_to_installed_purls(
16701631
// The pnpm PnP refusal's own remedy is the hosted lockfile
16711632
// rewrite — but only for versions the lock ACTUALLY resolves:
16721633
// keeping the whole fan-out would request grants for every
1673-
// version ever patched. Anchored probe over the raw lock text
1674-
// (see `pnpm_lock_resolves`); a hit is kept (the rewriter's
1634+
// version ever patched. The lock model's key probe
1635+
// (`PnpmLock::resolves`); a hit is kept (the rewriter's
16751636
// per-dep confirmation still decides). A judged MISS is a
16761637
// genuine "version not resolved" verdict — the layout blocked
16771638
// nothing — so it carries the same `package_not_installed` code
@@ -1680,9 +1641,9 @@ async fn filter_to_installed_purls(
16801641
let decoded = canon(&result.purl);
16811642
let coord = decoded.strip_prefix("pkg:npm/").unwrap_or(&decoded);
16821643
if mode == super::scan::ScanMode::Hosted {
1683-
match (pnpm_pnp_lock_text.as_deref(), coord.rsplit_once('@')) {
1684-
(Some(text), Some((name, version))) => {
1685-
if pnpm_lock_resolves(text, name, version) {
1644+
match (&pnpm_pnp_lock, coord.rsplit_once('@')) {
1645+
(Some(lock), Some((name, version))) => {
1646+
if lock.resolves(name, version) {
16861647
out.kept.push(result.clone());
16871648
continue;
16881649
}
@@ -3989,77 +3950,6 @@ pub(crate) fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
39893950
mod tests {
39903951
use super::*;
39913952

3992-
/// The pnpm-PnP hosted lock probe must be boundary-anchored: plain
3993-
/// substring matching collides on version prefixes, name suffixes, and
3994-
/// unscoped-inside-scoped names.
3995-
#[test]
3996-
fn pnpm_lock_resolves_is_boundary_anchored() {
3997-
// v9/v6/v5 key spellings all resolve.
3998-
assert!(pnpm_lock_resolves(
3999-
"lockfileVersion: '9.0'\n\nsnapshots:\n\n left-pad@1.3.0:\n",
4000-
"left-pad",
4001-
"1.3.0"
4002-
));
4003-
assert!(pnpm_lock_resolves(
4004-
" /left-pad@1.3.0:\n resolution: {}\n",
4005-
"left-pad",
4006-
"1.3.0"
4007-
));
4008-
assert!(pnpm_lock_resolves(
4009-
" /left-pad/1.3.0:\n resolution: {}\n",
4010-
"left-pad",
4011-
"1.3.0"
4012-
));
4013-
// Peer-qualified keys still resolve: v9 `(peer)` and v5 `_peer`.
4014-
assert!(pnpm_lock_resolves(
4015-
" 'left-pad@1.3.0(react@18.0.0)':\n",
4016-
"left-pad",
4017-
"1.3.0"
4018-
));
4019-
assert!(pnpm_lock_resolves(
4020-
" /left-pad/1.3.0_react@18.0.0:\n",
4021-
"left-pad",
4022-
"1.3.0"
4023-
));
4024-
// Scoped names resolve in both quoted-v9 and v6 spellings.
4025-
assert!(pnpm_lock_resolves(
4026-
" '@scope/name@1.0.0':\n",
4027-
"@scope/name",
4028-
"1.0.0"
4029-
));
4030-
assert!(pnpm_lock_resolves(
4031-
" /@scope/name@1.0.0:\n",
4032-
"@scope/name",
4033-
"1.0.0"
4034-
));
4035-
4036-
// Version-prefix collision: 1.3.0 must NOT match 1.3.0-beta.1.
4037-
assert!(!pnpm_lock_resolves(
4038-
" left-pad@1.3.0-beta.1:\n",
4039-
"left-pad",
4040-
"1.3.0"
4041-
));
4042-
// Name-suffix collision: `pad` must NOT match inside `left-pad`.
4043-
assert!(!pnpm_lock_resolves(" left-pad@1.3.0:\n", "pad", "1.3.0"));
4044-
assert!(!pnpm_lock_resolves(" /left-pad/1.3.0:\n", "pad", "1.3.0"));
4045-
// Unscoped-inside-scoped: `name` must NOT match `@scope/name`.
4046-
assert!(!pnpm_lock_resolves(
4047-
" '@scope/name@1.0.0':\n",
4048-
"name",
4049-
"1.0.0"
4050-
));
4051-
assert!(!pnpm_lock_resolves(
4052-
" /@scope/name@1.0.0:\n",
4053-
"name",
4054-
"1.0.0"
4055-
));
4056-
// Absent version: never resolves.
4057-
assert!(!pnpm_lock_resolves(
4058-
" left-pad@1.3.0:\n",
4059-
"left-pad",
4060-
"2.0.0"
4061-
));
4062-
}
40633953
use socket_patch_core::api::types::{PatchFileResponse, VulnerabilityResponse};
40643954
use std::collections::HashMap;
40653955

@@ -5038,36 +4928,6 @@ mod tests {
50384928
);
50394929
}
50404930

5041-
// --- pnpm_lock_resolves: needle at byte 0 ------------------------------
5042-
// The boundary probe reads the char BEFORE the match; a match at the very
5043-
// start of the text has none (`None => true`). A regression that indexes
5044-
// `text[..pos - 1]` unconditionally would underflow/panic here.
5045-
5046-
#[test]
5047-
fn pnpm_lock_resolves_needle_at_start_of_text() {
5048-
// pos == 0, plain v9 spelling: no preceding char is a valid boundary.
5049-
assert!(pnpm_lock_resolves("left-pad@1.3.0:\n", "left-pad", "1.3.0"));
5050-
// pos == 0, v5/v6 `/name/version` and `/name@version` spellings: the
5051-
// leading `/` delimiter itself has nothing before it.
5052-
assert!(pnpm_lock_resolves(
5053-
"/left-pad/1.3.0:\n",
5054-
"left-pad",
5055-
"1.3.0"
5056-
));
5057-
assert!(pnpm_lock_resolves(
5058-
"/left-pad@1.3.0:\n",
5059-
"left-pad",
5060-
"1.3.0"
5061-
));
5062-
// Still boundary-checked at the start of text: a scoped tail whose
5063-
// name begins mid-token must NOT match.
5064-
assert!(!pnpm_lock_resolves(
5065-
"@scope/left-pad@1.3.0:\n",
5066-
"left-pad",
5067-
"1.3.0"
5068-
));
5069-
}
5070-
50714931
// --- write_all_patch_blobs ---------------------------------------------
50724932
// The per-patch fan-out over write_blob_entry: the FIRST bad entry must
50734933
// fail the whole patch (Err(())) and leave nothing outside the blobs

0 commit comments

Comments
 (0)