Skip to content

Commit 1c8482b

Browse files
committed
refactor: share NuGet config structure with hosted rewrites
1 parent 93ab1af commit 1c8482b

4 files changed

Lines changed: 214 additions & 220 deletions

File tree

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,19 @@ fn nuget_hosted_dotnet_restore_then_manifestless_vex() {
752752
let store_fx = sb.dir("store-fixture");
753753
let registry = restore_fixture(&dn, &sb, &fixture, &store_fx);
754754

755+
// Regression #561/#585: templates often keep inactive sources/mappings.
756+
// A forward rewrite must agree with NuGet and VEX about which XML is live.
757+
let inactive = "<!-- <packageSources><add key=\"old\" value=\"https://old.invalid/v3/index.json\" /></packageSources><packageSourceMapping></packageSourceMapping> -->";
758+
std::fs::write(
759+
fixture.join("nuget.config"),
760+
registry.0.replacen(
761+
"<configuration>",
762+
&format!("<configuration>\n {inactive}"),
763+
1,
764+
),
765+
)
766+
.unwrap();
767+
755768
let pristine = std::fs::read(pkg_dir(&store_fx).join(FILE_KEY)).unwrap();
756769
let mut patched = pristine.clone();
757770
patched.extend_from_slice(MARKER);
@@ -794,6 +807,10 @@ fn nuget_hosted_dotnet_restore_then_manifestless_vex() {
794807
let doc: Value = serde_json::from_slice(&std::fs::read(&embedded).unwrap()).unwrap();
795808
assert_attested(&doc, PURL, HOSTED_UUID, Marker::Redirected, &vulns());
796809
let config = std::fs::read_to_string(fixture.join("nuget.config")).unwrap();
810+
assert!(
811+
config.contains(inactive),
812+
"inactive XML left byte-exact: {config}"
813+
);
797814
assert!(
798815
config.contains(&format!("socket-patch-{HOSTED_UUID}"))
799816
&& config.contains(&format!("pattern=\"{ID}\"")),

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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//! attribute or a mismatched close tag makes the whole file `None`.
1212
1313
use std::collections::BTreeSet;
14+
use std::ops::Range;
1415

1516
// ── pure reader ──
1617

@@ -28,6 +29,47 @@ pub(crate) struct NugetConfig {
2829
pub(crate) mappings: Vec<(String, Vec<String>)>,
2930
/// Keys `configuration/disabledPackageSources` turns off.
3031
pub(crate) disabled: BTreeSet<String>,
32+
/// Live XML locations for writers. Routing readers and writers share
33+
/// the same treatment of comments, quoted attributes and element scope.
34+
pub(crate) configuration: Option<ConfigSection>,
35+
pub(crate) package_sources: Option<ConfigSection>,
36+
pub(crate) source_mapping: Option<ConfigSection>,
37+
/// Preserve the routing reader's behavior on repeated sections, but do
38+
/// not let a writer guess which occurrence should receive an edit.
39+
pub(crate) repeated_sections: bool,
40+
}
41+
42+
#[derive(Debug)]
43+
pub(crate) struct ConfigSection {
44+
pub(crate) open: Range<usize>,
45+
/// `None` for a self-closing element (unclosed XML fails parsing).
46+
pub(crate) close_start: Option<usize>,
47+
/// After the last direct `<clear>` child, or the opening tag otherwise.
48+
pub(crate) insert_at: usize,
49+
}
50+
51+
fn section_mut<'a>(
52+
cfg: &'a mut NugetConfig,
53+
parents: &[&str],
54+
name: &str,
55+
) -> Option<&'a mut Option<ConfigSection>> {
56+
match (parents, name) {
57+
([], "configuration") => Some(&mut cfg.configuration),
58+
(["configuration"], "packageSources") => Some(&mut cfg.package_sources),
59+
(["configuration"], "packageSourceMapping") => Some(&mut cfg.source_mapping),
60+
_ => None,
61+
}
62+
}
63+
64+
fn record_clear(cfg: &mut NugetConfig, parents: &[&str], end: usize) {
65+
let section = match parents {
66+
["configuration", "packageSources"] => cfg.package_sources.as_mut(),
67+
["configuration", "packageSourceMapping"] => cfg.source_mapping.as_mut(),
68+
_ => None,
69+
};
70+
if let Some(section) = section {
71+
section.insert_at = end;
72+
}
3173
}
3274

3375
/// One open (or self-closing) tag.
@@ -68,13 +110,33 @@ pub(crate) fn parse_config(text: &str) -> Option<NugetConfig> {
68110
i = at + rest.find('>')? + 1;
69111
} else if let Some(close) = rest.strip_prefix("</") {
70112
let end = close.find('>')?;
71-
if stack.pop()? != close[..end].trim() {
113+
let name = stack.pop()?;
114+
if name != close[..end].trim() {
72115
return None;
73116
}
74117
i = at + 2 + end + 1;
118+
if let Some(Some(section)) = section_mut(&mut cfg, &stack, name) {
119+
section.close_start = Some(at);
120+
}
121+
if name == "clear" {
122+
record_clear(&mut cfg, &stack, i);
123+
}
75124
} else {
76125
let (tag, consumed) = parse_open_tag(&rest[1..])?;
77126
i = at + 1 + consumed;
127+
if let Some(slot) = section_mut(&mut cfg, &stack, tag.name) {
128+
let repeated = slot
129+
.replace(ConfigSection {
130+
open: at..i,
131+
close_start: None,
132+
insert_at: i,
133+
})
134+
.is_some();
135+
cfg.repeated_sections |= repeated;
136+
}
137+
if tag.name == "clear" && tag.self_closing {
138+
record_clear(&mut cfg, &stack, i);
139+
}
78140
visit(&stack, &tag, &mut cfg, &mut open_mapping);
79141
if !tag.self_closing {
80142
if stack.len() >= MAX_XML_DEPTH {

0 commit comments

Comments
 (0)