You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2026/08/31 13:54:11 UTC

(maven-resolver) branch maven-resolver-1.9.x updated: Compose path segments from repository ids on the 1.9.x line (#2101)

This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch maven-resolver-1.9.x
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git


The following commit(s) were added to refs/heads/maven-resolver-1.9.x by this push:
     new 7df893e1b Compose path segments from repository ids on the 1.9.x line (#2101)
7df893e1b is described below

commit 7df893e1b7a2a88d0f0d9c4c367e41fb2bd0987d
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Mon Aug 31 15:54:05 2026 +0200

    Compose path segments from repository ids on the 1.9.x line (#2101)
    
    The 1.9.x line predates the repository key function layer, so the split local repository prefix (`LocalPathPrefixComposerFactorySupport`, four sites) and the trusted checksums sources (`SummaryFileTrustedChecksumsSource`, `SparseDirectoryTrustedChecksumsSource`) splice the raw repository id into paths. This routes those ids through `PathUtils.stringToPathSegment` and validates the result, matching the 2.x behaviour after #2089 and #2099. Also ports `GAECVNameMapperTest` from master; ` [...]
    
    Repository ids that are exactly `.`/`..` or contain path separators now compose to a neutralized segment (e.g. `-DOTDOT-`) instead of being spliced verbatim; well-formed ids are unaffected.
---
 .../LocalPathPrefixComposerFactorySupport.java     |  19 ++-
 .../SparseDirectoryTrustedChecksumsSource.java     |   7 +-
 .../SummaryFileTrustedChecksumsSource.java         |   7 +-
 .../DefaultLocalPathPrefixComposerFactoryTest.java |  24 ++++
 .../SparseDirectoryTrustedChecksumsSourceTest.java |  45 ++++++
 .../SummaryFileTrustedChecksumsSourceTest.java     |  46 +++++++
 .../synccontext/named/GAECVNameMapperTest.java     | 153 +++++++++++++++++++++
 7 files changed, 295 insertions(+), 6 deletions(-)

diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java
index cf1587bda..89f3c4732 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java
@@ -23,6 +23,7 @@ import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.metadata.Metadata;
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.PathUtils;
 
 /**
  * Support class for {@link LocalPathPrefixComposerFactory} implementations: it predefines and makes re-usable
@@ -175,13 +176,13 @@ public abstract class LocalPathPrefixComposerFactorySupport implements LocalPath
             }
             String result = remotePrefix;
             if (!splitRemoteRepositoryLast && splitRemoteRepository) {
-                result += "/" + repository.getId();
+                result += "/" + repositoryIdSegment(repository);
             }
             if (splitRemote) {
                 result += "/" + (artifact.isSnapshot() ? snapshotsPrefix : releasesPrefix);
             }
             if (splitRemoteRepositoryLast && splitRemoteRepository) {
-                result += "/" + repository.getId();
+                result += "/" + repositoryIdSegment(repository);
             }
             return result;
         }
@@ -205,13 +206,13 @@ public abstract class LocalPathPrefixComposerFactorySupport implements LocalPath
             }
             String result = remotePrefix;
             if (!splitRemoteRepositoryLast && splitRemoteRepository) {
-                result += "/" + repository.getId();
+                result += "/" + repositoryIdSegment(repository);
             }
             if (splitRemote) {
                 result += "/" + (isSnapshot(metadata) ? snapshotsPrefix : releasesPrefix);
             }
             if (splitRemoteRepositoryLast && splitRemoteRepository) {
-                result += "/" + repository.getId();
+                result += "/" + repositoryIdSegment(repository);
             }
             return result;
         }
@@ -219,5 +220,15 @@ public abstract class LocalPathPrefixComposerFactorySupport implements LocalPath
         protected boolean isSnapshot(Metadata metadata) {
             return !metadata.getVersion().isEmpty() && metadata.getVersion().endsWith("-SNAPSHOT");
         }
+
+        /**
+         * Defense in depth: the repository id is spliced into the local repository path prefix, so it
+         * must be a safe path segment.
+         */
+        private String repositoryIdSegment(RemoteRepository repository) {
+            String id = PathUtils.stringToPathSegment(repository.getId());
+            PathUtils.validatePathComponent(id, "repository id");
+            return id;
+        }
     }
 }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
index 039d2d2f2..ec786587a 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
@@ -36,6 +36,7 @@ import org.eclipse.aether.internal.impl.LocalPathComposer;
 import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
 import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.util.PathUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -128,7 +129,11 @@ public final class SparseDirectoryTrustedChecksumsSource extends FileTrustedChec
         String path = localPathComposer.getPathForArtifact(artifact, false) + "."
                 + checksumAlgorithmFactory.getFileExtension();
         if (originAware) {
-            path = artifactRepository.getId() + "/" + path;
+            String safeRepositoryId = PathUtils.stringToPathSegment(artifactRepository.getId());
+            // defense in depth: the repository id is spliced into a path under the checksums basedir,
+            // so it must be a safe path segment
+            PathUtils.validatePathComponent(safeRepositoryId, "repository id");
+            path = safeRepositoryId + "/" + path;
         }
         return path;
     }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
