1
2
3
4
5
6
7 package fsys
8
9 import (
10 "encoding/json"
11 "errors"
12 "fmt"
13 "internal/godebug"
14 "io/fs"
15 "log"
16 "os"
17 pathpkg "path"
18 "path/filepath"
19 "runtime"
20 "runtime/debug"
21 "sort"
22 "strings"
23 "sync"
24 "time"
25 )
26
27
28
29
30
31
32 func Trace(op, path string) {
33 if !doTrace {
34 return
35 }
36 traceMu.Lock()
37 defer traceMu.Unlock()
38 fmt.Fprintf(traceFile, "%d gofsystrace %s %s\n", os.Getpid(), op, path)
39 if pattern := gofsystracestack.Value(); pattern != "" {
40 if match, _ := pathpkg.Match(pattern, path); match {
41 traceFile.Write(debug.Stack())
42 }
43 }
44 }
45
46 var (
47 doTrace bool
48 traceFile *os.File
49 traceMu sync.Mutex
50
51 gofsystrace = godebug.New("#gofsystrace")
52 gofsystracelog = godebug.New("#gofsystracelog")
53 gofsystracestack = godebug.New("#gofsystracestack")
54 )
55
56 func init() {
57 if gofsystrace.Value() != "1" {
58 return
59 }
60 doTrace = true
61 if f := gofsystracelog.Value(); f != "" {
62
63 var err error
64 traceFile, err = os.OpenFile(f, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
65 if err != nil {
66 log.Fatal(err)
67 }
68 } else {
69 traceFile = os.Stderr
70 }
71 }
72
73
74
75 var OverlayFile string
76
77
78
79
80
81
82 type OverlayJSON struct {
83 Replace map[string]string
84 }
85
86 type node struct {
87 actualFilePath string
88 children map[string]*node
89 }
90
91 func (n *node) isDir() bool {
92 return n.actualFilePath == "" && n.children != nil
93 }
94
95 func (n *node) isDeleted() bool {
96 return n.actualFilePath == "" && n.children == nil
97 }
98
99
100 var overlay map[string]*node
101 var cwd string
102
103
104
105
106
107
108
109 func canonicalize(path string) string {
110 if path == "" {
111 return ""
112 }
113 if filepath.IsAbs(path) {
114 return filepath.Clean(path)
115 }
116
117 if v := filepath.VolumeName(cwd); v != "" && path[0] == filepath.Separator {
118
119
120
121
122
123 return filepath.Join(v, path)
124 }
125
126
127 return filepath.Join(cwd, path)
128 }
129
130
131 func Init(wd string) error {
132 if overlay != nil {
133
134 return nil
135 }
136
137 cwd = wd
138
139 if OverlayFile == "" {
140 return nil
141 }
142
143 Trace("ReadFile", OverlayFile)
144 b, err := os.ReadFile(OverlayFile)
145 if err != nil {
146 return fmt.Errorf("reading overlay file: %v", err)
147 }
148
149 var overlayJSON OverlayJSON
150 if err := json.Unmarshal(b, &overlayJSON); err != nil {
151 return fmt.Errorf("parsing overlay JSON: %v", err)
152 }
153
154 return initFromJSON(overlayJSON)
155 }
156
157 func initFromJSON(overlayJSON OverlayJSON) error {
158
159
160
161 overlay = make(map[string]*node)
162 reverseCanonicalized := make(map[string]string)
163
164
165
166 replaceFrom := make([]string, 0, len(overlayJSON.Replace))
167 for k := range overlayJSON.Replace {
168 replaceFrom = append(replaceFrom, k)
169 }
170 sort.Strings(replaceFrom)
171
172 for _, from := range replaceFrom {
173 to := overlayJSON.Replace[from]
174
175 if from == "" {
176 return fmt.Errorf("empty string key in overlay file Replace map")
177 }
178 cfrom := canonicalize(from)
179 if to != "" {
180
181 to = canonicalize(to)
182 }
183 if otherFrom, seen := reverseCanonicalized[cfrom]; seen {
184 return fmt.Errorf(
185 "paths %q and %q both canonicalize to %q in overlay file Replace map", otherFrom, from, cfrom)
186 }
187 reverseCanonicalized[cfrom] = from
188 from = cfrom
189
190
191 dir, base := filepath.Dir(from), filepath.Base(from)
192 if n, ok := overlay[from]; ok {
193
194
195
196
197
198
199
200
201
202 for _, f := range n.children {
203 if !f.isDeleted() {
204 return fmt.Errorf("invalid overlay: path %v is used as both file and directory", from)
205 }
206 }
207 }
208 overlay[from] = &node{actualFilePath: to}
209
210
211 childNode := overlay[from]
212 for {
213 dirNode := overlay[dir]
214 if dirNode == nil || dirNode.isDeleted() {
215 dirNode = &node{children: make(map[string]*node)}
216 overlay[dir] = dirNode
217 }
218 if childNode.isDeleted() {
219
220
221
222
223 if dirNode.isDir() {
224 dirNode.children[base] = childNode
225 }
226 break
227 }
228 if !dirNode.isDir() {
229
230
231 return fmt.Errorf("invalid overlay: path %v is used as both file and directory", dir)
232 }
233 dirNode.children[base] = childNode
234 parent := filepath.Dir(dir)
235 if parent == dir {
236 break
237 }
238 dir, base = parent, filepath.Base(dir)
239 childNode = dirNode
240 }
241 }
242
243 return nil
244 }
245
246
247
248 func IsDir(path string) (bool, error) {
249 Trace("IsDir", path)
250 path = canonicalize(path)
251
252 if _, ok := parentIsOverlayFile(path); ok {
253 return false, nil
254 }
255
256 if n, ok := overlay[path]; ok {
257 return n.isDir(), nil
258 }
259
260 fi, err := os.Stat(path)
261 if err != nil {
262 return false, err
263 }
264
265 return fi.IsDir(), nil
266 }
267
268
269
270
271 func parentIsOverlayFile(name string) (string, bool) {
272 if overlay != nil {
273
274
275
276 prefix := name
277 for {
278 node := overlay[prefix]
279 if node != nil && !node.isDir() {
280 return prefix, true
281 }
282 parent := filepath.Dir(prefix)
283 if parent == prefix {
284 break
285 }
286 prefix = parent
287 }
288 }
289
290 return "", false
291 }
292
293
294
295
296 var errNotDir = errors.New("not a directory")
297
298 func nonFileInOverlayError(overlayPath string) error {
299 return fmt.Errorf("replacement path %q is a directory, not a file", overlayPath)
300 }
301
302
303
304
305 func readDir(dir string) ([]fs.FileInfo, error) {
306 entries, err := os.ReadDir(dir)
307 if err != nil {
308 if os.IsNotExist(err) {
309 return nil, err
310 }
311 if dirfi, staterr := os.Stat(dir); staterr == nil && !dirfi.IsDir() {
312 return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: errNotDir}
313 }
314 return nil, err
315 }
316
317 fis := make([]fs.FileInfo, 0, len(entries))
318 for _, entry := range entries {
319 info, err := entry.Info()
320 if err != nil {
321 continue
322 }
323 fis = append(fis, info)
324 }
325 return fis, nil
326 }
327
328
329
330 func ReadDir(dir string) ([]fs.FileInfo, error) {
331 Trace("ReadDir", dir)
332 dir = canonicalize(dir)
333 if _, ok := parentIsOverlayFile(dir); ok {
334 return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: errNotDir}
335 }
336
337 dirNode := overlay[dir]
338 if dirNode == nil {
339 return readDir(dir)
340 }
341 if dirNode.isDeleted() {
342 return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: fs.ErrNotExist}
343 }
344 diskfis, err := readDir(dir)
345 if err != nil && !os.IsNotExist(err) && !errors.Is(err, errNotDir) {
346 return nil, err
347 }
348
349
350 files := make(map[string]fs.FileInfo)
351 for _, f := range diskfis {
352 files[f.Name()] = f
353 }
354 for name, to := range dirNode.children {
355 switch {
356 case to.isDir():
357 files[name] = fakeDir(name)
358 case to.isDeleted():
359 delete(files, name)
360 default:
361
362
363
364
365
366 fi, err := os.Stat(to.actualFilePath)
367 if err != nil {
368 files[name] = missingFile(name)
369 continue
370 } else if fi.IsDir() {
371 return nil, &fs.PathError{Op: "Stat", Path: filepath.Join(dir, name), Err: nonFileInOverlayError(to.actualFilePath)}
372 }
373
374
375 files[name] = fakeFile{name, fi}
376 }
377 }
378 sortedFiles := diskfis[:0]
379 for _, f := range files {
380 sortedFiles = append(sortedFiles, f)
381 }
382 sort.Slice(sortedFiles, func(i, j int) bool { return sortedFiles[i].Name() < sortedFiles[j].Name() })
383 return sortedFiles, nil
384 }
385
386
387
388
389
390
391
392 func OverlayPath(path string) (string, bool) {
393 if p, ok := overlay[canonicalize(path)]; ok && !p.isDir() {
394 return p.actualFilePath, ok
395 }
396
397 return path, false
398 }
399
400
401 func Open(path string) (*os.File, error) {
402 Trace("Open", path)
403 return openFile(path, os.O_RDONLY, 0)
404 }
405
406
407 func OpenFile(path string, flag int, perm os.FileMode) (*os.File, error) {
408 Trace("OpenFile", path)
409 return openFile(path, flag, perm)
410 }
411
412 func openFile(path string, flag int, perm os.FileMode) (*os.File, error) {
413 cpath := canonicalize(path)
414 if node, ok := overlay[cpath]; ok {
415
416 if node.isDir() {
417 return nil, &fs.PathError{Op: "OpenFile", Path: path, Err: errors.New("fsys.OpenFile doesn't support opening directories yet")}
418 }
419
420 if perm != os.FileMode(os.O_RDONLY) {
421 return nil, &fs.PathError{Op: "OpenFile", Path: path, Err: errors.New("overlaid files can't be opened for write")}
422 }
423 return os.OpenFile(node.actualFilePath, flag, perm)
424 }
425 if parent, ok := parentIsOverlayFile(filepath.Dir(cpath)); ok {
426
427
428
429 return nil, &fs.PathError{
430 Op: "Open",
431 Path: path,
432 Err: fmt.Errorf("file %s does not exist: parent directory %s is replaced by a file in overlay", path, parent),
433 }
434 }
435 return os.OpenFile(cpath, flag, perm)
436 }
437
438
439
440 func IsDirWithGoFiles(dir string) (bool, error) {
441 Trace("IsDirWithGoFiles", dir)
442 fis, err := ReadDir(dir)
443 if os.IsNotExist(err) || errors.Is(err, errNotDir) {
444 return false, nil
445 }
446 if err != nil {
447 return false, err
448 }
449
450 var firstErr error
451 for _, fi := range fis {
452 if fi.IsDir() {
453 continue
454 }
455
456
457
458
459
460 if !strings.HasSuffix(fi.Name(), ".go") {
461 continue
462 }
463 if fi.Mode().IsRegular() {
464 return true, nil
465 }
466
467
468
469
470 actualFilePath, _ := OverlayPath(filepath.Join(dir, fi.Name()))
471 fi, err := os.Stat(actualFilePath)
472 if err == nil && fi.Mode().IsRegular() {
473 return true, nil
474 }
475 if err != nil && firstErr == nil {
476 firstErr = err
477 }
478 }
479
480
481 return false, firstErr
482 }
483
484
485
486 func walk(path string, info fs.FileInfo, walkFn filepath.WalkFunc) error {
487 if err := walkFn(path, info, nil); err != nil || !info.IsDir() {
488 return err
489 }
490
491 fis, err := ReadDir(path)
492 if err != nil {
493 return walkFn(path, info, err)
494 }
495
496 for _, fi := range fis {
497 filename := filepath.Join(path, fi.Name())
498 if err := walk(filename, fi, walkFn); err != nil {
499 if !fi.IsDir() || err != filepath.SkipDir {
500 return err
501 }
502 }
503 }
504 return nil
505 }
506
507
508
509 func Walk(root string, walkFn filepath.WalkFunc) error {
510 Trace("Walk", root)
511 info, err := Lstat(root)
512 if err != nil {
513 err = walkFn(root, nil, err)
514 } else {
515 err = walk(root, info, walkFn)
516 }
517 if err == filepath.SkipDir {
518 return nil
519 }
520 return err
521 }
522
523
524 func Lstat(path string) (fs.FileInfo, error) {
525 Trace("Lstat", path)
526 return overlayStat(path, os.Lstat, "lstat")
527 }
528
529
530 func Stat(path string) (fs.FileInfo, error) {
531 Trace("Stat", path)
532 return overlayStat(path, os.Stat, "stat")
533 }
534
535
536 func overlayStat(path string, osStat func(string) (fs.FileInfo, error), opName string) (fs.FileInfo, error) {
537 cpath := canonicalize(path)
538
539 if _, ok := parentIsOverlayFile(filepath.Dir(cpath)); ok {
540 return nil, &fs.PathError{Op: opName, Path: cpath, Err: fs.ErrNotExist}
541 }
542
543 node, ok := overlay[cpath]
544 if !ok {
545
546 return osStat(path)
547 }
548
549 switch {
550 case node.isDeleted():
551 return nil, &fs.PathError{Op: opName, Path: cpath, Err: fs.ErrNotExist}
552 case node.isDir():
553 return fakeDir(filepath.Base(path)), nil
554 default:
555
556
557
558
559
560 fi, err := os.Stat(node.actualFilePath)
561 if err != nil {
562 return nil, err
563 }
564 if fi.IsDir() {
565 return nil, &fs.PathError{Op: opName, Path: cpath, Err: nonFileInOverlayError(node.actualFilePath)}
566 }
567 return fakeFile{name: filepath.Base(path), real: fi}, nil
568 }
569 }
570
571
572
573
574 type fakeFile struct {
575 name string
576 real fs.FileInfo
577 }
578
579 func (f fakeFile) Name() string { return f.name }
580 func (f fakeFile) Size() int64 { return f.real.Size() }
581 func (f fakeFile) Mode() fs.FileMode { return f.real.Mode() }
582 func (f fakeFile) ModTime() time.Time { return f.real.ModTime() }
583 func (f fakeFile) IsDir() bool { return f.real.IsDir() }
584 func (f fakeFile) Sys() any { return f.real.Sys() }
585
586 func (f fakeFile) String() string {
587 return fs.FormatFileInfo(f)
588 }
589
590
591
592
593
594 type missingFile string
595
596 func (f missingFile) Name() string { return string(f) }
597 func (f missingFile) Size() int64 { return 0 }
598 func (f missingFile) Mode() fs.FileMode { return fs.ModeIrregular }
599 func (f missingFile) ModTime() time.Time { return time.Unix(0, 0) }
600 func (f missingFile) IsDir() bool { return false }
601 func (f missingFile) Sys() any { return nil }
602
603 func (f missingFile) String() string {
604 return fs.FormatFileInfo(f)
605 }
606
607
608
609
610 type fakeDir string
611
612 func (f fakeDir) Name() string { return string(f) }
613 func (f fakeDir) Size() int64 { return 0 }
614 func (f fakeDir) Mode() fs.FileMode { return fs.ModeDir | 0500 }
615 func (f fakeDir) ModTime() time.Time { return time.Unix(0, 0) }
616 func (f fakeDir) IsDir() bool { return true }
617 func (f fakeDir) Sys() any { return nil }
618
619 func (f fakeDir) String() string {
620 return fs.FormatFileInfo(f)
621 }
622
623
624 func Glob(pattern string) (matches []string, err error) {
625 Trace("Glob", pattern)
626
627 if _, err := filepath.Match(pattern, ""); err != nil {
628 return nil, err
629 }
630 if !hasMeta(pattern) {
631 if _, err = Lstat(pattern); err != nil {
632 return nil, nil
633 }
634 return []string{pattern}, nil
635 }
636
637 dir, file := filepath.Split(pattern)
638 volumeLen := 0
639 if runtime.GOOS == "windows" {
640 volumeLen, dir = cleanGlobPathWindows(dir)
641 } else {
642 dir = cleanGlobPath(dir)
643 }
644
645 if !hasMeta(dir[volumeLen:]) {
646 return glob(dir, file, nil)
647 }
648
649
650 if dir == pattern {
651 return nil, filepath.ErrBadPattern
652 }
653
654 var m []string
655 m, err = Glob(dir)
656 if err != nil {
657 return
658 }
659 for _, d := range m {
660 matches, err = glob(d, file, matches)
661 if err != nil {
662 return
663 }
664 }
665 return
666 }
667
668
669 func cleanGlobPath(path string) string {
670 switch path {
671 case "":
672 return "."
673 case string(filepath.Separator):
674
675 return path
676 default:
677 return path[0 : len(path)-1]
678 }
679 }
680
681 func volumeNameLen(path string) int {
682 isSlash := func(c uint8) bool {
683 return c == '\\' || c == '/'
684 }
685 if len(path) < 2 {
686 return 0
687 }
688
689 c := path[0]
690 if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
691 return 2
692 }
693
694 if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) &&
695 !isSlash(path[2]) && path[2] != '.' {
696
697 for n := 3; n < l-1; n++ {
698
699 if isSlash(path[n]) {
700 n++
701
702 if !isSlash(path[n]) {
703 if path[n] == '.' {
704 break
705 }
706 for ; n < l; n++ {
707 if isSlash(path[n]) {
708 break
709 }
710 }
711 return n
712 }
713 break
714 }
715 }
716 }
717 return 0
718 }
719
720
721 func cleanGlobPathWindows(path string) (prefixLen int, cleaned string) {
722 vollen := volumeNameLen(path)
723 switch {
724 case path == "":
725 return 0, "."
726 case vollen+1 == len(path) && os.IsPathSeparator(path[len(path)-1]):
727
728 return vollen + 1, path
729 case vollen == len(path) && len(path) == 2:
730 return vollen, path + "."
731 default:
732 if vollen >= len(path) {
733 vollen = len(path) - 1
734 }
735 return vollen, path[0 : len(path)-1]
736 }
737 }
738
739
740
741
742
743 func glob(dir, pattern string, matches []string) (m []string, e error) {
744 m = matches
745 fi, err := Stat(dir)
746 if err != nil {
747 return
748 }
749 if !fi.IsDir() {
750 return
751 }
752
753 list, err := ReadDir(dir)
754 if err != nil {
755 return
756 }
757
758 var names []string
759 for _, info := range list {
760 names = append(names, info.Name())
761 }
762 sort.Strings(names)
763
764 for _, n := range names {
765 matched, err := filepath.Match(pattern, n)
766 if err != nil {
767 return m, err
768 }
769 if matched {
770 m = append(m, filepath.Join(dir, n))
771 }
772 }
773 return
774 }
775
776
777
778 func hasMeta(path string) bool {
779 magicChars := `*?[`
780 if runtime.GOOS != "windows" {
781 magicChars = `*?[\`
782 }
783 return strings.ContainsAny(path, magicChars)
784 }
785
View as plain text