github.com/avfs/avfs@v0.33.1-0.20240303173310-c6ba67c33eb7/vfs_ostype_on.go (about) 1 // 2 // Copyright 2024 The AVFS authors 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 // 16 17 //go:build avfs_setostype 18 19 package avfs 20 21 import ( 22 "errors" 23 "path/filepath" 24 "strings" 25 "unicode/utf8" 26 ) 27 28 const buildFeatSetOSType = FeatSetOSType 29 30 // Base returns the last element of path. 31 // Trailing path separators are removed before extracting the last element. 32 // If the path is empty, Base returns ".". 33 // If the path consists entirely of separators, Base returns a single separator. 34 func Base[T VFSBase](vfs T, path string) string { 35 if path == "" { 36 return "." 37 } 38 39 // Strip trailing slashes. 40 for len(path) > 0 && IsPathSeparator(vfs, path[len(path)-1]) { 41 path = path[0 : len(path)-1] 42 } 43 44 // Throw away volume name 45 path = path[len(VolumeName(vfs, path)):] 46 47 // Find the last element 48 i := len(path) - 1 49 for i >= 0 && !IsPathSeparator(vfs, path[i]) { 50 i-- 51 } 52 53 if i >= 0 { 54 path = path[i+1:] 55 } 56 57 // If empty now, it had only slashes. 58 if path == "" { 59 return string(vfs.PathSeparator()) 60 } 61 62 return path 63 } 64 65 // Clean returns the shortest path name equivalent to path 66 // by purely lexical processing. It applies the following rules 67 // iteratively until no further processing can be done: 68 // 69 // 1. Replace multiple Separator elements with a single one. 70 // 2. Eliminate each . path name element (the current directory). 71 // 3. Eliminate each inner .. path name element (the parent directory) 72 // along with the non-.. element that precedes it. 73 // 4. Eliminate .. elements that begin a rooted path: 74 // that is, replace "/.." by "/" at the beginning of a path, 75 // assuming Separator is '/'. 76 // 77 // The returned path ends in a slash only if it represents a root directory, 78 // such as "/" on Unix or `C:\` on Windows. 79 // 80 // Finally, any occurrences of slash are replaced by Separator. 81 // 82 // If the result of this process is an empty string, Clean 83 // returns the string ".". 84 // 85 // On Windows, Clean does not modify the volume name other than to replace 86 // occurrences of "/" with `\`. 87 // For example, Clean("//host/share/../x") returns `\\host\share\x`. 88 // 89 // See also Rob Pike, “Lexical File Names in Plan 9 or 90 // Getting Dot-Dot Right,” 91 // https://9p.io/sys/doc/lexnames.html 92 func Clean[T VFSBase](vfs T, path string) string { 93 pathSeparator := vfs.PathSeparator() 94 originalPath := path 95 volLen := VolumeNameLen(vfs, path) 96 97 path = path[volLen:] 98 if path == "" { 99 if volLen > 1 && IsPathSeparator(vfs, originalPath[0]) && IsPathSeparator(vfs, originalPath[1]) { 100 // should be UNC 101 return FromSlash(vfs, originalPath) 102 } 103 104 return originalPath + "." 105 } 106 107 rooted := IsPathSeparator(vfs, path[0]) 108 109 // Invariants: 110 // reading from path; r is index of next byte to process. 111 // writing to buf; w is index of next byte to write. 112 // dotdot is index in buf where .. must stop, either because 113 // it is the leading slash or it is a leading ../../.. prefix. 114 n := len(path) 115 out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen} 116 r, dotdot := 0, 0 117 118 if rooted { 119 out.append(pathSeparator) 120 121 r, dotdot = 1, 1 122 } 123 124 for r < n { 125 switch { 126 case IsPathSeparator(vfs, path[r]): 127 // empty path element 128 r++ 129 case path[r] == '.' && (r+1 == n || IsPathSeparator(vfs, path[r+1])): 130 // . element 131 r++ 132 case path[r] == '.' && path[r+1] == '.' && (r+2 == n || IsPathSeparator(vfs, path[r+2])): 133 // .. element: remove to last separator 134 r += 2 135 136 switch { 137 case out.w > dotdot: 138 // can backtrack 139 out.w-- 140 for out.w > dotdot && !IsPathSeparator(vfs, out.index(out.w)) { 141 out.w-- 142 } 143 case !rooted: 144 // cannot backtrack, but not rooted, so append .. element. 145 if out.w > 0 { 146 out.append(pathSeparator) 147 } 148 149 out.append('.') 150 out.append('.') 151 dotdot = out.w 152 } 153 default: 154 // real path element. 155 // add slash if needed 156 if rooted && out.w != 1 || !rooted && out.w != 0 { 157 out.append(pathSeparator) 158 } 159 160 // If a ':' appears in the path element at the start of a Windows path, 161 // insert a .\ at the beginning to avoid converting relative paths 162 // like a/../c: into c:. 163 if vfs.OSType() == OsWindows && out.w == 0 && out.volLen == 0 && r != 0 { 164 for i := r; i < n && !IsPathSeparator(vfs, path[i]); i++ { 165 if path[i] == ':' { 166 out.append('.') 167 out.append(pathSeparator) 168 169 break 170 } 171 } 172 } 173 174 // copy element 175 for ; r < n && !IsPathSeparator(vfs, path[r]); r++ { 176 out.append(path[r]) 177 } 178 } 179 } 180 181 // Turn empty string into "." 182 if out.w == 0 { 183 out.append('.') 184 } 185 186 return FromSlash(vfs, out.string()) 187 } 188 189 // Dir returns all but the last element of path, typically the path's directory. 190 // After dropping the final element, Dir calls Clean on the path and trailing 191 // slashes are removed. 192 // If the path is empty, Dir returns ".". 193 // If the path consists entirely of separators, Dir returns a single separator. 194 // The returned path does not end in a separator unless it is the root directory. 195 func Dir[T VFSBase](vfs T, path string) string { 196 vol := VolumeName(vfs, path) 197 198 i := len(path) - 1 199 for i >= len(vol) && !IsPathSeparator(vfs, path[i]) { 200 i-- 201 } 202 203 dir := Clean(vfs, path[len(vol):i+1]) 204 if dir == "." && len(vol) > 2 { 205 // must be UNC 206 return vol 207 } 208 209 return vol + dir 210 } 211 212 // FromSlash returns the result of replacing each slash ('/') character 213 // in path with a separator character. Multiple slashes are replaced 214 // by multiple separators. 215 func FromSlash[T VFSBase](vfs T, path string) string { 216 pathSeparator := vfs.PathSeparator() 217 218 if vfs.OSType() != OsWindows { 219 return path 220 } 221 222 return strings.ReplaceAll(path, "/", string(pathSeparator)) 223 } 224 225 // getEsc gets a possibly-escaped character from chunk, for a character class. 226 func getEsc[T VFSBase](vfs T, chunk string) (r rune, nchunk string, err error) { 227 if chunk == "" || chunk[0] == '-' || chunk[0] == ']' { 228 err = filepath.ErrBadPattern 229 230 return 231 } 232 233 if chunk[0] == '\\' && vfs.OSType() != OsWindows { 234 chunk = chunk[1:] 235 if chunk == "" { 236 err = filepath.ErrBadPattern 237 238 return 239 } 240 } 241 242 r, n := utf8.DecodeRuneInString(chunk) 243 if r == utf8.RuneError && n == 1 { 244 err = filepath.ErrBadPattern 245 } 246 247 nchunk = chunk[n:] 248 if nchunk == "" { 249 err = filepath.ErrBadPattern 250 } 251 252 return 253 } 254 255 // IsAbs reports whether the path is absolute. 256 func IsAbs[T VFSBase](vfs T, path string) bool { 257 if vfs.OSType() != OsWindows { 258 return strings.HasPrefix(path, "/") 259 } 260 261 l := VolumeNameLen(vfs, path) 262 if l == 0 { 263 return false 264 } 265 266 // If the volume name starts with a double slash, this is an absolute path. 267 if isSlash(path[0]) && isSlash(path[1]) { 268 return true 269 } 270 271 path = path[l:] 272 if path == "" { 273 return false 274 } 275 276 return isSlash(path[0]) 277 } 278 279 // IsPathSeparator reports whether c is a directory separator character. 280 func IsPathSeparator[T VFSBase](vfs T, c uint8) bool { 281 if vfs.OSType() != OsWindows { 282 return c == '/' 283 } 284 285 return c == '\\' || c == '/' 286 } 287 288 func isSlash(c uint8) bool { 289 return c == '\\' || c == '/' 290 } 291 292 // Join joins any number of path elements into a single path, adding a 293 // separating slash if necessary. The result is Cleaned; in particular, 294 // all empty strings are ignored. 295 func Join[T VFSBase](vfs T, elem ...string) string { 296 pathSeparator := vfs.PathSeparator() 297 298 if vfs.OSType() != OsWindows { 299 // If there's a bug here, fix the logic in ./path_plan9.go too. 300 for i, e := range elem { 301 if e != "" { 302 return Clean(vfs, strings.Join(elem[i:], string(pathSeparator))) 303 } 304 } 305 306 return "" 307 } 308 309 return joinWindows(vfs, elem) 310 } 311 312 func joinWindows[T VFSBase](vfs T, elem []string) string { 313 var ( 314 b strings.Builder 315 lastChar byte 316 ) 317 318 for _, e := range elem { 319 switch { 320 case b.Len() == 0: 321 // Add the first non-empty path element unchanged. 322 case isSlash(lastChar): 323 // If the path ends in a slash, strip any leading slashes from the next 324 // path element to avoid creating a UNC path (any path starting with "\\") 325 // from non-UNC elements. 326 // 327 // The correct behavior for Join when the first element is an incomplete UNC 328 // path (for example, "\\") is underspecified. We currently join subsequent 329 // elements so Join("\\", "host", "share") produces "\\host\share". 330 for len(e) > 0 && isSlash(e[0]) { 331 e = e[1:] 332 } 333 case lastChar == ':': 334 // If the path ends in a colon, keep the path relative to the current directory 335 // on a drive and don't add a separator. Preserve leading slashes in the next 336 // path element, which may make the path absolute. 337 // 338 // Join(`C:`, `f`) = `C:f` 339 // Join(`C:`, `\f`) = `C:\f` 340 default: 341 // In all other cases, add a separator between elements. 342 b.WriteByte('\\') 343 344 lastChar = '\\' 345 } 346 347 if len(e) > 0 { 348 b.WriteString(e) 349 lastChar = e[len(e)-1] 350 } 351 } 352 353 if b.Len() == 0 { 354 return "" 355 } 356 357 return Clean(vfs, b.String()) 358 } 359 360 // Match reports whether name matches the shell file name pattern. 361 // The pattern syntax is: 362 // 363 // pattern: 364 // { term } 365 // term: 366 // '*' matches any sequence of non-Separator characters 367 // '?' matches any single non-Separator character 368 // '[' [ '^' ] { character-range } ']' 369 // character class (must be non-empty) 370 // c matches character c (c != '*', '?', '\\', '[') 371 // '\\' c matches character c 372 // 373 // character-range: 374 // c matches character c (c != '\\', '-', ']') 375 // '\\' c matches character c 376 // lo '-' hi matches character c for lo <= c <= hi 377 // 378 // Match requires pattern to match all of name, not just a substring. 379 // The only possible returned error is ErrBadPattern, when pattern 380 // is malformed. 381 // 382 // On Windows, escaping is disabled. Instead, '\\' is treated as 383 // path separator. 384 func Match[T VFSBase](vfs T, pattern, name string) (matched bool, err error) { 385 pathSeparator := vfs.PathSeparator() 386 387 Pattern: 388 for len(pattern) > 0 { 389 var star bool 390 var chunk string 391 392 star, chunk, pattern = scanChunk(vfs, pattern) 393 if star && chunk == "" { 394 // Trailing * matches rest of string unless it has a /. 395 return !strings.Contains(name, string(pathSeparator)), nil 396 } 397 398 // Look for match at current position. 399 t, ok, err := matchChunk(vfs, chunk, name) 400 401 // if we're the last chunk, make sure we've exhausted the name 402 // otherwise we'll give a false result even if we could still match 403 // using the star 404 if ok && (t == "" || len(pattern) > 0) { 405 name = t 406 407 continue 408 } 409 410 if err != nil { 411 return false, err 412 } 413 414 if star { 415 // Look for match skipping i+1 bytes. 416 // Cannot skip /. 417 for i := 0; i < len(name) && name[i] != pathSeparator; i++ { 418 t, ok, err := matchChunk(vfs, chunk, name[i+1:]) 419 if ok { 420 // if we're the last chunk, make sure we exhausted the name 421 if pattern == "" && len(t) > 0 { 422 continue 423 } 424 name = t 425 426 continue Pattern 427 } 428 if err != nil { 429 return false, err 430 } 431 } 432 } 433 434 return false, nil 435 } 436 437 return name == "", nil 438 } 439 440 // matchChunk checks whether chunk matches the beginning of s. 441 // If so, it returns the remainder of s (after the match). 442 // Chunk is all single-character operators: literals, char classes, and ?. 443 func matchChunk[T VFSBase](vfs T, chunk, s string) (rest string, ok bool, err error) { 444 pathSeparator := vfs.PathSeparator() 445 446 // failed records whether the match has failed. 447 // After the match fails, the loop continues on processing chunk, 448 // checking that the pattern is well-formed but no longer reading s. 449 failed := false 450 451 for len(chunk) > 0 { 452 if !failed && s == "" { 453 failed = true 454 } 455 456 switch chunk[0] { 457 case '[': 458 // character class 459 var r rune 460 461 if !failed { 462 var n int 463 r, n = utf8.DecodeRuneInString(s) 464 s = s[n:] 465 } 466 467 chunk = chunk[1:] 468 // possibly negated 469 negated := false 470 471 if len(chunk) > 0 && chunk[0] == '^' { 472 negated = true 473 chunk = chunk[1:] 474 } 475 476 // parse all ranges 477 match := false 478 nrange := 0 479 480 for { 481 if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 { 482 chunk = chunk[1:] 483 484 break 485 } 486 487 var lo, hi rune 488 489 if lo, chunk, err = getEsc(vfs, chunk); err != nil { 490 return "", false, err 491 } 492 493 hi = lo 494 495 if chunk[0] == '-' { 496 if hi, chunk, err = getEsc(vfs, chunk[1:]); err != nil { 497 return "", false, err 498 } 499 } 500 501 if lo <= r && r <= hi { 502 match = true 503 } 504 505 nrange++ 506 } 507 508 if match == negated { 509 failed = true 510 } 511 case '?': 512 if !failed { 513 if s[0] == pathSeparator { 514 failed = true 515 } 516 517 _, n := utf8.DecodeRuneInString(s) 518 s = s[n:] 519 } 520 521 chunk = chunk[1:] 522 case '\\': 523 if vfs.OSType() != OsWindows { 524 chunk = chunk[1:] 525 if chunk == "" { 526 return "", false, filepath.ErrBadPattern 527 } 528 } 529 530 fallthrough 531 default: 532 if !failed { 533 if chunk[0] != s[0] { 534 failed = true 535 } 536 537 s = s[1:] 538 } 539 540 chunk = chunk[1:] 541 } 542 } 543 544 if failed { 545 return "", false, nil 546 } 547 548 return s, true, nil 549 } 550 551 // Rel returns a relative path that is lexically equivalent to targpath when 552 // joined to basepath with an intervening separator. That is, 553 // Join(basepath, Rel(basepath, targpath)) is equivalent to targpath itself. 554 // On success, the returned path will always be relative to basepath, 555 // even if basepath and targpath share no elements. 556 // An error is returned if targpath can't be made relative to basepath or if 557 // knowing the current working directory would be necessary to compute it. 558 // Rel calls Clean on the result. 559 func Rel[T VFSBase](vfs T, basepath, targpath string) (string, error) { 560 pathSeparator := vfs.PathSeparator() 561 562 baseVol := VolumeName(vfs, basepath) 563 targVol := VolumeName(vfs, targpath) 564 base := Clean(vfs, basepath) 565 targ := Clean(vfs, targpath) 566 567 if sameWord(vfs, targ, base) { 568 return ".", nil 569 } 570 571 base = base[len(baseVol):] 572 targ = targ[len(targVol):] 573 574 if base == "." { 575 base = "" 576 } else if base == "" && VolumeNameLen(vfs, baseVol) > 2 /* isUNC */ { 577 // Treat any targetpath matching `\\host\share` basepath as absolute path. 578 base = string(pathSeparator) 579 } 580 581 // Can't use IsAbs - `\a` and `a` are both relative in Windows. 582 baseSlashed := len(base) > 0 && base[0] == pathSeparator 583 targSlashed := len(targ) > 0 && targ[0] == pathSeparator 584 585 if baseSlashed != targSlashed || !sameWord(vfs, baseVol, targVol) { 586 return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath) 587 } 588 589 // Position base[b0:bi] and targ[t0:ti] at the first differing elements. 590 bl := len(base) 591 tl := len(targ) 592 593 var b0, bi, t0, ti int 594 595 for { 596 for bi < bl && base[bi] != pathSeparator { 597 bi++ 598 } 599 600 for ti < tl && targ[ti] != pathSeparator { 601 ti++ 602 } 603 604 if !sameWord(vfs, targ[t0:ti], base[b0:bi]) { 605 break 606 } 607 608 if bi < bl { 609 bi++ 610 } 611 612 if ti < tl { 613 ti++ 614 } 615 616 b0 = bi 617 t0 = ti 618 } 619 620 if base[b0:bi] == ".." { 621 return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath) 622 } 623 624 if b0 != bl { 625 // Base elements left. Must go up before going down. 626 seps := strings.Count(base[b0:bl], string(pathSeparator)) 627 size := 2 + seps*3 628 629 if tl != t0 { 630 size += 1 + tl - t0 631 } 632 633 buf := make([]byte, size) 634 n := copy(buf, "..") 635 636 for i := 0; i < seps; i++ { 637 buf[n] = pathSeparator 638 copy(buf[n+1:], "..") 639 n += 3 640 } 641 642 if t0 != tl { 643 buf[n] = pathSeparator 644 copy(buf[n+1:], targ[t0:]) 645 } 646 647 return string(buf), nil 648 } 649 650 return targ[t0:], nil 651 } 652 653 func sameWord[T VFSBase](vfs T, a, b string) bool { 654 if vfs.OSType() != OsWindows { 655 return a == b 656 } 657 658 return strings.EqualFold(a, b) 659 } 660 661 // scanChunk gets the next segment of pattern, which is a non-star string 662 // possibly preceded by a star. 663 func scanChunk[T VFSBase](vfs T, pattern string) (star bool, chunk, rest string) { 664 for len(pattern) > 0 && pattern[0] == '*' { 665 pattern = pattern[1:] 666 star = true 667 } 668 669 inrange := false 670 671 var i int 672 673 Scan: 674 for i = 0; i < len(pattern); i++ { 675 switch pattern[i] { 676 case '\\': 677 if vfs.OSType() != OsWindows { 678 // error check handled in matchChunk: bad pattern. 679 if i+1 < len(pattern) { 680 i++ 681 } 682 } 683 case '[': 684 inrange = true 685 case ']': 686 inrange = false 687 case '*': 688 if !inrange { 689 break Scan 690 } 691 } 692 } 693 694 return star, pattern[0:i], pattern[i:] 695 } 696 697 // Split splits path immediately following the final Separator, 698 // separating it into a directory and file name component. 699 // If there is no Separator in path, Split returns an empty dir 700 // and file set to path. 701 // The returned values have the property that path = dir+file. 702 func Split[T VFSBase](vfs T, path string) (dir, file string) { 703 vol := VolumeName(vfs, path) 704 705 i := len(path) - 1 706 for i >= len(vol) && !IsPathSeparator(vfs, path[i]) { 707 i-- 708 } 709 710 return path[:i+1], path[i+1:] 711 } 712 713 // ToSlash returns the result of replacing each separator character 714 // in path with a slash ('/') character. Multiple separators are 715 // replaced by multiple slashes. 716 func ToSlash[T VFSBase](vfs T, path string) string { 717 pathSeparator := vfs.PathSeparator() 718 719 if pathSeparator == '/' { 720 return path 721 } 722 723 return strings.ReplaceAll(path, string(pathSeparator), "/") 724 } 725 726 // VolumeNameLen returns length of the leading volume name on Windows. 727 // It returns 0 elsewhere. 728 func VolumeNameLen[T VFSBase](vfs T, path string) int { 729 if vfs.OSType() != OsWindows { 730 return 0 731 } 732 733 if len(path) < 2 { 734 return 0 735 } 736 737 // with drive letter 738 c := path[0] 739 if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') { 740 return 2 741 } 742 743 // is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx 744 if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) && 745 !isSlash(path[2]) && path[2] != '.' { 746 // first, leading `\\` and next shouldn't be `\`. its server name. 747 for n := 3; n < l-1; n++ { 748 // second, next '\' shouldn't be repeated. 749 if isSlash(path[n]) { 750 n++ 751 // third, following something characters. its share name. 752 if !isSlash(path[n]) { 753 if path[n] == '.' { 754 break 755 } 756 757 for ; n < l; n++ { 758 if isSlash(path[n]) { 759 break 760 } 761 } 762 763 return n 764 } 765 766 break 767 } 768 } 769 } 770 771 return 0 772 } 773 774 // A lazybuf is a lazily constructed path buffer. 775 // It supports append, reading previously appended bytes, 776 // and retrieving the final string. It does not allocate a buffer 777 // to hold the output until that output diverges from s. 778 type lazybuf struct { 779 path string 780 volAndPath string 781 buf []byte 782 w int 783 volLen int 784 } 785 786 func (b *lazybuf) index(i int) byte { 787 if b.buf != nil { 788 return b.buf[i] 789 } 790 791 return b.path[i] 792 } 793 794 func (b *lazybuf) append(c byte) { 795 if b.buf == nil { 796 if b.w < len(b.path) && b.path[b.w] == c { 797 b.w++ 798 799 return 800 } 801 802 b.buf = make([]byte, len(b.path)) 803 copy(b.buf, b.path[:b.w]) 804 } 805 806 b.buf[b.w] = c 807 b.w++ 808 } 809 810 func (b *lazybuf) string() string { 811 if b.buf == nil { 812 return b.volAndPath[:b.volLen+b.w] 813 } 814 815 return b.volAndPath[:b.volLen] + string(b.buf[:b.w]) 816 }