Skip to content

Commit dc929ad

Browse files
committed
Append \. instead of \ for Windows directory path suffixes
Also consolidate namespace prefix stripping in the Windows path parser and add support for the \\.\ device namespace prefix.
1 parent 6002cad commit dc929ad

File tree

3 files changed

+84
-54
lines changed

3 files changed

+84
-54
lines changed

pkg/filesystem/path/builder.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,15 @@ func (b *Builder) GetWindowsString(format WindowsPathFormat) (string, error) {
110110
}
111111

112112
// Emit trailing slash in case the path refers to a directory,
113-
// or a dot or slash if the path is empty. The suffix is been
113+
// or a dot or slash if the path is empty. The suffix has been
114114
// constructed by platform-independent code that uses forward
115115
// slashes. To construct a Windows path we must use a
116-
// backslash.
116+
// backslash followed by a dot, as a bare trailing backslash
117+
// is not a valid pathname component.
118+
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/ffb795f3-027d-4a3c-997d-3085f2332f6f
117119
suffix := b.suffix
118120
if suffix == "/" {
119-
suffix = "\\"
121+
suffix = "\\."
120122
}
121123
out.WriteString(suffix)
122124
return out.String(), nil

pkg/filesystem/path/builder_test.go

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ func TestBuilder(t *testing.T) {
6363
for _, data := range [][]string{
6464
{".", "."},
6565
{"..", ".."},
66-
{"/", "\\"},
66+
{"/", "\\."},
6767
{"hello", "hello"},
68-
{"hello/", "hello\\"},
68+
{"hello/", "hello\\."},
6969
{"hello/..", "hello\\.."},
70-
{"/hello/", "\\hello\\"},
70+
{"/hello/", "\\hello\\."},
7171
{"/hello/..", "\\hello\\.."},
7272
{"/hello/../world", "\\hello\\..\\world"},
73-
{"/hello/../world/", "\\hello\\..\\world\\"},
73+
{"/hello/../world/", "\\hello\\..\\world\\."},
7474
{"/hello/../world/foo", "\\hello\\..\\world\\foo"},
7575
} {
7676
p := data[0]
@@ -92,16 +92,16 @@ func TestBuilder(t *testing.T) {
9292

9393
t.Run("WindowsIdentity", func(t *testing.T) {
9494
for _, p := range []string{
95-
"C:\\",
96-
"C:\\hello\\",
95+
"C:\\.",
96+
"C:\\hello\\.",
9797
"C:\\hello\\..",
9898
"C:\\hello\\..\\world",
99-
"C:\\hello\\..\\world\\",
99+
"C:\\hello\\..\\world\\.",
100100
"C:\\hello\\..\\world\\foo",
101101
"C:\\hello\\..\\world\\foo",
102-
"\\\\server\\share\\hello\\",
102+
"\\\\server\\share\\hello\\.",
103103
"\\\\server\\share\\hello\\..\\world",
104-
"\\\\server\\share\\hello\\..\\world\\",
104+
"\\\\server\\share\\hello\\..\\world\\.",
105105
"\\\\server\\share\\hello\\..\\world\\foo",
106106
} {
107107
t.Run(p, func(t *testing.T) {
@@ -146,19 +146,19 @@ func TestBuilder(t *testing.T) {
146146
t.Run("WindowsParseCasing", func(t *testing.T) {
147147
for _, data := range [][]string{
148148
{"./bar", "bar"},
149-
{"./bar\\", "bar\\"},
150-
{"c:", "C:\\"},
151-
{"c:.", "C:\\"},
149+
{"./bar\\", "bar\\."},
150+
{"c:", "C:\\."},
151+
{"c:.", "C:\\."},
152152
{"c:Hello", "C:\\Hello"},
153-
{"c:\\", "C:\\"},
154-
{"c:\\.", "C:\\"},
155-
{"c:\\Hello\\", "C:\\Hello\\"},
156-
{"c:\\Hello\\.", "C:\\Hello\\"},
153+
{"c:\\", "C:\\."},
154+
{"c:\\.", "C:\\."},
155+
{"c:\\Hello\\", "C:\\Hello\\."},
156+
{"c:\\Hello\\.", "C:\\Hello\\."},
157157
{"c:\\Hello\\..", "C:\\Hello\\.."},
158158
{"c:\\Hello\\.\\world", "C:\\Hello\\world"},
159159
{"c:\\Hello\\..\\world", "C:\\Hello\\..\\world"},
160160
{"c:\\Hello\\..\\world", "C:\\Hello\\..\\world"},
161-
{"c:\\Hello\\..\\world\\", "C:\\Hello\\..\\world\\"},
161+
{"c:\\Hello\\..\\world\\", "C:\\Hello\\..\\world\\."},
162162
{"c:\\Hello\\..\\world\\foo", "C:\\Hello\\..\\world\\foo"},
163163
{"c:\\\\Hello\\\\..\\world\\foo", "C:\\Hello\\..\\world\\foo"},
164164
{"\\\\Server\\Share\\Hello\\\\..\\world\\foo", "\\\\Server\\Share\\Hello\\..\\world\\foo"},
@@ -213,25 +213,27 @@ func TestBuilder(t *testing.T) {
213213
"./.": ".",
214214
"../": "..",
215215
"../.": "..",
216-
"/.": "\\",
217-
"/./": "\\",
218-
"/..": "\\",
219-
"/../": "\\",
220-
"/hello/.": "\\hello\\",
216+
"/.": "\\.",
217+
"/./": "\\.",
218+
"/..": "\\.",
219+
"/../": "\\.",
220+
"/hello/.": "\\hello\\.",
221221
"/hello/../.": "\\hello\\..",
222222
"//Server/Share/hello": "\\\\Server\\Share\\hello",
223-
"//Server/Share/.": "\\\\Server\\Share\\",
224-
"//Server/Share/./": "\\\\Server\\Share\\",
225-
"//Server/Share/..": "\\\\Server\\Share\\",
226-
"//Server/Share/../": "\\\\Server\\Share\\",
227-
"//Server/Share/hello/.": "\\\\Server\\Share\\hello\\",
223+
"//Server/Share/.": "\\\\Server\\Share\\.",
224+
"//Server/Share/./": "\\\\Server\\Share\\.",
225+
"//Server/Share/..": "\\\\Server\\Share\\.",
226+
"//Server/Share/../": "\\\\Server\\Share\\.",
227+
"//Server/Share/hello/.": "\\\\Server\\Share\\hello\\.",
228228
"//Server/Share/hello/../.": "\\\\Server\\Share\\hello\\..",
229229
"/\\Server\\Share/hello/../.": "\\\\Server\\Share\\hello\\..",
230-
"\\\\?\\C:\\hello\\.": "C:\\hello\\",
231-
"\\\\?\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\",
232-
"\\??\\C:\\hello\\.": "C:\\hello\\",
230+
"\\\\?\\C:\\hello\\.": "C:\\hello\\.",
231+
"\\\\?\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\.",
232+
"\\??\\C:\\hello\\.": "C:\\hello\\.",
233233
"\\??\\Z:\\file0": "Z:\\file0",
234-
"\\??\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\",
234+
"\\??\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\.",
235+
"\\\\.\\C:\\hello\\.": "C:\\hello\\.",
236+
"\\\\.\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\.",
235237
} {
236238
t.Run(from, func(t *testing.T) {
237239
builder1, scopeWalker1 := path.EmptyBuilder.Join(path.VoidScopeWalker)
@@ -445,6 +447,28 @@ func TestBuilder(t *testing.T) {
445447
require.Equal(t, "\\\\myserver\\myshare\\data.txt", mustGetWindowsString(builder))
446448
})
447449

450+
t.Run("DeviceNamespaceDrivePath", func(t *testing.T) {
451+
scopeWalker := mock.NewMockScopeWalker(ctrl)
452+
componentWalker := mock.NewMockComponentWalker(ctrl)
453+
scopeWalker.EXPECT().OnDriveLetter('C').Return(componentWalker, nil)
454+
componentWalker.EXPECT().OnTerminal(path.MustNewComponent("file.txt"))
455+
456+
builder, s := path.EmptyBuilder.Join(scopeWalker)
457+
require.NoError(t, path.Resolve(path.WindowsFormat.NewParser(`\\.\C:\file.txt`), s))
458+
require.Equal(t, "C:\\file.txt", mustGetWindowsString(builder))
459+
})
460+
461+
t.Run("DeviceNamespaceUNCPath", func(t *testing.T) {
462+
scopeWalker := mock.NewMockScopeWalker(ctrl)
463+
componentWalker := mock.NewMockComponentWalker(ctrl)
464+
scopeWalker.EXPECT().OnShare("server", "share").Return(componentWalker, nil)
465+
componentWalker.EXPECT().OnTerminal(path.MustNewComponent("file.txt"))
466+
467+
builder, s := path.EmptyBuilder.Join(scopeWalker)
468+
require.NoError(t, path.Resolve(path.WindowsFormat.NewParser(`\\.\UNC\server\share\file.txt`), s))
469+
require.Equal(t, "\\\\server\\share\\file.txt", mustGetWindowsString(builder))
470+
})
471+
448472
t.Run("RelativeDrivePaths", func(t *testing.T) {
449473
builder1, s := path.EmptyBuilder.Join(path.VoidScopeWalker)
450474
require.NoError(t, path.Resolve(path.WindowsFormat.NewParser("C:\\a\\b"), s))
@@ -471,11 +495,11 @@ func TestBuilder(t *testing.T) {
471495
t.Run("DevicePathFormat", func(t *testing.T) {
472496
t.Run("DriveLetterPaths", func(t *testing.T) {
473497
for from, expectedDevice := range map[string]string{
474-
"C:\\": "\\??\\C:\\",
498+
"C:\\": "\\??\\C:\\.",
475499
"C:\\hello": "\\??\\C:\\hello",
476-
"C:\\hello\\": "\\??\\C:\\hello\\",
500+
"C:\\hello\\": "\\??\\C:\\hello\\.",
477501
"C:\\hello\\world": "\\??\\C:\\hello\\world",
478-
"C:\\hello\\world\\": "\\??\\C:\\hello\\world\\",
502+
"C:\\hello\\world\\": "\\??\\C:\\hello\\world\\.",
479503
} {
480504
t.Run(from, func(t *testing.T) {
481505
builder, scopeWalker := path.EmptyBuilder.Join(path.VoidScopeWalker)
@@ -487,9 +511,9 @@ func TestBuilder(t *testing.T) {
487511

488512
t.Run("UNCPaths", func(t *testing.T) {
489513
for from, expectedDevice := range map[string]string{
490-
"\\\\server\\share\\": "\\??\\UNC\\server\\share\\",
514+
"\\\\server\\share\\": "\\??\\UNC\\server\\share\\.",
491515
"\\\\server\\share\\hello": "\\??\\UNC\\server\\share\\hello",
492-
"\\\\server\\share\\hello\\": "\\??\\UNC\\server\\share\\hello\\",
516+
"\\\\server\\share\\hello\\": "\\??\\UNC\\server\\share\\hello\\.",
493517
"\\\\server\\share\\hello\\world": "\\??\\UNC\\server\\share\\hello\\world",
494518
} {
495519
t.Run(from, func(t *testing.T) {
@@ -505,7 +529,7 @@ func TestBuilder(t *testing.T) {
505529
".": ".",
506530
"..": "..",
507531
"hello": "hello",
508-
"hello\\": "hello\\",
532+
"hello\\": "hello\\.",
509533
"hello\\world": "hello\\world",
510534
} {
511535
t.Run(from, func(t *testing.T) {
@@ -521,7 +545,7 @@ func TestBuilder(t *testing.T) {
521545
// Absolute paths cannot be represented as NT device
522546
// paths.
523547
for from, to := range map[string]string{
524-
"\\": "\\",
548+
"\\": "\\.",
525549
"\\hello\\world": "\\hello\\world",
526550
} {
527551
t.Run(from, func(t *testing.T) {

pkg/filesystem/path/windows_format.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,28 @@ type windowsParser struct {
6060
}
6161

6262
func (p windowsParser) ParseScope(scopeWalker ScopeWalker) (next ComponentWalker, remainder RelativeParser, err error) {
63-
// Handle extended-length paths starting with \\?\.
6463
path := p.path
64+
hasPrefix := false
6565
if len(p.path) >= 4 && p.path[0] == '\\' && p.path[1] == '\\' && p.path[2] == '?' && p.path[3] == '\\' {
66+
// Extended-length paths starting with \\?\.
6667
path = p.path[4:]
67-
// Handle \\?\UNC\.
68-
if len(path) >= 4 && strings.EqualFold(path[:4], "UNC\\") {
69-
return parseUNCPath(path[4:], scopeWalker)
70-
}
68+
hasPrefix = true
69+
} else if len(p.path) >= 4 && p.path[0] == '\\' && p.path[1] == '?' && p.path[2] == '?' && p.path[3] == '\\' {
70+
// NT object namespace paths starting with \??\.
71+
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-even/c1550f98-a1ce-426a-9991-7509e7c3787c
72+
path = p.path[4:]
73+
hasPrefix = true
74+
} else if len(p.path) >= 4 && p.path[0] == '\\' && p.path[1] == '\\' && p.path[2] == '.' && p.path[3] == '\\' {
75+
// Win32 device namespace paths starting with \\.\.
76+
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-device-namespaces
77+
path = p.path[4:]
78+
hasPrefix = true
7179
}
7280

73-
// Handle NT object namespace paths starting with \??\.
74-
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-even/c1550f98-a1ce-426a-9991-7509e7c3787c
75-
if len(p.path) >= 4 && p.path[0] == '\\' && p.path[1] == '?' && p.path[2] == '?' && p.path[3] == '\\' {
76-
path = p.path[4:]
77-
// Handle \??\UNC\
78-
if len(path) >= 4 && strings.EqualFold(path[:4], "UNC\\") {
79-
return parseUNCPath(path[4:], scopeWalker)
80-
}
81+
// Handle UNC paths following a namespace prefix
82+
// (e.g. \\?\UNC\server\share or \??\UNC\server\share).
83+
if hasPrefix && len(path) >= 4 && strings.EqualFold(path[:4], "UNC\\") {
84+
return parseUNCPath(path[4:], scopeWalker)
8185
}
8286

8387
if len(path) >= 2 {

0 commit comments

Comments
 (0)