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