From 005fc47c19c5e40232e039712cba01c92fec4bd0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 23:26:03 +0000 Subject: [PATCH 1/4] Start fix for #535, #550 Assisted-by: Claude Code:claude-opus-5-5 From 3cb508373c0f2aa2de35b009b8835e2411e76612 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 23:40:09 +0000 Subject: [PATCH 2/4] Read maven.config properties the way Maven does Vendoring a Maven reactor reads user properties from .mvn/maven.config so an override there beats a pom value. The reader split the whole file on whitespace and kept only -Dk=v tokens, so it: - used properties from commented-out "#" lines, which Maven 3.9+ ignores, and - missed --define=k=v and "-D k=v" (or -D and k=v on separate lines), which Maven applies. Either way vendor could rewrite a ${prop} dependency to the patched base, silently downgrading the build (and vex then attested it), or refuse a patch that should apply. Comment lines are now skipped, every -D/--define spelling is read, and -Dk sets k=true as in Maven. Fixes #535, #550. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/vendor/jvm/maven_reactor.rs | 111 +++++++++++++++++- 1 file changed, 105 insertions(+), 6 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs index db447fd6c..d1daf883c 100644 --- a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs +++ b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs @@ -1829,13 +1829,33 @@ fn has_fallback_repository(doc: &Doc) -> bool { }) } -/// `-Dk=v` tokens of a `maven.config`. +/// The user properties a `maven.config` defines, as Maven reads them: +/// `#` lines are comments (Maven 3.9+; older Maven rejects them), and a +/// property is `-Dk=v`, `--define=k=v`, or `-D` / `--define` followed by a +/// separate `k=v` token, on the same line or the next. `-Dk` sets `k=true`; +/// a later definition wins. fn cli_properties(config: &str) -> BTreeMap { - config - .split_whitespace() - .filter_map(|t| t.strip_prefix("-D")?.split_once('=')) - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect() + let mut tokens = config + .lines() + .map(str::trim) + .filter(|line| !line.starts_with('#')) + .flat_map(str::split_whitespace); + let mut props = BTreeMap::new(); + while let Some(token) = tokens.next() { + let def = match token { + "-D" | "--define" => tokens.next(), + _ => token + .strip_prefix("--define=") + .or_else(|| token.strip_prefix("-D")), + }; + let Some((k, v)) = def.map(|d| d.split_once('=').unwrap_or((d, "true"))) else { + continue; + }; + if !k.is_empty() { + props.insert(k.to_string(), v.to_string()); + } + } + props } // ── pom edits ─────────────────────────────────────────────────── @@ -3703,6 +3723,85 @@ mod tests { assert_eq!(text(&after, "a/pom.xml"), a.replace("${ct.version}", SV)); } + /// A module at `${ct.version}` whose pom says 1.10.0 (the patch base), + /// vendored with `config` as `.mvn/maven.config`. + fn vendor_with_maven_config(config: &str) -> (JvmPlan, Fs, String) { + let root = ROOT.replace( + " ", + " \n 1.10.0\n \n ", + ); + let a = module("a", &dep("${ct.version}")); + let files = fs(&[ + ("pom.xml", &root), + ("a/pom.xml", &a), + ("b/pom.xml", &module("b", "")), + (".mvn/maven.config", config), + ]); + let (plan, after) = vendor(&files); + (plan, after, a) + } + + // #535: Maven applies `--define=k=v`, `-D k=v` and `-D` / `k=v` on + // separate lines, so the override must beat the pom's 1.10.0. + #[test] + fn maven_config_long_and_split_define_spellings_win() { + for config in [ + "--define=ct.version=1.11.0\n", + "-D ct.version=1.11.0\n", + "-D\nct.version=1.11.0\n", + "--define\nct.version=1.11.0\n", + "-B\r\n--define=ct.version=1.11.0\r\n", + ] { + let (plan, after, a) = vendor_with_maven_config(config); + assert_eq!( + reasons(&plan), + ["conflicting_literal_version"], + "{config:?}" + ); + assert_eq!(text(&after, "a/pom.xml"), a, "{config:?}"); + } + } + + // #550: since Maven 3.9 a `#` line is a comment and defines nothing. + #[test] + fn maven_config_comment_lines_define_nothing() { + for config in [ + "# -Dct.version=1.11.0\n", + "# -Dct.version=1.11.0 old pin\n", + " # --define=ct.version=1.11.0\n", + "# just a note\n-B\n", + ] { + let (plan, after, a) = vendor_with_maven_config(config); + assert!(plan.warnings.is_empty(), "{config:?}: {:?}", plan.warnings); + assert_eq!( + text(&after, "a/pom.xml"), + a.replace("${ct.version}", SV), + "{config:?}" + ); + } + } + + #[test] + fn maven_config_user_properties_parse_like_maven() { + let props = cli_properties( + "# -Dgone=1\n-Da=1\n--define=b=2\n-D c=3\n-D\nd=4\n--define e=5\n-Dflag\n -Da=6 \n", + ); + let want = [ + ("a", "6"), + ("b", "2"), + ("c", "3"), + ("d", "4"), + ("e", "5"), + ("flag", "true"), + ]; + assert_eq!( + props, + want.iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>() + ); + } + #[test] fn project_version_expression_resolves() { let root = ROOT.replace("1-SNAPSHOT", "1.10.0"); From d3f741097c2b65ed52b0b3946c3ae20e1c927148 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 00:15:32 +0000 Subject: [PATCH 3/4] Read maven.config one argument per line Maven 3.9+ reads each maven.config line as one argument. Follow that: the value of -Dk=v is the rest of the line (so "-Dv=1.0 # note" is not 1.0), a wholly quoted argument is unquoted, trimming matches Java's String.trim, and a bare -D never swallows the next option. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/vendor/jvm/maven_reactor.rs | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs index d1daf883c..72931b239 100644 --- a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs +++ b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs @@ -1391,7 +1391,7 @@ struct Reactor { scope: Vec, /// pom → its local parent pom. local_parent: BTreeMap, - /// `-Dk=v` user properties from `.mvn/maven.config`. + /// User properties from `.mvn/maven.config` ([`cli_properties`]). cli_props: BTreeMap, } @@ -1829,28 +1829,47 @@ fn has_fallback_repository(doc: &Doc) -> bool { }) } -/// The user properties a `maven.config` defines, as Maven reads them: -/// `#` lines are comments (Maven 3.9+; older Maven rejects them), and a -/// property is `-Dk=v`, `--define=k=v`, or `-D` / `--define` followed by a -/// separate `k=v` token, on the same line or the next. `-Dk` sets `k=true`; -/// a later definition wins. +/// The user properties a `maven.config` defines, as Maven 3.9+ reads it: +/// one argument per line, `#` lines are comments (older Maven rejects +/// them), and a wholly quoted argument is unquoted. A property is +/// `-Dk=v`, `-D k=v`, `--define=k=v` or `--define k=v`, or a bare `-D` / +/// `--define` whose `k=v` is the next line. The value is the rest of the +/// argument, so `-Dv=1.0 # note` defines `1.0 # note`, as in Maven. +/// `-Dk` sets `k=true`; a later definition wins. fn cli_properties(config: &str) -> BTreeMap { - let mut tokens = config + // Java's `String.trim`: every char up to U+0020, nothing beyond. + fn trim(s: &str) -> &str { + s.trim_matches(|c: char| c <= ' ') + } + fn unquote(s: &str) -> &str { + s.strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + .map_or(s, trim) + } + let mut args = config .lines() - .map(str::trim) - .filter(|line| !line.starts_with('#')) - .flat_map(str::split_whitespace); + .map(|line| unquote(trim(line))) + .filter(|arg| !arg.is_empty() && !arg.starts_with('#')) + .peekable(); let mut props = BTreeMap::new(); - while let Some(token) = tokens.next() { - let def = match token { - "-D" | "--define" => tokens.next(), - _ => token - .strip_prefix("--define=") - .or_else(|| token.strip_prefix("-D")), + while let Some(arg) = args.next() { + let def = match arg.strip_prefix("--define") { + Some(rest) => rest + .strip_prefix('=') + .or_else(|| (rest.is_empty() || rest.starts_with(' ')).then_some(rest)), + None => arg.strip_prefix("-D"), }; - let Some((k, v)) = def.map(|d| d.split_once('=').unwrap_or((d, "true"))) else { + let Some(def) = def.map(trim) else { continue; }; + let def = match def { + "" => match args.next_if(|next| !next.starts_with('-')) { + Some(next) => next, + None => continue, + }, + def => def, + }; + let (k, v) = def.split_once('=').unwrap_or((def, "true")); if !k.is_empty() { props.insert(k.to_string(), v.to_string()); } @@ -3751,6 +3770,9 @@ mod tests { "-D\nct.version=1.11.0\n", "--define\nct.version=1.11.0\n", "-B\r\n--define=ct.version=1.11.0\r\n", + "\"-Dct.version=1.11.0\"\n", + // Maven 3.9+ reads the whole line as the value. + "-Dct.version=1.10.0 # TODO bump\n", ] { let (plan, after, a) = vendor_with_maven_config(config); assert_eq!( @@ -3784,7 +3806,8 @@ mod tests { #[test] fn maven_config_user_properties_parse_like_maven() { let props = cli_properties( - "# -Dgone=1\n-Da=1\n--define=b=2\n-D c=3\n-D\nd=4\n--define e=5\n-Dflag\n -Da=6 \n", + "# -Dgone=1\n-Da=1\n--define=b=2\n-D c=3\n-D\nd=4\n--define e=5\n-Dflag\n -Da=6 \n\ + \"-Dq=7\"\n-Dv=1.0 # note\n-D\n-Dw=8\n--defined=x\n-B\n", ); let want = [ ("a", "6"), @@ -3793,6 +3816,9 @@ mod tests { ("d", "4"), ("e", "5"), ("flag", "true"), + ("q", "7"), + ("v", "1.0 # note"), + ("w", "8"), ]; assert_eq!( props, From 0548a336661e2d95d44b0aa201e8f1287bb0744d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 02:05:06 +0000 Subject: [PATCH 4/4] Keep pre-3.9 multi-argument maven.config lines Maven before 3.9 splits .mvn/maven.config on whitespace, so a line like `-B -Dct.version=1.11.0` or `-Dfoo=1 -Dct.version=1.11.0` defines ct.version there. The old reader caught those tokens; the per-line reader dropped them, which let the planner rewrite a pom to the patched base while a Maven 3.6/3.8 build applies the override. Add any `-Dk=v` token from a whitespace split of the non-comment lines that the per-line reading did not define. Recognising an override that Maven 3.9 would not apply only makes the planner refuse to pin, so this stays fail-closed. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_011f9aeRoPDBr4nQsvdEk3uV --- .../src/vendor/jvm/maven_reactor.rs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs index 72931b239..8fcb01265 100644 --- a/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs +++ b/crates/socket-patch-core/src/vendor/jvm/maven_reactor.rs @@ -1835,7 +1835,9 @@ fn has_fallback_repository(doc: &Doc) -> bool { /// `-Dk=v`, `-D k=v`, `--define=k=v` or `--define k=v`, or a bare `-D` / /// `--define` whose `k=v` is the next line. The value is the rest of the /// argument, so `-Dv=1.0 # note` defines `1.0 # note`, as in Maven. -/// `-Dk` sets `k=true`; a later definition wins. +/// `-Dk` sets `k=true`; a later definition wins. Maven before 3.9 splits +/// the file on whitespace instead, so a `-Dk=v` token that only that reading +/// finds (`-B -Dk=v` on one line) is kept too, unless a line defines `k`. fn cli_properties(config: &str) -> BTreeMap { // Java's `String.trim`: every char up to U+0020, nothing beyond. fn trim(s: &str) -> &str { @@ -1874,6 +1876,18 @@ fn cli_properties(config: &str) -> BTreeMap { props.insert(k.to_string(), v.to_string()); } } + let tokens = config + .lines() + .filter(|line| !trim(line).starts_with('#')) + .flat_map(str::split_whitespace) + .filter_map(|t| t.strip_prefix("-D")?.split_once('=')); + let mut legacy = BTreeMap::new(); + for (k, v) in tokens { + if !k.is_empty() && !props.contains_key(k) { + legacy.insert(k.to_string(), v.to_string()); + } + } + props.extend(legacy); props } @@ -3773,6 +3787,9 @@ mod tests { "\"-Dct.version=1.11.0\"\n", // Maven 3.9+ reads the whole line as the value. "-Dct.version=1.10.0 # TODO bump\n", + // Maven before 3.9 splits a line into several arguments. + "-B -Dct.version=1.11.0\n", + "-Dfoo=1 -Dct.version=1.11.0\n", ] { let (plan, after, a) = vendor_with_maven_config(config); assert_eq!( @@ -3807,7 +3824,7 @@ mod tests { fn maven_config_user_properties_parse_like_maven() { let props = cli_properties( "# -Dgone=1\n-Da=1\n--define=b=2\n-D c=3\n-D\nd=4\n--define e=5\n-Dflag\n -Da=6 \n\ - \"-Dq=7\"\n-Dv=1.0 # note\n-D\n-Dw=8\n--defined=x\n-B\n", + \"-Dq=7\"\n-Dv=1.0 # note\n-D\n-Dw=8\n--defined=x\n-B\n-T 1C -Dy=9 -Dw=0\n", ); let want = [ ("a", "6"), @@ -3819,6 +3836,7 @@ mod tests { ("q", "7"), ("v", "1.0 # note"), ("w", "8"), + ("y", "9"), ]; assert_eq!( props,