f8c587ddcf
* Prohibit files with name "." or ".." * Detect cycles caused by files with name "/" * Prohibit copy outside the target directory The commit also moves zip-related tests to PathRecursiveFunctionsZipTest.
974 lines
40 KiB
Kotlin
974 lines
40 KiB
Kotlin
/*
|
|
* 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<Path> =
|
|
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<java.nio.file.FileSystemException>("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<java.nio.file.AccessDeniedException>(error.suppressedExceptions[0].let { it.cause ?: it })
|
|
assertIs<java.nio.file.AccessDeniedException>(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<java.nio.file.FileSystemException>("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<java.nio.file.AccessDeniedException>(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<java.nio.file.FileAlreadyExistsException> {
|
|
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<java.nio.file.FileAlreadyExistsException> {
|
|
src.copyToRecursively(dst, followLinks = false)
|
|
}
|
|
assertTrue(dst.isDirectory())
|
|
|
|
assertFailsWith<java.nio.file.DirectoryNotEmptyException> {
|
|
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<String>()
|
|
src.copyToRecursively(dst, followLinks = false, onError = { source, _, exception ->
|
|
assertIs<java.nio.file.FileAlreadyExistsException>(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<java.nio.file.FileAlreadyExistsException> {
|
|
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<java.nio.file.NoSuchFileException> {
|
|
src.copyToRecursively(dst, followLinks = false)
|
|
}
|
|
|
|
dst.deleteExisting()
|
|
assertFailsWith<java.nio.file.NoSuchFileException> {
|
|
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<java.nio.file.NoSuchFileException>(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<java.nio.file.AccessDeniedException>(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<java.nio.file.AccessDeniedException>(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<String>()
|
|
src.copyToRecursively(dst, followLinks = false, onError = { _, target, exception ->
|
|
assertIs<java.nio.file.AccessDeniedException>(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<String>()
|
|
src.copyToRecursively(dst, followLinks = false, overwrite = true, onError = { _, target, exception ->
|
|
assertIs<java.nio.file.AccessDeniedException>(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<java.nio.file.FileAlreadyExistsException> {
|
|
link.copyToRecursively(dst, followLinks = false)
|
|
}
|
|
|
|
// the same behavior as link.copyTo(dst)
|
|
dst.deleteExisting()
|
|
assertFailsWith<java.nio.file.NoSuchFileException> {
|
|
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<java.nio.file.NoSuchFileException> {
|
|
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<java.nio.file.FileAlreadyExistsException>(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<java.nio.file.FileAlreadyExistsException>(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<java.nio.file.FileSystemLoopException>(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<String>()
|
|
src.copyToRecursively(dst, followLinks = true, onError = { source, _, exception ->
|
|
assertIs<java.nio.file.FileSystemLoopException>(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<java.nio.file.FileSystemException> {
|
|
// 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<java.nio.file.FileSystemException> {
|
|
// 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<IllegalArgumentException>(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<java.nio.file.FileAlreadyExistsException> {
|
|
src.copyToRecursively(linkToSrc, followLinks = followLinks)
|
|
}
|
|
for (dst in targets) {
|
|
val error = assertFailsWith<java.nio.file.FileSystemException> {
|
|
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<java.nio.file.FileSystemException> {
|
|
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<java.nio.file.FileAlreadyExistsException> {
|
|
link.copyToRecursively(src, followLinks = false)
|
|
}
|
|
assertEquals(src.toString(), error.file)
|
|
link.copyToRecursively(src, followLinks = true)
|
|
|
|
for (followLinks in listOf(false, true)) {
|
|
assertFailsWith<java.nio.file.FileAlreadyExistsException> {
|
|
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<FileAlreadyExistsException> {
|
|
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<FileAlreadyExistsException> {
|
|
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<FileAlreadyExistsException> {
|
|
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<NoSuchFileException> {
|
|
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)
|
|
}
|
|
}
|