Files
neovim/test/functional/core/path_spec.lua
tao 13b2b14275 fix(path): prevent infinite loop when re-editing file without drive-letter
Problem:
Edit a file with a drive-letter path, then re-edit it without the drive letter
and colon. This cause `path_fnamencmp` to loop infinitely as `len` never
reaches 0, while `c1` and `c2` are already NUL.

Commit e18a578 accidentally used || before `(p_fic`, commit 4bcee96 fixed that,
but also moved the NUL check into a grouped condition. The bug remained hidden
because there weren't any cases where strings had different lengths and c1 and
c2 could both reach NUL. `c:/foo` vs `/foo` happens to be such a case, which is
why the infinite loop finally showed up.

Solution:
Break the loop when either `c1` or `c2` is NUL.
2026-06-02 18:59:13 -04:00

205 lines
6.2 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local clear = n.clear
local command = n.command
local eq = t.eq
local eval = n.eval
local feed = n.feed
local fn = n.fn
local insert = n.insert
local is_os = t.is_os
local mkdir = t.mkdir
local rmdir = n.rmdir
local write_file = t.write_file
local function join_path(...)
return table.concat({ ... }, '/')
end
describe('path', function()
local targetdir
local expected_path
before_each(function()
targetdir = join_path('test', 'functional', 'fixtures')
clear()
command('edit ' .. join_path(targetdir, 'tty-test.c'))
expected_path = eval('expand("%:p")')
end)
it('with /./ segment #7117', function()
command('edit ' .. join_path(targetdir, '.', 'tty-test.c'))
eq(expected_path, eval('expand("%:p")'))
end)
it('with ./ prefix #7117', function()
command('edit ' .. join_path('.', targetdir, 'tty-test.c'))
eq(expected_path, eval('expand("%:p")'))
end)
it('with ./ prefix, after directory change #7117', function()
command('edit ' .. join_path('.', targetdir, 'tty-test.c'))
command('cd test')
eq(expected_path, eval('expand("%:p")'))
end)
it('with /../ segment #7117', function()
command('edit ' .. join_path(targetdir, '..', 'fixtures', 'tty-test.c'))
eq(expected_path, eval('expand("%:p")'))
end)
it('with ../ and different starting directory #7117', function()
command('cd test')
command('edit ' .. join_path('..', targetdir, 'tty-test.c'))
eq(expected_path, eval('expand("%:p")'))
end)
it('with ./../ and different starting directory #7117', function()
command('cd test')
command('edit ' .. join_path('.', '..', targetdir, 'tty-test.c'))
eq(expected_path, eval('expand("%:p")'))
end)
it('with implicit drive letter #40013', function()
t.skip(not is_os('win'), 'N/A: only works on Windows')
command('edit ' .. expected_path:sub(3))
eq(1, #fn.getbufinfo())
eq(expected_path, eval('expand("%:p")'))
end)
end)
describe('expand wildcard', function()
before_each(clear)
it('with special characters #24421', function()
local folders = is_os('win') and {
'{folder}',
'folder$name',
} or {
'folder-name',
'folder#name',
}
for _, folder in ipairs(folders) do
mkdir(folder)
local file = join_path(folder, 'file.txt')
write_file(file, '')
eq(file, eval('expand("' .. folder .. '/*")'))
rmdir(folder)
end
end)
end)
describe('file search', function()
local testdir = 'Xtest_path_spec'
before_each(clear)
setup(function()
mkdir(testdir)
end)
teardown(function()
rmdir(testdir)
end)
it('gf finds multibyte filename in line #20517', function()
command('cd test/functional/fixtures')
insert('filename_with_unicode_ααα')
eq('', eval('expand("%")'))
feed('gf')
eq('filename_with_unicode_ααα', eval('expand("%:t")'))
end)
it('gf/<cfile> matches Windows drive-letter filepaths (without ":" in &isfname)', function()
local iswin = is_os('win')
local function test_cfile(input, expected, expected_win)
expected = (iswin and expected_win or expected) or input
command('%delete')
insert(input)
command('norm! 0')
eq(expected, eval('expand("<cfile>")'))
end
-- test_cfile([[c:/d:/e:/foo/bar.txt]], 'c:/d:/e') -- TODO(justinmk): should return "d:/foo/bar.txt" ?
test_cfile([[c:/d:/foo/bar.txt]]) -- TODO(justinmk): should return "d:/foo/bar.txt" ?
test_cfile([[//share/c:/foo/bar/]])
test_cfile([[file://c:/foo/bar]])
test_cfile([[file://c:/foo/bar:42]])
test_cfile([[file://c:/foo/bar:42:666]])
test_cfile([[https://c:/foo/bar]])
test_cfile([[\foo\bar]], [[foo]], [[\foo\bar]])
test_cfile([[/foo/bar]], [[/foo/bar]])
test_cfile([[c:\foo\bar]], [[c:]], [[c:\foo\bar]])
test_cfile([[c:\foo\bar:42:666]], [[c:]], [[c:\foo\bar]])
test_cfile([[c:/foo/bar]])
test_cfile([[c:/foo/bar:42]], [[c:/foo/bar]])
test_cfile([[c:/foo/bar:42:666]], [[c:/foo/bar]])
test_cfile([[c:foo\bar]], [[c]])
test_cfile([[c:foo/bar]], [[c]])
test_cfile([[c:foo]], [[c]])
-- Examples from: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#example-ways-to-refer-to-the-same-file
test_cfile([[c:\temp\test-file.txt]], [[c:]], [[c:\temp\test-file.txt]])
test_cfile(
[[\\127.0.0.1\c$\temp\test-file.txt]],
[[127.0.0.1]],
[[\\127.0.0.1\c$\temp\test-file.txt]]
)
test_cfile(
[[\\LOCALHOST\c$\temp\test-file.txt]],
[[LOCALHOST]],
[[\\LOCALHOST\c$\temp\test-file.txt]]
)
-- not supported yet
test_cfile([[\\.\c:\temp\test-file.txt]], [[.]], [[\\.\c]])
-- not supported yet
test_cfile([[\\?\c:\temp\test-file.txt]], [[c:]], [[\\]])
test_cfile(
[[\\.\UNC\LOCALHOST\c$\temp\test-file.txt]],
[[.]],
[[\\.\UNC\LOCALHOST\c$\temp\test-file.txt]]
)
test_cfile(
[[\\127.0.0.1\c$\temp\test-file.txt]],
[[127.0.0.1]],
[[\\127.0.0.1\c$\temp\test-file.txt]]
)
end)
it('gf/<cfile> handles local file: paths', function()
local path = fn.fnamemodify(join_path(testdir, 'gf-target.txt'), ':p'):gsub('\\', '/')
local uri = 'file:' .. (is_os('win') and '/' or '') .. path
write_file(path, '')
insert(uri)
command('norm! 0')
eq(path, eval('expand("<cfile>")'))
feed('gf')
eq(path, fn.fnamemodify(eval('expand("%:p")'), ':p'):gsub('\\', '/'))
end)
---@param funcname 'finddir' | 'findfile'
local function test_find_func(funcname, folder, item)
local d = join_path(testdir, folder)
mkdir(d)
local expected = join_path(d, item)
if funcname == 'finddir' then
mkdir(expected)
else
write_file(expected, '')
end
eq(expected, fn[funcname](item, d:gsub(' ', [[\ ]])))
end
it('finddir()', function()
test_find_func('finddir', 'directory', 'folder')
test_find_func('finddir', 'directory', 'folder name')
test_find_func('finddir', 'fold#er name', 'directory')
end)
it('findfile()', function()
test_find_func('findfile', 'directory', 'file.txt')
test_find_func('findfile', 'directory', 'file name.txt')
test_find_func('findfile', 'fold#er name', 'file.txt')
end)
end)