feat(path): fnamemodify(':h') preserves logical root #40447

Problem:
On Windows, `fnamemodify('//foo/C$', ':h')` incorrectly removes `C$`
as a regular file name and returns `//foo`. However, this is a valid
UNC path, `foo` is a server name and `C$` is a share name.
The correct result should be `//foo/C$`.

Solution:
Extend `os_fileinfo2` and `FileInfo` with `prefix_off`, `rest_off` to
identify path types and logical root boundaries. ':h' can use this info
to prevent traversing past the logical root.

Examples:
  /foo               => /
  //foo              => // (POSIX)
  //foo/bar          => //foo (POSIX)
  //server/share/foo => //server/share/ (Windows)
  C:/foo             => C:/
  //?/C:/foo         => //?/C:/

Co-authored-by: Barrett Ruth <br@barrettruth.com>
This commit is contained in:
tao
2026-07-01 20:25:21 +08:00
committed by GitHub
parent 05d7040425
commit fa9b3381bc
5 changed files with 139 additions and 63 deletions

View File

@@ -106,44 +106,60 @@ describe('fnamemodify()', function()
end)
it('handles :h', function()
-- generic path
eq('.', fnamemodify('hello.txt', ':h'))
eq('path/to', fnamemodify('path/to/hello.txt', ':h'))
eq('/', fnamemodify('/', ':h'))
eq('/', fnamemodify('/foo', ':h'))
eq_slashconvert('path/to', fnamemodify('path/to/hello.txt', ':h'))
-- collapses more than two leading slashes into a single slash
eq('/', fnamemodify('///', ':h'))
eq('/', fnamemodify('////', ':h'))
eq('/', fnamemodify('///foo', ':h'))
eq('/foo', fnamemodify('///foo/bar', ':h'))
eq('/foo', fnamemodify('///foo////bar', ':h'))
eq('/foo', fnamemodify('////foo/bar', ':h'))
if is_os('win') then
-- Current Windows behavior for slash-style UNC paths, such as "//foo/C$/".
eq('/', fnamemodify('/', ':h'))
eq('/', fnamemodify('/foo', ':h'))
eq('//', fnamemodify('//', ':h'))
eq('//', fnamemodify('//foo', ':h'))
eq('//foo', fnamemodify('//foo/bar', ':h'))
eq('//foo', fnamemodify('//foo///bar', ':h'))
eq('//foo', fnamemodify('//foo/C$', ':h'))
eq('//foo/C$', fnamemodify('//foo/C$/', ':h'))
eq('//foo/C$', fnamemodify('//foo/C$/bar', ':h'))
eq('///', fnamemodify('///', ':h'))
eq('////', fnamemodify('////', ':h'))
eq('///', fnamemodify('///foo', ':h'))
eq('///foo', fnamemodify('///foo/bar', ':h'))
eq('///foo', fnamemodify('///foo////bar', ':h'))
else
-- POSIX roots.
eq('/', fnamemodify('/', ':h'))
eq('/', fnamemodify('/foo', ':h'))
-- preserves exactly two leading slashes
eq('//', fnamemodify('//', ':h'))
if not is_os('win') then
-- POSIX permits special handling for exactly two leading slashes.
eq('//', fnamemodify('//', ':h'))
eq('//', fnamemodify('//foo', ':h'))
eq('//foo', fnamemodify('//foo/bar', ':h'))
eq('//foo', fnamemodify('//foo///bar', ':h'))
else
eq('//server', fnamemodify('//server', ':h'))
eq('//server/share', fnamemodify('//server/share', ':h'))
eq('//server/share/', fnamemodify('//server/share/', ':h'))
eq('//server///share', fnamemodify('//server///share', ':h'))
eq('//server/share/', fnamemodify('//server/share/foo', ':h'))
-- More than two leading slashes are ordinary absolute paths.
eq('/', fnamemodify('///', ':h'))
eq('/', fnamemodify('////', ':h'))
eq('/', fnamemodify('///foo', ':h'))
eq('/foo', fnamemodify('///foo/bar', ':h'))
eq('/foo', fnamemodify('///foo////bar', ':h'))
eq('/foo', fnamemodify('////foo/bar', ':h'))
eq([[\\foo\C$]], fnamemodify([[\\foo\C$]], ':h'))
eq([[\\foo\C$\]], fnamemodify([[\\foo\C$\]], ':h'))
eq([[\\foo\C$\]], fnamemodify([[\\foo\C$\bar]], ':h'))
eq([[\\foo\C$\]], fnamemodify([[\\foo\C$\bar]], ':h:h'))
eq([[//foo\C$/]], fnamemodify([[//foo\C$/bar]], ':h'))
-- `C$` is a share name, not a file name
eq('', fnamemodify('//foo/C$/bar', ':h:t'))
eq('C:', fnamemodify('C:foo', ':h'))
eq('C:/', fnamemodify('C:/foo', ':h'))
eq('//?/C:/', fnamemodify('//?/C:/', ':h'))
eq('//?/C:/', fnamemodify('//?/C:/foo', ':h'))
eq(
'//?/Volume{b75e2c83-0000-0000-0000-602f00000000}/',
fnamemodify('//?/Volume{b75e2c83-0000-0000-0000-602f00000000}/foo', ':h')
)
eq(
'//?/Volume{b75e2c83-0000-0000-0000-602f00000000}/',
fnamemodify('///?/Volume{b75e2c83-0000-0000-0000-602f00000000}/foo', ':h')
)
eq('//?/UNC/server', fnamemodify('//?/UNC/server', ':h'))
eq('//?/UNC/server/share', fnamemodify('//?/UNC/server/share', ':h'))
eq('//?/UNC/server/share/', fnamemodify('//?/UNC/server/share/foo', ':h'))
end
end)