github.com/twelsh-aw/go/src@v0.0.0-20230516233729-a56fe86a7c81/path/filepath/path_windows.go (about) 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 filepath 6 7 import ( 8 "strings" 9 "syscall" 10 ) 11 12 func isSlash(c uint8) bool { 13 return c == '\\' || c == '/' 14 } 15 16 func toUpper(c byte) byte { 17 if 'a' <= c && c <= 'z' { 18 return c - ('a' - 'A') 19 } 20 return c 21 } 22 23 // isReservedName reports if name is a Windows reserved device name or a console handle. 24 // It does not detect names with an extension, which are also reserved on some Windows versions. 25 // 26 // For details, search for PRN in 27 // https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file. 28 func isReservedName(name string) bool { 29 if 3 <= len(name) && len(name) <= 4 { 30 switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) { 31 case "CON", "PRN", "AUX", "NUL": 32 return len(name) == 3 33 case "COM", "LPT": 34 return len(name) == 4 && '1' <= name[3] && name[3] <= '9' 35 } 36 } 37 // Passing CONIN$ or CONOUT$ to CreateFile opens a console handle. 38 // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles 39 // 40 // While CONIN$ and CONOUT$ aren't documented as being files, 41 // they behave the same as CON. For example, ./CONIN$ also opens the console input. 42 if len(name) == 6 && name[5] == '$' && strings.EqualFold(name, "CONIN$") { 43 return true 44 } 45 if len(name) == 7 && name[6] == '$' && strings.EqualFold(name, "CONOUT$") { 46 return true 47 } 48 return false 49 } 50 51 func isLocal(path string) bool { 52 if path == "" { 53 return false 54 } 55 if isSlash(path[0]) { 56 // Path rooted in the current drive. 57 return false 58 } 59 if strings.IndexByte(path, ':') >= 0 { 60 // Colons are only valid when marking a drive letter ("C:foo"). 61 // Rejecting any path with a colon is conservative but safe. 62 return false 63 } 64 hasDots := false // contains . or .. path elements 65 for p := path; p != ""; { 66 var part string 67 part, p, _ = cutPath(p) 68 if part == "." || part == ".." { 69 hasDots = true 70 } 71 // Trim the extension and look for a reserved name. 72 base, _, hasExt := strings.Cut(part, ".") 73 if isReservedName(base) { 74 if !hasExt { 75 return false 76 } 77 // The path element is a reserved name with an extension. Some Windows 78 // versions consider this a reserved name, while others do not. Use 79 // FullPath to see if the name is reserved. 80 // 81 // FullPath will convert references to reserved device names to their 82 // canonical form: \\.\${DEVICE_NAME} 83 // 84 // FullPath does not perform this conversion for paths which contain 85 // a reserved device name anywhere other than in the last element, 86 // so check the part rather than the full path. 87 if p, _ := syscall.FullPath(part); len(p) >= 4 && p[:4] == `\\.\` { 88 return false 89 } 90 } 91 } 92 if hasDots { 93 path = Clean(path) 94 } 95 if path == ".." || strings.HasPrefix(path, `..\`) { 96 return false 97 } 98 return true 99 } 100 101 // IsAbs reports whether the path is absolute. 102 func IsAbs(path string) (b bool) { 103 l := volumeNameLen(path) 104 if l == 0 { 105 return false 106 } 107 // If the volume name starts with a double slash, this is an absolute path. 108 if isSlash(path[0]) && isSlash(path[1]) { 109 return true 110 } 111 path = path[l:] 112 if path == "" { 113 return false 114 } 115 return isSlash(path[0]) 116 } 117 118 // volumeNameLen returns length of the leading volume name on Windows. 119 // It returns 0 elsewhere. 120 // 121 // See: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats 122 func volumeNameLen(path string) int { 123 if len(path) < 2 { 124 return 0 125 } 126 // with drive letter 127 c := path[0] 128 if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') { 129 return 2 130 } 131 // UNC and DOS device paths start with two slashes. 132 if !isSlash(path[0]) || !isSlash(path[1]) { 133 return 0 134 } 135 rest := path[2:] 136 p1, rest, _ := cutPath(rest) 137 p2, rest, ok := cutPath(rest) 138 if !ok { 139 return len(path) 140 } 141 if p1 != "." && p1 != "?" { 142 // This is a UNC path: \\${HOST}\${SHARE}\ 143 return len(path) - len(rest) - 1 144 } 145 // This is a DOS device path. 146 if len(p2) == 3 && toUpper(p2[0]) == 'U' && toUpper(p2[1]) == 'N' && toUpper(p2[2]) == 'C' { 147 // This is a DOS device path that links to a UNC: \\.\UNC\${HOST}\${SHARE}\ 148 _, rest, _ = cutPath(rest) // host 149 _, rest, ok = cutPath(rest) // share 150 if !ok { 151 return len(path) 152 } 153 } 154 return len(path) - len(rest) - 1 155 } 156 157 // cutPath slices path around the first path separator. 158 func cutPath(path string) (before, after string, found bool) { 159 for i := range path { 160 if isSlash(path[i]) { 161 return path[:i], path[i+1:], true 162 } 163 } 164 return path, "", false 165 } 166 167 // HasPrefix exists for historical compatibility and should not be used. 168 // 169 // Deprecated: HasPrefix does not respect path boundaries and 170 // does not ignore case when required. 171 func HasPrefix(p, prefix string) bool { 172 if strings.HasPrefix(p, prefix) { 173 return true 174 } 175 return strings.HasPrefix(strings.ToLower(p), strings.ToLower(prefix)) 176 } 177 178 func splitList(path string) []string { 179 // The same implementation is used in LookPath in os/exec; 180 // consider changing os/exec when changing this. 181 182 if path == "" { 183 return []string{} 184 } 185 186 // Split path, respecting but preserving quotes. 187 list := []string{} 188 start := 0 189 quo := false 190 for i := 0; i < len(path); i++ { 191 switch c := path[i]; { 192 case c == '"': 193 quo = !quo 194 case c == ListSeparator && !quo: 195 list = append(list, path[start:i]) 196 start = i + 1 197 } 198 } 199 list = append(list, path[start:]) 200 201 // Remove quotes. 202 for i, s := range list { 203 list[i] = strings.ReplaceAll(s, `"`, ``) 204 } 205 206 return list 207 } 208 209 func abs(path string) (string, error) { 210 if path == "" { 211 // syscall.FullPath returns an error on empty path, because it's not a valid path. 212 // To implement Abs behavior of returning working directory on empty string input, 213 // special-case empty path by changing it to "." path. See golang.org/issue/24441. 214 path = "." 215 } 216 fullPath, err := syscall.FullPath(path) 217 if err != nil { 218 return "", err 219 } 220 return Clean(fullPath), nil 221 } 222 223 func join(elem []string) string { 224 var b strings.Builder 225 var lastChar byte 226 for _, e := range elem { 227 switch { 228 case b.Len() == 0: 229 // Add the first non-empty path element unchanged. 230 case isSlash(lastChar): 231 // If the path ends in a slash, strip any leading slashes from the next 232 // path element to avoid creating a UNC path (any path starting with "\\") 233 // from non-UNC elements. 234 // 235 // The correct behavior for Join when the first element is an incomplete UNC 236 // path (for example, "\\") is underspecified. We currently join subsequent 237 // elements so Join("\\", "host", "share") produces "\\host\share". 238 for len(e) > 0 && isSlash(e[0]) { 239 e = e[1:] 240 } 241 case lastChar == ':': 242 // If the path ends in a colon, keep the path relative to the current directory 243 // on a drive and don't add a separator. Preserve leading slashes in the next 244 // path element, which may make the path absolute. 245 // 246 // Join(`C:`, `f`) = `C:f` 247 // Join(`C:`, `\f`) = `C:\f` 248 default: 249 // In all other cases, add a separator between elements. 250 b.WriteByte('\\') 251 lastChar = '\\' 252 } 253 if len(e) > 0 { 254 b.WriteString(e) 255 lastChar = e[len(e)-1] 256 } 257 } 258 if b.Len() == 0 { 259 return "" 260 } 261 return Clean(b.String()) 262 } 263 264 // joinNonEmpty is like join, but it assumes that the first element is non-empty. 265 func joinNonEmpty(elem []string) string { 266 if len(elem[0]) == 2 && elem[0][1] == ':' { 267 // First element is drive letter without terminating slash. 268 // Keep path relative to current directory on that drive. 269 // Skip empty elements. 270 i := 1 271 for ; i < len(elem); i++ { 272 if elem[i] != "" { 273 break 274 } 275 } 276 return Clean(elem[0] + strings.Join(elem[i:], string(Separator))) 277 } 278 // The following logic prevents Join from inadvertently creating a 279 // UNC path on Windows. Unless the first element is a UNC path, Join 280 // shouldn't create a UNC path. See golang.org/issue/9167. 281 p := Clean(strings.Join(elem, string(Separator))) 282 if !isUNC(p) { 283 return p 284 } 285 // p == UNC only allowed when the first element is a UNC path. 286 head := Clean(elem[0]) 287 if isUNC(head) { 288 return p 289 } 290 // head + tail == UNC, but joining two non-UNC paths should not result 291 // in a UNC path. Undo creation of UNC path. 292 tail := Clean(strings.Join(elem[1:], string(Separator))) 293 if head[len(head)-1] == Separator { 294 return head + tail 295 } 296 return head + string(Separator) + tail 297 } 298 299 // isUNC reports whether path is a UNC path. 300 func isUNC(path string) bool { 301 return len(path) > 1 && isSlash(path[0]) && isSlash(path[1]) 302 } 303 304 func sameWord(a, b string) bool { 305 return strings.EqualFold(a, b) 306 }