From 0a5f40c207f3f50d0693bd0c852e8bc059b40249 Mon Sep 17 00:00:00 2001 From: tao <2471314@gmail.com> Date: Wed, 17 Jun 2026 00:34:43 +0800 Subject: [PATCH] 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 --- src/nvim/memline.c | 17 +++++++++-- src/nvim/os/fs.c | 34 +++++++++++++++++++++ src/nvim/os/fs_defs.h | 17 ++++++++++- test/functional/core/startup_spec.lua | 43 +++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 4 deletions(-) diff --git a/src/nvim/memline.c b/src/nvim/memline.c index a4ec90fd95..c1d0884c00 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -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; } diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c index f8df77b89d..77438ff8aa 100644 --- a/src/nvim/os/fs.c +++ b/src/nvim/os/fs.c @@ -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. diff --git a/src/nvim/os/fs_defs.h b/src/nvim/os/fs_defs.h index e5355ddefd..1d8ab5bc06 100644 --- a/src/nvim/os/fs_defs.h +++ b/src/nvim/os/fs_defs.h @@ -2,9 +2,24 @@ #include -/// 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. diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua index bc7e2dcf80..c3b12638a3 100644 --- a/test/functional/core/startup_spec.lua +++ b/test/functional/core/startup_spec.lua @@ -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()