diff --git a/lib/utopia/path.rb b/lib/utopia/path.rb index b6422cf..af3a1de 100644 --- a/lib/utopia/path.rb +++ b/lib/utopia/path.rb @@ -215,19 +215,34 @@ def -(other) end def simplify - result = absolute? ? [""] : [] + components = [] - @components.each do |bit| - if bit == ".." - result.pop - elsif bit != "." && bit != "" - result << bit - end + index = 0 + + if @components[0] == "" + components << "" + index += 1 end - result << "" if directory? + while index < @components.size + bit = @components[index] + if bit == "." + # No-op (ignore current directory) + elsif bit == "" && index != @components.size - 1 + # No-op (ignore multiple slashes) + elsif bit == ".." && components.last && components.last != ".." + if components.last != "" + # We can go up one level: + components.pop + end + else + components << bit + end + + index += 1 + end - return self.class.new(result) + return self.class.new(components) end # Returns the first path component. diff --git a/releases.md b/releases.md index 94236dd..47890ec 100644 --- a/releases.md +++ b/releases.md @@ -3,6 +3,7 @@ ## Unreleased - Add agent context. + - Better simplification of relative paths, e.g. `../../foo` is not modified to `foo`. ## v2.30.1 diff --git a/test/utopia/path.rb b/test/utopia/path.rb index d7bdbfd..417f2b8 100755 --- a/test/utopia/path.rb +++ b/test/utopia/path.rb @@ -195,6 +195,7 @@ expect(directory.to_directory).to be == directory end end + it "should start with the given path" do path = Utopia::Path["/a/b/c/d/e"] @@ -263,4 +264,59 @@ expect((output + short).simplify).to be == input end + + with "#simplify" do + it "doesn't remove leading .. from relative paths" do + path = Utopia::Path["../foo/bar"] + simplified = path.simplify + + expect(simplified.components).to be == ["..", "foo", "bar"] + end + + it "removes redundant .. from absolute paths" do + path = Utopia::Path["/foo/../../bar"] + simplified = path.simplify + + expect(simplified.components).to be == ["", "bar"] + end + + it "preserves excess .. for purely relative paths" do + # ../../foo cannot be reduced without a base; keep leading .. components + path = Utopia::Path["../../foo"] + simplified = path.simplify + + expect(simplified.components).to be == ["..", "..", "foo"] + end + + it "reduces relative paths only as far as components allow" do + # foo/../../bar -> ../bar (pop foo for first .., then keep remaining ..) + path = Utopia::Path["foo/../../bar"] + simplified = path.simplify + + expect(simplified.components).to be == ["..", "bar"] + end + + it "does not traverse above root for absolute paths" do + # /a/b/../../../c -> /c (ignore .. beyond root) + path = Utopia::Path["/a/b/../../../c"] + simplified = path.simplify + + expect(simplified.components).to be == ["", "c"] + end + + it "ignores leading .. at absolute root" do + # /../../c -> /c + path = Utopia::Path["/../../c"] + simplified = path.simplify + + expect(simplified.components).to be == ["", "c"] + end + + it "preserves trailing slash for directories after simplify" do + path = Utopia::Path["/a/./b/../c/"] + simplified = path.simplify + + expect(simplified.components).to be == ["", "a", "c", ""] + end + end end