Workaround bugs in Path.relativize

Also add actual paths to the relativeTo error message.

#KT-19192
This commit is contained in:
Ilya Gorbunov
2020-10-06 21:34:02 +03:00
parent b3a87356bd
commit f6d2400208
2 changed files with 105 additions and 53 deletions
@@ -55,7 +55,11 @@ public val Path.nameWithoutExtension: String
*/ */
@SinceKotlin("1.4") @SinceKotlin("1.4")
@ExperimentalStdlibApi @ExperimentalStdlibApi
public fun Path.relativeTo(base: Path): Path = base.relativize(this) public fun Path.relativeTo(base: Path): Path = try {
PathRelativizer.tryRelativeTo(this, base)
} catch (e: IllegalArgumentException) {
throw java.lang.IllegalArgumentException(e.message + "\nthis path: $this\nbase path: $base", e)
}
/** /**
* Calculates the relative path for this path from a [base] path. * Calculates the relative path for this path from a [base] path.
@@ -78,11 +82,38 @@ public fun Path.relativeToOrSelf(base: Path): Path =
*/ */
@SinceKotlin("1.4") @SinceKotlin("1.4")
@ExperimentalStdlibApi @ExperimentalStdlibApi
public fun Path.relativeToOrNull(base: Path): Path? { public fun Path.relativeToOrNull(base: Path): Path? = try {
return try { PathRelativizer.tryRelativeTo(this, base)
base.relativize(this) } catch (e: IllegalArgumentException) {
} catch (e: IllegalArgumentException) { null
null }
internal object PathRelativizer {
private val emptyPath = Paths.get("")
private val parentPath = Paths.get("..")
// Workarounds some bugs in Path.relativize that were fixed only in JDK9
fun tryRelativeTo(path: Path, base: Path): Path {
val bn = base.normalize()
val pn = path.normalize()
val rn = bn.relativize(pn)
// work around https://bugs.openjdk.java.net/browse/JDK-8066943
for (i in 0 until minOf(bn.nameCount, pn.nameCount)) {
if (bn.getName(i) != parentPath) break
if (pn.getName(i) != parentPath) throw IllegalArgumentException("Unable to compute relative path")
}
// work around https://bugs.openjdk.java.net/browse/JDK-8072495
val r = if (pn != bn && bn == emptyPath) {
pn
} else {
val rnString = rn.toString()
// drop invalid dangling separator from path string https://bugs.openjdk.java.net/browse/JDK-8140449
if (rnString.endsWith(rn.fileSystem.separator))
rn.fileSystem.getPath(rnString.dropLast(rn.fileSystem.separator.length))
else
rn
}
return r
} }
} }
@@ -222,48 +222,68 @@ class PathExtensionsTest {
assertFailsWith<NotDirectoryException> { file.forEachDirectoryEntry { } } assertFailsWith<NotDirectoryException> { file.forEachDirectoryEntry { } }
} }
private fun testRelativeTo(expected: String?, path: String, base: String) =
testRelativeTo(expected?.let { Paths.get(it) }, Paths.get(path), Paths.get(base))
private fun testRelativeTo(expected: String, path: Path, base: Path) =
testRelativeTo(Paths.get(expected), path, base)
private fun testRelativeTo(expected: Path?, path: Path, base: Path) {
val context = "path: '$path', base: '$base'"
if (expected != null) {
assertEquals(expected, path.relativeTo(base), context)
} else {
val e = assertFailsWith<IllegalArgumentException>(context) { path.relativeTo(base) }
val message = assertNotNull(e.message)
assertTrue(path.toString() in message, message)
assertTrue(base.toString() in message, message)
}
assertEquals(expected, path.relativeToOrNull(base), context)
assertEquals(expected ?: path, path.relativeToOrSelf(base), context)
}
@Test @Test
fun relativeToRooted() { fun relativeToRooted() {
val file1 = Paths.get("/foo/bar/baz") val file1 = "/foo/bar/baz"
val file2 = Paths.get("/foo/baa/ghoo") val file2 = "/foo/baa/ghoo"
assertEquals("../../bar/baz", file1.relativeTo(file2).invariantSeparatorsPath) testRelativeTo("../../bar/baz", file1, file2)
val file3 = Paths.get("/foo/bar") val file3 = "/foo/bar"
assertEquals("baz", file1.relativeTo(file3).toString()) testRelativeTo("baz", file1, file3)
assertEquals("..", file3.relativeTo(file1).toString()) testRelativeTo("..", file3, file1)
val file4 = Paths.get("/foo/bar/") val file4 = "/foo/bar/"
assertEquals("baz", file1.relativeTo(file4).toString()) testRelativeTo("baz", file1, file4)
assertEquals("..", file4.relativeTo(file1).toString()) testRelativeTo("..", file4, file1)
assertEquals("", file3.relativeTo(file4).toString()) testRelativeTo("", file3, file4)
assertEquals("", file4.relativeTo(file3).toString()) testRelativeTo("", file4, file3)
val file5 = Paths.get("/foo/baran") val file5 = "/foo/baran"
assertEquals("../bar", file3.relativeTo(file5).invariantSeparatorsPath) testRelativeTo("../bar", file3, file5)
assertEquals("../baran", file5.relativeTo(file3).invariantSeparatorsPath) testRelativeTo("../baran", file5, file3)
assertEquals("../bar", file4.relativeTo(file5).invariantSeparatorsPath) testRelativeTo("../bar", file4, file5)
assertEquals("../baran", file5.relativeTo(file4).invariantSeparatorsPath) testRelativeTo("../baran", file5, file4)
if (isBackslashSeparator) { if (isBackslashSeparator) {
val file6 = Paths.get("C:\\Users\\Me") val file6 = "C:\\Users\\Me"
val file7 = Paths.get("C:\\Users\\Me\\Documents") val file7 = "C:\\Users\\Me\\Documents"
assertEquals("..", file6.relativeTo(file7).toString()) testRelativeTo("..", file6, file7)
assertEquals("Documents", file7.relativeTo(file6).toString()) testRelativeTo("Documents", file7, file6)
val file8 = Paths.get("""\\my.host\home/user/documents/vip""") val file8 = """\\my.host\home/user/documents/vip"""
val file9 = Paths.get("""\\my.host\home/other/images/nice""") val file9 = """\\my.host\home/other/images/nice"""
assertEquals("../../../user/documents/vip", file8.relativeTo(file9).invariantSeparatorsPath) testRelativeTo("../../../user/documents/vip", file8, file9)
assertEquals("../../../other/images/nice", file9.relativeTo(file8).invariantSeparatorsPath) testRelativeTo("../../../other/images/nice", file9, file8)
} }
if (isCaseInsensitiveFileSystem) { if (isCaseInsensitiveFileSystem) {
assertEquals("bar", Paths.get("C:/bar").relativeTo(Paths.get("c:/")).toString()) testRelativeTo("bar", "C:/bar", "c:/")
} }
} }
@@ -272,30 +292,33 @@ class PathExtensionsTest {
val nested = Paths.get("foo/bar") val nested = Paths.get("foo/bar")
val base = Paths.get("foo") val base = Paths.get("foo")
assertEquals("bar", nested.relativeTo(base).toString()) testRelativeTo("bar", nested, base)
assertEquals("..", base.relativeTo(nested).toString()) testRelativeTo("..", base, nested)
val empty = Paths.get("")
val current = Paths.get(".") val current = Paths.get(".")
val parent = Paths.get("..") val parent = Paths.get("..")
val outOfRoot = Paths.get("../bar") val outOfRoot = Paths.get("../bar")
assertEquals(Paths.get("../../bar"), outOfRoot.relativeTo(base)) testRelativeTo("../bar", outOfRoot, empty)
assertEquals("bar", outOfRoot.relativeTo(parent).toString()) testRelativeTo("../../bar", outOfRoot, base)
assertEquals("..", parent.relativeTo(outOfRoot).toString()) testRelativeTo("bar", outOfRoot, parent)
testRelativeTo("..", parent, outOfRoot)
val root = Paths.get("/root") val root = Paths.get("/root")
val files = listOf(nested, base, outOfRoot, current, parent) val files = listOf(nested, base, empty, outOfRoot, current, parent)
val bases = listOf(nested, base, current) val bases = listOf(nested, base, empty, current)
for (file in files) for (file in files)
assertEquals("", file.relativeTo(file).toString(), "file should have empty path relative to itself: $file") // file should have empty path relative to itself
testRelativeTo("", file, file)
for (file in files) { for (file in files) {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
for (base in bases) { for (base in bases) {
val rootedFile = root.resolve(file) val rootedFile = root.resolve(file)
val rootedBase = root.resolve(base) val rootedBase = root.resolve(base)
assertEquals(file.relativeTo(base), rootedFile.relativeTo(rootedBase), "nested: $file, base: $base") assertEquals(rootedFile.relativeTo(rootedBase), file.relativeTo(base), "nested: $file, base: $base")
} }
} }
} }
@@ -307,30 +330,28 @@ class PathExtensionsTest {
val networkShare1 = Paths.get("""\\my.host\share1/folder""") val networkShare1 = Paths.get("""\\my.host\share1/folder""")
val networkShare2 = Paths.get("""\\my.host\share2\folder""") val networkShare2 = Paths.get("""\\my.host\share2\folder""")
fun assertFailsRelativeTo(file: Path, base: Path) {
val e = assertFailsWith<IllegalArgumentException>("file: $file, base: $base") { file.relativeTo(base) }
assertNotNull(e.message)
}
val allFiles = listOf(absolute, relative) + if (isBackslashSeparator) listOf(networkShare1, networkShare2) else emptyList() val allFiles = listOf(absolute, relative) + if (isBackslashSeparator) listOf(networkShare1, networkShare2) else emptyList()
for (file in allFiles) { for (file in allFiles) {
for (base in allFiles) { for (base in allFiles) {
if (file != base) assertFailsRelativeTo(file, base) if (file != base) testRelativeTo(null, file, base)
} }
} }
if (isBackslashSeparator) { if (isBackslashSeparator) {
val fileOnC = Paths.get("C:/dir1") testRelativeTo(null, "C:/dir1", "D:/dir2")
val fileOnD = Paths.get("D:/dir2")
assertFailsRelativeTo(fileOnC, fileOnD)
} }
testRelativeTo(null, "foo", "..")
testRelativeTo(null, "../foo", "../..")
} }
@Test @Test
fun relativeTo() { fun relativeTo() {
assertEquals("kotlin", Paths.get("src/kotlin").relativeTo(Paths.get("src")).toString()) testRelativeTo("kotlin", "src/kotlin", "src")
assertEquals("", Paths.get("dir").relativeTo(Paths.get("dir")).toString()) testRelativeTo("", "dir", "dir")
assertEquals("..", Paths.get("dir").relativeTo(Paths.get("dir/subdir")).toString()) testRelativeTo("..", "dir", "dir/subdir")
assertEquals(Paths.get("../../test"), Paths.get("test").relativeTo(Paths.get("dir/dir"))) testRelativeTo("../../test", "test", "dir/dir")
testRelativeTo("foo/bar", "../../foo/bar", "../../sub/../.")
testRelativeTo(null, "../../foo/bar", "../../sub/../..")
} }
} }