Skip to content

Commit 32f7756

Browse files
committed
Fix exact name treated as ambiguous in Target::ambiguity
When a bare package name like 'lodash' matched both an exact full name (pkg:npm/lodash) and a last-segment hit (pkg:npm/@types/lodash), the ambiguity check incorrectly flagged it as ambiguous and refused to act. The fix checks if exactly one package has a full name that exactly matches the typed name (case-insensitive, with PyPI canonicalization). When such a unique exact match exists, the name is not ambiguous and the exact match is preferred over any last-segment hits. This preserves the existing behavior for truly ambiguous cases: - 'core' matching both @angular/core and @babel/core (0 exact matches) - 'six' matching both pkg:pypi/six and pkg:npm/six (2 exact matches) While fixing the regression for common TypeScript scenarios: - 'lodash' now correctly selects pkg:npm/lodash, not ambiguous with @types/lodash - 'react' now correctly selects pkg:npm/react, not ambiguous with @types/react
1 parent a4e3afe commit 32f7756

21 files changed

Lines changed: 205 additions & 63 deletions

File tree

‎crates/socket-patch-cli/src/commands/scan/policy.rs‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,9 @@ impl ScanPolicy {
237237
}
238238

239239
fn recorded_uuid(&self, purl: &str) -> Option<&str> {
240-
self.recorded.get(&PurlKey::new(purl).into_string()).map(String::as_str)
240+
self.recorded
241+
.get(&PurlKey::new(purl).into_string())
242+
.map(String::as_str)
241243
}
242244

