Improve File.relativeTo handling relative paths. Paths are normalized before calculating relative path. "x".relativeTo("../y") now is considered to have incompatible roots.

This commit is contained in:
Ilya Gorbunov
2015-12-04 13:53:16 +03:00
parent 9f5a972816
commit 162b152133
2 changed files with 81 additions and 43 deletions
+59 -32
View File
@@ -120,31 +120,55 @@ public val File.nameWithoutExtension: String
* *
* @throws IllegalArgumentException if child and parent have different roots. * @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 // Check roots
val components = filePathComponents() val thisComponents = this.filePathComponents().normalize()
val baseComponents = base.filePathComponents() val baseComponents = base.filePathComponents().normalize()
if (components.rootName != baseComponents.rootName) if (thisComponents.rootName != baseComponents.rootName) {
throw IllegalArgumentException("this and base files have different roots: ${components.rootName} and ${baseComponents.rootName}") return null
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 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 // Add remaining this components
if (sameCount < components.size() - 1) if (sameCount < thisCount) {
res.append(components.subPath(sameCount, components.size() - 1)).append(File.separator) // If some .. were appended
// The last one should be without separator if (sameCount < baseCount)
if (sameCount < components.size()) res.append(File.separatorChar)
res.append(components.subPath(components.size() - 1, components.size()))
res.append(thisComponents.subPath(sameCount, thisCount))
}
return res.toString() return res.toString()
} }
@@ -359,19 +383,22 @@ public fun File.endsWith(other: String): Boolean = endsWith(File(other))
* *
* @return normalized pathname with . and possibly .. removed. * @return normalized pathname with . and possibly .. removed.
*/ */
public fun File.normalize(): File { public fun File.normalize(): File
val components = filePathComponents() = filePathComponents().let { File(it.fileList.normalize().joinToString(File.separator, it.rootName)) }
val rootName = components.rootName
val list: MutableList<String> = ArrayList() private fun FilePathComponents.normalize(): FilePathComponents
for (file in components.fileList) { = FilePathComponents(rootName, fileList.normalize())
val name = file.toString()
when (name) { private fun List<File>.normalize(): List<File> {
val list: MutableList<File> = 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) ".." -> if (!list.isEmpty() && list.last().name != "..") list.removeAt(list.size - 1) else list.add(file)
else -> list.add(name) else -> list.add(file)
} }
} }
return File(list.joinToString(File.separator, rootName)) return list
} }
/** /**
+22 -11
View File
@@ -113,23 +113,29 @@ class FilesTest {
assertEquals("bar", nested.relativeTo(base)) assertEquals("bar", nested.relativeTo(base))
assertEquals("..", base.relativeTo(nested)) assertEquals("..", base.relativeTo(nested))
assertEquals("", base.relativeTo(base))
assertEquals("", nested.relativeTo(nested))
val empty = File("") val empty = File("")
val current = File(".") val current = File(".")
val parent = File("..") val parent = File("..")
val outOfRoot = File("../bar") 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 root = File("/root")
val files = listOf(nested, base, empty, outOfRoot, current, parent) val files = listOf(nested, base, empty, outOfRoot, current, parent)
for (file1 in files) { val bases = listOf(nested, base, empty, current)
for (file2 in files) {
val rootedFile1 = root.resolve(file1) for (file in files)
val rootedFile2 = root.resolve(file2) assertEquals("", file.relativeTo(file), "file should have empty path relative to itself: $file")
assertEquals(file1.relativeTo(file2), rootedFile1.relativeTo(rootedFile2), "nested: $file1, base: $file2")
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 networkShare1 = File("""\\my.host\share1/folder""")
val networkShare2 = File("""\\my.host\share2/folder""") val networkShare2 = File("""\\my.host\share2/folder""")
fun assertFailsRelativeTo(file: File, base: File) {
val e = assertFailsWith<IllegalArgumentException>("file: $file, base: $base") { file.relativeTo(base) }
println(e.message)
}
val allFiles = listOf(absolute, relative, networkShare1, networkShare2) val allFiles = listOf(absolute, relative, networkShare1, networkShare2)
for (file in allFiles) { for (file in allFiles) {
for (base in allFiles) { for (base in allFiles) {
if (file != base) { if (file != base) assertFailsRelativeTo(file, base)
assertFailsWith<IllegalArgumentException>("file: $file, base: $base") { file.relativeTo(base) }
}
} }
} }
assertFailsRelativeTo(File("y"), File("../x"))
} }
@test fun relativeTo() { @test fun relativeTo() {