summaryrefslogtreecommitdiffstats
path: root/src/cmd/go/internal/fsys
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/go/internal/fsys')
-rw-r--r--src/cmd/go/internal/fsys/fsys.go689
-rw-r--r--src/cmd/go/internal/fsys/fsys_test.go1094
2 files changed, 1783 insertions, 0 deletions
diff --git a/src/cmd/go/internal/fsys/fsys.go b/src/cmd/go/internal/fsys/fsys.go
new file mode 100644
index 0000000..7b06c3c
--- /dev/null
+++ b/src/cmd/go/internal/fsys/fsys.go
@@ -0,0 +1,689 @@
+// Package fsys is an abstraction for reading files that
+// allows for virtual overlays on top of the files on disk.
+package fsys
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/fs"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strings"
+ "time"
+)
+
+// OverlayFile is the path to a text file in the OverlayJSON format.
+// It is the value of the -overlay flag.
+var OverlayFile string
+
+// OverlayJSON is the format overlay files are expected to be in.
+// The Replace map maps from overlaid paths to replacement paths:
+// the Go command will forward all reads trying to open
+// each overlaid path to its replacement path, or consider the overlaid
+// path not to exist if the replacement path is empty.
+type OverlayJSON struct {
+ Replace map[string]string
+}
+
+type node struct {
+ actualFilePath string // empty if a directory
+ children map[string]*node // path element → file or directory
+}
+
+func (n *node) isDir() bool {
+ return n.actualFilePath == "" && n.children != nil
+}
+
+func (n *node) isDeleted() bool {
+ return n.actualFilePath == "" && n.children == nil
+}
+
+// TODO(matloob): encapsulate these in an io/fs-like interface
+var overlay map[string]*node // path -> file or directory node
+var cwd string // copy of base.Cwd to avoid dependency
+
+// Canonicalize a path for looking it up in the overlay.
+// Important: filepath.Join(cwd, path) doesn't always produce
+// the correct absolute path if path is relative, because on
+// Windows producing the correct absolute path requires making
+// a syscall. So this should only be used when looking up paths
+// in the overlay, or canonicalizing the paths in the overlay.
+func canonicalize(path string) string {
+ if path == "" {
+ return ""
+ }
+ if filepath.IsAbs(path) {
+ return filepath.Clean(path)
+ }
+
+ if v := filepath.VolumeName(cwd); v != "" && path[0] == filepath.Separator {
+ // On Windows filepath.Join(cwd, path) doesn't always work. In general
+ // filepath.Abs needs to make a syscall on Windows. Elsewhere in cmd/go
+ // use filepath.Join(cwd, path), but cmd/go specifically supports Windows
+ // paths that start with "\" which implies the path is relative to the
+ // volume of the working directory. See golang.org/issue/8130.
+ return filepath.Join(v, path)
+ }
+
+ // Make the path absolute.
+ return filepath.Join(cwd, path)
+}
+
+// Init initializes the overlay, if one is being used.
+func Init(wd string) error {
+ if overlay != nil {
+ // already initialized
+ return nil
+ }
+
+ cwd = wd
+
+ if OverlayFile == "" {
+ return nil
+ }
+
+ b, err := os.ReadFile(OverlayFile)
+ if err != nil {
+ return fmt.Errorf("reading overlay file: %v", err)
+ }
+
+ var overlayJSON OverlayJSON
+ if err := json.Unmarshal(b, &overlayJSON); err != nil {
+ return fmt.Errorf("parsing overlay JSON: %v", err)
+ }
+
+ return initFromJSON(overlayJSON)
+}
+
+func initFromJSON(overlayJSON OverlayJSON) error {
+ // Canonicalize the paths in in the overlay map.
+ // Use reverseCanonicalized to check for collisions:
+ // no two 'from' paths should canonicalize to the same path.
+ overlay = make(map[string]*node)
+ reverseCanonicalized := make(map[string]string) // inverse of canonicalize operation, to check for duplicates
+ // Build a table of file and directory nodes from the replacement map.
+
+ // Remove any potential non-determinism from iterating over map by sorting it.
+ replaceFrom := make([]string, 0, len(overlayJSON.Replace))
+ for k := range overlayJSON.Replace {
+ replaceFrom = append(replaceFrom, k)
+ }
+ sort.Strings(replaceFrom)
+
+ for _, from := range replaceFrom {
+ to := overlayJSON.Replace[from]
+ // Canonicalize paths and check for a collision.
+ if from == "" {
+ return fmt.Errorf("empty string key in overlay file Replace map")
+ }
+ cfrom := canonicalize(from)
+ if to != "" {
+ // Don't canonicalize "", meaning to delete a file, because then it will turn into ".".
+ to = canonicalize(to)
+ }
+ if otherFrom, seen := reverseCanonicalized[cfrom]; seen {
+ return fmt.Errorf(
+ "paths %q and %q both canonicalize to %q in overlay file Replace map", otherFrom, from, cfrom)
+ }
+ reverseCanonicalized[cfrom] = from
+ from = cfrom
+
+ // Create node for overlaid file.
+ dir, base := filepath.Dir(from), filepath.Base(from)
+ if n, ok := overlay[from]; ok {
+ // All 'from' paths in the overlay are file paths. Since the from paths
+ // are in a map, they are unique, so if the node already exists we added
+ // it below when we create parent directory nodes. That is, that
+ // both a file and a path to one of its parent directories exist as keys
+ // in the Replace map.
+ //
+ // This only applies if the overlay directory has any files or directories
+ // in it: placeholder directories that only contain deleted files don't
+ // count. They are safe to be overwritten with actual files.
+ for _, f := range n.children {
+ if !f.isDeleted() {
+ return fmt.Errorf("invalid overlay: path %v is used as both file and directory", from)
+ }
+ }
+ }
+ overlay[from] = &node{actualFilePath: to}
+
+ // Add parent directory nodes to overlay structure.
+ childNode := overlay[from]
+ for {
+ dirNode := overlay[dir]
+ if dirNode == nil || dirNode.isDeleted() {
+ dirNode = &node{children: make(map[string]*node)}
+ overlay[dir] = dirNode
+ }
+ if childNode.isDeleted() {
+ // Only create one parent for a deleted file:
+ // the directory only conditionally exists if
+ // there are any non-deleted children, so
+ // we don't create their parents.
+ if dirNode.isDir() {
+ dirNode.children[base] = childNode
+ }
+ break
+ }
+ if !dirNode.isDir() {
+ // This path already exists as a file, so it can't be a parent
+ // directory. See comment at error above.
+ return fmt.Errorf("invalid overlay: path %v is used as both file and directory", dir)
+ }
+ dirNode.children[base] = childNode
+ parent := filepath.Dir(dir)
+ if parent == dir {
+ break // reached the top; there is no parent
+ }
+ dir, base = parent, filepath.Base(dir)
+ childNode = dirNode
+ }
+ }
+
+ return nil
+}
+
+// IsDir returns true if path is a directory on disk or in the
+// overlay.
+func IsDir(path string) (bool, error) {
+ path = canonicalize(path)
+
+ if _, ok := parentIsOverlayFile(path); ok {
+ return false, nil
+ }
+
+ if n, ok := overlay[path]; ok {
+ return n.isDir(), nil
+ }
+
+ fi, err := os.Stat(path)
+ if err != nil {
+ return false, err
+ }
+
+ return fi.IsDir(), nil
+}
+
+// parentIsOverlayFile returns whether name or any of
+// its parents are files in the overlay, and the first parent found,
+// including name itself, that's a file in the overlay.
+func parentIsOverlayFile(name string) (string, bool) {
+ if overlay != nil {
+ // Check if name can't possibly be a directory because
+ // it or one of its parents is overlaid with a file.
+ // TODO(matloob): Maybe save this to avoid doing it every time?
+ prefix := name
+ for {
+ node := overlay[prefix]
+ if node != nil && !node.isDir() {
+ return prefix, true
+ }
+ parent := filepath.Dir(prefix)
+ if parent == prefix {
+ break
+ }
+ prefix = parent
+ }
+ }
+
+ return "", false
+}
+
+// errNotDir is used to communicate from ReadDir to IsDirWithGoFiles
+// that the argument is not a directory, so that IsDirWithGoFiles doesn't
+// return an error.
+var errNotDir = errors.New("not a directory")
+
+// readDir reads a dir on disk, returning an error that is errNotDir if the dir is not a directory.
+// Unfortunately, the error returned by ioutil.ReadDir if dir is not a directory
+// can vary depending on the OS (Linux, Mac, Windows return ENOTDIR; BSD returns EINVAL).
+func readDir(dir string) ([]fs.FileInfo, error) {
+ fis, err := ioutil.ReadDir(dir)
+ if err == nil {
+ return fis, nil
+ }
+
+ if os.IsNotExist(err) {
+ return nil, err
+ }
+ if dirfi, staterr := os.Stat(dir); staterr == nil && !dirfi.IsDir() {
+ return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: errNotDir}
+ }
+ return nil, err
+}
+
+// ReadDir provides a slice of fs.FileInfo entries corresponding
+// to the overlaid files in the directory.
+func ReadDir(dir string) ([]fs.FileInfo, error) {
+ dir = canonicalize(dir)
+ if _, ok := parentIsOverlayFile(dir); ok {
+ return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: errNotDir}
+ }
+
+ dirNode := overlay[dir]
+ if dirNode == nil {
+ return readDir(dir)
+ }
+ if dirNode.isDeleted() {
+ return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: fs.ErrNotExist}
+ }
+ diskfis, err := readDir(dir)
+ if err != nil && !os.IsNotExist(err) && !errors.Is(err, errNotDir) {
+ return nil, err
+ }
+
+ // Stat files in overlay to make composite list of fileinfos
+ files := make(map[string]fs.FileInfo)
+ for _, f := range diskfis {
+ files[f.Name()] = f
+ }
+ for name, to := range dirNode.children {
+ switch {
+ case to.isDir():
+ files[name] = fakeDir(name)
+ case to.isDeleted():
+ delete(files, name)
+ default:
+ // This is a regular file.
+ f, err := os.Lstat(to.actualFilePath)
+ if err != nil {
+ files[name] = missingFile(name)
+ continue
+ } else if f.IsDir() {
+ return nil, fmt.Errorf("for overlay of %q to %q: overlay Replace entries can't point to dirctories",
+ filepath.Join(dir, name), to.actualFilePath)
+ }
+ // Add a fileinfo for the overlaid file, so that it has
+ // the original file's name, but the overlaid file's metadata.
+ files[name] = fakeFile{name, f}
+ }
+ }
+ sortedFiles := diskfis[:0]
+ for _, f := range files {
+ sortedFiles = append(sortedFiles, f)
+ }
+ sort.Slice(sortedFiles, func(i, j int) bool { return sortedFiles[i].Name() < sortedFiles[j].Name() })
+ return sortedFiles, nil
+}
+
+// OverlayPath returns the path to the overlaid contents of the
+// file, the empty string if the overlay deletes the file, or path
+// itself if the file is not in the overlay, the file is a directory
+// in the overlay, or there is no overlay.
+// It returns true if the path is overlaid with a regular file
+// or deleted, and false otherwise.
+func OverlayPath(path string) (string, bool) {
+ if p, ok := overlay[canonicalize(path)]; ok && !p.isDir() {
+ return p.actualFilePath, ok
+ }
+
+ return path, false
+}
+
+// Open opens the file at or overlaid on the given path.
+func Open(path string) (*os.File, error) {
+ return OpenFile(path, os.O_RDONLY, 0)
+}
+
+// OpenFile opens the file at or overlaid on the given path with the flag and perm.
+func OpenFile(path string, flag int, perm os.FileMode) (*os.File, error) {
+ cpath := canonicalize(path)
+ if node, ok := overlay[cpath]; ok {
+ // Opening a file in the overlay.
+ if node.isDir() {
+ return nil, &fs.PathError{Op: "OpenFile", Path: path, Err: errors.New("fsys.OpenFile doesn't support opening directories yet")}
+ }
+ // We can't open overlaid paths for write.
+ if perm != os.FileMode(os.O_RDONLY) {
+ return nil, &fs.PathError{Op: "OpenFile", Path: path, Err: errors.New("overlaid files can't be opened for write")}
+ }
+ return os.OpenFile(node.actualFilePath, flag, perm)
+ }
+ if parent, ok := parentIsOverlayFile(filepath.Dir(cpath)); ok {
+ // The file is deleted explicitly in the Replace map,
+ // or implicitly because one of its parent directories was
+ // replaced by a file.
+ return nil, &fs.PathError{
+ Op: "Open",
+ Path: path,
+ Err: fmt.Errorf("file %s does not exist: parent directory %s is replaced by a file in overlay", path, parent),
+ }
+ }
+ return os.OpenFile(cpath, flag, perm)
+}
+
+// IsDirWithGoFiles reports whether dir is a directory containing Go files
+// either on disk or in the overlay.
+func IsDirWithGoFiles(dir string) (bool, error) {
+ fis, err := ReadDir(dir)
+ if os.IsNotExist(err) || errors.Is(err, errNotDir) {
+ return false, nil
+ }
+ if err != nil {
+ return false, err
+ }
+
+ var firstErr error
+ for _, fi := range fis {
+ if fi.IsDir() {
+ continue
+ }
+
+ // TODO(matloob): this enforces that the "from" in the map
+ // has a .go suffix, but the actual destination file
+ // doesn't need to have a .go suffix. Is this okay with the
+ // compiler?
+ if !strings.HasSuffix(fi.Name(), ".go") {
+ continue
+ }
+ if fi.Mode().IsRegular() {
+ return true, nil
+ }
+
+ // fi is the result of an Lstat, so it doesn't follow symlinks.
+ // But it's okay if the file is a symlink pointing to a regular
+ // file, so use os.Stat to follow symlinks and check that.
+ actualFilePath, _ := OverlayPath(filepath.Join(dir, fi.Name()))
+ fi, err := os.Stat(actualFilePath)
+ if err == nil && fi.Mode().IsRegular() {
+ return true, nil
+ }
+ if err != nil && firstErr == nil {
+ firstErr = err
+ }
+ }
+
+ // No go files found in directory.
+ return false, firstErr
+}
+
+// walk recursively descends path, calling walkFn. Copied, with some
+// modifications from path/filepath.walk.
+func walk(path string, info fs.FileInfo, walkFn filepath.WalkFunc) error {
+ if !info.IsDir() {
+ return walkFn(path, info, nil)
+ }
+
+ fis, readErr := ReadDir(path)
+ walkErr := walkFn(path, info, readErr)
+ // If readErr != nil, walk can't walk into this directory.
+ // walkErr != nil means walkFn want walk to skip this directory or stop walking.
+ // Therefore, if one of readErr and walkErr isn't nil, walk will return.
+ if readErr != nil || walkErr != nil {
+ // The caller's behavior is controlled by the return value, which is decided
+ // by walkFn. walkFn may ignore readErr and return nil.
+ // If walkFn returns SkipDir, it will be handled by the caller.
+ // So walk should return whatever walkFn returns.
+ return walkErr
+ }
+
+ for _, fi := range fis {
+ filename := filepath.Join(path, fi.Name())
+ if walkErr = walk(filename, fi, walkFn); walkErr != nil {
+ if !fi.IsDir() || walkErr != filepath.SkipDir {
+ return walkErr
+ }
+ }
+ }
+ return nil
+}
+
+// Walk walks the file tree rooted at root, calling walkFn for each file or
+// directory in the tree, including root.
+func Walk(root string, walkFn filepath.WalkFunc) error {
+ info, err := Lstat(root)
+ if err != nil {
+ err = walkFn(root, nil, err)
+ } else {
+ err = walk(root, info, walkFn)
+ }
+ if err == filepath.SkipDir {
+ return nil
+ }
+ return err
+}
+
+// lstat implements a version of os.Lstat that operates on the overlay filesystem.
+func Lstat(path string) (fs.FileInfo, error) {
+ return overlayStat(path, os.Lstat, "lstat")
+}
+
+// Stat implements a version of os.Stat that operates on the overlay filesystem.
+func Stat(path string) (fs.FileInfo, error) {
+ return overlayStat(path, os.Stat, "stat")
+}
+
+// overlayStat implements lstat or Stat (depending on whether os.Lstat or os.Stat is passed in).
+func overlayStat(path string, osStat func(string) (fs.FileInfo, error), opName string) (fs.FileInfo, error) {
+ cpath := canonicalize(path)
+
+ if _, ok := parentIsOverlayFile(filepath.Dir(cpath)); ok {
+ return nil, &fs.PathError{Op: opName, Path: cpath, Err: fs.ErrNotExist}
+ }
+
+ node, ok := overlay[cpath]
+ if !ok {
+ // The file or directory is not overlaid.
+ return osStat(path)
+ }
+
+ switch {
+ case node.isDeleted():
+ return nil, &fs.PathError{Op: "lstat", Path: cpath, Err: fs.ErrNotExist}
+ case node.isDir():
+ return fakeDir(filepath.Base(path)), nil
+ default:
+ fi, err := osStat(node.actualFilePath)
+ if err != nil {
+ return nil, err
+ }
+ return fakeFile{name: filepath.Base(path), real: fi}, nil
+ }
+}
+
+// fakeFile provides an fs.FileInfo implementation for an overlaid file,
+// so that the file has the name of the overlaid file, but takes all
+// other characteristics of the replacement file.
+type fakeFile struct {
+ name string
+ real fs.FileInfo
+}
+
+func (f fakeFile) Name() string { return f.name }
+func (f fakeFile) Size() int64 { return f.real.Size() }
+func (f fakeFile) Mode() fs.FileMode { return f.real.Mode() }
+func (f fakeFile) ModTime() time.Time { return f.real.ModTime() }
+func (f fakeFile) IsDir() bool { return f.real.IsDir() }
+func (f fakeFile) Sys() interface{} { return f.real.Sys() }
+
+// missingFile provides an fs.FileInfo for an overlaid file where the
+// destination file in the overlay doesn't exist. It returns zero values
+// for the fileInfo methods other than Name, set to the file's name, and Mode
+// set to ModeIrregular.
+type missingFile string
+
+func (f missingFile) Name() string { return string(f) }
+func (f missingFile) Size() int64 { return 0 }
+func (f missingFile) Mode() fs.FileMode { return fs.ModeIrregular }
+func (f missingFile) ModTime() time.Time { return time.Unix(0, 0) }
+func (f missingFile) IsDir() bool { return false }
+func (f missingFile) Sys() interface{} { return nil }
+
+// fakeDir provides an fs.FileInfo implementation for directories that are
+// implicitly created by overlaid files. Each directory in the
+// path of an overlaid file is considered to exist in the overlay filesystem.
+type fakeDir string
+
+func (f fakeDir) Name() string { return string(f) }
+func (f fakeDir) Size() int64 { return 0 }
+func (f fakeDir) Mode() fs.FileMode { return fs.ModeDir | 0500 }
+func (f fakeDir) ModTime() time.Time { return time.Unix(0, 0) }
+func (f fakeDir) IsDir() bool { return true }
+func (f fakeDir) Sys() interface{} { return nil }
+
+// Glob is like filepath.Glob but uses the overlay file system.
+func Glob(pattern string) (matches []string, err error) {
+ // Check pattern is well-formed.
+ if _, err := filepath.Match(pattern, ""); err != nil {
+ return nil, err
+ }
+ if !hasMeta(pattern) {
+ if _, err = Lstat(pattern); err != nil {
+ return nil, nil
+ }
+ return []string{pattern}, nil
+ }
+
+ dir, file := filepath.Split(pattern)
+ volumeLen := 0
+ if runtime.GOOS == "windows" {
+ volumeLen, dir = cleanGlobPathWindows(dir)
+ } else {
+ dir = cleanGlobPath(dir)
+ }
+
+ if !hasMeta(dir[volumeLen:]) {
+ return glob(dir, file, nil)
+ }
+
+ // Prevent infinite recursion. See issue 15879.
+ if dir == pattern {
+ return nil, filepath.ErrBadPattern
+ }
+
+ var m []string
+ m, err = Glob(dir)
+ if err != nil {
+ return
+ }
+ for _, d := range m {
+ matches, err = glob(d, file, matches)
+ if err != nil {
+ return
+ }
+ }
+ return
+}
+
+// cleanGlobPath prepares path for glob matching.
+func cleanGlobPath(path string) string {
+ switch path {
+ case "":
+ return "."
+ case string(filepath.Separator):
+ // do nothing to the path
+ return path
+ default:
+ return path[0 : len(path)-1] // chop off trailing separator
+ }
+}
+
+func volumeNameLen(path string) int {
+ isSlash := func(c uint8) bool {
+ return c == '\\' || c == '/'
+ }
+ if len(path) < 2 {
+ return 0
+ }
+ // with drive letter
+ c := path[0]
+ if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
+ return 2
+ }
+ // is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+ if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) &&
+ !isSlash(path[2]) && path[2] != '.' {
+ // first, leading `\\` and next shouldn't be `\`. its server name.
+ for n := 3; n < l-1; n++ {
+ // second, next '\' shouldn't be repeated.
+ if isSlash(path[n]) {
+ n++
+ // third, following something characters. its share name.
+ if !isSlash(path[n]) {
+ if path[n] == '.' {
+ break
+ }
+ for ; n < l; n++ {
+ if isSlash(path[n]) {
+ break
+ }
+ }
+ return n
+ }
+ break
+ }
+ }
+ }
+ return 0
+}
+
+// cleanGlobPathWindows is windows version of cleanGlobPath.
+func cleanGlobPathWindows(path string) (prefixLen int, cleaned string) {
+ vollen := volumeNameLen(path)
+ switch {
+ case path == "":
+ return 0, "."
+ case vollen+1 == len(path) && os.IsPathSeparator(path[len(path)-1]): // /, \, C:\ and C:/
+ // do nothing to the path
+ return vollen + 1, path
+ case vollen == len(path) && len(path) == 2: // C:
+ return vollen, path + "." // convert C: into C:.
+ default:
+ if vollen >= len(path) {
+ vollen = len(path) - 1
+ }
+ return vollen, path[0 : len(path)-1] // chop off trailing separator
+ }
+}
+
+// glob searches for files matching pattern in the directory dir
+// and appends them to matches. If the directory cannot be
+// opened, it returns the existing matches. New matches are
+// added in lexicographical order.
+func glob(dir, pattern string, matches []string) (m []string, e error) {
+ m = matches
+ fi, err := Stat(dir)
+ if err != nil {
+ return // ignore I/O error
+ }
+ if !fi.IsDir() {
+ return // ignore I/O error
+ }
+
+ list, err := ReadDir(dir)
+ if err != nil {
+ return // ignore I/O error
+ }
+
+ var names []string
+ for _, info := range list {
+ names = append(names, info.Name())
+ }
+ sort.Strings(names)
+
+ for _, n := range names {
+ matched, err := filepath.Match(pattern, n)
+ if err != nil {
+ return m, err
+ }
+ if matched {
+ m = append(m, filepath.Join(dir, n))
+ }
+ }
+ return
+}
+
+// hasMeta reports whether path contains any of the magic characters
+// recognized by filepath.Match.
+func hasMeta(path string) bool {
+ magicChars := `*?[`
+ if runtime.GOOS != "windows" {
+ magicChars = `*?[\`
+ }
+ return strings.ContainsAny(path, magicChars)
+}
diff --git a/src/cmd/go/internal/fsys/fsys_test.go b/src/cmd/go/internal/fsys/fsys_test.go
new file mode 100644
index 0000000..7f175c7
--- /dev/null
+++ b/src/cmd/go/internal/fsys/fsys_test.go
@@ -0,0 +1,1094 @@
+package fsys
+
+import (
+ "cmd/go/internal/txtar"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "internal/testenv"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+)
+
+// initOverlay resets the overlay state to reflect the config.
+// config should be a text archive string. The comment is the overlay config
+// json, and the files, in the archive are laid out in a temp directory
+// that cwd is set to.
+func initOverlay(t *testing.T, config string) {
+ t.Helper()
+
+ // Create a temporary directory and chdir to it.
+ prevwd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ cwd = filepath.Join(t.TempDir(), "root")
+ if err := os.Mkdir(cwd, 0777); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chdir(cwd); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ overlay = nil
+ if err := os.Chdir(prevwd); err != nil {
+ t.Fatal(err)
+ }
+ })
+
+ a := txtar.Parse([]byte(config))
+ for _, f := range a.Files {
+ name := filepath.Join(cwd, f.Name)
+ if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(name, f.Data, 0666); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ var overlayJSON OverlayJSON
+ if err := json.Unmarshal(a.Comment, &overlayJSON); err != nil {
+ t.Fatal(fmt.Errorf("parsing overlay JSON: %v", err))
+ }
+
+ initFromJSON(overlayJSON)
+}
+
+func TestIsDir(t *testing.T) {
+ initOverlay(t, `
+{
+ "Replace": {
+ "subdir2/file2.txt": "overlayfiles/subdir2_file2.txt",
+ "subdir4": "overlayfiles/subdir4",
+ "subdir3/file3b.txt": "overlayfiles/subdir3_file3b.txt",
+ "subdir5": "",
+ "subdir6": ""
+ }
+}
+-- subdir1/file1.txt --
+
+-- subdir3/file3a.txt --
+33
+-- subdir4/file4.txt --
+444
+-- overlayfiles/subdir2_file2.txt --
+2
+-- overlayfiles/subdir3_file3b.txt --
+66666
+-- overlayfiles/subdir4 --
+x
+-- subdir6/file6.txt --
+six
+`)
+
+ testCases := []struct {
+ path string
+ want, wantErr bool
+ }{
+ {"", true, true},
+ {".", true, false},
+ {cwd, true, false},
+ {cwd + string(filepath.Separator), true, false},
+ // subdir1 is only on disk
+ {filepath.Join(cwd, "subdir1"), true, false},
+ {"subdir1", true, false},
+ {"subdir1" + string(filepath.Separator), true, false},
+ {"subdir1/file1.txt", false, false},
+ {"subdir1/doesntexist.txt", false, true},
+ {"doesntexist", false, true},
+ // subdir2 is only in overlay
+ {filepath.Join(cwd, "subdir2"), true, false},
+ {"subdir2", true, false},
+ {"subdir2" + string(filepath.Separator), true, false},
+ {"subdir2/file2.txt", false, false},
+ {"subdir2/doesntexist.txt", false, true},
+ // subdir3 has files on disk and in overlay
+ {filepath.Join(cwd, "subdir3"), true, false},
+ {"subdir3", true, false},
+ {"subdir3" + string(filepath.Separator), true, false},
+ {"subdir3/file3a.txt", false, false},
+ {"subdir3/file3b.txt", false, false},
+ {"subdir3/doesntexist.txt", false, true},
+ // subdir4 is overlaid with a file
+ {filepath.Join(cwd, "subdir4"), false, false},
+ {"subdir4", false, false},
+ {"subdir4" + string(filepath.Separator), false, false},
+ {"subdir4/file4.txt", false, false},
+ {"subdir4/doesntexist.txt", false, false},
+ // subdir5 doesn't exist, and is overlaid with a "delete" entry
+ {filepath.Join(cwd, "subdir5"), false, false},
+ {"subdir5", false, false},
+ {"subdir5" + string(filepath.Separator), false, false},
+ {"subdir5/file5.txt", false, false},
+ {"subdir5/doesntexist.txt", false, false},
+ // subdir6 does exist, and is overlaid with a "delete" entry
+ {filepath.Join(cwd, "subdir6"), false, false},
+ {"subdir6", false, false},
+ {"subdir6" + string(filepath.Separator), false, false},
+ {"subdir6/file6.txt", false, false},
+ {"subdir6/doesntexist.txt", false, false},
+ }
+
+ for _, tc := range testCases {
+ got, err := IsDir(tc.path)
+ if err != nil {
+ if !tc.wantErr {
+ t.Errorf("IsDir(%q): got error with string %q, want no error", tc.path, err.Error())
+ }
+ continue
+ }
+ if tc.wantErr {
+ t.Errorf("IsDir(%q): got no error, want error", tc.path)
+ }
+ if tc.want != got {
+ t.Errorf("IsDir(%q) = %v, want %v", tc.path, got, tc.want)
+ }
+ }
+}
+
+const readDirOverlay = `
+{
+ "Replace": {
+ "subdir2/file2.txt": "overlayfiles/subdir2_file2.txt",
+ "subdir4": "overlayfiles/subdir4",
+ "subdir3/file3b.txt": "overlayfiles/subdir3_file3b.txt",
+ "subdir5": "",
+ "subdir6/asubsubdir/afile.txt": "overlayfiles/subdir6_asubsubdir_afile.txt",
+ "subdir6/asubsubdir/zfile.txt": "overlayfiles/subdir6_asubsubdir_zfile.txt",
+ "subdir6/zsubsubdir/file.txt": "overlayfiles/subdir6_zsubsubdir_file.txt",
+ "subdir7/asubsubdir/file.txt": "overlayfiles/subdir7_asubsubdir_file.txt",
+ "subdir7/zsubsubdir/file.txt": "overlayfiles/subdir7_zsubsubdir_file.txt",
+ "subdir8/doesntexist": "this_file_doesnt_exist_anywhere",
+ "other/pointstodir": "overlayfiles/this_is_a_directory",
+ "parentoverwritten/subdir1": "overlayfiles/parentoverwritten_subdir1",
+ "subdir9/this_file_is_overlaid.txt": "overlayfiles/subdir9_this_file_is_overlaid.txt",
+ "subdir10/only_deleted_file.txt": "",
+ "subdir11/deleted.txt": "",
+ "subdir11": "overlayfiles/subdir11",
+ "textfile.txt/file.go": "overlayfiles/textfile_txt_file.go"
+ }
+}
+-- subdir1/file1.txt --
+
+-- subdir3/file3a.txt --
+33
+-- subdir4/file4.txt --
+444
+-- subdir6/file.txt --
+-- subdir6/asubsubdir/file.txt --
+-- subdir6/anothersubsubdir/file.txt --
+-- subdir9/this_file_is_overlaid.txt --
+-- subdir10/only_deleted_file.txt --
+this will be deleted in overlay
+-- subdir11/deleted.txt --
+-- parentoverwritten/subdir1/subdir2/subdir3/file.txt --
+-- textfile.txt --
+this will be overridden by textfile.txt/file.go
+-- overlayfiles/subdir2_file2.txt --
+2
+-- overlayfiles/subdir3_file3b.txt --
+66666
+-- overlayfiles/subdir4 --
+x
+-- overlayfiles/subdir6_asubsubdir_afile.txt --
+-- overlayfiles/subdir6_asubsubdir_zfile.txt --
+-- overlayfiles/subdir6_zsubsubdir_file.txt --
+-- overlayfiles/subdir7_asubsubdir_file.txt --
+-- overlayfiles/subdir7_zsubsubdir_file.txt --
+-- overlayfiles/parentoverwritten_subdir1 --
+x
+-- overlayfiles/subdir9_this_file_is_overlaid.txt --
+99999999
+-- overlayfiles/subdir11 --
+-- overlayfiles/this_is_a_directory/file.txt --
+-- overlayfiles/textfile_txt_file.go --
+x
+`
+
+func TestReadDir(t *testing.T) {
+ initOverlay(t, readDirOverlay)
+
+ type entry struct {
+ name string
+ size int64
+ isDir bool
+ }
+
+ testCases := []struct {
+ dir string
+ want []entry
+ }{
+ {
+ ".", []entry{
+ {"other", 0, true},
+ {"overlayfiles", 0, true},
+ {"parentoverwritten", 0, true},
+ {"subdir1", 0, true},
+ {"subdir10", 0, true},
+ {"subdir11", 0, false},
+ {"subdir2", 0, true},
+ {"subdir3", 0, true},
+ {"subdir4", 2, false},
+ // no subdir5.
+ {"subdir6", 0, true},
+ {"subdir7", 0, true},
+ {"subdir8", 0, true},
+ {"subdir9", 0, true},
+ {"textfile.txt", 0, true},
+ },
+ },
+ {
+ "subdir1", []entry{
+ {"file1.txt", 1, false},
+ },
+ },
+ {
+ "subdir2", []entry{
+ {"file2.txt", 2, false},
+ },
+ },
+ {
+ "subdir3", []entry{
+ {"file3a.txt", 3, false},
+ {"file3b.txt", 6, false},
+ },
+ },
+ {
+ "subdir6", []entry{
+ {"anothersubsubdir", 0, true},
+ {"asubsubdir", 0, true},
+ {"file.txt", 0, false},
+ {"zsubsubdir", 0, true},
+ },
+ },
+ {
+ "subdir6/asubsubdir", []entry{
+ {"afile.txt", 0, false},
+ {"file.txt", 0, false},
+ {"zfile.txt", 0, false},
+ },
+ },
+ {
+ "subdir8", []entry{
+ {"doesntexist", 0, false}, // entry is returned even if destination file doesn't exist
+ },
+ },
+ {
+ // check that read dir actually redirects files that already exist
+ // the original this_file_is_overlaid.txt is empty
+ "subdir9", []entry{
+ {"this_file_is_overlaid.txt", 9, false},
+ },
+ },
+ {
+ "subdir10", []entry{},
+ },
+ {
+ "parentoverwritten", []entry{
+ {"subdir1", 2, false},
+ },
+ },
+ {
+ "textfile.txt", []entry{
+ {"file.go", 2, false},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ dir, want := tc.dir, tc.want
+ infos, err := ReadDir(dir)
+ if err != nil {
+ t.Errorf("ReadDir(%q): %v", dir, err)
+ continue
+ }
+ // Sorted diff of want and infos.
+ for len(infos) > 0 || len(want) > 0 {
+ switch {
+ case len(want) == 0 || len(infos) > 0 && infos[0].Name() < want[0].name:
+ t.Errorf("ReadDir(%q): unexpected entry: %s IsDir=%v Size=%v", dir, infos[0].Name(), infos[0].IsDir(), infos[0].Size())
+ infos = infos[1:]
+ case len(infos) == 0 || len(want) > 0 && want[0].name < infos[0].Name():
+ t.Errorf("ReadDir(%q): missing entry: %s IsDir=%v Size=%v", dir, want[0].name, want[0].isDir, want[0].size)
+ want = want[1:]
+ default:
+ infoSize := infos[0].Size()
+ if want[0].isDir {
+ infoSize = 0
+ }
+ if infos[0].IsDir() != want[0].isDir || want[0].isDir && infoSize != want[0].size {
+ t.Errorf("ReadDir(%q): %s: IsDir=%v Size=%v, want IsDir=%v Size=%v", dir, want[0].name, infos[0].IsDir(), infoSize, want[0].isDir, want[0].size)
+ }
+ infos = infos[1:]
+ want = want[1:]
+ }
+ }
+ }
+
+ errCases := []string{
+ "subdir1/file1.txt", // regular file on disk
+ "subdir2/file2.txt", // regular file in overlay
+ "subdir4", // directory overlaid with regular file
+ "subdir5", // directory deleted in overlay
+ "parentoverwritten/subdir1/subdir2/subdir3", // parentoverwritten/subdir1 overlaid with regular file
+ "parentoverwritten/subdir1/subdir2", // parentoverwritten/subdir1 overlaid with regular file
+ "subdir11", // directory with deleted child, overlaid with regular file
+ "other/pointstodir",
+ }
+
+ for _, dir := range errCases {
+ _, err := ReadDir(dir)
+ if _, ok := err.(*fs.PathError); !ok {
+ t.Errorf("ReadDir(%q): err = %T (%v), want fs.PathError", dir, err, err)
+ }
+ }
+}
+
+func TestGlob(t *testing.T) {
+ initOverlay(t, readDirOverlay)
+
+ testCases := []struct {
+ pattern string
+ match []string
+ }{
+ {
+ "*o*",
+ []string{
+ "other",
+ "overlayfiles",
+ "parentoverwritten",
+ },
+ },
+ {
+ "subdir2/file2.txt",
+ []string{
+ "subdir2/file2.txt",
+ },
+ },
+ {
+ "*/*.txt",
+ []string{
+ "overlayfiles/subdir2_file2.txt",
+ "overlayfiles/subdir3_file3b.txt",
+ "overlayfiles/subdir6_asubsubdir_afile.txt",
+ "overlayfiles/subdir6_asubsubdir_zfile.txt",
+ "overlayfiles/subdir6_zsubsubdir_file.txt",
+ "overlayfiles/subdir7_asubsubdir_file.txt",
+ "overlayfiles/subdir7_zsubsubdir_file.txt",
+ "overlayfiles/subdir9_this_file_is_overlaid.txt",
+ "subdir1/file1.txt",
+ "subdir2/file2.txt",
+ "subdir3/file3a.txt",
+ "subdir3/file3b.txt",
+ "subdir6/file.txt",
+ "subdir9/this_file_is_overlaid.txt",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ pattern := tc.pattern
+ match, err := Glob(pattern)
+ if err != nil {
+ t.Errorf("Glob(%q): %v", pattern, err)
+ continue
+ }
+ want := tc.match
+ for i, name := range want {
+ if name != tc.pattern {
+ want[i] = filepath.FromSlash(name)
+ }
+ }
+ for len(match) > 0 || len(want) > 0 {
+ switch {
+ case len(match) == 0 || len(want) > 0 && want[0] < match[0]:
+ t.Errorf("Glob(%q): missing match: %s", pattern, want[0])
+ want = want[1:]
+ case len(want) == 0 || len(match) > 0 && match[0] < want[0]:
+ t.Errorf("Glob(%q): extra match: %s", pattern, match[0])
+ match = match[1:]
+ default:
+ want = want[1:]
+ match = match[1:]
+ }
+ }
+ }
+}
+
+func TestOverlayPath(t *testing.T) {
+ initOverlay(t, `
+{
+ "Replace": {
+ "subdir2/file2.txt": "overlayfiles/subdir2_file2.txt",
+ "subdir3/doesntexist": "this_file_doesnt_exist_anywhere",
+ "subdir4/this_file_is_overlaid.txt": "overlayfiles/subdir4_this_file_is_overlaid.txt",
+ "subdir5/deleted.txt": "",
+ "parentoverwritten/subdir1": ""
+ }
+}
+-- subdir1/file1.txt --
+file 1
+-- subdir4/this_file_is_overlaid.txt --
+these contents are replaced by the overlay
+-- parentoverwritten/subdir1/subdir2/subdir3/file.txt --
+-- subdir5/deleted.txt --
+deleted
+-- overlayfiles/subdir2_file2.txt --
+file 2
+-- overlayfiles/subdir4_this_file_is_overlaid.txt --
+99999999
+`)
+
+ testCases := []struct {
+ path string
+ wantPath string
+ wantOK bool
+ }{
+ {"subdir1/file1.txt", "subdir1/file1.txt", false},
+ // OverlayPath returns false for directories
+ {"subdir2", "subdir2", false},
+ {"subdir2/file2.txt", filepath.Join(cwd, "overlayfiles/subdir2_file2.txt"), true},
+ // OverlayPath doesn't stat a file to see if it exists, so it happily returns
+ // the 'to' path and true even if the 'to' path doesn't exist on disk.
+ {"subdir3/doesntexist", filepath.Join(cwd, "this_file_doesnt_exist_anywhere"), true},
+ // Like the subdir2/file2.txt case above, but subdir4 exists on disk, but subdir2 does not.
+ {"subdir4/this_file_is_overlaid.txt", filepath.Join(cwd, "overlayfiles/subdir4_this_file_is_overlaid.txt"), true},
+ {"subdir5", "subdir5", false},
+ {"subdir5/deleted.txt", "", true},
+ }
+
+ for _, tc := range testCases {
+ gotPath, gotOK := OverlayPath(tc.path)
+ if gotPath != tc.wantPath || gotOK != tc.wantOK {
+ t.Errorf("OverlayPath(%q): got %v, %v; want %v, %v",
+ tc.path, gotPath, gotOK, tc.wantPath, tc.wantOK)
+ }
+ }
+}
+
+func TestOpen(t *testing.T) {
+ initOverlay(t, `
+{
+ "Replace": {
+ "subdir2/file2.txt": "overlayfiles/subdir2_file2.txt",
+ "subdir3/doesntexist": "this_file_doesnt_exist_anywhere",
+ "subdir4/this_file_is_overlaid.txt": "overlayfiles/subdir4_this_file_is_overlaid.txt",
+ "subdir5/deleted.txt": "",
+ "parentoverwritten/subdir1": "",
+ "childoverlay/subdir1.txt/child.txt": "overlayfiles/child.txt",
+ "subdir11/deleted.txt": "",
+ "subdir11": "overlayfiles/subdir11",
+ "parentdeleted": "",
+ "parentdeleted/file.txt": "overlayfiles/parentdeleted_file.txt"
+ }
+}
+-- subdir11/deleted.txt --
+-- subdir1/file1.txt --
+file 1
+-- subdir4/this_file_is_overlaid.txt --
+these contents are replaced by the overlay
+-- parentoverwritten/subdir1/subdir2/subdir3/file.txt --
+-- childoverlay/subdir1.txt --
+this file doesn't exist because the path
+childoverlay/subdir1.txt/child.txt is in the overlay
+-- subdir5/deleted.txt --
+deleted
+-- parentdeleted --
+this will be deleted so that parentdeleted/file.txt can exist
+-- overlayfiles/subdir2_file2.txt --
+file 2
+-- overlayfiles/subdir4_this_file_is_overlaid.txt --
+99999999
+-- overlayfiles/child.txt --
+-- overlayfiles/subdir11 --
+11
+-- overlayfiles/parentdeleted_file.txt --
+this can exist because the parent directory is deleted
+`)
+
+ testCases := []struct {
+ path string
+ wantContents string
+ isErr bool
+ }{
+ {"subdir1/file1.txt", "file 1\n", false},
+ {"subdir2/file2.txt", "file 2\n", false},
+ {"subdir3/doesntexist", "", true},
+ {"subdir4/this_file_is_overlaid.txt", "99999999\n", false},
+ {"subdir5/deleted.txt", "", true},
+ {"parentoverwritten/subdir1/subdir2/subdir3/file.txt", "", true},
+ {"childoverlay/subdir1.txt", "", true},
+ {"subdir11", "11\n", false},
+ {"parentdeleted/file.txt", "this can exist because the parent directory is deleted\n", false},
+ }
+
+ for _, tc := range testCases {
+ f, err := Open(tc.path)
+ if tc.isErr {
+ if err == nil {
+ f.Close()
+ t.Errorf("Open(%q): got no error, but want error", tc.path)
+ }
+ continue
+ }
+ if err != nil {
+ t.Errorf("Open(%q): got error %v, want nil", tc.path, err)
+ continue
+ }
+ contents, err := io.ReadAll(f)
+ if err != nil {
+ t.Errorf("unexpected error reading contents of file: %v", err)
+ }
+ if string(contents) != tc.wantContents {
+ t.Errorf("contents of file opened with Open(%q): got %q, want %q",
+ tc.path, contents, tc.wantContents)
+ }
+ f.Close()
+ }
+}
+
+func TestIsDirWithGoFiles(t *testing.T) {
+ initOverlay(t, `
+{
+ "Replace": {
+ "goinoverlay/file.go": "dummy",
+ "directory/removed/by/file": "dummy",
+ "directory_with_go_dir/dir.go/file.txt": "dummy",
+ "otherdirectory/deleted.go": "",
+ "nonexistentdirectory/deleted.go": "",
+ "textfile.txt/file.go": "dummy"
+ }
+}
+-- dummy --
+a destination file for the overlay entries to point to
+contents don't matter for this test
+-- nogo/file.txt --
+-- goondisk/file.go --
+-- goinoverlay/file.txt --
+-- directory/removed/by/file/in/overlay/file.go --
+-- otherdirectory/deleted.go --
+-- textfile.txt --
+`)
+
+ testCases := []struct {
+ dir string
+ want bool
+ wantErr bool
+ }{
+ {"nogo", false, false},
+ {"goondisk", true, false},
+ {"goinoverlay", true, false},
+ {"directory/removed/by/file/in/overlay", false, false},
+ {"directory_with_go_dir", false, false},
+ {"otherdirectory", false, false},
+ {"nonexistentdirectory", false, false},
+ {"textfile.txt", true, false},
+ }
+
+ for _, tc := range testCases {
+ got, gotErr := IsDirWithGoFiles(tc.dir)
+ if tc.wantErr {
+ if gotErr == nil {
+ t.Errorf("IsDirWithGoFiles(%q): got %v, %v; want non-nil error", tc.dir, got, gotErr)
+ }
+ continue
+ }
+ if gotErr != nil {
+ t.Errorf("IsDirWithGoFiles(%q): got %v, %v; want nil error", tc.dir, got, gotErr)
+ }
+ if got != tc.want {
+ t.Errorf("IsDirWithGoFiles(%q) = %v; want %v", tc.dir, got, tc.want)
+ }
+ }
+}
+
+func TestWalk(t *testing.T) {
+ // The root of the walk must be a name with an actual basename, not just ".".
+ // Walk uses Lstat to obtain the name of the root, and Lstat on platforms
+ // other than Plan 9 reports the name "." instead of the actual base name of
+ // the directory. (See https://golang.org/issue/42115.)
+
+ type file struct {
+ path string
+ name string
+ size int64
+ mode fs.FileMode
+ isDir bool
+ }
+ testCases := []struct {
+ name string
+ overlay string
+ root string
+ wantFiles []file
+ }{
+ {"no overlay", `
+{}
+-- dir/file.txt --
+`,
+ "dir",
+ []file{
+ {"dir", "dir", 0, fs.ModeDir | 0700, true},
+ {"dir/file.txt", "file.txt", 0, 0600, false},
+ },
+ },
+ {"overlay with different file", `
+{
+ "Replace": {
+ "dir/file.txt": "dir/other.txt"
+ }
+}
+-- dir/file.txt --
+-- dir/other.txt --
+contents of other file
+`,
+ "dir",
+ []file{
+ {"dir", "dir", 0, fs.ModeDir | 0500, true},
+ {"dir/file.txt", "file.txt", 23, 0600, false},
+ {"dir/other.txt", "other.txt", 23, 0600, false},
+ },
+ },
+ {"overlay with new file", `
+{
+ "Replace": {
+ "dir/file.txt": "dir/other.txt"
+ }
+}
+-- dir/other.txt --
+contents of other file
+`,
+ "dir",
+ []file{
+ {"dir", "dir", 0, fs.ModeDir | 0500, true},
+ {"dir/file.txt", "file.txt", 23, 0600, false},
+ {"dir/other.txt", "other.txt", 23, 0600, false},
+ },
+ },
+ {"overlay with new directory", `
+{
+ "Replace": {
+ "dir/subdir/file.txt": "dir/other.txt"
+ }
+}
+-- dir/other.txt --
+contents of other file
+`,
+ "dir",
+ []file{
+ {"dir", "dir", 0, fs.ModeDir | 0500, true},
+ {"dir/other.txt", "other.txt", 23, 0600, false},
+ {"dir/subdir", "subdir", 0, fs.ModeDir | 0500, true},
+ {"dir/subdir/file.txt", "file.txt", 23, 0600, false},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ initOverlay(t, tc.overlay)
+
+ var got []file
+ Walk(tc.root, func(path string, info fs.FileInfo, err error) error {
+ got = append(got, file{path, info.Name(), info.Size(), info.Mode(), info.IsDir()})
+ return nil
+ })
+
+ if len(got) != len(tc.wantFiles) {
+ t.Errorf("Walk: saw %#v in walk; want %#v", got, tc.wantFiles)
+ }
+ for i := 0; i < len(got) && i < len(tc.wantFiles); i++ {
+ wantPath := filepath.FromSlash(tc.wantFiles[i].path)
+ if got[i].path != wantPath {
+ t.Errorf("path of file #%v in walk, got %q, want %q", i, got[i].path, wantPath)
+ }
+ if got[i].name != tc.wantFiles[i].name {
+ t.Errorf("name of file #%v in walk, got %q, want %q", i, got[i].name, tc.wantFiles[i].name)
+ }
+ if got[i].mode&(fs.ModeDir|0700) != tc.wantFiles[i].mode {
+ t.Errorf("mode&(fs.ModeDir|0700) for mode of file #%v in walk, got %v, want %v", i, got[i].mode&(fs.ModeDir|0700), tc.wantFiles[i].mode)
+ }
+ if got[i].isDir != tc.wantFiles[i].isDir {
+ t.Errorf("isDir for file #%v in walk, got %v, want %v", i, got[i].isDir, tc.wantFiles[i].isDir)
+ }
+ if tc.wantFiles[i].isDir {
+ continue // don't check size for directories
+ }
+ if got[i].size != tc.wantFiles[i].size {
+ t.Errorf("size of file #%v in walk, got %v, want %v", i, got[i].size, tc.wantFiles[i].size)
+ }
+ }
+ })
+ }
+}
+
+func TestWalkSkipDir(t *testing.T) {
+ initOverlay(t, `
+{
+ "Replace": {
+ "dir/skip/file.go": "dummy.txt",
+ "dir/dontskip/file.go": "dummy.txt",
+ "dir/dontskip/skip/file.go": "dummy.txt"
+ }
+}
+-- dummy.txt --
+`)
+
+ var seen []string
+ Walk("dir", func(path string, info fs.FileInfo, err error) error {
+ seen = append(seen, filepath.ToSlash(path))
+ if info.Name() == "skip" {
+ return filepath.SkipDir
+ }
+ return nil
+ })
+
+ wantSeen := []string{"dir", "dir/dontskip", "dir/dontskip/file.go", "dir/dontskip/skip", "dir/skip"}
+
+ if len(seen) != len(wantSeen) {
+ t.Errorf("paths seen in walk: got %v entries; want %v entries", len(seen), len(wantSeen))
+ }
+
+ for i := 0; i < len(seen) && i < len(wantSeen); i++ {
+ if seen[i] != wantSeen[i] {
+ t.Errorf("path #%v seen walking tree: want %q, got %q", i, seen[i], wantSeen[i])
+ }
+ }
+}
+
+func TestWalkError(t *testing.T) {
+ initOverlay(t, "{}")
+
+ alreadyCalled := false
+ err := Walk("foo", func(path string, info fs.FileInfo, err error) error {
+ if alreadyCalled {
+ t.Fatal("expected walk function to be called exactly once, but it was called more than once")
+ }
+ alreadyCalled = true
+ return errors.New("returned from function")
+ })
+ if !alreadyCalled {
+ t.Fatal("expected walk function to be called exactly once, but it was never called")
+
+ }
+ if err == nil {
+ t.Fatalf("Walk: got no error, want error")
+ }
+ if err.Error() != "returned from function" {
+ t.Fatalf("Walk: got error %v, want \"returned from function\" error", err)
+ }
+}
+
+func TestWalkSymlink(t *testing.T) {
+ testenv.MustHaveSymlink(t)
+
+ initOverlay(t, `{
+ "Replace": {"overlay_symlink": "symlink"}
+}
+-- dir/file --`)
+
+ // Create symlink
+ if err := os.Symlink("dir", "symlink"); err != nil {
+ t.Error(err)
+ }
+
+ testCases := []struct {
+ name string
+ dir string
+ wantFiles []string
+ }{
+ {"control", "dir", []string{"dir", "dir" + string(filepath.Separator) + "file"}},
+ // ensure Walk doesn't walk into the directory pointed to by the symlink
+ // (because it's supposed to use Lstat instead of Stat).
+ {"symlink_to_dir", "symlink", []string{"symlink"}},
+ {"overlay_to_symlink_to_dir", "overlay_symlink", []string{"overlay_symlink"}},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var got []string
+
+ err := Walk(tc.dir, func(path string, info fs.FileInfo, err error) error {
+ got = append(got, path)
+ if err != nil {
+ t.Errorf("walkfn: got non nil err argument: %v, want nil err argument", err)
+ }
+ return nil
+ })
+ if err != nil {
+ t.Errorf("Walk: got error %q, want nil", err)
+ }
+
+ if !reflect.DeepEqual(got, tc.wantFiles) {
+ t.Errorf("files examined by walk: got %v, want %v", got, tc.wantFiles)
+ }
+ })
+ }
+
+}
+
+func TestLstat(t *testing.T) {
+ type file struct {
+ name string
+ size int64
+ mode fs.FileMode // mode & (fs.ModeDir|0x700): only check 'user' permissions
+ isDir bool
+ }
+
+ testCases := []struct {
+ name string
+ overlay string
+ path string
+
+ want file
+ wantErr bool
+ }{
+ {
+ "regular_file",
+ `{}
+-- file.txt --
+contents`,
+ "file.txt",
+ file{"file.txt", 9, 0600, false},
+ false,
+ },
+ {
+ "new_file_in_overlay",
+ `{"Replace": {"file.txt": "dummy.txt"}}
+-- dummy.txt --
+contents`,
+ "file.txt",
+ file{"file.txt", 9, 0600, false},
+ false,
+ },
+ {
+ "file_replaced_in_overlay",
+ `{"Replace": {"file.txt": "dummy.txt"}}
+-- file.txt --
+-- dummy.txt --
+contents`,
+ "file.txt",
+ file{"file.txt", 9, 0600, false},
+ false,
+ },
+ {
+ "file_cant_exist",
+ `{"Replace": {"deleted": "dummy.txt"}}
+-- deleted/file.txt --
+-- dummy.txt --
+`,
+ "deleted/file.txt",
+ file{},
+ true,
+ },
+ {
+ "deleted",
+ `{"Replace": {"deleted": ""}}
+-- deleted --
+`,
+ "deleted",
+ file{},
+ true,
+ },
+ {
+ "dir_on_disk",
+ `{}
+-- dir/foo.txt --
+`,
+ "dir",
+ file{"dir", 0, 0700 | fs.ModeDir, true},
+ false,
+ },
+ {
+ "dir_in_overlay",
+ `{"Replace": {"dir/file.txt": "dummy.txt"}}
+-- dummy.txt --
+`,
+ "dir",
+ file{"dir", 0, 0500 | fs.ModeDir, true},
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ initOverlay(t, tc.overlay)
+ got, err := Lstat(tc.path)
+ if tc.wantErr {
+ if err == nil {
+ t.Errorf("lstat(%q): got no error, want error", tc.path)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("lstat(%q): got error %v, want no error", tc.path, err)
+ }
+ if got.Name() != tc.want.name {
+ t.Errorf("lstat(%q).Name(): got %q, want %q", tc.path, got.Name(), tc.want.name)
+ }
+ if got.Mode()&(fs.ModeDir|0700) != tc.want.mode {
+ t.Errorf("lstat(%q).Mode()&(fs.ModeDir|0700): got %v, want %v", tc.path, got.Mode()&(fs.ModeDir|0700), tc.want.mode)
+ }
+ if got.IsDir() != tc.want.isDir {
+ t.Errorf("lstat(%q).IsDir(): got %v, want %v", tc.path, got.IsDir(), tc.want.isDir)
+ }
+ if tc.want.isDir {
+ return // don't check size for directories
+ }
+ if got.Size() != tc.want.size {
+ t.Errorf("lstat(%q).Size(): got %v, want %v", tc.path, got.Size(), tc.want.size)
+ }
+ })
+ }
+}
+
+func TestStat(t *testing.T) {
+ testenv.MustHaveSymlink(t)
+
+ type file struct {
+ name string
+ size int64
+ mode os.FileMode // mode & (os.ModeDir|0x700): only check 'user' permissions
+ isDir bool
+ }
+
+ testCases := []struct {
+ name string
+ overlay string
+ path string
+
+ want file
+ wantErr bool
+ }{
+ {
+ "regular_file",
+ `{}
+-- file.txt --
+contents`,
+ "file.txt",
+ file{"file.txt", 9, 0600, false},
+ false,
+ },
+ {
+ "new_file_in_overlay",
+ `{"Replace": {"file.txt": "dummy.txt"}}
+-- dummy.txt --
+contents`,
+ "file.txt",
+ file{"file.txt", 9, 0600, false},
+ false,
+ },
+ {
+ "file_replaced_in_overlay",
+ `{"Replace": {"file.txt": "dummy.txt"}}
+-- file.txt --
+-- dummy.txt --
+contents`,
+ "file.txt",
+ file{"file.txt", 9, 0600, false},
+ false,
+ },
+ {
+ "file_cant_exist",
+ `{"Replace": {"deleted": "dummy.txt"}}
+-- deleted/file.txt --
+-- dummy.txt --
+`,
+ "deleted/file.txt",
+ file{},
+ true,
+ },
+ {
+ "deleted",
+ `{"Replace": {"deleted": ""}}
+-- deleted --
+`,
+ "deleted",
+ file{},
+ true,
+ },
+ {
+ "dir_on_disk",
+ `{}
+-- dir/foo.txt --
+`,
+ "dir",
+ file{"dir", 0, 0700 | os.ModeDir, true},
+ false,
+ },
+ {
+ "dir_in_overlay",
+ `{"Replace": {"dir/file.txt": "dummy.txt"}}
+-- dummy.txt --
+`,
+ "dir",
+ file{"dir", 0, 0500 | os.ModeDir, true},
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ initOverlay(t, tc.overlay)
+ got, err := Stat(tc.path)
+ if tc.wantErr {
+ if err == nil {
+ t.Errorf("Stat(%q): got no error, want error", tc.path)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("Stat(%q): got error %v, want no error", tc.path, err)
+ }
+ if got.Name() != tc.want.name {
+ t.Errorf("Stat(%q).Name(): got %q, want %q", tc.path, got.Name(), tc.want.name)
+ }
+ if got.Mode()&(os.ModeDir|0700) != tc.want.mode {
+ t.Errorf("Stat(%q).Mode()&(os.ModeDir|0700): got %v, want %v", tc.path, got.Mode()&(os.ModeDir|0700), tc.want.mode)
+ }
+ if got.IsDir() != tc.want.isDir {
+ t.Errorf("Stat(%q).IsDir(): got %v, want %v", tc.path, got.IsDir(), tc.want.isDir)
+ }
+ if tc.want.isDir {
+ return // don't check size for directories
+ }
+ if got.Size() != tc.want.size {
+ t.Errorf("Stat(%q).Size(): got %v, want %v", tc.path, got.Size(), tc.want.size)
+ }
+ })
+ }
+}
+
+func TestStatSymlink(t *testing.T) {
+ testenv.MustHaveSymlink(t)
+
+ initOverlay(t, `{
+ "Replace": {"file.go": "symlink"}
+}
+-- to.go --
+0123456789
+`)
+
+ // Create symlink
+ if err := os.Symlink("to.go", "symlink"); err != nil {
+ t.Error(err)
+ }
+
+ f := "file.go"
+ fi, err := Stat(f)
+ if err != nil {
+ t.Errorf("Stat(%q): got error %q, want nil error", f, err)
+ }
+
+ if !fi.Mode().IsRegular() {
+ t.Errorf("Stat(%q).Mode(): got %v, want regular mode", f, fi.Mode())
+ }
+
+ if fi.Size() != 11 {
+ t.Errorf("Stat(%q).Size(): got %v, want 11", f, fi.Size())
+ }
+}