From 162b152133835f06dfd33326f44b14ebd1e163dd Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Fri, 4 Dec 2015 13:53:16 +0300 Subject: [PATCH] Improve File.relativeTo handling relative paths. Paths are normalized before calculating relative path. "x".relativeTo("../y") now is considered to have incompatible roots. --- libraries/stdlib/src/kotlin/io/files/Utils.kt | 91 ++++++++++++------- libraries/stdlib/test/io/Files.kt | 33 ++++--- 2 files changed, 81 insertions(+), 43 deletions(-) diff --git a/libraries/stdlib/src/kotlin/io/files/Utils.kt b/libraries/stdlib/src/kotlin/io/files/Utils.kt index 9db45580fb4..7f757f9459c 100644 --- a/libraries/stdlib/src/kotlin/io/files/Utils.kt +++ b/libraries/stdlib/src/kotlin/io/files/Utils.kt @@ -120,31 +120,55 @@ public val File.nameWithoutExtension: String * * @throws IllegalArgumentException if child and parent have different roots. */ -public fun File.relativeTo(base: File): String { +public fun File.relativeTo(base: File): String + = relativeToOrNull(base) ?: throw IllegalArgumentException("this and base files have different roots: $this and $base") + +// TODO +//private fun File.relativeToOrPath(base: File): String +// = relativeToOrNull(base) ?: toString() + +private fun File.relativeToOrNull(base: File): String? { // Check roots - val components = filePathComponents() - val baseComponents = base.filePathComponents() - if (components.rootName != baseComponents.rootName) - throw IllegalArgumentException("this and base files have different roots: ${components.rootName} and ${baseComponents.rootName}") - var i = 0 - while (i < components.size() && i < baseComponents.size() && components.fileList[i] == baseComponents.fileList[i]) - i++ - val sameCount = i - val baseCount = baseComponents.size() - // Add all .. - val res = StringBuilder() - for (j in sameCount..baseCount - 2) - res.append("..").append(File.separator) - // If .. is the last element, no separator should present - if (baseCount > sameCount) { - res.append(if (sameCount < components.size()) ".." + File.separator else "..") + val thisComponents = this.filePathComponents().normalize() + val baseComponents = base.filePathComponents().normalize() + if (thisComponents.rootName != baseComponents.rootName) { + return null } + + val baseCount = baseComponents.size() + val thisCount = thisComponents.size() + + val sameCount = run countSame@ { + var i = 0 + val maxSameCount = Math.min(thisCount, baseCount) + while (i < maxSameCount && thisComponents.fileList[i] == baseComponents.fileList[i]) + i++ + return@countSame i + } + + // Annihilate differing base components by adding required number of .. parts + val res = StringBuilder() + for (i in baseCount - 1 downTo sameCount) { + if (baseComponents.fileList[i].name == "..") { + return null + } + + res.append("..") + + if (i != sameCount) { + res.append(File.separatorChar) + } + } + // Add remaining this components - if (sameCount < components.size() - 1) - res.append(components.subPath(sameCount, components.size() - 1)).append(File.separator) - // The last one should be without separator - if (sameCount < components.size()) - res.append(components.subPath(components.size() - 1, components.size())) + if (sameCount < thisCount) { + // If some .. were appended + if (sameCount < baseCount) + res.append(File.separatorChar) + + res.append(thisComponents.subPath(sameCount, thisCount)) + } + return res.toString() } @@ -359,19 +383,22 @@ public fun File.endsWith(other: String): Boolean = endsWith(File(other)) * * @return normalized pathname with . and possibly .. removed. */ -public fun File.normalize(): File { - val components = filePathComponents() - val rootName = components.rootName - val list: MutableList = ArrayList() - for (file in components.fileList) { - val name = file.toString() - when (name) { +public fun File.normalize(): File + = filePathComponents().let { File(it.fileList.normalize().joinToString(File.separator, it.rootName)) } + +private fun FilePathComponents.normalize(): FilePathComponents + = FilePathComponents(rootName, fileList.normalize()) + +private fun List.normalize(): List { + val list: MutableList = ArrayList(this.size) + for (file in this) { + when (file.name) { "." -> {} - ".." -> if (!list.isEmpty() && list.get(list.size - 1) != "..") list.removeAt(list.size - 1) else list.add(name) - else -> list.add(name) + ".." -> if (!list.isEmpty() && list.last().name != "..") list.removeAt(list.size - 1) else list.add(file) + else -> list.add(file) } } - return File(list.joinToString(File.separator, rootName)) + return list } /** diff --git a/libraries/stdlib/test/io/Files.kt b/libraries/stdlib/test/io/Files.kt index 344c087fd08..586ddc3b1e6 100644 --- a/libraries/stdlib/test/io/Files.kt +++ b/libraries/stdlib/test/io/Files.kt @@ -113,23 +113,29 @@ class FilesTest { assertEquals("bar", nested.relativeTo(base)) assertEquals("..", base.relativeTo(nested)) - assertEquals("", base.relativeTo(base)) - assertEquals("", nested.relativeTo(nested)) val empty = File("") val current = File(".") val parent = File("..") val outOfRoot = File("../bar") - assertEquals("../bar".separatorsToSystem(), outOfRoot.relativeTo(empty)) + assertEquals(File("../bar"), File(outOfRoot.relativeTo(empty))) + assertEquals(File("../../bar"), File(outOfRoot.relativeTo(base))) + assertEquals("bar", outOfRoot.relativeTo(parent)) + assertEquals("..", parent.relativeTo(outOfRoot)) val root = File("/root") val files = listOf(nested, base, empty, outOfRoot, current, parent) - for (file1 in files) { - for (file2 in files) { - val rootedFile1 = root.resolve(file1) - val rootedFile2 = root.resolve(file2) - assertEquals(file1.relativeTo(file2), rootedFile1.relativeTo(rootedFile2), "nested: $file1, base: $file2") + val bases = listOf(nested, base, empty, current) + + for (file in files) + assertEquals("", file.relativeTo(file), "file should have empty path relative to itself: $file") + + for (file in files) { + for (base in bases) { + val rootedFile = root.resolve(file) + val rootedBase = root.resolve(base) + assertEquals(file.relativeTo(base), rootedFile.relativeTo(rootedBase), "nested: $file, base: $base") } } } @@ -140,14 +146,19 @@ class FilesTest { val networkShare1 = File("""\\my.host\share1/folder""") val networkShare2 = File("""\\my.host\share2/folder""") + fun assertFailsRelativeTo(file: File, base: File) { + val e = assertFailsWith("file: $file, base: $base") { file.relativeTo(base) } + println(e.message) + } + val allFiles = listOf(absolute, relative, networkShare1, networkShare2) for (file in allFiles) { for (base in allFiles) { - if (file != base) { - assertFailsWith("file: $file, base: $base") { file.relativeTo(base) } - } + if (file != base) assertFailsRelativeTo(file, base) } } + + assertFailsRelativeTo(File("y"), File("../x")) } @test fun relativeTo() {