243245
/// Step 3: the root, ecosystem and package filters. Returns whether the
@@ -269,7 +271,10 @@ impl ScanPolicy {
269271
}
270272
if self.root_verdict.is_err() {
271273
// Already reported as the root's one entry.
272-
} else if report.filtered_purls.insert(PurlKey::new(purl).into_string()) {
274+
} else if report
275+
.filtered_purls
276+
.insert(PurlKey::new(purl).into_string())
277+
{
273278
report.filtered.push(FilteredEntry {
274279
purl: Some(PurlKey::new(purl).into_string()),
275280
uuid: None,
@@ -284,7 +289,10 @@ impl ScanPolicy {
284289
/// Record the purls with a newer patch (`updates[]`), for
285290
/// `retained[].upgradeAvailable`.
286291
pub(crate) fn set_update_purls<'a>(&self, purls: impl IntoIterator<Item = &'a str>) {
287-
self.report().update_purls = purls.into_iter().map(|p| PurlKey::new(p).into_string()).collect();
292+
self.report().update_purls = purls
293+
.into_iter()
294+
.map(|p| PurlKey::new(p).into_string())
295+
.collect();
288296
}
289297

290298
/// Steps 5-6: group the tier-accessible offers, keep retained packages
@@ -298,7 +306,10 @@ impl ScanPolicy {
298306
{
299307
let report = self.report();
300308
for offer in accessible {
301-
if report.retained_purls.contains(&PurlKey::new(&offer.purl).into_string()) {
309+
if report
310+
.retained_purls
311+
.contains(&PurlKey::new(&offer.purl).into_string())
312+
{
302313
continue;
303314
}
304315
grouped.entry(offer.purl.clone()).or_default().push(offer);

‎crates/socket-patch-cli/tests/e2e_redirect_yarn_classic_build.rs‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,10 @@ async fn classic_offline_mirror_refuses_hosted_and_keeps_installs_working() {
900900
return;
901901
};
902902
assert!(
903-
fx.proj.join("mirror").join(format!("{DEP}-{DEP_VERSION}.tgz")).is_file(),
903+
fx.proj
904+
.join("mirror")
905+
.join(format!("{DEP}-{DEP_VERSION}.tgz"))
906+
.is_file(),
904907
"the fixture install must populate the offline mirror"
905908
);
906909
let fresh = fx.tmp.path().join("fresh");
@@ -926,7 +929,11 @@ async fn classic_offline_mirror_refuses_hosted_and_keeps_installs_working() {
926929
String::from_utf8_lossy(&ci.stderr)
927930
);
928931
assert!(
929-
!fresh.join("node_modules").join(DEP).join("index.js").exists(),
932+
!fresh
933+
.join("node_modules")
934+
.join(DEP)
935+
.join("index.js")
936+
.exists(),
930937
"yarn < 1.7 is expected to install nothing from the mirror"
931938
);
932939
return;
@@ -948,7 +955,10 @@ async fn classic_offline_mirror_refuses_hosted_and_keeps_installs_working() {
948955
);
949956
let installed =
950957
std::fs::read(fresh.join("node_modules").join(DEP).join("index.js")).unwrap();
951-
assert_eq!(installed, fx.orig, "the untouched lock installs the upstream bytes");
958+
assert_eq!(
959+
installed, fx.orig,
960+
"the untouched lock installs the upstream bytes"
961+
);
952962
std::fs::remove_dir_all(fresh.join("node_modules")).unwrap();
953963
}
954964
}

‎crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs‎

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,14 @@ async fn run_berry_capstone(driver: VendorDriver, yarnrc_extra: &str) {
729729

730730
fn git(cwd: &Path, args: &[&str]) -> Output {
731731
let out = Command::new("git")
732-
.args(["-c", "user.name=t", "-c", "user.email=t@t", "-c", "init.defaultBranch=main"])
732+
.args([
733+
"-c",
734+
"user.name=t",
735+
"-c",
736+
"user.email=t@t",
737+
"-c",
738+
"init.defaultBranch=main",
739+
])
733740
.args(args)
734741
.current_dir(cwd)
735742
.output()
@@ -810,16 +817,30 @@ fn yarn_berry_vendored_tarball_survives_a_tgz_gitignore_rule() {
810817
};
811818
let (code, stdout, stderr) = run_socket(
812819
&proj,
813-
&["vendor", "--json", "--offline", "--cwd", proj.to_str().unwrap()],
820+
&[
821+
"vendor",
822+
"--json",
823+
"--offline",
824+
"--cwd",
825+
proj.to_str().unwrap(),
826+
],
827+
);
828+
assert_eq!(
829+
code, 0,
830+
"vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}"
814831
);
815-
assert_eq!(code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}");
816832

817833
git(&proj, &["add", "-A"]);
818834
git(&proj, &["commit", "-qm", "vendored"]);
819835
let fresh = tmp.path().join("fresh");
820836
git(
821837
tmp.path(),
822-
&["clone", "-q", proj.to_str().unwrap(), fresh.to_str().unwrap()],
838+
&[
839+
"clone",
840+
"-q",
841+
proj.to_str().unwrap(),
842+
fresh.to_str().unwrap(),
843+
],
823844
);
824845
let fresh_global = tmp.path().join("fresh-yarn-global");
825846
let ci = corepack(
@@ -854,14 +875,26 @@ fn yarn_berry_vendor_refuses_a_gitignored_socket_dir() {
854875
let pkg_before = std::fs::read(proj.join("package.json")).unwrap();
855876
let (code, stdout, stderr) = run_socket(
856877
&proj,
857-
&["vendor", "--json", "--offline", "--cwd", proj.to_str().unwrap()],
878+
&[
879+
"vendor",
880+
"--json",
881+
"--offline",
882+
"--cwd",
883+
proj.to_str().unwrap(),
884+
],
885+
);
886+
assert_eq!(
887+
code, 1,
888+
"vendor must fail.\nstdout:\n{stdout}\nstderr:\n{stderr}"
858889
);
859-
assert_eq!(code, 1, "vendor must fail.\nstdout:\n{stdout}\nstderr:\n{stderr}");
860890
assert!(
861891
stdout.contains("vendor_artifact_gitignored"),
862892
"refusal code expected:\n{stdout}"
863893
);
864894
assert_eq!(std::fs::read(proj.join("yarn.lock")).unwrap(), lock_before);
865-
assert_eq!(std::fs::read(proj.join("package.json")).unwrap(), pkg_before);
895+
assert_eq!(
896+
std::fs::read(proj.join("package.json")).unwrap(),
897+
pkg_before
898+
);
866899
assert!(!proj.join(format!(".socket/vendor/npm/{UUID}")).exists());
867900
}

‎crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,8 +556,7 @@ async fn live_pipfile_lock_conflict_vetoes_the_requirements_redirect() {
556556
#[tokio::test]
557557
#[serial]
558558
async fn platform_wheel_is_not_pinned_into_the_lock() {
559-
assert_wheel_tag_is_not_pinned("cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64")
560-
.await;
559+
assert_wheel_tag_is_not_pinned("cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64").await;
561560
}
562561

563562
/// #1048: a pure wheel bound to one interpreter (`cp311-none-any`) fails

‎crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,10 @@ async fn hosted_bom_lock_and_workspace_read_like_their_plain_twins() {
434434
assert_eq!(code, 0, "scan --mode hosted should succeed on a BOM lock");
435435
let lock = std::fs::read_to_string(&lock_path).unwrap();
436436
assert!(lock.starts_with("\u{feff}lockfileVersion:"), "{lock}");
437-
assert!(lock.contains(HOSTED_URL), "the BOM lock is redirected: {lock}");
437+
assert!(
438+
lock.contains(HOSTED_URL),
439+
"the BOM lock is redirected: {lock}"
440+
);
438441
let ws_path = tmp.path().join("pnpm-workspace.yaml");
439442
assert_eq!(
440443
std::fs::read_to_string(&ws_path).ok().as_deref(),
@@ -449,7 +452,10 @@ async fn hosted_bom_lock_and_workspace_read_like_their_plain_twins() {
449452
pristine,
450453
"rollback restores the BOM lock byte for byte"
451454
);
452-
assert!(!ws_path.exists(), "the auto-created workspace file goes too");
455+
assert!(
456+
!ws_path.exists(),
457+
"the auto-created workspace file goes too"
458+
);
453459

454460
// A BOM workspace file whose first key is the user's opt-out: left
455461
// byte-identical (no duplicate `trustLockfile`), lock still redirected.
@@ -475,7 +481,11 @@ async fn hosted_bom_lock_and_workspace_read_like_their_plain_twins() {
475481
"the lock is still redirected for {user_ws:?}"
476482
);
477483
let ws = std::fs::read_to_string(tmp.path().join("pnpm-workspace.yaml")).unwrap();
478-
assert_eq!(ws, want.unwrap_or(user_ws), "workspace file for {user_ws:?}");
484+
assert_eq!(
485+
ws,
486+
want.unwrap_or(user_ws),
487+
"workspace file for {user_ws:?}"
488+
);
479489
assert_eq!(ws.matches("trustLockfile").count(), 1, "{ws:?}");
480490
}
481491
}

‎crates/socket-patch-cli/tests/mode_migration_pypi.rs‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,9 +1195,18 @@ async fn ledger_update_failure_changes_nothing() {
11951195
set_mode(0o755);
11961196
assert_eq!(code, 1, "{env:#}");
11971197
assert_eq!(env["status"], "error", "{env:#}");
1198-
assert!(!env.to_string().contains("redirect_takeover_unpatched"), "{env:#}");
1199-
assert_eq!(std::fs::read(root.join("requirements.txt")).unwrap(), vendored);
1200-
assert_eq!(std::fs::read(root.join(".socket/vendor/state.json")).unwrap(), state);
1198+
assert!(
1199+
!env.to_string().contains("redirect_takeover_unpatched"),
1200+
"{env:#}"
1201+
);
1202+
assert_eq!(
1203+
std::fs::read(root.join("requirements.txt")).unwrap(),
1204+
vendored
1205+
);
1206+
assert_eq!(
1207+
std::fs::read(root.join(".socket/vendor/state.json")).unwrap(),
1208+
state
1209+
);
12011210
assert!(root.join(format!(".socket/vendor/pypi/{UUID}")).exists());
12021211
}
12031212

‎crates/socket-patch-core/src/crawlers/cargo_crawler.rs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,7 @@ fn read_crate_cargo_toml(crate_path: &Path, dir_name: &str) -> Option<(String, S
300300
let content = crate::utils::fs::read_regular_to_string_sync(&cargo_toml_path).ok()?;
301301

302302
// Fallback: parse directory name as <name>-<version>
303-
package_name_version(&content)
304-
.or_else(|| CargoCrawler::parse_dir_name_version(dir_name))
303+
package_name_version(&content).or_else(|| CargoCrawler::parse_dir_name_version(dir_name))
305304
}
306305

307306
/// SECURITY: `find_by_purls` formats name/version into a `<name>-<version>`

‎crates/socket-patch-core/src/formats/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
pub(crate) mod bun;
3030
pub mod cargo;
3131
pub mod composer;
32-
pub mod governing_locks;
3332
pub mod gem;
33+
pub mod governing_locks;
3434
pub(crate) mod maven;
3535
pub(crate) mod nuget;
3636
pub mod pnpm;

‎crates/socket-patch-core/src/hosted/memory/mod.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ use crate::rollout::stage::{
6666
classify, lookup_incomplete, mentioned_uuids, offers_from_results, Offers, RecordedIndex, Row,
6767
Stage, ROLLOUT_DEFERRED,
6868
};
69+
use crate::utils::purl_key::PurlKey;
6970
use discover::Provider;
7071
use stages::{Planned, RewriteRefused, Rewritten, StageOptions};
71-
use crate::utils::purl_key::PurlKey;
7272

7373
/// `"<crate version>+<git sha or 'unknown'>"`; the sha comes from the
7474
/// `SOCKET_PATCH_GIT_SHA` build-time variable.

‎crates/socket-patch-core/src/patch/redirect/mod.rs‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ use crate::formats::yarn::patterns::{
7777
use crate::formats::yarn::source::{classic_copy_source, CopySource};
7878
use crate::formats::yarn::stanzas::{stanza_key, BerryStanzas};
7979
#[cfg(test)]
80-
mod pnpm_equivalence_tests;
81-
#[cfg(test)]
8280
mod platform_wheel_tests;
81+
#[cfg(test)]
82+
mod pnpm_equivalence_tests;
8383
mod poetry;
8484
mod pypi_takeover;
8585
pub use pypi_takeover::preflight_pypi_takeover;
@@ -565,7 +565,7 @@ pub fn rewrite_registry_redirect_with_pipenv_version(
565565
bun_lockb_present,
566566
&std::collections::BTreeSet::new(),
567567
&std::collections::BTreeSet::new(),
568-
&yarnrc::OuterYarnMirror::default(),
568+
&yarnrc::OuterYarnMirror::default(),
569569
)
570570
}
571571

@@ -6760,8 +6760,10 @@ fn rewrite_maven_pom(
67606760
// One pass over the pom's repositories: `(id, url)` of each, which also
67616761
// answers the per-dep URL-refresh check below while the pom is still
67626762
// unchanged (a no-op rescan then never re-scans the pom per dep).
6763-
let original_repos: Vec<(String, Option<String>)> =
6764-
pom.as_deref().map(maven_repository_ids_and_urls).unwrap_or_default();
6763+
let original_repos: Vec<(String, Option<String>)> = pom
6764+
.as_deref()
6765+
.map(maven_repository_ids_and_urls)
6766+
.unwrap_or_default();
67656767
let hosted_repo_generations: std::collections::BTreeSet<String> = original_repos
67666768
.iter()
67676769
.filter_map(|(id, _)| generation::pin_name_uuid(id, false).map(str::to_string))
@@ -10833,7 +10835,10 @@ mod tests {
1083310835
&[(YARNRC_REL, "yarn-offline-mirror: false\n")],
1083410836
&[(YARNRC_REL, "yarn-offline-mirror:\n")],
1083510837
&[(YARNRC_REL, "yarn-offline-mirror \"\"\n")],
10836-
&[(YARNRC_REL, "yarn-offline-mirror-pruning true\n# yarn-offline-mirror ./m\n")],
10838+
&[(
10839+
YARNRC_REL,
10840+
"yarn-offline-mirror-pruning true\n# yarn-offline-mirror ./m\n",
10841+
)],
1083710842
&[(npmrc::NPMRC_REL, "[scope]\nyarn-offline-mirror=./m\n")],
1083810843
&[
1083910844
(YARNRC_REL, "yarn-offline-mirror false\n"),
@@ -10849,7 +10854,10 @@ mod tests {
1084910854
let mut r = RewriteResult::default();
1085010855
rewrite_yarn_classic(&files, std::slice::from_ref(&ovr), &mut r);
1085110856
assert!(r.warnings.is_empty(), "{rcs:?}: {:?}", r.warnings);
10852-
assert!(r.files["yarn.lock"].contains("http://p.test/lp.tgz"), "{rcs:?}");
10857+
assert!(
10858+
r.files["yarn.lock"].contains("http://p.test/lp.tgz"),
10859+
"{rcs:?}"
10860+
);
1085310861
assert!(r.refused_yarn_classic_uuids.is_empty(), "{rcs:?}");
1085410862
}
1085510863
}
@@ -10874,7 +10882,10 @@ mod tests {
1087410882
rewrite_yarn_classic(&files, std::slice::from_ref(&other), &mut r);
1087510883
assert!(r.refused_yarn_classic_uuids.is_empty());
1087610884
assert_eq!(
10877-
r.warnings.iter().map(|w| w.code.as_str()).collect::<Vec<_>>(),
10885+
r.warnings
10886+
.iter()
10887+
.map(|w| w.code.as_str())
10888+
.collect::<Vec<_>>(),
1087810889
["redirect_yarn_classic_entry_not_found"]
1087910890
);
1088010891
}

0 commit comments

Comments
 (0)