fix(path): open swapfile when using device path #39982

Problem:
Can't open swapfile when using a device path starting with `//?/`,
because `?` is a reserved char on Widnows.

Solution:
For device UNC paths, replace `//?/UNC/` and `//./UNC/` with `//`.
For other device paths, just strip their prefix (i.e. `//?/`, `//./`).
This aligns swapfile naming for device paths with regular UNC and DOS
paths.

Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
This commit is contained in:
tao
2026-06-17 00:34:43 +08:00
committed by GitHub
parent 355d010ddf
commit 0a5f40c207
4 changed files with 107 additions and 4 deletions

View File

@@ -1425,11 +1425,22 @@ char *make_percent_swname(char *dir, char *dir_end, const char *name)
FUNC_ATTR_NONNULL_ARG(1, 2)
{
String fixed_fname;
fixed_fname.data = fix_fname(name != NULL ? name : "");
if (fixed_fname.data == NULL) {
char *fname = fix_fname(name != NULL ? name : "");
if (fname == NULL) {
return NULL;
}
FileInfo file_info;
if (!os_fileinfo2(fname, &file_info)) {
xfree(fname);
return NULL;
}
fixed_fname.data = fname + file_info.root_off;
if (file_info.type == kPathDeviceUNC) {
assert(file_info.root_off >= 2);
fixed_fname.data -= 2;
fixed_fname.data[0] = '/'; // Fixup //?/UNC/server/ path: "C/server/..." -> "//server/..."
}
char *p;
for (p = fixed_fname.data; *p != NUL; MB_PTR_ADV(p)) {
if (vim_ispathsep(*p)) {
@@ -1442,7 +1453,7 @@ char *make_percent_swname(char *dir, char *dir_end, const char *name)
p = &dir_end[-1];
*p = NUL;
String d = concat_fnames(cbuf_as_string(dir, (size_t)(p - dir)), fixed_fname, true);
xfree(fixed_fname.data);
xfree(fname);
return d.data;
}

View File

@@ -1188,6 +1188,40 @@ 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
///
/// TODO(ntdiary): Could be extended for path.c cleanup and path normalization
/// logic. Eventually merge this with `os_fileinfo`
///
/// TODO(justinmk): return value is meaningless currently.
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) {
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;
}
return true;
}
#endif
return true;
}
/// Get the file information for a given path without following links
///
/// @param path Path to the file.

View File

@@ -2,9 +2,24 @@
#include <uv.h>
/// Struct which encapsulates stat information.
/// Currently supports Windows and is extensible.
/// @see https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
typedef enum {
kPathUnknown = 0,
kPathDOS, ///< 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.
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;
PathType type;
} FileInfo;
/// Struct which encapsulates inode/dev_id information.

View File

@@ -118,6 +118,49 @@ describe('startup', function()
command('filetype detect')
eq('filetype detection:ON plugin:OFF indent:OFF', exec_capture('filetype'))
end)
it('opens swapfile when using device path #31606', function()
t.skip(not is_os('win'), 'N/A: Windows feature')
local cwd = vim.fs.normalize(vim.uv.cwd())
local drive, path = cwd:sub(1, 1), cwd:sub(3)
local swapdir = ('%s/Xtest_swap_dir'):format(cwd)
local dos_path = ('%s/file.txt'):format(cwd)
local device_quest_path = ('//?/%s'):format(dos_path)
clear({
args = {
'--embed',
'--headless',
'--cmd',
('set directory=%s//'):format(swapdir),
'--',
device_quest_path,
},
merge = false,
})
local escaped_name = ('%s/%s.swp'):format(swapdir, dos_path:gsub('[:/]', '%%'))
eq(escaped_name, fn.swapname('%'))
command('bw!')
api.nvim_buf_set_name(0, dos_path)
eq(escaped_name, fn.swapname('%'))
command('bw!')
local device_dot_path = ('//./%s'):format(dos_path)
api.nvim_buf_set_name(0, device_dot_path)
eq(escaped_name, fn.swapname('%'))
command('bw!')
local device_unc_path = ('//?/UNC/localhost/%s$%s/file.txt'):format(drive, path)
api.nvim_buf_set_name(0, device_unc_path)
eq(('%s/%%%s.swp'):format(swapdir, device_unc_path:sub(8):gsub('/', '%%')), fn.swapname('%'))
command('bw!')
-- Volume GUID form, like \\?\Volume{d4857377-2ac4-45d8-a4cb-9b2e447fd02e}\
local guid = vim.fs.normalize(vim.trim(vim.fn.system(('mountvol %s: /L'):format(drive))))
local device_guid_path = ('%s%s/file.txt'):format(guid, path:sub(2))
api.nvim_buf_set_name(0, device_guid_path)
eq(escaped_name, fn.swapname('%'))
end)
end)
describe('startup', function()