github.com/evanw/esbuild@v0.21.4/internal/fs/filepath.go (about) 1 // Code in this file has been forked from the "filepath" module in the Go 2 // source code to work around bugs with the WebAssembly build target. More 3 // information about why here: https://github.com/golang/go/issues/43768. 4 5 //////////////////////////////////////////////////////////////////////////////// 6 7 // Copyright (c) 2009 The Go Authors. All rights reserved. 8 // 9 // Redistribution and use in source and binary forms, with or without 10 // modification, are permitted provided that the following conditions are 11 // met: 12 // 13 // * Redistributions of source code must retain the above copyright 14 // notice, this list of conditions and the following disclaimer. 15 // * Redistributions in binary form must reproduce the above 16 // copyright notice, this list of conditions and the following disclaimer 17 // in the documentation and/or other materials provided with the 18 // distribution. 19 // * Neither the name of Google Inc. nor the names of its 20 // contributors may be used to endorse or promote products derived from 21 // this software without specific prior written permission. 22 // 23 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 29 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 35 package fs 36 37 import ( 38 "errors" 39 "os" 40 "strings" 41 "syscall" 42 ) 43 44 type goFilepath struct { 45 cwd string 46 isWindows bool 47 pathSeparator byte 48 } 49 50 func isSlash(c uint8) bool { 51 return c == '\\' || c == '/' 52 } 53 54 // reservedNames lists reserved Windows names. Search for PRN in 55 // https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file 56 // for details. 57 var reservedNames = []string{ 58 "CON", "PRN", "AUX", "NUL", 59 "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", 60 "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", 61 } 62 63 // isReservedName returns true, if path is Windows reserved name. 64 // See reservedNames for the full list. 65 func isReservedName(path string) bool { 66 if len(path) == 0 { 67 return false 68 } 69 for _, reserved := range reservedNames { 70 if strings.EqualFold(path, reserved) { 71 return true 72 } 73 } 74 return false 75 } 76 77 // IsAbs reports whether the path is absolute. 78 func (fp goFilepath) isAbs(path string) bool { 79 if !fp.isWindows { 80 return strings.HasPrefix(path, "/") 81 } 82 if isReservedName(path) { 83 return true 84 } 85 l := fp.volumeNameLen(path) 86 if l == 0 { 87 return false 88 } 89 path = path[l:] 90 if path == "" { 91 return false 92 } 93 return isSlash(path[0]) 94 } 95 96 // Abs returns an absolute representation of path. 97 // If the path is not absolute it will be joined with the current 98 // working directory to turn it into an absolute path. The absolute 99 // path name for a given file is not guaranteed to be unique. 100 // Abs calls Clean on the result. 101 func (fp goFilepath) abs(path string) (string, error) { 102 if fp.isAbs(path) { 103 return fp.clean(path), nil 104 } 105 return fp.join([]string{fp.cwd, path}), nil 106 } 107 108 // IsPathSeparator reports whether c is a directory separator character. 109 func (fp goFilepath) isPathSeparator(c uint8) bool { 110 return c == '/' || (fp.isWindows && c == '\\') 111 } 112 113 // volumeNameLen returns length of the leading volume name on Windows. 114 // It returns 0 elsewhere. 115 func (fp goFilepath) volumeNameLen(path string) int { 116 if !fp.isWindows { 117 return 0 118 } 119 if len(path) < 2 { 120 return 0 121 } 122 // with drive letter 123 c := path[0] 124 if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') { 125 return 2 126 } 127 // is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx 128 if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) && 129 !isSlash(path[2]) && path[2] != '.' { 130 // first, leading `\\` and next shouldn't be `\`. its server name. 131 for n := 3; n < l-1; n++ { 132 // second, next '\' shouldn't be repeated. 133 if isSlash(path[n]) { 134 n++ 135 // third, following something characters. its share name. 136 if !isSlash(path[n]) { 137 if path[n] == '.' { 138 break 139 } 140 for ; n < l; n++ { 141 if isSlash(path[n]) { 142 break 143 } 144 } 145 return n 146 } 147 break 148 } 149 } 150 } 151 return 0 152 } 153 154 // EvalSymlinks returns the path name after the evaluation of any symbolic 155 // links. 156 // If path is relative the result will be relative to the current directory, 157 // unless one of the components is an absolute symbolic link. 158 // EvalSymlinks calls Clean on the result. 159 func (fp goFilepath) evalSymlinks(path string) (string, error) { 160 volLen := fp.volumeNameLen(path) 161 pathSeparator := string(fp.pathSeparator) 162 163 if volLen < len(path) && fp.isPathSeparator(path[volLen]) { 164 volLen++ 165 } 166 vol := path[:volLen] 167 dest := vol 168 linksWalked := 0 169 for start, end := volLen, volLen; start < len(path); start = end { 170 for start < len(path) && fp.isPathSeparator(path[start]) { 171 start++ 172 } 173 end = start 174 for end < len(path) && !fp.isPathSeparator(path[end]) { 175 end++ 176 } 177 178 // On Windows, "." can be a symlink. 179 // We look it up, and use the value if it is absolute. 180 // If not, we just return ".". 181 isWindowsDot := fp.isWindows && path[fp.volumeNameLen(path):] == "." 182 183 // The next path component is in path[start:end]. 184 if end == start { 185 // No more path components. 186 break 187 } else if path[start:end] == "." && !isWindowsDot { 188 // Ignore path component ".". 189 continue 190 } else if path[start:end] == ".." { 191 // Back up to previous component if possible. 192 // Note that volLen includes any leading slash. 193 194 // Set r to the index of the last slash in dest, 195 // after the volume. 196 var r int 197 for r = len(dest) - 1; r >= volLen; r-- { 198 if fp.isPathSeparator(dest[r]) { 199 break 200 } 201 } 202 if r < volLen || dest[r+1:] == ".." { 203 // Either path has no slashes 204 // (it's empty or just "C:") 205 // or it ends in a ".." we had to keep. 206 // Either way, keep this "..". 207 if len(dest) > volLen { 208 dest += pathSeparator 209 } 210 dest += ".." 211 } else { 212 // Discard everything since the last slash. 213 dest = dest[:r] 214 } 215 continue 216 } 217 218 // Ordinary path component. Add it to result. 219 220 if len(dest) > fp.volumeNameLen(dest) && !fp.isPathSeparator(dest[len(dest)-1]) { 221 dest += pathSeparator 222 } 223 224 dest += path[start:end] 225 226 // Resolve symlink. 227 228 fi, err := os.Lstat(dest) 229 if err != nil { 230 return "", err 231 } 232 233 if fi.Mode()&os.ModeSymlink == 0 { 234 if !fi.Mode().IsDir() && end < len(path) { 235 return "", syscall.ENOTDIR 236 } 237 continue 238 } 239 240 // Found symlink. 241 242 linksWalked++ 243 if linksWalked > 255 { 244 return "", errors.New("EvalSymlinks: too many links") 245 } 246 247 link, err := os.Readlink(dest) 248 if err != nil { 249 return "", err 250 } 251 252 if isWindowsDot && !fp.isAbs(link) { 253 // On Windows, if "." is a relative symlink, 254 // just return ".". 255 break 256 } 257 258 path = link + path[end:] 259 260 v := fp.volumeNameLen(link) 261 if v > 0 { 262 // Symlink to drive name is an absolute path. 263 if v < len(link) && fp.isPathSeparator(link[v]) { 264 v++ 265 } 266 vol = link[:v] 267 dest = vol 268 end = len(vol) 269 } else if len(link) > 0 && fp.isPathSeparator(link[0]) { 270 // Symlink to absolute path. 271 dest = link[:1] 272 end = 1 273 } else { 274 // Symlink to relative path; replace last 275 // path component in dest. 276 var r int 277 for r = len(dest) - 1; r >= volLen; r-- { 278 if fp.isPathSeparator(dest[r]) { 279 break 280 } 281 } 282 if r < volLen { 283 dest = vol 284 } else { 285 dest = dest[:r] 286 } 287 end = 0 288 } 289 } 290 return fp.clean(dest), nil 291 } 292 293 // A lazybuf is a lazily constructed path buffer. 294 // It supports append, reading previously appended bytes, 295 // and retrieving the final string. It does not allocate a buffer 296 // to hold the output until that output diverges from s. 297 type lazybuf struct { 298 path string 299 volAndPath string 300 buf []byte 301 w int 302 volLen int 303 } 304 305 func (b *lazybuf) index(i int) byte { 306 if b.buf != nil { 307 return b.buf[i] 308 } 309 return b.path[i] 310 } 311 312 func (b *lazybuf) append(c byte) { 313 if b.buf == nil { 314 if b.w < len(b.path) && b.path[b.w] == c { 315 b.w++ 316 return 317 } 318 b.buf = make([]byte, len(b.path)) 319 copy(b.buf, b.path[:b.w]) 320 } 321 b.buf[b.w] = c 322 b.w++ 323 } 324 325 func (b *lazybuf) string() string { 326 if b.buf == nil { 327 return b.volAndPath[:b.volLen+b.w] 328 } 329 return b.volAndPath[:b.volLen] + string(b.buf[:b.w]) 330 } 331 332 // FromSlash returns the result of replacing each slash ('/') character 333 // in path with a separator character. Multiple slashes are replaced 334 // by multiple separators. 335 func (fp goFilepath) fromSlash(path string) string { 336 if !fp.isWindows { 337 return path 338 } 339 return strings.ReplaceAll(path, "/", "\\") 340 } 341 342 // Clean returns the shortest path name equivalent to path 343 // by purely lexical processing. It applies the following rules 344 // iteratively until no further processing can be done: 345 // 346 // 1. Replace multiple Separator elements with a single one. 347 // 2. Eliminate each . path name element (the current directory). 348 // 3. Eliminate each inner .. path name element (the parent directory) 349 // along with the non-.. element that precedes it. 350 // 4. Eliminate .. elements that begin a rooted path: 351 // that is, replace "/.." by "/" at the beginning of a path, 352 // assuming Separator is '/'. 353 // 354 // The returned path ends in a slash only if it represents a root directory, 355 // such as "/" on Unix or `C:\` on Windows. 356 // 357 // Finally, any occurrences of slash are replaced by Separator. 358 // 359 // If the result of this process is an empty string, Clean 360 // returns the string ".". 361 // 362 // See also Rob Pike, "Lexical File Names in Plan 9 or 363 // Getting Dot-Dot Right," 364 // https://9p.io/sys/doc/lexnames.html 365 func (fp goFilepath) clean(path string) string { 366 originalPath := path 367 volLen := fp.volumeNameLen(path) 368 path = path[volLen:] 369 if path == "" { 370 if volLen > 1 && originalPath[1] != ':' { 371 // should be UNC 372 return fp.fromSlash(originalPath) 373 } 374 return originalPath + "." 375 } 376 rooted := fp.isPathSeparator(path[0]) 377 378 // Invariants: 379 // reading from path; r is index of next byte to process. 380 // writing to buf; w is index of next byte to write. 381 // dotdot is index in buf where .. must stop, either because 382 // it is the leading slash or it is a leading ../../.. prefix. 383 n := len(path) 384 out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen} 385 r, dotdot := 0, 0 386 if rooted { 387 out.append(fp.pathSeparator) 388 r, dotdot = 1, 1 389 } 390 391 for r < n { 392 switch { 393 case fp.isPathSeparator(path[r]): 394 // empty path element 395 r++ 396 case path[r] == '.' && (r+1 == n || fp.isPathSeparator(path[r+1])): 397 // . element 398 r++ 399 case path[r] == '.' && path[r+1] == '.' && (r+2 == n || fp.isPathSeparator(path[r+2])): 400 // .. element: remove to last separator 401 r += 2 402 switch { 403 case out.w > dotdot: 404 // can backtrack 405 out.w-- 406 for out.w > dotdot && !fp.isPathSeparator(out.index(out.w)) { 407 out.w-- 408 } 409 case !rooted: 410 // cannot backtrack, but not rooted, so append .. element. 411 if out.w > 0 { 412 out.append(fp.pathSeparator) 413 } 414 out.append('.') 415 out.append('.') 416 dotdot = out.w 417 } 418 default: 419 // real path element. 420 // add slash if needed 421 if rooted && out.w != 1 || !rooted && out.w != 0 { 422 out.append(fp.pathSeparator) 423 } 424 // copy element 425 for ; r < n && !fp.isPathSeparator(path[r]); r++ { 426 out.append(path[r]) 427 } 428 } 429 } 430 431 // Turn empty string into "." 432 if out.w == 0 { 433 out.append('.') 434 } 435 436 return fp.fromSlash(out.string()) 437 } 438 439 // VolumeName returns leading volume name. 440 // Given "C:\foo\bar" it returns "C:" on Windows. 441 // Given "\\host\share\foo" it returns "\\host\share". 442 // On other platforms it returns "". 443 func (fp goFilepath) volumeName(path string) string { 444 return path[:fp.volumeNameLen(path)] 445 } 446 447 // Base returns the last element of path. 448 // Trailing path separators are removed before extracting the last element. 449 // If the path is empty, Base returns ".". 450 // If the path consists entirely of separators, Base returns a single separator. 451 func (fp goFilepath) base(path string) string { 452 if path == "" { 453 return "." 454 } 455 // Strip trailing slashes. 456 for len(path) > 0 && fp.isPathSeparator(path[len(path)-1]) { 457 path = path[0 : len(path)-1] 458 } 459 // Throw away volume name 460 path = path[len(fp.volumeName(path)):] 461 // Find the last element 462 i := len(path) - 1 463 for i >= 0 && !fp.isPathSeparator(path[i]) { 464 i-- 465 } 466 if i >= 0 { 467 path = path[i+1:] 468 } 469 // If empty now, it had only slashes. 470 if path == "" { 471 return string(fp.pathSeparator) 472 } 473 return path 474 } 475 476 // Dir returns all but the last element of path, typically the path's directory. 477 // After dropping the final element, Dir calls Clean on the path and trailing 478 // slashes are removed. 479 // If the path is empty, Dir returns ".". 480 // If the path consists entirely of separators, Dir returns a single separator. 481 // The returned path does not end in a separator unless it is the root directory. 482 func (fp goFilepath) dir(path string) string { 483 vol := fp.volumeName(path) 484 i := len(path) - 1 485 for i >= len(vol) && !fp.isPathSeparator(path[i]) { 486 i-- 487 } 488 dir := fp.clean(path[len(vol) : i+1]) 489 if dir == "." && len(vol) > 2 { 490 // must be UNC 491 return vol 492 } 493 return vol + dir 494 } 495 496 // Ext returns the file name extension used by path. 497 // The extension is the suffix beginning at the final dot 498 // in the final element of path; it is empty if there is 499 // no dot. 500 func (fp goFilepath) ext(path string) string { 501 for i := len(path) - 1; i >= 0 && !fp.isPathSeparator(path[i]); i-- { 502 if path[i] == '.' { 503 return path[i:] 504 } 505 } 506 return "" 507 } 508 509 // Join joins any number of path elements into a single path, 510 // separating them with an OS specific Separator. Empty elements 511 // are ignored. The result is Cleaned. However, if the argument 512 // list is empty or all its elements are empty, Join returns 513 // an empty string. 514 // On Windows, the result will only be a UNC path if the first 515 // non-empty element is a UNC path. 516 func (fp goFilepath) join(elem []string) string { 517 for i, e := range elem { 518 if e != "" { 519 if fp.isWindows { 520 return fp.joinNonEmpty(elem[i:]) 521 } 522 return fp.clean(strings.Join(elem[i:], string(fp.pathSeparator))) 523 } 524 } 525 return "" 526 } 527 528 // joinNonEmpty is like join, but it assumes that the first element is non-empty. 529 func (fp goFilepath) joinNonEmpty(elem []string) string { 530 if len(elem[0]) == 2 && elem[0][1] == ':' { 531 // First element is drive letter without terminating slash. 532 // Keep path relative to current directory on that drive. 533 // Skip empty elements. 534 i := 1 535 for ; i < len(elem); i++ { 536 if elem[i] != "" { 537 break 538 } 539 } 540 return fp.clean(elem[0] + strings.Join(elem[i:], string(fp.pathSeparator))) 541 } 542 // The following logic prevents Join from inadvertently creating a 543 // UNC path on Windows. Unless the first element is a UNC path, Join 544 // shouldn't create a UNC path. See golang.org/issue/9167. 545 p := fp.clean(strings.Join(elem, string(fp.pathSeparator))) 546 if !fp.isUNC(p) { 547 return p 548 } 549 // p == UNC only allowed when the first element is a UNC path. 550 head := fp.clean(elem[0]) 551 if fp.isUNC(head) { 552 return p 553 } 554 // head + tail == UNC, but joining two non-UNC paths should not result 555 // in a UNC path. Undo creation of UNC path. 556 tail := fp.clean(strings.Join(elem[1:], string(fp.pathSeparator))) 557 if head[len(head)-1] == fp.pathSeparator { 558 return head + tail 559 } 560 return head + string(fp.pathSeparator) + tail 561 } 562 563 // isUNC reports whether path is a UNC path. 564 func (fp goFilepath) isUNC(path string) bool { 565 return fp.volumeNameLen(path) > 2 566 } 567 568 // Rel returns a relative path that is lexically equivalent to targpath when 569 // joined to basepath with an intervening separator. That is, 570 // Join(basepath, Rel(basepath, targpath)) is equivalent to targpath itself. 571 // On success, the returned path will always be relative to basepath, 572 // even if basepath and targpath share no elements. 573 // An error is returned if targpath can't be made relative to basepath or if 574 // knowing the current working directory would be necessary to compute it. 575 // Rel calls Clean on the result. 576 func (fp goFilepath) rel(basepath, targpath string) (string, error) { 577 baseVol := fp.volumeName(basepath) 578 targVol := fp.volumeName(targpath) 579 base := fp.clean(basepath) 580 targ := fp.clean(targpath) 581 if fp.sameWord(targ, base) { 582 return ".", nil 583 } 584 base = base[len(baseVol):] 585 targ = targ[len(targVol):] 586 if base == "." { 587 base = "" 588 } 589 // Can't use IsAbs - `\a` and `a` are both relative in Windows. 590 baseSlashed := len(base) > 0 && base[0] == fp.pathSeparator 591 targSlashed := len(targ) > 0 && targ[0] == fp.pathSeparator 592 if baseSlashed != targSlashed || !fp.sameWord(baseVol, targVol) { 593 return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath) 594 } 595 // Position base[b0:bi] and targ[t0:ti] at the first differing elements. 596 bl := len(base) 597 tl := len(targ) 598 var b0, bi, t0, ti int 599 for { 600 for bi < bl && base[bi] != fp.pathSeparator { 601 bi++ 602 } 603 for ti < tl && targ[ti] != fp.pathSeparator { 604 ti++ 605 } 606 if !fp.sameWord(targ[t0:ti], base[b0:bi]) { 607 break 608 } 609 if bi < bl { 610 bi++ 611 } 612 if ti < tl { 613 ti++ 614 } 615 b0 = bi 616 t0 = ti 617 } 618 if base[b0:bi] == ".." { 619 return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath) 620 } 621 if b0 != bl { 622 // Base elements left. Must go up before going down. 623 seps := strings.Count(base[b0:bl], string(fp.pathSeparator)) 624 size := 2 + seps*3 625 if tl != t0 { 626 size += 1 + tl - t0 627 } 628 buf := make([]byte, size) 629 n := copy(buf, "..") 630 for i := 0; i < seps; i++ { 631 buf[n] = fp.pathSeparator 632 copy(buf[n+1:], "..") 633 n += 3 634 } 635 if t0 != tl { 636 buf[n] = fp.pathSeparator 637 copy(buf[n+1:], targ[t0:]) 638 } 639 return string(buf), nil 640 } 641 return targ[t0:], nil 642 } 643 644 func (fp goFilepath) sameWord(a, b string) bool { 645 if !fp.isWindows { 646 return a == b 647 } 648 return strings.EqualFold(a, b) 649 }