mirror of
https://github.com/neovim/neovim.git
synced 2026-07-09 10:59:38 +00:00
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:
@@ -369,6 +369,7 @@ These existing features changed their behavior.
|
||||
• Markdown inline highlighting now conceals the backslash in backslash escapes.
|
||||
• Markdown inline backslash escapes and hard line breaks no longer use the
|
||||
`@string.escape` capture.
|
||||
• `fnamemodify(':h')` preserves logical roots for more path formats.
|
||||
|
||||
==============================================================================
|
||||
REMOVED FEATURES *news-removed*
|
||||
|
||||
@@ -208,34 +208,25 @@ repeat:
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef MSWIN
|
||||
FileInfo file_info;
|
||||
os_fileinfo2(*fnamep, &file_info);
|
||||
if (src[*usedlen] == ':' && src[*usedlen + 1] == 'h') {
|
||||
char *fname = *fnamep;
|
||||
// POSIX reserves exactly "//"; longer slash runs are ordinary absolute paths.
|
||||
// "///foo/bar" => "/foo"
|
||||
// "//foo/bar" => "//foo"
|
||||
if (fname[0] == '/' && fname[1] == '/' && fname[2] == '/') {
|
||||
while (fname[1] == '/') {
|
||||
fname++;
|
||||
}
|
||||
*fnamep = fname;
|
||||
}
|
||||
s = *fnamep + file_info.rest_off;
|
||||
*fnamep = *fnamep + file_info.prefix_off;
|
||||
}
|
||||
#endif
|
||||
|
||||
char *tail = path_tail(*fnamep);
|
||||
*fnamelen = strlen(*fnamep);
|
||||
|
||||
// ":h" - head, remove "/file_name", can be repeated
|
||||
// Don't remove the first "/" or "c:\"
|
||||
// Don't remove the logical root, see `FileInfo`.
|
||||
while (src[*usedlen] == ':' && src[*usedlen + 1] == 'h') {
|
||||
valid |= VALID_HEAD;
|
||||
*usedlen += 2;
|
||||
s = get_past_head(*fnamep);
|
||||
while (tail > s && after_pathsep(s, tail)) {
|
||||
MB_PTR_BACK(*fnamep, tail);
|
||||
}
|
||||
*fnamelen = (size_t)(tail - *fnamep);
|
||||
*fnamelen = tail <= s ? (size_t)(s - *fnamep) : (size_t)(tail - *fnamep);
|
||||
if (*fnamelen == 0) {
|
||||
// Result is empty. Turn it into "." to make ":cd %:h" work.
|
||||
xfree(*bufp);
|
||||
|
||||
@@ -1202,9 +1202,15 @@ bool os_fileinfo(const char *path, FileInfo *file_info)
|
||||
return os_stat(path, &(file_info->stat)) == kLibuvSuccess;
|
||||
}
|
||||
|
||||
/// Identify Windows path type and provide root offset for now, examples:
|
||||
/// - "//?/C:/foo" => type is `kPathDevice` root offset is 4
|
||||
/// - "//?/UNC/localhost/C$/foo" => type is `kPathDeviceUNC`, root offset is 8
|
||||
static const char *path_skip_sep(const char *path)
|
||||
{
|
||||
while (*path != NUL && vim_ispathsep_nocolon(*path)) {
|
||||
path++;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// Parses `path` into a `FileInfo` structure.
|
||||
///
|
||||
/// TODO(ntdiary): Could be extended for path.c cleanup and path normalization
|
||||
/// logic. Eventually merge this with `os_fileinfo`
|
||||
@@ -1214,25 +1220,55 @@ bool os_fileinfo2(const char *path, FileInfo *info)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
CLEAR_POINTER(info);
|
||||
#ifdef MSWIN
|
||||
size_t len = strlen(path);
|
||||
if (len < 4) {
|
||||
if (path_with_url(path)) {
|
||||
return true;
|
||||
}
|
||||
if (vim_ispathsep_nocolon(path[0])
|
||||
&& vim_ispathsep_nocolon(path[1])
|
||||
&& (path[2] == '?' || path[2] == '.')
|
||||
&& vim_ispathsep_nocolon(path[3])) {
|
||||
if (len >= 8 && vim_strnicmp_asc(path + 4, "unc/", 4) == 0) {
|
||||
info->root_off = 8;
|
||||
info->type = kPathDeviceUNC;
|
||||
} else {
|
||||
info->root_off = 4;
|
||||
info->type = kPathDevice;
|
||||
// Preserves the leading two "/"; runs of 3+ "/" collapse to a single "/" (IEEE 1003.1).
|
||||
// The same rule applies to kPathUNC and kPathGeneric on Windows, but not to kPathDevice
|
||||
// or kPathDeviceUNC, where the leading "//" is significant.
|
||||
const char *p = path_skip_sep(path);
|
||||
size_t leading_slashes = (size_t)(p - path);
|
||||
#ifdef MSWIN
|
||||
if (leading_slashes == 0 && ASCII_ISALPHA(p[0]) && p[1] == ':') {
|
||||
info->type = kPathDrive;
|
||||
p = path_skip_sep(p + 2);
|
||||
info->rest_off = (size_t)(p - path);
|
||||
return true;
|
||||
}
|
||||
if (p[0] == '?' || p[0] == '.') {
|
||||
if (!vim_ispathsep_nocolon(p[1])) {
|
||||
return true;
|
||||
}
|
||||
info->type = kPathDevice;
|
||||
info->prefix_off = leading_slashes - 2;
|
||||
p = path_skip_sep(p + 2);
|
||||
if (vim_strnicmp_asc(p, "unc", 3) == 0 && vim_ispathsep_nocolon(p[3])) {
|
||||
info->type = kPathDeviceUNC;
|
||||
p = path_skip_sep(p + 4);
|
||||
info->root_off = (size_t)(p - path);
|
||||
goto server;
|
||||
}
|
||||
info->root_off = (size_t)(p - path);
|
||||
if (ASCII_ISALPHA(p[0]) && p[1] == ':') {
|
||||
p += 2;
|
||||
}
|
||||
p = path_skip_sep(path_next_component(p));
|
||||
info->rest_off = (size_t)(p - path);
|
||||
return true;
|
||||
}
|
||||
if (leading_slashes == 2) {
|
||||
info->type = kPathUNC;
|
||||
server:
|
||||
p = path_skip_sep(path_next_component(p));
|
||||
p = path_skip_sep(path_next_component(p));
|
||||
info->rest_off = (size_t)(p - path);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
info->type = kPathGeneric;
|
||||
info->rest_off = leading_slashes;
|
||||
info->root_off = leading_slashes > 2 ? leading_slashes - 1 : 0;
|
||||
info->prefix_off = info->root_off;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,19 +6,51 @@
|
||||
/// @see https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
|
||||
typedef enum {
|
||||
kPathUnknown = 0,
|
||||
kPathDOS, ///< C:/foo/bar
|
||||
kPathGeneric, ///< foo/bar or /foo/bar
|
||||
kPathDrive, ///< C:/foo/bar
|
||||
kPathUNC, ///< //server/share/foo/bar
|
||||
kPathDevice, ///< //?/C:/foo/bar or //?/Volume{xxx}/foo/bar
|
||||
kPathDeviceUNC, ///< //?/UNC/server/share/foo/bar
|
||||
} PathType;
|
||||
|
||||
/// Struct which encapsulates file stat and path information.
|
||||
///
|
||||
/// A path is divided into three parts: prefix, logical root, rest.
|
||||
/// Examples:
|
||||
/// foo/bar generic relative path
|
||||
/// ^------- rest
|
||||
///
|
||||
/// /foo/bar generic absolute path
|
||||
/// ^^------- rest
|
||||
/// +-------- root & prefix
|
||||
///
|
||||
/// C:foo/bar Windows drive path, relative path
|
||||
/// ^ ^------- rest
|
||||
/// +--------- root & prefix
|
||||
///
|
||||
/// C:/foo/bar Windows drive path, absolute path
|
||||
/// ^ ^------- rest
|
||||
/// +---------- root & prefix
|
||||
///
|
||||
/// //?/C:/foo/bar Windows device path
|
||||
/// ^ ^ ^------- rest
|
||||
/// +---------- root
|
||||
/// +-------------- prefix
|
||||
///
|
||||
/// //server/share/foo/bar Windows UNC path
|
||||
/// ^ ^------- rest
|
||||
/// +---------------------- root & prefix
|
||||
///
|
||||
/// //?/UNC/server/share/foo/bar Windows device UNC path
|
||||
/// ^ ^ ^------- rest
|
||||
/// +-------------------- root
|
||||
/// +---------------------------- prefix
|
||||
typedef struct {
|
||||
uv_stat_t stat; ///< @private
|
||||
|
||||
/// Offset of the root component after any path type prefix. Currently
|
||||
/// only used for Widnows device paths. e.g. "//?/" and "//?/UNC/"
|
||||
size_t root_off;
|
||||
size_t prefix_off; ///< Offset where the type-determining prefix starts.
|
||||
size_t root_off; ///< Offset where the logical root starts.
|
||||
size_t rest_off; ///< Offset where the remaining path starts.
|
||||
PathType type;
|
||||
} FileInfo;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user