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