diff --git a/compat/maven-compat/src/main/java/org/apache/maven/project/artifact/MavenMetadataSource.java b/compat/maven-compat/src/main/java/org/apache/maven/project/artifact/MavenMetadataSource.java index 568bc263e8d9..4a33b006f417 100644 --- a/compat/maven-compat/src/main/java/org/apache/maven/project/artifact/MavenMetadataSource.java +++ b/compat/maven-compat/src/main/java/org/apache/maven/project/artifact/MavenMetadataSource.java @@ -595,16 +595,19 @@ private ProjectRelocation retrieveRelocatedProject(Artifact artifact, MetadataRe if (relocation != null) { if (relocation.getGroupId() != null) { + requireValidCoordinateComponent(relocation.getGroupId(), "groupId", artifact); artifact.setGroupId(relocation.getGroupId()); relocatedArtifact = artifact; project.setGroupId(relocation.getGroupId()); } if (relocation.getArtifactId() != null) { + requireValidCoordinateComponent(relocation.getArtifactId(), "artifactId", artifact); artifact.setArtifactId(relocation.getArtifactId()); relocatedArtifact = artifact; project.setArtifactId(relocation.getArtifactId()); } if (relocation.getVersion() != null) { + requireValidCoordinateComponent(relocation.getVersion(), "version", artifact); // note: see MNG-3454. This causes a problem, but fixing it may break more. artifact.setVersionRange(VersionRange.createFromVersion(relocation.getVersion())); relocatedArtifact = artifact; @@ -668,6 +671,34 @@ private ProjectRelocation retrieveRelocatedProject(Artifact artifact, MetadataRe return rel; } + /** + * Checks that a relocation coordinate component is usable as an artifact coordinate component before it + * is applied to the artifact and project. A component outside the coordinate character set is rejected so + * that only well-formed coordinates enter resolution. + */ + private static void requireValidCoordinateComponent(String value, String component, Artifact artifact) + throws ArtifactMetadataRetrievalException { + if (isInvalidCoordinateComponent(value)) { + throw new ArtifactMetadataRetrievalException( + "Invalid relocation " + component + " '" + value + "' for " + artifact.getId() + + ": not a valid artifact coordinate component", + null, + artifact); + } + } + + 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; + } + private ModelProblem hasMissingParentPom(ProjectBuildingException e) { if (e.getCause() instanceof ModelBuildingException mbe) { for (ModelProblem problem : mbe.getProblems()) { diff --git a/compat/maven-compat/src/test/java/org/apache/maven/project/artifact/MavenMetadataSourceRelocationTest.java b/compat/maven-compat/src/test/java/org/apache/maven/project/artifact/MavenMetadataSourceRelocationTest.java new file mode 100644 index 000000000000..34ed688eb3b3 --- /dev/null +++ b/compat/maven-compat/src/test/java/org/apache/maven/project/artifact/MavenMetadataSourceRelocationTest.java @@ -0,0 +1,167 @@ +/* + * 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.project.artifact; + +import java.util.Collections; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.factory.ArtifactFactory; +import org.apache.maven.artifact.handler.DefaultArtifactHandler; +import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException; +import org.apache.maven.artifact.metadata.ResolutionGroup; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.metadata.RepositoryMetadataManager; +import org.apache.maven.bridge.MavenRepositorySystem; +import org.apache.maven.model.DistributionManagement; +import org.apache.maven.model.Relocation; +import org.apache.maven.plugin.LegacySupport; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.ProjectBuilder; +import org.apache.maven.project.ProjectBuildingRequest; +import org.apache.maven.project.ProjectBuildingResult; +import org.apache.maven.repository.legacy.metadata.DefaultMetadataResolutionRequest; +import org.apache.maven.repository.legacy.metadata.MetadataResolutionRequest; +import org.eclipse.aether.RepositorySystemSession; +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.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Verifies that a relocation read from a resolved project's distribution management is validated before its + * components are applied to the artifact and project being resolved, exercising the real + * {@link MavenMetadataSource#retrieve(MetadataResolutionRequest)} code path with mocked collaborators (no + * on-disk artifact resolution, so this test does not depend on, or share, any module-level local repository). + */ +class MavenMetadataSourceRelocationTest { + + private MavenMetadataSource newSource(ProjectBuilder projectBuilder) { + ArtifactFactory artifactFactory = mock(ArtifactFactory.class); + when(artifactFactory.createProjectArtifact(any(), any(), any(), any())) + .thenAnswer(invocation -> new DefaultArtifact( + (String) invocation.getArgument(0), + (String) invocation.getArgument(1), + (String) invocation.getArgument(2), + (String) invocation.getArgument(3), + "pom", + null, + new DefaultArtifactHandler("pom"))); + + LegacySupport legacySupport = mock(LegacySupport.class); + RepositorySystemSession repositorySession = mock(RepositorySystemSession.class); + when(legacySupport.getRepositorySession()).thenReturn(repositorySession); + when(legacySupport.getSession()).thenReturn(null); + + return new MavenMetadataSource( + mock(RepositoryMetadataManager.class), + artifactFactory, + projectBuilder, + mock(MavenMetadataCache.class), + legacySupport, + mock(MavenRepositorySystem.class)); + } + + private static Artifact newArtifact(String groupId, String artifactId, String version) { + return new DefaultArtifact( + groupId, artifactId, version, Artifact.SCOPE_COMPILE, "pom", null, new DefaultArtifactHandler("pom")); + } + + private static MavenProject newProject(String groupId, String artifactId, String version, Relocation relocation) { + MavenProject project = new MavenProject(); + project.setGroupId(groupId); + project.setArtifactId(artifactId); + project.setVersion(version); + if (relocation != null) { + DistributionManagement distMgmt = new DistributionManagement(); + distMgmt.setRelocation(relocation); + project.setDistributionManagement(distMgmt); + } + return project; + } + + private static MetadataResolutionRequest newRequest(Artifact artifact) { + MetadataResolutionRequest request = new DefaultMetadataResolutionRequest(); + request.setArtifact(artifact); + request.setLocalRepository(mock(ArtifactRepository.class)); + request.setRemoteRepositories(Collections.emptyList()); + return request; + } + + @Test + void testRelocationInvalidArtifactIdIsRejected() throws Exception { + Relocation relocation = new Relocation(); + relocation.setArtifactId("a/b"); + + MavenProject relocatingProject = newProject("group", "original", "1.0", relocation); + MavenProject finalProject = newProject("group", "a/b", "1.0", null); + + ProjectBuildingResult first = mock(ProjectBuildingResult.class); + when(first.getProject()).thenReturn(relocatingProject); + ProjectBuildingResult second = mock(ProjectBuildingResult.class); + when(second.getProject()).thenReturn(finalProject); + + ProjectBuilder projectBuilder = mock(ProjectBuilder.class); + when(projectBuilder.build(any(Artifact.class), any(ProjectBuildingRequest.class))) + .thenReturn(first, second); + + MavenMetadataSource source = newSource(projectBuilder); + Artifact artifact = newArtifact("group", "original", "1.0"); + MetadataResolutionRequest request = newRequest(artifact); + + ArtifactMetadataRetrievalException exception = + assertThrows(ArtifactMetadataRetrievalException.class, () -> source.retrieve(request)); + assertEquals(true, exception.getMessage().contains("a/b")); + assertEquals(true, exception.getMessage().contains("artifactId")); + } + + @Test + void testWellFormedRelocationIsApplied() throws Exception { + Relocation relocation = new Relocation(); + relocation.setGroupId("group.moved"); + relocation.setArtifactId("artifact-moved"); + relocation.setVersion("2.0"); + + MavenProject relocatingProject = newProject("group", "original", "1.0", relocation); + MavenProject finalProject = newProject("group.moved", "artifact-moved", "2.0", null); + + ProjectBuildingResult first = mock(ProjectBuildingResult.class); + when(first.getProject()).thenReturn(relocatingProject); + ProjectBuildingResult second = mock(ProjectBuildingResult.class); + when(second.getProject()).thenReturn(finalProject); + + ProjectBuilder projectBuilder = mock(ProjectBuilder.class); + when(projectBuilder.build(any(Artifact.class), any(ProjectBuildingRequest.class))) + .thenReturn(first, second); + + MavenMetadataSource source = newSource(projectBuilder); + Artifact artifact = newArtifact("group", "original", "1.0"); + MetadataResolutionRequest request = newRequest(artifact); + + ResolutionGroup result = source.retrieve(request); + + assertEquals("group.moved", artifact.getGroupId()); + assertEquals("artifact-moved", artifact.getArtifactId()); + assertEquals("2.0", artifact.getVersion()); + assertEquals(artifact, result.getRelocatedArtifact()); + } +} diff --git a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java index ef2e32469d55..0788a4bea2f4 100644 --- a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java +++ b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java @@ -76,6 +76,8 @@ class DefaultModelResolver implements ModelResolver { private final Set repositoryIds; + private final Set externalRepositoryIds; + DefaultModelResolver( RepositorySystemSession session, RequestTrace trace, @@ -94,6 +96,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) { @@ -106,6 +113,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 @@ -124,6 +132,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/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java index e96c0deaa510..082e65b41f72 100644 --- a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java +++ b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java @@ -22,6 +22,7 @@ import javax.inject.Named; import javax.inject.Singleton; +import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.util.ArrayList; @@ -235,8 +236,12 @@ private Versioning readVersions( if (metadata.getPath() != null && Files.exists(metadata.getPath())) { try (InputStream in = Files.newInputStream(metadata.getPath())) { - versioning = new Versioning( + Versioning parsed = new Versioning( new MetadataStaxReader().read(in, false).getVersioning()); + + validateVersioning(parsed); + + versioning = parsed; } } } @@ -249,6 +254,35 @@ 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, "version"); + } + validateVersionToken(versioning.getLatest(), "latest version"); + validateVersionToken(versioning.getRelease(), "release version"); + } + + private static void validateVersionToken(String value, String description) throws IOException { + if (value == null || value.isEmpty()) { + return; + } + boolean invalid = "..".equals(value) || value.contains("/") || value.contains("\\") || value.contains(":"); + for (int i = 0; i < value.length() && !invalid; i++) { + invalid = Character.isISOControl(value.charAt(i)); + } + if (invalid) { + throw new IOException("Rejecting metadata with invalid " + description + " '" + value + + "': must not contain '..', '/', '\\', ':' or control characters"); + } + } + private Versioning filterVersionsByRepositoryType(Versioning versioning, RemoteRepository remoteRepository) { if (remoteRepository == null) { return versioning; diff --git a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java index 5b349022f577..7284e37205eb 100644 --- a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java +++ b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java @@ -245,9 +245,13 @@ private Versioning readVersions( if (metadata.getPath() != null && Files.exists(metadata.getPath())) { try (InputStream in = Files.newInputStream(metadata.getPath())) { - versioning = new Versioning( + Versioning parsed = new Versioning( new MetadataStaxReader().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 @@ -278,6 +282,39 @@ 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(), "latest version"); + validateVersionToken(versioning.getRelease(), "release version"); + for (SnapshotVersion snapshotVersion : versioning.getSnapshotVersions()) { + validateVersionToken(snapshotVersion.getVersion(), "snapshot version"); + } + Snapshot snapshot = versioning.getSnapshot(); + if (snapshot != null) { + validateVersionToken(snapshot.getTimestamp(), "snapshot timestamp"); + } + } + + private static void validateVersionToken(String value, String description) throws IOException { + if (value == null || value.isEmpty()) { + return; + } + boolean invalid = "..".equals(value) || value.contains("/") || value.contains("\\") || value.contains(":"); + for (int i = 0; i < value.length() && !invalid; i++) { + invalid = Character.isISOControl(value.charAt(i)); + } + if (invalid) { + throw new IOException("Rejecting metadata with invalid " + description + " '" + value + + "': must not contain '..', '/', '\\', ':' or control characters"); + } + } + private void invalidMetadata( RepositorySystemSession session, RequestTrace trace, diff --git a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java index 7121855f3055..ce4fd1a5bf02 100644 --- a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java +++ b/compat/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; @@ -52,11 +53,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(), @@ -75,4 +80,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/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java b/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java index 7083c9c9d07e..b2adaf54c666 100644 --- a/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java +++ b/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java @@ -18,18 +18,23 @@ */ package org.apache.maven.repository.internal; +import java.io.File; import java.net.MalformedURLException; 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.eclipse.aether.RepositorySystemSession; 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; @@ -181,6 +186,46 @@ void testResolveDependencySuccessfullyResolvesExistingDependencyUsingHighestVers assertEquals("1.0", dependency.getVersion()); } + @Test + void testConstructionSuppliedRepositoryKeepsPrecedence(@TempDir File localRepository) throws Exception { + // An isolated local repository, so resolution has to consult the remote repository list + // rather than a copy cached by another test in this class. + final RepositorySystemSession isolatedSession = newMavenRepositorySystemSession(system, localRepository); + + final ModelResolver resolver = new DefaultModelResolver( + isolatedSession, + null, + this.getClass().getName(), + getContainer().lookup(ArtifactResolver.class), + getContainer().lookup(VersionRangeResolver.class), + getContainer().lookup(RemoteRepositoryManager.class), + 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 static RepositorySystemSession newMavenRepositorySystemSession( + org.eclipse.aether.RepositorySystem system, File localRepository) { + RepositorySystemSession.SessionBuilder builder = new MavenSessionBuilderSupplier(system).get(); + builder.withLocalRepositories(new LocalRepository(localRepository, "simple")); + return builder.build(); + } + private ModelResolver newModelResolver() throws ComponentLookupException, MalformedURLException { return new DefaultModelResolver( this.session, diff --git a/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionRangeResolverTest.java b/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionRangeResolverTest.java new file mode 100644 index 000000000000..546eacf29e38 --- /dev/null +++ b/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionRangeResolverTest.java @@ -0,0 +1,47 @@ +/* + * 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 org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.resolution.VersionRangeRequest; +import org.eclipse.aether.resolution.VersionRangeResult; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DefaultVersionRangeResolverTest extends AbstractRepositoryTestCase { + @Inject + private DefaultVersionRangeResolver versionRangeResolver; + + @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/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java b/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java index 8231324df9fd..e539037068ac 100644 --- a/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java +++ b/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java @@ -74,4 +74,32 @@ void testResolveSeparateInstalledClassifiedNonVersionedArtifacts() throws Except VersionResult resultB = versionResolver.resolveVersion(session, requestB); assertEquals(versionB, resultB.getVersion()); } + + @Test + 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 + 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 metadata + // is treated as invalid and resolution falls back to the requested base version. + assertEquals("1.0-SNAPSHOT", result.getVersion()); + } } diff --git a/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSourceTest.java b/compat/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSourceTest.java new file mode 100644 index 000000000000..f0d6b18d454c --- /dev/null +++ b/compat/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/compat/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml b/compat/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml new file mode 100644 index 000000000000..be649a0d6346 --- /dev/null +++ b/compat/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml @@ -0,0 +1,35 @@ + + + + + + org.apache.maven.its + dep-invalid-range + + 2.0 + 2.0 + + 1.0 + 1.0:2.0 + 2.0 + + 20120809112920 + + diff --git a/compat/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml b/compat/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..55a21aedc56c --- /dev/null +++ b/compat/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml @@ -0,0 +1,40 @@ + + + + + + org.apache.maven.its + dep-invalid-sv + 1.0-SNAPSHOT + + + 20120809.112920 + 1 + + 20120809112920 + + + jar + 1.0:2.0 + 20120809112920 + + + + diff --git a/compat/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml b/compat/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..3bf9f11780c0 --- /dev/null +++ b/compat/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml @@ -0,0 +1,33 @@ + + + + + + org.apache.maven.its + dep-invalid-ts + 1.0-SNAPSHOT + + + 20120809.112920:1 + 1 + + 20120809112920 + + diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java index b16caa3b72e7..9db0ae7428e2 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionRangeResolver.java @@ -18,6 +18,7 @@ */ package org.apache.maven.impl.resolver; +import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.util.ArrayList; @@ -231,8 +232,12 @@ private Versioning readVersions( if (metadata.getPath() != null && Files.exists(metadata.getPath())) { try (InputStream in = Files.newInputStream(metadata.getPath())) { - versioning = + Versioning parsed = new MetadataStaxReader().read(in, false).getVersioning(); + + validateVersioning(parsed); + + versioning = parsed; } } } @@ -245,6 +250,35 @@ private Versioning readVersions( return (versioning != null) ? versioning : Versioning.newInstance(); } + /** + * 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, "version"); + } + validateVersionToken(versioning.getLatest(), "latest version"); + validateVersionToken(versioning.getRelease(), "release version"); + } + + private static void validateVersionToken(String value, String description) throws IOException { + if (value == null || value.isEmpty()) { + return; + } + boolean invalid = "..".equals(value) || value.contains("/") || value.contains("\\") || value.contains(":"); + for (int i = 0; i < value.length() && !invalid; i++) { + invalid = Character.isISOControl(value.charAt(i)); + } + if (invalid) { + throw new IOException("Rejecting metadata with invalid " + description + " '" + value + + "': must not contain '..', '/', '\\', ':' or control characters"); + } + } + private Versioning filterVersionsByRepositoryType(Versioning versioning, RemoteRepository remoteRepository) { if (remoteRepository == null) { return versioning; diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java index 05a756cc2016..dfc1cebcff9b 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultVersionResolver.java @@ -243,9 +243,13 @@ private Versioning readVersions( if (metadata.getPath() != null && Files.exists(metadata.getPath())) { try (InputStream in = Files.newInputStream(metadata.getPath())) { - versioning = + Versioning parsed = new MetadataStaxReader().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 @@ -277,6 +281,39 @@ private Versioning readVersions( return (versioning != null) ? versioning : Versioning.newInstance(); } + /** + * 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(), "latest version"); + validateVersionToken(versioning.getRelease(), "release version"); + for (SnapshotVersion snapshotVersion : versioning.getSnapshotVersions()) { + validateVersionToken(snapshotVersion.getVersion(), "snapshot version"); + } + Snapshot snapshot = versioning.getSnapshot(); + if (snapshot != null) { + validateVersionToken(snapshot.getTimestamp(), "snapshot timestamp"); + } + } + + private static void validateVersionToken(String value, String description) throws IOException { + if (value == null || value.isEmpty()) { + return; + } + boolean invalid = "..".equals(value) || value.contains("/") || value.contains("\\") || value.contains(":"); + for (int i = 0; i < value.length() && !invalid; i++) { + invalid = Character.isISOControl(value.charAt(i)); + } + if (invalid) { + throw new IOException("Rejecting metadata with invalid " + description + " '" + value + + "': must not contain '..', '/', '\\', ':' or control characters"); + } + } + private void invalidMetadata( RepositorySystemSession session, RequestTrace trace, diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSource.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSource.java index 2549b4f653eb..fd86d0f58c2f 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSource.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSource.java @@ -28,6 +28,7 @@ import org.apache.maven.impl.resolver.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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,11 +50,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(), @@ -72,4 +77,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/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/MetadataVersionTokenValidationTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/MetadataVersionTokenValidationTest.java new file mode 100644 index 000000000000..dbc547e5ff69 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/MetadataVersionTokenValidationTest.java @@ -0,0 +1,110 @@ +/* + * 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.impl.resolver; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import org.apache.maven.api.ArtifactCoordinates; +import org.apache.maven.api.RemoteRepository; +import org.apache.maven.api.Session; +import org.apache.maven.api.Version; +import org.apache.maven.api.di.Named; +import org.apache.maven.api.di.Provides; +import org.apache.maven.impl.standalone.ApiRunner; +import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor; +import org.eclipse.aether.spi.io.PathProcessor; +import org.eclipse.aether.transport.apache.ApacheTransporterFactory; +import org.eclipse.aether.transport.file.FileTransporterFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that version tokens adopted from downloaded {@code maven-metadata.xml} are checked before they are + * spliced into a resolved coordinate, exercising the real {@link DefaultVersionResolver} and + * {@link DefaultVersionRangeResolver} used by the Maven 4 resolver stack (as opposed to the legacy compat + * path under {@code maven-resolver-provider}, which has its own equivalent tests). + */ +class MetadataVersionTokenValidationTest { + + Session session; + + @BeforeEach + void setup() { + Path basedir = Paths.get(System.getProperty("basedir", "")); + Path localRepoPath = basedir.resolve("target/local-repo"); + Path remoteRepoPath = basedir.resolve("src/test/remote-repo"); + Session s = ApiRunner.createSession( + injector -> injector.bindInstance(MetadataVersionTokenValidationTest.class, this), localRepoPath); + RemoteRepository remoteRepository = s.createRemoteRepository( + RemoteRepository.CENTRAL_ID, remoteRepoPath.toUri().toString()); + session = s.withRemoteRepositories(List.of(remoteRepository)); + } + + @Test + void testSnapshotVersionFromMetadataWithInvalidTokenIsRejected() { + ArtifactCoordinates coordinates = + session.createArtifactCoordinates("org.apache.maven.its:dep-invalid-sv:1.0-SNAPSHOT"); + + // 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. + Version version = session.resolveVersion(coordinates); + assertEquals("1.0-SNAPSHOT", version.toString()); + } + + @Test + void testSnapshotTimestampFromMetadataWithInvalidTokenIsRejected() { + ArtifactCoordinates coordinates = + session.createArtifactCoordinates("org.apache.maven.its:dep-invalid-ts:1.0-SNAPSHOT"); + + // The metadata carries a snapshot timestamp that is not a valid coordinate component, so the metadata + // is treated as invalid and resolution falls back to the requested base version. + Version version = session.resolveVersion(coordinates); + assertEquals("1.0-SNAPSHOT", version.toString()); + } + + @Test + void testRangeResolutionWithInvalidTokenInMetadataIsRejected() { + ArtifactCoordinates coordinates = + session.createArtifactCoordinates("org.apache.maven.its:dep-invalid-range:jar:[1.0,2.0]"); + + // 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. + List versions = session.resolveVersionRange(coordinates); + assertTrue(versions.isEmpty(), "expected no versions, got " + versions); + } + + @Provides + @Named(FileTransporterFactory.NAME) + static FileTransporterFactory newFileTransporterFactory() { + return new FileTransporterFactory(); + } + + @Provides + @Named(ApacheTransporterFactory.NAME) + static ApacheTransporterFactory newApacheTransporterFactory( + ChecksumExtractor checksumExtractor, PathProcessor pathProcessor) { + return new ApacheTransporterFactory(checksumExtractor, pathProcessor); + } +} diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java new file mode 100644 index 000000000000..304f1f58dec8 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/relocation/DistributionManagementArtifactRelocationSourceTest.java @@ -0,0 +1,108 @@ +/* + * 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.impl.resolver.relocation; + +import org.apache.maven.api.model.DistributionManagement; +import org.apache.maven.api.model.Model; +import org.apache.maven.api.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 = Relocation.newBuilder() + .groupId(groupId) + .artifactId(artifactId) + .version(version) + .build(); + + final DistributionManagement distMgmt = + DistributionManagement.newBuilder().relocation(relocation).build(); + + return Model.newBuilder().distributionManagement(distMgmt).build(); + } + + @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/impl/maven-impl/src/test/remote-repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml b/impl/maven-impl/src/test/remote-repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml new file mode 100644 index 000000000000..50a3cf6db744 --- /dev/null +++ b/impl/maven-impl/src/test/remote-repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml @@ -0,0 +1,15 @@ + + + org.apache.maven.its + dep-invalid-range + + 2.0 + 2.0 + + 1.0 + 1.0:2.0 + 2.0 + + 20120809112920 + + diff --git a/impl/maven-impl/src/test/remote-repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml b/impl/maven-impl/src/test/remote-repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml new file mode 100644 index 000000000000..1cc2a906b28f --- /dev/null +++ b/impl/maven-impl/src/test/remote-repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml @@ -0,0 +1,20 @@ + + + org.apache.maven.its + dep-invalid-sv + 1.0-SNAPSHOT + + + 20120809.112920 + 1 + + 20120809112920 + + + jar + 1.0:2.0 + 20120809112920 + + + + diff --git a/impl/maven-impl/src/test/remote-repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml b/impl/maven-impl/src/test/remote-repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml new file mode 100644 index 000000000000..ac47d2c7d2bd --- /dev/null +++ b/impl/maven-impl/src/test/remote-repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml @@ -0,0 +1,13 @@ + + + org.apache.maven.its + dep-invalid-ts + 1.0-SNAPSHOT + + + 20120809.112920:1 + 1 + + 20120809112920 + +