From 38c18ad3a8f231664abc0d12a9c57e73f93fafc6 Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Sun, 17 May 2026 10:54:59 -0400 Subject: [PATCH] fix(vimscript): normalize multi-slash heads in fnamemodify() #39805 Problem: `fnamemodify(..., ':h')` mishandles POSIX leading slash runs longer than `//`. Solution: Collapse those slash runs to `/` before computing the head. --- src/nvim/eval/fs.c | 15 ++++++++ .../functional/vimscript/fnamemodify_spec.lua | 36 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/nvim/eval/fs.c b/src/nvim/eval/fs.c index c3809b5faf..8bf7414d8e 100644 --- a/src/nvim/eval/fs.c +++ b/src/nvim/eval/fs.c @@ -203,6 +203,21 @@ repeat: } } +#ifndef MSWIN + 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; + } + } +#endif + char *tail = path_tail(*fnamep); *fnamelen = strlen(*fnamep); diff --git a/test/functional/vimscript/fnamemodify_spec.lua b/test/functional/vimscript/fnamemodify_spec.lua index 7b581c225f..f3d160f9c5 100644 --- a/test/functional/vimscript/fnamemodify_spec.lua +++ b/test/functional/vimscript/fnamemodify_spec.lua @@ -109,6 +109,42 @@ describe('fnamemodify()', function() eq('.', fnamemodify('hello.txt', ':h')) eq_slashconvert('path/to', fnamemodify('path/to/hello.txt', ':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')) + + -- 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')) + + -- 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')) + end end) it('handles :t', function()