/* * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package kotlin.jdk7.test import java.nio.file.* import kotlin.io.path.* import kotlin.jdk7.test.PathTreeWalkTest.Companion.createTestFiles import kotlin.jdk7.test.PathTreeWalkTest.Companion.referenceFilenames import kotlin.jdk7.test.PathTreeWalkTest.Companion.referenceFilesOnly import kotlin.jdk7.test.PathTreeWalkTest.Companion.testVisitedFiles import kotlin.test.* class PathRecursiveFunctionsTest : AbstractPathTest() { @Test fun deleteFile() { val file = createTempFile() assertTrue(file.exists()) file.deleteRecursively() assertFalse(file.exists()) file.createFile().writeText("non-empty file") assertTrue(file.exists()) file.deleteRecursively() assertFalse(file.exists()) file.deleteRecursively() // successfully deletes recursively a non-existent file } @Test fun deleteDirectory() { val dir = createTestFiles() assertTrue(dir.exists()) dir.deleteRecursively() assertFalse(dir.exists()) dir.deleteRecursively() // successfully deletes recursively a non-existent directory } @Test fun deleteNotExistingParent() { val basedir = createTempDirectory().cleanupRecursively() basedir.resolve("a/b").deleteRecursively() basedir.resolve("a/b/c").deleteRecursively() } private fun Path.walkIncludeDirectories(): Sequence = this.walk(PathWalkOption.INCLUDE_DIRECTORIES) @Test fun deleteRestrictedRead() { val basedir = createTestFiles().cleanupRecursively() val restrictedEmptyDir = basedir.resolve("6") val restrictedDir = basedir.resolve("1") val restrictedFile = basedir.resolve("7.txt") withRestrictedRead(restrictedEmptyDir, restrictedDir, restrictedFile) { val error = assertFailsWith("Expected incomplete recursive deletion") { basedir.deleteRecursively() } // AccessDeniedException when opening restrictedEmptyDir and restrictedDir, wrapped in FileSystemException if SecureDirectoryStream was used // DirectoryNotEmptyException is not thrown from parent directory assertEquals(2, error.suppressedExceptions.size) assertIs(error.suppressedExceptions[0].let { it.cause ?: it }) assertIs(error.suppressedExceptions[1].let { it.cause ?: it }) // Couldn't read directory entries. // No attempt to delete even when empty directories can be removed without write permission assertTrue(restrictedEmptyDir.exists()) assertTrue(restrictedDir.exists()) // couldn't read directory entries assertFalse(restrictedFile.exists()) // restricted read allows removal of file restrictedEmptyDir.toFile().setReadable(true) restrictedDir.toFile().setReadable(true) testVisitedFiles(listOf("", "1", "1/2", "1/3", "1/3/4.txt", "1/3/5.txt", "6"), basedir.walkIncludeDirectories(), basedir) basedir.deleteRecursively() } } @Test fun deleteRestrictedWrite() { val basedir = createTestFiles().cleanupRecursively() val restrictedEmptyDir = basedir.resolve("6") val restrictedDir = basedir.resolve("8") val restrictedFile = basedir.resolve("1/3/5.txt") withRestrictedWrite(restrictedEmptyDir, restrictedDir, restrictedFile) { val error = assertFailsWith("Expected incomplete recursive deletion") { basedir.deleteRecursively() } // AccessDeniedException when deleting "8/9.txt", wrapped in FileSystemException if SecureDirectoryStream was used // DirectoryNotEmptyException is not thrown from parent directories when (val accessDenied = error.suppressedExceptions.single()) { is java.nio.file.AccessDeniedException -> { assertEquals(restrictedDir.resolve("9.txt").toString(), accessDenied.file) } is java.nio.file.FileSystemException -> { assertEquals(restrictedDir.resolve("9.txt").toString(), accessDenied.file) assertIs(accessDenied.cause) } else -> { fail("Unexpected exception $accessDenied") } } assertFalse(restrictedEmptyDir.exists()) // empty directories can be removed without write permission assertTrue(restrictedDir.exists()) assertTrue(restrictedDir.resolve("9.txt").exists()) assertFalse(restrictedFile.exists()) // plain files can be removed without write permission } } @Test fun deleteBaseSymlinkToFile() { val file = createTempFile().cleanup() val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(file) ?: return link.deleteRecursively() assertFalse(link.exists(LinkOption.NOFOLLOW_LINKS)) assertTrue(file.exists()) } @Test fun deleteBaseSymlinkToDirectory() { val dir = createTestFiles().cleanupRecursively() val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(dir) ?: return link.deleteRecursively() assertFalse(link.exists(LinkOption.NOFOLLOW_LINKS)) testVisitedFiles(listOf("") + referenceFilenames, dir.walkIncludeDirectories(), dir) } @Test fun deleteSymlinkToFile() { val file = createTempFile().cleanup() val dir = createTestFiles().cleanupRecursively().also { it.resolve("8/link").tryCreateSymbolicLinkTo(file) ?: return } dir.deleteRecursively() assertFalse(dir.exists()) assertTrue(file.exists()) } @Test fun deleteSymlinkToDirectory() { val dir1 = createTestFiles().cleanupRecursively() val dir2 = createTestFiles().cleanupRecursively().also { it.resolve("8/link").tryCreateSymbolicLinkTo(dir1) ?: return } dir2.deleteRecursively() assertFalse(dir2.exists()) testVisitedFiles(listOf("") + referenceFilenames, dir1.walkIncludeDirectories(), dir1) } @Test fun deleteParentSymlink() { val dir1 = createTestFiles().cleanupRecursively() val dir2 = createTempDirectory().cleanupRecursively().also { it.resolve("link").tryCreateSymbolicLinkTo(dir1) ?: return } dir2.resolve("link/8").deleteRecursively() assertFalse(dir1.resolve("8").exists()) dir2.resolve("link/1/3").deleteRecursively() assertFalse(dir1.resolve("1/3").exists()) } @Test fun deleteSymlinkToSymlink() { val dir = createTestFiles().cleanupRecursively() val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(dir) ?: return val linkToLink = createTempDirectory().cleanupRecursively().resolve("linkToLink").tryCreateSymbolicLinkTo(link) ?: return linkToLink.deleteRecursively() assertFalse(linkToLink.exists(LinkOption.NOFOLLOW_LINKS)) assertTrue(link.exists(LinkOption.NOFOLLOW_LINKS)) testVisitedFiles(listOf("") + referenceFilenames, dir.walkIncludeDirectories(), dir) } @Test fun deleteSymlinkCyclic() { val basedir = createTestFiles().cleanupRecursively() val original = basedir.resolve("1") original.resolve("2/link").tryCreateSymbolicLinkTo(original) ?: return basedir.deleteRecursively() assertFalse(basedir.exists()) } @Test fun deleteSymlinkCyclicWithTwo() { val basedir = createTestFiles().cleanupRecursively() val dir8 = basedir.resolve("8") val dir2 = basedir.resolve("1/2") dir8.resolve("linkTo2").tryCreateSymbolicLinkTo(dir2) ?: return dir2.resolve("linkTo8").tryCreateSymbolicLinkTo(dir8) ?: return basedir.deleteRecursively() assertFalse(basedir.exists()) } @Test fun deleteSymlinkPointingToItself() { val basedir = createTempDirectory().cleanupRecursively() val link = basedir.resolve("link") link.tryCreateSymbolicLinkTo(link) ?: return basedir.deleteRecursively() assertFalse(basedir.exists()) } @Test fun deleteSymlinkTwoPointingToEachOther() { val basedir = createTempDirectory().cleanupRecursively() val link1 = basedir.resolve("link1") val link2 = basedir.resolve("link2").tryCreateSymbolicLinkTo(link1) ?: return link1.tryCreateSymbolicLinkTo(link2) ?: return basedir.deleteRecursively() assertFalse(basedir.exists()) } private fun compareFiles(src: Path, dst: Path, message: String? = null) { assertTrue(dst.exists()) assertEquals(src.isRegularFile(), dst.isRegularFile(), message) assertEquals(src.isDirectory(), dst.isDirectory(), message) if (dst.isRegularFile()) { assertTrue(src.readBytes().contentEquals(dst.readBytes()), message) } } private fun compareDirectories(src: Path, dst: Path) { for (srcFile in src.walkIncludeDirectories()) { val dstFile = dst.resolve(srcFile.relativeTo(src)) compareFiles(srcFile, dstFile) } } @Test fun copyFileToFile() { val src = createTempFile().cleanup().also { it.writeText("hello") } val dst = createTempDirectory().cleanupRecursively().resolve("dst") val copyResult = src.copyToRecursively(dst, followLinks = false) assertEquals(dst, copyResult) compareFiles(src, dst) dst.writeText("bye") assertFailsWith { src.copyToRecursively(dst, followLinks = false) } assertEquals("bye", dst.readText()) src.copyToRecursively(dst, followLinks = false, overwrite = true) compareFiles(src, dst) } @Test fun copyFileToDirectory() { val src = createTempFile().cleanup().also { it.writeText("hello") } val dst = createTestFiles().cleanupRecursively() assertFailsWith { src.copyToRecursively(dst, followLinks = false) } assertTrue(dst.isDirectory()) assertFailsWith { src.copyToRecursively(dst, followLinks = false) { source, target -> source.copyTo(target, overwrite = true) CopyActionResult.CONTINUE } } assertTrue(dst.isDirectory()) val copyResult = src.copyToRecursively(dst, followLinks = false, overwrite = true) assertEquals(dst, copyResult) compareFiles(src, dst) } private fun Path.relativePathString(base: Path): String { return relativeToOrSelf(base).invariantSeparatorsPathString } @Test fun copyDirectoryToDirectory() { val src = createTestFiles().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively().resolve("dst") val copyResult = src.copyToRecursively(dst, followLinks = false) assertEquals(dst, copyResult) compareDirectories(src, dst) src.resolve("1/3/4.txt").writeText("hello") dst.resolve("10").createDirectory() val conflictingFiles = mutableListOf() src.copyToRecursively(dst, followLinks = false, onError = { source, _, exception -> assertIs(exception) conflictingFiles.add(source.relativePathString(src)) OnErrorResult.SKIP_SUBTREE }) assertEquals(referenceFilesOnly.sorted(), conflictingFiles.sorted()) assertTrue(dst.resolve("1/3/4.txt").readText().isEmpty()) src.copyToRecursively(dst, followLinks = false, overwrite = true) compareDirectories(src, dst) assertTrue(dst.resolve("10").exists()) } @Test fun copyDirectoryToFile() { val src = createTestFiles().cleanupRecursively() val dst = createTempFile().cleanupRecursively().also { it.writeText("hello") } val existsException = assertFailsWith { src.copyToRecursively(dst, followLinks = false) } // attempted to copy only the root directory(src) assertEquals(dst.toString(), existsException.file) assertTrue(dst.isRegularFile()) src.copyToRecursively(dst, followLinks = false, overwrite = true) compareDirectories(src, dst) } @Test fun copyNonExistentSource() { val src = createTempDirectory().also { it.deleteExisting() } val dst = createTempDirectory() assertFailsWith { src.copyToRecursively(dst, followLinks = false) } dst.deleteExisting() assertFailsWith { src.copyToRecursively(dst, followLinks = false) } } @Test fun copyNonExistentDestinationParent() { val src = createTempDirectory().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively().resolve("parent/dst") assertFalse(dst.parent.exists()) src.copyToRecursively(dst, followLinks = false, onError = { source, target, exception -> assertIs(exception) assertEquals(src, source) assertEquals(dst, target) assertEquals(dst.toString(), exception.file) OnErrorResult.SKIP_SUBTREE }) src.copyToRecursively(dst.createParentDirectories(), followLinks = false) } @Test fun copyRestrictedReadInSource() { val src = createTestFiles().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively() val restrictedDir = src.resolve("1/3") val restrictedFile = src.resolve("7.txt") withRestrictedRead(restrictedDir, restrictedFile, alsoReset = listOf(dst.resolve("1/3"), dst.resolve("7.txt"))) { // Restricted directories fail during traversal, while files fail when copied. // Because Files.walkFileTree opens a directory before calling FileVisitor.onPreVisitDirectory with it. src.copyToRecursively(dst, followLinks = false, onError = { source, _, exception -> assertIs(exception) assertEquals(source.toString(), exception.file) assertEquals("1/3", source.relativePathString(src)) OnErrorResult.SKIP_SUBTREE }) { source, target -> try { source.copyToIgnoringExistingDirectory(target, followLinks = false) } catch (exception: Throwable) { assertIs(exception) assertEquals(source.toString(), exception.file) assertEquals("7.txt", source.relativePathString(src)) } CopyActionResult.CONTINUE } assertFalse(dst.resolve("1/3").exists()) // restricted directory is not copied assertFalse(dst.resolve("7.txt").exists()) // restricted file is not copied } } @Test fun copyRestrictedWriteInSource() { val src = createTestFiles().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively() val restrictedDir = src.resolve("1/3") val restrictedFile = src.resolve("7.txt") withRestrictedWrite(restrictedDir, restrictedFile, alsoReset = listOf(dst.resolve("1/3"), dst.resolve("7.txt"))) { val accessDeniedFiles = mutableListOf() src.copyToRecursively(dst, followLinks = false, onError = { _, target, exception -> assertIs(exception) assertEquals(target.toString(), exception.file) accessDeniedFiles.add(target.relativePathString(dst)) OnErrorResult.SKIP_SUBTREE }) assertEquals(listOf("1/3/4.txt", "1/3/5.txt"), accessDeniedFiles.sorted()) assertTrue(dst.resolve("1/3").exists()) // restricted directory is copied assertFalse(dst.resolve("1/3").isWritable()) // access permissions are copied assertTrue(dst.resolve("7.txt").exists()) // restricted file is copied assertFalse(dst.resolve("7.txt").isWritable()) // access permissions are copied } } @Test fun copyRestrictedWriteInDestination() { val src = createTestFiles().cleanupRecursively() val dst = createTestFiles().cleanupRecursively() src.resolve("1/3/4.txt").writeText("hello") src.resolve("7.txt").writeText("hi") val restrictedDir = dst.resolve("1/3") val restrictedFile = dst.resolve("7.txt") withRestrictedWrite(restrictedDir, restrictedFile) { val accessDeniedFiles = mutableListOf() src.copyToRecursively(dst, followLinks = false, overwrite = true, onError = { _, target, exception -> assertIs(exception) assertEquals(target.toString(), exception.file) accessDeniedFiles.add(target.relativePathString(dst)) OnErrorResult.SKIP_SUBTREE }) assertEquals(listOf("1/3/4.txt", "1/3/5.txt"), accessDeniedFiles.sorted()) assertNotEquals(src.resolve("1/3/4.txt").readText(), dst.resolve("1/3/4.txt").readText()) assertEquals(src.resolve("7.txt").readText(), dst.resolve("7.txt").readText()) } } @Test fun copyBrokenBaseSymlink() { val basedir = createTempDirectory().cleanupRecursively() val target = basedir.resolve("target") val link = basedir.resolve("link").tryCreateSymbolicLinkTo(target) ?: return val dst = basedir.resolve("dst") // the same behavior as link.copyTo(dst, LinkOption.NOFOLLOW_LINKS) link.copyToRecursively(dst, followLinks = false) assertTrue(dst.isSymbolicLink()) assertTrue(dst.exists(LinkOption.NOFOLLOW_LINKS)) assertFalse(dst.exists()) assertFailsWith { link.copyToRecursively(dst, followLinks = false) } // the same behavior as link.copyTo(dst) dst.deleteExisting() assertFailsWith { link.copyToRecursively(dst, followLinks = true) } assertFalse(dst.exists(LinkOption.NOFOLLOW_LINKS)) } @Test fun copyBrokenSymlink() { val src = createTestFiles().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively().resolve("dst") val target = createTempDirectory().cleanupRecursively().resolve("target") src.resolve("8/link").tryCreateSymbolicLinkTo(target) ?: return val dstLink = dst.resolve("8/link") // the same behavior as link.copyTo(dst, LinkOption.NOFOLLOW_LINKS) src.copyToRecursively(dst, followLinks = false) assertTrue(dstLink.isSymbolicLink()) assertTrue(dstLink.exists(LinkOption.NOFOLLOW_LINKS)) assertFalse(dstLink.exists()) // the same behavior as link.copyTo(dst) dst.deleteRecursively() assertFailsWith { src.copyToRecursively(dst, followLinks = true) } assertFalse(dstLink.exists(LinkOption.NOFOLLOW_LINKS)) } @Test fun copyBaseSymlinkPointingToFile() { val src = createTempFile().cleanup().also { it.writeText("hello") } val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(src) ?: return val dst = createTempDirectory().cleanupRecursively().resolve("dst") link.copyToRecursively(dst, followLinks = false) compareFiles(link, dst) dst.deleteExisting() link.copyToRecursively(dst, followLinks = true) compareFiles(src, dst) } @Test fun copyBaseSymlinkPointingToDirectory() { val src = createTestFiles().cleanupRecursively() val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(src) ?: return val dst = createTempDirectory().cleanupRecursively().resolve("dst") link.copyToRecursively(dst, followLinks = false) compareFiles(link, dst) dst.deleteExisting() link.copyToRecursively(dst, followLinks = true) compareDirectories(src, dst) } @Test fun copySymlinkPointingToDirectory() { val symlinkTarget = createTestFiles().cleanupRecursively() val src = createTestFiles().cleanupRecursively().also { it.resolve("8/link").tryCreateSymbolicLinkTo(symlinkTarget) ?: return } val dst = createTempDirectory().cleanupRecursively().resolve("dst") src.copyToRecursively(dst, followLinks = false) val srcContent = listOf("", "8/link") + referenceFilenames testVisitedFiles(srcContent, dst.walkIncludeDirectories(), dst) dst.deleteRecursively() src.copyToRecursively(dst, followLinks = true) val expectedDstContent = srcContent + referenceFilenames.map { "8/link/$it" } testVisitedFiles(expectedDstContent, dst.walkIncludeDirectories(), dst) } @Test fun copyIgnoreExistingDirectoriesFollowLinks() { val src = createTestFiles().cleanupRecursively() val symlinkTarget = createTempDirectory().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively().also { it.resolve("1").createDirectory() it.resolve("1/3").tryCreateSymbolicLinkTo(symlinkTarget) ?: return } src.copyToRecursively(dst, followLinks = true, onError = { source, target, exception -> assertIs(exception) assertEquals(src.resolve("1/3"), source) assertEquals(dst.resolve("1/3"), target) assertEquals(target.toString(), exception.file) OnErrorResult.SKIP_SUBTREE }) assertTrue(dst.resolve("1/3").isSymbolicLink()) assertTrue(symlinkTarget.listDirectoryEntries().isEmpty()) src.copyToRecursively(dst, followLinks = true, overwrite = true) assertFalse(dst.resolve("1/3").isSymbolicLink()) assertTrue(symlinkTarget.listDirectoryEntries().isEmpty()) } @Test fun copyIgnoreExistingDirectoriesNoFollowLinks() { val src = createTestFiles().cleanupRecursively() val symlinkTarget = createTempDirectory().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively().also { it.resolve("1").createDirectory() it.resolve("1/3").tryCreateSymbolicLinkTo(symlinkTarget) ?: return } src.copyToRecursively(dst, followLinks = false, onError = { source, target, exception -> assertIs(exception) assertEquals(src.resolve("1/3"), source) assertEquals(dst.resolve("1/3"), target) assertEquals(target.toString(), exception.file) OnErrorResult.SKIP_SUBTREE }) assertTrue(dst.resolve("1/3").isSymbolicLink()) assertTrue(symlinkTarget.listDirectoryEntries().isEmpty()) src.copyToRecursively(dst, followLinks = false, overwrite = true) assertFalse(dst.resolve("1/3").isSymbolicLink()) assertTrue(symlinkTarget.listDirectoryEntries().isEmpty()) } @Test fun copyParentSymlink() { val source = createTestFiles().cleanupRecursively() val linkToSource = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(source) ?: return val sources = listOf( source to referenceFilenames, linkToSource.resolve("8") to listOf("9.txt"), linkToSource.resolve("1/3") to listOf("4.txt", "5.txt") ) for ((src, srcContent) in sources) { for (followLinks in listOf(false, true)) { val target = createTempDirectory().cleanupRecursively().also { it.resolve("a/b").createDirectories() } val linkToTarget = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(target) ?: return val targets = listOf( target to listOf("a", "a/b"), linkToTarget.resolve("a") to listOf("b"), linkToTarget.resolve("a/b") to listOf() ) for ((dst, dstContent) in targets) { src.copyToRecursively(dst, followLinks = followLinks) val expectedDstContent = listOf("") + dstContent + srcContent testVisitedFiles(expectedDstContent, dst.walkIncludeDirectories(), dst) } } } } @Test fun copySymlinkToSymlink() { val src = createTestFiles().cleanupRecursively() val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(src) ?: return val linkToLink = createTempDirectory().cleanupRecursively().resolve("linkToLink").tryCreateSymbolicLinkTo(link) ?: return val dst = createTempDirectory().cleanupRecursively().resolve("dst") linkToLink.copyToRecursively(dst, followLinks = true) testVisitedFiles(listOf("") + referenceFilenames, dst.walkIncludeDirectories(), dst) } @Test fun copySymlinkCyclic() { val src = createTestFiles().cleanupRecursively() val original = src.resolve("1") original.resolve("2/link").tryCreateSymbolicLinkTo(original) ?: return val dst = createTempDirectory().cleanupRecursively().resolve("dst") src.copyToRecursively(dst, followLinks = true, onError = { source, _, exception -> assertIs(exception) assertEquals(src.resolve("1/2/link"), source) assertEquals(source.toString(), exception.file) OnErrorResult.SKIP_SUBTREE }) // partial copy, only "1/2/link" is not copied testVisitedFiles(listOf("") + referenceFilenames, dst.walkIncludeDirectories(), dst) } @Test fun copySymlinkCyclicWithTwo() { val src = createTestFiles().cleanupRecursively() val dir8 = src.resolve("8") val dir2 = src.resolve("1/2") dir8.resolve("linkTo2").tryCreateSymbolicLinkTo(dir2) ?: return dir2.resolve("linkTo8").tryCreateSymbolicLinkTo(dir8) ?: return val dst = createTempDirectory().cleanupRecursively().resolve("dst") val loops = mutableListOf() src.copyToRecursively(dst, followLinks = true, onError = { source, _, exception -> assertIs(exception) assertEquals(source.toString(), exception.file) loops.add(source.relativePathString(src)) OnErrorResult.SKIP_SUBTREE }) assertEquals(listOf("1/2/linkTo8/linkTo2", "8/linkTo2/linkTo8"), loops.sorted()) // partial copy, only "1/2/linkTo8/linkTo2" and "8/linkTo2/linkTo8" are not copied val expected = listOf("", "1/2/linkTo8", "1/2/linkTo8/9.txt", "8/linkTo2") + referenceFilenames testVisitedFiles(expected, dst.walkIncludeDirectories(), dst) } @Test fun copySymlinkPointingToItself() { val src = createTempDirectory().cleanupRecursively() val link = src.resolve("link") link.tryCreateSymbolicLinkTo(link) ?: return val dst = createTempDirectory().cleanupRecursively().resolve("dst") assertFailsWith { // throws with message "Too many levels of symbolic links" src.copyToRecursively(dst, followLinks = true) } } @Test fun copySymlinkTwoPointingToEachOther() { val src = createTempDirectory().cleanupRecursively() val link1 = src.resolve("link1") val link2 = src.resolve("link2").tryCreateSymbolicLinkTo(link1) ?: return link1.tryCreateSymbolicLinkTo(link2) ?: return val dst = createTempDirectory().cleanupRecursively().resolve("dst") assertFailsWith { // throws with message "Too many levels of symbolic links" src.copyToRecursively(dst, followLinks = true) } } @Test fun copyWithNestedCopyToRecursively() { val src = createTestFiles().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively().resolve("dst") val nested = createTestFiles().cleanupRecursively() src.copyToRecursively(dst, followLinks = false) { source, target -> if (source.name == "2") { nested.copyToRecursively(target, followLinks = false) } else { source.copyToIgnoringExistingDirectory(target, followLinks = false) } CopyActionResult.CONTINUE } val expected = listOf("") + referenceFilenames + referenceFilenames.map { "1/2/$it" } testVisitedFiles(expected, dst.walkIncludeDirectories(), dst) } @Test fun copyWithSkipSubtree() { val src = createTestFiles().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively().resolve("dst") src.copyToRecursively(dst, followLinks = false) { source, target -> source.copyToIgnoringExistingDirectory(target, followLinks = false) if (source.name == "3" || source.name == "9.txt") { CopyActionResult.SKIP_SUBTREE } else { CopyActionResult.CONTINUE } } // both "3" and "9.txt" are copied val copied3 = dst.resolve("1/3").exists() val copied9 = dst.resolve("8/9.txt").exists() assertTrue(copied3 && copied9) // content of "3" is not copied assertTrue(dst.resolve("1/3").listDirectoryEntries().isEmpty()) } @Test fun copyWithTerminate() { val src = createTestFiles().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively().resolve("dst") src.copyToRecursively(dst, followLinks = false) { source, target -> source.copyToIgnoringExistingDirectory(target, followLinks = false) if (source.name == "3" || source.name == "9.txt") { CopyActionResult.TERMINATE } else { CopyActionResult.CONTINUE } } // either "3" or "9.txt" is not copied val copied3 = dst.resolve("1/3").exists() val copied9 = dst.resolve("8/9.txt").exists() assertTrue(copied3 || copied9) assertFalse(copied3 && copied9) } @Test fun copyFailureWithTerminate() { val src = createTestFiles().cleanupRecursively() val dst = createTempDirectory().cleanupRecursively().resolve("dst") src.copyToRecursively(dst, followLinks = false, onError = { source, _, exception -> assertIs(exception) assertTrue(source.name == "3" || source.name == "9.txt") OnErrorResult.TERMINATE }) { source, target -> source.copyToIgnoringExistingDirectory(target, followLinks = false) if (source.name == "3" || source.name == "9.txt") throw IllegalArgumentException() CopyActionResult.CONTINUE } // either "3" or "9.txt" is not copied val copied3 = dst.resolve("1/3").exists() val copied9 = dst.resolve("8/9.txt").exists() assertTrue(copied3 || copied9) assertFalse(copied3 && copied9) } @Test fun copyIntoSourceDirectory() { val source = createTestFiles().cleanupRecursively() val linkToSource = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(source) ?: return val sources = listOf( source to source, linkToSource.resolve("8") to source.resolve("8"), linkToSource.resolve("1/3") to source.resolve("1/3") ) for ((src, resolvedSrc) in sources) { val linkToSrc = createTempDirectory().cleanupRecursively().resolve("linkToSrc").tryCreateSymbolicLinkTo(resolvedSrc) ?: return val targets = listOf( linkToSrc.resolve("a").createDirectory(), linkToSrc.resolve("a/b").createDirectories() ) for (followLinks in listOf(false, true)) { assertFailsWith { src.copyToRecursively(linkToSrc, followLinks = followLinks) } for (dst in targets) { val error = assertFailsWith { src.copyToRecursively(dst, followLinks = followLinks) } assertEquals("Recursively copying a directory into its subdirectory is prohibited.", error.reason) } } } } @Test fun kt38678() { val src = createTempDirectory().cleanupRecursively() src.resolve("test.txt").writeText("plain text file") val dst = src.resolve("x") val error = assertFailsWith { src.copyToRecursively(dst, followLinks = false) } assertEquals("Recursively copying a directory into its subdirectory is prohibited.", error.reason) } @Test fun copyToTheSameFile() { for (src in listOf(createTempFile().cleanupRecursively(), createTestFiles().cleanupRecursively())) { src.copyToRecursively(src, followLinks = false) val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(src) ?: return val error = assertFailsWith { link.copyToRecursively(src, followLinks = false) } assertEquals(src.toString(), error.file) link.copyToRecursively(src, followLinks = true) for (followLinks in listOf(false, true)) { assertFailsWith { src.copyToRecursively(link, followLinks = followLinks) } } } } @Test fun copyDstLinkPointingToSrc() { for (followLinks in listOf(false, true)) { val root = createTempDirectory().cleanupRecursively() val src = root.resolve("src").createFile() val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(src) ?: return assertTrue(src.isSameFileAs(dstLink)) assertTrue(dstLink.isSameFileAs(src)) assertFailsWith { src.copyToRecursively(dstLink, followLinks = followLinks) } assertTrue(dstLink.isSymbolicLink()) } } @Test fun copyDstLinkPointingToSrcOverwrite() { for (followLinks in listOf(false, true)) { val root = createTempDirectory().cleanupRecursively() val src = root.resolve("src").createFile() val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(src) ?: return src.copyToRecursively(dstLink, followLinks = followLinks, overwrite = true) assertFalse(dstLink.isSymbolicLink()) } } @Test fun copySrcLinkAndDstLinkPointingToSameFile() { for (followLinks in listOf(false, true)) { val root = createTempDirectory().cleanupRecursively() val original = root.resolve("original").createFile() val srcLink = root.resolve("srcLink").tryCreateSymbolicLinkTo(original) ?: return val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(original) ?: return assertTrue(srcLink.isSameFileAs(dstLink)) assertTrue(dstLink.isSameFileAs(srcLink)) assertFailsWith { srcLink.copyToRecursively(dstLink, followLinks = followLinks) } assertTrue(dstLink.isSymbolicLink()) } } @Test fun copySrcLinkAndDstLinkPointingToSameFileOverwrite() { for (followLinks in listOf(false, true)) { val root = createTempDirectory().cleanupRecursively() val original = root.resolve("original").createFile() val srcLink = root.resolve("srcLink").tryCreateSymbolicLinkTo(original) ?: return val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(original) ?: return srcLink.copyToRecursively(dstLink, followLinks = followLinks, overwrite = true) if (!followLinks) { assertTrue(dstLink.isSymbolicLink()) // src symlink was copied } else { assertFalse(dstLink.isSymbolicLink()) // target of src symlink was copied } } } @Test fun copySameLinkDifferentRoute() { for (followLinks in listOf(false, true)) { val root = createTempDirectory().cleanupRecursively() val original = root.resolve("original").createFile() val srcLink = root.resolve("srcLink").tryCreateSymbolicLinkTo(original) ?: return val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(root)?.resolve("srcLink") ?: return assertTrue(srcLink.isSameFileAs(dstLink)) assertTrue(dstLink.isSameFileAs(srcLink)) if (!followLinks) { srcLink.copyToRecursively(dstLink, followLinks = followLinks) // same file } else { assertFailsWith { srcLink.copyToRecursively(dstLink, followLinks = followLinks) // target of srcLink copied to srcLink location } } assertTrue(dstLink.isSymbolicLink()) } } @Test fun copySameLinkDifferentRouteOverwrite() { for (followLinks in listOf(false, true)) { val root = createTempDirectory().cleanupRecursively() val original = root.resolve("original").createFile() val srcLink = root.resolve("srcLink").tryCreateSymbolicLinkTo(original) ?: return val dstLink = root.resolve("dstLink").tryCreateSymbolicLinkTo(root)?.resolve("srcLink") ?: return if (!followLinks) { srcLink.copyToRecursively(dstLink, followLinks = followLinks, overwrite = true) // same file } else { // dstLink is deleted before srcLink gets copied. // Actually srcLink gets removed because dstLink is srcLink with different path. val error = assertFailsWith { srcLink.copyToRecursively(dstLink, followLinks = followLinks, overwrite = true) } assertEquals(srcLink.toString(), error.file) assertFalse(srcLink.exists(LinkOption.NOFOLLOW_LINKS)) assertFalse(dstLink.exists(LinkOption.NOFOLLOW_LINKS)) } } } @Test fun copySameFileDifferentRoute() { val root = createTempDirectory().cleanupRecursively() val src = root.resolve("src").createFile() val dst = root.resolve("dstLink").tryCreateSymbolicLinkTo(root)?.resolve("src") ?: return assertTrue(src.isSameFileAs(dst)) assertTrue(dst.isSameFileAs(src)) src.copyToRecursively(dst, followLinks = false) } @Test fun copyToSameFileDifferentRouteOverwrite() { val root = createTempDirectory().cleanupRecursively() val src = root.resolve("src").createFile() val dst = root.resolve("dstLink").tryCreateSymbolicLinkTo(root)?.resolve("src") ?: return src.copyToRecursively(dst, followLinks = false, overwrite = true) } }