Skip to content

fix(utils): handle edge case for is_subpath #1655

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions lua/neo-tree/utils/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -862,26 +862,37 @@ M.normalize_path = function(path)
path = path:sub(1, 1):upper() .. path:sub(2)
-- Turn mixed forward and back slashes into all forward slashes
-- using NeoVim's logic
path = vim.fs.normalize(path)
path = vim.fs.normalize(path, { win = true })
-- Now use backslashes, as expected by the rest of Neo-Tree's code
path = path:gsub("/", M.path_separator)
end
return path
end

---Check if a path is a subpath of another.
--@param base string The base path.
--@param path string The path to check is a subpath.
--@return boolean boolean True if it is a subpath, false otherwise.
---@param base string The base path.
---@param path string The path to check is a subpath.
---@return boolean boolean True if it is a subpath, false otherwise.
M.is_subpath = function(base, path)
if not M.truthy(base) or not M.truthy(path) then
return false
elseif base == path then
return true
end

base = M.normalize_path(base)
path = M.normalize_path(path)
return string.sub(path, 1, string.len(base)) == base
if path:sub(1, #base) == base then
local base_parts = M.split(base, M.path_separator)
local path_parts = M.split(path, M.path_separator)
for i, part in ipairs(base_parts) do
if path_parts[i] ~= part then
return false
end
end
return true
end
return false
end

---The file system path separator for the current platform.
Expand Down
66 changes: 66 additions & 0 deletions tests/neo-tree/utils/path_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
pcall(require, "luacov")
local utils = require("neo-tree.utils")

describe("is_subpath", function()
local common_tests = function()
-- Relative paths
assert.are.same(true, utils.is_subpath("a", "a/subpath"))
assert.are.same(false, utils.is_subpath("a", "b/c"))
assert.are.same(false, utils.is_subpath("a", "b"))
end
it("should work with unix paths", function()
local old = utils.is_windows
utils.is_windows = false
common_tests()
assert.are.same(true, utils.is_subpath("/a", "/a/subpath"))
assert.are.same(false, utils.is_subpath("/a", "/b/c"))

-- Edge cases
assert.are.same(false, utils.is_subpath("", ""))
assert.are.same(true, utils.is_subpath("/", "/"))

-- Paths with trailing slashes
assert.are.same(true, utils.is_subpath("/a/", "/a/subpath"))
assert.are.same(true, utils.is_subpath("/a/", "/a/subpath/"))
assert.are.same(true, utils.is_subpath("/a", "/a/subpath"))
assert.are.same(true, utils.is_subpath("/a", "/a/subpath/"))

-- Paths with different casing
assert.are.same(true, utils.is_subpath("/TeSt", "/TeSt/subpath"))
assert.are.same(false, utils.is_subpath("/A", "/a/subpath"))
assert.are.same(false, utils.is_subpath("/A", "/a/subpath"))
utils.is_windows = old
end)
it("should work on windows paths", function()
local old = utils.is_windows
utils.is_windows = true
common_tests()
assert.are.same(true, utils.is_subpath("C:", "C:"))
assert.are.same(false, utils.is_subpath("C:", "D:"))
assert.are.same(true, utils.is_subpath("C:/A", [[C:\A]]))

-- Test Windows paths with backslashes
assert.are.same(true, utils.is_subpath([[C:\Users\user]], [[C:\Users\user\Documents]]))
assert.are.same(false, utils.is_subpath([[C:\Users\user]], [[D:\Users\user]]))
assert.are.same(false, utils.is_subpath([[C:\Users\user]], [[C:\Users\usera]]))

-- Test Windows paths with forward slashes
assert.are.same(true, utils.is_subpath("C:/Users/user", "C:/Users/user/Documents"))
assert.are.same(false, utils.is_subpath("C:/Users/user", "D:/Users/user"))
assert.are.same(false, utils.is_subpath("C:/Users/user", "C:/Users/usera"))

-- Test Windows paths with drive letters
assert.are.same(true, utils.is_subpath("C:", "C:/Users/user"))
assert.are.same(false, utils.is_subpath("C:", "D:/Users/user"))

-- Test Windows paths with UNC paths
assert.are.same(true, utils.is_subpath([[\\server\share]], [[\\server\share\folder]]))
assert.are.same(false, utils.is_subpath([[\\server\share]], [[\\server2\share]]))

-- Test Windows paths with trailing backslashes
assert.are.same(true, utils.is_subpath([[C:\Users\user\]], [[C:\Users\user\Documents]]))
assert.are.same(true, utils.is_subpath("C:/Users/user/", "C:/Users/user/Documents"))

utils.is_windows = old
end)
end)