diff --git a/impl/maven-support/src/test/java/org/apache/maven/model/v4/MavenStaxReaderNamespaceTest.java b/impl/maven-support/src/test/java/org/apache/maven/model/v4/MavenStaxReaderNamespaceTest.java new file mode 100644 index 000000000000..654646f4af18 --- /dev/null +++ b/impl/maven-support/src/test/java/org/apache/maven/model/v4/MavenStaxReaderNamespaceTest.java @@ -0,0 +1,111 @@ +/* + * 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. + */ +package org.apache.maven.model.v4; + +import javax.xml.stream.XMLStreamException; + +import java.io.StringReader; +import java.util.Locale; + +import org.apache.maven.api.model.InputSource; +import org.apache.maven.api.model.Model; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MavenStaxReaderNamespaceTest { + + private static final InputSource NO_INPUT_SOURCE = null; + + private static final String POM_41 = "" + + "" + + " 4.1.0" + + " com.acme" + + " demo" + + " 1.0" + + ""; + + private static final String POM_40 = "" + + "" + + " 4.0.0" + + " com.acme" + + " demo" + + " 1.0" + + ""; + + private static final String POM_NO_NS = "" + + "" + + " 4.0.0" + + " com.acme" + + " demo" + + " 1.0" + + ""; + + private static final String POM_BAD_NS = "" + + "" + + " 4.1.0" + + " com.acme" + + " demo" + + " 1.0" + + ""; + + @Test + void acceptsPom41() throws Exception { + MavenStaxReader reader = new MavenStaxReader(); + Model model = reader.read(new StringReader(POM_41), /*strict*/ true, NO_INPUT_SOURCE); + assertNotNull(model); + assertEquals("com.acme", model.getGroupId()); + assertEquals("demo", model.getArtifactId()); + assertEquals("1.0", model.getVersion()); + } + + @Test + void acceptsPom40() throws Exception { + MavenStaxReader reader = new MavenStaxReader(); + Model model = reader.read(new StringReader(POM_40), true, NO_INPUT_SOURCE); + assertNotNull(model); + assertEquals("com.acme", model.getGroupId()); + assertEquals("demo", model.getArtifactId()); + assertEquals("1.0", model.getVersion()); + } + + @Test + void acceptsPomWithoutNamespace() throws Exception { + MavenStaxReader reader = new MavenStaxReader(); + Model model = reader.read(new StringReader(POM_NO_NS), true, NO_INPUT_SOURCE); + assertNotNull(model); + assertEquals("com.acme", model.getGroupId()); + assertEquals("demo", model.getArtifactId()); + assertEquals("1.0", model.getVersion()); + } + + @Test + void rejectsUnexpectedPomNamespace() { + MavenStaxReader reader = new MavenStaxReader(); + XMLStreamException ex = assertThrows( + XMLStreamException.class, () -> reader.read(new StringReader(POM_BAD_NS), true, NO_INPUT_SOURCE)); + // sanity check: message mentions unrecognized namespace + assertTrue(ex.getMessage().toLowerCase(Locale.ROOT).contains("unrecognized pom namespace")); + } +} diff --git a/impl/maven-support/src/test/java/org/apache/maven/model/v4/MetadataStaxReaderNamespaceTest.java b/impl/maven-support/src/test/java/org/apache/maven/model/v4/MetadataStaxReaderNamespaceTest.java new file mode 100644 index 000000000000..275514ea371d --- /dev/null +++ b/impl/maven-support/src/test/java/org/apache/maven/model/v4/MetadataStaxReaderNamespaceTest.java @@ -0,0 +1,88 @@ +/* + * 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. + */ +package org.apache.maven.model.v4; + +import javax.xml.stream.XMLStreamException; + +import java.io.StringReader; + +import org.apache.maven.api.metadata.Metadata; +import org.apache.maven.metadata.v4.MetadataStaxReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MetadataStaxReaderNamespaceTest { + + private static final String META_110 = "" + + "" + + " com.acme" + + " demo" + + " " + + " 1.0" + + " 1.0" + + " " + + ""; + + private static final String META_NO_NS = "" + + "" + + " com.acme" + + " demo" + + " " + + " 1.0" + + " " + + ""; + + private static final String META_BAD_NS = "" + + "" + + " com.acme" + + " demo" + + ""; + + @Test + void acceptsMetadata110() throws Exception { + MetadataStaxReader reader = new MetadataStaxReader(); + Metadata metadata = reader.read(new StringReader(META_110), /*strict*/ true); + assertNotNull(metadata); + assertEquals("com.acme", metadata.getGroupId()); + assertEquals("demo", metadata.getArtifactId()); + assertNotNull(metadata.getVersioning()); + assertEquals("1.0", metadata.getVersioning().getLatest()); + } + + @Test + void acceptsMetadataWithoutNamespace() throws Exception { + MetadataStaxReader reader = new MetadataStaxReader(); + Metadata metadata = reader.read(new StringReader(META_NO_NS), true); + assertNotNull(metadata); + assertEquals("com.acme", metadata.getGroupId()); + assertEquals("demo", metadata.getArtifactId()); + } + + @Test + void rejectsUnexpectedMetadataNamespace() { + MetadataStaxReader reader = new MetadataStaxReader(); + XMLStreamException ex = + assertThrows(XMLStreamException.class, () -> reader.read(new StringReader(META_BAD_NS), true)); + assertTrue(ex.getMessage().toLowerCase().contains("unrecognized metadata namespace")); + } +} diff --git a/src/mdo/reader-stax.vm b/src/mdo/reader-stax.vm index 06aee9bc54b7..86cf107915b0 100644 --- a/src/mdo/reader-stax.vm +++ b/src/mdo/reader-stax.vm @@ -263,32 +263,58 @@ public class ${className} { #if ( $needXmlContext ) Deque context = new ArrayDeque<>(); #end - $rootUcapName $rootLcapName = null; - int eventType = parser.getEventType(); - boolean parsed = false; - while (eventType != XMLStreamReader.END_DOCUMENT) { - if (eventType == XMLStreamReader.START_ELEMENT) { - if (strict && ! "${rootTag}".equals(parser.getLocalName())) { - throw new XMLStreamException("Expected root element '${rootTag}' but found '" + parser.getName() + "'", parser.getLocation(), null); - } else if (parsed) { - // fallback, already expected a XMLStreamException due to invalid XML - throw new XMLStreamException("Duplicated tag: '${rootTag}'", parser.getLocation(), null); - } -#if ( $locationTracking ) - $rootLcapName = parse${rootUcapName}(parser, strict, parser.getNamespaceURI(), inputSrc); -#elseif ( $needXmlContext ) - $rootLcapName = parse${rootUcapName}(parser, strict, parser.getNamespaceURI(), context); -#else - $rootLcapName = parse${rootUcapName}(parser, strict, parser.getNamespaceURI()); -#end - parsed = true; +$rootUcapName $rootLcapName = null; + int eventType = parser.getEventType(); + boolean parsed = false; + while (eventType != XMLStreamReader.END_DOCUMENT) { + if (eventType == XMLStreamReader.START_ELEMENT) { + if (strict && ! "${rootTag}".equals(parser.getLocalName())) { + throw new XMLStreamException("Expected root element '${rootTag}' but found '" + parser.getName() + "'", parser.getLocation(), null); + } else if (parsed) { + throw new XMLStreamException("Duplicated tag: '${rootTag}'", parser.getLocation(), null); + } + // Root namespace policy: + // - POM ('project'): allow no namespace or any http://maven.apache.org/POM/ + // - METADATA ('metadata'): allow no namespace or any http://maven.apache.org/METADATA/ + // - Other readers: do not enforce root namespace here (child checks still apply when strict) + if (strict) { + final String rootNs = parser.getNamespaceURI(); + final boolean hasNs = rootNs != null && !rootNs.isEmpty(); + + if ("project".equals("${rootTag}")) { + if (hasNs && !rootNs.startsWith("http://maven.apache.org/POM/")) { + throw new XMLStreamException("Unrecognized POM namespace '" + rootNs + "'. Expected something like 'http://maven.apache.org/POM/4.x.y' or no namespace.", + parser.getLocation(), null); } - eventType = parser.next(); - } - if (parsed) { - return $rootLcapName; - } - throw new XMLStreamException("Expected root element '${rootTag}' but found no element at all: invalid XML document", parser.getLocation(), null); + } else if ("metadata".equals("${rootTag}")) { + if (hasNs && !rootNs.startsWith("http://maven.apache.org/METADATA/")) { + throw new XMLStreamException("Unrecognized METADATA namespace '" + rootNs + "'. Expected something like 'http://maven.apache.org/METADATA/1.x.y' or no namespace.", + parser.getLocation(), null); + } + } else { + // Other generated models (settings, lifecycles, extensions, plugin, toolchains, …): + // no hard-coded enforcement at the root level. + } + } + + #if ( $locationTracking ) + $rootLcapName = parse${rootUcapName}(parser, strict, parser.getNamespaceURI(), inputSrc); + #elseif ( $needXmlContext ) + $rootLcapName = parse${rootUcapName}(parser, strict, parser.getNamespaceURI(), context); + #else + $rootLcapName = parse${rootUcapName}(parser, strict, parser.getNamespaceURI()); + #end + parsed = true; + } + eventType = parser.next(); + } + + if (parsed) { + return $rootLcapName; + } + throw new XMLStreamException( + "Expected root element '${rootTag}' but found no element at all: invalid XML document", + parser.getLocation(), null); } //-- ${root.name} read(XMLStreamReader, boolean) #foreach ( $class in $model.allClasses )