index 870fcec6c..be6482018 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSource.java
@@ -44,6 +44,7 @@ import org.eclipse.aether.internal.impl.LocalPathComposer;
 import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
 import org.eclipse.aether.util.FileUtils;
+import org.eclipse.aether.util.PathUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -154,7 +155,11 @@ public final class SummaryFileTrustedChecksumsSource extends FileTrustedChecksum
     private Path summaryFile(Path basedir, boolean originAware, String repositoryId, String checksumExtension) {
         String fileName = CHECKSUMS_FILE_PREFIX;
         if (originAware) {
-            fileName += "-" + repositoryId;
+            String safeRepositoryId = PathUtils.stringToPathSegment(repositoryId);
+            // defense in depth: the repository id is spliced into the summary file name,
+            // so it must be a safe path segment
+            PathUtils.validatePathComponent(safeRepositoryId, "repository id");
+            fileName += "-" + safeRepositoryId;
         }
         return basedir.resolve(fileName + "." + checksumExtension);
     }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java
index cee4892bc..149ad636a 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java
@@ -225,4 +225,28 @@ public class DefaultLocalPathPrefixComposerFactoryTest {
         assertNotNull(prefix);
         assertEquals("cached/my-repo/releases", prefix);
     }
+
+    @Test
+    public void dotDotRepositoryIdIsNeutralizedInSplitPrefix() {
+        DefaultRepositorySystemSession session = TestUtils.newSession();
+        session.setConfigProperty("aether.enhancedLocalRepository.split", Boolean.TRUE.toString());
+        session.setConfigProperty("aether.enhancedLocalRepository.splitRemoteRepository", Boolean.TRUE.toString());
+
+        LocalPathPrefixComposerFactory factory = new DefaultLocalPathPrefixComposerFactory();
+        LocalPathPrefixComposer composer = factory.createComposer(session);
+        assertNotNull(composer);
+
+        // a repository id of ".." must be neutralized into a harmless path segment rather than spliced
+        // into the prefix as-is
+        RemoteRepository dotDotRepository =
+                new RemoteRepository.Builder("..", "default", "https://repo.example.org/").build();
+
+        String prefix = composer.getPathPrefixForRemoteArtifact(releaseArtifact, dotDotRepository);
+        assertNotNull(prefix);
+        assertEquals("cached/-DOTDOT-", prefix);
+
+        prefix = composer.getPathPrefixForRemoteMetadata(releaseMetadata, dotDotRepository);
+        assertNotNull(prefix);
+        assertEquals("cached/-DOTDOT-", prefix);
+    }
 }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
index 0dd0a0705..b6b017ec1 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
@@ -18,10 +18,25 @@
  */
 package org.eclipse.aether.internal.impl.checksum;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
+
 import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.impl.RepositorySystemLifecycle;
 import org.eclipse.aether.internal.impl.DefaultFileProcessor;
 import org.eclipse.aether.internal.impl.DefaultLocalPathComposer;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
+import org.junit.Test;
+
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 public class SparseDirectoryTrustedChecksumsSourceTest extends FileTrustedChecksumsSourceTestSupport {
     @Override
@@ -33,4 +48,34 @@ public class SparseDirectoryTrustedChecksumsSourceTest extends FileTrustedChecks
     protected void enableSource(DefaultRepositorySystemSession session) {
         session.setConfigProperty("aether.trustedChecksumsSource.sparseDirectory", Boolean.TRUE.toString());
     }
+
+    @Test
+    public void dotDotRepositoryIdIsNeutralized() throws Exception {
+        DefaultRepositorySystemSession session = TestUtils.newSession();
+        enableSource(session);
+
+        SparseDirectoryTrustedChecksumsSource source =
+                new SparseDirectoryTrustedChecksumsSource(new DefaultFileProcessor(), new DefaultLocalPathComposer());
+
+        TrustedChecksumsSource.Writer writer = source.getTrustedArtifactChecksumsWriter(session);
+        assertNotNull(writer);
+
+        // a repository id of ".." must be neutralized into a harmless path segment rather than spliced
+        // into the checksums basedir path as-is
+        RemoteRepository dotDotRepository =
+                new RemoteRepository.Builder("..", "default", "https://repo.example.org/").build();
+        Artifact artifact = new DefaultArtifact("test:test:1.0");
+        Sha1ChecksumAlgorithmFactory checksumAlgorithmFactory = new Sha1ChecksumAlgorithmFactory();
+
+        writer.addTrustedArtifactChecksums(
+                artifact,
+                dotDotRepository,
+                singletonList(checksumAlgorithmFactory),
+                singletonMap(checksumAlgorithmFactory.getName(), "000"));
+
+        Path checksumsBasedir =
+                session.getLocalRepository().getBasedir().toPath().resolve(".checksums");
+        assertTrue(Files.isRegularFile(checksumsBasedir.resolve("-DOTDOT-/test/test/1.0/test-1.0.jar.sha1")));
+        assertFalse(Files.exists(checksumsBasedir.getParent().resolve("test/test/1.0/test-1.0.jar.sha1")));
+    }
 }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
