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/10/12 16:57:35 UTC
(maven-resolver) branch master updated: Allow symlinked local path prefix directories (#2184)
This is an automated email from the ASF dual-hosted git repository.
cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
The following commit(s) were added to refs/heads/master by this push:
new 05b2e683d Allow symlinked local path prefix directories (#2184)
05b2e683d is described below
commit 05b2e683d558c2f2e7456c03320da76f668f942e
Author: Piotr Findeisen <pi...@gmail.com>
AuthorDate: Mon Oct 12 18:57:29 2026 +0200
Allow symlinked local path prefix directories (#2184)
The real-path check compared the whole path below the local repository base directory with its symlink-resolved form, so any symbolic link below the base directory made every artifact beneath it "not present". This rejects layouts where a split prefix directory (e.g. the remote cache) is a symbolic link to a cache shared by several local repositories, forcing a re-download of every artifact on each build.
Only the coordinate-derived part of the path can be aliased by case-colliding coordinates; the base directory and the prefix directories come from configuration. Resolve the prefix directory and compare real(prefix directory) + coordinate path with real(full path). Case and normalization aliases in the coordinates are still rejected.
<!-- Add your description here... what, why and how you resolve the issue -->
- Fixes #2183
---
To make clear that you license your contribution under
the [Apache License Version 2.0, January 2004](https://www.apache.org/licenses/LICENSE-2.0)
check one of the following:
- [x] I hereby declare this contribution to be licensed under the [Apache License Version 2.0, January 2004](https://www.apache.org/licenses/LICENSE-2.0)
- [x] I hereby declare that I have filed an [Apache Individual Contributor License Agreement](https://www.apache.org/licenses/icla.pdf).
---
.../impl/EnhancedLocalRepositoryManager.java | 51 ++++++++++++----------
.../EnhancedLocalRepositoryManagerFactory.java | 27 +++++-------
.../impl/EnhancedLocalRepositoryManagerTest.java | 12 ++---
.../EnhancedSplitLocalRepositoryManagerTest.java | 50 +++++++++++++++++++++
4 files changed, 95 insertions(+), 45 deletions(-)
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
index f835c7014..bb6a70535 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
@@ -134,12 +134,6 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
*/
private final ConcurrentHashMap<Path, Properties> trackingFileCache = new ConcurrentHashMap<>();
- /**
- * Real (symlink-resolved) path of the local repository base directory, used by
- * {@link #hasFaithfulRealPath(Path)}. It cannot change during the lifetime of this manager.
- */
- private final Path realBasePath;
-
EnhancedLocalRepositoryManager(
Path basedir,
LocalPathComposer localPathComposer,
@@ -147,17 +141,13 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
String trackingFilename,
boolean legacyLocalRepository,
TrackingFileManager trackingFileManager,
- LocalPathPrefixComposer localPathPrefixComposer)
- throws IOException {
+ LocalPathPrefixComposer localPathPrefixComposer) {
super(basedir, "enhanced", localPathComposer);
this.trackingRepositoryKeyFunction = requireNonNull(trackingRepositoryKeyFunction);
this.trackingFilename = requireNonNull(trackingFilename);
this.legacyLocalRepository = legacyLocalRepository;
this.trackingFileManager = requireNonNull(trackingFileManager);
this.localPathPrefixComposer = requireNonNull(localPathPrefixComposer);
- // a fresh local repository does not exist yet; toRealPath() requires it to
- Files.createDirectories(getRepository().getBasePath());
- this.realBasePath = getRepository().getBasePath().toRealPath();
}
private String concatPaths(String prefix, String artifactPath) {
@@ -218,14 +208,23 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
// Local repository CANNOT have timestamped installed, they are created only during deploy
if (Objects.equals(artifact.getVersion(), artifact.getBaseVersion())) {
filePath = getAbsolutePathForLocalArtifact(artifact);
- checkFind(filePath, result, verifyRealPath);
+ checkFind(
+ getPrefixDirectory(localPathPrefixComposer.getPathPrefixForLocalArtifact(artifact)),
+ filePath,
+ result,
+ verifyRealPath);
}
if (!result.isAvailable()) {
for (RemoteRepository repository : request.getRepositories()) {
filePath = getAbsolutePathForRemoteArtifact(artifact, repository, request.getContext());
- checkFind(filePath, result, verifyRealPath);
+ checkFind(
+ getPrefixDirectory(
+ localPathPrefixComposer.getPathPrefixForRemoteArtifact(artifact, repository)),
+ filePath,
+ result,
+ verifyRealPath);
if (result.isAvailable()) {
break;
@@ -236,23 +235,29 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
return result;
}
+ private Path getPrefixDirectory(String prefix) {
+ Path basePath = getRepository().getBasePath();
+ return prefix == null || prefix.isEmpty() ? basePath : basePath.resolve(prefix);
+ }
+
/**
* Verifies that the real (on-disk) spelling of the given artifact path matches the requested spelling,
- * relative to the local repository base directory. On case-insensitive or normalization-preserving
+ * relative to the directory of its local path prefix. On case-insensitive or normalization-preserving
* filesystems (the macOS and Windows defaults) a cached file whose stored name differs from the requested one
* - for example one cached for case-colliding coordinates - still passes the file-existence check, while the
* tracking data in the tracking file is compared exactly: the aliased file would then be treated as
* present-but-untracked and accepted with no download and no checksum verification. Such aliases are treated
- * as "not present" instead (fail closed), forcing a proper download for the requested coordinates. The
- * comparison is relative to the (symlink-resolved) base directory, so a symlinked base directory is
- * supported; symbolic links below the base directory are not - see
+ * as "not present" instead (fail closed), forcing a proper download for the requested coordinates. Only the
+ * coordinate-derived part of the path is compared, as a suffix of the real path: the base directory and the
+ * prefix directories are configuration, not coordinates, so they may be symbolic links - see
* {@link EnhancedLocalRepositoryManagerFactory#CONFIG_PROP_VERIFY_REAL_PATH} to opt out.
*/
- private boolean hasFaithfulRealPath(Path path) {
+ private boolean hasFaithfulRealPath(Path prefixDirectory, Path path) {
try {
- String requested = getRepository().getBasePath().relativize(path).toString();
- String real = realBasePath.relativize(path.toRealPath()).toString();
- if (!requested.equals(real)) {
+ String requested = prefixDirectory.relativize(path).toString();
+ String real = path.toRealPath().toString();
+ // compare strings: Path comparison ignores case on Windows
+ if (!real.endsWith(path.getFileSystem().getSeparator() + requested)) {
LOGGER.warn(
"Rejecting locally cached artifact {}: its on-disk path {} does not match the requested"
+ " coordinates (filesystem case/normalization alias); treating it as not present",
@@ -267,8 +272,8 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
}
}
- private void checkFind(Path path, LocalArtifactResult result, boolean verifyRealPath) {
- if (Files.isRegularFile(path) && (!verifyRealPath || hasFaithfulRealPath(path))) {
+ private void checkFind(Path prefixDirectory, Path path, LocalArtifactResult result, boolean verifyRealPath) {
+ if (Files.isRegularFile(path) && (!verifyRealPath || hasFaithfulRealPath(prefixDirectory, path))) {
result.setPath(path);
Properties props = readRepos(path);
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
index 19d0b931e..26383d20d 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
@@ -22,8 +22,6 @@ import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
-import java.io.IOException;
-
import org.eclipse.aether.ConfigurationProperties;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.LocalRepository;
@@ -68,8 +66,9 @@ public class EnhancedLocalRepositoryManagerFactory implements LocalRepositoryMan
* such an aliased file is treated as present-but-untracked and accepted with no download and no checksum
* verification, letting case-colliding coordinates poison distinct GAVs. When enabled (the default), an
* artifact whose on-disk path spelling differs from the requested one is treated as not present, forcing a
- * proper download. Disable only if the local repository intentionally contains symbolic links below its base
- * directory (a symlinked base directory itself is supported either way).
+ * proper download. Only the coordinate-derived part of the path is verified, so the base directory and the
+ * local path prefix directories (see {@link DefaultLocalPathPrefixComposerFactory}) may be symbolic links.
+ * Disable only if the local repository intentionally contains symbolic links below those directories.
*
* @configurationSource {@link RepositorySystemSession#getConfigProperties()}
* @configurationType {@link java.lang.Boolean}
@@ -144,18 +143,14 @@ public class EnhancedLocalRepositoryManagerFactory implements LocalRepositoryMan
ConfigUtils.getBoolean(session, DEFAULT_LEGACY_LOCAL_REPOSITORY, CONFIG_PROP_LEGACY_LOCAL_REPOSITORY);
if ("".equals(repository.getContentType()) || "default".equals(repository.getContentType())) {
- try {
- return new EnhancedLocalRepositoryManager(
- repository.getBasePath(),
- localPathComposer,
- repositoryKeyFunctionFactory.trackingRepositoryKeyFunction(session),
- trackingFilename,
- legacyLocalRepository,
- trackingFileManager,
- localPathPrefixComposerFactory.createComposer(session));
- } catch (IOException e) {
- throw new NoLocalRepositoryManagerException(repository, e);
- }
+ return new EnhancedLocalRepositoryManager(
+ repository.getBasePath(),
+ localPathComposer,
+ repositoryKeyFunctionFactory.trackingRepositoryKeyFunction(session),
+ trackingFilename,
+ legacyLocalRepository,
+ trackingFileManager,
+ localPathPrefixComposerFactory.createComposer(session));
} else {
throw new NoLocalRepositoryManagerException(repository);
}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
index 4a21ab11a..4e71f6ebe 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
@@ -52,7 +52,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class EnhancedLocalRepositoryManagerTest {
- private Artifact artifact;
+ protected Artifact artifact;
private Artifact snapshot;
@@ -63,9 +63,9 @@ public class EnhancedLocalRepositoryManagerTest {
private File artifactFile;
- private RemoteRepository repository;
+ protected RemoteRepository repository;
- private final String testContext = "project/compile";
+ protected final String testContext = "project/compile";
protected TrackingFileManager trackingFileManager;
@@ -130,14 +130,14 @@ public class EnhancedLocalRepositoryManagerTest {
artifact = null;
}
- private long addLocalArtifact(Artifact artifact) throws IOException {
+ protected long addLocalArtifact(Artifact artifact) throws IOException {
manager.add(session, new LocalArtifactRegistration(artifact));
String path = manager.getPathForLocalArtifact(artifact);
return copy(artifact, path);
}
- private long addRemoteArtifact(Artifact artifact) throws IOException {
+ protected long addRemoteArtifact(Artifact artifact) throws IOException {
Collection<String> contexts = Arrays.asList(testContext);
manager.add(session, new LocalArtifactRegistration(artifact, repository, contexts));
String path = manager.getPathForRemoteArtifact(artifact, repository, testContext);
@@ -255,7 +255,7 @@ public class EnhancedLocalRepositoryManagerTest {
* Simulates a case-aliasing filesystem (the macOS and Windows defaults) on any filesystem by creating a
* symbolic link whose name differs from the on-disk file only by case, and returns the aliased artifact.
*/
- private Artifact createCaseAliasedArtifact() throws Exception {
+ protected Artifact createCaseAliasedArtifact() throws Exception {
addLocalArtifact(artifact);
Artifact aliased = new DefaultArtifact("gid", "aid", "", "JAR", "1-test");
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
index 8d2206185..9b7fadd71 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
@@ -19,14 +19,21 @@
package org.eclipse.aether.internal.impl;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.util.repository.RepositoryIdHelper;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class EnhancedSplitLocalRepositoryManagerTest extends EnhancedLocalRepositoryManagerTest {
@@ -75,4 +82,47 @@ public class EnhancedSplitLocalRepositoryManagerTest extends EnhancedLocalReposi
"cached/g/i/d/a.i.d/1.0-SNAPSHOT/a.i.d-1.0-20110329.221805-4.jar",
manager.getPathForRemoteArtifact(artifact, remoteRepo, ""));
}
+
+ /**
+ * Replaces the given prefix directory of the local repository with a symbolic link to a directory elsewhere,
+ * like a local repository sharing its download cache with other local repositories.
+ */
+ private void symlinkPrefixDirectory(String prefix, Path target) {
+ try {
+ Files.createSymbolicLink(basedir.toPath().resolve(prefix), target);
+ } catch (IOException | UnsupportedOperationException e) {
+ assumeTrue(false, "filesystem does not support symbolic links");
+ }
+ }
+
+ @Test
+ void testFindLocalArtifactBelowSymlinkedPrefixDirectory(@TempDir Path target) throws Exception {
+ symlinkPrefixDirectory("installed", target);
+ addLocalArtifact(artifact);
+
+ LocalArtifactRequest request = new LocalArtifactRequest(artifact, null, null);
+ LocalArtifactResult result = manager.find(session, request);
+ assertTrue(result.isAvailable());
+ }
+
+ @Test
+ void testFindRemoteArtifactBelowSymlinkedPrefixDirectory(@TempDir Path target) throws Exception {
+ symlinkPrefixDirectory("cached", target);
+ addRemoteArtifact(artifact);
+
+ LocalArtifactRequest request =
+ new LocalArtifactRequest(artifact, Collections.singletonList(repository), testContext);
+ LocalArtifactResult result = manager.find(session, request);
+ assertTrue(result.isAvailable());
+ }
+
+ @Test
+ void testFindDoesNotAcceptCaseAliasBelowSymlinkedPrefixDirectory(@TempDir Path target) throws Exception {
+ symlinkPrefixDirectory("installed", target);
+ Artifact aliased = createCaseAliasedArtifact();
+
+ LocalArtifactRequest request = new LocalArtifactRequest(aliased, null, null);
+ LocalArtifactResult result = manager.find(session, request);
+ assertFalse(result.isAvailable());
+ }
}