You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/10/19 20:38:36 UTC
(maven) branch maven-4.0.x updated: fix: settings interpolation for non-String fields and strict parsing mode (#13626)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/maven-4.0.x by this push:
new dc96bcd929 fix: settings interpolation for non-String fields and strict parsing mode (#13626)
dc96bcd929 is described below
commit dc96bcd929188e9ac034e9903f8c14ef24497d43
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Oct 19 22:38:29 2026 +0200
fix: settings interpolation for non-String fields and strict parsing mode (#13626)
- Raw XML interpolation before parsing: non-String settings fields
(e.g. proxy.port as int, proxy.active as boolean) containing
${...} property placeholders now resolve correctly instead of
failing to parse or silently using default values
- XML-escape interpolated values before re-parsing to prevent parse
errors when property values contain XML-special characters (MNG-4625)
- Strict parsing mode: settings XML parse errors are now reported at
ERROR level with fail-fast behaviour when strict parsing is enabled
via the MAVEN_SETTINGS_STRICT_PARSING constant
- Tests: proxy interpolation with integer/boolean fields, strict
parsing failure test, stream-close behaviour coverage
---
.../main/java/org/apache/maven/api/Constants.java | 8 ++
.../apache/maven/impl/DefaultSettingsBuilder.java | 119 ++++++++++++++++++++-
.../impl/DefaultSettingsBuilderFactoryTest.java | 112 +++++++++++++++++++
.../settings/settings-proxy-interpolation.xml | 35 ++++++
.../settings/settings-unknown-element.xml | 25 +++++
.../settings/settings-xml-markup-interpolation.xml | 40 +++++++
6 files changed, 337 insertions(+), 2 deletions(-)
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
index 1282228845..1c6586935a 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
@@ -198,6 +198,14 @@ public final class Constants {
public static final String MAVEN_SETTINGS_SECURITY_FILE_NAME = "settings-security4.xml";
+ /**
+ * When set to {@code "true"}, settings parsing errors are fatal instead of warnings.
+ *
+ * @since 4.0.0
+ */
+ @Config(defaultValue = "false")
+ public static final String MAVEN_SETTINGS_STRICT_PARSING = "maven.settings.strictParsing";
+
public static final String MAVEN_STYLE_PREFIX = "maven.style.";
// Style Names
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsBuilder.java
index fd621703cb..b0dd1350a0 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsBuilder.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSettingsBuilder.java
@@ -21,9 +21,11 @@
import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamException;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
@@ -155,17 +157,37 @@ private Settings readSettings(
return Settings.newInstance();
}
+ // Determine if strict parsing mode is requested
+ boolean strictParsing = Boolean.parseBoolean(
+ request.getSession().getEffectiveProperties().get(Constants.MAVEN_SETTINGS_STRICT_PARSING));
+
Settings settings;
+ // Interpolate the raw XML before parsing so that non-String fields
+ // (e.g. proxy.port, proxy.active) are resolved at the text level
+ // before Modello converts them to int/boolean.
+ Source interpolatedSource = interpolateSource(settingsSource, request);
+
try {
- try (InputStream is = settingsSource.openStream()) {
+ try (InputStream is = interpolatedSource.openStream()) {
settings = settingsXmlFactory.read(XmlReaderRequest.builder()
.inputStream(is)
.location(settingsSource.getLocation())
.strict(true)
.build());
} catch (XmlReaderException e) {
- try (InputStream is = settingsSource.openStream()) {
+ if (strictParsing) {
+ Location loc = e.getCause() instanceof XMLStreamException xe ? xe.getLocation() : null;
+ problems.reportProblem(new DefaultBuilderProblem(
+ settingsSource.getLocation(),
+ loc != null ? loc.getLineNumber() : -1,
+ loc != null ? loc.getColumnNumber() : -1,
+ e,
+ e.getMessage(),
+ BuilderProblem.Severity.FATAL));
+ return Settings.newInstance();
+ }
+ try (InputStream is = interpolatedSource.openStream()) {
settings = settingsXmlFactory.read(XmlReaderRequest.builder()
.inputStream(is)
.location(settingsSource.getLocation())
@@ -245,6 +267,99 @@ private Server serverAlias(Server server, String id) {
return Server.newBuilder(server, true).id(id).aliases(List.of()).build();
}
+ /**
+ * XML-escapes a value so that it can be safely embedded in XML text content.
+ * Replaces the five predefined XML entities: {@code &}, {@code <}, {@code >},
+ * {@code "}, and {@code '}.
+ */
+ private static String xmlEscape(String value) {
+ if (value == null) {
+ return null;
+ }
+ // Quick check: if none of the special chars are present, return as-is
+ if (value.indexOf('&') < 0
+ && value.indexOf('<') < 0
+ && value.indexOf('>') < 0
+ && value.indexOf('"') < 0
+ && value.indexOf('\'') < 0) {
+ return value;
+ }
+ StringBuilder sb = new StringBuilder(value.length() + 16);
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ switch (c) {
+ case '&':
+ sb.append("&");
+ break;
+ case '<':
+ sb.append("<");
+ break;
+ case '>':
+ sb.append(">");
+ break;
+ case '"':
+ sb.append(""");
+ break;
+ case '\'':
+ sb.append("'");
+ break;
+ default:
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Wraps the source so that when {@code openStream()} is called, the raw XML is read,
+ * interpolated as a string (resolving non-String fields like {@code <port>${test.port}</port>}
+ * before Modello converts them to int/boolean), and returned as a new stream.
+ *
+ * <p>Resolved property values are XML-escaped before substitution so that values containing
+ * XML special characters (e.g. {@code &x=y<>}) do not corrupt the XML structure.</p>
+ */
+ private Source interpolateSource(Source settingsSource, SettingsBuilderRequest request) {
+ UnaryOperator<String> src;
+ if (request.getInterpolationSource().isPresent()) {
+ src = request.getInterpolationSource().get();
+ } else {
+ Map<String, String> userProperties = request.getSession().getUserProperties();
+ Map<String, String> systemProperties = request.getSession().getSystemProperties();
+ src = Interpolator.chain(userProperties::get, systemProperties::get);
+ }
+
+ return new Source() {
+ @Override
+ public Path getPath() {
+ return settingsSource.getPath();
+ }
+
+ @Override
+ public InputStream openStream() throws IOException {
+ String rawXml;
+ try (InputStream in = settingsSource.openStream()) {
+ byte[] bytes = in.readAllBytes();
+ rawXml = new String(bytes, StandardCharsets.UTF_8);
+ }
+ // XML-escape resolved values so that property values containing XML special
+ // characters (e.g. &, <, >) do not produce malformed XML after substitution.
+ String interpolatedXml =
+ interpolator.interpolate(rawXml, src, (name, value) -> xmlEscape(value), false);
+ return new ByteArrayInputStream(interpolatedXml.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public String getLocation() {
+ return settingsSource.getLocation();
+ }
+
+ @Override
+ public Source resolve(String relative) {
+ return settingsSource.resolve(relative);
+ }
+ };
+ }
+
private Settings interpolate(
Settings settings, SettingsBuilderRequest request, ProblemCollector<BuilderProblem> problems) {
UnaryOperator<String> src;
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsBuilderFactoryTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsBuilderFactoryTest.java
index 944b6c9a71..7b63dca00e 100644
--- a/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsBuilderFactoryTest.java
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/DefaultSettingsBuilderFactoryTest.java
@@ -28,10 +28,12 @@
import org.apache.maven.api.services.BuilderProblem;
import org.apache.maven.api.services.ProblemCollector;
import org.apache.maven.api.services.SettingsBuilder;
+import org.apache.maven.api.services.SettingsBuilderException;
import org.apache.maven.api.services.SettingsBuilderRequest;
import org.apache.maven.api.services.SettingsBuilderResult;
import org.apache.maven.api.services.Sources;
import org.apache.maven.api.services.xml.SettingsXmlFactory;
+import org.apache.maven.api.settings.Proxy;
import org.apache.maven.api.settings.Server;
import org.apache.maven.api.settings.Settings;
import org.apache.maven.impl.model.DefaultInterpolator;
@@ -44,7 +46,9 @@
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -64,6 +68,21 @@ void setup() {
}
SettingsBuilderResult execute(String settingsName) {
+ return execute(settingsName, Map.of(), Map.of());
+ }
+
+ SettingsBuilderResult execute(String settingsName, Map<String, String> userProperties) {
+ return execute(settingsName, userProperties, Map.of());
+ }
+
+ SettingsBuilderResult execute(
+ String settingsName, Map<String, String> userProperties, Map<String, String> systemProperties) {
+ Mockito.lenient().when(session.getUserProperties()).thenReturn(userProperties);
+ Mockito.lenient().when(session.getSystemProperties()).thenReturn(systemProperties);
+ Map<String, String> effectiveProperties = new java.util.HashMap<>(systemProperties);
+ effectiveProperties.putAll(userProperties);
+ Mockito.lenient().when(session.getEffectiveProperties()).thenReturn(effectiveProperties);
+
SettingsBuilder builder =
new DefaultSettingsBuilder(new DefaultSettingsXmlFactory(), new DefaultInterpolator(), Map.of());
assertNotNull(builder);
@@ -211,6 +230,99 @@ void projectSettingsCannotWidenServerCredentialOrigins(@TempDir Path tempDir) th
assertTrue(result.getProblems().hasWarningProblems());
}
+ @Test
+ void testProxyFieldInterpolation() {
+ // Note: in Maven 4 the settings model stores proxy.port and proxy.active as
+ // String-backed fields (portString / activeString), so both the pre-parse
+ // interpolateSource() step and the post-parse SettingsTransformer-based
+ // interpolate() step can resolve ${...} placeholders for these fields.
+ // The pre-parse step is retained for:
+ // - forward compatibility with model changes that may introduce true typed fields
+ // - custom SettingsParser implementations that bypass the transformer
+ // - consistency with the Maven 3.x fix for the same issue
+ // This test verifies end-to-end that proxy field interpolation works correctly.
+ Map<String, String> userProperties = Map.of(
+ "test.proxy.active", "true",
+ "test.proxy.port", "9090",
+ "test.proxy.nonProxyHosts", "localhost|*.example.org");
+
+ Settings settings =
+ execute("settings-proxy-interpolation", userProperties).getEffectiveSettings();
+
+ List<Proxy> proxies = settings.getProxies();
+ assertEquals(1, proxies.size());
+
+ Proxy proxy = proxies.get(0);
+ assertEquals("test-proxy", proxy.getId());
+ assertTrue(proxy.isActive(), "proxy.active should be interpolated to true");
+ assertEquals(9090, proxy.getPort(), "proxy.port should be interpolated to 9090");
+ assertEquals("localhost|*.example.org", proxy.getNonProxyHosts());
+ assertEquals("http", proxy.getProtocol());
+ assertEquals("proxy.example.org", proxy.getHost());
+
+ // Also verify that a custom interpolationSource (bypassing session properties) works.
+ // This exercises the interpolationSource branch in both interpolateSource() and interpolate().
+ Mockito.lenient().when(session.getUserProperties()).thenReturn(Map.of());
+ Mockito.lenient().when(session.getSystemProperties()).thenReturn(Map.of());
+ Mockito.lenient().when(session.getEffectiveProperties()).thenReturn(Map.of());
+ SettingsBuilder builder =
+ new DefaultSettingsBuilder(new DefaultSettingsXmlFactory(), new DefaultInterpolator(), Map.of());
+ SettingsBuilderRequest requestWithSource = SettingsBuilderRequest.builder()
+ .session(session)
+ .userSettingsSource(Sources.buildSource(getSettings("settings-proxy-interpolation")))
+ .interpolationSource(userProperties::get)
+ .build();
+ Settings settingsFromSource = builder.build(requestWithSource).getEffectiveSettings();
+ List<Proxy> proxiesFromSource = settingsFromSource.getProxies();
+ assertEquals(1, proxiesFromSource.size());
+ Proxy proxyFromSource = proxiesFromSource.get(0);
+ assertTrue(proxyFromSource.isActive(), "proxy.active should be interpolated via interpolationSource");
+ assertEquals(9090, proxyFromSource.getPort(), "proxy.port should be interpolated via interpolationSource");
+ }
+
+ @Test
+ void testInterpolationWithXmlMarkupInPropertyValue() {
+ // Regression test for MNG-4625: interpolating a property whose value contains XML special
+ // characters (& < >) must not produce malformed XML and must preserve the raw value.
+ Map<String, String> userProperties = Map.of("test.prop", "&x=y<>");
+
+ Settings settings =
+ execute("settings-xml-markup-interpolation", userProperties).getEffectiveSettings();
+
+ // The jdbcUrl property should contain the raw (unescaped) value, not XML entities
+ String jdbcUrl = settings.getProfiles().stream()
+ .filter(p -> "xml-markup".equals(p.getId()))
+ .findFirst()
+ .orElseThrow()
+ .getProperties()
+ .get("jdbcUrl");
+ assertEquals("&x=y<>", jdbcUrl, "Property value with XML markup must survive settings interpolation");
+ }
+
+ @Test
+ void testStrictParsingReportsError() {
+ Map<String, String> systemProperties = Map.of("maven.settings.strictParsing", "true");
+
+ // Valid settings should parse fine even in strict mode
+ Settings validSettings =
+ execute("settings-simple", Map.of(), systemProperties).getEffectiveSettings();
+ assertNotNull(validSettings);
+
+ // Settings with an unknown element must throw with a fatal problem in strict mode
+ SettingsBuilderException ex = assertThrows(
+ SettingsBuilderException.class, () -> execute("settings-unknown-element", Map.of(), systemProperties));
+ assertTrue(
+ ex.getProblemCollector().hasFatalProblems(),
+ "Strict parsing must produce a fatal problem for unknown XML elements");
+
+ // Without strict mode, the same settings should only produce a warning, not throw
+ SettingsBuilderResult lenientResult = execute("settings-unknown-element");
+ assertFalse(
+ lenientResult.getProblems().hasFatalProblems(),
+ "Lenient parsing must not produce a fatal problem for unknown XML elements");
+ assertNotNull(lenientResult.getEffectiveSettings());
+ }
+
private Path getSettings(String name) {
return Paths.get("src/test/resources/settings/" + name + ".xml").toAbsolutePath();
}
diff --git a/impl/maven-impl/src/test/resources/settings/settings-proxy-interpolation.xml b/impl/maven-impl/src/test/resources/settings/settings-proxy-interpolation.xml
new file mode 100644
index 0000000000..b7380a8426
--- /dev/null
+++ b/impl/maven-impl/src/test/resources/settings/settings-proxy-interpolation.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<settings xmlns='http://maven.apache.org/SETTINGS/2.0.0'>
+ <proxies>
+ <proxy>
+ <id>test-proxy</id>
+ <active>${test.proxy.active}</active>
+ <protocol>http</protocol>
+ <host>proxy.example.org</host>
+ <port>${test.proxy.port}</port>
+ <username>proxyuser</username>
+ <password>proxypass</password>
+ <nonProxyHosts>${test.proxy.nonProxyHosts}</nonProxyHosts>
+ </proxy>
+ </proxies>
+</settings>
diff --git a/impl/maven-impl/src/test/resources/settings/settings-unknown-element.xml b/impl/maven-impl/src/test/resources/settings/settings-unknown-element.xml
new file mode 100644
index 0000000000..724631eb39
--- /dev/null
+++ b/impl/maven-impl/src/test/resources/settings/settings-unknown-element.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<settings xmlns='http://maven.apache.org/SETTINGS/1.0.0'>
+ <localRepository>${user.home}/.m2/repository</localRepository>
+ <unknownElement>this-element-does-not-exist-in-the-schema</unknownElement>
+</settings>
diff --git a/impl/maven-impl/src/test/resources/settings/settings-xml-markup-interpolation.xml b/impl/maven-impl/src/test/resources/settings/settings-xml-markup-interpolation.xml
new file mode 100644
index 0000000000..3d95fd6b8a
--- /dev/null
+++ b/impl/maven-impl/src/test/resources/settings/settings-xml-markup-interpolation.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<!--
+ Settings file that uses a property whose value contains XML special characters
+ (e.g. &, <, >) to verify that interpolateSource() XML-escapes resolved values
+ before substituting them into the raw XML string (MNG-4625 regression test).
+-->
+<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
+ <profiles>
+ <profile>
+ <id>xml-markup</id>
+ <properties>
+ <!-- test.prop value is "&x=y<>" (raw, contains XML special characters) -->
+ <jdbcUrl>${test.prop}</jdbcUrl>
+ </properties>
+ </profile>
+ </profiles>
+ <activeProfiles>
+ <activeProfile>xml-markup</activeProfile>
+ </activeProfiles>
+</settings>