to string
}
+var binds []replace
+
+// Bind makes the virtual file system use dir as if it were mounted at mtpt,
+// like Plan 9's “bind” or Linux's “mount --bind”, or like os.Symlink
+// but without the symbolic link.
+//
+// For now, the behavior of using Bind on multiple overlapping
+// mountpoints (for example Bind("x", "/a") and Bind("y", "/a/b"))
+// is undefined.
+func Bind(dir, mtpt string) {
+ if dir == "" || mtpt == "" {
+ panic("Bind of empty directory")
+ }
+ binds = append(binds, replace{abs(mtpt), abs(dir)})
+}
+
// cwd returns the current directory, caching it on first use.
var cwd = sync.OnceValue(cwdOnce)
abs string
deleted bool
replaced bool
- dir bool
+ dir bool // must be dir
+ file bool // must be file
actual string
}
return info{abs: apath, actual: path}
}
+ // Apply bind replacements before applying overlay.
+ replaced := false
+ for _, r := range binds {
+ if str.HasFilePathPrefix(apath, r.from) {
+ // apath is below r.from.
+ // Replace prefix with r.to and fall through to overlay.
+ apath = r.to + apath[len(r.from):]
+ path = apath
+ replaced = true
+ break
+ }
+ if str.HasFilePathPrefix(r.from, apath) {
+ // apath is above r.from.
+ // Synthesize a directory in case one does not exist.
+ return info{abs: apath, replaced: true, dir: true, actual: path}
+ }
+ }
+
// Binary search for apath to find the nearest relevant entry in the overlay.
i, ok := slices.BinarySearchFunc(overlay, apath, searchcmp)
if ok {
return info{abs: apath, replaced: true, dir: true, actual: path}
}
// Replaced file.
- return info{abs: apath, replaced: true, actual: r.to}
+ return info{abs: apath, replaced: true, file: true, actual: r.to}
}
if i < len(overlay) && str.HasFilePathPrefix(overlay[i].from, apath) {
// Replacement for child path; infer existence of parent directory.
// Parent replaced by file; path is deleted.
return info{abs: apath, deleted: true}
}
- return info{abs: apath, actual: path}
+ return info{abs: apath, replaced: replaced, actual: path}
}
// children returns a sequence of (name, info)
// implied by the overlay.
func (i *info) children() iter.Seq2[string, info] {
return func(yield func(string, info) bool) {
+ // Build list of directory children implied by the binds.
+ // Binds are not sorted, so just loop over them.
+ var dirs []string
+ for _, m := range binds {
+ if str.HasFilePathPrefix(m.from, i.abs) && m.from != i.abs {
+ name := m.from[len(i.abs)+1:]
+ if i := strings.IndexByte(name, filepath.Separator); i >= 0 {
+ name = name[:i]
+ }
+ dirs = append(dirs, name)
+ }
+ }
+ if len(dirs) > 1 {
+ slices.Sort(dirs)
+ str.Uniq(&dirs)
+ }
+
// Loop looking for next possible child in sorted overlay,
// which is previous child plus "\x00".
target := i.abs + string(filepath.Separator) + "\x00"
}
if j >= len(overlay) {
// Nothing found at all.
- return
+ break
}
r := overlay[j]
if !str.HasFilePathPrefix(r.from, i.abs) {
// Next entry in overlay is beyond the directory we want; all done.
- return
+ break
}
// Found the next child in the directory.
dir: dir || strings.HasSuffix(r.to, string(filepath.Separator)),
actual: actual,
}
+ for ; len(dirs) > 0 && dirs[0] < name; dirs = dirs[1:] {
+ if !yield(dirs[0], info{abs: filepath.Join(i.abs, dirs[0]), replaced: true, dir: true}) {
+ return
+ }
+ }
+ if len(dirs) > 0 && dirs[0] == name {
+ dirs = dirs[1:]
+ }
if !yield(name, ci) {
return
}
goto Loop
}
}
+
+ for _, dir := range dirs {
+ if !yield(dir, info{abs: filepath.Join(i.abs, dir), replaced: true, dir: true}) {
+ return
+ }
+ }
}
}
if !info.replaced {
return osReadDir(name)
}
- if !info.dir {
+ if info.file {
return nil, &fs.PathError{Op: "read", Path: name, Err: errNotDir}
}
// Start with normal disk listing.
- dirs, err := osReadDir(name)
+ dirs, err := osReadDir(info.actual)
if err != nil && !os.IsNotExist(err) && !errors.Is(err, errNotDir) {
return nil, err
}
+ dirErr := err
// Merge disk listing and overlay entries in map.
all := make(map[string]fs.DirEntry)
dirs = append(dirs, d)
}
slices.SortFunc(dirs, func(x, y fs.DirEntry) int { return strings.Compare(x.Name(), y.Name()) })
+
+ if len(dirs) == 0 {
+ return nil, dirErr
+ }
return dirs, nil
}
func resetForTesting() {
cwd = sync.OnceValue(cwdOnce)
overlay = nil
+ binds = nil
}
// initOverlay resets the overlay state to reflect the config.
}{
{"foo", info{abs: "/tmp/foo", actual: "foo"}},
{"foo/bar/baz/quux", info{abs: "/tmp/foo/bar/baz/quux", actual: "foo/bar/baz/quux"}},
- {"x", info{abs: "/tmp/x", replaced: true, actual: "/tmp/replace/x"}},
- {"/tmp/x", info{abs: "/tmp/x", replaced: true, actual: "/tmp/replace/x"}},
+ {"x", info{abs: "/tmp/x", replaced: true, file: true, actual: "/tmp/replace/x"}},
+ {"/tmp/x", info{abs: "/tmp/x", replaced: true, file: true, actual: "/tmp/replace/x"}},
{"x/y", info{abs: "/tmp/x/y", deleted: true}},
{"a", info{abs: "/tmp/a", replaced: true, dir: true, actual: "a"}},
{"a/b", info{abs: "/tmp/a/b", replaced: true, dir: true, actual: "a/b"}},
- {"a/b/c", info{abs: "/tmp/a/b/c", replaced: true, actual: "/tmp/replace/c"}},
+ {"a/b/c", info{abs: "/tmp/a/b/c", replaced: true, file: true, actual: "/tmp/replace/c"}},
{"d/e", info{abs: "/tmp/d/e", deleted: true}},
{"d", info{abs: "/tmp/d", replaced: true, dir: true, actual: "d"}},
}
}
}
+func TestBindOverlay(t *testing.T) {
+ initOverlay(t, `{"Replace": {"mtpt/x.go": "xx.go"}}
+-- mtpt/x.go --
+mtpt/x.go
+-- mtpt/y.go --
+mtpt/y.go
+-- mtpt2/x.go --
+mtpt/x.go
+-- replaced/x.go --
+replaced/x.go
+-- replaced/x/y/z.go --
+replaced/x/y/z.go
+-- xx.go --
+xx.go
+`)
+
+ testReadFile(t, "mtpt/x.go", "xx.go\n")
+
+ Bind("replaced", "mtpt")
+ testReadFile(t, "mtpt/x.go", "replaced/x.go\n")
+ testReadDir(t, "mtpt/x", "y/")
+ testReadDir(t, "mtpt/x/y", "z.go")
+ testReadFile(t, "mtpt/x/y/z.go", "replaced/x/y/z.go\n")
+ testReadFile(t, "mtpt/y.go", "ERROR")
+
+ Bind("replaced", "mtpt2/a/b")
+ testReadDir(t, "mtpt2", "a/", "x.go")
+ testReadDir(t, "mtpt2/a", "b/")
+ testReadDir(t, "mtpt2/a/b", "x/", "x.go")
+ testReadFile(t, "mtpt2/a/b/x.go", "replaced/x.go\n")
+}
+
var badOverlayTests = []struct {
json string
err string
}
}
}
+
+func testReadFile(t *testing.T, name string, want string) {
+ t.Helper()
+ data, err := ReadFile(name)
+ if want == "ERROR" {
+ if data != nil || err == nil {
+ t.Errorf("ReadFile(%q) = %q, %v, want nil, error", name, data, err)
+ }
+ return
+ }
+ if string(data) != want || err != nil {
+ t.Errorf("ReadFile(%q) = %q, %v, want %q, nil", name, data, err, want)
+ }
+}
+
+func testReadDir(t *testing.T, name string, want ...string) {
+ t.Helper()
+ dirs, err := ReadDir(name)
+ var names []string
+ for _, d := range dirs {
+ name := d.Name()
+ if d.IsDir() {
+ name += "/"
+ }
+ names = append(names, name)
+ }
+ if !slices.Equal(names, want) || err != nil {
+ t.Errorf("ReadDir(%q) = %q, %v, want %q, nil", name, names, err, want)
+ }
+}