diff --git a/pkg/filesystem/path/builder.go b/pkg/filesystem/path/builder.go index 80be90aa..8ca6c9ff 100644 --- a/pkg/filesystem/path/builder.go +++ b/pkg/filesystem/path/builder.go @@ -109,14 +109,23 @@ func (b *Builder) GetWindowsString(format WindowsPathFormat) (string, error) { prefix = "\\" } - // Emit trailing slash in case the path refers to a directory, - // or a dot or slash if the path is empty. The suffix is been - // constructed by platform-independent code that uses forward - // slashes. To construct a Windows path we must use a + // If the path refers to a directory we should emit a trailing slash, + // and since the suffix has been constructed by platform-independent code + // that uses forward slashes we need to convert the path to use a // backslash. + // + // Unfortunately, a trailing backslash is not a valid pathname component: + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ffb795f3-027d-4a3c-997d-3085f2332f6f + // Instead we emit `\.` and rely on win32 path normalization. However, + // for WindowsPathFormatDevicePath we cannot do this as device paths + // bypass win32 path normalization so have to just emit a backlash. suffix := b.suffix if suffix == "/" { - suffix = "\\" + if format == WindowsPathFormatDevicePath { + suffix = "\\" + } else { + suffix = "\\." + } } out.WriteString(suffix) return out.String(), nil diff --git a/pkg/filesystem/path/builder_test.go b/pkg/filesystem/path/builder_test.go index 33ef2004..157b1c5a 100644 --- a/pkg/filesystem/path/builder_test.go +++ b/pkg/filesystem/path/builder_test.go @@ -63,14 +63,14 @@ func TestBuilder(t *testing.T) { for _, data := range [][]string{ {".", "."}, {"..", ".."}, - {"/", "\\"}, + {"/", "\\."}, {"hello", "hello"}, - {"hello/", "hello\\"}, + {"hello/", "hello\\."}, {"hello/..", "hello\\.."}, - {"/hello/", "\\hello\\"}, + {"/hello/", "\\hello\\."}, {"/hello/..", "\\hello\\.."}, {"/hello/../world", "\\hello\\..\\world"}, - {"/hello/../world/", "\\hello\\..\\world\\"}, + {"/hello/../world/", "\\hello\\..\\world\\."}, {"/hello/../world/foo", "\\hello\\..\\world\\foo"}, } { p := data[0] @@ -92,16 +92,16 @@ func TestBuilder(t *testing.T) { t.Run("WindowsIdentity", func(t *testing.T) { for _, p := range []string{ - "C:\\", - "C:\\hello\\", + "C:\\.", + "C:\\hello\\.", "C:\\hello\\..", "C:\\hello\\..\\world", - "C:\\hello\\..\\world\\", + "C:\\hello\\..\\world\\.", "C:\\hello\\..\\world\\foo", "C:\\hello\\..\\world\\foo", - "\\\\server\\share\\hello\\", + "\\\\server\\share\\hello\\.", "\\\\server\\share\\hello\\..\\world", - "\\\\server\\share\\hello\\..\\world\\", + "\\\\server\\share\\hello\\..\\world\\.", "\\\\server\\share\\hello\\..\\world\\foo", } { t.Run(p, func(t *testing.T) { @@ -146,19 +146,19 @@ func TestBuilder(t *testing.T) { t.Run("WindowsParseCasing", func(t *testing.T) { for _, data := range [][]string{ {"./bar", "bar"}, - {"./bar\\", "bar\\"}, - {"c:", "C:\\"}, - {"c:.", "C:\\"}, + {"./bar\\", "bar\\."}, + {"c:", "C:\\."}, + {"c:.", "C:\\."}, {"c:Hello", "C:\\Hello"}, - {"c:\\", "C:\\"}, - {"c:\\.", "C:\\"}, - {"c:\\Hello\\", "C:\\Hello\\"}, - {"c:\\Hello\\.", "C:\\Hello\\"}, + {"c:\\", "C:\\."}, + {"c:\\.", "C:\\."}, + {"c:\\Hello\\", "C:\\Hello\\."}, + {"c:\\Hello\\.", "C:\\Hello\\."}, {"c:\\Hello\\..", "C:\\Hello\\.."}, {"c:\\Hello\\.\\world", "C:\\Hello\\world"}, {"c:\\Hello\\..\\world", "C:\\Hello\\..\\world"}, {"c:\\Hello\\..\\world", "C:\\Hello\\..\\world"}, - {"c:\\Hello\\..\\world\\", "C:\\Hello\\..\\world\\"}, + {"c:\\Hello\\..\\world\\", "C:\\Hello\\..\\world\\."}, {"c:\\Hello\\..\\world\\foo", "C:\\Hello\\..\\world\\foo"}, {"c:\\\\Hello\\\\..\\world\\foo", "C:\\Hello\\..\\world\\foo"}, {"\\\\Server\\Share\\Hello\\\\..\\world\\foo", "\\\\Server\\Share\\Hello\\..\\world\\foo"}, @@ -213,25 +213,27 @@ func TestBuilder(t *testing.T) { "./.": ".", "../": "..", "../.": "..", - "/.": "\\", - "/./": "\\", - "/..": "\\", - "/../": "\\", - "/hello/.": "\\hello\\", + "/.": "\\.", + "/./": "\\.", + "/..": "\\.", + "/../": "\\.", + "/hello/.": "\\hello\\.", "/hello/../.": "\\hello\\..", "//Server/Share/hello": "\\\\Server\\Share\\hello", - "//Server/Share/.": "\\\\Server\\Share\\", - "//Server/Share/./": "\\\\Server\\Share\\", - "//Server/Share/..": "\\\\Server\\Share\\", - "//Server/Share/../": "\\\\Server\\Share\\", - "//Server/Share/hello/.": "\\\\Server\\Share\\hello\\", + "//Server/Share/.": "\\\\Server\\Share\\.", + "//Server/Share/./": "\\\\Server\\Share\\.", + "//Server/Share/..": "\\\\Server\\Share\\.", + "//Server/Share/../": "\\\\Server\\Share\\.", + "//Server/Share/hello/.": "\\\\Server\\Share\\hello\\.", "//Server/Share/hello/../.": "\\\\Server\\Share\\hello\\..", "/\\Server\\Share/hello/../.": "\\\\Server\\Share\\hello\\..", - "\\\\?\\C:\\hello\\.": "C:\\hello\\", - "\\\\?\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\", - "\\??\\C:\\hello\\.": "C:\\hello\\", + "\\\\?\\C:\\hello\\.": "C:\\hello\\.", + "\\\\?\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\.", + "\\??\\C:\\hello\\.": "C:\\hello\\.", "\\??\\Z:\\file0": "Z:\\file0", - "\\??\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\", + "\\??\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\.", + "\\\\.\\C:\\hello\\.": "C:\\hello\\.", + "\\\\.\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\.", } { t.Run(from, func(t *testing.T) { builder1, scopeWalker1 := path.EmptyBuilder.Join(path.VoidScopeWalker) @@ -445,6 +447,28 @@ func TestBuilder(t *testing.T) { require.Equal(t, "\\\\myserver\\myshare\\data.txt", mustGetWindowsString(builder)) }) + t.Run("DeviceNamespaceDrivePath", func(t *testing.T) { + scopeWalker := mock.NewMockScopeWalker(ctrl) + componentWalker := mock.NewMockComponentWalker(ctrl) + scopeWalker.EXPECT().OnDriveLetter('C').Return(componentWalker, nil) + componentWalker.EXPECT().OnTerminal(path.MustNewComponent("file.txt")) + + builder, s := path.EmptyBuilder.Join(scopeWalker) + require.NoError(t, path.Resolve(path.WindowsFormat.NewParser(`\\.\C:\file.txt`), s)) + require.Equal(t, "C:\\file.txt", mustGetWindowsString(builder)) + }) + + t.Run("DeviceNamespaceUNCPath", func(t *testing.T) { + scopeWalker := mock.NewMockScopeWalker(ctrl) + componentWalker := mock.NewMockComponentWalker(ctrl) + scopeWalker.EXPECT().OnShare("server", "share").Return(componentWalker, nil) + componentWalker.EXPECT().OnTerminal(path.MustNewComponent("file.txt")) + + builder, s := path.EmptyBuilder.Join(scopeWalker) + require.NoError(t, path.Resolve(path.WindowsFormat.NewParser(`\\.\UNC\server\share\file.txt`), s)) + require.Equal(t, "\\\\server\\share\\file.txt", mustGetWindowsString(builder)) + }) + t.Run("RelativeDrivePaths", func(t *testing.T) { builder1, s := path.EmptyBuilder.Join(path.VoidScopeWalker) require.NoError(t, path.Resolve(path.WindowsFormat.NewParser("C:\\a\\b"), s)) diff --git a/pkg/filesystem/path/windows_format.go b/pkg/filesystem/path/windows_format.go index 73fc3df8..1cf94f8b 100644 --- a/pkg/filesystem/path/windows_format.go +++ b/pkg/filesystem/path/windows_format.go @@ -60,24 +60,28 @@ type windowsParser struct { } func (p windowsParser) ParseScope(scopeWalker ScopeWalker) (next ComponentWalker, remainder RelativeParser, err error) { - // Handle extended-length paths starting with \\?\. path := p.path + hasPrefix := false if len(p.path) >= 4 && p.path[0] == '\\' && p.path[1] == '\\' && p.path[2] == '?' && p.path[3] == '\\' { + // Extended-length paths starting with \\?\. path = p.path[4:] - // Handle \\?\UNC\. - if len(path) >= 4 && strings.EqualFold(path[:4], "UNC\\") { - return parseUNCPath(path[4:], scopeWalker) - } + hasPrefix = true + } else if len(p.path) >= 4 && p.path[0] == '\\' && p.path[1] == '?' && p.path[2] == '?' && p.path[3] == '\\' { + // NT object namespace paths starting with \??\. + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-even/c1550f98-a1ce-426a-9991-7509e7c3787c + path = p.path[4:] + hasPrefix = true + } else if len(p.path) >= 4 && p.path[0] == '\\' && p.path[1] == '\\' && p.path[2] == '.' && p.path[3] == '\\' { + // Win32 device namespace paths starting with \\.\. + // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-device-namespaces + path = p.path[4:] + hasPrefix = true } - // Handle NT object namespace paths starting with \??\. - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-even/c1550f98-a1ce-426a-9991-7509e7c3787c - if len(p.path) >= 4 && p.path[0] == '\\' && p.path[1] == '?' && p.path[2] == '?' && p.path[3] == '\\' { - path = p.path[4:] - // Handle \??\UNC\ - if len(path) >= 4 && strings.EqualFold(path[:4], "UNC\\") { - return parseUNCPath(path[4:], scopeWalker) - } + // Handle UNC paths following a namespace prefix + // (e.g. \\?\UNC\server\share or \??\UNC\server\share). + if hasPrefix && len(path) >= 4 && strings.EqualFold(path[:4], "UNC\\") { + return parseUNCPath(path[4:], scopeWalker) } if len(path) >= 2 {