Skip to content

Commit fd11d3e

Browse files
tomgrEdSchouten
authored andcommitted
Support additional path formats on Windows
This supports paths on Windows that start with: * a \\??\ prefix (extended length paths) * a \??\ prefix (NT object namespace paths) These are needed for the WinFSP support in buildbarn/bb-remote-execution#185 as that generates some exotic paths.
1 parent 7e9fd4e commit fd11d3e

2 files changed

Lines changed: 122 additions & 52 deletions

File tree

pkg/filesystem/path/builder_test.go

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -200,25 +200,30 @@ func TestBuilder(t *testing.T) {
200200

201201
t.Run("WindowsNormalized", func(t *testing.T) {
202202
for from, to := range map[string]string{
203-
"": ".",
204-
"./": ".",
205-
"./.": ".",
206-
"../": "..",
207-
"../.": "..",
208-
"/.": "\\",
209-
"/./": "\\",
210-
"/..": "\\",
211-
"/../": "\\",
212-
"/hello/.": "\\hello\\",
213-
"/hello/../.": "\\hello\\..",
214-
"//Server/Share/hello": "\\\\Server\\Share\\hello",
215-
"//Server/Share/.": "\\\\Server\\Share\\",
216-
"//Server/Share/./": "\\\\Server\\Share\\",
217-
"//Server/Share/..": "\\\\Server\\Share\\",
218-
"//Server/Share/../": "\\\\Server\\Share\\",
219-
"//Server/Share/hello/.": "\\\\Server\\Share\\hello\\",
220-
"//Server/Share/hello/../.": "\\\\Server\\Share\\hello\\..",
221-
"/\\Server\\Share/hello/../.": "\\\\Server\\Share\\hello\\..",
203+
"": ".",
204+
"./": ".",
205+
"./.": ".",
206+
"../": "..",
207+
"../.": "..",
208+
"/.": "\\",
209+
"/./": "\\",
210+
"/..": "\\",
211+
"/../": "\\",
212+
"/hello/.": "\\hello\\",
213+
"/hello/../.": "\\hello\\..",
214+
"//Server/Share/hello": "\\\\Server\\Share\\hello",
215+
"//Server/Share/.": "\\\\Server\\Share\\",
216+
"//Server/Share/./": "\\\\Server\\Share\\",
217+
"//Server/Share/..": "\\\\Server\\Share\\",
218+
"//Server/Share/../": "\\\\Server\\Share\\",
219+
"//Server/Share/hello/.": "\\\\Server\\Share\\hello\\",
220+
"//Server/Share/hello/../.": "\\\\Server\\Share\\hello\\..",
221+
"/\\Server\\Share/hello/../.": "\\\\Server\\Share\\hello\\..",
222+
"\\\\?\\C:\\hello\\.": "C:\\hello\\",
223+
"\\\\?\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\",
224+
"\\??\\C:\\hello\\.": "C:\\hello\\",
225+
"\\??\\Z:\\file0": "Z:\\file0",
226+
"\\??\\UNC\\Server\\Share\\hello\\.": "\\\\Server\\Share\\hello\\",
222227
} {
223228
t.Run(from, func(t *testing.T) {
224229
builder1, scopeWalker1 := path.EmptyBuilder.Join(path.VoidScopeWalker)
@@ -387,4 +392,48 @@ func TestBuilder(t *testing.T) {
387392
require.NoError(t, path.Resolve(path.WindowsFormat.NewParser("\\\\server\\share\\file.txt"), s))
388393
require.Equal(t, "\\\\server\\share\\file.txt", mustGetWindowsString(builder))
389394
})
395+
396+
t.Run("ExtendedDrivePath", func(t *testing.T) {
397+
scopeWalker := mock.NewMockScopeWalker(ctrl)
398+
componentWalker := mock.NewMockComponentWalker(ctrl)
399+
scopeWalker.EXPECT().OnDriveLetter('C').Return(componentWalker, nil)
400+
componentWalker.EXPECT().OnTerminal(path.MustNewComponent("file.txt"))
401+
402+
builder, s := path.EmptyBuilder.Join(scopeWalker)
403+
require.NoError(t, path.Resolve(path.WindowsFormat.NewParser("\\\\?\\C:\\file.txt"), s))
404+
require.Equal(t, "C:\\file.txt", mustGetWindowsString(builder))
405+
})
406+
407+
t.Run("ExtendedUNCPath", func(t *testing.T) {
408+
scopeWalker := mock.NewMockScopeWalker(ctrl)
409+
componentWalker := mock.NewMockComponentWalker(ctrl)
410+
scopeWalker.EXPECT().OnShare("server", "share").Return(componentWalker, nil)
411+
componentWalker.EXPECT().OnTerminal(path.MustNewComponent("file.txt"))
412+
413+
builder, s := path.EmptyBuilder.Join(scopeWalker)
414+
require.NoError(t, path.Resolve(path.WindowsFormat.NewParser("\\\\?\\UNC\\server\\share\\file.txt"), s))
415+
require.Equal(t, "\\\\server\\share\\file.txt", mustGetWindowsString(builder))
416+
})
417+
418+
t.Run("NTObjectNamespaceDrivePath", func(t *testing.T) {
419+
scopeWalker := mock.NewMockScopeWalker(ctrl)
420+
componentWalker := mock.NewMockComponentWalker(ctrl)
421+
scopeWalker.EXPECT().OnDriveLetter('Z').Return(componentWalker, nil)
422+
componentWalker.EXPECT().OnTerminal(path.MustNewComponent("file0"))
423+
424+
builder, s := path.EmptyBuilder.Join(scopeWalker)
425+
require.NoError(t, path.Resolve(path.WindowsFormat.NewParser("\\??\\Z:\\file0"), s))
426+
require.Equal(t, "Z:\\file0", mustGetWindowsString(builder))
427+
})
428+
429+
t.Run("NTObjectNamespaceUNCPath", func(t *testing.T) {
430+
scopeWalker := mock.NewMockScopeWalker(ctrl)
431+
componentWalker := mock.NewMockComponentWalker(ctrl)
432+
scopeWalker.EXPECT().OnShare("myserver", "myshare").Return(componentWalker, nil)
433+
componentWalker.EXPECT().OnTerminal(path.MustNewComponent("data.txt"))
434+
435+
builder, s := path.EmptyBuilder.Join(scopeWalker)
436+
require.NoError(t, path.Resolve(path.WindowsFormat.NewParser("\\??\\UNC\\myserver\\myshare\\data.txt"), s))
437+
require.Equal(t, "\\\\myserver\\myshare\\data.txt", mustGetWindowsString(builder))
438+
})
390439
}

pkg/filesystem/path/windows_format.go

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,63 +30,84 @@ func stripWindowsSeparators(p string) string {
3030
return p
3131
}
3232

33+
func parseUNCPath(uncPath string, scopeWalker ScopeWalker) (ComponentWalker, RelativeParser, error) {
34+
serverLen := strings.IndexAny(uncPath, "\\/")
35+
if serverLen == -1 {
36+
return nil, nil, status.Error(codes.InvalidArgument, "Invalid UNC path: expected a non-empty server and share name")
37+
}
38+
if serverLen < 1 {
39+
return nil, nil, status.Error(codes.InvalidArgument, "Invalid UNC path: expected a non-empty server name")
40+
}
41+
42+
server := uncPath[:serverLen]
43+
shareStart := serverLen + 1
44+
shareLen := strings.IndexAny(uncPath[shareStart:], "\\/")
45+
if shareLen < 1 {
46+
return nil, nil, status.Error(codes.InvalidArgument, "Invalid UNC path: expected a non-empty share name")
47+
}
48+
share := uncPath[shareStart : shareStart+shareLen]
49+
remainder := uncPath[shareStart+shareLen+1:]
50+
51+
next, err := scopeWalker.OnShare(server, share)
52+
if err != nil {
53+
return nil, nil, err
54+
}
55+
return next, windowsRelativeParser{remainder}, nil
56+
}
57+
3358
type windowsParser struct {
3459
path string
3560
}
3661

3762
func (p windowsParser) ParseScope(scopeWalker ScopeWalker) (next ComponentWalker, remainder RelativeParser, err error) {
38-
if len(p.path) >= 2 {
39-
upperDriveLetter := p.path[0] &^ 0x20
40-
if upperDriveLetter >= 'A' && upperDriveLetter <= 'Z' && p.path[1] == ':' {
63+
// Handle extended-length paths starting with \\?\.
64+
path := p.path
65+
if len(p.path) >= 4 && p.path[0] == '\\' && p.path[1] == '\\' && p.path[2] == '?' && p.path[3] == '\\' {
66+
path = p.path[4:]
67+
// Handle \\?\UNC\.
68+
if len(path) >= 4 && strings.EqualFold(path[:4], "UNC\\") {
69+
return parseUNCPath(path[4:], scopeWalker)
70+
}
71+
}
72+
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+
}
82+
83+
if len(path) >= 2 {
84+
upperDriveLetter := path[0] &^ 0x20
85+
if upperDriveLetter >= 'A' && upperDriveLetter <= 'Z' && path[1] == ':' {
4186
next, err = scopeWalker.OnDriveLetter(rune(upperDriveLetter))
4287
if err != nil {
4388
return nil, nil, err
4489
}
45-
return next, windowsRelativeParser{stripWindowsSeparators(p.path[2:])}, nil
90+
return next, windowsRelativeParser{stripWindowsSeparators(path[2:])}, nil
4691
}
4792

48-
if (p.path[0] == '\\' || p.path[0] == '/') && (p.path[1] == '\\' || p.path[1] == '/') {
49-
serverStart := 2
50-
serverLen := strings.IndexAny(p.path[serverStart:], "\\/")
51-
if serverLen == -1 {
52-
return nil, nil, status.Error(codes.InvalidArgument, "Invalid UNC path: expected a non-empty server and share name")
53-
}
54-
if serverLen < 1 {
55-
return nil, nil, status.Error(codes.InvalidArgument, "Invalid UNC path: expected a non-empty server name")
56-
}
57-
// This is a UNC-style path. UNC paths are formatted as:
58-
// \\server\share\path
59-
// The format is not very well-specified, so we are tolerant
60-
// to slashes in either direction.
61-
server := p.path[serverStart : serverStart+serverLen]
62-
shareStart := serverStart + serverLen + 1
63-
shareLen := strings.IndexAny(p.path[shareStart:], "\\/")
64-
if shareLen < 1 {
65-
return nil, nil, status.Error(codes.InvalidArgument, "Invalid UNC path: expected a non-empty share name")
66-
}
67-
share := p.path[shareStart : shareStart+shareLen]
68-
remainder := p.path[shareStart+shareLen+1:]
69-
next, err = scopeWalker.OnShare(server, share)
70-
if err != nil {
71-
return nil, nil, err
72-
}
73-
return next, windowsRelativeParser{remainder}, nil
93+
if (path[0] == '\\' || path[0] == '/') && (path[1] == '\\' || path[1] == '/') {
94+
return parseUNCPath(path[2:], scopeWalker)
7495
}
7596
}
7697

77-
if len(p.path) >= 1 && (p.path[0] == '\\' || p.path[0] == '/') {
98+
if len(path) >= 1 && (path[0] == '\\' || path[0] == '/') {
7899
next, err = scopeWalker.OnAbsolute()
79100
if err != nil {
80101
return nil, nil, err
81102
}
82-
return next, windowsRelativeParser{stripWindowsSeparators(p.path)}, nil
103+
return next, windowsRelativeParser{stripWindowsSeparators(path)}, nil
83104
}
84105

85106
next, err = scopeWalker.OnRelative()
86107
if err != nil {
87108
return nil, nil, err
88109
}
89-
return next, windowsRelativeParser{p.path}, nil
110+
return next, windowsRelativeParser{path}, nil
90111
}
91112

92113
type windowsRelativeParser struct {

0 commit comments

Comments
 (0)