From f035c9f692f4709e89983fddbe0d7890c8301180 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 22:22:34 +0000 Subject: [PATCH 1/4] Start fix for #1540 Assisted-by: Claude Code:claude-opus-5-5 From 2d3dac89cdee16737e67d5e45f98d6164d5fd105 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 22:32:58 +0000 Subject: [PATCH 2/4] Block VEX for unread yarn portal:/link: copies A yarn berry portal: or link: dependency with an absolute path (e.g. "lp2": "portal:/home/me/left-pad") links an unread directory into node_modules. Its lock version is a placeholder, so it may be any version of any package. Socket-patch dropped it, so hosted and vendored scans reported success with no copy warning and `vex` attested not_affected while that copy installed unpatched. Such a copy is now recorded as an unidentified copy of any version: hosted scans warn redirect_copy_unpatched, vendored scans warn vendor_copy_unpatched, and `vex` withholds every package the lock wires, as it already does for an unread file: copy at its version. Fixes #1540 Assisted-by: Claude Code:claude-opus-5-5 --- .../src/vendor/yarn_berry_lock.rs | 44 ++++++++++ .../src/vendor/yarn_copies.rs | 15 ++-- .../socket-patch-core/src/vex/discover/mod.rs | 50 +++++++---- .../src/vex/discover/yarn.rs | 84 +++++++++++++++---- docs/ecosystems.md | 5 +- 5 files changed, 162 insertions(+), 36 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs index 84571be78..43c1c95db 100644 --- a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs +++ b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs @@ -2581,6 +2581,50 @@ __metadata: } } + /// #1540: a `portal:` / `link:` copy with an ABSOLUTE path is never read + /// (#1516), so its package and version are unknown. It is named as a + /// possible unpatched copy (`vendor_copy_unpatched`, unidentified + /// wording) whatever its target holds, never taken as proof the package + /// is installed, and the registry entry still vendors. It was dropped + /// silently. + #[tokio::test] + async fn issue_1540_absolute_portal_and_link_copies_are_named() { + for protocol in ["portal", "link"] { + for fork in ["left-pad", "other-pkg"] { + let ext = tempfile::tempdir().unwrap(); + tokio::fs::write( + ext.path().join("package.json"), + format!(r#"{{"name":"{fork}","version":"1.3.0"}}"#), + ) + .await + .unwrap(); + let abs = ext.path().to_string_lossy().replace('\\', "/"); + let key = format!("lp2@{protocol}:{abs}::locator=vendor-spike%40workspace%3A."); + let block = format!( + "\n\"{key}\":\n version: 0.0.0-use.local\n resolution: \"{key}\"\n \ + languageName: node\n linkType: soft\n" + ); + let lock = format!("{B3_BEFORE_LOCK}{block}"); + let fx = fixture_with(B3_BEFORE_PKG, &lock).await; + + let (result, entry, warnings) = expect_done(fx.vendor(false).await); + assert!(result.success, "{protocol}/{fork}: {:?}", result.error); + assert!(entry.is_some(), "{protocol}/{fork}: registry entry vendors"); + let copies: Vec<&VendorWarning> = warnings + .iter() + .filter(|w| w.code == "vendor_copy_unpatched") + .collect(); + assert_eq!(copies.len(), 1, "{protocol}/{fork}: {warnings:?}"); + assert!( + copies[0].detail.contains(&format!("`\"{key}\"`")) + && copies[0].detail.contains("can't be read"), + "{protocol}/{fork}: {}", + copies[0].detail + ); + } + } + } + /// #939 (review): when left-pad@1.3.0 is locked ONLY under another /// dependency name, vendoring refuses `vendor_lock_entry_not_rewritable` /// naming that copy, in the loop and in the download plan alike, not diff --git a/crates/socket-patch-core/src/vendor/yarn_copies.rs b/crates/socket-patch-core/src/vendor/yarn_copies.rs index 038d45969..53abbfcc7 100644 --- a/crates/socket-patch-core/src/vendor/yarn_copies.rs +++ b/crates/socket-patch-core/src/vendor/yarn_copies.rs @@ -30,9 +30,10 @@ pub(crate) struct OtherNameCopy { pub(crate) source: CopyRef, /// The copy itself says it is `name@version`. `false` for a copy of /// that version whose package cannot be read (a missing directory, a - /// tarball with no readable `package.json`, a non-registry url): it may - /// be `name`, so it is named, but never taken as proof that `name` is - /// installed (#1516). + /// tarball with no readable `package.json`, a non-registry url, or an + /// unread `portal:` / `link:` directory of any version): it may be + /// `name`, so it is named, but never taken as proof that `name` is + /// installed (#1516, #1540). pub(crate) identified: bool, } @@ -40,9 +41,10 @@ pub(crate) struct OtherNameCopy { /// `name@version` under another dependency name, in lock order. A copy of /// that version whose package cannot be read is kept, unidentified: it /// may hold `name`, and leaving it out would let the scan and `vex` vouch -/// for a lock that installs it unpatched (#1516). A `portal:` / `link:` -/// copy whose manifest cannot be read is left out: its version is that -/// manifest's, so nothing ties it to `version`. +/// for a lock that installs it unpatched (#1516). So is a `portal:` / +/// `link:` copy whose manifest cannot be read (an absolute path is never +/// read): its version is that manifest's, so it may be `version` too +/// (#1540). pub(crate) async fn other_name_copies( project_root: &Path, lock_text: &str, @@ -108,7 +110,6 @@ pub(crate) async fn other_name_copies( let identified = match copy_identity(&source, &lock_version, bytes).await { Some((n, v)) if n == name && v == version => true, Some(_) => continue, - None if matches!(source, CopyRef::Linked(_)) => continue, None => false, }; out.push(OtherNameCopy { diff --git a/crates/socket-patch-core/src/vex/discover/mod.rs b/crates/socket-patch-core/src/vex/discover/mod.rs index fafc90b54..de86cdad0 100644 --- a/crates/socket-patch-core/src/vex/discover/mod.rs +++ b/crates/socket-patch-core/src/vex/discover/mod.rs @@ -442,8 +442,10 @@ pub struct UnpatchedCopy { pub struct UnidentifiedCopy { /// Root-relative lock file. pub file: PathBuf, - /// The lock entry's version. - pub version: String, + /// The lock entry's version; `None` when the lock does not say (a yarn + /// berry `portal:` / `link:` copy, whose version is its unread + /// manifest's), so it may be any version (#1540). + pub version: Option, /// The lock entry's key, as the lock spells it. pub key: String, /// How that entry installs, completing "lock entry `` …". @@ -892,39 +894,59 @@ impl Discovery { /// directory, a tarball with no readable `package.json`). It may be any /// package at that version, so [`Discovery::contest_within_locks`] /// takes it as an unpatched copy of every package `file` wires at - /// `version`: vouching for one would rest on a copy nobody read - /// (#1516). Matched there, once every extractor has pushed its refs. - pub(crate) fn unidentified_copy(&mut self, file: &str, version: &str, key: &str, how: &str) { + /// `version` (at any version when `version` is `None`, #1540): vouching + /// for one would rest on a copy nobody read (#1516). Matched there, + /// once every extractor has pushed its refs. + pub(crate) fn unidentified_copy( + &mut self, + file: &str, + version: Option<&str>, + key: &str, + how: &str, + ) { self.unidentified_copies.push(UnidentifiedCopy { file: PathBuf::from(file), - version: version.to_string(), + version: version.map(str::to_string), key: key.to_string(), how: how.to_string(), }); } /// Fold [`Discovery::unidentified_copies`] into `unpatched_copies`: one - /// copy per ref of the same file at the same version. + /// copy per ref of the same file at the same version (at any version + /// for a copy with none). fn expand_unidentified_copies(&mut self) { if self.unidentified_copies.is_empty() { return; } - let mut wired: BTreeMap<(&Path, &str), BTreeSet<&str>> = BTreeMap::new(); + let mut wired: BTreeMap<&Path, BTreeMap<&str, BTreeSet<&str>>> = BTreeMap::new(); for r in &self.refs { if let Some((_, version)) = r.purl.rsplit_once('@') { wired - .entry((r.source_file.as_path(), version)) + .entry(r.source_file.as_path()) + .or_default() + .entry(version) .or_default() .insert(r.purl.as_str()); } } let mut expanded = Vec::new(); for u in &self.unidentified_copies { - for purl in wired - .get(&(u.file.as_path(), u.version.as_str())) - .into_iter() - .flatten() - { + let by_version = wired.get(u.file.as_path()); + let purls: BTreeSet<&str> = match &u.version { + Some(v) => by_version + .and_then(|m| m.get(v.as_str())) + .into_iter() + .flatten() + .copied() + .collect(), + None => by_version + .into_iter() + .flat_map(|m| m.values().flatten()) + .copied() + .collect(), + }; + for purl in purls { expanded.push(UnpatchedCopy { purl: purl.to_string(), file: u.file.clone(), diff --git a/crates/socket-patch-core/src/vex/discover/yarn.rs b/crates/socket-patch-core/src/vex/discover/yarn.rs index 7fedd9ae9..cbaa596b4 100644 --- a/crates/socket-patch-core/src/vex/discover/yarn.rs +++ b/crates/socket-patch-core/src/vex/discover/yarn.rs @@ -864,8 +864,9 @@ struct LockCopy { /// non-registry url) may be any package at its lock version, so it is /// recorded as a copy of every package this lock wires at that version: /// vouching for those would rest on a copy nobody read (#1516). A -/// `portal:` / `link:` copy with no readable manifest has no version, and -/// is left alone. +/// `portal:` / `link:` copy with no readable manifest (an absolute path, +/// never read) has no version either, so it may be any version of any +/// package, and contests every package this lock wires (#1540). async fn record_copies(ctx: &DiscoverCtx<'_>, copies: Vec, out: &mut Discovery) { for copy in copies { let bytes = match copy.source.read_path() { @@ -875,18 +876,22 @@ async fn record_copies(ctx: &DiscoverCtx<'_>, copies: Vec, out: &mut D let Some((name, version)) = crate::vendor::yarn_copies::copy_identity(©.source, ©.version, bytes).await else { - if !matches!(copy.source, CopyRef::Linked(_)) { - out.unidentified_copy( - YARN_LOCK, - ©.version, - ©.key, - &format!( - "{}, whose package can't be read, so it may be this version of any \ - package (yarn keys it by the dependency name)", - copy.source.how() - ), - ); - } + let linked = matches!(copy.source, CopyRef::Linked(_)); + out.unidentified_copy( + YARN_LOCK, + (!linked).then_some(copy.version.as_str()), + ©.key, + &format!( + "{}, whose package can't be read, so it may be {} of any package \ + (yarn keys it by the dependency name)", + copy.source.how(), + if linked { + "any version" + } else { + "this version" + } + ), + ); continue; }; out.unpatched_copy( @@ -1878,6 +1883,57 @@ mod tests { } } + /// #1540: a berry `portal:` / `link:` copy with an ABSOLUTE path is never + /// read (a lock is untrusted input, #1516), and its lock version is the + /// placeholder `0.0.0-use.local`, so it may be any version of any + /// package. It contests every ref its lock wires, whatever the target + /// holds, like an unread `file:` copy does at its lock version; it was + /// dropped, so `vex` attested `not_affected` while `node_modules/lp2` + /// held unpatched left-pad@1.3.0. + #[tokio::test] + async fn issue_1540_absolute_portal_and_link_copies_contest_every_ref() { + let wired = berry_vendored_block("left-pad", "1.3.0", UUID_A); + for protocol in ["portal", "link"] { + for manifest in [ + r#"{"name":"left-pad","version":"1.3.0"}"#, + r#"{"name":"other-pkg","version":"9.9.9"}"#, + ] { + let p = Project::new(); + p.write("ext/lp/package.json", manifest); + let abs = p.root().join("ext/lp").to_string_lossy().replace('\\', "/"); + let descriptor = format!("lp2@{protocol}:{abs}::locator=app%40workspace%3A."); + p.write( + "yarn.lock", + berry(&[ + wired.clone(), + berry_block(&descriptor, "0.0.0-use.local", &descriptor, None), + ]), + ); + p.write( + "package.json", + package_json_with_resolutions(serde_json::json!({ + "left-pad": format!("file:./.socket/vendor/npm/{UUID_A}/left-pad-1.3.0.tgz"), + })), + ); + let out = run(&p).await; + assert!( + out.refs.is_empty(), + "{protocol} {manifest}: {:#?}", + out.refs + ); + assert!( + out.diagnostics + .iter() + .any(|d| d.code == DIAG_REF_UNATTRIBUTABLE + && d.detail.contains(&format!("lp2@{protocol}:")) + && d.detail.contains("can't be read")), + "{protocol} {manifest}: {:#?}", + out.diagnostics + ); + } + } + } + /// A berry registry locator beside a hosted one of the same /// `name@version` (scoped `resolutions`, or a workspace member added /// after the rewire, then `yarn install`) installs the registry bytes diff --git a/docs/ecosystems.md b/docs/ecosystems.md index 485fe81db..6d1e65644 100644 --- a/docs/ecosystems.md +++ b/docs/ecosystems.md @@ -294,7 +294,10 @@ Each Legacy format has an upgrade path and an undo path. Both work in v5: yarn strips the first path segment. A copy whose package can't be read (a non-registry URL, a missing directory) is treated as possibly any package at its lock version: it is named as a copy it may be, and `vex` attests nothing that lock wires at that - version. Hosted mode still pins the + version. An absolute path (`portal:/home/me/lp`, `file:/abs/lp.tgz`) is never read, as + the lock is untrusted input; a `portal:` / `link:` copy whose `package.json` isn't + read has no version either, so it may be any version of any package and `vex` attests + nothing that lock wires. Use a relative path to let the copy be read. Hosted mode still pins the registry entry and names the copy (`redirect_copy_unpatched`, unless a yarn classic skip warning already names it); vendored mode still wires the registry entry and names it (`vendor_copy_unpatched` for berry; the yarn classic codes below for classic); and From af452c2cd9f5e580db69ef420a99e9f605d6e42f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 22:43:58 +0000 Subject: [PATCH 3/4] Test hosted scan names absolute portal copies Covers #1540 at the CLI level: a hosted yarn berry scan with an absolute portal: or link: dependency must warn redirect_copy_unpatched instead of reporting a clean success. Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/hosted_memory_engine.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/crates/socket-patch-cli/tests/hosted_memory_engine.rs b/crates/socket-patch-cli/tests/hosted_memory_engine.rs index c4196724a..d44408988 100644 --- a/crates/socket-patch-cli/tests/hosted_memory_engine.rs +++ b/crates/socket-patch-cli/tests/hosted_memory_engine.rs @@ -1879,3 +1879,65 @@ async fn issue_939_berry_renamed_copy_is_named() { } } } + +/// #1540: a berry `portal:` / `link:` copy with an ABSOLUTE path is never +/// read (#1516), and its lock version is the placeholder +/// `0.0.0-use.local`, so it may be any version of any package. The hosted +/// scan still pins the registry entry, and names that copy as possibly +/// unpatched (`redirect_copy_unpatched`) whatever it links to; it used to +/// report a clean success while `node_modules/lp2` held unpatched bytes. +#[tokio::test] +async fn issue_1540_berry_absolute_portal_and_link_copies_are_named() { + const UUID: &str = "15401540-1540-4540-8540-154015401540"; + let ws = "app%40workspace%3A."; + for protocol in ["portal", "link"] { + let spec = format!("{protocol}:/home/me/shared/left-pad"); + let key = format!("lp2@{spec}::locator={ws}"); + let server = MockServer::start().await; + mount_left_pad_grant(&server, UUID).await; + let mut files = BTreeMap::new(); + files.insert( + "package.json".to_string(), + format!( + r#"{{"name":"app","version":"1.0.0","dependencies":{{"left-pad":"^1.3.0","lp2":"{spec}"}}}}"# + ) + .into_bytes(), + ); + files.insert( + "yarn.lock".to_string(), + format!( + "__metadata:\n version: 8\n cacheKey: 10c0\n\n\ + \"app@workspace:.\":\n version: 0.0.0-use.local\n \ + resolution: \"app@workspace:.\"\n dependencies:\n \ + left-pad: \"npm:^1.3.0\"\n lp2: \"{spec}\"\n \ + languageName: unknown\n linkType: soft\n\n\ + \"left-pad@npm:^1.3.0\":\n version: 1.3.0\n \ + resolution: \"left-pad@npm:1.3.0\"\n checksum: 10c0/{}\n \ + languageName: node\n linkType: hard\n\n\ + \"{key}\":\n version: 0.0.0-use.local\n resolution: \"{key}\"\n \ + languageName: node\n linkType: soft\n", + "1".repeat(128) + ) + .into_bytes(), + ); + let output = run_engine(&server, build_input(&files, &[], options(false))).await; + let project = &output.projects[0]; + assert!(project.error.is_none(), "{protocol}: {:?}", project.error); + assert_eq!( + project.redirected.len(), + 1, + "{protocol}: {:?} {:#}", + project.skipped, + project.redirect + ); + let details = copy_unpatched_details(project); + assert_eq!(details.len(), 1, "{protocol}: {:#}", project.redirect); + assert!( + details[0].contains(key.as_str()) + && details[0].contains("can't be read") + && details[0].contains("UNPATCHED"), + "{protocol}: {}", + details[0] + ); + } +} From 7a3fb872080b1eed48e741f8dacb471acb8a171e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 23:13:51 +0000 Subject: [PATCH 4/4] Only unread portal copies count as any version Review follow-up for #1540. Treat a portal:/link: copy as possibly any package only when it is never read (an absolute path). A relative link: to a plain directory with no package.json is read, holds no package, and stays ignored as before, so it no longer blocks VEX for the whole lock. Also keep a Windows drive path (portal:C:/...) intact when parsing berry references, word the vendored warning for an unread portal copy without a made-up version, and simplify the expansion of unidentified copies. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/formats/yarn/copy.rs | 35 ++++++++++++++- .../src/vendor/yarn_berry_lock.rs | 10 ++++- .../src/vendor/yarn_copies.rs | 7 +-- .../socket-patch-core/src/vex/discover/mod.rs | 44 ++++++------------- .../src/vex/discover/yarn.rs | 44 ++++++++++++++++--- 5 files changed, 98 insertions(+), 42 deletions(-) diff --git a/crates/socket-patch-core/src/formats/yarn/copy.rs b/crates/socket-patch-core/src/formats/yarn/copy.rs index b00e09378..a262ed783 100644 --- a/crates/socket-patch-core/src/formats/yarn/copy.rs +++ b/crates/socket-patch-core/src/formats/yarn/copy.rs @@ -58,6 +58,13 @@ impl CopyRef { } } + /// A `portal:` / `link:` copy that is never read ([`CopyRef::read_path`] + /// is `None`: an absolute path). Its lock version is the placeholder + /// `0.0.0-use.local`, so it may be any version of any package (#1540). + pub(crate) fn is_unread_linked(&self) -> bool { + matches!(self, CopyRef::Linked(_)) && self.read_path().is_none() + } + /// The `(name, version)` this copy installs, given the lock entry's /// `version` and the bytes read at [`CopyRef::read_path`]. `None` when /// the copy does not say (an unreadable or nameless manifest, a @@ -151,7 +158,10 @@ pub(crate) fn berry_copy_ref(reference: &str) -> Option { } else { return None; }; - let path = path.split(['#', ':']).next().unwrap_or_default(); + // Cut the `::locator=…` bindings and any `#` selector; a single `:` is + // part of the path (a Windows drive, `portal:C:/Users/me/lp`). + let path = path.split("::").next().unwrap_or_default(); + let path = path.split('#').next().unwrap_or_default(); let rel = berry_copy_path(reference, path)?; Some(if linked { CopyRef::Linked(rel) @@ -320,6 +330,29 @@ mod tests { assert_eq!(berry_copy_ref("workspace:packages/b"), None); } + /// #1540: an absolute `portal:` / `link:` path keeps its whole spelling, + /// a Windows drive's `:` included, and is never read. + #[test] + fn absolute_linked_refs_keep_the_path_and_are_unread() { + let ws = "::locator=app%40workspace%3A."; + for (reference, path) in [ + (format!("portal:/home/me/lp{ws}"), "/home/me/lp"), + (format!("link:/home/me/lp{ws}"), "/home/me/lp"), + (format!("portal:C:/Users/me/lp{ws}"), "C:/Users/me/lp"), + ] { + assert_eq!( + berry_copy_ref(&reference), + Some(CopyRef::Linked(path.into())), + "{reference}" + ); + } + let unix = CopyRef::Linked("/home/me/lp".into()); + assert!(unix.read_path().is_none() && unix.is_unread_linked()); + let relative = CopyRef::Linked("vend/lp".into()); + assert!(!relative.is_unread_linked()); + assert!(!CopyRef::Directory("/home/me/lp".into()).is_unread_linked()); + } + #[test] fn linked_identity_reads_name_and_version_from_the_manifest() { let linked = CopyRef::Linked("vend/lp".into()); diff --git a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs index 43c1c95db..ca2e33f66 100644 --- a/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs +++ b/crates/socket-patch-core/src/vendor/yarn_berry_lock.rs @@ -275,8 +275,15 @@ impl NpmLockBackend for YarnBerryBackend { vendoring writes never reaches it, so that copy stays UNPATCHED" ) } else { + // An unread `portal:` / `link:` copy has no version + // to name (#1540). + let what = if copy.source.is_unread_linked() { + "a package".to_string() + } else { + format!("version {version}") + }; format!( - "{YARN_LOCK} entry `{key}` installs version {version} under another \ + "{YARN_LOCK} entry `{key}` installs {what} under another \ dependency name, from {origin}, whose package can't be read, so it \ may be {name}@{version}; the name-keyed resolutions entry vendoring \ writes never reaches it, so if it is, that copy stays UNPATCHED" @@ -2617,6 +2624,7 @@ __metadata: assert_eq!(copies.len(), 1, "{protocol}/{fork}: {warnings:?}"); assert!( copies[0].detail.contains(&format!("`\"{key}\"`")) + && copies[0].detail.contains("installs a package under") && copies[0].detail.contains("can't be read"), "{protocol}/{fork}: {}", copies[0].detail diff --git a/crates/socket-patch-core/src/vendor/yarn_copies.rs b/crates/socket-patch-core/src/vendor/yarn_copies.rs index 53abbfcc7..847d78f0c 100644 --- a/crates/socket-patch-core/src/vendor/yarn_copies.rs +++ b/crates/socket-patch-core/src/vendor/yarn_copies.rs @@ -42,9 +42,9 @@ pub(crate) struct OtherNameCopy { /// that version whose package cannot be read is kept, unidentified: it /// may hold `name`, and leaving it out would let the scan and `vex` vouch /// for a lock that installs it unpatched (#1516). So is a `portal:` / -/// `link:` copy whose manifest cannot be read (an absolute path is never -/// read): its version is that manifest's, so it may be `version` too -/// (#1540). +/// `link:` copy that is never read (an absolute path): its version is its +/// manifest's, so it may be `version` too (#1540). One read and found to +/// hold no versioned package (a `link:` to a plain directory) is left out. pub(crate) async fn other_name_copies( project_root: &Path, lock_text: &str, @@ -110,6 +110,7 @@ pub(crate) async fn other_name_copies( let identified = match copy_identity(&source, &lock_version, bytes).await { Some((n, v)) if n == name && v == version => true, Some(_) => continue, + None if matches!(source, CopyRef::Linked(_)) && !source.is_unread_linked() => continue, None => false, }; out.push(OtherNameCopy { diff --git a/crates/socket-patch-core/src/vex/discover/mod.rs b/crates/socket-patch-core/src/vex/discover/mod.rs index de86cdad0..14b6e989a 100644 --- a/crates/socket-patch-core/src/vex/discover/mod.rs +++ b/crates/socket-patch-core/src/vex/discover/mod.rs @@ -435,9 +435,9 @@ pub struct UnpatchedCopy { pub how: String, } -/// A lock entry that installs SOME package at `version` under another -/// dependency name, from a copy whose package can't be read -/// ([`Discovery::unidentified_copy`], #1516). +/// A lock entry that installs SOME package (at `version`, when known) under +/// another dependency name, from a copy whose package can't be read +/// ([`Discovery::unidentified_copy`], #1516, #1540). #[derive(Debug, Clone, PartialEq, Eq)] pub struct UnidentifiedCopy { /// Root-relative lock file. @@ -916,36 +916,18 @@ impl Discovery { /// copy per ref of the same file at the same version (at any version /// for a copy with none). fn expand_unidentified_copies(&mut self) { - if self.unidentified_copies.is_empty() { - return; - } - let mut wired: BTreeMap<&Path, BTreeMap<&str, BTreeSet<&str>>> = BTreeMap::new(); - for r in &self.refs { - if let Some((_, version)) = r.purl.rsplit_once('@') { - wired - .entry(r.source_file.as_path()) - .or_default() - .entry(version) - .or_default() - .insert(r.purl.as_str()); - } - } let mut expanded = Vec::new(); for u in &self.unidentified_copies { - let by_version = wired.get(u.file.as_path()); - let purls: BTreeSet<&str> = match &u.version { - Some(v) => by_version - .and_then(|m| m.get(v.as_str())) - .into_iter() - .flatten() - .copied() - .collect(), - None => by_version - .into_iter() - .flat_map(|m| m.values().flatten()) - .copied() - .collect(), - }; + let purls: BTreeSet<&str> = self + .refs + .iter() + .filter(|r| r.source_file == u.file) + .filter(|r| match &u.version { + Some(v) => r.purl.rsplit_once('@').is_some_and(|(_, rv)| rv == v), + None => true, + }) + .map(|r| r.purl.as_str()) + .collect(); for purl in purls { expanded.push(UnpatchedCopy { purl: purl.to_string(), diff --git a/crates/socket-patch-core/src/vex/discover/yarn.rs b/crates/socket-patch-core/src/vex/discover/yarn.rs index cbaa596b4..146cdeecb 100644 --- a/crates/socket-patch-core/src/vex/discover/yarn.rs +++ b/crates/socket-patch-core/src/vex/discover/yarn.rs @@ -864,9 +864,10 @@ struct LockCopy { /// non-registry url) may be any package at its lock version, so it is /// recorded as a copy of every package this lock wires at that version: /// vouching for those would rest on a copy nobody read (#1516). A -/// `portal:` / `link:` copy with no readable manifest (an absolute path, -/// never read) has no version either, so it may be any version of any -/// package, and contests every package this lock wires (#1540). +/// `portal:` / `link:` copy has no lock version: one never read (an +/// absolute path) may be any version of any package, and contests every +/// package this lock wires (#1540); one read and found to hold no +/// versioned package (a `link:` to a plain directory) is left alone. async fn record_copies(ctx: &DiscoverCtx<'_>, copies: Vec, out: &mut Discovery) { for copy in copies { let bytes = match copy.source.read_path() { @@ -876,16 +877,19 @@ async fn record_copies(ctx: &DiscoverCtx<'_>, copies: Vec, out: &mut D let Some((name, version)) = crate::vendor::yarn_copies::copy_identity(©.source, ©.version, bytes).await else { - let linked = matches!(copy.source, CopyRef::Linked(_)); + let unread_linked = copy.source.is_unread_linked(); + if matches!(copy.source, CopyRef::Linked(_)) && !unread_linked { + continue; + } out.unidentified_copy( YARN_LOCK, - (!linked).then_some(copy.version.as_str()), + (!unread_linked).then_some(copy.version.as_str()), ©.key, &format!( "{}, whose package can't be read, so it may be {} of any package \ (yarn keys it by the dependency name)", copy.source.how(), - if linked { + if unread_linked { "any version" } else { "this version" @@ -1934,6 +1938,34 @@ mod tests { } } + /// #1540 control: a RELATIVE `link:` to a plain directory (no + /// `package.json`, which `link:` allows) is read and holds no package, + /// so it contests nothing. + #[tokio::test] + async fn issue_1540_relative_link_to_a_plain_directory_contests_nothing() { + let wired = berry_vendored_block("left-pad", "1.3.0", UUID_A); + let descriptor = "assets@link:./assets::locator=app%40workspace%3A."; + let p = Project::new(); + p.write("assets/logo.svg", ""); + p.write( + "yarn.lock", + berry(&[ + wired, + berry_block(descriptor, "0.0.0-use.local", descriptor, None), + ]), + ); + p.write( + "package.json", + package_json_with_resolutions(serde_json::json!({ + "left-pad": format!("file:./.socket/vendor/npm/{UUID_A}/left-pad-1.3.0.tgz"), + })), + ); + assert_refs( + &run(&p).await, + &[("pkg:npm/left-pad@1.3.0", UUID_A, WiringMode::Vendored)], + ); + } + /// A berry registry locator beside a hosted one of the same /// `name@version` (scoped `resolutions`, or a workspace member added /// after the rewire, then `yarn install`) installs the registry bytes