1
2
3
4
5
6
7
8
9
10
11 package fsys
12
13 import (
14 "cmd/go/internal/str"
15 "encoding/json"
16 "errors"
17 "fmt"
18 "internal/godebug"
19 "io"
20 "io/fs"
21 "iter"
22 "log"
23 "maps"
24 "os"
25 pathpkg "path"
26 "path/filepath"
27 "runtime/debug"
28 "slices"
29 "strings"
30 "sync"
31 "time"
32 )
33
34
35
36
37
38
39 func Trace(op, path string) {
40 if !doTrace {
41 return
42 }
43 traceMu.Lock()
44 defer traceMu.Unlock()
45 fmt.Fprintf(traceFile, "%d gofsystrace %s %s\n", os.Getpid(), op, path)
46 if pattern := gofsystracestack.Value(); pattern != "" {
47 if match, _ := pathpkg.Match(pattern, path); match {
48 traceFile.Write(debug.Stack())
49 }
50 }
51 }
52
53 var (
54 doTrace bool
55 traceFile *os.File
56 traceMu sync.Mutex
57
58 gofsystrace = godebug.New("#gofsystrace")
59 gofsystracelog = godebug.New("#gofsystracelog")
60 gofsystracestack = godebug.New("#gofsystracestack")
61 )
62
63 func init() {
64 if gofsystrace.Value() != "1" {
65 return
66 }
67 doTrace = true
68 if f := gofsystracelog.Value(); f != "" {
69
70 var err error
71 traceFile, err = os.OpenFile(f, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
72 if err != nil {
73 log.Fatal(err)
74 }
75 } else {
76 traceFile = os.Stderr
77 }
78 }
79
80
81
82 var OverlayFile string
83
84
85 type overlayJSON struct {
86
87
88
89 Replace map[string]string
90 }
91
92
93
94
95
96
97
98 var overlay []replace
99
100
101 type replace struct {
102
103
104 from string
105
106
107
108
109
110
111
112
113
114 to string
115 }
116
117 var binds []replace
118
119
120
121
122
123
124
125
126 func Bind(dir, mtpt string) {
127 if dir == "" || mtpt == "" {
128 panic("Bind of empty directory")
129 }
130 binds = append(binds, replace{abs(mtpt), abs(dir)})
131 }
132
133
134 var cwd = sync.OnceValue(cwdOnce)
135
136 func cwdOnce() string {
137 wd, err := os.Getwd()
138 if err != nil {
139
140 log.Fatalf("cannot determine current directory: %v", err)
141 }
142 return wd
143 }
144
145
146
147
148 func abs(path string) string {
149 if path == "" {
150 return ""
151 }
152 if filepath.IsAbs(path) {
153 return filepath.Clean(path)
154 }
155
156 dir := cwd()
157 if vol := filepath.VolumeName(dir); vol != "" && (path[0] == '\\' || path[0] == '/') {
158
159
160
161 return filepath.Join(vol, path)
162 }
163
164 return filepath.Join(dir, path)
165 }
166
167 func searchcmp(r replace, t string) int {
168 return cmp(r.from, t)
169 }
170
171
172
173 type info struct {
174 abs string
175 deleted bool
176 replaced bool
177 dir bool
178 file bool
179 actual string
180 }
181
182
183 func stat(path string) info {
184 apath := abs(path)
185 if path == "" {
186 return info{abs: apath, actual: path}
187 }
188
189
190 replaced := false
191 for _, r := range binds {
192 if str.HasFilePathPrefix(apath, r.from) {
193
194
195 apath = r.to + apath[len(r.from):]
196 path = apath
197 replaced = true
198 break
199 }
200 if str.HasFilePathPrefix(r.from, apath) {
201
202
203 return info{abs: apath, replaced: true, dir: true, actual: path}
204 }
205 }
206
207
208 i, ok := slices.BinarySearchFunc(overlay, apath, searchcmp)
209 if ok {
210
211 r := overlay[i]
212 if r.to == "" {
213
214 return info{abs: apath, deleted: true}
215 }
216 if strings.HasSuffix(r.to, string(filepath.Separator)) {
217
218
219
220 return info{abs: apath, replaced: true, dir: true, actual: path}
221 }
222
223 return info{abs: apath, replaced: true, file: true, actual: r.to}
224 }
225 if i < len(overlay) && str.HasFilePathPrefix(overlay[i].from, apath) {
226
227 return info{abs: apath, replaced: true, dir: true, actual: path}
228 }
229 if i > 0 && str.HasFilePathPrefix(apath, overlay[i-1].from) {
230
231 r := overlay[i-1]
232 if strings.HasSuffix(r.to, string(filepath.Separator)) {
233
234
235
236 p := r.to + apath[len(r.from)+1:]
237 return info{abs: apath, replaced: true, actual: p}
238 }
239
240 return info{abs: apath, deleted: true}
241 }
242 return info{abs: apath, replaced: replaced, actual: path}
243 }
244
245
246
247
248 func (i *info) children() iter.Seq2[string, info] {
249 return func(yield func(string, info) bool) {
250
251
252 var dirs []string
253 for _, m := range binds {
254 if str.HasFilePathPrefix(m.from, i.abs) && m.from != i.abs {
255 name := m.from[len(i.abs)+1:]
256 if i := strings.IndexByte(name, filepath.Separator); i >= 0 {
257 name = name[:i]
258 }
259 dirs = append(dirs, name)
260 }
261 }
262 if len(dirs) > 1 {
263 slices.Sort(dirs)
264 str.Uniq(&dirs)
265 }
266
267
268
269 target := i.abs + string(filepath.Separator) + "\x00"
270 for {
271
272 j, _ := slices.BinarySearchFunc(overlay, target, func(r replace, t string) int {
273 return cmp(r.from, t)
274 })
275
276 Loop:
277
278 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)) {
279 j++
280 }
281 if j >= len(overlay) {
282
283 break
284 }
285 r := overlay[j]
286 if !str.HasFilePathPrefix(r.from, i.abs) {
287
288 break
289 }
290
291
292
293 name := r.from[len(i.abs)+1:]
294 actual := r.to
295 dir := false
296 if j := strings.IndexByte(name, filepath.Separator); j >= 0 {
297
298
299 name = name[:j]
300 dir = true
301 actual = ""
302 }
303 deleted := !dir && r.to == ""
304 ci := info{
305 abs: filepath.Join(i.abs, name),
306 deleted: deleted,
307 replaced: !deleted,
308 dir: dir || strings.HasSuffix(r.to, string(filepath.Separator)),
309 actual: actual,
310 }
311 for ; len(dirs) > 0 && dirs[0] < name; dirs = dirs[1:] {
312 if !yield(dirs[0], info{abs: filepath.Join(i.abs, dirs[0]), replaced: true, dir: true}) {
313 return
314 }
315 }
316 if len(dirs) > 0 && dirs[0] == name {
317 dirs = dirs[1:]
318 }
319 if !yield(name, ci) {
320 return
321 }
322
323
324 target = ci.abs + "\x00"
325
326
327
328 if j+1 < len(overlay) && cmp(overlay[j+1].from, target) >= 0 {
329 j++
330 goto Loop
331 }
332 }
333
334 for _, dir := range dirs {
335 if !yield(dir, info{abs: filepath.Join(i.abs, dir), replaced: true, dir: true}) {
336 return
337 }
338 }
339 }
340 }
341
342
343 func Init() error {
344 if overlay != nil {
345
346 return nil
347 }
348
349 if OverlayFile == "" {
350 return nil
351 }
352
353 Trace("ReadFile", OverlayFile)
354 b, err := os.ReadFile(OverlayFile)
355 if err != nil {
356 return fmt.Errorf("reading overlay: %v", err)
357 }
358 return initFromJSON(b)
359 }
360
361 func initFromJSON(js []byte) error {
362 var ojs overlayJSON
363 if err := json.Unmarshal(js, &ojs); err != nil {
364 return fmt.Errorf("parsing overlay JSON: %v", err)
365 }
366
367 seen := make(map[string]string)
368 var list []replace
369 for _, from := range slices.Sorted(maps.Keys(ojs.Replace)) {
370 if from == "" {
371 return fmt.Errorf("empty string key in overlay map")
372 }
373 afrom := abs(from)
374 if old, ok := seen[afrom]; ok {
375 return fmt.Errorf("duplicate paths %s and %s in overlay map", old, from)
376 }
377 seen[afrom] = from
378 list = append(list, replace{from: afrom, to: abs(ojs.Replace[from])})
379 }
380
381 slices.SortFunc(list, func(x, y replace) int { return cmp(x.from, y.from) })
382
383 for i, r := range list {
384 if r.to == "" {
385 continue
386 }
387
388 prefix := r.from + string(filepath.Separator)
389 for _, next := range list[i+1:] {
390 if !strings.HasPrefix(next.from, prefix) {
391 break
392 }
393 if next.to != "" {
394
395 return fmt.Errorf("inconsistent files %s and %s in overlay map", r.from, next.from)
396 }
397 }
398 }
399
400 overlay = list
401 return nil
402 }
403
404
405
406 func IsDir(path string) (bool, error) {
407 Trace("IsDir", path)
408
409 switch info := stat(path); {
410 case info.dir:
411 return true, nil
412 case info.deleted, info.replaced:
413 return false, nil
414 }
415
416 info, err := os.Stat(path)
417 if err != nil {
418 return false, err
419 }
420 return info.IsDir(), nil
421 }
422
423
424
425
426 var errNotDir = errors.New("not a directory")
427
428
429
430 func osReadDir(name string) ([]fs.DirEntry, error) {
431 dirs, err := os.ReadDir(name)
432 if err != nil && !os.IsNotExist(err) {
433 if info, err := os.Stat(name); err == nil && !info.IsDir() {
434 return nil, &fs.PathError{Op: "ReadDir", Path: name, Err: errNotDir}
435 }
436 }
437 return dirs, err
438 }
439
440
441 func ReadDir(name string) ([]fs.DirEntry, error) {
442 Trace("ReadDir", name)
443
444 info := stat(name)
445 if info.deleted {
446 return nil, &fs.PathError{Op: "read", Path: name, Err: fs.ErrNotExist}
447 }
448 if !info.replaced {
449 return osReadDir(name)
450 }
451 if info.file {
452 return nil, &fs.PathError{Op: "read", Path: name, Err: errNotDir}
453 }
454
455
456 dirs, err := osReadDir(info.actual)
457 if err != nil && !os.IsNotExist(err) && !errors.Is(err, errNotDir) {
458 return nil, err
459 }
460 dirErr := err
461
462
463 all := make(map[string]fs.DirEntry)
464 for _, d := range dirs {
465 all[d.Name()] = d
466 }
467 for cname, cinfo := range info.children() {
468 if cinfo.dir {
469 all[cname] = fs.FileInfoToDirEntry(fakeDir(cname))
470 continue
471 }
472 if cinfo.deleted {
473 delete(all, cname)
474 continue
475 }
476
477
478
479 cinfo, err := os.Stat(cinfo.actual)
480 if err != nil {
481 all[cname] = fs.FileInfoToDirEntry(missingFile(cname))
482 continue
483 }
484 if cinfo.IsDir() {
485 return nil, &fs.PathError{Op: "read", Path: name, Err: fmt.Errorf("overlay maps child %s to directory", cname)}
486 }
487 all[cname] = fs.FileInfoToDirEntry(fakeFile{cname, cinfo})
488 }
489
490
491 dirs = dirs[:0]
492 for _, d := range all {
493 dirs = append(dirs, d)
494 }
495 slices.SortFunc(dirs, func(x, y fs.DirEntry) int { return strings.Compare(x.Name(), y.Name()) })
496
497 if len(dirs) == 0 {
498 return nil, dirErr
499 }
500 return dirs, nil
501 }
502
503
504
505 func Actual(name string) string {
506 info := stat(name)
507 if info.deleted {
508 return ""
509 }
510 if info.dir || info.replaced {
511 return info.actual
512 }
513 return name
514 }
515
516
517
518 func Replaced(name string) bool {
519 info := stat(name)
520 return info.deleted || info.replaced && !info.dir
521 }
522
523
524
525 func Open(name string) (*os.File, error) {
526 Trace("Open", name)
527
528 bad := func(msg string) (*os.File, error) {
529 return nil, &fs.PathError{
530 Op: "Open",
531 Path: name,
532 Err: errors.New(msg),
533 }
534 }
535
536 info := stat(name)
537 if info.deleted {
538 return bad("deleted in overlay")
539 }
540 if info.dir {
541 return bad("cannot open directory in overlay")
542 }
543 if info.replaced {
544 name = info.actual
545 }
546
547 return os.Open(name)
548 }
549
550
551
552 func ReadFile(name string) ([]byte, error) {
553 f, err := Open(name)
554 if err != nil {
555 return nil, err
556 }
557 defer f.Close()
558
559 return io.ReadAll(f)
560 }
561
562
563
564 func IsGoDir(name string) (bool, error) {
565 Trace("IsGoDir", name)
566 fis, err := ReadDir(name)
567 if os.IsNotExist(err) || errors.Is(err, errNotDir) {
568 return false, nil
569 }
570 if err != nil {
571 return false, err
572 }
573
574 var firstErr error
575 for _, d := range fis {
576 if d.IsDir() || !strings.HasSuffix(d.Name(), ".go") {
577 continue
578 }
579 if d.Type().IsRegular() {
580 return true, nil
581 }
582
583
584
585 if actual := Actual(filepath.Join(name, d.Name())); actual != "" {
586 fi, err := os.Stat(actual)
587 if err == nil && fi.Mode().IsRegular() {
588 return true, nil
589 }
590 if err != nil && firstErr == nil {
591 firstErr = err
592 }
593 }
594 }
595
596
597 return false, firstErr
598 }
599
600
601
602 func Lstat(name string) (fs.FileInfo, error) {
603 Trace("Lstat", name)
604 return overlayStat("lstat", name, os.Lstat)
605 }
606
607
608
609 func Stat(name string) (fs.FileInfo, error) {
610 Trace("Stat", name)
611 return overlayStat("stat", name, os.Stat)
612 }
613
614
615 func overlayStat(op, path string, osStat func(string) (fs.FileInfo, error)) (fs.FileInfo, error) {
616 info := stat(path)
617 if info.deleted {
618 return nil, &fs.PathError{Op: op, Path: path, Err: fs.ErrNotExist}
619 }
620 if info.dir {
621 return fakeDir(filepath.Base(path)), nil
622 }
623 if info.replaced {
624
625
626
627
628
629 ainfo, err := os.Stat(info.actual)
630 if err != nil {
631 return nil, err
632 }
633 if ainfo.IsDir() {
634 return nil, &fs.PathError{Op: op, Path: path, Err: fmt.Errorf("overlay maps to directory")}
635 }
636 return fakeFile{name: filepath.Base(path), real: ainfo}, nil
637 }
638 return osStat(path)
639 }
640
641
642
643
644 type fakeFile struct {
645 name string
646 real fs.FileInfo
647 }
648
649 func (f fakeFile) Name() string { return f.name }
650 func (f fakeFile) Size() int64 { return f.real.Size() }
651 func (f fakeFile) Mode() fs.FileMode { return f.real.Mode() }
652 func (f fakeFile) ModTime() time.Time { return f.real.ModTime() }
653 func (f fakeFile) IsDir() bool { return f.real.IsDir() }
654 func (f fakeFile) Sys() any { return f.real.Sys() }
655
656 func (f fakeFile) String() string {
657 return fs.FormatFileInfo(f)
658 }
659
660
661
662
663
664 type missingFile string
665
666 func (f missingFile) Name() string { return string(f) }
667 func (f missingFile) Size() int64 { return 0 }
668 func (f missingFile) Mode() fs.FileMode { return fs.ModeIrregular }
669 func (f missingFile) ModTime() time.Time { return time.Unix(0, 0) }
670 func (f missingFile) IsDir() bool { return false }
671 func (f missingFile) Sys() any { return nil }
672
673 func (f missingFile) String() string {
674 return fs.FormatFileInfo(f)
675 }
676
677
678
679
680 type fakeDir string
681
682 func (f fakeDir) Name() string { return string(f) }
683 func (f fakeDir) Size() int64 { return 0 }
684 func (f fakeDir) Mode() fs.FileMode { return fs.ModeDir | 0500 }
685 func (f fakeDir) ModTime() time.Time { return time.Unix(0, 0) }
686 func (f fakeDir) IsDir() bool { return true }
687 func (f fakeDir) Sys() any { return nil }
688
689 func (f fakeDir) String() string {
690 return fs.FormatFileInfo(f)
691 }
692
693 func cmp(x, y string) int {
694 for i := 0; i < len(x) && i < len(y); i++ {
695 xi := int(x[i])
696 yi := int(y[i])
697 if xi == filepath.Separator {
698 xi = -1
699 }
700 if yi == filepath.Separator {
701 yi = -1
702 }
703 if xi != yi {
704 return xi - yi
705 }
706 }
707 return len(x) - len(y)
708 }
709
View as plain text