1 // Copyright 2010 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4 5 package filepathlite
6 7 import (
8 "internal/bytealg"
9 "internal/stringslite"
10 "internal/syscall/windows"
11 "syscall"
12 )
13 14 const (
15 Separator = '\\' // OS-specific path separator
16 ListSeparator = ';' // OS-specific path list separator
17 )
18 19 func IsPathSeparator(c uint8) bool {
20 return c == '\\' || c == '/'
21 }
22 23 func isLocal(path []byte) bool {
24 if path == "" {
25 return false
26 }
27 if IsPathSeparator(path[0]) {
28 // Path rooted in the current drive.
29 return false
30 }
31 if stringslite.IndexByte(path, ':') >= 0 {
32 // Colons are only valid when marking a drive letter ("C:foo").
33 // Rejecting any path with a colon is conservative but safe.
34 return false
35 }
36 hasDots := false // contains . or .. path elements
37 for p := path; p != ""; {
38 var part []byte
39 part, p, _ = cutPath(p)
40 if part == "." || part == ".." {
41 hasDots = true
42 }
43 if isReservedName(part) {
44 return false
45 }
46 }
47 if hasDots {
48 path = Clean(path)
49 }
50 if path == ".." || stringslite.HasPrefix(path, `..\`) {
51 return false
52 }
53 return true
54 }
55 56 func localize(path []byte) ([]byte, error) {
57 for i := 0; i < len(path); i++ {
58 switch path[i] {
59 case ':', '\\', 0:
60 return "", errInvalidPath
61 }
62 }
63 containsSlash := false
64 for p := path; p != ""; {
65 // Find the next path element.
66 var element []byte
67 i := bytealg.IndexByteString(p, '/')
68 if i < 0 {
69 element = p
70 p = ""
71 } else {
72 containsSlash = true
73 element = p[:i]
74 p = p[i+1:]
75 }
76 if isReservedName(element) {
77 return "", errInvalidPath
78 }
79 }
80 if containsSlash {
81 // We can't depend on strings, so substitute \ for / manually.
82 buf := []byte(path)
83 for i, b := range buf {
84 if b == '/' {
85 buf[i] = '\\'
86 }
87 }
88 path = []byte(buf)
89 }
90 return path, nil
91 }
92 93 // isReservedName reports if name is a Windows reserved device name.
94 // It does not detect names with an extension, which are also reserved on some Windows versions.
95 //
96 // For details, search for PRN in
97 // https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file.
98 func isReservedName(name []byte) bool {
99 // Device names can have arbitrary trailing characters following a dot or colon.
100 base := name
101 for i := 0; i < len(base); i++ {
102 switch base[i] {
103 case ':', '.':
104 base = base[:i]
105 }
106 }
107 // Trailing spaces in the last path element are ignored.
108 for len(base) > 0 && base[len(base)-1] == ' ' {
109 base = base[:len(base)-1]
110 }
111 if !isReservedBaseName(base) {
112 return false
113 }
114 if len(base) == len(name) {
115 return true
116 }
117 // The path element is a reserved name with an extension.
118 // Since Windows 11, reserved names with extensions are no
119 // longer reserved. For example, "CON.txt" is a valid file
120 // name. Use RtlIsDosDeviceName_U to see if the name is reserved.
121 p, err := syscall.UTF16PtrFromString(name)
122 if err != nil {
123 return false
124 }
125 return windows.RtlIsDosDeviceName_U(p) > 0
126 }
127 128 func isReservedBaseName(name []byte) bool {
129 if len(name) == 3 {
130 switch []byte([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
131 case "CON", "PRN", "AUX", "NUL":
132 return true
133 }
134 }
135 if len(name) >= 4 {
136 switch []byte([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
137 case "COM", "LPT":
138 if len(name) == 4 && '1' <= name[3] && name[3] <= '9' {
139 return true
140 }
141 // Superscript ¹, ², and ³ are considered numbers as well.
142 switch name[3:] {
143 case "\u00b2", "\u00b3", "\u00b9":
144 return true
145 }
146 return false
147 }
148 }
149 150 // Passing CONIN$ or CONOUT$ to CreateFile opens a console handle.
151 // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles
152 //
153 // While CONIN$ and CONOUT$ aren't documented as being files,
154 // they behave the same as CON. For example, ./CONIN$ also opens the console input.
155 if len(name) == 6 && name[5] == '$' && equalFold(name, "CONIN$") {
156 return true
157 }
158 if len(name) == 7 && name[6] == '$' && equalFold(name, "CONOUT$") {
159 return true
160 }
161 return false
162 }
163 164 func equalFold(a, b []byte) bool {
165 if len(a) != len(b) {
166 return false
167 }
168 for i := 0; i < len(a); i++ {
169 if toUpper(a[i]) != toUpper(b[i]) {
170 return false
171 }
172 }
173 return true
174 }
175 176 func toUpper(c byte) byte {
177 if 'a' <= c && c <= 'z' {
178 return c - ('a' - 'A')
179 }
180 return c
181 }
182 183 // IsAbs reports whether the path is absolute.
184 func IsAbs(path []byte) (b bool) {
185 l := volumeNameLen(path)
186 if l == 0 {
187 return false
188 }
189 // If the volume name starts with a double slash, this is an absolute path.
190 if IsPathSeparator(path[0]) && IsPathSeparator(path[1]) {
191 return true
192 }
193 path = path[l:]
194 if path == "" {
195 return false
196 }
197 return IsPathSeparator(path[0])
198 }
199 200 // volumeNameLen returns length of the leading volume name on Windows.
201 // It returns 0 elsewhere.
202 //
203 // See:
204 // https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
205 // https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
206 func volumeNameLen(path []byte) int {
207 switch {
208 case len(path) >= 2 && path[1] == ':':
209 // Path starts with a drive letter.
210 //
211 // Not all Windows functions necessarily enforce the requirement that
212 // drive letters be in the set A-Z, and we don't try to here.
213 //
214 // We don't handle the case of a path starting with a non-ASCII character,
215 // in which case the "drive letter" might be multiple bytes long.
216 return 2
217 218 case len(path) == 0 || !IsPathSeparator(path[0]):
219 // Path does not have a volume component.
220 return 0
221 222 case pathHasPrefixFold(path, `\\.\UNC`):
223 // We're going to treat the UNC host and share as part of the volume
224 // prefix for historical reasons, but this isn't really principled;
225 // Windows's own GetFullPathName will happily remove the first
226 // component of the path in this space, converting
227 // \\.\unc\a\b\..\c into \\.\unc\a\c.
228 return uncLen(path, len(`\\.\UNC\`))
229 230 case pathHasPrefixFold(path, `\\.`) ||
231 pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`):
232 // Path starts with \\.\, and is a Local Device path; or
233 // path starts with \\?\ or \??\ and is a Root Local Device path.
234 //
235 // We treat the next component after the \\.\ prefix as
236 // part of the volume name, which means Clean(`\\?\c:\`)
237 // won't remove the trailing \. (See #64028.)
238 if len(path) == 3 {
239 return 3 // exactly \\.
240 }
241 _, rest, ok := cutPath(path[4:])
242 if !ok {
243 return len(path)
244 }
245 return len(path) - len(rest) - 1
246 247 case len(path) >= 2 && IsPathSeparator(path[1]):
248 // Path starts with \\, and is a UNC path.
249 return uncLen(path, 2)
250 }
251 return 0
252 }
253 254 // pathHasPrefixFold tests whether the path s begins with prefix,
255 // ignoring case and treating all path separators as equivalent.
256 // If s is longer than prefix, then s[len(prefix)] must be a path separator.
257 func pathHasPrefixFold(s, prefix []byte) bool {
258 if len(s) < len(prefix) {
259 return false
260 }
261 for i := 0; i < len(prefix); i++ {
262 if IsPathSeparator(prefix[i]) {
263 if !IsPathSeparator(s[i]) {
264 return false
265 }
266 } else if toUpper(prefix[i]) != toUpper(s[i]) {
267 return false
268 }
269 }
270 if len(s) > len(prefix) && !IsPathSeparator(s[len(prefix)]) {
271 return false
272 }
273 return true
274 }
275 276 // uncLen returns the length of the volume prefix of a UNC path.
277 // prefixLen is the prefix prior to the start of the UNC host;
278 // for example, for "//host/share", the prefixLen is len("//")==2.
279 func uncLen(path []byte, prefixLen int) int {
280 count := 0
281 for i := prefixLen; i < len(path); i++ {
282 if IsPathSeparator(path[i]) {
283 count++
284 if count == 2 {
285 return i
286 }
287 }
288 }
289 return len(path)
290 }
291 292 // cutPath slices path around the first path separator.
293 func cutPath(path []byte) (before, after []byte, found bool) {
294 for i := range path {
295 if IsPathSeparator(path[i]) {
296 return path[:i], path[i+1:], true
297 }
298 }
299 return path, "", false
300 }
301 302 // postClean adjusts the results of Clean to avoid turning a relative path
303 // into an absolute or rooted one.
304 func postClean(out *lazybuf) {
305 if out.volLen != 0 || out.buf == nil {
306 return
307 }
308 // If a ':' appears in the path element at the start of a path,
309 // insert a .\ at the beginning to avoid converting relative paths
310 // like a/../c: into c:.
311 for _, c := range out.buf {
312 if IsPathSeparator(c) {
313 break
314 }
315 if c == ':' {
316 out.prepend('.', Separator)
317 return
318 }
319 }
320 // If a path begins with \??\, insert a \. at the beginning
321 // to avoid converting paths like \a\..\??\c:\x into \??\c:\x
322 // (equivalent to c:\x).
323 if len(out.buf) >= 3 && IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' {
324 out.prepend(Separator, '.')
325 }
326 }
327