Fix security vulnerability in Path recursive functions #KT-63103
* 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.
This commit is contained in:
committed by
Space Team
parent
4c06673483
commit
f8c587ddcf
@@ -188,18 +188,35 @@ public fun Path.copyToRecursively(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val normalizedTarget = target.normalize()
|
||||||
|
|
||||||
fun destination(source: Path): Path {
|
fun destination(source: Path): Path {
|
||||||
val relativePath = source.relativeTo(this@copyToRecursively)
|
val relativePath = source.relativeTo(this@copyToRecursively)
|
||||||
return target.resolve(relativePath.pathString)
|
val destination = target.resolve(relativePath.pathString)
|
||||||
|
if (!destination.normalize().startsWith(normalizedTarget)) {
|
||||||
|
throw IllegalFileNameException(
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
"Copying files to outside the specified target directory is prohibited. The directory being recursively copied might contain an entry with an illegal name."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
fun error(source: Path, exception: Exception): FileVisitResult {
|
fun error(source: Path, exception: Exception): FileVisitResult {
|
||||||
return onError(source, destination(source), exception).toFileVisitResult()
|
return onError(source, destination(source), exception).toFileVisitResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val stack = arrayListOf<Path>()
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun copy(source: Path, attributes: BasicFileAttributes): FileVisitResult {
|
fun copy(source: Path, attributes: BasicFileAttributes): FileVisitResult {
|
||||||
return try {
|
return try {
|
||||||
|
if (stack.isNotEmpty()) {
|
||||||
|
// Check entries other than the starting path of traversal
|
||||||
|
source.checkFileName()
|
||||||
|
source.checkNotSameAs(stack.last())
|
||||||
|
}
|
||||||
DefaultCopyActionContext.copyAction(source, destination(source)).toFileVisitResult()
|
DefaultCopyActionContext.copyAction(source, destination(source)).toFileVisitResult()
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
error(source, exception)
|
error(source, exception)
|
||||||
@@ -207,10 +224,15 @@ public fun Path.copyToRecursively(
|
|||||||
}
|
}
|
||||||
|
|
||||||
visitFileTree(followLinks = followLinks) {
|
visitFileTree(followLinks = followLinks) {
|
||||||
onPreVisitDirectory(::copy)
|
onPreVisitDirectory { directory, attributes ->
|
||||||
|
copy(directory, attributes).also {
|
||||||
|
if (it == FileVisitResult.CONTINUE) stack.add(directory)
|
||||||
|
}
|
||||||
|
}
|
||||||
onVisitFile(::copy)
|
onVisitFile(::copy)
|
||||||
onVisitFileFailed(::error)
|
onVisitFileFailed(::error)
|
||||||
onPostVisitDirectory { directory, exception ->
|
onPostVisitDirectory { directory, exception ->
|
||||||
|
stack.removeLast()
|
||||||
if (exception == null) {
|
if (exception == null) {
|
||||||
FileVisitResult.CONTINUE
|
FileVisitResult.CONTINUE
|
||||||
} else {
|
} else {
|
||||||
@@ -330,13 +352,13 @@ private fun Path.deleteRecursivelyImpl(): List<Exception> {
|
|||||||
if (stream is SecureDirectoryStream<Path>) {
|
if (stream is SecureDirectoryStream<Path>) {
|
||||||
useInsecure = false
|
useInsecure = false
|
||||||
collector.path = parent
|
collector.path = parent
|
||||||
stream.handleEntry(this.fileName, collector)
|
stream.handleEntry(this.fileName, null, collector)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useInsecure) {
|
if (useInsecure) {
|
||||||
insecureHandleEntry(this, collector)
|
insecureHandleEntry(this, null, collector)
|
||||||
}
|
}
|
||||||
|
|
||||||
return collector.collectedExceptions
|
return collector.collectedExceptions
|
||||||
@@ -356,10 +378,16 @@ private inline fun <R> tryIgnoreNoSuchFileException(function: () -> R): R? {
|
|||||||
|
|
||||||
// secure walk
|
// secure walk
|
||||||
|
|
||||||
private fun SecureDirectoryStream<Path>.handleEntry(name: Path, collector: ExceptionsCollector) {
|
private fun SecureDirectoryStream<Path>.handleEntry(name: Path, parent: Path?, collector: ExceptionsCollector) {
|
||||||
collector.enterEntry(name)
|
collector.enterEntry(name)
|
||||||
|
|
||||||
collectIfThrows(collector) {
|
collectIfThrows(collector) {
|
||||||
|
if (parent != null) {
|
||||||
|
// Check entries other than the starting path of traversal
|
||||||
|
val entry = collector.path!!
|
||||||
|
entry.checkFileName()
|
||||||
|
entry.checkNotSameAs(parent)
|
||||||
|
}
|
||||||
if (this.isDirectory(name, LinkOption.NOFOLLOW_LINKS)) {
|
if (this.isDirectory(name, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
val preEnterTotalExceptions = collector.totalExceptions
|
val preEnterTotalExceptions = collector.totalExceptions
|
||||||
|
|
||||||
@@ -384,7 +412,7 @@ private fun SecureDirectoryStream<Path>.enterDirectory(name: Path, collector: Ex
|
|||||||
this.newDirectoryStream(name, LinkOption.NOFOLLOW_LINKS)
|
this.newDirectoryStream(name, LinkOption.NOFOLLOW_LINKS)
|
||||||
}?.use { directoryStream ->
|
}?.use { directoryStream ->
|
||||||
for (entry in directoryStream) {
|
for (entry in directoryStream) {
|
||||||
directoryStream.handleEntry(entry.fileName, collector)
|
directoryStream.handleEntry(entry.fileName, collector.path, collector)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,8 +426,13 @@ private fun SecureDirectoryStream<Path>.isDirectory(entryName: Path, vararg opti
|
|||||||
|
|
||||||
// insecure walk
|
// insecure walk
|
||||||
|
|
||||||
private fun insecureHandleEntry(entry: Path, collector: ExceptionsCollector) {
|
private fun insecureHandleEntry(entry: Path, parent: Path?, collector: ExceptionsCollector) {
|
||||||
collectIfThrows(collector) {
|
collectIfThrows(collector) {
|
||||||
|
if (parent != null) {
|
||||||
|
// Check entries other than the starting path of traversal
|
||||||
|
entry.checkFileName()
|
||||||
|
entry.checkNotSameAs(parent)
|
||||||
|
}
|
||||||
if (entry.isDirectory(LinkOption.NOFOLLOW_LINKS)) {
|
if (entry.isDirectory(LinkOption.NOFOLLOW_LINKS)) {
|
||||||
val preEnterTotalExceptions = collector.totalExceptions
|
val preEnterTotalExceptions = collector.totalExceptions
|
||||||
|
|
||||||
@@ -422,8 +455,67 @@ private fun insecureEnterDirectory(path: Path, collector: ExceptionsCollector) {
|
|||||||
Files.newDirectoryStream(path)
|
Files.newDirectoryStream(path)
|
||||||
}?.use { directoryStream ->
|
}?.use { directoryStream ->
|
||||||
for (entry in directoryStream) {
|
for (entry in directoryStream) {
|
||||||
insecureHandleEntry(entry, collector)
|
insecureHandleEntry(entry, path, collector)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// illegal file name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the name of this file is legal for traversal to prevent cycles.
|
||||||
|
*
|
||||||
|
* Some names are considered illegal as they may cause traversal cycles.
|
||||||
|
* This function is intended for use with entries whose parent directories have already been traversed.
|
||||||
|
* The file being checked is not the starting point of traversal.
|
||||||
|
*
|
||||||
|
* For instance, "/a/b/.." is a valid starting path for traversal. However, if traversal begins from "/a"
|
||||||
|
* and reaches "a/b/..", it will result in a cycle.
|
||||||
|
*
|
||||||
|
* @throws IllegalFileNameException if the file name is "..", "../", , "..\", ".", "./", or ".\" since these may lead to traversal cycles.
|
||||||
|
*
|
||||||
|
* See KT-63103 for more details on the issue.
|
||||||
|
*/
|
||||||
|
internal fun Path.checkFileName() {
|
||||||
|
val fileName = this.name
|
||||||
|
if (fileName == ".." || fileName == "../" || fileName == "..\\" ||
|
||||||
|
fileName == "." || fileName == "./" || fileName == ".\\") throw IllegalFileNameException(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that this entry is not the same as the specified [parent] path to prevent traversal cycles.
|
||||||
|
*
|
||||||
|
* When reading entries of a directory, there are cases where the directory itself is returned,
|
||||||
|
* such as when a zip entry name is '/'. Including the directory itself in the list of its entries can lead to traversal cycles.
|
||||||
|
*
|
||||||
|
* Unfortunately, [Files.walkFileTree], utilized in [copyToRecursively], may not detect such cycles when links are not followed.
|
||||||
|
* Similarly, [deleteRecursively] lacks cycle detection capabilities as it never follows links.
|
||||||
|
*
|
||||||
|
* This function is intended for use with entries whose parent directories have already been traversed.
|
||||||
|
* The file being checked is not the starting point of traversal.
|
||||||
|
*
|
||||||
|
* For instance, "/a/b/.." is a valid starting path for traversal. However, if traversal begins from "/a"
|
||||||
|
* and reaches "a/b/..", it will result in a cycle.
|
||||||
|
*
|
||||||
|
* @throws FileSystemLoopException if this entry is the same as the [parent] path, indicating a potential traversal cycle.
|
||||||
|
*
|
||||||
|
* See KT-63103 for more details on the issue.
|
||||||
|
*/
|
||||||
|
private fun Path.checkNotSameAs(parent: Path) {
|
||||||
|
// Symlinks are skipped:
|
||||||
|
// If this path is a symlink pointing to [parent] and links are not followed, the path is perfectly fine for traversal.
|
||||||
|
// However, [Path.isSameFileAs] always follows links.
|
||||||
|
// [parent] can't be a symlink:
|
||||||
|
// Otherwise, it would mean links are followed and [Files.walkFileTree] would have already detected the cycle.
|
||||||
|
if (!isSymbolicLink() && isSameFileAs(parent))
|
||||||
|
throw FileSystemLoopException(this.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class IllegalFileNameException(
|
||||||
|
file: Path,
|
||||||
|
other: Path?,
|
||||||
|
message: String?
|
||||||
|
) : FileSystemException(file.toString(), other?.toString(), message) {
|
||||||
|
constructor(file: Path) : this(file, null, null)
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ internal class PathTreeWalk(
|
|||||||
entriesAction: (List<PathNode>) -> Unit
|
entriesAction: (List<PathNode>) -> Unit
|
||||||
) {
|
) {
|
||||||
val path = node.path
|
val path = node.path
|
||||||
|
if (node.parent != null) {
|
||||||
|
// Check entries other than the starting path of traversal
|
||||||
|
path.checkFileName()
|
||||||
|
}
|
||||||
if (path.isDirectory(*linkOptions)) {
|
if (path.isDirectory(*linkOptions)) {
|
||||||
if (node.createsCycle())
|
if (node.createsCycle())
|
||||||
throw FileSystemLoopException(path.toString())
|
throw FileSystemLoopException(path.toString())
|
||||||
|
|||||||
@@ -5,10 +5,7 @@
|
|||||||
|
|
||||||
package kotlin.jdk7.test
|
package kotlin.jdk7.test
|
||||||
|
|
||||||
import java.net.URI
|
|
||||||
import java.nio.file.*
|
import java.nio.file.*
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
import kotlin.io.path.*
|
import kotlin.io.path.*
|
||||||
import kotlin.jdk7.test.PathTreeWalkTest.Companion.createTestFiles
|
import kotlin.jdk7.test.PathTreeWalkTest.Companion.createTestFiles
|
||||||
import kotlin.jdk7.test.PathTreeWalkTest.Companion.referenceFilenames
|
import kotlin.jdk7.test.PathTreeWalkTest.Companion.referenceFilenames
|
||||||
@@ -973,50 +970,4 @@ class PathRecursiveFunctionsTest : AbstractPathTest() {
|
|||||||
|
|
||||||
src.copyToRecursively(dst, followLinks = false, overwrite = true)
|
src.copyToRecursively(dst, followLinks = false, overwrite = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createZipFile(parent: Path, name: String): Path {
|
|
||||||
val zipRoot = parent.resolve(name)
|
|
||||||
ZipOutputStream(zipRoot.outputStream()).use { out ->
|
|
||||||
out.putNextEntry(ZipEntry("directory/file.txt"))
|
|
||||||
out.write("hello".toByteArray())
|
|
||||||
out.closeEntry()
|
|
||||||
}
|
|
||||||
return zipRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun zipToDefaultPath() {
|
|
||||||
val root = createTempDirectory().cleanupRecursively()
|
|
||||||
val zipRoot = createZipFile(root, "src.zip")
|
|
||||||
val dst = root.resolve("dst")
|
|
||||||
|
|
||||||
val classLoader: ClassLoader? = null
|
|
||||||
FileSystems.newFileSystem(zipRoot, classLoader).use { zipFs ->
|
|
||||||
val src = zipFs.getPath("/directory")
|
|
||||||
|
|
||||||
src.copyToRecursively(dst, followLinks = false)
|
|
||||||
|
|
||||||
val expected = listOf("", "file.txt")
|
|
||||||
testVisitedFiles(expected, dst.walkIncludeDirectories(), dst)
|
|
||||||
assertEquals("hello", dst.resolve("file.txt").readText())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun defaultPathToZip() {
|
|
||||||
val root = createTestFiles().cleanupRecursively()
|
|
||||||
val zipRoot = createZipFile(root, "dst.zip")
|
|
||||||
val src = root.resolve("1").also { it.resolve("3/4.txt").writeText("hello") }
|
|
||||||
|
|
||||||
val classLoader: ClassLoader? = null
|
|
||||||
FileSystems.newFileSystem(zipRoot, classLoader).use { zipFs ->
|
|
||||||
val dst = zipFs.getPath("/directory")
|
|
||||||
|
|
||||||
src.copyToRecursively(dst, followLinks = false)
|
|
||||||
|
|
||||||
val expected = listOf("", "2", "3", "3/4.txt", "3/5.txt", "file.txt")
|
|
||||||
testVisitedFiles(expected, dst.walkIncludeDirectories(), dst)
|
|
||||||
assertEquals("hello", zipFs.getPath("/directory/3/4.txt").readText())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,605 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2024 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 test.testOnJvm8
|
||||||
|
import test.testOnJvm9AndAbove
|
||||||
|
import java.lang.NullPointerException
|
||||||
|
import java.nio.file.*
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipException
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
import kotlin.io.path.*
|
||||||
|
import kotlin.jdk7.test.PathTreeWalkTest.Companion.createTestFiles
|
||||||
|
import kotlin.jdk7.test.PathTreeWalkTest.Companion.referenceFilenames
|
||||||
|
import kotlin.jdk7.test.PathTreeWalkTest.Companion.testVisitedFiles
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
class PathRecursiveFunctionsZipTest : AbstractPathTest() {
|
||||||
|
|
||||||
|
private fun Path.walkIncludeDirectories(): Sequence<Path> =
|
||||||
|
this.walk(PathWalkOption.INCLUDE_DIRECTORIES)
|
||||||
|
|
||||||
|
private fun createZipFile(parent: Path, name: String, entries: List<String>): Path {
|
||||||
|
val zipRoot = parent.resolve(name)
|
||||||
|
ZipOutputStream(zipRoot.outputStream()).use { out ->
|
||||||
|
for (fileName in entries) {
|
||||||
|
out.putNextEntry(ZipEntry(fileName))
|
||||||
|
if (!fileName.endsWith("/")) {
|
||||||
|
out.write(fileName.toByteArray())
|
||||||
|
}
|
||||||
|
out.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return zipRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun withZip(name: String, entries: List<String>, block: (parent: Path, zipRoot: Path) -> Unit) {
|
||||||
|
val parent = createTempDirectory().cleanupRecursively()
|
||||||
|
val archive = createZipFile(parent, name, entries)
|
||||||
|
val zipFs: FileSystem
|
||||||
|
try {
|
||||||
|
val classLoader: ClassLoader? = null
|
||||||
|
zipFs = FileSystems.newFileSystem(archive, classLoader)
|
||||||
|
} catch (e: ZipException) {
|
||||||
|
// Later JDK versions do not allow opening zip files that have entry named "." or "..".
|
||||||
|
// See https://bugs.openjdk.org/browse/JDK-8251329 and https://bugs.openjdk.org/browse/JDK-8283486
|
||||||
|
if (e.message?.contains("a '.' or '..' element") == true) {
|
||||||
|
println("Opening a ZIP file failed with $e")
|
||||||
|
} else {
|
||||||
|
// Potentially a different cause for the failed opening.
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
zipFs.use {
|
||||||
|
val zipRoot = it.getPath("/")
|
||||||
|
block(parent, zipRoot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun zipToDefaultPath() {
|
||||||
|
withZip("src.zip", listOf("directory/", "directory/file.txt")) { root, zipRoot ->
|
||||||
|
val dst = root.resolve("dst")
|
||||||
|
val src = zipRoot.resolve("directory")
|
||||||
|
|
||||||
|
src.copyToRecursively(dst, followLinks = false)
|
||||||
|
|
||||||
|
val expected = listOf("", "file.txt")
|
||||||
|
testVisitedFiles(expected, dst.walkIncludeDirectories(), dst)
|
||||||
|
assertEquals("directory/file.txt", dst.resolve("file.txt").readText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun defaultPathToZip() {
|
||||||
|
val srcRoot = createTestFiles().cleanupRecursively()
|
||||||
|
withZip("dst.zip", listOf("directory/", "directory/file.txt")) { _, zipRoot ->
|
||||||
|
val src = srcRoot.resolve("1").also { it.resolve("3/4.txt").writeText("hello") }
|
||||||
|
val dst = zipRoot.resolve("directory")
|
||||||
|
|
||||||
|
src.copyToRecursively(dst, followLinks = false)
|
||||||
|
|
||||||
|
val expected = listOf("", "2", "3", "3/4.txt", "3/5.txt", "file.txt")
|
||||||
|
testVisitedFiles(expected, dst.walkIncludeDirectories(), dst)
|
||||||
|
assertEquals("hello", zipRoot.resolve("directory/3/4.txt").readText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testWalkSucceeds(path: Path, vararg expectedContent: Set<Path>) {
|
||||||
|
val content = path.walkIncludeDirectories().toSet()
|
||||||
|
assertContains(expectedContent.toList(), content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testWalkFailsWithIllegalFileName(path: Path) {
|
||||||
|
assertFailsWith<IllegalFileNameException> {
|
||||||
|
path.walkIncludeDirectories().toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> testWalkMaybeFailsWith(path: Path, vararg expectedContent: Set<Path>) {
|
||||||
|
try {
|
||||||
|
testWalkSucceeds(path, *expectedContent)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
assertIs<T>(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testCopySucceeds(source: Path, target: Path, vararg expectedTargetContent: Set<Path>) {
|
||||||
|
source.copyToRecursively(target, followLinks = false)
|
||||||
|
val content = target.walkIncludeDirectories().toSet()
|
||||||
|
assertContains(expectedTargetContent.toList(), content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testCopyFailsWithIllegalFileName(source: Path, target: Path) {
|
||||||
|
assertFailsWith<IllegalFileNameException> {
|
||||||
|
source.copyToRecursively(target, followLinks = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> testCopyMaybeFailsWith(source: Path, target: Path, expectedTargetContent: Set<Path>) {
|
||||||
|
try {
|
||||||
|
testCopySucceeds(source, target, expectedTargetContent)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
assertIs<T>(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testDeleteSucceeds(path: Path) {
|
||||||
|
path.deleteRecursively()
|
||||||
|
assertFalse(path.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> testDeleteFailsWith(path: Path) {
|
||||||
|
assertFailsWith<FileSystemException> {
|
||||||
|
path.deleteRecursively()
|
||||||
|
}.also { exception ->
|
||||||
|
val suppressed = exception.suppressed.single()
|
||||||
|
assertIs<T>(suppressed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> testDeleteMaybeFailsWith(path: Path): Boolean {
|
||||||
|
return try {
|
||||||
|
testDeleteSucceeds(path)
|
||||||
|
true
|
||||||
|
} catch (exception: FileSystemException) {
|
||||||
|
val suppressed = exception.suppressed.single()
|
||||||
|
assertIs<T>(suppressed)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Path.resolve(vararg entryNames: String): Set<Path> {
|
||||||
|
return entryNames.map { resolve(it) }.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun zipDotFileName() {
|
||||||
|
withZip("Archive1.zip", listOf("normal", ".")) { root, zipRoot ->
|
||||||
|
val dotFile = zipRoot.resolve(".")
|
||||||
|
val dotDir = zipRoot.resolve("./")
|
||||||
|
testWalkFailsWithIllegalFileName(zipRoot)
|
||||||
|
testWalkFailsWithIllegalFileName(dotFile)
|
||||||
|
// Succeeds on jvm8, fails on jvm9+
|
||||||
|
testWalkMaybeFailsWith<IllegalFileNameException>(dotDir, setOf(dotDir))
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive1")
|
||||||
|
testCopyFailsWithIllegalFileName(zipRoot, target)
|
||||||
|
val dotFileTarget = root.resolve("UnzipArchive1-dotFile")
|
||||||
|
testCopyFailsWithIllegalFileName(dotFile, dotFileTarget)
|
||||||
|
val dotDirTarget = root.resolve("UnzipArchive1-dotDir")
|
||||||
|
// Succeeds on jvm8, fails on jvm9+
|
||||||
|
testCopyMaybeFailsWith<IllegalFileNameException>(dotDir, dotDirTarget, setOf(dotDirTarget))
|
||||||
|
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(zipRoot)
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(dotFile)
|
||||||
|
// Succeeds on jvm8, fails on jvm9+
|
||||||
|
testDeleteMaybeFailsWith<IllegalFileNameException>(dotDir)
|
||||||
|
assertTrue(dotFile.exists())
|
||||||
|
assertTrue(zipRoot.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive2.zip", listOf("normal", "./")) { root, zipRoot ->
|
||||||
|
val dotFile = zipRoot.resolve(".")
|
||||||
|
val dotDir = zipRoot.resolve("./")
|
||||||
|
testWalkFailsWithIllegalFileName(zipRoot)
|
||||||
|
testWalkFailsWithIllegalFileName(dotFile)
|
||||||
|
// Succeeds on jvm8, fails on jvm9+
|
||||||
|
testWalkMaybeFailsWith<IllegalFileNameException>(dotDir, setOf(dotDir))
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive2")
|
||||||
|
testCopyFailsWithIllegalFileName(zipRoot, target)
|
||||||
|
val dotFileTarget = root.resolve("UnzipArchive2-dotFile")
|
||||||
|
testCopyFailsWithIllegalFileName(dotFile, dotFileTarget)
|
||||||
|
val dotDirTarget = root.resolve("UnzipArchive2-dotDir")
|
||||||
|
// Succeeds on jvm8, fails on jvm9+
|
||||||
|
testCopyMaybeFailsWith<IllegalFileNameException>(dotDir, dotDirTarget, setOf(dotDirTarget))
|
||||||
|
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(zipRoot)
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(dotFile)
|
||||||
|
// Succeeds on jvm8, fails on jvm9+
|
||||||
|
testDeleteMaybeFailsWith<IllegalFileNameException>(dotDir)
|
||||||
|
assertTrue(dotFile.exists())
|
||||||
|
assertTrue(zipRoot.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive3.zip", listOf("a/", "a/.")) { root, zipRoot ->
|
||||||
|
val a = zipRoot.resolve("a")
|
||||||
|
testWalkFailsWithIllegalFileName(zipRoot)
|
||||||
|
testWalkFailsWithIllegalFileName(a)
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive3")
|
||||||
|
testCopyFailsWithIllegalFileName(zipRoot, target)
|
||||||
|
val aTarget = root.resolve("UnzipArchive3-a")
|
||||||
|
testCopyFailsWithIllegalFileName(a, aTarget)
|
||||||
|
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(zipRoot)
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive4.zip", listOf("a/", "a/./")) { root, zipRoot ->
|
||||||
|
val a = zipRoot.resolve("a")
|
||||||
|
testWalkFailsWithIllegalFileName(zipRoot)
|
||||||
|
testWalkFailsWithIllegalFileName(a)
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive4")
|
||||||
|
testCopyFailsWithIllegalFileName(zipRoot, target)
|
||||||
|
val aTarget = root.resolve("UnzipArchive4-a")
|
||||||
|
testCopyFailsWithIllegalFileName(a, aTarget)
|
||||||
|
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(zipRoot)
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun zipSlashFileName() {
|
||||||
|
withZip("Archive1.zip", listOf("normal", "/")) { root, zipRoot ->
|
||||||
|
// Fails in jvm8-10, succeeds in jvm11
|
||||||
|
testWalkMaybeFailsWith<FileSystemLoopException>(zipRoot, zipRoot.resolve("", "normal"))
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive1")
|
||||||
|
// Fails in jvm8-10, succeeds in jvm11
|
||||||
|
testCopyMaybeFailsWith<FileSystemLoopException>(zipRoot, target, target.resolve("", "normal"))
|
||||||
|
|
||||||
|
// Throws FileSystemLoopException in jvm8-10
|
||||||
|
// Path.deleteIfExists on the root directory of the archive throws NullPointerException in jvm9+
|
||||||
|
assertFails { zipRoot.deleteRecursively() }
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive2.zip", listOf("normal", "//")) { root, zipRoot ->
|
||||||
|
// Fails in jvm8, succeeds in jvm9+
|
||||||
|
try {
|
||||||
|
zipRoot.walkIncludeDirectories().toList()
|
||||||
|
// ["/", "//", "normal"] in jvm9-10
|
||||||
|
// ["/", "/normal"] in jvm11
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
assertIs<FileSystemLoopException>(exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive2")
|
||||||
|
// Fails in jvm8, succeeds in jvm9+
|
||||||
|
testCopyMaybeFailsWith<FileSystemLoopException>(zipRoot, target, target.resolve("", "normal"))
|
||||||
|
|
||||||
|
// Throws FileSystemLoopException in jvm8
|
||||||
|
// Path.deleteIfExists on the root directory of the archive throws NullPointerException in jvm9+
|
||||||
|
assertFails { zipRoot.deleteRecursively() }
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive3.zip", listOf("a/", "a//")) { root, zipRoot ->
|
||||||
|
val aFile = zipRoot.resolve("a")
|
||||||
|
val aDir = zipRoot.resolve("a/")
|
||||||
|
// Fails in jvm8, succeeds in jvm9+
|
||||||
|
try {
|
||||||
|
zipRoot.walkIncludeDirectories().toList()
|
||||||
|
// ["/", "/a", "/a/"] in jvm9-10
|
||||||
|
// ["/", "/a"] in jvm11
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
assertIs<FileSystemLoopException>(exception)
|
||||||
|
}
|
||||||
|
testWalkMaybeFailsWith<FileSystemLoopException>(aFile, setOf(aFile))
|
||||||
|
testWalkMaybeFailsWith<FileSystemLoopException>(aDir, setOf(aFile))
|
||||||
|
|
||||||
|
// Fails in jvm8, succeeds in jvm9+
|
||||||
|
val zipRootTarget = root.resolve("UnzipArchive3")
|
||||||
|
testCopyMaybeFailsWith<FileSystemLoopException>(zipRoot, zipRootTarget, zipRootTarget.resolve("", "a"))
|
||||||
|
val aFileTarget = root.resolve("UnzipArchive3-aFile")
|
||||||
|
testCopyMaybeFailsWith<FileSystemLoopException>(aFile, aFileTarget, setOf(aFileTarget))
|
||||||
|
val aDirTarget = root.resolve("UnzipArchive3-aDir")
|
||||||
|
// Fails with jdk8 "IllegalFileNameException: Copying files to outside the specified target directory is prohibited."
|
||||||
|
testCopyMaybeFailsWith<IllegalFileNameException>(aDir, aDirTarget, setOf(aDirTarget))
|
||||||
|
|
||||||
|
// Throws FileSystemLoopException in jvm8
|
||||||
|
// Path.deleteIfExists on the root directory of the archive throws NullPointerException in jvm9+
|
||||||
|
assertFails { zipRoot.deleteRecursively() }
|
||||||
|
// Fails in jvm8, succeeds in jvm9+
|
||||||
|
testDeleteMaybeFailsWith<FileSystemLoopException>(aFile)
|
||||||
|
testDeleteMaybeFailsWith<FileSystemLoopException>(aDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun copyOutsideTargetIsProhibited() {
|
||||||
|
withZip("Archive.zip", listOf("a", "a//")) { root, zipRoot ->
|
||||||
|
val aDir = zipRoot.resolve("a/")
|
||||||
|
testWalkSucceeds(aDir, zipRoot.resolve("a/", "a"))
|
||||||
|
|
||||||
|
val aDirTarget = root.resolve("UnzipArchive-aDir")
|
||||||
|
// Fails with jdk8 "IllegalFileNameException: Copying files to outside the specified target directory is prohibited."
|
||||||
|
testCopyMaybeFailsWith<IllegalFileNameException>(aDir, aDirTarget, setOf(aDirTarget))
|
||||||
|
// No file is copied outside the target
|
||||||
|
testWalkSucceeds(root, root.resolve("", "Archive.zip", "UnzipArchive-aDir"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteZipRootDirectory() {
|
||||||
|
withZip("Archive.zip", emptyList()) { _, zipRoot ->
|
||||||
|
// Deleting the root directory of a zip archive throws NullPointerException.
|
||||||
|
assertFailsWith<NullPointerException> {
|
||||||
|
zipRoot.deleteIfExists()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KT-63103
|
||||||
|
@Test
|
||||||
|
fun zipDoubleDotsFileName() {
|
||||||
|
withZip("Archive1.zip", listOf("normal", "../sneaky")) { root, zipRoot ->
|
||||||
|
root.resolve("sneaky").createFile().also { it.writeText("outer sneaky") }
|
||||||
|
testWalkFailsWithIllegalFileName(zipRoot)
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive1")
|
||||||
|
testCopyFailsWithIllegalFileName(zipRoot, target)
|
||||||
|
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(zipRoot)
|
||||||
|
}
|
||||||
|
withZip("Archive2.zip", listOf("normal", "../normal")) { root, zipRoot ->
|
||||||
|
testWalkFailsWithIllegalFileName(zipRoot)
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive2")
|
||||||
|
testCopyFailsWithIllegalFileName(zipRoot, target)
|
||||||
|
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(zipRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive3.zip", listOf("normal", "../")) { root, zipRoot ->
|
||||||
|
testWalkFailsWithIllegalFileName(zipRoot)
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive3")
|
||||||
|
testCopyFailsWithIllegalFileName(zipRoot, target)
|
||||||
|
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(zipRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive4.zip", listOf("normal", "..")) { root, zipRoot ->
|
||||||
|
testWalkFailsWithIllegalFileName(zipRoot)
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive4")
|
||||||
|
testCopyFailsWithIllegalFileName(zipRoot, target)
|
||||||
|
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(zipRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive5.zip", listOf("normal", "../..")) { root, zipRoot ->
|
||||||
|
testWalkFailsWithIllegalFileName(zipRoot)
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive5")
|
||||||
|
testCopyFailsWithIllegalFileName(zipRoot, target)
|
||||||
|
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(zipRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive6.zip", listOf("normal", "../")) { root, zipRoot ->
|
||||||
|
val targetParent = root.resolve("UnzipArchive6Parent").createDirectory()
|
||||||
|
val targetSibling = targetParent.resolve("UnzipArchive6Sibling").createFile()
|
||||||
|
val target = targetParent.resolve("UnzipArchive6").createDirectory()
|
||||||
|
assertFailsWith<IllegalFileNameException> {
|
||||||
|
zipRoot.copyToRecursively(target, followLinks = false) { src, dst ->
|
||||||
|
if (dst != target) {
|
||||||
|
dst.deleteRecursively()
|
||||||
|
src.copyTo(dst)
|
||||||
|
}
|
||||||
|
CopyActionResult.CONTINUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(target.exists())
|
||||||
|
assertTrue(targetSibling.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive7.zip", listOf("normal", "..a..b..")) { root, zipRoot ->
|
||||||
|
testWalkSucceeds(zipRoot, zipRoot.resolve("", "normal", "..a..b.."))
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive7")
|
||||||
|
val unix = target.resolve("", "normal", "..a..b..") // In Linux and macOS
|
||||||
|
val windows = target.resolve("", "normal", "..a..b") // In Windows
|
||||||
|
testCopySucceeds(zipRoot, target, unix, windows)
|
||||||
|
|
||||||
|
// Path.deleteIfExists on the root directory of the archive throws NullPointerException
|
||||||
|
testDeleteFailsWith<NullPointerException>(zipRoot)
|
||||||
|
assertEquals(emptyList(), zipRoot.listDirectoryEntries())
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive8.zip", listOf("b", "a/", "a/../b")) { root, zipRoot ->
|
||||||
|
val a = zipRoot.resolve("a")
|
||||||
|
|
||||||
|
testWalkFailsWithIllegalFileName(a)
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive8")
|
||||||
|
testCopyFailsWithIllegalFileName(a, target)
|
||||||
|
|
||||||
|
testDeleteFailsWith<IllegalFileNameException>(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
withZip("Archive9.zip", listOf("b/", "b/d", "a/", "a/../b/c")) { root, zipRoot ->
|
||||||
|
val b = zipRoot.resolve("a/../b")
|
||||||
|
// Traverses the "b" directory outside "a"
|
||||||
|
val jvm8 = zipRoot.resolve("a/../b", "b/d")
|
||||||
|
val jvm11 = zipRoot.resolve("a/../b", "a/../b/d")
|
||||||
|
testWalkSucceeds(b, jvm8, jvm11)
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive9")
|
||||||
|
// Copied content of the "b" directory that is outside "a"
|
||||||
|
testCopySucceeds(b, target, target.resolve("", "d"))
|
||||||
|
|
||||||
|
testDeleteSucceeds(b)
|
||||||
|
// The deleted "/a/../b" path actually deleted the "b" outside "a"
|
||||||
|
assertNull(zipRoot.listDirectoryEntries().find { it.name == "b" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun zipWindowsPathSeparators() {
|
||||||
|
// When creating a zip archive, entries are added with the exact given names.
|
||||||
|
testOnJvm8 {
|
||||||
|
// JDK8 converts backslashes to slashes when reading entries, but later can't find those entries
|
||||||
|
withZip("Archive1.zip", listOf("b\\", "b\\d", "a\\")) { root, zipRoot ->
|
||||||
|
assertFailsWith<NoSuchFileException> {
|
||||||
|
zipRoot.walkIncludeDirectories().toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is no directory with name "a", thus empty walk sequence
|
||||||
|
testWalkSucceeds(zipRoot.resolve("a"), emptySet())
|
||||||
|
|
||||||
|
assertFailsWith<NoSuchFileException> {
|
||||||
|
val target = root.resolve("UnzipArchive1")
|
||||||
|
zipRoot.copyToRecursively(target, followLinks = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFailsWith<FileSystemException> {
|
||||||
|
zipRoot.deleteRecursively()
|
||||||
|
}.also { exception ->
|
||||||
|
exception.suppressed.forEach {
|
||||||
|
assertIs<NoSuchFileException>(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testOnJvm9AndAbove {
|
||||||
|
// JDK9+ treats backslashes as part of entry name
|
||||||
|
withZip("Archive1.zip", listOf("b\\", "b\\d", "a\\", "a\\..\\b\\c")) { root, zipRoot ->
|
||||||
|
val expectedWalk = listOf("", "b\\", "b\\d", "a\\", "a\\..\\b\\c").map { "/$it" }.toSet()
|
||||||
|
val walk = zipRoot.walkIncludeDirectories().map { it.toString() }.toSet()
|
||||||
|
assertEquals(expectedWalk, walk)
|
||||||
|
|
||||||
|
// There is no directory with name "a", thus empty walk sequence
|
||||||
|
testWalkSucceeds(zipRoot.resolve("a"), emptySet())
|
||||||
|
|
||||||
|
val target = root.resolve("UnzipArchive1")
|
||||||
|
// Fails in Windows
|
||||||
|
testCopyMaybeFailsWith<NoSuchFileException>(zipRoot, target, target.resolve("", "b\\", "b\\d", "a\\", "a\\..\\b\\c"))
|
||||||
|
|
||||||
|
// Deleting a zip root throws NPE
|
||||||
|
testDeleteFailsWith<NullPointerException>(zipRoot)
|
||||||
|
// All entries inside are deleted
|
||||||
|
testWalkSucceeds(zipRoot, setOf(zipRoot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun copyIllegalFileNameExceptionPassedToOnError() {
|
||||||
|
withZip("Archive1.zip", listOf("normal", "..")) { root, zipRoot ->
|
||||||
|
val target = root.resolve("UnzipArchive1")
|
||||||
|
var failed = false
|
||||||
|
zipRoot.copyToRecursively(target, followLinks = false, onError = { _, _, exception ->
|
||||||
|
failed = true
|
||||||
|
assertIs<IllegalFileNameException>(exception)
|
||||||
|
OnErrorResult.SKIP_SUBTREE
|
||||||
|
})
|
||||||
|
assertTrue(failed)
|
||||||
|
}
|
||||||
|
withZip("Archive2.zip", listOf("normal", "/")) { root, zipRoot ->
|
||||||
|
val target = root.resolve("UnzipArchive2")
|
||||||
|
// FileSystemLoopException in jvm8-10, no exception in jvm11
|
||||||
|
zipRoot.copyToRecursively(target, followLinks = false, onError = { _, _, exception ->
|
||||||
|
assertIs<FileSystemLoopException>(exception)
|
||||||
|
OnErrorResult.SKIP_SUBTREE
|
||||||
|
})
|
||||||
|
}
|
||||||
|
withZip("Archive3.zip", listOf("normal", ".")) { root, zipRoot ->
|
||||||
|
val target = root.resolve("UnzipArchive3")
|
||||||
|
var failed = false
|
||||||
|
zipRoot.copyToRecursively(target, followLinks = false, onError = { _, _, exception ->
|
||||||
|
failed = true
|
||||||
|
assertIs<IllegalFileNameException>(exception)
|
||||||
|
OnErrorResult.SKIP_SUBTREE
|
||||||
|
})
|
||||||
|
assertTrue(failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To demonstrate how recursive functions behave when the traversal starting path ends with "." or ".."
|
||||||
|
// in the default file system, not inside a zip.
|
||||||
|
@Test
|
||||||
|
fun legalDirectorySymbols() {
|
||||||
|
createTestFiles().cleanupRecursively().let { root ->
|
||||||
|
val path = root.resolve("1/3/.")
|
||||||
|
|
||||||
|
testWalkSucceeds(path, path.resolve("", "4.txt", "5.txt"))
|
||||||
|
|
||||||
|
val target = createTempDirectory().cleanupRecursively().resolve("target")
|
||||||
|
testCopySucceeds(path, target, target.resolve("", "4.txt", "5.txt"))
|
||||||
|
|
||||||
|
// Fails in Linux and macOS, succeeds in Windows
|
||||||
|
// deleteIfExists/deleteDirectory() throws FileSystemException: /1/3/.: Invalid argument
|
||||||
|
val deleteSucceeded = testDeleteMaybeFailsWith<FileSystemException>(path)
|
||||||
|
if (deleteSucceeded) {
|
||||||
|
// Only the "1/3" directory and its content is deleted
|
||||||
|
val expectedRootContent = setOf(root) + referenceFilenames.filter { !it.contains("3") }.map { root.resolve(it) }
|
||||||
|
testWalkSucceeds(root, expectedRootContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTestFiles().cleanupRecursively().let { root ->
|
||||||
|
val path = root.resolve("1/3/4.txt/.")
|
||||||
|
|
||||||
|
val unix = emptySet<Path>() // In Linux and macOS
|
||||||
|
val windows = setOf(path) // In Windows
|
||||||
|
testWalkSucceeds(path, unix, windows)
|
||||||
|
|
||||||
|
val target = createTempDirectory().cleanupRecursively().resolve("target")
|
||||||
|
// Copy fails in Linux and macOS, succeeds in Windows
|
||||||
|
// Path.copyToRecursively throws NoSuchFileException: /1/3/4.txt/.: The source file doesn't exist.
|
||||||
|
testCopyMaybeFailsWith<NoSuchFileException>(path, target, setOf(target))
|
||||||
|
|
||||||
|
// Delete fails in Linux and macOS, succeeds in Windows
|
||||||
|
// Path.deleteIfExists() throws FileSystemException: /1/3/4.txt/.: Not a directory
|
||||||
|
val deleteSucceeded = testDeleteMaybeFailsWith<FileSystemException>(path)
|
||||||
|
if (deleteSucceeded) {
|
||||||
|
// Only the "1/3/4.txt" file is deleted
|
||||||
|
val expectedRootContent = setOf(root) + referenceFilenames.filter { !it.contains("4.txt") }.map { root.resolve(it) }
|
||||||
|
testWalkSucceeds(root, expectedRootContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTestFiles().cleanupRecursively().let { root ->
|
||||||
|
val path = root.resolve("1/3/..")
|
||||||
|
|
||||||
|
testWalkSucceeds(path, path.resolve("", "2", "3", "3/4.txt", "3/5.txt"))
|
||||||
|
|
||||||
|
val target = createTempDirectory().cleanupRecursively().resolve("target")
|
||||||
|
testCopySucceeds(path, target, target.resolve("", "2", "3", "3/4.txt", "3/5.txt"))
|
||||||
|
|
||||||
|
// Fails in macOS and Linux, succeeds in Windows
|
||||||
|
// In macOS Path.isSameFileAs() throws NoSuchFileException: /1/3/../2
|
||||||
|
// In Linux deleteDirectory() throws DirectoryNotEmptyException wrapped into FileSystemException: /1/3/..
|
||||||
|
val deleteSucceeded = testDeleteMaybeFailsWith<FileSystemException>(path)
|
||||||
|
if (deleteSucceeded) {
|
||||||
|
// Only the "1" directory and its content is deleted
|
||||||
|
val expectedRootContent = setOf(root) + referenceFilenames.filter { !it.contains("1") }.map { root.resolve(it) }
|
||||||
|
testWalkSucceeds(root, expectedRootContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTestFiles().cleanupRecursively().let { root ->
|
||||||
|
val path = root.resolve("1/3/4.txt/..")
|
||||||
|
|
||||||
|
val unix = emptySet<Path>() // In Linux and macOS
|
||||||
|
val windows = path.resolve("", "4.txt", "5.txt") // In Windows
|
||||||
|
testWalkSucceeds(path, unix, windows)
|
||||||
|
|
||||||
|
val target = createTempDirectory().cleanupRecursively().resolve("target")
|
||||||
|
// Copy fails in Linux and macOS, succeeds in Windows
|
||||||
|
// Path.copyToRecursively throws NoSuchFileException: /1/3/4.txt/..: The source file doesn't exist.
|
||||||
|
testCopyMaybeFailsWith<NoSuchFileException>(path, target, setOf(target, target.resolve("4.txt"), target.resolve("5.txt")))
|
||||||
|
|
||||||
|
// Delete fails in Linux and macOS, succeeds in Windows
|
||||||
|
// Path.deleteIfExists() throws FileSystemException: /1/3/4.txt/..: Not a directory
|
||||||
|
val deleteSucceeded = testDeleteMaybeFailsWith<FileSystemException>(path)
|
||||||
|
if (deleteSucceeded) {
|
||||||
|
// Only the "1/3" directory and its content is deleted
|
||||||
|
val expectedRootContent = setOf(root) + referenceFilenames.filter { !it.contains("3") }.map { root.resolve(it) }
|
||||||
|
testWalkSucceeds(root, expectedRootContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,21 @@ package test
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
private val isJava8 = System.getProperty("java.version").startsWith("1.8.")
|
||||||
|
|
||||||
|
internal fun testOnJvm8(f: () -> Unit) {
|
||||||
|
if (isJava8) {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun testOnJvm9AndAbove(f: () -> Unit) {
|
||||||
|
if (!isJava8) {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public actual fun assertTypeEquals(expected: Any?, actual: Any?) {
|
public actual fun assertTypeEquals(expected: Any?, actual: Any?) {
|
||||||
assertEquals(expected?.javaClass, actual?.javaClass)
|
assertEquals(expected?.javaClass, actual?.javaClass)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user