index 403f1cb9c..2727aea55 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SummaryFileTrustedChecksumsSourceTest.java
@@ -18,9 +18,24 @@
  */
 package org.eclipse.aether.internal.impl.checksum;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
+
 import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.impl.RepositorySystemLifecycle;
 import org.eclipse.aether.internal.impl.DefaultLocalPathComposer;
+import org.eclipse.aether.internal.impl.DefaultRepositorySystemLifecycle;
+import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.spi.checksums.TrustedChecksumsSource;
+import org.junit.Test;
+
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 public class SummaryFileTrustedChecksumsSourceTest extends FileTrustedChecksumsSourceTestSupport {
     @Override
@@ -32,4 +47,35 @@ public class SummaryFileTrustedChecksumsSourceTest extends FileTrustedChecksumsS
     protected void enableSource(DefaultRepositorySystemSession session) {
         session.setConfigProperty("aether.trustedChecksumsSource.summaryFile", Boolean.TRUE.toString());
     }
+
+    @Test
+    public void dotDotRepositoryIdIsNeutralized() throws Exception {
+        DefaultRepositorySystemSession session = TestUtils.newSession();
+        enableSource(session);
+
+        DefaultRepositorySystemLifecycle lifecycle = new DefaultRepositorySystemLifecycle();
+        SummaryFileTrustedChecksumsSource source =
+                new SummaryFileTrustedChecksumsSource(new DefaultLocalPathComposer(), lifecycle);
+
+        TrustedChecksumsSource.Writer writer = source.getTrustedArtifactChecksumsWriter(session);
+        assertNotNull(writer);
+
+        // a repository id of ".." must be neutralized into a harmless path segment rather than spliced
+        // into the summary file name as-is
+        RemoteRepository dotDotRepository =
+                new RemoteRepository.Builder("..", "default", "https://repo.example.org/").build();
+        Artifact artifact = new DefaultArtifact("test:test:1.0");
+        Sha1ChecksumAlgorithmFactory checksumAlgorithmFactory = new Sha1ChecksumAlgorithmFactory();
+
+        writer.addTrustedArtifactChecksums(
+                artifact,
+                dotDotRepository,
+                singletonList(checksumAlgorithmFactory),
+                singletonMap(checksumAlgorithmFactory.getName(), "000"));
+        lifecycle.systemEnded();
+
+        Path checksumsBasedir =
+                session.getLocalRepository().getBasedir().toPath().resolve(".checksums");
+        assertTrue(Files.isRegularFile(checksumsBasedir.resolve("checksums--DOTDOT-.sha1")));
+    }
 }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/GAECVNameMapperTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/GAECVNameMapperTest.java
