diff --git a/csharp/ql/test/library-tests/utils/FilepathNormalizeTest.expected b/csharp/ql/test/library-tests/utils/FilepathNormalizeTest.expected new file mode 100644 index 00000000000..0a43c01b186 --- /dev/null +++ b/csharp/ql/test/library-tests/utils/FilepathNormalizeTest.expected @@ -0,0 +1,10 @@ +| | . | +| ./a/b/c/../d | a/b/d | +| / | / | +| /a/b/../c | /a/c | +| /a/b/c/../../d/ | /a/d | +| a/.. | . | +| a/b/../c | a/c | +| a/b//////c/./d/../e//d// | a/b/c/e/d | +| a/b/c | a/b/c | +| a/b/c/../../../../d/e/../f | ../d/f | diff --git a/csharp/ql/test/library-tests/utils/FilepathNormalizeTest.ql b/csharp/ql/test/library-tests/utils/FilepathNormalizeTest.ql new file mode 100644 index 00000000000..4d9a2855dc9 --- /dev/null +++ b/csharp/ql/test/library-tests/utils/FilepathNormalizeTest.ql @@ -0,0 +1,14 @@ +import codeql.util.FilePath + +class FilepathTest extends NormalizableFilepath { + FilepathTest() { + this = + [ + "a/b/c", "a/b/../c", "a/..", "/a/b/../c", "a/b/c/../../../../d/e/../f", "", "/", + "./a/b/c/../d", "a/b//////c/./d/../e//d//", "/a/b/c/../../d/" + ] + } +} + +from FilepathTest s +select s, s.getNormalizedPath() diff --git a/shared/util/change-notes/2023-10-13-filepath-normalization.md b/shared/util/change-notes/2023-10-13-filepath-normalization.md new file mode 100644 index 00000000000..3ffdadc85b3 --- /dev/null +++ b/shared/util/change-notes/2023-10-13-filepath-normalization.md @@ -0,0 +1,4 @@ +--- +category: feature +--- +* Added `FilePath` API for normalizing filepaths. \ No newline at end of file diff --git a/shared/util/codeql/util/FilePath.qll b/shared/util/codeql/util/FilePath.qll new file mode 100644 index 00000000000..1b047f3c91c --- /dev/null +++ b/shared/util/codeql/util/FilePath.qll @@ -0,0 +1,88 @@ +/** Provides a utility for normalizing filepaths. */ + +/** + * A filepath that should be normalized. + * + * Extend to provide additional strings that should be normalized as filepaths. + */ +abstract class NormalizableFilepath extends string { + bindingset[this] + NormalizableFilepath() { any() } + + /** Gets the `i`th path component of this string. */ + private string getComponent(int i) { result = this.splitAt("/", i) } + + /** Gets the number of path components of thi string. */ + private int getNumComponents() { result = strictcount(int i | exists(this.getComponent(i))) } + + /** In the normalized path starting from component `i`, counts the number of `..` segments that path starts with. */ + private int dotdotCountFrom(int i) { + result = 0 and i = this.getNumComponents() + or + exists(string c | c = this.getComponent(i) | + if c = "" + then result = this.dotdotCountFrom(i + 1) + else + if c = "." + then result = this.dotdotCountFrom(i + 1) + else + if c = ".." + then result = this.dotdotCountFrom(i + 1) + 1 + else result = (this.dotdotCountFrom(i + 1) - 1).maximum(0) + ) + } + + /** In the normalized path up to (excluding) component `i`, counts the number of non-`..` segments that path ends with. */ + private int segmentCountUntil(int i) { + result = 0 and i = 0 + or + exists(string c | c = this.getComponent(i - 1) | + if c = "" + then result = this.segmentCountUntil(i - 1) + else + if c = "." + then result = this.segmentCountUntil(i - 1) + else + if c = ".." + then result = (this.segmentCountUntil(i - 1) - 1).maximum(0) + else result = this.segmentCountUntil(i - 1) + 1 + ) + } + + /** Gets the `i`th component if that component should be included in the normalized path. */ + private string part(int i) { + result = this.getComponent(i) and + result != "." and + if result = "" + then i = 0 + else ( + result != ".." and + 0 = this.dotdotCountFrom(i + 1) + or + result = ".." and + 0 = this.segmentCountUntil(i) + ) + } + + /** + * Gets the normalized filepath for this string. + * + * Normalizes `..` paths, `.` paths, and multiple `/`s as much as possible, but does not normalize case, resolve symlinks, or make relative paths absolute. + * + * The normalized path will be absolute (begin with `/`) if and only if the original path is. + * + * The normalized path will not have a trailing `/`. + * + * Only `/` is treated as a path separator. + */ + string getNormalizedPath() { + exists(string norm | norm = concat(string s, int i | s = this.part(i) | s, "/" order by i) | + if norm != "" + then result = norm + else + if this.matches("/%") + then result = "/" + else result = "." + ) + } +}