From ed20f2aa57a8494782e362decb48a3561936c194 Mon Sep 17 00:00:00 2001
From: Gary Gregory
+ *
+ * source/
+ * dir/
+ * file
+ * symlink-to-file
+ * symlink-to-dir
+ *
+ */
+ @Disabled
+ @Test
+ void testCopyDirectoryPreservesSymlinks(@TempDir final Path tempDir) throws Exception {
+ final Path sourceDir = Files.createDirectory(tempDir.resolve("source"));
+ final Path dir = Files.createDirectory(sourceDir.resolve("dir"));
+ final Path dirLink = Files.createSymbolicLink(sourceDir.resolve("link-to-dir"), dir);
+ assertTrue(Files.exists(dirLink));
+ final Path file = Files.createFile(dir.resolve("file"));
+ final Path fileLink = Files.createSymbolicLink(dir.resolve("link-to-file"), file);
+ assertTrue(Files.exists(fileLink));
+ final Path targetDir = tempDir.resolve("target");
+ PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
+ final Path copyOfDir = targetDir.resolve("dir");
+ assertTrue(Files.exists(copyOfDir));
+ final Path copyOfDirLink = targetDir.resolve("link-to-dir");
+ assertTrue(Files.exists(copyOfDirLink));
+ assertTrue(Files.isSymbolicLink(copyOfDirLink));
+ final Path copyOfFileLink = copyOfDir.resolve("link-to-file");
+ assertTrue(Files.exists(copyOfFileLink));
+ assertTrue(Files.isSymbolicLink(copyOfFileLink));
+ }
+
@Test
void testCopyFile() throws IOException {
final Path sourceFile = Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1/file-size-1.bin");
@@ -150,4 +191,273 @@ void testCopyURL() throws IOException {
assertEquals(Files.size(sourceFile), Files.size(targetFile));
}
+ /**
+ * Illustrates how copy with {@link LinkOption#NOFOLLOW_LINKS} preserves relative symlinks to directories.
+ * This simulates to the behavior of Linux {@code cp -r}.
+ * Given the source directory structure:
+ * {@code
+ * user@host:/tmp$ tree source/
+ * source/
+ * ├── dir1
+ * │ └── symlink -> ../dir2
+ * └── dir2
+ * }
+ * When doing {@code user@host:/tmp$ cp -r source target}, then the resulting target directory structure is:
+ * {@code
+ * user@host:/tmp$ tree target/
+ * target/
+ * ├── dir1
+ * │ └── symlink -> ../dir2
+ * └── dir2
+ * }
+ */
+ @Test
+ void testCopyDirectoryWithNoFollowLinksPreservesRelativeSymbolicLinkToDir() throws Exception {
+ // Given
+ final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
+ final Path dir1 = Files.createDirectory(sourceDir.resolve("dir1"));
+ final Path dir2 = Files.createDirectory(sourceDir.resolve("dir2"));
+ // source/dir1/symlink -> ../dir2
+ Files.createSymbolicLink(dir1.resolve("symlink"), dir1.relativize(dir2));
+ final Path targetDir = tempDirPath.resolve("target");
+ // When
+ final PathCounters pathCounters = PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
+ // Then
+ // assertEquals(0L, pathCounters.getByteCounter().get());
+ assertEquals(3L, pathCounters.getDirectoryCounter().get());
+ // Verify that symlink with NOFOLLOW_LINKS counts as file
+ assertEquals(1L, pathCounters.getFileCounter().get());
+ final Path copyOfDir2 = targetDir.resolve("dir2");
+ final Path copyOfRelativeSymlinkToDir2 = targetDir.resolve("dir1").resolve("symlink");
+ assertTrue(Files.isSymbolicLink(copyOfRelativeSymlinkToDir2));
+ assertTrue(Files.isDirectory(copyOfRelativeSymlinkToDir2));
+ // Verify that target/dir1/symlink resolves to /tmp/target/dir2
+ assertEquals(copyOfDir2.toRealPath(), copyOfRelativeSymlinkToDir2.toRealPath());
+ }
+
+ /**
+ * Illustrates how copy with {@link LinkOption#NOFOLLOW_LINKS} preserves absolute symlinks to directories.
+ * This simulates to the behavior of Linux {@code cp -r}.
+ * Given the source directory structure:
+ * {@code
+ * user@host:/tmp$ tree source/ external/
+ * source/
+ * └── dir
+ * └── symlink -> /tmp/external
+ * external/
+ * }
+ * When doing {@code user@host:/tmp$ cp -r source target}, then the resulting target directory structure is:
+ * {@code
+ * user@host:/tmp$ tree target/
+ * target/
+ * └── dir
+ * └── symlink -> /tmp/external
+ * }
+ */
+ @Test
+ void testCopyDirectoryWithNoFollowLinksPreservesAbsoluteSymbolicLinkToDir() throws Exception {
+ // Given
+ final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
+ final Path externalDir = Files.createDirectory(tempDirPath.resolve("external"));
+ final Path dir = Files.createDirectory(sourceDir.resolve("dir"));
+ // source/dir/symlink -> /tmp/external
+ Files.createSymbolicLink(dir.resolve("symlink"), externalDir.toAbsolutePath());
+ final Path targetDir = tempDirPath.resolve("target");
+ // When
+ final PathCounters pathCounters = PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
+ // Then
+ // assertEquals(0L, pathCounters.getByteCounter().get());
+ assertEquals(2L, pathCounters.getDirectoryCounter().get());
+ // Verify that symlink with NOFOLLOW_LINKS counts as file
+ assertEquals(1L, pathCounters.getFileCounter().get());
+ final Path copyOfAbsoluteSymlinkToDir = targetDir.resolve("dir").resolve("symlink");
+ assertTrue(Files.isSymbolicLink(copyOfAbsoluteSymlinkToDir));
+ assertTrue(Files.isDirectory(copyOfAbsoluteSymlinkToDir));
+ // Verify that target/dir/symlink resolves to /tmp/external
+ assertEquals(externalDir.toRealPath(), copyOfAbsoluteSymlinkToDir.toRealPath());
+ }
+
+ /**
+ * Illustrates how copy with {@link LinkOption#NOFOLLOW_LINKS} preserves relative symlinks to files.
+ * This simulates to the behavior of Linux {@code cp -r}.
+ * Given the source directory structure:
+ * {@code
+ * user@host:/tmp$ tree source/
+ * source/
+ * ├── dir
+ * │ └── symlink -> ../file
+ * └── file
+ * }
+ * When doing {@code user@host:/tmp$ cp -r source target}, then the resulting target directory structure is:
+ * {@code
+ * user@host:/tmp$ tree target/
+ * target/
+ * ├── dir
+ * │ └── symlink -> ../file
+ * └── file
+ * }
+ */
+ @Test
+ void testCopyDirectoryWithNoFollowLinksPreservesRelativeSymbolicLinkToFile() throws Exception {
+ // Given
+ final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
+ final Path dir = Files.createDirectory(sourceDir.resolve("dir"));
+ final Path file = Files.write(sourceDir.resolve("file"), PathUtilsTest.BYTE_ARRAY_FIXTURE);
+ // source/dir/symlink -> ../file
+ Files.createSymbolicLink(dir.resolve("symlink"), dir.relativize(file));
+ final Path targetDir = tempDirPath.resolve("target");
+ // When
+ final PathCounters pathCounters = PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
+ // Then
+ // assertEquals(11L, pathCounters.getByteCounter().get());
+ assertEquals(2L, pathCounters.getDirectoryCounter().get());
+ // Verify that file + symlink with NOFOLLOW_LINKS counts as 2 files
+ assertEquals(2L, pathCounters.getFileCounter().get());
+ final Path copyOfFile = targetDir.resolve("file");
+ final Path copyOfRelativeSymlinkToFile = targetDir.resolve("dir").resolve("symlink");
+ assertTrue(Files.isSymbolicLink(copyOfRelativeSymlinkToFile));
+ assertTrue(Files.isRegularFile(copyOfRelativeSymlinkToFile));
+ // Verify that /tmp/target/dir/symlink resolves to /tmp/target/file
+ assertEquals(copyOfFile.toRealPath(), copyOfRelativeSymlinkToFile.toRealPath());
+ }
+
+ /**
+ * Illustrates how copy with {@link LinkOption#NOFOLLOW_LINKS} preserves relative symlinks to files.
+ * This simulates to the behavior of Linux {@code cp -r}.
+ * Given the source directory structure:
+ * {@code
+ * user@host:/tmp$ tree source/
+ * source/
+ * ├── dir
+ * │ └── symlink -> ../file
+ * └── file
+ * }
+ * When doing {@code user@host:/tmp$ cp -r source target}, then the resulting target directory structure is:
+ * {@code
+ * user@host:/tmp$ tree target/
+ * target/
+ * ├── dir
+ * │ └── symlink -> ../file
+ * └── file
+ * }
+ */
+ @Test
+ void testCopyDirectoryWithNoFollowLinksPreservesAbsoluteSymbolicLinkToFile() throws Exception {
+ // Given
+ final Path externalDir = Files.createDirectory(tempDirPath.resolve("external"));
+ final Path file = Files.write(externalDir.resolve("file"), PathUtilsTest.BYTE_ARRAY_FIXTURE);
+ final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
+ final Path dir = Files.createDirectory(sourceDir.resolve("dir"));
+ // source/dir/symlink -> /tmp/file
+ Files.createSymbolicLink(dir.resolve("symlink"), file.toAbsolutePath());
+ final Path targetDir = tempDirPath.resolve("target");
+ // When
+ final PathCounters pathCounters = PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
+ // Then
+ // assertEquals(0L, pathCounters.getByteCounter().get());
+ assertEquals(2L, pathCounters.getDirectoryCounter().get());
+ assertEquals(1L, pathCounters.getFileCounter().get());
+ final Path copyOfAbsoluteSymlinkToFile = targetDir.resolve("dir").resolve("symlink");
+ assertTrue(Files.isSymbolicLink(copyOfAbsoluteSymlinkToFile));
+ assertTrue(Files.isRegularFile(copyOfAbsoluteSymlinkToFile));
+ // Verify that /tmp/target/dir/symlink resolves to /tmp/source/file
+ assertEquals(file.toRealPath(), copyOfAbsoluteSymlinkToFile.toRealPath());
+ }
+
+ @Test
+ void testCopyDirectoryCyclicSymbolicLink() throws Exception {
+ // sourceDir = tempDirPath/source
+ final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
+ // dir1 = tempDirPath/source/dir1
+ final Path dir1 = Files.createDirectory(sourceDir.resolve("dir1"));
+ // dir2 = tempDirPath/source/dir1/dir2
+ final Path dir2 = Files.createDirectory(dir1.resolve("dir2"));
+ // link = tempDirPath/source/dir1/dir2/cyclic-symlink
+ // target = ..
+ Files.createSymbolicLink(dir2.resolve("cyclic-symlink"), dir2.relativize(dir1));
+ final Path targetDir = tempDirPath.resolve("target");
+ PathUtils.copyDirectory(sourceDir, targetDir);
+ assertTrue(Files.exists(targetDir));
+ final Path copyOfDir2 = targetDir.resolve("dir1").resolve("dir2");
+ assertTrue(Files.exists(copyOfDir2));
+ assertTrue(Files.isDirectory(copyOfDir2));
+ assertTrue(Files.exists(copyOfDir2.resolve("cyclic-symlink")));
+ }
+
+ @Test
+ void testCopyDirectoryWithNoFollowLinksPreservesCyclicSymbolicLink() throws Exception {
+ final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
+ final Path dir1 = Files.createDirectory(sourceDir.resolve("dir1"));
+ final Path dir2 = Files.createDirectory(dir1.resolve("dir2"));
+ Files.createSymbolicLink(dir2.resolve("cyclic-symlink"), dir2.relativize(dir1));
+ final Path targetDir = tempDirPath.resolve("target");
+ PathUtils.copyDirectory(sourceDir, targetDir, LinkOption.NOFOLLOW_LINKS);
+ assertTrue(Files.exists(targetDir));
+ final Path copyOfDir1 = targetDir.resolve("dir1");
+ final Path copyOfDir2 = copyOfDir1.resolve("dir2");
+ assertTrue(Files.exists(copyOfDir2));
+ assertTrue(Files.isDirectory(copyOfDir2));
+ final Path copyOfCyclicSymlink = copyOfDir2.resolve("cyclic-symlink");
+ assertTrue(Files.exists(copyOfCyclicSymlink));
+ assertEquals(copyOfDir1.toRealPath(), copyOfCyclicSymlink.toRealPath());
+ }
+
+ @ParameterizedTest
+ @ArgumentsSource(CopyOptionsArgumentsProvider.class)
+ void testCopyDirectoryIgnoresBrokenSymbolicLink(final CopyOption... copyOptions) throws Exception {
+ final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
+ final Path dir = Files.createDirectory(sourceDir.resolve("dir"));
+ Files.createSymbolicLink(dir.resolve("broken-symlink"), dir.relativize(sourceDir.resolve("file")));
+ final Path targetDir = tempDirPath.resolve("target");
+ PathUtils.copyDirectory(sourceDir, targetDir, copyOptions);
+ assertTrue(Files.exists(targetDir));
+ final Path copyOfDir = targetDir.resolve("dir");
+ assertTrue(Files.exists(copyOfDir));
+ assertTrue(Files.isDirectory(copyOfDir));
+ assertFalse(Files.exists(copyOfDir.resolve("broken-symlink")));
+ }
+
+ private static class CopyOptionsArgumentsProvider implements ArgumentsProvider {
+
+ @Override
+ public Stream extends Arguments> provideArguments(final ParameterDeclarations parameters, final ExtensionContext context) {
+ return Stream.of(
+ Arguments.of((Object) new CopyOption[0]),
+ Arguments.of((Object) new CopyOption[] { LinkOption.NOFOLLOW_LINKS })
+ );
+ }
+ }
+
+ @Test
+ void testCopyDirectoryFollowsAbsoluteSymbolicLinkToDirectory() throws Exception {
+ // Given
+ final Path externalDir = Files.createDirectory(tempDirPath.resolve("external"));
+ final Path dir1 = Files.createDirectory(externalDir.resolve("dir1"));
+ final Path file2 = Files.write(dir1.resolve("file2"), PathUtilsTest.BYTE_ARRAY_FIXTURE);
+ final Path sourceDir = Files.createDirectory(tempDirPath.resolve("source"));
+ final Path dir3 = Files.createDirectory(sourceDir.resolve("dir3"));
+ final Path file4 = Files.write(dir3.resolve("file4"), PathUtilsTest.BYTE_ARRAY_FIXTURE);
+ Files.createSymbolicLink(sourceDir.resolve("symlink1"), dir1.toAbsolutePath());
+ Files.createSymbolicLink(sourceDir.resolve("symlink2"), sourceDir.relativize(file2));
+ Files.createSymbolicLink(sourceDir.resolve("symlink3"), sourceDir.relativize(dir3));
+ Files.createSymbolicLink(dir3.resolve("symlink4"), file4.toAbsolutePath());
+ final Path targetDir = tempDirPath.resolve("target");
+ // When
+ final PathCounters pathCounters = PathUtils.copyDirectory(sourceDir, targetDir);
+ // Then
+ // 6 * 11 bytes == 66:
+ // file2
+ // file4
+ // symlink2 -> file2
+ // symlink4 -> file4
+ // symlink1 -> dir1 containing file2
+ // symlink3 -> dir3 containing file4
+ //
+ // Different result value depending on the Java version and operating system, but should not throw an exception or loop infinitely.
+ pathCounters.getByteCounter().get();
+ assertEquals(2L, pathCounters.getDirectoryCounter().get());
+ assertEquals(5L, pathCounters.getFileCounter().get());
+ assertTrue(Files.exists(targetDir.resolve("dir3").resolve("file4")));
+ assertTrue(Files.exists(targetDir.resolve("dir3").resolve("symlink4")));
+ }
}
diff --git a/src/test/java/org/apache/commons/io/file/PathUtilsTest.java b/src/test/java/org/apache/commons/io/file/PathUtilsTest.java
index cdf4360acc6..5d7e270b351 100644
--- a/src/test/java/org/apache/commons/io/file/PathUtilsTest.java
+++ b/src/test/java/org/apache/commons/io/file/PathUtilsTest.java
@@ -60,7 +60,7 @@ class PathUtilsTest extends AbstractTempDirTest {
private static final String STRING_FIXTURE = "Hello World";
- private static final byte[] BYTE_ARRAY_FIXTURE = STRING_FIXTURE.getBytes(StandardCharsets.UTF_8);
+ static final byte[] BYTE_ARRAY_FIXTURE = STRING_FIXTURE.getBytes(StandardCharsets.UTF_8);
private static final String PATH_FIXTURE = "NOTICE.txt";