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. 4 5package fstest 6 7import ( 8 "io" 9 "io/fs" 10 "path" 11 "slices" 12 "strings" 13 "time" 14) 15 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. 19// 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. 25// 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. 33type MapFS map[string]*MapFile 34 35// A MapFile describes a single file in a [MapFS]. 36type MapFile struct { 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 41} 42 43var _ fs.FS = MapFS(nil) 44var _ fs.File = (*openMapFile)(nil) 45 46// Open opens the named file. 47func (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} 50 } 51 file := fsys[name] 52 if file != nil && file.Mode&fs.ModeDir == 0 { 53 // Ordinary file 54 return &openMapFile{name, mapFileInfo{path.Base(name), file}, 0}, nil 55 } 56 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 62 var elem string 63 var need = make(map[string]bool) 64 if name == "." { 65 elem = "." 66 for fname, f := range fsys { 67 i := strings.Index(fname, "/") 68 if i < 0 { 69 if fname != "." { 70 list = append(list, mapFileInfo{fname, f}) 71 } 72 } else { 73 need[fname[:i]] = true 74 } 75 } 76 } else { 77 elem = name[strings.LastIndex(name, "/")+1:] 78 prefix := name + "/" 79 for fname, f := range fsys { 80 if strings.HasPrefix(fname, prefix) { 81 felem := fname[len(prefix):] 82 i := strings.Index(felem, "/") 83 if i < 0 { 84 list = append(list, mapFileInfo{felem, f}) 85 } else { 86 need[fname[len(prefix):len(prefix)+i]] = true 87 } 88 } 89 } 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} 95 } 96 } 97 for _, fi := range list { 98 delete(need, fi.name) 99 } 100 for name := range need { 101 list = append(list, mapFileInfo{name, &MapFile{Mode: fs.ModeDir | 0555}}) 102 } 103 slices.SortFunc(list, func(a, b mapFileInfo) int { 104 return strings.Compare(a.name, b.name) 105 }) 106 107 if file == nil { 108 file = &MapFile{Mode: fs.ModeDir | 0555} 109 } 110 return &mapDir{name, mapFileInfo{elem, file}, list, 0}, nil 111} 112 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.) 119type fsOnly struct{ fs.FS } 120 121func (fsys MapFS) ReadFile(name string) ([]byte, error) { 122 return fs.ReadFile(fsOnly{fsys}, name) 123} 124 125func (fsys MapFS) Stat(name string) (fs.FileInfo, error) { 126 return fs.Stat(fsOnly{fsys}, name) 127} 128 129func (fsys MapFS) ReadDir(name string) ([]fs.DirEntry, error) { 130 return fs.ReadDir(fsOnly{fsys}, name) 131} 132 133func (fsys MapFS) Glob(pattern string) ([]string, error) { 134 return fs.Glob(fsOnly{fsys}, pattern) 135} 136 137type noSub struct { 138 MapFS 139} 140 141func (noSub) Sub() {} // not the fs.SubFS signature 142 143func (fsys MapFS) Sub(dir string) (fs.FS, error) { 144 return fs.Sub(noSub{fsys}, dir) 145} 146 147// A mapFileInfo implements fs.FileInfo and fs.DirEntry for a given map file. 148type mapFileInfo struct { 149 name string 150 f *MapFile 151} 152 153func (i *mapFileInfo) Name() string { return path.Base(i.name) } 154func (i *mapFileInfo) Size() int64 { return int64(len(i.f.Data)) } 155func (i *mapFileInfo) Mode() fs.FileMode { return i.f.Mode } 156func (i *mapFileInfo) Type() fs.FileMode { return i.f.Mode.Type() } 157func (i *mapFileInfo) ModTime() time.Time { return i.f.ModTime } 158func (i *mapFileInfo) IsDir() bool { return i.f.Mode&fs.ModeDir != 0 } 159func (i *mapFileInfo) Sys() any { return i.f.Sys } 160func (i *mapFileInfo) Info() (fs.FileInfo, error) { return i, nil } 161 162func (i *mapFileInfo) String() string { 163 return fs.FormatFileInfo(i) 164} 165 166// An openMapFile is a regular (non-directory) fs.File open for reading. 167type openMapFile struct { 168 path string 169 mapFileInfo 170 offset int64 171} 172 173func (f *openMapFile) Stat() (fs.FileInfo, error) { return &f.mapFileInfo, nil } 174 175func (f *openMapFile) Close() error { return nil } 176 177func (f *openMapFile) Read(b []byte) (int, error) { 178 if f.offset >= int64(len(f.f.Data)) { 179 return 0, io.EOF 180 } 181 if f.offset < 0 { 182 return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid} 183 } 184 n := copy(b, f.f.Data[f.offset:]) 185 f.offset += int64(n) 186 return n, nil 187} 188 189func (f *openMapFile) Seek(offset int64, whence int) (int64, error) { 190 switch whence { 191 case 0: 192 // offset += 0 193 case 1: 194 offset += f.offset 195 case 2: 196 offset += int64(len(f.f.Data)) 197 } 198 if offset < 0 || offset > int64(len(f.f.Data)) { 199 return 0, &fs.PathError{Op: "seek", Path: f.path, Err: fs.ErrInvalid} 200 } 201 f.offset = offset 202 return offset, nil 203} 204 205func (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} 208 } 209 n := copy(b, f.f.Data[offset:]) 210 if n < len(b) { 211 return n, io.EOF 212 } 213 return n, nil 214} 215 216// A mapDir is a directory fs.File (so also an fs.ReadDirFile) open for reading. 217type mapDir struct { 218 path string 219 mapFileInfo 220 entry []mapFileInfo 221 offset int 222} 223 224func (d *mapDir) Stat() (fs.FileInfo, error) { return &d.mapFileInfo, nil } 225func (d *mapDir) Close() error { return nil } 226func (d *mapDir) Read(b []byte) (int, error) { 227 return 0, &fs.PathError{Op: "read", Path: d.path, Err: fs.ErrInvalid} 228} 229 230func (d *mapDir) ReadDir(count int) ([]fs.DirEntry, error) { 231 n := len(d.entry) - d.offset 232 if n == 0 && count > 0 { 233 return nil, io.EOF 234 } 235 if count > 0 && n > count { 236 n = count 237 } 238 list := make([]fs.DirEntry, n) 239 for i := range list { 240 list[i] = &d.entry[d.offset+i] 241 } 242 d.offset += n 243 return list, nil 244} 245