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