-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
fs.go
251 lines (219 loc) · 6.65 KB
/
fs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
package mapfs
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"time"
"golang.org/x/xerrors"
xsync "github.com/aquasecurity/trivy/pkg/x/sync"
)
type allFS interface {
fs.ReadFileFS
fs.ReadDirFS
fs.StatFS
fs.GlobFS
fs.SubFS
}
// Make sure FS implements all the interfaces
var _ allFS = &FS{}
// FS is an in-memory filesystem
type FS struct {
root *file
// When the underlyingRoot has a value, it allows access to the local filesystem outside of this in-memory filesystem.
// The set path is used as the starting point when accessing the local filesystem.
// In other words, although mapfs.Open("../foo") would normally result in an error, if this option is enabled,
// it will be executed as os.Open(filepath.Join(underlyingRoot, "../foo")).
underlyingRoot string
}
type Option func(*FS)
// WithUnderlyingRoot returns an option to set the underlying root path for the in-memory filesystem.
func WithUnderlyingRoot(root string) Option {
return func(fsys *FS) {
fsys.underlyingRoot = root
}
}
// New creates a new filesystem
func New(opts ...Option) *FS {
fsys := &FS{
root: &file{
stat: fileStat{
name: ".",
size: 0x100,
modTime: time.Now(),
mode: 0o0700 | fs.ModeDir,
},
files: xsync.Map[string, *file]{},
},
}
for _, opt := range opts {
opt(fsys)
}
return fsys
}
// Filter removes the specified skippedFiles and returns a new FS
func (m *FS) Filter(skippedFiles []string) (*FS, error) {
if len(skippedFiles) == 0 {
return m, nil
}
filter := func(path string, _ fs.DirEntry) (bool, error) {
return slices.Contains(skippedFiles, path), nil
}
return m.FilterFunc(filter)
}
func (m *FS) FilterFunc(fn func(path string, d fs.DirEntry) (bool, error)) (*FS, error) {
newFS := New(WithUnderlyingRoot(m.underlyingRoot))
err := fs.WalkDir(m, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return newFS.MkdirAll(path, d.Type().Perm())
}
if filtered, err := fn(path, d); err != nil {
return err
} else if filtered {
return nil
}
f, err := m.root.getFile(path)
if err != nil {
return xerrors.Errorf("unable to get %s: %w", path, err)
}
// Virtual file
if f.underlyingPath == "" {
return newFS.WriteVirtualFile(path, f.data, f.stat.mode)
}
return newFS.WriteFile(path, f.underlyingPath)
})
if err != nil {
return nil, xerrors.Errorf("walk error %w", err)
}
return newFS, nil
}
func (m *FS) CopyFilesUnder(dir string) error {
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
} else if d.IsDir() {
return m.MkdirAll(path, d.Type())
}
return m.WriteFile(path, path)
})
}
// Stat returns a FileInfo describing the file.
func (m *FS) Stat(name string) (fs.FileInfo, error) {
if m.isPathAboveRoot(name) {
return os.Stat(filepath.Join(m.underlyingRoot, name))
}
name = cleanPath(name)
f, err := m.root.getFile(name)
if err != nil {
return nil, &fs.PathError{
Op: "stat",
Path: name,
Err: err,
}
}
if f.isVirtual() {
return &f.stat, nil
}
return os.Stat(f.underlyingPath)
}
// ReadDir reads the named directory
// and returns a list of directory entries sorted by filename.
func (m *FS) ReadDir(name string) ([]fs.DirEntry, error) {
if m.isPathAboveRoot(name) {
return os.ReadDir(filepath.Join(m.underlyingRoot, name))
}
return m.root.ReadDir(cleanPath(name))
}
// Open opens the named file for reading.
func (m *FS) Open(name string) (fs.File, error) {
if m.isPathAboveRoot(name) {
return os.Open(filepath.Join(m.underlyingRoot, name))
}
return m.root.Open(cleanPath(name))
}
// WriteFile creates a mapping between path and underlyingPath.
func (m *FS) WriteFile(path, underlyingPath string) error {
return m.root.WriteFile(cleanPath(path), underlyingPath)
}
// WriteVirtualFile writes the specified bytes to the named file. If the file exists, it will be overwritten.
func (m *FS) WriteVirtualFile(path string, data []byte, mode fs.FileMode) error {
return m.root.WriteVirtualFile(cleanPath(path), data, mode)
}
// MkdirAll creates a directory named path,
// along with any necessary parents, and returns nil,
// or else returns an error.
// The permission bits perm (before umask) are used for all
// directories that MkdirAll creates.
// If path is already a directory, MkdirAll does nothing
// and returns nil.
func (m *FS) MkdirAll(path string, perm fs.FileMode) error {
return m.root.MkdirAll(cleanPath(path), perm)
}
// ReadFile reads the named file and returns its contents.
// A successful call returns a nil error, not io.EOF.
// (Because ReadFile reads the whole file, the expected EOF
// from the final Read is not treated as an error to be reported.)
//
// The caller is permitted to modify the returned byte slice.
// This method should return a copy of the underlying data.
func (m *FS) ReadFile(name string) ([]byte, error) {
if m.isPathAboveRoot(name) {
return os.ReadFile(filepath.Join(m.underlyingRoot, name))
}
f, err := m.root.Open(cleanPath(name))
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()
return io.ReadAll(f)
}
// Sub returns an FS corresponding to the subtree rooted at dir.
func (m *FS) Sub(dir string) (fs.FS, error) {
d, err := m.root.getFile(cleanPath(dir))
if err != nil {
return nil, err
}
return &FS{
root: d,
}, nil
}
// Glob returns the names of all files matching pattern or nil
// if there is no matching file. The syntax of patterns is the same
// as in Match. The pattern may describe hierarchical names such as
// /usr/*/bin/ed (assuming the Separator is '/').
//
// Glob ignores file system errors such as I/O errors reading directories.
// The only possible returned error is ErrBadPattern, when pattern
// is malformed.
func (m *FS) Glob(pattern string) ([]string, error) {
return m.root.glob(pattern)
}
// Remove deletes a file or directory from the filesystem
func (m *FS) Remove(path string) error {
return m.root.Remove(cleanPath(path))
}
// RemoveAll deletes a file or directory and any children if present from the filesystem
func (m *FS) RemoveAll(path string) error {
return m.root.RemoveAll(cleanPath(path))
}
func cleanPath(path string) string {
// Convert the volume name like 'C:' into dir like 'C\'
if vol := filepath.VolumeName(path); vol != "" {
newVol := strings.TrimSuffix(vol, ":")
newVol = fmt.Sprintf("%s%c", newVol, filepath.Separator)
path = strings.Replace(path, vol, newVol, 1)
}
path = filepath.Clean(path)
path = filepath.ToSlash(path)
path = strings.TrimLeft(path, "/") // Remove the leading slash
return path
}
func (m *FS) isPathAboveRoot(name string) bool {
return (name == ".." || strings.HasPrefix(name, "../")) && m.underlyingRoot != ""
}