Skip to content

Commit faade96

Browse files
committed
Resolve empty tilde registry segments as the npm alias
1 parent d9f5278 commit faade96

4 files changed

Lines changed: 174 additions & 42 deletions

File tree

‎crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs‎

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ use super::npm::{by_uuid, fetch_dists, read_or_refuse, refuse_all_in};
2626
use super::{Ctx, FormatResult, HostedPin, View};
2727
use crate::vendor::vlt_lock_text::{
2828
default_registry_alias, entry_text, is_default_registry, nodes_block, parse_node_line,
29-
registry_base, render_entry_line, render_tuple_with_slots, sniff_lock, split_dep_id,
30-
split_lines, DepIdEra, DepIdKind, LockSniff,
29+
registry_base, registry_segment, render_entry_line, render_tuple_with_slots, sniff_lock,
30+
split_dep_id, split_lines, DepIdEra, DepIdKind, LockSniff,
3131
};
3232

3333
/// One hosted node line to restore.
@@ -56,6 +56,7 @@ fn records_url(
5656
siblings: &[(DepIdEra, bool)],
5757
options: Option<&Map<String, Value>>,
5858
) -> bool {
59+
let segment = registry_segment(era, segment);
5960
let same_era: Vec<bool> = siblings
6061
.iter()
6162
.filter(|(e, _)| *e == era)
@@ -70,14 +71,15 @@ fn records_url(
7071
.and_then(|o| o.get("registries"))
7172
.is_some_and(Value::is_object)
7273
&& (segment.is_empty() || default_registry_alias(options) == Some(segment))
73-
&& !under_configured_registry(segment, name, options)
74+
&& !under_configured_registry(era, segment, name, options)
7475
}
7576

7677
/// Would a default-registry node on `segment` resolve under the lock's
7778
/// recorded `options.registry`? vlt omits slot [3] for such a node
7879
/// (`lockfile/save.ts`: `customRegistry = resolved && (!registry ||
7980
/// !resolved.startsWith(registry))`).
8081
fn under_configured_registry(
82+
era: DepIdEra,
8183
segment: &str,
8284
name: &str,
8385
options: Option<&Map<String, Value>>,
@@ -89,7 +91,7 @@ fn under_configured_registry(
8991
else {
9092
return false;
9193
};
92-
registry_base(segment, name, options).is_some_and(|base| base.starts_with(registry))
94+
registry_base(era, segment, name, options).is_some_and(|base| base.starts_with(registry))
9395
}
9496

9597
pub(crate) async fn restore(
@@ -173,7 +175,8 @@ pub(crate) async fn restore(
173175
let hosted = slot3.as_deref().and_then(|u| ctx.hosted_uuid(u));
174176
let Some(uuid) = hosted else {
175177
if let Some(dep_id) = dep_id.filter(|d| {
176-
d.kind == DepIdKind::Registry && is_default_registry(&d.first, options)
178+
d.kind == DepIdKind::Registry
179+
&& is_default_registry(registry_segment(d.era, &d.first), options)
177180
}) {
178181
siblings.push((dep_id.era, slot3.is_some()));
179182
}
@@ -205,7 +208,7 @@ pub(crate) async fn restore(
205208
);
206209
continue;
207210
};
208-
if !is_default_registry(&dep_id.first, options) {
211+
if !is_default_registry(registry_segment(dep_id.era, &dep_id.first), options) {
209212
result.refuse(
210213
&uuid,
211214
format!(
@@ -252,7 +255,7 @@ pub(crate) async fn restore(
252255
let line = parse_node_line(lines[hit.line]).expect("the hit line parsed above");
253256
let json = |s: &str| serde_json::to_string(s).expect("a str serializes to JSON");
254257
let slot3 = if records_url(hit.era, &hit.segment, &hit.name, &siblings, options) {
255-
let Some(base) = registry_base(&hit.segment, &hit.name, options) else {
258+
let Some(base) = registry_base(hit.era, &hit.segment, &hit.name, options) else {
256259
result.refuse(
257260
&hit.uuid,
258261
format!("{rel} maps no registry for the `{}` segment", hit.segment),
@@ -523,19 +526,21 @@ mod tests {
523526
let server = registry(&[("left-pad", "1.3.0", Some(LP_UPSTREAM))]).await;
524527
std::env::set_var("SOCKET_NPM_REGISTRY", server.uri());
525528
let url = hosted(LP_UUID, "left-pad-1.3.0.tgz");
526-
for (segment, options, want) in REGISTRY_BASE_CASES {
529+
for (era, segment, options, want) in REGISTRY_BASE_CASES {
527530
if segment.starts_with("http") {
528531
continue;
529532
}
530533
// A same-era sibling that records slot [3] makes restore write
531534
// one for the pin.
535+
let delimiter = era.delimiter();
536+
let lock_version = u8::from(*era == DepIdEra::Tilde);
532537
let text = format!(
533-
"{{\n \"lockfileVersion\": 1,\n \"options\": {options},\n \"nodes\": {{\n \"~{segment}~left-pad@1.3.0\": [0,\"left-pad\",\"sha512-AA==\",\"{url}\"],\n \"~{segment}~ms@2.1.3\": [0,\"ms\",\"sha512-M==\",\"https://x.example/ms/-/ms-2.1.3.tgz\"]\n }},\n \"edges\": {{}}\n}}\n"
538+
"{{\n \"lockfileVersion\": {lock_version},\n \"options\": {options},\n \"nodes\": {{\n \"{delimiter}{segment}{delimiter}left-pad@1.3.0\": [0,\"left-pad\",\"sha512-AA==\",\"{url}\"],\n \"{delimiter}{segment}{delimiter}ms@2.1.3\": [0,\"ms\",\"sha512-M==\",\"https://x.example/ms/-/ms-2.1.3.tgz\"]\n }},\n \"edges\": {{}}\n}}\n"
534539
);
535540
let (outcome, after) =
536541
run(&text, &[pin("pkg:npm/left-pad@1.3.0", LP_UUID)], false).await;
537542
let opts = opts(options);
538-
if is_default_registry(segment, Some(&opts)) {
543+
if is_default_registry(registry_segment(*era, segment), Some(&opts)) {
539544
let want = want.expect("a default registry always has a base");
540545
assert!(
541546
refused(&outcome).is_empty(),
@@ -558,6 +563,56 @@ mod tests {
558563
std::env::remove_var("SOCKET_NPM_REGISTRY");
559564
}
560565

566+
#[tokio::test]
567+
#[serial_test::serial]
568+
async fn restore_empty_tilde_resolution_and_sibling_admission() {
569+
let server = registry(&[("left-pad", "1.3.0", Some(LP_UPSTREAM))]).await;
570+
std::env::set_var("SOCKET_NPM_REGISTRY", server.uri());
571+
let url = hosted(LP_UUID, "left-pad-1.3.0.tgz");
572+
for (segment, options, sibling, base) in [
573+
(
574+
"",
575+
r#"{"registry":"https://a.example/","registries":{"npm":"https://b.example/"}}"#,
576+
"",
577+
"https://b.example/",
578+
),
579+
(
580+
"npm",
581+
r#"{"registry":"https://a.example/","registries":{"npm":"https://b.example/"}}"#,
582+
"",
583+
"https://b.example/",
584+
),
585+
(
586+
"corp",
587+
r#"{"default-registry-alias":"corp","registries":{"npm":"https://b.example/","corp":"https://c.example/"}}"#,
588+
// Empty tilde means npm, which is foreign here. Its lack
589+
// of slot 3 must not determine the corp node's convention.
590+
",\n \"~~ms@2.1.3\": [0,\"ms\",\"sha512-M==\"]",
591+
"https://c.example/",
592+
),
593+
] {
594+
let key = format!("~{segment}~left-pad@1.3.0");
595+
let text = format!(
596+
"{{\n \"lockfileVersion\": 1,\n \"options\": {options},\n \"nodes\": {{\n \"{key}\": [0,\"left-pad\",\"sha512-AA==\",\"{url}\"]{sibling}\n }},\n \"edges\": {{}}\n}}\n"
597+
);
598+
let (outcome, after) =
599+
run(&text, &[pin("pkg:npm/left-pad@1.3.0", LP_UUID)], false).await;
600+
assert!(refused(&outcome).is_empty(), "{:?}", refused(&outcome));
601+
let after: Value = serde_json::from_str(&after).unwrap();
602+
assert_eq!(
603+
after["nodes"][&key],
604+
serde_json::json!([
605+
0,
606+
"left-pad",
607+
LP_UPSTREAM,
608+
format!("{base}left-pad/-/left-pad-1.3.0.tgz")
609+
]),
610+
"{segment:?} {options}"
611+
);
612+
}
613+
std::env::remove_var("SOCKET_NPM_REGISTRY");
614+
}
615+
561616
#[tokio::test]
562617
#[serial_test::serial]
563618
async fn restore_honors_scope_for_slot3_and_configured_registry_omission() {

‎crates/socket-patch-core/src/vendor/lock_inventory/tests.rs‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2701,20 +2701,23 @@ async fn vlt_default_registry_base_follows_the_lock_options() {
27012701
#[tokio::test]
27022702
async fn vlt_inventory_resolves_registries_through_the_shared_registry_base() {
27032703
use crate::vendor::vlt_lock_text::REGISTRY_BASE_CASES;
2704-
for (segment, options, want) in REGISTRY_BASE_CASES {
2704+
for (era, segment, options, want) in REGISTRY_BASE_CASES {
27052705
// A URL segment is percent-encoded in a DepID; the table's other
27062706
// rows cover every precedence rule.
27072707
if segment.starts_with("http") {
27082708
continue;
27092709
}
27102710
let tmp = tempfile::tempdir().unwrap();
2711-
let node = format!(r#""~{segment}~@s/a@1.0.0": [0,"@s/a","sha512-a=="]"#);
2712-
write(
2713-
tmp.path(),
2714-
"vlt-lock.json",
2715-
&vlt_lock(options, &[node.as_str()]),
2716-
)
2717-
.await;
2711+
let delimiter = era.delimiter();
2712+
let name = crate::vendor::vlt_lock_text::encode_segment("@s/a@1.0.0", *era);
2713+
let node = format!(r#""{delimiter}{segment}{delimiter}{name}": [0,"@s/a","sha512-a=="]"#);
2714+
let lock = vlt_lock(options, &[node.as_str()]);
2715+
let lock = if *era == crate::vendor::vlt_lock_text::DepIdEra::Legacy {
2716+
lock.replacen("\"lockfileVersion\": 1", "\"lockfileVersion\": 0", 1)
2717+
} else {
2718+
lock
2719+
};
2720+
write(tmp.path(), "vlt-lock.json", &lock).await;
27182721
let entries = inventory_vlt(tmp.path()).await.unwrap();
27192722
assert_eq!(entries.len(), 1, "{segment:?} {options}");
27202723
assert_eq!(
@@ -2728,7 +2731,7 @@ async fn vlt_inventory_resolves_registries_through_the_shared_registry_base() {
27282731
#[tokio::test]
27292732
async fn vlt_inventory_honors_scope_for_named_and_url_registry_segments() {
27302733
use crate::vendor::vlt_lock_text::{encode_segment, DepIdEra, SCOPED_REGISTRY_OPTIONS};
2731-
for segment in ["npm", "corp", "https://explicit.example/npm"] {
2734+
for segment in ["", "npm", "corp", "https://explicit.example/npm"] {
27322735
let tmp = tempfile::tempdir().unwrap();
27332736
let node = format!(
27342737
r#""~{}~@s+a@1.0.0": [0,"@s/a","sha512-a=="]"#,

‎crates/socket-patch-core/src/vendor/lock_inventory/vlt.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use std::path::Path;
66

77
use serde_json::{Map, Value};
88

9-
use crate::constants::npm_family::VLT_LOCK;
109
use super::view::ProjectView;
10+
use crate::constants::npm_family::VLT_LOCK;
1111
use crate::vendor::vlt_lock_text::{
1212
registry_base, sniff_lock, split_dep_id, DepId, DepIdKind, LockSniff,
1313
};
@@ -106,7 +106,7 @@ pub(crate) fn vlt_registry_entries(lock: &VltLock) -> Vec<LockfileEntry> {
106106
return None;
107107
}
108108
let resolved = node.location.as_deref().and_then(http_url).or_else(|| {
109-
registry_base(&node.dep_id.first, name, options)
109+
registry_base(node.dep_id.era, &node.dep_id.first, name, options)
110110
.map(|base| npm_tarball_url(base.trim_end_matches('/'), name, version))
111111
});
112112
let integrity = node

0 commit comments

Comments
 (0)