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