Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions pkg/filesystem/path/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 55 additions & 31 deletions pkg/filesystem/path/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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) {
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
30 changes: 17 additions & 13 deletions pkg/filesystem/path/windows_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down