You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/09/26 17:09:36 UTC
(maven-filtering) branch master updated: fix: handle symlinks in source and destination for copyFile (MSHARED-1004)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-filtering.git
The following commit(s) were added to refs/heads/master by this push:
new b33d17a fix: handle symlinks in source and destination for copyFile (MSHARED-1004)
b33d17a is described below
commit b33d17a853bec1241561498a84a20f2896f12d3a
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Sat Sep 26 19:09:30 2026 +0200
fix: handle symlinks in source and destination for copyFile (MSHARED-1004)
- Detect symlink at destination in FilteringUtils.copyFile and delete it before opening for write
- Ensures output is always a regular file, preventing FileAlreadyExistsException and NoSuchFileException on incremental builds
- Files.isSymbolicLink() is safe for non-existent paths (returns false), so this is safe for first-write case too
- Fixes #248 (MSHARED-1004)
---
.../maven/shared/filtering/FilteringUtils.java | 7 ++
.../filtering/DefaultMavenFileFilterTest.java | 55 ++++++++++++++
.../maven/shared/filtering/FilteringUtilsTest.java | 83 ++++++++++++++++++++++
3 files changed, 145 insertions(+)
diff --git a/src/main/java/org/apache/maven/shared/filtering/FilteringUtils.java b/src/main/java/org/apache/maven/shared/filtering/FilteringUtils.java
index 732eae0..d8fcc8d 100644
--- a/src/main/java/org/apache/maven/shared/filtering/FilteringUtils.java
+++ b/src/main/java/org/apache/maven/shared/filtering/FilteringUtils.java
@@ -298,6 +298,13 @@ public final class FilteringUtils {
*/
public static void copyFile(Path from, Path to, String encoding, FilterWrapper[] wrappers, boolean overwrite)
throws IOException {
+ // If the destination is a symbolic link (dangling or pointing to a regular file left by an
+ // earlier build that used NOFOLLOW_LINKS), delete it before writing so that the output is
+ // always a regular file and CachingOutputStream / CachingWriter do not fail with
+ // NoSuchFileException when trying to follow a dangling link.
+ if (Files.isSymbolicLink(to)) {
+ Files.delete(to);
+ }
if (wrappers == null || wrappers.length == 0) {
try (OutputStream os = new CachingOutputStream(to)) {
Files.copy(from, os);
diff --git a/src/test/java/org/apache/maven/shared/filtering/DefaultMavenFileFilterTest.java b/src/test/java/org/apache/maven/shared/filtering/DefaultMavenFileFilterTest.java
index 0be2ce7..77aae53 100644
--- a/src/test/java/org/apache/maven/shared/filtering/DefaultMavenFileFilterTest.java
+++ b/src/test/java/org/apache/maven/shared/filtering/DefaultMavenFileFilterTest.java
@@ -40,12 +40,15 @@ import org.apache.maven.di.Injector;
import org.codehaus.plexus.interpolation.AbstractValueSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
import org.sonatype.plexus.build.incremental.BuildContext;
import static org.apache.maven.api.di.testing.MavenDIExtension.getBasedir;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
@@ -244,4 +247,56 @@ class DefaultMavenFileFilterTest {
assertEquals("toto@titi.com bar", IOUtils.toString(reader));
}
}
+
+ /**
+ * MSHARED-1004: a file that is a symbolic link in the source directory must be followed.
+ * Its content must be written as a regular file at the destination, not as a symlink.
+ * On a second invocation the regular file must be silently overwritten (no FileAlreadyExistsException).
+ */
+ @Test
+ @DisabledOnOs(OS.WINDOWS)
+ void copyFileSymlinkSourceIsFollowed() throws Exception {
+ Path realFile = tempDir.resolve("real.properties");
+ Files.writeString(realFile, "key=value");
+
+ // symlink that lives in the same directory as the real file (relative target)
+ Path symlink = tempDir.resolve("link.properties");
+ Files.createSymbolicLink(symlink, realFile.getFileName());
+
+ Path toFile = tempDir.resolve("output.properties");
+
+ MavenFileFilter mavenFileFilter = container.getInstance(MavenFileFilter.class);
+
+ // First copy
+ mavenFileFilter.copyFile(symlink, toFile, false, null, null);
+ assertFalse(Files.isSymbolicLink(toFile), "output must be a regular file, not a symlink");
+ assertEquals("key=value", Files.readString(toFile));
+
+ // Second copy must not throw FileAlreadyExistsException or NoSuchFileException
+ mavenFileFilter.copyFile(symlink, toFile, false, null, null);
+ assertFalse(Files.isSymbolicLink(toFile));
+ assertEquals("key=value", Files.readString(toFile));
+ }
+
+ /**
+ * MSHARED-1004: when the destination already is a dangling symbolic link (left by an older
+ * version of maven-filtering that used NOFOLLOW_LINKS), copyFile must replace it with a regular
+ * file without throwing.
+ */
+ @Test
+ @DisabledOnOs(OS.WINDOWS)
+ void copyFileReplacesDanglingSymlinkAtDestination() throws Exception {
+ Path realFile = tempDir.resolve("real.txt");
+ Files.writeString(realFile, "content");
+
+ Path toFile = tempDir.resolve("output.txt");
+ // Simulate a dangling symlink at the destination (as written by old NOFOLLOW_LINKS code)
+ Files.createSymbolicLink(toFile, Path.of("../nonexistent/target.txt"));
+
+ MavenFileFilter mavenFileFilter = container.getInstance(MavenFileFilter.class);
+ mavenFileFilter.copyFile(realFile, toFile, false, null, null);
+
+ assertFalse(Files.isSymbolicLink(toFile), "dangling symlink must be replaced by a regular file");
+ assertEquals("content", Files.readString(toFile));
+ }
}
diff --git a/src/test/java/org/apache/maven/shared/filtering/FilteringUtilsTest.java b/src/test/java/org/apache/maven/shared/filtering/FilteringUtilsTest.java
index a83a68d..cf2b1fd 100644
--- a/src/test/java/org/apache/maven/shared/filtering/FilteringUtilsTest.java
+++ b/src/test/java/org/apache/maven/shared/filtering/FilteringUtilsTest.java
@@ -26,9 +26,13 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
import static org.apache.maven.api.di.testing.MavenDIExtension.getBasedir;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
/**
* @author John Casey
@@ -39,6 +43,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
class FilteringUtilsTest {
private static final Path TEST_DIRECTORY = Paths.get(getBasedir(), "target/test-classes/");
+ @TempDir
+ Path tempDir;
+
@Test
void mshared1213CopyWithTargetAlreadyExisting0ByteFile() throws Exception {
Path fromFile = Paths.get(getBasedir() + "/src/test/units-files/MSHARED-1213/enunciate.xml");
@@ -164,4 +171,80 @@ class FilteringUtilsTest {
assertEquals("../../bin", FilteringUtils.getRelativeFilePath("/usr/local/", "/bin"));
assertEquals("../usr/local/", FilteringUtils.getRelativeFilePath("/bin", "/usr/local/"));
}
+
+ // MSHARED-1004: symbolic links in resources
+
+ /**
+ * A file-symlink in the source directory must be followed: the target file's content is
+ * copied to the destination as a regular file (not as a symlink).
+ */
+ @Test
+ @DisabledOnOs(OS.WINDOWS)
+ void copyFileSymlinkIsFollowedAndWrittenAsRegularFile() throws Exception {
+ // real file
+ Path realFile = tempDir.resolve("real.txt");
+ Files.writeString(realFile, "symlink content");
+
+ // symlink → real file
+ Path symlink = tempDir.resolve("link.txt");
+ Files.createSymbolicLink(symlink, realFile.getFileName()); // relative: ../real.txt
+
+ Path toFile = tempDir.resolve("output.txt");
+ FilteringUtils.copyFile(symlink, toFile, "UTF-8", new FilterWrapper[0], false);
+
+ assertFalse(Files.isSymbolicLink(toFile), "destination must be a regular file, not a symlink");
+ assertEquals("symlink content", Files.readString(toFile));
+ }
+
+ /**
+ * When the destination is a dangling symlink left by a previous build (e.g. from an older
+ * version of maven-filtering that copied symlinks verbatim), copyFile must replace it with a
+ * regular file instead of failing with NoSuchFileException.
+ */
+ @Test
+ @DisabledOnOs(OS.WINDOWS)
+ void copyFileReplacesDanglingSymlinkAtDestination() throws Exception {
+ Path realFile = tempDir.resolve("real.txt");
+ Files.writeString(realFile, "hello");
+
+ Path toFile = tempDir.resolve("output.txt");
+ // Simulate a dangling symlink left by old code
+ Files.createSymbolicLink(toFile, Path.of("../nonexistent/B"));
+
+ // Must succeed even though the symlink is dangling
+ FilteringUtils.copyFile(realFile, toFile, "UTF-8", new FilterWrapper[0], false);
+
+ assertFalse(Files.isSymbolicLink(toFile), "dangling symlink must be replaced by a regular file");
+ assertEquals("hello", Files.readString(toFile));
+ }
+
+ /**
+ * Same as above but with filtering active (wrappers path through CachingWriter).
+ */
+ @Test
+ @DisabledOnOs(OS.WINDOWS)
+ void copyFileWithFilteringReplacesDanglingSymlinkAtDestination() throws Exception {
+ Path realFile = tempDir.resolve("real.txt");
+ Files.writeString(realFile, "hello");
+
+ Path toFile = tempDir.resolve("output.txt");
+ Files.createSymbolicLink(toFile, Path.of("../nonexistent/B"));
+
+ FilteringUtils.copyFile(
+ realFile,
+ toFile,
+ "UTF-8",
+ new FilterWrapper[] {
+ new FilterWrapper() {
+ @Override
+ public Reader getReader(Reader fileReader) {
+ return fileReader;
+ }
+ }
+ },
+ false);
+
+ assertFalse(Files.isSymbolicLink(toFile), "dangling symlink must be replaced by a regular file");
+ assertEquals("hello", Files.readString(toFile));
+ }
}