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("&amp;");
+                    break;
+                case '<':
+                    sb.append("&lt;");
+                    break;
+                case '>':
+                    sb.append("&gt;");
+                    break;
+                case '"':
+                    sb.append("&quot;");
+                    break;
+                case '\'':
+                    sb.append("&apos;");
+                    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>