Skip to content

Commit 172a25f

Browse files
mikolalysenkoclaude
andcommitted
Merge origin/main into arch-fix/takeover-atomic
Resolve the hosted.rs conflict with #1045: this branch moved the takeover out of hosted.rs into hosted/takeover.rs, so keep that layout and port #1045's change there. The takeover's ledger drop now compares PurlKeys and the local canonical_purl key helper is gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2 parents a9adbd3 + 628542d commit 172a25f

40 files changed

Lines changed: 1304 additions & 747 deletions

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

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ use socket_patch_core::patch::redirect::golang_local::{
1515
use socket_patch_core::patch::sidecars::{maven as maven_sidecars, SidecarAdvisoryCode};
1616
use socket_patch_core::telemetry::{track_patch_applied, track_patch_apply_failed};
1717
use socket_patch_core::utils::purl::parse_golang_purl;
18-
use socket_patch_core::utils::purl::{normalize_purl, purl_eq, strip_purl_qualifiers};
18+
use socket_patch_core::utils::purl::{normalize_purl, strip_purl_qualifiers};
19+
use socket_patch_core::utils::purl_key::PurlKey;
1920
use socket_patch_core::vendor::purl_keys_cover;
2021
use std::collections::{BTreeMap, HashMap, HashSet};
2122
use std::path::{Path, PathBuf};
@@ -103,7 +104,7 @@ async fn ensure_blobs_for_mismatches(
103104
args: &ApplyArgs,
104105
manifest: &PatchManifest,
105106
all_packages: &HashMap<String, Vec<PathBuf>>,
106-
vendored_purls: &HashSet<String>,
107+
vendored_purls: &HashSet<PurlKey>,
107108
staged: &mut StagedSources,
108109
client: &ApiClient,
109110
) {
@@ -224,7 +225,7 @@ fn format_mismatch_fetch_result(downloaded: usize, needed: usize) -> String {
224225
async fn mismatch_blob_gaps(
225226
manifest: &PatchManifest,
226227
all_packages: &HashMap<String, Vec<PathBuf>>,
227-
vendored_purls: &HashSet<String>,
228+
vendored_purls: &HashSet<PurlKey>,
228229
blobs_path: &Path,
229230
force: bool,
230231
) -> HashSet<String> {
@@ -247,10 +248,11 @@ async fn mismatch_blob_gaps(
247248
};
248249
let variant_eco = Ecosystem::from_purl(purl).is_some_and(|e| e.supports_release_variants());
249250
let stripped = strip_purl_qualifiers(purl);
251+
let identity = PurlKey::new(purl);
250252
let records: Vec<(&String, &PatchRecord)> = manifest
251253
.patches
252254
.iter()
253-
.filter(|(key, _)| *key == purl || strip_purl_qualifiers(key) == stripped)
255+
.filter(|(key, _)| *key == purl || PurlKey::new(key) == identity)
254256
.collect();
255257
if purl_keys_cover(vendored_purls, purl)
256258
|| records
@@ -1729,7 +1731,7 @@ async fn report_apply_failure(
17291731
/// main-thread stack in debug builds.
17301732
fn synthesize_vendor_owned_results(
17311733
target_manifest_purls: &HashSet<String>,
1732-
vendored_purls: &HashSet<String>,
1734+
vendored_purls: &HashSet<PurlKey>,
17331735
) -> (Vec<ApplyResult>, HashSet<String>, HashSet<String>) {
17341736
let is_vendored = |p: &str| purl_keys_cover(vendored_purls, p);
17351737
let mut results: Vec<ApplyResult> = Vec::new();
@@ -3264,18 +3266,10 @@ async fn lockfile_resolved(common: &GlobalArgs, unmatched: &[String]) -> HashSet
32643266
}
32653267
let ctx = crate::commands::context::ProjectContext::new(common);
32663268
let entries = &ctx.locks().await.entries;
3267-
let lock_purls: HashSet<String> = entries
3268-
.iter()
3269-
.map(|e| normalize_purl(strip_purl_qualifiers(&e.purl)).into_owned())
3270-
.collect();
3269+
let lock_purls: HashSet<PurlKey> = entries.iter().map(|e| PurlKey::new(&e.purl)).collect();
32713270
unmatched
32723271
.iter()
3273-
.filter(|p| {
3274-
let base = strip_purl_qualifiers(p);
3275-
lock_purls.contains(normalize_purl(base).as_ref())
3276-
|| (base.starts_with("pkg:composer/")
3277-
&& entries.iter().any(|e| purl_eq(&e.purl, base)))
3278-
})
3272+
.filter(|p| lock_purls.contains(&PurlKey::new(p)))
32793273
.cloned()
32803274
.collect()
32813275
}
@@ -4001,7 +3995,7 @@ mod tests {
40013995
"without a vendor claim the drifted singleton must queue (fixture sanity)"
40023996
);
40033997

4004-
let vendored = HashSet::from(["pkg:gem/foo@1.0.0".to_string()]);
3998+
let vendored = HashSet::from([PurlKey::new("pkg:gem/foo@1.0.0")]);
40053999
let needed = mismatch_blob_gaps(&manifest, &all_packages, &vendored, &blobs, false).await;
40064000
assert!(
40074001
needed.is_empty(),

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

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use socket_patch_core::utils::concurrent::{api_concurrency_for, ordered_concurre
2323
use socket_patch_core::utils::purl::{
2424
canonical_purl, is_purl, normalize_purl, strip_purl_qualifiers,
2525
};
26+
use socket_patch_core::utils::purl_key::PurlKey;
2627
use socket_patch_core::vendor::{load_state, lookup_entry, VendorEntry, VendorState};
2728
use std::collections::HashMap;
2829
use std::fmt;
@@ -1430,9 +1431,8 @@ struct InstalledNarrowing {
14301431
/// runs — the coarse layer above [`filter_to_installed_releases`]'s
14311432
/// per-release variant narrowing (which still runs later, unchanged).
14321433
///
1433-
/// Presence evidence per result purl (compared on
1434-
/// `normalize_purl(strip_purl_qualifiers(..))` — API purls are
1435-
/// percent-encoded/qualified, crawler purls literal):
1434+
/// Presence evidence per result purl (compared by [`PurlKey`] — API purls
1435+
/// are percent-encoded/qualified/mixed-case, crawler purls literal):
14361436
/// * installed on disk — `find_packages_for_rollback` over the deduped base
14371437
/// purls (the qualified-aware resolver; memory invariant);
14381438
/// * already tracked in the manifest — the user opted this purl in earlier,
@@ -1465,8 +1465,6 @@ async fn filter_to_installed_purls(
14651465
use socket_patch_core::vendor::lock_inventory;
14661466
use std::collections::HashSet;
14671467

1468-
let canon = canonical_purl;
1469-
14701468
// Deduped base purls, probed against the installed tree. The resolver
14711469
// keys its result by the purls we pass, so canonicalize the found keys
14721470
// the same way as the membership probes below.
@@ -1480,14 +1478,14 @@ async fn filter_to_installed_purls(
14801478
};
14811479
let partitioned = partition_purls(&bases, None);
14821480
let found = find_packages_for_rollback(&partitioned, &common.crawler_options(), true).await;
1483-
let mut present: HashSet<String> = found.keys().map(|k| canon(k)).collect();
1481+
let mut present: HashSet<PurlKey> = found.keys().map(|k| PurlKey::new(k)).collect();
14841482

14851483
let ctx = super::context::ProjectContext::rooted(common, common.cwd.clone());
14861484
// Manifest membership counts as presence (read-only probe: a corrupt
14871485
// manifest degrades to "no extension" here — the download path's
14881486
// fail-closed read still guards every write).
14891487
if let Some(manifest) = ctx.ledgers().await.manifest {
1490-
present.extend(manifest.patches.keys().map(|k| canon(k)));
1488+
present.extend(manifest.patches.keys().map(|k| PurlKey::new(k)));
14911489
}
14921490

14931491
// scan's lockfile + vendored-ledger discovery supplements (and their
@@ -1498,11 +1496,11 @@ async fn filter_to_installed_purls(
14981496
let supplement = super::scan::project_lockfile_supplement(&ctx, &[], None).await;
14991497
pnp_diags = supplement.unsupported;
15001498
if mode != super::scan::ScanMode::Agent {
1501-
present.extend(supplement.entries.iter().map(|e| canon(&e.purl)));
1499+
present.extend(supplement.entries.iter().map(|e| PurlKey::new(&e.purl)));
15021500
let vendored =
15031501
super::scan::project_vendored_supplement(common, &[], &ctx.loaded().await.vendor)
15041502
.await;
1505-
present.extend(vendored.packages.iter().map(|p| canon(&p.purl)));
1503+
present.extend(vendored.packages.iter().map(|p| PurlKey::new(&p.purl)));
15061504
}
15071505
}
15081506

@@ -1536,7 +1534,7 @@ async fn filter_to_installed_purls(
15361534
warnings,
15371535
};
15381536
for result in accessible {
1539-
if present.contains(&canon(&result.purl)) {
1537+
if present.contains(&PurlKey::new(&result.purl)) {
15401538
out.kept.push(result.clone());
15411539
continue;
15421540
}
@@ -1565,7 +1563,7 @@ async fn filter_to_installed_purls(
15651563
// nothing — so it carries the same `package_not_installed` code
15661564
// a non-PnP pnpm project would get; only an UNREADABLE lock
15671565
// (no judgment possible) keeps the layout-refusal code.
1568-
let decoded = canon(&result.purl);
1566+
let decoded = canonical_purl(&result.purl);
15691567
let coord = decoded.strip_prefix("pkg:npm/").unwrap_or(&decoded);
15701568
if mode == super::scan::ScanMode::Hosted {
15711569
match (&pnpm_pnp_lock, coord.rsplit_once('@')) {
@@ -1780,7 +1778,8 @@ async fn lock_text_refusals_for(
17801778
)
17811779
.await,
17821780
);
1783-
let claimed: Vec<String> = pins.iter().map(|pin| canonical_purl(&pin.purl)).collect();
1781+
let claimed: std::collections::HashSet<PurlKey> =
1782+
pins.iter().map(|pin| PurlKey::new(&pin.purl)).collect();
17841783
let fetchable: Vec<&PatchSearchResult> = selected
17851784
.iter()
17861785
.filter(|sr| bun_refusal.filter(|r| r.applies_to(&sr.purl)).is_none())
@@ -1791,7 +1790,7 @@ async fn lock_text_refusals_for(
17911790
.collect();
17921791
let candidates: Vec<(&str, &str)> = fetchable
17931792
.iter()
1794-
.filter(|sr| !claimed.contains(&canonical_purl(&sr.purl)))
1793+
.filter(|sr| !claimed.contains(&PurlKey::new(&sr.purl)))
17951794
.map(|sr| (sr.purl.as_str(), sr.uuid.as_str()))
17961795
.collect();
17971796
let refused = socket_patch_core::vendor::lock_text_refusals(cwd, &candidates).await;
@@ -1815,7 +1814,7 @@ async fn lock_text_refusals_for(
18151814
cwd,
18161815
fetchable
18171816
.iter()
1818-
.filter(|sr| claimed.contains(&canonical_purl(&sr.purl)))
1817+
.filter(|sr| claimed.contains(&PurlKey::new(&sr.purl)))
18191818
.map(|sr| sr.purl.as_str()),
18201819
&pins,
18211820
false,
@@ -2389,8 +2388,8 @@ async fn run_nested_apply(
23892388
/// qualified record (apply keys a release-variant base by its base purl).
23902389
/// A qualified key never covers a sibling variant.
23912390
fn apply_key_covers(key: &str, record: &str) -> bool {
2392-
let (key, record) = (normalize_purl(key), normalize_purl(record));
2393-
key == record || (!key.contains(['?', '#']) && record.split(['?', '#']).next() == Some(&*key))
2391+
PurlKey::qualified(key) == PurlKey::qualified(record)
2392+
|| (!key.trim().contains(['?', '#']) && PurlKey::same(key, record))
23942393
}
23952394

23962395
/// Fold a failed nested apply into a `get` / `scan --mode agent` JSON
@@ -2433,7 +2432,8 @@ fn fold_apply_failures(
24332432
}
24342433
}
24352434
let appended = patches[selected..].iter().any(|r| {
2436-
normalize_purl(r["purl"].as_str().unwrap_or_default()) == normalize_purl(&failure.purl)
2435+
PurlKey::qualified(r["purl"].as_str().unwrap_or_default())
2436+
== PurlKey::qualified(&failure.purl)
24372437
});
24382438
if !hit && !appended {
24392439
let mut rec = serde_json::json!({

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,14 @@ impl HostedListing {
7070
/// One listing per hosted pin in `pins`, detailed from `legacy` where
7171
/// it records the same purl and uuid.
7272
pub(crate) fn from_pins(pins: &[HostedPin], legacy: Option<&RedirectState>) -> Vec<Self> {
73-
let canon = |p: &str| {
74-
socket_patch_core::utils::purl::normalize_purl(
75-
socket_patch_core::utils::purl::strip_purl_qualifiers(p),
76-
)
77-
.into_owned()
78-
};
73+
use socket_patch_core::utils::purl_key::PurlKey;
7974
pins.iter()
8075
.map(|pin| {
8176
let record = legacy
8277
.and_then(|l| {
8378
l.records
8479
.iter()
85-
.find(|(k, r)| canon(k) == canon(&pin.purl) && r.uuid == pin.uuid)
80+
.find(|(k, r)| PurlKey::same(k, &pin.purl) && r.uuid == pin.uuid)
8681
.map(|(_, r)| r.clone())
8782
})
8883
.unwrap_or_else(|| PatchRecord {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use socket_patch_core::manifest::schema::PatchManifest;
77
use socket_patch_core::patch::redirect::upstream::HostedPin;
88
use socket_patch_core::telemetry::{track_patch_remove_failed, track_patch_removed};
99
use socket_patch_core::utils::purl::patch_matches;
10+
use socket_patch_core::utils::purl_key::PurlKey;
1011
use socket_patch_core::vendor::{
1112
load_state, RevertOpts, VendorEntry, VendorState, VENDOR_STATE_REL,
1213
};
@@ -562,7 +563,7 @@ pub async fn run(args: RemoveArgs) -> i32 {
562563
// Vendor-owned purls are excluded from the in-place restore (the
563564
// vendored leg below reverts them); an unreadable ledger degrades to
564565
// "nothing vendored" here and fails closed at that leg.
565-
let vendored_keys: HashSet<String> = vendor_state_result
566+
let vendored_keys: HashSet<PurlKey> = vendor_state_result
566567
.as_ref()
567568
.map(socket_patch_core::vendor::VendorState::purl_keys)
568569
.unwrap_or_default();

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use socket_patch_core::patch::rollback::{
1515
};
1616
use socket_patch_core::telemetry::{track_patch_rollback_failed, track_patch_rolled_back};
1717
use socket_patch_core::utils::purl::{patch_matches, strip_purl_qualifiers};
18+
use socket_patch_core::utils::purl_key::PurlKey;
1819
use socket_patch_core::vendor::{purl_keys_cover, RevertOpts, VendorState};
19-
use socket_patch_core::vex::discover::{canonical_base_purl, same_release};
2020
use std::collections::{HashMap, HashSet};
2121
use std::path::{Path, PathBuf};
2222
use std::time::Duration;
@@ -1168,7 +1168,7 @@ pub async fn run(args: RollbackArgs) -> i32 {
11681168
// runs, and the ledger does not own the global copies, so the in-place
11691169
// leg restores them.
11701170
let loaded_vendor_state = socket_patch_core::vendor::load_state(&cwd).await;
1171-
let project_vendored_keys: HashSet<String> = loaded_vendor_state
1171+
let project_vendored_keys: HashSet<PurlKey> = loaded_vendor_state
11721172
.as_ref()
11731173
.map(VendorState::purl_keys)
11741174
.unwrap_or_default();
@@ -1181,7 +1181,7 @@ pub async fn run(args: RollbackArgs) -> i32 {
11811181
let vendor_corrupt = vendor_state_result.is_err();
11821182
// An unreadable ledger degrades to "nothing vendored" for the in-place
11831183
// leg (its own containment is the `vendor_state_unreadable` exit below).
1184-
let vendored_keys: HashSet<String> = vendor_state_result
1184+
let vendored_keys: HashSet<PurlKey> = vendor_state_result
11851185
.as_ref()
11861186
.map(VendorState::purl_keys)
11871187
.unwrap_or_default();
@@ -2020,7 +2020,7 @@ pub(crate) async fn rollback_patches_inner(
20202020
common: &GlobalArgs,
20212021
socket_dir: &Path,
20222022
manifest: &PatchManifest,
2023-
vendored_keys: &HashSet<String>,
2023+
vendored_keys: &HashSet<PurlKey>,
20242024
selection: InnerSelection<'_>,
20252025
// Manifest purl -> the hosted uuid a live lockfile pin superseded its
20262026
// record with ([`superseded_by_hosted`]); empty when no hosted pin
@@ -2811,10 +2811,10 @@ pub(crate) fn superseded_by_hosted(
28112811
.patches
28122812
.iter()
28132813
.filter_map(|(purl, record)| {
2814-
let pkg = canonical_base_purl(purl);
2814+
let pkg = PurlKey::new(purl);
28152815
let same: Vec<&HostedPin> = pins
28162816
.iter()
2817-
.filter(|pin| same_release(&pin.purl, &pkg))
2817+
.filter(|pin| PurlKey::new(&pin.purl) == pkg)
28182818
.collect();
28192819
if same.iter().any(|pin| pin.uuid == record.uuid) {
28202820
return None;

0 commit comments

Comments
 (0)