github.com/visualfc/goembed@v0.3.3/fsys/fsys.go (about) 1 // Copyright 2020 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 fsys is an abstraction for reading files that 6 // allows for virtual overlays on top of the files on disk. 7 package fsys 8 9 import ( 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io/ioutil" 14 "os" 15 "path/filepath" 16 "runtime" 17 "sort" 18 "strings" 19 "time" 20 21 "github.com/visualfc/goembed/fs" 22 ) 23 24 // OverlayFile is the path to a text file in the OverlayJSON format. 25 // It is the value of the -overlay flag. 26 var OverlayFile string 27 28 // OverlayJSON is the format overlay files are expected to be in. 29 // The Replace map maps from overlaid paths to replacement paths: 30 // the Go command will forward all reads trying to open 31 // each overlaid path to its replacement path, or consider the overlaid 32 // path not to exist if the replacement path is empty. 33 type OverlayJSON struct { 34 Replace map[string]string 35 } 36 37 type node struct { 38 actualFilePath string // empty if a directory 39 children map[string]*node // path element → file or directory 40 } 41 42 func (n *node) isDir() bool { 43 return n.actualFilePath == "" && n.children != nil 44 } 45 46 func (n *node) isDeleted() bool { 47 return n.actualFilePath == "" && n.children == nil 48 } 49 50 // TODO(matloob): encapsulate these in an io/fs-like interface 51 var overlay map[string]*node // path -> file or directory node 52 var cwd string // copy of base.Cwd to avoid dependency 53 54 // Canonicalize a path for looking it up in the overlay. 55 // Important: filepath.Join(cwd, path) doesn't always produce 56 // the correct absolute path if path is relative, because on 57 // Windows producing the correct absolute path requires making 58 // a syscall. So this should only be used when looking up paths 59 // in the overlay, or canonicalizing the paths in the overlay. 60 func canonicalize(path string) string { 61 if path == "" { 62 return "" 63 } 64 if filepath.IsAbs(path) { 65 return filepath.Clean(path) 66 } 67 68 if v := filepath.VolumeName(cwd); v != "" && path[0] == filepath.Separator { 69 // On Windows filepath.Join(cwd, path) doesn't always work. In general 70 // filepath.Abs needs to make a syscall on Windows. Elsewhere in cmd/go 71 // use filepath.Join(cwd, path), but cmd/go specifically supports Windows 72 // paths that start with "\" which implies the path is relative to the 73 // volume of the working directory. See golang.org/issue/8130. 74 return filepath.Join(v, path) 75 } 76 77 // Make the path absolute. 78 return filepath.Join(cwd, path) 79 } 80 81 // Init initializes the overlay, if one is being used. 82 func Init(wd string) error { 83 if overlay != nil { 84 // already initialized 85 return nil 86 } 87 88 cwd = wd 89 90 if OverlayFile == "" { 91 return nil 92 } 93 94 b, err := ioutil.ReadFile(OverlayFile) 95 if err != nil { 96 return fmt.Errorf("reading overlay file: %v", err) 97 } 98 99 var overlayJSON OverlayJSON 100 if err := json.Unmarshal(b, &overlayJSON); err != nil { 101 return fmt.Errorf("parsing overlay JSON: %v", err) 102 } 103 104 return initFromJSON(overlayJSON) 105 } 106 107 func initFromJSON(overlayJSON OverlayJSON) error { 108 // Canonicalize the paths in in the overlay map. 109 // Use reverseCanonicalized to check for collisions: 110 // no two 'from' paths should canonicalize to the same path. 111 overlay = make(map[string]*node) 112 reverseCanonicalized := make(map[string]string) // inverse of canonicalize operation, to check for duplicates 113 // Build a table of file and directory nodes from the replacement map. 114 115 // Remove any potential non-determinism from iterating over map by sorting it. 116 replaceFrom := make([]string, 0, len(overlayJSON.Replace)) 117 for k := range overlayJSON.Replace { 118 replaceFrom = append(replaceFrom, k) 119 } 120 sort.Strings(replaceFrom) 121 122 for _, from := range replaceFrom { 123 to := overlayJSON.Replace[from] 124 // Canonicalize paths and check for a collision. 125 if from == "" { 126 return fmt.Errorf("empty string key in overlay file Replace map") 127 } 128 cfrom := canonicalize(from) 129 if to != "" { 130 // Don't canonicalize "", meaning to delete a file, because then it will turn into ".". 131 to = canonicalize(to) 132 } 133 if otherFrom, seen := reverseCanonicalized[cfrom]; seen { 134 return fmt.Errorf( 135 "paths %q and %q both canonicalize to %q in overlay file Replace map", otherFrom, from, cfrom) 136 } 137 reverseCanonicalized[cfrom] = from 138 from = cfrom 139 140 // Create node for overlaid file. 141 dir, base := filepath.Dir(from), filepath.Base(from) 142 if n, ok := overlay[from]; ok { 143 // All 'from' paths in the overlay are file paths. Since the from paths 144 // are in a map, they are unique, so if the node already exists we added 145 // it below when we create parent directory nodes. That is, that 146 // both a file and a path to one of its parent directories exist as keys 147 // in the Replace map. 148 // 149 // This only applies if the overlay directory has any files or directories 150 // in it: placeholder directories that only contain deleted files don't 151 // count. They are safe to be overwritten with actual files. 152 for _, f := range n.children { 153 if !f.isDeleted() { 154 return fmt.Errorf("invalid overlay: path %v is used as both file and directory", from) 155 } 156 } 157 } 158 overlay[from] = &node{actualFilePath: to} 159 160 // Add parent directory nodes to overlay structure. 161 childNode := overlay[from] 162 for { 163 dirNode := overlay[dir] 164 if dirNode == nil || dirNode.isDeleted() { 165 dirNode = &node{children: make(map[string]*node)} 166 overlay[dir] = dirNode 167 } 168 if childNode.isDeleted() { 169 // Only create one parent for a deleted file: 170 // the directory only conditionally exists if 171 // there are any non-deleted children, so 172 // we don't create their parents. 173 if dirNode.isDir() { 174 dirNode.children[base] = childNode 175 } 176 break 177 } 178 if !dirNode.isDir() { 179 // This path already exists as a file, so it can't be a parent 180 // directory. See comment at error above. 181 return fmt.Errorf("invalid overlay: path %v is used as both file and directory", dir) 182 } 183 dirNode.children[base] = childNode 184 parent := filepath.Dir(dir) 185 if parent == dir { 186 break // reached the top; there is no parent 187 } 188 dir, base = parent, filepath.Base(dir) 189 childNode = dirNode 190 } 191 } 192 193 return nil 194 } 195 196 // IsDir returns true if path is a directory on disk or in the 197 // overlay. 198 func IsDir(path string) (bool, error) { 199 path = canonicalize(path) 200 201 if _, ok := parentIsOverlayFile(path); ok { 202 return false, nil 203 } 204 205 if n, ok := overlay[path]; ok { 206 return n.isDir(), nil 207 } 208 209 fi, err := os.Stat(path) 210 if err != nil { 211 return false, err 212 } 213 214 return fi.IsDir(), nil 215 } 216 217 // parentIsOverlayFile returns whether name or any of 218 // its parents are files in the overlay, and the first parent found, 219 // including name itself, that's a file in the overlay. 220 func parentIsOverlayFile(name string) (string, bool) { 221 if overlay != nil { 222 // Check if name can't possibly be a directory because 223 // it or one of its parents is overlaid with a file. 224 // TODO(matloob): Maybe save this to avoid doing it every time? 225 prefix := name 226 for { 227 node := overlay[prefix] 228 if node != nil && !node.isDir() { 229 return prefix, true 230 } 231 parent := filepath.Dir(prefix) 232 if parent == prefix { 233 break 234 } 235 prefix = parent 236 } 237 } 238 239 return "", false 240 } 241 242 // errNotDir is used to communicate from ReadDir to IsDirWithGoFiles 243 // that the argument is not a directory, so that IsDirWithGoFiles doesn't 244 // return an error. 245 var errNotDir = errors.New("not a directory") 246 247 // readDir reads a dir on disk, returning an error that is errNotDir if the dir is not a directory. 248 // Unfortunately, the error returned by ioutil.ReadDir if dir is not a directory 249 // can vary depending on the OS (Linux, Mac, Windows return ENOTDIR; BSD returns EINVAL). 250 func readDir(dir string) ([]fs.FileInfo, error) { 251 fis, err := ioutil.ReadDir(dir) 252 if err == nil { 253 return fis, nil 254 } 255 256 if os.IsNotExist(err) { 257 return nil, err 258 } 259 if dirfi, staterr := os.Stat(dir); staterr == nil && !dirfi.IsDir() { 260 return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: errNotDir} 261 } 262 return nil, err 263 } 264 265 // ReadDir provides a slice of fs.FileInfo entries corresponding 266 // to the overlaid files in the directory. 267 func ReadDir(dir string) ([]fs.FileInfo, error) { 268 dir = canonicalize(dir) 269 if _, ok := parentIsOverlayFile(dir); ok { 270 return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: errNotDir} 271 } 272 273 dirNode := overlay[dir] 274 if dirNode == nil { 275 return readDir(dir) 276 } 277 if dirNode.isDeleted() { 278 return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: fs.ErrNotExist} 279 } 280 diskfis, err := readDir(dir) 281 if err != nil && !os.IsNotExist(err) && !errors.Is(err, errNotDir) { 282 return nil, err 283 } 284 285 // Stat files in overlay to make composite list of fileinfos 286 files := make(map[string]fs.FileInfo) 287 for _, f := range diskfis { 288 files[f.Name()] = f 289 } 290 for name, to := range dirNode.children { 291 switch { 292 case to.isDir(): 293 files[name] = fakeDir(name) 294 case to.isDeleted(): 295 delete(files, name) 296 default: 297 // This is a regular file. 298 f, err := os.Lstat(to.actualFilePath) 299 if err != nil { 300 files[name] = missingFile(name) 301 continue 302 } else if f.IsDir() { 303 return nil, fmt.Errorf("for overlay of %q to %q: overlay Replace entries can't point to dirctories", 304 filepath.Join(dir, name), to.actualFilePath) 305 } 306 // Add a fileinfo for the overlaid file, so that it has 307 // the original file's name, but the overlaid file's metadata. 308 files[name] = fakeFile{name, f} 309 } 310 } 311 sortedFiles := diskfis[:0] 312 for _, f := range files { 313 sortedFiles = append(sortedFiles, f) 314 } 315 sort.Slice(sortedFiles, func(i, j int) bool { return sortedFiles[i].Name() < sortedFiles[j].Name() }) 316 return sortedFiles, nil 317 } 318 319 // OverlayPath returns the path to the overlaid contents of the 320 // file, the empty string if the overlay deletes the file, or path 321 // itself if the file is not in the overlay, the file is a directory 322 // in the overlay, or there is no overlay. 323 // It returns true if the path is overlaid with a regular file 324 // or deleted, and false otherwise. 325 func OverlayPath(path string) (string, bool) { 326 if p, ok := overlay[canonicalize(path)]; ok && !p.isDir() { 327 return p.actualFilePath, ok 328 } 329 330 return path, false 331 } 332 333 // Open opens the file at or overlaid on the given path. 334 func Open(path string) (*os.File, error) { 335 return OpenFile(path, os.O_RDONLY, 0) 336 } 337 338 // OpenFile opens the file at or overlaid on the given path with the flag and perm. 339 func OpenFile(path string, flag int, perm os.FileMode) (*os.File, error) { 340 cpath := canonicalize(path) 341 if node, ok := overlay[cpath]; ok { 342 // Opening a file in the overlay. 343 if node.isDir() { 344 return nil, &fs.PathError{Op: "OpenFile", Path: path, Err: errors.New("fsys.OpenFile doesn't support opening directories yet")} 345 } 346 // We can't open overlaid paths for write. 347 if perm != os.FileMode(os.O_RDONLY) { 348 return nil, &fs.PathError{Op: "OpenFile", Path: path, Err: errors.New("overlaid files can't be opened for write")} 349 } 350 return os.OpenFile(node.actualFilePath, flag, perm) 351 } 352 if parent, ok := parentIsOverlayFile(filepath.Dir(cpath)); ok { 353 // The file is deleted explicitly in the Replace map, 354 // or implicitly because one of its parent directories was 355 // replaced by a file. 356 return nil, &fs.PathError{ 357 Op: "Open", 358 Path: path, 359 Err: fmt.Errorf("file %s does not exist: parent directory %s is replaced by a file in overlay", path, parent), 360 } 361 } 362 return os.OpenFile(cpath, flag, perm) 363 } 364 365 // IsDirWithGoFiles reports whether dir is a directory containing Go files 366 // either on disk or in the overlay. 367 func IsDirWithGoFiles(dir string) (bool, error) { 368 fis, err := ReadDir(dir) 369 if os.IsNotExist(err) || errors.Is(err, errNotDir) { 370 return false, nil 371 } 372 if err != nil { 373 return false, err 374 } 375 376 var firstErr error 377 for _, fi := range fis { 378 if fi.IsDir() { 379 continue 380 } 381 382 // TODO(matloob): this enforces that the "from" in the map 383 // has a .go suffix, but the actual destination file 384 // doesn't need to have a .go suffix. Is this okay with the 385 // compiler? 386 if !strings.HasSuffix(fi.Name(), ".go") { 387 continue 388 } 389 if fi.Mode().IsRegular() { 390 return true, nil 391 } 392 393 // fi is the result of an Lstat, so it doesn't follow symlinks. 394 // But it's okay if the file is a symlink pointing to a regular 395 // file, so use os.Stat to follow symlinks and check that. 396 actualFilePath, _ := OverlayPath(filepath.Join(dir, fi.Name())) 397 fi, err := os.Stat(actualFilePath) 398 if err == nil && fi.Mode().IsRegular() { 399 return true, nil 400 } 401 if err != nil && firstErr == nil { 402 firstErr = err 403 } 404 } 405 406 // No go files found in directory. 407 return false, firstErr 408 } 409 410 // walk recursively descends path, calling walkFn. Copied, with some 411 // modifications from path/filepath.walk. 412 func walk(path string, info fs.FileInfo, walkFn filepath.WalkFunc) error { 413 if !info.IsDir() { 414 return walkFn(path, info, nil) 415 } 416 417 fis, readErr := ReadDir(path) 418 walkErr := walkFn(path, info, readErr) 419 // If readErr != nil, walk can't walk into this directory. 420 // walkErr != nil means walkFn want walk to skip this directory or stop walking. 421 // Therefore, if one of readErr and walkErr isn't nil, walk will return. 422 if readErr != nil || walkErr != nil { 423 // The caller's behavior is controlled by the return value, which is decided 424 // by walkFn. walkFn may ignore readErr and return nil. 425 // If walkFn returns SkipDir, it will be handled by the caller. 426 // So walk should return whatever walkFn returns. 427 return walkErr 428 } 429 430 for _, fi := range fis { 431 filename := filepath.Join(path, fi.Name()) 432 if walkErr = walk(filename, fi, walkFn); walkErr != nil { 433 if !fi.IsDir() || walkErr != filepath.SkipDir { 434 return walkErr 435 } 436 } 437 } 438 return nil 439 } 440 441 // Walk walks the file tree rooted at root, calling walkFn for each file or 442 // directory in the tree, including root. 443 func Walk(root string, walkFn filepath.WalkFunc) error { 444 info, err := Lstat(root) 445 if err != nil { 446 err = walkFn(root, nil, err) 447 } else { 448 err = walk(root, info, walkFn) 449 } 450 if err == filepath.SkipDir { 451 return nil 452 } 453 return err 454 } 455 456 // lstat implements a version of os.Lstat that operates on the overlay filesystem. 457 func Lstat(path string) (fs.FileInfo, error) { 458 return overlayStat(path, os.Lstat, "lstat") 459 } 460 461 // Stat implements a version of os.Stat that operates on the overlay filesystem. 462 func Stat(path string) (fs.FileInfo, error) { 463 return overlayStat(path, os.Stat, "stat") 464 } 465 466 // overlayStat implements lstat or Stat (depending on whether os.Lstat or os.Stat is passed in). 467 func overlayStat(path string, osStat func(string) (fs.FileInfo, error), opName string) (fs.FileInfo, error) { 468 cpath := canonicalize(path) 469 470 if _, ok := parentIsOverlayFile(filepath.Dir(cpath)); ok { 471 return nil, &fs.PathError{Op: opName, Path: cpath, Err: fs.ErrNotExist} 472 } 473 474 node, ok := overlay[cpath] 475 if !ok { 476 // The file or directory is not overlaid. 477 return osStat(path) 478 } 479 480 switch { 481 case node.isDeleted(): 482 return nil, &fs.PathError{Op: "lstat", Path: cpath, Err: fs.ErrNotExist} 483 case node.isDir(): 484 return fakeDir(filepath.Base(path)), nil 485 default: 486 fi, err := osStat(node.actualFilePath) 487 if err != nil { 488 return nil, err 489 } 490 return fakeFile{name: filepath.Base(path), real: fi}, nil 491 } 492 } 493 494 // fakeFile provides an fs.FileInfo implementation for an overlaid file, 495 // so that the file has the name of the overlaid file, but takes all 496 // other characteristics of the replacement file. 497 type fakeFile struct { 498 name string 499 real fs.FileInfo 500 } 501 502 func (f fakeFile) Name() string { return f.name } 503 func (f fakeFile) Size() int64 { return f.real.Size() } 504 func (f fakeFile) Mode() fs.FileMode { return f.real.Mode() } 505 func (f fakeFile) ModTime() time.Time { return f.real.ModTime() } 506 func (f fakeFile) IsDir() bool { return f.real.IsDir() } 507 func (f fakeFile) Sys() interface{} { return f.real.Sys() } 508 509 // missingFile provides an fs.FileInfo for an overlaid file where the 510 // destination file in the overlay doesn't exist. It returns zero values 511 // for the fileInfo methods other than Name, set to the file's name, and Mode 512 // set to ModeIrregular. 513 type missingFile string 514 515 func (f missingFile) Name() string { return string(f) } 516 func (f missingFile) Size() int64 { return 0 } 517 func (f missingFile) Mode() fs.FileMode { return fs.ModeIrregular } 518 func (f missingFile) ModTime() time.Time { return time.Unix(0, 0) } 519 func (f missingFile) IsDir() bool { return false } 520 func (f missingFile) Sys() interface{} { return nil } 521 522 // fakeDir provides an fs.FileInfo implementation for directories that are 523 // implicitly created by overlaid files. Each directory in the 524 // path of an overlaid file is considered to exist in the overlay filesystem. 525 type fakeDir string 526 527 func (f fakeDir) Name() string { return string(f) } 528 func (f fakeDir) Size() int64 { return 0 } 529 func (f fakeDir) Mode() fs.FileMode { return fs.ModeDir | 0500 } 530 func (f fakeDir) ModTime() time.Time { return time.Unix(0, 0) } 531 func (f fakeDir) IsDir() bool { return true } 532 func (f fakeDir) Sys() interface{} { return nil } 533 534 // Glob is like filepath.Glob but uses the overlay file system. 535 func Glob(pattern string) (matches []string, err error) { 536 // Check pattern is well-formed. 537 if _, err := filepath.Match(pattern, ""); err != nil { 538 return nil, err 539 } 540 if !hasMeta(pattern) { 541 if _, err = Lstat(pattern); err != nil { 542 return nil, nil 543 } 544 return []string{pattern}, nil 545 } 546 547 dir, file := filepath.Split(pattern) 548 volumeLen := 0 549 if runtime.GOOS == "windows" { 550 volumeLen, dir = cleanGlobPathWindows(dir) 551 } else { 552 dir = cleanGlobPath(dir) 553 } 554 555 if !hasMeta(dir[volumeLen:]) { 556 return glob(dir, file, nil) 557 } 558 559 // Prevent infinite recursion. See issue 15879. 560 if dir == pattern { 561 return nil, filepath.ErrBadPattern 562 } 563 564 var m []string 565 m, err = Glob(dir) 566 if err != nil { 567 return 568 } 569 for _, d := range m { 570 matches, err = glob(d, file, matches) 571 if err != nil { 572 return 573 } 574 } 575 return 576 } 577 578 // cleanGlobPath prepares path for glob matching. 579 func cleanGlobPath(path string) string { 580 switch path { 581 case "": 582 return "." 583 case string(filepath.Separator): 584 // do nothing to the path 585 return path 586 default: 587 return path[0 : len(path)-1] // chop off trailing separator 588 } 589 } 590 591 func volumeNameLen(path string) int { 592 isSlash := func(c uint8) bool { 593 return c == '\\' || c == '/' 594 } 595 if len(path) < 2 { 596 return 0 597 } 598 // with drive letter 599 c := path[0] 600 if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') { 601 return 2 602 } 603 // is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx 604 if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) && 605 !isSlash(path[2]) && path[2] != '.' { 606 // first, leading `\\` and next shouldn't be `\`. its server name. 607 for n := 3; n < l-1; n++ { 608 // second, next '\' shouldn't be repeated. 609 if isSlash(path[n]) { 610 n++ 611 // third, following something characters. its share name. 612 if !isSlash(path[n]) { 613 if path[n] == '.' { 614 break 615 } 616 for ; n < l; n++ { 617 if isSlash(path[n]) { 618 break 619 } 620 } 621 return n 622 } 623 break 624 } 625 } 626 } 627 return 0 628 } 629 630 // cleanGlobPathWindows is windows version of cleanGlobPath. 631 func cleanGlobPathWindows(path string) (prefixLen int, cleaned string) { 632 vollen := volumeNameLen(path) 633 switch { 634 case path == "": 635 return 0, "." 636 case vollen+1 == len(path) && os.IsPathSeparator(path[len(path)-1]): // /, \, C:\ and C:/ 637 // do nothing to the path 638 return vollen + 1, path 639 case vollen == len(path) && len(path) == 2: // C: 640 return vollen, path + "." // convert C: into C:. 641 default: 642 if vollen >= len(path) { 643 vollen = len(path) - 1 644 } 645 return vollen, path[0 : len(path)-1] // chop off trailing separator 646 } 647 } 648 649 // glob searches for files matching pattern in the directory dir 650 // and appends them to matches. If the directory cannot be 651 // opened, it returns the existing matches. New matches are 652 // added in lexicographical order. 653 func glob(dir, pattern string, matches []string) (m []string, e error) { 654 m = matches 655 fi, err := Stat(dir) 656 if err != nil { 657 return // ignore I/O error 658 } 659 if !fi.IsDir() { 660 return // ignore I/O error 661 } 662 663 list, err := ReadDir(dir) 664 if err != nil { 665 return // ignore I/O error 666 } 667 668 var names []string 669 for _, info := range list { 670 names = append(names, info.Name()) 671 } 672 sort.Strings(names) 673 674 for _, n := range names { 675 matched, err := filepath.Match(pattern, n) 676 if err != nil { 677 return m, err 678 } 679 if matched { 680 m = append(m, filepath.Join(dir, n)) 681 } 682 } 683 return 684 } 685 686 // hasMeta reports whether path contains any of the magic characters 687 // recognized by filepath.Match. 688 func hasMeta(path string) bool { 689 magicChars := `*?[` 690 if runtime.GOOS != "windows" { 691 magicChars = `*?[\` 692 } 693 return strings.ContainsAny(path, magicChars) 694 }