Skip to content

Commit df1546a

Browse files
committed
Don't normalize paths in Path#parent
Calling java.nio.file.Path#normalize may result in resolving to a different path than intended, as in the case where the given path contains a `..` element and the preceding name is symbolic link.
1 parent f2937c3 commit df1546a

File tree

2 files changed

+64
-24
lines changed

2 files changed

+64
-24
lines changed

compiler/src/dotty/tools/io/Path.scala

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,33 @@ class Path private[io] (val jpath: JPath) {
126126
/**
127127
* @return The path of the parent directory, or root if path is already root
128128
*/
129-
def parent: Directory = jpath.normalize.getParent match {
130-
case null =>
131-
if (isAbsolute) // it should be a root
132-
toDirectory
133-
else if (segments.nonEmpty && segments.last == "..")
134-
(this / "..").toDirectory
135-
else path match {
136-
case "" | "." => Directory("..")
137-
case _ => Directory(".")
138-
}
139-
case parent => Directory(parent)
129+
def parent: Directory = {
130+
// We don't call JPath#normalize here because it may result in resolving
131+
// to a different path than intended, such as when the given path contains
132+
// a `..` component and the preceding name is a symbolic link.
133+
// https://docs.oracle.com/javase/8/docs/api/java/nio/file/Path.html#normalize--
134+
//
135+
// Paths ending with `..` or `.` are handled specially here as
136+
// JPath#getParent wants to simply strip away that last element.
137+
// For the `..` case, we should take care not to fall into the above trap
138+
// as the preceding name may be a symbolic link. This leaves little choice
139+
// (since we don't want to access the file system) but to return the given
140+
// path with `..` appended. By contrast, a `.` as the final path element
141+
// is always redundant and should be removed before computing the parent.
142+
if path.isEmpty then
143+
Directory("..")
144+
else if jpath.endsWith("..") then
145+
(this / "..").toDirectory
146+
else if jpath.endsWith(".") then
147+
jpath.getParent match // strip the trailing `.` element
148+
case null => Directory("..")
149+
case p => new Path(p).parent
150+
else jpath.getParent match
151+
case null =>
152+
if isAbsolute then toDirectory // it should be a root
153+
else Directory(".")
154+
case x =>
155+
Directory(x)
140156
}
141157
def parents: List[Directory] = {
142158
val p = parent
Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,49 @@
11
package dotty.tools.io
22

3-
import org.junit.Assert._
43
import org.junit.Test
54

65
class PathTest {
76
// Ref https://github.com/lampepfl/dotty/issues/11644#issuecomment-792457275
87
@Test def parent(): Unit = {
9-
assertEquals(Directory(".."), Path("").parent)
10-
assertEquals(Directory(".."), Path(".").parent)
11-
assertEquals(Directory("..") / "..", Path("..").parent)
12-
assertEquals(Directory("."), Path("foo.txt").parent)
13-
assertEquals(Directory("."), (Path(".") / "foo.txt").parent)
14-
assertEquals(Directory("."), (Path("bar") / ".").parent)
15-
assertEquals(Directory("."), (Path("foo") / ".." / "." / "bar").parent)
16-
assertEquals(Directory("baz") / "bar", (Path(".") / "baz" / "bar" / "foo.txt").parent)
8+
testParent(Path(""), Directory(".."))
9+
testParent(Path("."), Directory(".."))
10+
testParent(Path(".") / ".", Directory(".."))
11+
testParent(Path(".."), Directory("..") / "..")
12+
testParent(Path("..") / ".", Directory("..") / "..")
13+
testParent(Path("..") / "..", Directory("..") / ".." / "..")
14+
testParent(Path(".") / "..",
15+
Directory("..") / "..",
16+
Directory(".") / ".." / "..")
17+
18+
testParent(Path("foo") / ".", Directory("."))
19+
testParent(Path("foo") / ".." / "bar", Directory("foo") / "..")
20+
testParent(Path("foo") / ".." / "." / "bar", Directory("foo") / ".." / ".")
21+
22+
testParent(Path("foo.txt"), Directory("."))
23+
testParent(Path(".") / "foo.txt", Directory("."))
24+
testParent(Path(".") / "baz" / "bar" / "foo.txt",
25+
Directory(".") / "baz" / "bar",
26+
Directory("baz") / "bar")
1727

1828
for (root <- Path.roots) {
19-
val rootDir = Directory(root.path)
20-
assertEquals(rootDir, root.parent)
21-
assertEquals(rootDir, (root / "foo.txt").parent)
22-
assertEquals(rootDir / "baz" / "bar", (root / "baz" / "bar" / "foo.txt").parent)
29+
testParent(root, root)
30+
testParent(root / ".", root)
31+
testParent(root / "..", root / ".." / "..")
32+
testParent(root / "foo" / ".", root)
33+
testParent(root / "foo.txt", root)
34+
testParent(root / "baz" / "bar" / "foo.txt", root / "baz" / "bar")
35+
testParent(root / "foo" / "bar" / "..", root / "foo" / "bar" / ".." / "..")
2336
}
2437
}
38+
39+
/** The parent of a path may have multiple valid non-canonical representations.
40+
* Here we test that the parent of the specified path is among a curated list
41+
* of representations we consider to be valid.
42+
*/
43+
private def testParent(path: Path, expected: Path*): Unit = {
44+
val actual = path.parent
45+
val some = if (expected.length > 1) " one of" else ""
46+
assert(expected.contains(actual),
47+
s"""expected$some: ${expected.mkString("<",">, <",">")} but was: <$actual>""")
48+
}
2549
}

0 commit comments

Comments
 (0)