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:
@@ -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<String> = 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<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)
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<IllegalArgumentException>("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<IllegalArgumentException>("file: $file, base: $base") { file.relativeTo(base) }
|
||||
}
|
||||
if (file != base) assertFailsRelativeTo(file, base)
|
||||
}
|
||||
}
|
||||
|
||||
assertFailsRelativeTo(File("y"), File("../x"))
|
||||
}
|
||||
|
||||
@test fun relativeTo() {
|
||||
|
||||
Reference in New Issue
Block a user