Skip to content

Commit 44cea12

Browse files
committed
cmd/go/internal/fsys: replace file tree with sorted list
Replace the tree of nodes with a sorted list of file replacements. The most important property of this representation is that it allows replacing directories: a replacement x -> y where y is a directory could not be implemented before, because it would require making a node for every file in the tree rooted at y, or else it would require unsuccessful lookups for files like x/a/b/c/d/e/f/g/h/i/j/k to try every possible parent in order to discover the x -> y mapping. The sorted list makes it easy to find the x -> y mapping: when you do the binary search for x/a/b/c/d/e/f/g/h/i/j/k, you end up immediately after the x -> y mapping, so stepping backward one entry provides the mapping we need, if it exists. This CL does not allow overlay files to include directories, but now it is possible. This is at least useful for other kinds of experiments (like FIPS). Change-Id: Ief0afaee82e644dab8ae4eafeec20440afee2e36 Reviewed-on: https://go-review.googlesource.com/c/go/+/628701 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Michael Matloob <[email protected]>
1 parent 2ef00e2 commit 44cea12

File tree

2 files changed

+213
-85
lines changed

2 files changed

+213
-85
lines changed

src/cmd/go/internal/fsys/fsys.go

+116-85
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package fsys
1212

1313
import (
14+
"cmd/go/internal/str"
1415
"encoding/json"
1516
"errors"
1617
"fmt"
@@ -88,27 +89,31 @@ type overlayJSON struct {
8889
Replace map[string]string
8990
}
9091

92+
// overlay is a list of replacements to be applied, sorted by cmp of the from field.
93+
// cmp sorts the filepath.Separator less than any other byte so that x is always
94+
// just before any children x/a, x/b, and so on, before x.go. (This would not
95+
// be the case with byte-wise sorting, which would produce x, x.go, x/a.)
96+
// The sorting lets us find the relevant overlay entry quickly even if it is for a
97+
// parent of the path being searched.
98+
var overlay []replace
99+
100+
// A replace represents a single replaced path.
91101
type replace struct {
102+
// from is the old path being replaced.
103+
// It is an absolute path returned by abs.
92104
from string
93-
to string
94-
}
95-
96-
type node struct {
97-
actual string // empty if a directory
98-
children map[string]*node // path element → file or directory
99-
}
100-
101-
func (n *node) isDir() bool {
102-
return n.actual == "" && n.children != nil
103-
}
104105

105-
func (n *node) isDeleted() bool {
106-
return n.actual == "" && n.children == nil
106+
// to is the replacement for the old path.
107+
// It is an absolute path returned by abs.
108+
// If it is the empty string, the old path appears deleted.
109+
// Otherwise the old path appears to be the file named by to.
110+
// If to ends in a trailing slash, the overlay code below treats
111+
// it as a directory replacement, akin to a bind mount.
112+
// However, our processing of external overlay maps removes
113+
// such paths by calling abs, except for / or C:\.
114+
to string
107115
}
108116

109-
// TODO(matloob): encapsulate these in an io/fs-like interface
110-
var overlay map[string]*node // path -> file or directory node
111-
112117
// cwd returns the current directory, caching it on first use.
113118
var cwd = sync.OnceValue(cwdOnce)
114119

@@ -143,6 +148,10 @@ func abs(path string) string {
143148
return filepath.Join(dir, path)
144149
}
145150

151+
func searchcmp(r replace, t string) int {
152+
return cmp(r.from, t)
153+
}
154+
146155
// info is a summary of the known information about a path
147156
// being looked up in the virtual file system.
148157
type info struct {
@@ -156,56 +165,110 @@ type info struct {
156165
// stat returns info about the path in the virtual file system.
157166
func stat(path string) info {
158167
apath := abs(path)
159-
if n, ok := overlay[apath]; ok {
160-
if n.isDir() {
161-
return info{abs: apath, replaced: true, dir: true, actual: path}
162-
}
163-
if n.isDeleted() {
164-
return info{abs: apath, deleted: true}
165-
}
166-
return info{abs: apath, replaced: true, actual: n.actual}
168+
if path == "" {
169+
return info{abs: apath, actual: path}
167170
}
168171

169-
// Check whether any parents are replaced by files,
170-
// meaning this path and the directory that contained it
171-
// have been deleted.
172-
prefix := apath
173-
for {
174-
if n, ok := overlay[prefix]; ok {
175-
if n.children == nil {
176-
return info{abs: apath, deleted: true}
177-
}
178-
break
172+
// Binary search for apath to find the nearest relevant entry in the overlay.
173+
i, ok := slices.BinarySearchFunc(overlay, apath, searchcmp)
174+
if ok {
175+
// Exact match; overlay[i].from == apath.
176+
r := overlay[i]
177+
if r.to == "" {
178+
// Deleted.
179+
return info{abs: apath, deleted: true}
180+
}
181+
if strings.HasSuffix(r.to, string(filepath.Separator)) {
182+
// Replacement ends in slash, denoting directory.
183+
// Note that this is impossible in current overlays since we call abs
184+
// and it strips the trailing slashes. But we could support it in the future.
185+
return info{abs: apath, replaced: true, dir: true, actual: path}
179186
}
180-
parent := filepath.Dir(prefix)
181-
if parent == prefix {
182-
break
187+
// Replaced file.
188+
return info{abs: apath, replaced: true, actual: r.to}
189+
}
190+
if i < len(overlay) && str.HasFilePathPrefix(overlay[i].from, apath) {
191+
// Replacement for child path; infer existence of parent directory.
192+
return info{abs: apath, replaced: true, dir: true, actual: path}
193+
}
194+
if i > 0 && str.HasFilePathPrefix(apath, overlay[i-1].from) {
195+
// Replacement for parent.
196+
r := overlay[i-1]
197+
if strings.HasSuffix(r.to, string(filepath.Separator)) {
198+
// Parent replaced by directory; apply replacement in our path.
199+
// Note that this is impossible in current overlays since we call abs
200+
// and it strips the trailing slashes. But we could support it in the future.
201+
p := r.to + apath[len(r.from)+1:]
202+
return info{abs: apath, replaced: true, actual: p}
183203
}
184-
prefix = parent
204+
// Parent replaced by file; path is deleted.
205+
return info{abs: apath, deleted: true}
185206
}
186-
187207
return info{abs: apath, actual: path}
188208
}
189209

190210
// children returns a sequence of (name, info)
191-
// for all the children of the directory i.
211+
// for all the children of the directory i
212+
// implied by the overlay.
192213
func (i *info) children() iter.Seq2[string, info] {
193214
return func(yield func(string, info) bool) {
194-
n := overlay[i.abs]
195-
if n == nil {
196-
return
197-
}
198-
for name, c := range n.children {
215+
// Loop looking for next possible child in sorted overlay,
216+
// which is previous child plus "\x00".
217+
target := i.abs + string(filepath.Separator) + "\x00"
218+
for {
219+
// Search for next child: first entry in overlay >= target.
220+
j, _ := slices.BinarySearchFunc(overlay, target, func(r replace, t string) int {
221+
return cmp(r.from, t)
222+
})
223+
224+
Loop:
225+
// Skip subdirectories with deleted children (but not direct deleted children).
226+
for j < len(overlay) && overlay[j].to == "" && str.HasFilePathPrefix(overlay[j].from, i.abs) && strings.Contains(overlay[j].from[len(i.abs)+1:], string(filepath.Separator)) {
227+
j++
228+
}
229+
if j >= len(overlay) {
230+
// Nothing found at all.
231+
return
232+
}
233+
r := overlay[j]
234+
if !str.HasFilePathPrefix(r.from, i.abs) {
235+
// Next entry in overlay is beyond the directory we want; all done.
236+
return
237+
}
238+
239+
// Found the next child in the directory.
240+
// Yield it and its info.
241+
name := r.from[len(i.abs)+1:]
242+
actual := r.to
243+
dir := false
244+
if j := strings.IndexByte(name, filepath.Separator); j >= 0 {
245+
// Child is multiple levels down, so name must be a directory,
246+
// and there is no actual replacement.
247+
name = name[:j]
248+
dir = true
249+
actual = ""
250+
}
251+
deleted := !dir && r.to == ""
199252
ci := info{
200253
abs: filepath.Join(i.abs, name),
201-
deleted: c.isDeleted(),
202-
replaced: c.children != nil || c.actual != "",
203-
dir: c.isDir(),
204-
actual: c.actual,
254+
deleted: deleted,
255+
replaced: !deleted,
256+
dir: dir || strings.HasSuffix(r.to, string(filepath.Separator)),
257+
actual: actual,
205258
}
206259
if !yield(name, ci) {
207260
return
208261
}
262+
263+
// Next target is first name after the one we just returned.
264+
target = ci.abs + "\x00"
265+
266+
// Optimization: Check whether the very next element
267+
// is the next child. If so, skip the binary search.
268+
if j+1 < len(overlay) && cmp(overlay[j+1].from, target) >= 0 {
269+
j++
270+
goto Loop
271+
}
209272
}
210273
}
211274
}
@@ -246,7 +309,7 @@ func initFromJSON(js []byte) error {
246309
return fmt.Errorf("duplicate paths %s and %s in overlay map", old, from)
247310
}
248311
seen[afrom] = from
249-
list = append(list, replace{from: afrom, to: ojs.Replace[from]})
312+
list = append(list, replace{from: afrom, to: abs(ojs.Replace[from])})
250313
}
251314

252315
slices.SortFunc(list, func(x, y replace) int { return cmp(x.from, y.from) })
@@ -268,39 +331,7 @@ func initFromJSON(js []byte) error {
268331
}
269332
}
270333

271-
overlay = make(map[string]*node)
272-
for _, r := range list {
273-
n := &node{actual: abs(r.to)}
274-
from := r.from
275-
overlay[from] = n
276-
277-
for {
278-
dir, base := filepath.Dir(from), filepath.Base(from)
279-
if dir == from {
280-
break
281-
}
282-
dn := overlay[dir]
283-
if dn == nil || dn.isDeleted() {
284-
dn = &node{children: make(map[string]*node)}
285-
overlay[dir] = dn
286-
}
287-
if n.isDeleted() && !dn.isDir() {
288-
break
289-
}
290-
if !dn.isDir() {
291-
panic("fsys inconsistency")
292-
}
293-
dn.children[base] = n
294-
if n.isDeleted() {
295-
// Deletion is recorded now.
296-
// Don't need to create entire parent chain,
297-
// because we don't need to force parents to exist.
298-
break
299-
}
300-
from, n = dir, dn
301-
}
302-
}
303-
334+
overlay = list
304335
return nil
305336
}
306337

@@ -414,8 +445,8 @@ func Actual(name string) string {
414445
// Replaced reports whether the named file has been modified
415446
// in the virtual file system compared to the OS file system.
416447
func Replaced(name string) bool {
417-
p, ok := overlay[abs(name)]
418-
return ok && !p.isDir()
448+
info := stat(name)
449+
return info.deleted || info.replaced && !info.dir
419450
}
420451

421452
// Open opens the named file in the virtual file system.

src/cmd/go/internal/fsys/fsys_test.go

+97
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"path/filepath"
1515
"reflect"
1616
"runtime"
17+
"slices"
1718
"strings"
1819
"sync"
1920
"testing"
@@ -51,6 +52,102 @@ func initOverlay(t *testing.T, config string) {
5152
}
5253
}
5354

55+
var statInfoOverlay = `{"Replace": {
56+
"x": "replace/x",
57+
"a/b/c": "replace/c",
58+
"d/e": ""
59+
}}`
60+
61+
var statInfoTests = []struct {
62+
path string
63+
info info
64+
}{
65+
{"foo", info{abs: "/tmp/foo", actual: "foo"}},
66+
{"foo/bar/baz/quux", info{abs: "/tmp/foo/bar/baz/quux", actual: "foo/bar/baz/quux"}},
67+
{"x", info{abs: "/tmp/x", replaced: true, actual: "/tmp/replace/x"}},
68+
{"/tmp/x", info{abs: "/tmp/x", replaced: true, actual: "/tmp/replace/x"}},
69+
{"x/y", info{abs: "/tmp/x/y", deleted: true}},
70+
{"a", info{abs: "/tmp/a", replaced: true, dir: true, actual: "a"}},
71+
{"a/b", info{abs: "/tmp/a/b", replaced: true, dir: true, actual: "a/b"}},
72+
{"a/b/c", info{abs: "/tmp/a/b/c", replaced: true, actual: "/tmp/replace/c"}},
73+
{"d/e", info{abs: "/tmp/d/e", deleted: true}},
74+
{"d", info{abs: "/tmp/d", replaced: true, dir: true, actual: "d"}},
75+
}
76+
77+
var statInfoChildrenTests = []struct {
78+
path string
79+
children []info
80+
}{
81+
{"foo", nil},
82+
{"foo/bar", nil},
83+
{"foo/bar/baz", nil},
84+
{"x", nil},
85+
{"x/y", nil},
86+
{"a", []info{{abs: "/tmp/a/b", replaced: true, dir: true, actual: ""}}},
87+
{"a/b", []info{{abs: "/tmp/a/b/c", replaced: true, actual: "/tmp/replace/c"}}},
88+
{"d", []info{{abs: "/tmp/d/e", deleted: true}}},
89+
{"d/e", nil},
90+
{".", []info{
91+
{abs: "/tmp/a", replaced: true, dir: true, actual: ""},
92+
// {abs: "/tmp/d", replaced: true, dir: true, actual: ""},
93+
{abs: "/tmp/x", replaced: true, actual: "/tmp/replace/x"},
94+
}},
95+
}
96+
97+
func TestStatInfo(t *testing.T) {
98+
tmp := "/tmp"
99+
if runtime.GOOS == "windows" {
100+
tmp = `C:\tmp`
101+
}
102+
cwd = sync.OnceValue(func() string { return tmp })
103+
104+
winFix := func(s string) string {
105+
if runtime.GOOS == "windows" {
106+
s = strings.ReplaceAll(s, `/tmp`, tmp) // fix tmp
107+
s = strings.ReplaceAll(s, `/`, `\`) // use backslashes
108+
}
109+
return s
110+
}
111+
112+
overlay := statInfoOverlay
113+
overlay = winFix(overlay)
114+
overlay = strings.ReplaceAll(overlay, `\`, `\\`) // JSON escaping
115+
if err := initFromJSON([]byte(overlay)); err != nil {
116+
t.Fatal(err)
117+
}
118+
119+
for _, tt := range statInfoTests {
120+
tt.path = winFix(tt.path)
121+
tt.info.abs = winFix(tt.info.abs)
122+
tt.info.actual = winFix(tt.info.actual)
123+
info := stat(tt.path)
124+
if info != tt.info {
125+
t.Errorf("stat(%#q):\nhave %+v\nwant %+v", tt.path, info, tt.info)
126+
}
127+
}
128+
129+
for _, tt := range statInfoChildrenTests {
130+
tt.path = winFix(tt.path)
131+
for i, info := range tt.children {
132+
info.abs = winFix(info.abs)
133+
info.actual = winFix(info.actual)
134+
tt.children[i] = info
135+
}
136+
parent := stat(winFix(tt.path))
137+
var children []info
138+
for name, child := range parent.children() {
139+
if name != filepath.Base(child.abs) {
140+
t.Errorf("stat(%#q): child %#q has inconsistent abs %#q", tt.path, name, child.abs)
141+
}
142+
children = append(children, child)
143+
}
144+
slices.SortFunc(children, func(x, y info) int { return cmp(x.abs, y.abs) })
145+
if !slices.Equal(children, tt.children) {
146+
t.Errorf("stat(%#q) children:\nhave %+v\nwant %+v", tt.path, children, tt.children)
147+
}
148+
}
149+
}
150+
54151
func TestIsDir(t *testing.T) {
55152
initOverlay(t, `
56153
{

0 commit comments

Comments
 (0)