Skip to content

Commit

Permalink
fix(base): fix resolving multiple ../ and add test (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
pysan3 authored Mar 28, 2024
1 parent 77013ed commit 6fb37e0
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 19 deletions.
45 changes: 26 additions & 19 deletions lua/pathlib/base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ function Path:_init(...)
self:copy_all_from(s)
else
assert(not s:is_absolute(), ("new: invalid root path object in %sth argument: %s"):format(i, s))
if s:peek(1) == ".." then
run_resolve = true
end
self._raw_paths:extend(s._raw_paths)
end
elseif type(s) == "string" then
Expand Down Expand Up @@ -525,11 +528,18 @@ end

---Resolves path. Eliminates `../` representation.
---Changes internal. (See `Path:resolve_copy` to create new object)
function Path:resolve()
---@param allow_abs2rel boolean|nil # Allow absolute path to be converted to relative path when there are too many '../'
function Path:resolve(allow_abs2rel)
local accum, length = 1, self:depth()
for _, value in ipairs(self._raw_paths) do
if value == ".." and accum > 1 then
accum = accum - 1
local negatives, was_absolute = 0, self:is_absolute()
local raw_path = table.concat(self._raw_paths, self.sep_str)
for _, value in ipairs(vim.tbl_values(self._raw_paths)) do
if value == ".." then
if accum > 1 then
accum = accum - 1
else
negatives = negatives + 1
end
else
self._raw_paths[accum] = value
accum = accum + 1
Expand All @@ -538,27 +548,24 @@ function Path:resolve()
for i = accum, length do
self._raw_paths[i] = nil
end
if not allow_abs2rel and was_absolute ~= self:is_absolute() then
errs.assert_function("Path:resolve", function()
return was_absolute == self:is_absolute()
end, string.format("'%s' was absolute but too many ../ included in path to resolve -> %s", raw_path, self))
end
for _ = 1, negatives do
table.insert(self._raw_paths, 1, "..")
end
self.__string_cache = nil
return self
end

---Resolves path. Eliminates `../` representation and returns a new object. `self` is not changed.
---@param allow_abs2rel boolean|nil # Allow absolute path to be converted to relative path when there are too many '../'
---@return PathlibPath
function Path:resolve_copy()
local accum, length, new = 1, self:depth(), self:deep_copy()
for _, value in ipairs(self._raw_paths) do
if value == ".." and accum > 1 then
accum = accum - 1
else
new._raw_paths[accum] = value
accum = accum + 1
end
end
for i = accum, length do
new._raw_paths[i] = nil
end
new.__string_cache = nil
return new
function Path:resolve_copy(allow_abs2rel)
local new = self:deep_copy()
return new:resolve(allow_abs2rel)
end

---Run `vim.fn.globpath` on this path.
Expand Down
184 changes: 184 additions & 0 deletions spec/resolve_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
local _ = require("pathlib")

describe("Resolve paths", function()
local Posix = require("pathlib.posix")
local Windows = require("pathlib.windows")
local utils = require("pathlib.utils")

local abs_posix = Posix("/etc")
local abs_win = Windows("C:/Users")
local rel_posix = Posix("./folder")
local rel_windows = Windows("./folder")

describe("Resolve relative Posix", function()
it("no ../", function()
assert.are_equal(Posix("folder/passwd"), rel_posix / "passwd")
assert.are_equal("folder/passwd", tostring(rel_posix / "passwd"))
end)
it("one ../", function()
assert.are_equal(Posix("var/lib"), rel_posix / "../var/lib")
assert.are_equal(Posix("var/lib"), rel_posix / ".." / "var/lib")
assert.are_equal(Posix("var/lib"), rel_posix.new(rel_posix .. "/../" .. "var/lib"))
assert.are_equal("var/lib", tostring(rel_posix / ".." / "var/lib"))
end)
it("too many ../", function()
assert.are_equal(Posix("../var/lib"), rel_posix / "../../var/lib")
assert.are_equal(Posix("../var/lib"), rel_posix / ".." / ".." / "var/lib")
assert.are_equal(Posix("../var/lib"), rel_posix.new(rel_posix .. "/../../" .. "var/lib"))
assert.are_equal("../var/lib", tostring(rel_posix / "../.." / "var/lib"))
end)
it("overflow ../", function()
assert.are_equal(Posix("../../var/lib"), rel_posix / "../../../var/lib")
assert.are_equal(Posix("../../var/lib"), rel_posix / ".." / ".." / ".." / "var/lib")
assert.are_equal(Posix("../../var/lib"), rel_posix.new(rel_posix .. "/../../../" .. "var/lib"))
assert.are_equal("../../var/lib", tostring(rel_posix / "../../.." / "var/lib"))
end)
end)

describe("Resolve multiple ../", function()
it("", function()
assert.are_equal(Posix("lib"), rel_posix / ".." / "var" / ".." / "lib")
assert.are_equal(Posix("lib"), rel_posix / "../var/../lib")
end)
end)

describe("Resolve relative Windows", function()
it("no ../", function()
assert.are_equal(Windows("folder/passwd"), rel_windows / "passwd")
assert.are_equal([[folder\passwd]], tostring(rel_windows / "passwd"))
end)
it("one ../", function()
assert.are_equal(Windows("var/lib"), rel_windows / "../var/lib")
assert.are_equal(Windows("var/lib"), rel_windows / ".." / "var/lib")
assert.are_equal(Windows("var/lib"), rel_windows.new(rel_windows .. "/../" .. "var/lib"))
assert.are_equal([[var\lib]], tostring(rel_windows / ".." / "var/lib"))
end)
it("too many ../", function()
assert.are_equal(Windows("../var/lib"), rel_windows / "../../var/lib")
assert.are_equal(Windows("../var/lib"), rel_windows / ".." / ".." / "var/lib")
assert.are_equal(Windows("../var/lib"), rel_windows.new(rel_windows .. "/../../" .. "var/lib"))
assert.are_equal([[..\var\lib]], tostring(rel_windows / "../.." / "var/lib"))
end)
it("overflow ../", function()
assert.are_equal(Windows("../../var/lib"), rel_windows / "../../../var/lib")
assert.are_equal(Windows("../../var/lib"), rel_windows / ".." / ".." / ".." / "var/lib")
assert.are_equal(Windows("../../var/lib"), rel_windows.new(rel_windows .. "/../../../" .. "var/lib"))
assert.are_equal([[..\..\var\lib]], tostring(rel_windows / [[../../..]] / [[var/lib]]))
end)
end)

describe("Resolve absolute Posix", function()
it("no ../", function()
assert.are_equal(Posix("/etc/passwd"), abs_posix / "passwd")
assert.are_equal("/etc/passwd", tostring(abs_posix / "passwd"))
end)
it("one ../", function()
assert.are_equal(Posix("/var/lib"), abs_posix / "../var/lib")
assert.are_equal(Posix("/var/lib"), abs_posix / ".." / "var/lib")
assert.are_equal(Posix("/var/lib"), abs_posix.new(abs_posix .. "/../" .. "var/lib"))
assert.are_equal("/var/lib", tostring(abs_posix / ".." / "var/lib"))
end)
it("too many ../", function()
assert.has_error(function()
return abs_posix / "../../var/lib"
end)
assert.has_error(function()
return abs_posix / ".." / ".." / "var/lib"
end)
assert.has_error(function()
return abs_posix.new(abs_posix .. "/../../" .. "var/lib")
end)
assert.has_error(function()
return tostring(abs_posix / "../.." / "var/lib")
end)
end)
it("overflow ../", function()
assert.has_error(function()
return abs_posix / "../../../var/lib"
end)
assert.has_error(function()
return abs_posix / ".." / ".." / ".." / "var/lib"
end)
assert.has_error(function()
return abs_posix.new(abs_posix .. "/../../../" .. "var/lib")
end)
assert.has_error(function()
return tostring(abs_posix / "../../.." / "var/lib")
end)

-- allow_abs2rel
assert.has_no_error(function()
return abs_posix:child_unpack("../../var/lib"):resolve(true)
end)
assert.are_equal(Posix("../var/lib"), abs_posix:descendant("..", "..", "..", "var", "lib"):resolve(true))
end)
end)

describe("Resolve absolute Windows", function()
it("no ../", function()
assert.are_equal(Windows("C:/Users/passwd"), abs_win / "passwd")
assert.are_equal([[C:\Users\passwd]], tostring(abs_win / "passwd"))
end)
it("one ../", function()
assert.are_equal(Windows([[C:\var\lib]]), abs_win / "../var/lib")
assert.are_equal(Windows([[C:\var\lib]]), abs_win / ".." / "var/lib")
assert.are_equal(Windows([[C:\var\lib]]), abs_win.new(abs_win .. "/../" .. "var/lib"))
assert.are_equal([[C:\var\lib]], tostring(abs_win / ".." / "var/lib"))
end)
it("too many ../", function()
assert.has_error(function()
return abs_win / "../../var/lib"
end)
assert.has_error(function()
return abs_win / ".." / ".." / "var/lib"
end)
assert.has_error(function()
return abs_win.new(abs_win .. "/../../" .. "var/lib")
end)
assert.has_error(function()
return tostring(abs_win / "../.." / "var/lib")
end)

-- allow_abs2rel
assert.has_no_error(function()
return abs_win:child_unpack([[..\..\var\lib]]):resolve(true)
end)
assert.are_equal(Windows([[C:../var/lib]]), abs_win:descendant("..", "..", "..", "var", "lib"):resolve(true))
end)
it("overflow ../", function()
assert.has_error(function()
return abs_win / "../../../var/lib"
end)
assert.has_error(function()
return abs_win / ".." / ".." / ".." / "var/lib"
end)
assert.has_error(function()
return abs_win.new(abs_win .. "/../../../" .. "var/lib")
end)
assert.has_error(function()
return tostring(abs_win / "../../.." / "var/lib")
end)
end)
end)

describe("Resolve multiple ../ in between", function()
it("posix", function()
assert.are_equal(Posix("lib"), rel_posix / ".." / "var" / ".." / "lib")
assert.are_equal(Posix("lib"), rel_posix / "../var/../lib")
end)

it("posix overflow", function()
assert.are_equal(Posix("../lib"), rel_posix / ".." / "var" / ".." / ".." / "lib")
assert.are_equal(Posix("../lib"), rel_posix / "../var/../../lib")
end)

it("windows", function()
assert.are_equal(Windows("lib"), rel_windows / ".." / "var" / ".." / "lib")
assert.are_equal(Windows("lib"), rel_windows / "../var/../lib")
end)
it("windows overflow", function()
assert.are_equal(Windows("../lib"), rel_windows / ".." / "var" / ".." / ".." / "lib")
assert.are_equal(Windows("../lib"), rel_windows / "../var/../../lib")
end)
end)
end)

0 comments on commit 6fb37e0

Please sign in to comment.