Skip to content

Commit 3f11d83

Browse files
Merge pull request github#14500 from joefarebrother/shared-filepath-normalize
Shared: Add library for filepath normalization
2 parents e301223 + aa418dc commit 3f11d83

File tree

4 files changed

+116
-0
lines changed

4 files changed

+116
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
| | . |
2+
| ./a/b/c/../d | a/b/d |
3+
| / | / |
4+
| /a/b/../c | /a/c |
5+
| /a/b/c/../../d/ | /a/d |
6+
| a/.. | . |
7+
| a/b/../c | a/c |
8+
| a/b//////c/./d/../e//d// | a/b/c/e/d |
9+
| a/b/c | a/b/c |
10+
| a/b/c/../../../../d/e/../f | ../d/f |
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import codeql.util.FilePath
2+
3+
class FilepathTest extends NormalizableFilepath {
4+
FilepathTest() {
5+
this =
6+
[
7+
"a/b/c", "a/b/../c", "a/..", "/a/b/../c", "a/b/c/../../../../d/e/../f", "", "/",
8+
"./a/b/c/../d", "a/b//////c/./d/../e//d//", "/a/b/c/../../d/"
9+
]
10+
}
11+
}
12+
13+
from FilepathTest s
14+
select s, s.getNormalizedPath()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: feature
3+
---
4+
* Added `FilePath` API for normalizing filepaths.

shared/util/codeql/util/FilePath.qll

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/** Provides a utility for normalizing filepaths. */
2+
3+
/**
4+
* A filepath that should be normalized.
5+
*
6+
* Extend to provide additional strings that should be normalized as filepaths.
7+
*/
8+
abstract class NormalizableFilepath extends string {
9+
bindingset[this]
10+
NormalizableFilepath() { any() }
11+
12+
/** Gets the `i`th path component of this string. */
13+
private string getComponent(int i) { result = this.splitAt("/", i) }
14+
15+
/** Gets the number of path components of thi string. */
16+
private int getNumComponents() { result = strictcount(int i | exists(this.getComponent(i))) }
17+
18+
/** In the normalized path starting from component `i`, counts the number of `..` segments that path starts with. */
19+
private int dotdotCountFrom(int i) {
20+
result = 0 and i = this.getNumComponents()
21+
or
22+
exists(string c | c = this.getComponent(i) |
23+
if c = ""
24+
then result = this.dotdotCountFrom(i + 1)
25+
else
26+
if c = "."
27+
then result = this.dotdotCountFrom(i + 1)
28+
else
29+
if c = ".."
30+
then result = this.dotdotCountFrom(i + 1) + 1
31+
else result = (this.dotdotCountFrom(i + 1) - 1).maximum(0)
32+
)
33+
}
34+
35+
/** In the normalized path up to (excluding) component `i`, counts the number of non-`..` segments that path ends with. */
36+
private int segmentCountUntil(int i) {
37+
result = 0 and i = 0
38+
or
39+
exists(string c | c = this.getComponent(i - 1) |
40+
if c = ""
41+
then result = this.segmentCountUntil(i - 1)
42+
else
43+
if c = "."
44+
then result = this.segmentCountUntil(i - 1)
45+
else
46+
if c = ".."
47+
then result = (this.segmentCountUntil(i - 1) - 1).maximum(0)
48+
else result = this.segmentCountUntil(i - 1) + 1
49+
)
50+
}
51+
52+
/** Gets the `i`th component if that component should be included in the normalized path. */
53+
private string part(int i) {
54+
result = this.getComponent(i) and
55+
result != "." and
56+
if result = ""
57+
then i = 0
58+
else (
59+
result != ".." and
60+
0 = this.dotdotCountFrom(i + 1)
61+
or
62+
result = ".." and
63+
0 = this.segmentCountUntil(i)
64+
)
65+
}
66+
67+
/**
68+
* Gets the normalized filepath for this string.
69+
*
70+
* Normalizes `..` paths, `.` paths, and multiple `/`s as much as possible, but does not normalize case, resolve symlinks, or make relative paths absolute.
71+
*
72+
* The normalized path will be absolute (begin with `/`) if and only if the original path is.
73+
*
74+
* The normalized path will not have a trailing `/`.
75+
*
76+
* Only `/` is treated as a path separator.
77+
*/
78+
string getNormalizedPath() {
79+
exists(string norm | norm = concat(string s, int i | s = this.part(i) | s, "/" order by i) |
80+
if norm != ""
81+
then result = norm
82+
else
83+
if this.matches("/%")
84+
then result = "/"
85+
else result = "."
86+
)
87+
}
88+
}

0 commit comments

Comments
 (0)