diff --git a/maven-compat/src/main/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManager.java b/maven-compat/src/main/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManager.java index 14f73e12907d..0169406bea76 100644 --- a/maven-compat/src/main/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManager.java +++ b/maven-compat/src/main/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManager.java @@ -273,6 +273,8 @@ protected Metadata readMetadata(File mappingFile) throws RepositoryMetadataReadE ValidatingMetadataXpp3Reader mappingReader = new ValidatingMetadataXpp3Reader(); result = mappingReader.read(reader, false); + + validateVersioning(result); } catch (FileNotFoundException e) { throw new RepositoryMetadataReadException("Cannot read metadata from '" + mappingFile + "'", e); } catch (IOException | XmlPullParserException e) { @@ -282,6 +284,51 @@ protected Metadata readMetadata(File mappingFile) throws RepositoryMetadataReadE return result; } + /** + * Version tokens adopted from repository metadata must be valid coordinate components; metadata carrying + * anything else is treated as invalid. + */ + private static void validateVersioning(Metadata metadata) throws RepositoryMetadataReadException { + if (metadata == null) { + return; + } + Versioning versioning = metadata.getVersioning(); + if (versioning == null) { + return; + } + validateVersionToken(versioning.getLatest()); + validateVersionToken(versioning.getRelease()); + for (String version : versioning.getVersions()) { + validateVersionToken(version); + } + for (SnapshotVersion snapshotVersion : versioning.getSnapshotVersions()) { + validateVersionToken(snapshotVersion.getVersion()); + } + Snapshot snapshot = versioning.getSnapshot(); + if (snapshot != null) { + validateVersionToken(snapshot.getTimestamp()); + } + } + + private static void validateVersionToken(String value) throws RepositoryMetadataReadException { + if (value == null || value.isEmpty()) { + return; + } + boolean valid = !"..".equals(value); + if (valid) { + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '/' || c == '\\' || c == ':' || Character.isISOControl(c)) { + valid = false; + break; + } + } + } + if (!valid) { + throw new RepositoryMetadataReadException("Metadata contains an invalid version token: '" + value + "'"); + } + } + /** * Ensures the last updated timestamp of the specified metadata does not refer to the future and fixes the local * metadata if necessary to allow proper merging/updating of metadata during deployment. diff --git a/maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerTest.java b/maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerTest.java new file mode 100644 index 000000000000..b56dc2be6546 --- /dev/null +++ b/maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerTest.java @@ -0,0 +1,63 @@ +/* + * 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.artifact.repository.metadata; + +import java.io.File; +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests that {@link DefaultRepositoryMetadataManager} rejects repository metadata carrying version tokens that + * are not valid coordinate components, on the legacy read path used when metadata is loaded for merging. + */ +public class DefaultRepositoryMetadataManagerTest { + + private final DefaultRepositoryMetadataManager manager = new DefaultRepositoryMetadataManager(); + + @Test + void testMetadataWithInvalidVersionTokenIsRejected() { + File metadataFile = testFile("metadata-invalid-token/maven-metadata.xml"); + + RepositoryMetadataReadException exception = + assertThrows(RepositoryMetadataReadException.class, () -> manager.readMetadata(metadataFile)); + + assertTrue(exception.getMessage().contains("invalid version token"), exception.getMessage()); + } + + @Test + void testMetadataWithInvalidSnapshotTimestampIsRejected() { + File metadataFile = testFile("metadata-invalid-timestamp/maven-metadata.xml"); + + RepositoryMetadataReadException exception = + assertThrows(RepositoryMetadataReadException.class, () -> manager.readMetadata(metadataFile)); + + assertTrue(exception.getMessage().contains("invalid version token"), exception.getMessage()); + } + + private static File testFile(String resource) { + URL url = Thread.currentThread().getContextClassLoader().getResource(resource); + assertNotNull(url, "test resource not found: " + resource); + return new File(url.getFile()); + } +} diff --git a/maven-compat/src/test/resources/metadata-invalid-timestamp/maven-metadata.xml b/maven-compat/src/test/resources/metadata-invalid-timestamp/maven-metadata.xml new file mode 100644 index 000000000000..fdd7cc27fdfb --- /dev/null +++ b/maven-compat/src/test/resources/metadata-invalid-timestamp/maven-metadata.xml @@ -0,0 +1,35 @@ + + + + + + org.apache.maven.its + dep-invalid-timestamp + 1.0-SNAPSHOT + + + 20120809.112920:1 + 1 + + 20120809112920 + + diff --git a/maven-compat/src/test/resources/metadata-invalid-token/maven-metadata.xml b/maven-compat/src/test/resources/metadata-invalid-token/maven-metadata.xml new file mode 100644 index 000000000000..92e1b6b813d3 --- /dev/null +++ b/maven-compat/src/test/resources/metadata-invalid-token/maven-metadata.xml @@ -0,0 +1,32 @@ + + + + + + org.apache.maven.its + dep-invalid-token + 1.0-SNAPSHOT + + 1.0:2.0 + 20120809112920 + + diff --git a/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReader.java b/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReader.java index f1ad14f5227a..d4a94851b788 100644 --- a/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReader.java +++ b/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReader.java @@ -29,6 +29,10 @@ import java.util.Objects; import org.apache.maven.artifact.repository.metadata.Metadata; +import org.apache.maven.artifact.repository.metadata.Plugin; +import org.apache.maven.artifact.repository.metadata.Snapshot; +import org.apache.maven.artifact.repository.metadata.SnapshotVersion; +import org.apache.maven.artifact.repository.metadata.Versioning; import org.apache.maven.repository.internal.metadata.ValidatingMetadataXpp3Reader; import org.codehaus.plexus.util.ReaderFactory; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; @@ -54,7 +58,9 @@ public Metadata read(Reader input, Map options) throws IOException { Objects.requireNonNull(input, "input cannot be null"); try (Reader in = input) { - return new ValidatingMetadataXpp3Reader().read(in, isStrict(options)); + Metadata metadata = new ValidatingMetadataXpp3Reader().read(in, isStrict(options)); + validateMetadata(metadata); + return metadata; } catch (XmlPullParserException e) { throw new MetadataParseException(e.getMessage(), e.getLineNumber(), e.getColumnNumber(), e); } @@ -64,7 +70,9 @@ public Metadata read(InputStream input, Map options) throws IOExcepti Objects.requireNonNull(input, "input cannot be null"); try (InputStream in = input) { - return new ValidatingMetadataXpp3Reader().read(in, isStrict(options)); + Metadata metadata = new ValidatingMetadataXpp3Reader().read(in, isStrict(options)); + validateMetadata(metadata); + return metadata; } catch (XmlPullParserException e) { throw new MetadataParseException(e.getMessage(), e.getLineNumber(), e.getColumnNumber(), e); } @@ -74,4 +82,57 @@ private boolean isStrict(Map options) { Object value = (options != null) ? options.get(IS_STRICT) : null; return value == null || Boolean.parseBoolean(value.toString()); } + + /** + * Coordinate-shaped tokens read from this metadata (versions, plugin artifactIds and prefixes) get carried + * forward by callers as if they were already-validated path and coordinate components. Reject anything that + * would not itself be a valid coordinate component here, before it leaves this reader. + */ + private static void validateMetadata(Metadata metadata) throws IOException { + if (metadata == null) { + return; + } + + Versioning versioning = metadata.getVersioning(); + if (versioning != null) { + validateToken("version", versioning.getRelease()); + validateToken("version", versioning.getLatest()); + for (String version : versioning.getVersions()) { + validateToken("version", version); + } + for (SnapshotVersion snapshotVersion : versioning.getSnapshotVersions()) { + validateToken("version", snapshotVersion.getVersion()); + } + Snapshot snapshot = versioning.getSnapshot(); + if (snapshot != null) { + validateToken("snapshot timestamp", snapshot.getTimestamp()); + } + } + + if (metadata.getPlugins() != null) { + for (Plugin plugin : metadata.getPlugins()) { + validateToken("plugin artifactId", plugin.getArtifactId()); + validateToken("plugin prefix", plugin.getPrefix()); + } + } + } + + private static void validateToken(String field, String value) throws IOException { + if (value == null || value.isEmpty()) { + return; + } + boolean valid = !"..".equals(value); + if (valid) { + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '/' || c == '\\' || c == ':' || Character.isISOControl(c)) { + valid = false; + break; + } + } + } + if (!valid) { + throw new IOException("Metadata contains an invalid " + field + ": '" + value + "'"); + } + } } diff --git a/maven-core/src/test/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReaderTest.java b/maven-core/src/test/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReaderTest.java new file mode 100644 index 000000000000..f80da9ba2ac2 --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReaderTest.java @@ -0,0 +1,67 @@ +/* + * 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.artifact.repository.metadata.io; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Collections; + +import org.apache.maven.artifact.repository.metadata.Metadata; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DefaultMetadataReaderTest { + + private final DefaultMetadataReader reader = new DefaultMetadataReader(); + + private File resource(String name) throws URISyntaxException { + return new File(getClass().getResource(name).toURI()); + } + + @Test + public void testWellFormedMetadataParsesUnchanged() throws Exception { + Metadata metadata = reader.read(resource("well-formed-metadata.xml"), Collections.emptyMap()); + + assertEquals("org.apache.maven.its", metadata.getGroupId()); + assertEquals("sample", metadata.getArtifactId()); + assertEquals("1.1", metadata.getVersioning().getRelease()); + assertEquals("1.1", metadata.getVersioning().getLatest()); + assertEquals("maven-sample-plugin", metadata.getPlugins().get(0).getArtifactId()); + } + + @Test + public void testVersionContainingColonIsRejected() throws Exception { + File input = resource("invalid-version-token.xml"); + + IOException e = assertThrows(IOException.class, () -> reader.read(input, Collections.emptyMap())); + assertTrue(e.getMessage().contains("1.0:evil")); + } + + @Test + public void testPluginArtifactIdContainingSlashIsRejected() throws Exception { + File input = resource("invalid-plugin-artifactid.xml"); + + IOException e = assertThrows(IOException.class, () -> reader.read(input, Collections.emptyMap())); + assertTrue(e.getMessage().contains("maven/sample-plugin")); + } +} diff --git a/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-plugin-artifactid.xml b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-plugin-artifactid.xml new file mode 100644 index 000000000000..aec5e1aa04f3 --- /dev/null +++ b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-plugin-artifactid.xml @@ -0,0 +1,31 @@ + + + + org.apache.maven.its + sample + + + Sample Plugin + sample + + maven/sample-plugin + + + diff --git a/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-version-token.xml b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-version-token.xml new file mode 100644 index 000000000000..14216e28319d --- /dev/null +++ b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-version-token.xml @@ -0,0 +1,28 @@ + + + + org.apache.maven.its + sample + + + 1.0:evil + 20150428055824 + + diff --git a/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/well-formed-metadata.xml b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/well-formed-metadata.xml new file mode 100644 index 000000000000..ce3b4fa9365a --- /dev/null +++ b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/well-formed-metadata.xml @@ -0,0 +1,39 @@ + + + + org.apache.maven.its + sample + + 1.1 + 1.1 + + 1.0 + 1.1 + + 20150428055824 + + + + Sample Plugin + sample + maven-sample-plugin + + + diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java index 398652003453..4dc86735a1fb 100644 --- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java +++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java @@ -75,6 +75,8 @@ class DefaultModelResolver implements ModelResolver { private final Set repositoryIds; + private final Set externalRepositoryIds; + DefaultModelResolver( RepositorySystemSession session, RequestTrace trace, @@ -93,6 +95,11 @@ class DefaultModelResolver implements ModelResolver { this.externalRepositories = Collections.unmodifiableList(new ArrayList<>(repositories)); this.repositoryIds = new HashSet<>(); + Set externalIds = new HashSet<>(); + for (RemoteRepository externalRepository : this.externalRepositories) { + externalIds.add(externalRepository.getId()); + } + this.externalRepositoryIds = Collections.unmodifiableSet(externalIds); } private DefaultModelResolver(DefaultModelResolver original) { @@ -105,6 +112,7 @@ private DefaultModelResolver(DefaultModelResolver original) { this.repositories = new ArrayList<>(original.repositories); this.externalRepositories = original.externalRepositories; this.repositoryIds = new HashSet<>(original.repositoryIds); + this.externalRepositoryIds = original.externalRepositoryIds; } @Override @@ -123,6 +131,13 @@ public void addRepository(final Repository repository, boolean replace) throws I return; } + if (externalRepositoryIds.contains(repository.getId())) { + // Replacement is meant to refresh a repository this model declared earlier, e.g. + // once its URL has been interpolated. Repositories supplied by the request or the + // session are not model-declared, so they keep precedence and are left in place. + return; + } + removeMatchingRepository(repositories, repository.getId()); } diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java index 9542bece8977..f5e1879bdab5 100644 --- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java +++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java @@ -23,6 +23,7 @@ import javax.inject.Singleton; import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; @@ -274,9 +275,13 @@ private Versioning readVersions( if (metadata.getFile() != null && metadata.getFile().exists()) { try (InputStream in = new FileInputStream(metadata.getFile())) { - versioning = new ValidatingMetadataXpp3Reader() + Versioning parsed = new ValidatingMetadataXpp3Reader() .read(in, false) .getVersioning(); + + validateVersioning(parsed); + + versioning = parsed; } } } @@ -289,6 +294,40 @@ private Versioning readVersions( return (versioning != null) ? versioning : new Versioning(); } + /** + * Version tokens adopted from repository metadata must be valid coordinate components; metadata carrying + * anything else is treated as invalid. + */ + private static void validateVersioning(Versioning versioning) throws IOException { + if (versioning == null) { + return; + } + for (String version : versioning.getVersions()) { + validateVersionToken(version); + } + validateVersionToken(versioning.getLatest()); + validateVersionToken(versioning.getRelease()); + } + + private static void validateVersionToken(String value) throws IOException { + if (value == null || value.isEmpty()) { + return; + } + boolean valid = !"..".equals(value); + if (valid) { + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '/' || c == '\\' || c == ':' || Character.isISOControl(c)) { + valid = false; + break; + } + } + } + if (!valid) { + throw new IOException("Metadata contains an invalid version token: '" + value + "'"); + } + } + private Versioning filterVersionsByRepositoryType(Versioning versioning, RemoteRepository remoteRepository) { if (remoteRepository == null) { return versioning; diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java index b3b5fe70cac6..70b65d9b2602 100644 --- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java +++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java @@ -278,10 +278,14 @@ private Versioning readVersions( if (metadata.getFile() != null && metadata.getFile().exists()) { try (InputStream in = new FileInputStream(metadata.getFile())) { - versioning = new ValidatingMetadataXpp3Reader() + Versioning parsed = new ValidatingMetadataXpp3Reader() .read(in, false) .getVersioning(); + validateVersioning(parsed); + + versioning = parsed; + /* NOTE: Users occasionally misuse the id "local" for remote repos which screws up the metadata of the local repository. This is especially troublesome during snapshot resolution so we try @@ -312,6 +316,44 @@ private Versioning readVersions( return (versioning != null) ? versioning : new Versioning(); } + /** + * Version tokens adopted from repository metadata must be valid coordinate components; metadata carrying + * anything else is treated as invalid. + */ + private static void validateVersioning(Versioning versioning) throws IOException { + if (versioning == null) { + return; + } + validateVersionToken(versioning.getLatest()); + validateVersionToken(versioning.getRelease()); + for (SnapshotVersion snapshotVersion : versioning.getSnapshotVersions()) { + validateVersionToken(snapshotVersion.getVersion()); + } + Snapshot snapshot = versioning.getSnapshot(); + if (snapshot != null) { + validateVersionToken(snapshot.getTimestamp()); + } + } + + private static void validateVersionToken(String value) throws IOException { + if (value == null || value.isEmpty()) { + return; + } + boolean valid = !"..".equals(value); + if (valid) { + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '/' || c == '\\' || c == ':' || Character.isISOControl(c)) { + valid = false; + break; + } + } + } + if (!valid) { + throw new IOException("Metadata contains an invalid version token: '" + value + "'"); + } + } + private void invalidMetadata( RepositorySystemSession session, RequestTrace trace, diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java index 790e85280a98..83b1c259bb15 100644 --- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java +++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java @@ -28,6 +28,7 @@ import org.apache.maven.repository.internal.RelocatedArtifact; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.resolution.ArtifactDescriptorException; import org.eclipse.aether.resolution.ArtifactDescriptorResult; import org.eclipse.sisu.Priority; import org.slf4j.Logger; @@ -50,11 +51,15 @@ public final class DistributionManagementArtifactRelocationSource implements Mav @Override public Artifact relocatedTarget( - RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model) { + RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model) + throws ArtifactDescriptorException { DistributionManagement distMgmt = model.getDistributionManagement(); if (distMgmt != null) { Relocation relocation = distMgmt.getRelocation(); if (relocation != null) { + validateCoordinateComponent(relocation.getGroupId(), "groupId", artifactDescriptorResult); + validateCoordinateComponent(relocation.getArtifactId(), "artifactId", artifactDescriptorResult); + validateCoordinateComponent(relocation.getVersion(), "version", artifactDescriptorResult); Artifact result = new RelocatedArtifact( artifactDescriptorResult.getRequest().getArtifact(), relocation.getGroupId(), @@ -73,4 +78,36 @@ public Artifact relocatedTarget( } return null; } + + /** + * Checks that a relocation coordinate component is usable as an artifact coordinate. Components outside + * the coordinate character set are rejected so that only well-formed coordinates enter resolution. + */ + private static void validateCoordinateComponent( + String value, String component, ArtifactDescriptorResult artifactDescriptorResult) + throws ArtifactDescriptorException { + if (value == null || value.isEmpty()) { + return; // component is not relocated: the original artifact's value is kept + } + if (isInvalidCoordinateComponent(value)) { + IllegalArgumentException cause = new IllegalArgumentException("Invalid relocation " + component + " '" + + value + "' in artifact descriptor for " + + artifactDescriptorResult.getRequest().getArtifact() + + ": not a valid artifact coordinate component"); + artifactDescriptorResult.addException(cause); + throw new ArtifactDescriptorException(artifactDescriptorResult, cause.getMessage(), cause); + } + } + + private static boolean isInvalidCoordinateComponent(String value) { + if ("..".equals(value) || value.contains("/") || value.contains("\\") || value.contains(":")) { + return true; + } + for (int i = 0; i < value.length(); i++) { + if (Character.isISOControl(value.charAt(i))) { + return true; + } + } + return false; + } } diff --git a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java index 747d24461f94..6b2b880ded4d 100644 --- a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java +++ b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java @@ -20,19 +20,25 @@ import javax.inject.Inject; +import java.io.File; import java.net.MalformedURLException; +import java.nio.file.Path; import java.util.Arrays; import org.apache.maven.model.Dependency; import org.apache.maven.model.Parent; +import org.apache.maven.model.Repository; import org.apache.maven.model.resolution.ModelResolver; import org.apache.maven.model.resolution.UnresolvableModelException; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.codehaus.plexus.testing.PlexusTest; +import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.impl.ArtifactResolver; import org.eclipse.aether.impl.RemoteRepositoryManager; import org.eclipse.aether.impl.VersionRangeResolver; +import org.eclipse.aether.repository.LocalRepository; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -195,6 +201,41 @@ public void testResolveDependencySuccessfullyResolvesExistingDependencyUsingHigh @Inject private RemoteRepositoryManager remoteRepositoryManager; + @Test + public void testConstructionSuppliedRepositoryKeepsPrecedence(@TempDir Path localRepository) throws Exception { + // An empty local repository, so resolution has to consult the remote repository list + // rather than a copy cached by another test in this class. + final DefaultRepositorySystemSession isolatedSession = MavenRepositorySystemUtils.newSession(); + isolatedSession.setLocalRepositoryManager( + system.newLocalRepositoryManager(isolatedSession, new LocalRepository(localRepository.toFile()))); + + final ModelResolver resolver = new DefaultModelResolver( + isolatedSession, + null, + this.getClass().getName(), + artifactResolver, + versionRangeResolver, + remoteRepositoryManager, + Arrays.asList(newTestRepository())); + + // A model-declared repository that reuses the external repository's id; the external + // repository must keep its slot. + final Repository repository = new Repository(); + repository.setId("repo"); + repository.setUrl(new File("target/no-such-repository").toURI().toURL().toString()); + + resolver.addRepository(repository); + resolver.addRepository(repository, true); + + final Parent parent = new Parent(); + parent.setGroupId("ut.simple"); + parent.setArtifactId("artifact"); + parent.setVersion("1.0"); + + // The external repository kept its slot, so the artifact still resolves. + assertNotNull(resolver.resolveModel(parent)); + } + private ModelResolver newModelResolver() throws ComponentLookupException, MalformedURLException { return new DefaultModelResolver( this.session, diff --git a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionRangeResolverTest.java b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionRangeResolverTest.java new file mode 100644 index 000000000000..99dd25904c3b --- /dev/null +++ b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionRangeResolverTest.java @@ -0,0 +1,75 @@ +/* + * 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.repository.internal; + +import javax.inject.Inject; + +import java.nio.file.Path; + +import org.codehaus.plexus.testing.PlexusTest; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.impl.VersionRangeResolver; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.resolution.VersionRangeRequest; +import org.eclipse.aether.resolution.VersionRangeResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.apache.maven.repository.internal.AbstractRepositoryTest.newTestRepository; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@PlexusTest +public class DefaultVersionRangeResolverTest { + + @Inject + private RepositorySystem system; + + @Inject + private VersionRangeResolver versionRangeResolver; + + private RepositorySystemSession session; + + // Each test gets its own local repository (rather than the module-wide target/local-repo) so that a + // previously cached resolution from another test cannot mask this test's outcome. + @BeforeEach + void setUp(@TempDir Path localRepoDir) { + DefaultRepositorySystemSession newSession = MavenRepositorySystemUtils.newSession(); + LocalRepository localRepo = new LocalRepository(localRepoDir.toFile()); + newSession.setLocalRepositoryManager(system.newLocalRepositoryManager(newSession, localRepo)); + session = newSession; + } + + @Test + void testRangeResolutionWithInvalidTokenInMetadataIsRejected() throws Exception { + VersionRangeRequest request = new VersionRangeRequest(); + request.addRepository(newTestRepository()); + request.setArtifact(new DefaultArtifact("org.apache.maven.its", "dep-invalid-range", "jar", "[1.0,2.0]")); + + VersionRangeResult result = versionRangeResolver.resolveVersionRange(session, request); + + // The metadata carries a versions[] entry that is not a valid coordinate component, so the whole + // document is treated as invalid and none of its versions (including the otherwise-valid 1.0 and 2.0) + // are offered as candidates for the range. + assertTrue(result.getVersions().isEmpty(), "expected no versions, got " + result.getVersions()); + } +} diff --git a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java index 5ad5727913cd..7670b5ea18b0 100644 --- a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java +++ b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java @@ -78,4 +78,31 @@ public void testResolveSeparateInstalledClassifiedNonVersionedArtifacts() throws VersionResult resultB = versionResolver.resolveVersion(session, requestB); assertEquals(versionB, resultB.getVersion()); } + + @Test + public void testSnapshotVersionFromMetadataWithInvalidTokenIsRejected() throws Exception { + VersionRequest request = new VersionRequest(); + request.addRepository(newTestRepository()); + Artifact artifact = new DefaultArtifact("org.apache.maven.its", "dep-invalid-sv", "", "jar", "1.0-SNAPSHOT"); + request.setArtifact(artifact); + + VersionResult result = versionResolver.resolveVersion(session, request); + + // The metadata carries a snapshotVersion value that is not a valid coordinate component, so the + // metadata is treated as invalid and resolution falls back to the requested base version. + assertEquals("1.0-SNAPSHOT", result.getVersion()); + } + + @Test + public void testSnapshotTimestampFromMetadataWithInvalidTokenIsRejected() throws Exception { + VersionRequest request = new VersionRequest(); + request.addRepository(newTestRepository()); + Artifact artifact = new DefaultArtifact("org.apache.maven.its", "dep-invalid-ts", "", "jar", "1.0-SNAPSHOT"); + request.setArtifact(artifact); + + VersionResult result = versionResolver.resolveVersion(session, request); + + // The metadata carries a snapshot timestamp that is not a valid coordinate component, so the + assertEquals("1.0-SNAPSHOT", result.getVersion()); + } } diff --git a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSourceTest.java b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSourceTest.java new file mode 100644 index 000000000000..f0d6b18d454c --- /dev/null +++ b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSourceTest.java @@ -0,0 +1,109 @@ +/* + * 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.repository.internal.relocation; + +import org.apache.maven.model.DistributionManagement; +import org.apache.maven.model.Model; +import org.apache.maven.model.Relocation; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.resolution.ArtifactDescriptorException; +import org.eclipse.aether.resolution.ArtifactDescriptorRequest; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; +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; + +/** + * Test cases for {@code DistributionManagementArtifactRelocationSource} coordinate handling. + */ +class DistributionManagementArtifactRelocationSourceTest { + + private final DistributionManagementArtifactRelocationSource source = + new DistributionManagementArtifactRelocationSource(); + + private static ArtifactDescriptorResult newResult() { + final ArtifactDescriptorRequest request = new ArtifactDescriptorRequest(); + request.setArtifact(new DefaultArtifact("ut.simple:artifact:1.0")); + return new ArtifactDescriptorResult(request); + } + + private static Model newModel(String groupId, String artifactId, String version) { + final Relocation relocation = new Relocation(); + relocation.setGroupId(groupId); + relocation.setArtifactId(artifactId); + relocation.setVersion(version); + + final DistributionManagement distMgmt = new DistributionManagement(); + distMgmt.setRelocation(relocation); + + final Model model = new Model(); + model.setDistributionManagement(distMgmt); + return model; + } + + @Test + void testWellFormedRelocationIsApplied() throws Exception { + final Artifact relocated = source.relocatedTarget(null, newResult(), newModel("ut.moved", "artifact", "2.0")); + + assertNotNull(relocated); + assertEquals("ut.moved", relocated.getGroupId()); + assertEquals("artifact", relocated.getArtifactId()); + assertEquals("2.0", relocated.getVersion()); + } + + @Test + void testRelocationGroupIdWithBackslashIsRejected() { + assertThrows( + ArtifactDescriptorException.class, + () -> source.relocatedTarget(null, newResult(), newModel("a\\b", null, null))); + } + + @Test + void testRelocationWithInvalidArtifactIdIsRejected() { + assertThrows( + ArtifactDescriptorException.class, + () -> source.relocatedTarget(null, newResult(), newModel(null, "a/b", null))); + } + + @Test + void testRelocationArtifactIdWithControlCharacterIsRejected() { + assertThrows( + ArtifactDescriptorException.class, + () -> source.relocatedTarget(null, newResult(), newModel(null, "a\nb", null))); + } + + @Test + void testRelocationWithInvalidVersionIsRejected() { + assertThrows( + ArtifactDescriptorException.class, + () -> source.relocatedTarget(null, newResult(), newModel(null, null, "1.0:2.0"))); + } + + @Test + void testVersionWithTrailingDotsIsAccepted() throws Exception { + // only the exact ".." token is rejected; "1.." is an unusual but valid version string + final Artifact relocated = source.relocatedTarget(null, newResult(), newModel(null, null, "1..")); + + assertNotNull(relocated); + assertEquals("1..", relocated.getVersion()); + } +} diff --git a/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml new file mode 100644 index 000000000000..637dc3c26a42 --- /dev/null +++ b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml @@ -0,0 +1,37 @@ + + + + + + org.apache.maven.its + dep-invalid-range + + 2.0 + 2.0 + + 1.0 + 1.0:2.0 + 2.0 + + 20120809112920 + + diff --git a/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml new file mode 100644 index 000000000000..bea32d36ca24 --- /dev/null +++ b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml @@ -0,0 +1,42 @@ + + + + + + org.apache.maven.its + dep-invalid-sv + 1.0-SNAPSHOT + + + 20120809.112920 + 1 + + 20120809112920 + + + jar + 1.0:2.0 + 20120809112920 + + + + diff --git a/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml new file mode 100644 index 000000000000..5c096f4886b7 --- /dev/null +++ b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml @@ -0,0 +1,35 @@ + + + + + + org.apache.maven.its + dep-invalid-ts + 1.0-SNAPSHOT + + + 20120809.112920:1 + 1 + + 20120809112920 + +