11
11
package fsys
12
12
13
13
import (
14
+ "cmd/go/internal/str"
14
15
"encoding/json"
15
16
"errors"
16
17
"fmt"
@@ -88,27 +89,31 @@ type overlayJSON struct {
88
89
Replace map [string ]string
89
90
}
90
91
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.
91
101
type replace struct {
102
+ // from is the old path being replaced.
103
+ // It is an absolute path returned by abs.
92
104
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
- }
104
105
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
107
115
}
108
116
109
- // TODO(matloob): encapsulate these in an io/fs-like interface
110
- var overlay map [string ]* node // path -> file or directory node
111
-
112
117
// cwd returns the current directory, caching it on first use.
113
118
var cwd = sync .OnceValue (cwdOnce )
114
119
@@ -143,6 +148,10 @@ func abs(path string) string {
143
148
return filepath .Join (dir , path )
144
149
}
145
150
151
+ func searchcmp (r replace , t string ) int {
152
+ return cmp (r .from , t )
153
+ }
154
+
146
155
// info is a summary of the known information about a path
147
156
// being looked up in the virtual file system.
148
157
type info struct {
@@ -156,56 +165,110 @@ type info struct {
156
165
// stat returns info about the path in the virtual file system.
157
166
func stat (path string ) info {
158
167
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 }
167
170
}
168
171
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 }
179
186
}
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 }
183
203
}
184
- prefix = parent
204
+ // Parent replaced by file; path is deleted.
205
+ return info {abs : apath , deleted : true }
185
206
}
186
-
187
207
return info {abs : apath , actual : path }
188
208
}
189
209
190
210
// 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.
192
213
func (i * info ) children () iter.Seq2 [string , info ] {
193
214
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 == ""
199
252
ci := info {
200
253
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 ,
205
258
}
206
259
if ! yield (name , ci ) {
207
260
return
208
261
}
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
+ }
209
272
}
210
273
}
211
274
}
@@ -246,7 +309,7 @@ func initFromJSON(js []byte) error {
246
309
return fmt .Errorf ("duplicate paths %s and %s in overlay map" , old , from )
247
310
}
248
311
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 ]) })
250
313
}
251
314
252
315
slices .SortFunc (list , func (x , y replace ) int { return cmp (x .from , y .from ) })
@@ -268,39 +331,7 @@ func initFromJSON(js []byte) error {
268
331
}
269
332
}
270
333
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
304
335
return nil
305
336
}
306
337
@@ -414,8 +445,8 @@ func Actual(name string) string {
414
445
// Replaced reports whether the named file has been modified
415
446
// in the virtual file system compared to the OS file system.
416
447
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
419
450
}
420
451
421
452
// Open opens the named file in the virtual file system.
0 commit comments