Skip to content

Commit 7eb3472

Browse files
committed
update IsWindowsAbs code and export func
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent c2eed30 commit 7eb3472

File tree

4 files changed

+191
-37
lines changed

4 files changed

+191
-37
lines changed

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ linters:
7272
- third_party$
7373
- builtin$
7474
- examples$
75+
- paths/windows_path.go
76+
7577
issues:
7678
max-issues-per-linter: 0
7779
max-same-issues: 0

paths/unix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func (r *relativePathsResolver) maybeUnixPath(a any) (any, error) {
3535
// Note that this is not required for Docker for Windows when specifying
3636
// a local Windows path, because Docker for Windows translates the Windows
3737
// path into a valid path within the VM.
38-
if !path.IsAbs(p) && !isWindowsAbs(p) {
38+
if !path.IsAbs(p) && !IsWindowsAbs(p) {
3939
if filepath.IsAbs(p) {
4040
return p, nil
4141
}

paths/windows_path.go

Lines changed: 187 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,66 +16,218 @@
1616

1717
package paths
1818

19+
// This file contains utilities to check for Windows absolute paths on Linux.
20+
// The code in this file was largely copied from the Golang filepath package
21+
// https://github.com/golang/go/blob/master/src/internal/filepathlite/path_windows.go
22+
23+
import "slices"
24+
1925
// Copyright 2010 The Go Authors. All rights reserved.
2026
// Use of this source code is governed by a BSD-style
2127
// license that can be found in the LICENSE file.
2228
// https://github.com/golang/go/blob/master/LICENSE
2329

24-
// This file contains utilities to check for Windows absolute paths on Linux.
25-
// The code in this file was largely copied from the Golang filepath package
26-
// https://github.com/golang/go/blob/1d0e94b1e13d5e8a323a63cd1cc1ef95290c9c36/src/path/filepath/path_windows.go#L12-L65
27-
28-
func isSlash(c uint8) bool {
30+
func IsPathSeparator(c uint8) bool {
2931
return c == '\\' || c == '/'
3032
}
3133

32-
// isAbs reports whether the path is a Windows absolute path.
33-
func isWindowsAbs(path string) (b bool) {
34+
// IsWindowsAbs reports whether the path is absolute.
35+
// copied from IsAbs(path string) (b bool) from internal.filetpathlite
36+
func IsWindowsAbs(path string) (b bool) {
3437
l := volumeNameLen(path)
3538
if l == 0 {
3639
return false
3740
}
41+
// If the volume name starts with a double slash, this is an absolute path.
42+
if IsPathSeparator(path[0]) && IsPathSeparator(path[1]) {
43+
return true
44+
}
3845
path = path[l:]
3946
if path == "" {
4047
return false
4148
}
42-
return isSlash(path[0])
49+
return IsPathSeparator(path[0])
4350
}
4451

4552
// volumeNameLen returns length of the leading volume name on Windows.
4653
// It returns 0 elsewhere.
54+
//
55+
// See:
56+
// https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
57+
// https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
4758
func volumeNameLen(path string) int {
48-
if len(path) < 2 {
59+
switch {
60+
case len(path) >= 2 && path[1] == ':':
61+
// Path starts with a drive letter.
62+
//
63+
// Not all Windows functions necessarily enforce the requirement that
64+
// drive letters be in the set A-Z, and we don't try to here.
65+
//
66+
// We don't handle the case of a path starting with a non-ASCII character,
67+
// in which case the "drive letter" might be multiple bytes long.
68+
return 2
69+
70+
case len(path) == 0 || !IsPathSeparator(path[0]):
71+
// Path does not have a volume component.
4972
return 0
73+
74+
case pathHasPrefixFold(path, `\\.\UNC`):
75+
// We're going to treat the UNC host and share as part of the volume
76+
// prefix for historical reasons, but this isn't really principled;
77+
// Windows's own GetFullPathName will happily remove the first
78+
// component of the path in this space, converting
79+
// \\.\unc\a\b\..\c into \\.\unc\a\c.
80+
return uncLen(path, len(`\\.\UNC\`))
81+
82+
case pathHasPrefixFold(path, `\\.`) ||
83+
pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`):
84+
// Path starts with \\.\, and is a Local Device path; or
85+
// path starts with \\?\ or \??\ and is a Root Local Device path.
86+
//
87+
// We treat the next component after the \\.\ prefix as
88+
// part of the volume name, which means Clean(`\\?\c:\`)
89+
// won't remove the trailing \. (See #64028.)
90+
if len(path) == 3 {
91+
return 3 // exactly \\.
92+
}
93+
_, rest, ok := cutPath(path[4:])
94+
if !ok {
95+
return len(path)
96+
}
97+
return len(path) - len(rest) - 1
98+
99+
case len(path) >= 2 && IsPathSeparator(path[1]):
100+
// Path starts with \\, and is a UNC path.
101+
return uncLen(path, 2)
50102
}
51-
// with drive letter
52-
c := path[0]
53-
if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
54-
return 2
103+
return 0
104+
}
105+
106+
// pathHasPrefixFold tests whether the path s begins with prefix,
107+
// ignoring case and treating all path separators as equivalent.
108+
// If s is longer than prefix, then s[len(prefix)] must be a path separator.
109+
func pathHasPrefixFold(s, prefix string) bool {
110+
if len(s) < len(prefix) {
111+
return false
55112
}
56-
// is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
57-
if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) &&
58-
!isSlash(path[2]) && path[2] != '.' {
59-
// first, leading `\\` and next shouldn't be `\`. its server name.
60-
for n := 3; n < l-1; n++ {
61-
// second, next '\' shouldn't be repeated.
62-
if isSlash(path[n]) {
63-
n++
64-
// third, following something characters. its share name.
65-
if !isSlash(path[n]) {
66-
if path[n] == '.' {
67-
break
68-
}
69-
for ; n < l; n++ {
70-
if isSlash(path[n]) {
71-
break
72-
}
73-
}
74-
return n
75-
}
76-
break
113+
for i := 0; i < len(prefix); i++ {
114+
if IsPathSeparator(prefix[i]) {
115+
if !IsPathSeparator(s[i]) {
116+
return false
77117
}
118+
} else if toUpper(prefix[i]) != toUpper(s[i]) {
119+
return false
78120
}
79121
}
80-
return 0
122+
if len(s) > len(prefix) && !IsPathSeparator(s[len(prefix)]) {
123+
return false
124+
}
125+
return true
126+
}
127+
128+
// uncLen returns the length of the volume prefix of a UNC path.
129+
// prefixLen is the prefix prior to the start of the UNC host;
130+
// for example, for "//host/share", the prefixLen is len("//")==2.
131+
func uncLen(path string, prefixLen int) int {
132+
count := 0
133+
for i := prefixLen; i < len(path); i++ {
134+
if IsPathSeparator(path[i]) {
135+
count++
136+
if count == 2 {
137+
return i
138+
}
139+
}
140+
}
141+
return len(path)
142+
}
143+
144+
// cutPath slices path around the first path separator.
145+
func cutPath(path string) (before, after string, found bool) {
146+
for i := range path {
147+
if IsPathSeparator(path[i]) {
148+
return path[:i], path[i+1:], true
149+
}
150+
}
151+
return path, "", false
152+
}
153+
154+
// postClean adjusts the results of Clean to avoid turning a relative path
155+
// into an absolute or rooted one.
156+
func postClean(out *lazybuf) {
157+
if out.volLen != 0 || out.buf == nil {
158+
return
159+
}
160+
// If a ':' appears in the path element at the start of a path,
161+
// insert a .\ at the beginning to avoid converting relative paths
162+
// like a/../c: into c:.
163+
for _, c := range out.buf {
164+
if IsPathSeparator(c) {
165+
break
166+
}
167+
if c == ':' {
168+
out.prepend('.', Separator)
169+
return
170+
}
171+
}
172+
// If a path begins with \??\, insert a \. at the beginning
173+
// to avoid converting paths like \a\..\??\c:\x into \??\c:\x
174+
// (equivalent to c:\x).
175+
if len(out.buf) >= 3 && IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' {
176+
out.prepend(Separator, '.')
177+
}
178+
}
179+
180+
func toUpper(c byte) byte {
181+
if 'a' <= c && c <= 'z' {
182+
return c - ('a' - 'A')
183+
}
184+
return c
185+
}
186+
187+
const (
188+
Separator = '\\' // OS-specific path separator
189+
)
190+
191+
// A lazybuf is a lazily constructed path buffer.
192+
// It supports append, reading previously appended bytes,
193+
// and retrieving the final string. It does not allocate a buffer
194+
// to hold the output until that output diverges from s.
195+
type lazybuf struct {
196+
path string
197+
buf []byte
198+
w int
199+
volAndPath string
200+
volLen int
201+
}
202+
203+
func (b *lazybuf) index(i int) byte {
204+
if b.buf != nil {
205+
return b.buf[i]
206+
}
207+
return b.path[i]
208+
}
209+
210+
func (b *lazybuf) append(c byte) {
211+
if b.buf == nil {
212+
if b.w < len(b.path) && b.path[b.w] == c {
213+
b.w++
214+
return
215+
}
216+
b.buf = make([]byte, len(b.path))
217+
copy(b.buf, b.path[:b.w])
218+
}
219+
b.buf[b.w] = c
220+
b.w++
221+
}
222+
223+
func (b *lazybuf) prepend(prefix ...byte) {
224+
b.buf = slices.Insert(b.buf, 0, prefix...)
225+
b.w += len(prefix)
226+
}
227+
228+
func (b *lazybuf) string() string {
229+
if b.buf == nil {
230+
return b.volAndPath[:b.volLen+b.w]
231+
}
232+
return b.volAndPath[:b.volLen] + string(b.buf[:b.w])
81233
}

paths/windows_path_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func TestIsAbs(t *testing.T) {
7171
}
7272

7373
for _, test := range tests {
74-
if r := isWindowsAbs(test.path); r != test.isAbs {
74+
if r := IsWindowsAbs(test.path); r != test.isAbs {
7575
t.Errorf("IsAbs(%q) = %v, want %v", test.path, r, test.isAbs)
7676
}
7777
}

0 commit comments

Comments
 (0)