1 // Copyright 2020 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
16 // A MapFS is a simple in-memory file system for use in tests,
17 // represented as a map from path names (arguments to Open)
18 // to information about the files or directories they represent.
20 // The map need not include parent directories for files contained
21 // in the map; those will be synthesized if needed.
22 // But a directory can still be included by setting the MapFile.Mode's [fs.ModeDir] bit;
23 // this may be necessary for detailed control over the directory's [fs.FileInfo]
24 // or to create an empty directory.
26 // File system operations read directly from the map,
27 // so that the file system can be changed by editing the map as needed.
28 // An implication is that file system operations must not run concurrently
29 // with changes to the map, which would be a race.
30 // Another implication is that opening or reading a directory requires
31 // iterating over the entire map, so a MapFS should typically be used with not more
32 // than a few hundred entries or directory reads.
33 type MapFS map[string]*MapFile
35 // A MapFile describes a single file in a [MapFS].
37 Data []byte // file content
38 Mode fs.FileMode // fs.FileInfo.Mode
39 ModTime time.Time // fs.FileInfo.ModTime
40 Sys any // fs.FileInfo.Sys
43 var _ fs.FS = MapFS(nil)
44 var _ fs.File = (*openMapFile)(nil)
46 // Open opens the named file.
47 func (fsys MapFS) Open(name string) (fs.File, error) {
48 if !fs.ValidPath(name) {
49 return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
52 if file != nil && file.Mode&fs.ModeDir == 0 {
54 return &openMapFile{name, mapFileInfo{path.Base(name), file}, 0}, nil
57 // Directory, possibly synthesized.
58 // Note that file can be nil here: the map need not contain explicit parent directories for all its files.
59 // But file can also be non-nil, in case the user wants to set metadata for the directory explicitly.
60 // Either way, we need to construct the list of children of this directory.
61 var list []mapFileInfo
63 var need = make(map[string]bool)
66 for fname, f := range fsys {
67 i := strings.Index(fname, "/")
70 list = append(list, mapFileInfo{fname, f})
73 need[fname[:i]] = true
77 elem = name[strings.LastIndex(name, "/")+1:]
79 for fname, f := range fsys {
80 if strings.HasPrefix(fname, prefix) {
81 felem := fname[len(prefix):]
82 i := strings.Index(felem, "/")
84 list = append(list, mapFileInfo{felem, f})
86 need[fname[len(prefix):len(prefix)+i]] = true
90 // If the directory name is not in the map,
91 // and there are no children of the name in the map,
92 // then the directory is treated as not existing.
93 if file == nil && list == nil && len(need) == 0 {
94 return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
97 for _, fi := range list {
100 for name := range need {
101 list = append(list, mapFileInfo{name, &MapFile{Mode: fs.ModeDir | 0555}})
103 sort.Slice(list, func(i, j int) bool {
104 return list[i].name < list[j].name
108 file = &MapFile{Mode: fs.ModeDir | 0555}
110 return &mapDir{name, mapFileInfo{elem, file}, list, 0}, nil
113 // fsOnly is a wrapper that hides all but the fs.FS methods,
114 // to avoid an infinite recursion when implementing special
115 // methods in terms of helpers that would use them.
116 // (In general, implementing these methods using the package fs helpers
117 // is redundant and unnecessary, but having the methods may make
118 // MapFS exercise more code paths when used in tests.)
119 type fsOnly struct{ fs.FS }
121 func (fsys MapFS) ReadFile(name string) ([]byte, error) {
122 return fs.ReadFile(fsOnly{fsys}, name)
125 func (fsys MapFS) Stat(name string) (fs.FileInfo, error) {
126 return fs.Stat(fsOnly{fsys}, name)
129 func (fsys MapFS) ReadDir(name string) ([]fs.DirEntry, error) {
130 return fs.ReadDir(fsOnly{fsys}, name)
133 func (fsys MapFS) Glob(pattern string) ([]string, error) {
134 return fs.Glob(fsOnly{fsys}, pattern)
141 func (noSub) Sub() {} // not the fs.SubFS signature
143 func (fsys MapFS) Sub(dir string) (fs.FS, error) {
144 return fs.Sub(noSub{fsys}, dir)
147 // A mapFileInfo implements fs.FileInfo and fs.DirEntry for a given map file.
148 type mapFileInfo struct {
153 func (i *mapFileInfo) Name() string { return i.name }
154 func (i *mapFileInfo) Size() int64 { return int64(len(i.f.Data)) }
155 func (i *mapFileInfo) Mode() fs.FileMode { return i.f.Mode }
156 func (i *mapFileInfo) Type() fs.FileMode { return i.f.Mode.Type() }
157 func (i *mapFileInfo) ModTime() time.Time { return i.f.ModTime }
158 func (i *mapFileInfo) IsDir() bool { return i.f.Mode&fs.ModeDir != 0 }
159 func (i *mapFileInfo) Sys() any { return i.f.Sys }
160 func (i *mapFileInfo) Info() (fs.FileInfo, error) { return i, nil }
162 func (i *mapFileInfo) String() string {
163 return fs.FormatFileInfo(i)
166 // An openMapFile is a regular (non-directory) fs.File open for reading.
167 type openMapFile struct {
173 func (f *openMapFile) Stat() (fs.FileInfo, error) { return &f.mapFileInfo, nil }
175 func (f *openMapFile) Close() error { return nil }
177 func (f *openMapFile) Read(b []byte) (int, error) {
178 if f.offset >= int64(len(f.f.Data)) {
182 return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid}
184 n := copy(b, f.f.Data[f.offset:])
189 func (f *openMapFile) Seek(offset int64, whence int) (int64, error) {
196 offset += int64(len(f.f.Data))
198 if offset < 0 || offset > int64(len(f.f.Data)) {
199 return 0, &fs.PathError{Op: "seek", Path: f.path, Err: fs.ErrInvalid}
205 func (f *openMapFile) ReadAt(b []byte, offset int64) (int, error) {
206 if offset < 0 || offset > int64(len(f.f.Data)) {
207 return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid}
209 n := copy(b, f.f.Data[offset:])
216 // A mapDir is a directory fs.File (so also an fs.ReadDirFile) open for reading.
224 func (d *mapDir) Stat() (fs.FileInfo, error) { return &d.mapFileInfo, nil }
225 func (d *mapDir) Close() error { return nil }
226 func (d *mapDir) Read(b []byte) (int, error) {
227 return 0, &fs.PathError{Op: "read", Path: d.path, Err: fs.ErrInvalid}
230 func (d *mapDir) ReadDir(count int) ([]fs.DirEntry, error) {
231 n := len(d.entry) - d.offset
232 if n == 0 && count > 0 {
235 if count > 0 && n > count {
238 list := make([]fs.DirEntry, n)
239 for i := range list {
240 list[i] = &d.entry[d.offset+i]