new file mode 100644
index 000000000..cb42e41e9
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/GAECVNameMapperTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.eclipse.aether.internal.impl.synccontext.named;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.metadata.DefaultMetadata;
+import org.eclipse.aether.metadata.Metadata;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+
+public class GAECVNameMapperTest extends NameMapperTestSupport {
+    NameMapper mapper = NameMappers.gaecvNameMapper(true);
+
+    @Test
+    public void nullsAndEmptyInputs() {
+        Collection<String> names;
+
+        names = mapper.nameLocks(session, null, null);
+        assertThat(names, Matchers.empty());
+
+        names = mapper.nameLocks(session, null, emptyList());
+        assertThat(names, Matchers.empty());
+
+        names = mapper.nameLocks(session, emptyList(), null);
+        assertThat(names, Matchers.empty());
+
+        names = mapper.nameLocks(session, emptyList(), emptyList());
+        assertThat(names, Matchers.empty());
+    }
+
+    @Test
+    public void singleArtifactWithoutClassifier() {
+        DefaultArtifact artifact = new DefaultArtifact("group:artifact:1.0");
+        Collection<String> names = mapper.nameLocks(session, singletonList(artifact), null);
+
+        assertThat(names, hasSize(1));
+        assertThat(names.iterator().next(), equalTo("artifact~group~artifact~jar~1.0.lock"));
+    }
+
+    @Test
+    public void singleArtifactWithClassifier() {
+        DefaultArtifact artifact = new DefaultArtifact("group", "artifact", "classifier", "jar", "1.0");
+        Collection<String> names = mapper.nameLocks(session, singletonList(artifact), null);
+
+        assertThat(names, hasSize(1));
+        assertThat(names.iterator().next(), equalTo("artifact~group~artifact~jar~classifier~1.0.lock"));
+    }
+
+    @Test
+    public void singleMetadata() {
+        DefaultMetadata metadata =
+                new DefaultMetadata("group", "artifact", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT);
+        Collection<String> names = mapper.nameLocks(session, null, singletonList(metadata));
+
+        assertThat(names, hasSize(1));
+        assertThat(names.iterator().next(), equalTo("metadata~group~artifact.lock"));
+    }
+
+    @Test
+    public void pathSeparatorsInArtifactCoordinatesAreNeutralized() {
+        // wire-supplied coordinate fields must not be able to select a lock path outside the locks directory
+        DefaultArtifact artifact = new DefaultArtifact("group", "artifact", "jar", "1/../../../../home/user/x");
+        Collection<String> names = mapper.nameLocks(session, singletonList(artifact), null);
+
+        assertThat(names, hasSize(1));
+        String name = names.iterator().next();
+        assertThat(
+                name,
+                equalTo(
+                        "artifact~group~artifact~jar~1-SLASH-..-SLASH-..-SLASH-..-SLASH-..-SLASH-home-SLASH-user-SLASH-x.lock"));
+        assertThat(name, Matchers.not(Matchers.containsString("/")));
+        assertThat(name, Matchers.not(Matchers.containsString("\\")));
+    }
+
+    @Test
+    public void pathSeparatorsInExtensionAreNeutralized() {
+        // extension is one of the coordinate fields GAECV adds on top of GAV, must be neutralized the same way
+        DefaultArtifact artifact = new DefaultArtifact("group", "artifact", "", "j/a\\r", "1.0");
+        Collection<String> names = mapper.nameLocks(session, singletonList(artifact), null);
+
+        assertThat(names, hasSize(1));
+        String name = names.iterator().next();
+        assertThat(name, equalTo("artifact~group~artifact~j-SLASH-a-BACKSLASH-r~1.0.lock"));
+        assertThat(name, Matchers.not(Matchers.containsString("/")));
+        assertThat(name, Matchers.not(Matchers.containsString("\\")));
+    }
+
+    @Test
+    public void pathSeparatorsInClassifierAreNeutralized() {
+        // classifier is the other coordinate field GAECV adds on top of GAV, must be neutralized the same way
+        DefaultArtifact artifact = new DefaultArtifact("group", "artifact", "../../etc/passwd", "jar", "1.0");
+        Collection<String> names = mapper.nameLocks(session, singletonList(artifact), null);
+
+        assertThat(names, hasSize(1));
+        String name = names.iterator().next();
+        assertThat(name, equalTo("artifact~group~artifact~jar~..-SLASH-..-SLASH-etc-SLASH-passwd~1.0.lock"));
+        assertThat(name, Matchers.not(Matchers.containsString("/")));
+        assertThat(name, Matchers.not(Matchers.containsString("\\")));
+    }
+
+    @Test
+    public void pathSeparatorsInMetadataCoordinatesAreNeutralized() {
+        DefaultMetadata metadata = new DefaultMetadata(
+                "group", "artifact", "1\\..\\..\\x", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT);
+        Collection<String> names = mapper.nameLocks(session, null, singletonList(metadata));
+
+        assertThat(names, hasSize(1));
+        String name = names.iterator().next();
+        assertThat(name, equalTo("metadata~group~artifact~1-BACKSLASH-..-BACKSLASH-..-BACKSLASH-x.lock"));
+        assertThat(name, Matchers.not(Matchers.containsString("/")));
+        assertThat(name, Matchers.not(Matchers.containsString("\\")));
+    }
+
+    @Test
+    public void oneAndOne() {
+        DefaultArtifact artifact = new DefaultArtifact("agroup:artifact:1.0");
+        DefaultMetadata metadata =
+                new DefaultMetadata("bgroup", "artifact", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT);
+        Collection<String> names = mapper.nameLocks(session, singletonList(artifact), singletonList(metadata));
+
+        assertThat(names, hasSize(2));
+        Iterator<String> namesIterator = names.iterator();
+
+        // they are sorted as well
+        assertThat(namesIterator.next(), equalTo("artifact~agroup~artifact~jar~1.0.lock"));
+        assertThat(namesIterator.next(), equalTo("metadata~bgroup~artifact.lock"));
+    }
+}