Source file src/testing/fstest/mapfs.go

     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  
     5  package fstest
     6  
     7  import (
     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, directories, or symbolic links 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.
    33  type MapFS map[string]*MapFile
    34  
    35  // A MapFile describes a single file in a [MapFS].
    36  type MapFile struct {
    37  	Data    []byte      // file content or symlink destination
    38  	Mode    fs.FileMode // fs.FileInfo.Mode
    39  	ModTime time.Time   // fs.FileInfo.ModTime
    40  	Sys     any         // fs.FileInfo.Sys
    41  }
    42  
    43  var _ fs.FS = MapFS(nil)
    44  var _ fs.ReadLinkFS = MapFS(nil)
    45  var _ fs.File = (*openMapFile)(nil)
    46  
    47  // Open opens the named file after following any symbolic links.
    48  func (fsys MapFS) Open(name string) (fs.File, error) {
    49  	if !fs.ValidPath(name) {
    50  		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
    51  	}
    52  	realName, ok := fsys.resolveSymlinks(name)
    53  	if !ok {
    54  		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
    55  	}
    56  
    57  	file := fsys[realName]
    58  	if file != nil && file.Mode&fs.ModeDir == 0 {
    59  		// Ordinary file
    60  		return &openMapFile{name, mapFileInfo{path.Base(name), file}, 0}, nil
    61  	}
    62  
    63  	// Directory, possibly synthesized.
    64  	// Note that file can be nil here: the map need not contain explicit parent directories for all its files.
    65  	// But file can also be non-nil, in case the user wants to set metadata for the directory explicitly.
    66  	// Either way, we need to construct the list of children of this directory.
    67  	var list []mapFileInfo
    68  	var need = make(map[string]bool)
    69  	if realName == "." {
    70  		for fname, f := range fsys {
    71  			i := strings.Index(fname, "/")
    72  			if i < 0 {
    73  				if fname != "." {
    74  					list = append(list, mapFileInfo{fname, f})
    75  				}
    76  			} else {
    77  				need[fname[:i]] = true
    78  			}
    79  		}
    80  	} else {
    81  		prefix := realName + "/"
    82  		for fname, f := range fsys {
    83  			if strings.HasPrefix(fname, prefix) {
    84  				felem := fname[len(prefix):]
    85  				i := strings.Index(felem, "/")
    86  				if i < 0 {
    87  					list = append(list, mapFileInfo{felem, f})
    88  				} else {
    89  					need[fname[len(prefix):len(prefix)+i]] = true
    90  				}
    91  			}
    92  		}
    93  		// If the directory name is not in the map,
    94  		// and there are no children of the name in the map,
    95  		// then the directory is treated as not existing.
    96  		if file == nil && list == nil && len(need) == 0 {
    97  			return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
    98  		}
    99  	}
   100  	for _, fi := range list {
   101  		delete(need, fi.name)
   102  	}
   103  	for name := range need {
   104  		list = append(list, mapFileInfo{name, &MapFile{Mode: fs.ModeDir | 0555}})
   105  	}
   106  	slices.SortFunc(list, func(a, b mapFileInfo) int {
   107  		return strings.Compare(a.name, b.name)
   108  	})
   109  
   110  	if file == nil {
   111  		file = &MapFile{Mode: fs.ModeDir | 0555}
   112  	}
   113  	var elem string
   114  	if name == "." {
   115  		elem = "."
   116  	} else {
   117  		elem = name[strings.LastIndex(name, "/")+1:]
   118  	}
   119  	return &mapDir{name, mapFileInfo{elem, file}, list, 0}, nil
   120  }
   121  
   122  func (fsys MapFS) resolveSymlinks(name string) (_ string, ok bool) {
   123  	// Fast path: if a symlink is in the map, resolve it.
   124  	if file := fsys[name]; file != nil && file.Mode.Type() == fs.ModeSymlink {
   125  		target := string(file.Data)
   126  		if path.IsAbs(target) {
   127  			return "", false
   128  		}
   129  		return fsys.resolveSymlinks(path.Join(path.Dir(name), target))
   130  	}
   131  
   132  	// Check if each parent directory (starting at root) is a symlink.
   133  	for i := 0; i < len(name); {
   134  		j := strings.Index(name[i:], "/")
   135  		var dir string
   136  		if j < 0 {
   137  			dir = name
   138  			i = len(name)
   139  		} else {
   140  			dir = name[:i+j]
   141  			i += j
   142  		}
   143  		if file := fsys[dir]; file != nil && file.Mode.Type() == fs.ModeSymlink {
   144  			target := string(file.Data)
   145  			if path.IsAbs(target) {
   146  				return "", false
   147  			}
   148  			return fsys.resolveSymlinks(path.Join(path.Dir(dir), target) + name[i:])
   149  		}
   150  		i += len("/")
   151  	}
   152  	return name, fs.ValidPath(name)
   153  }
   154  
   155  // ReadLink returns the destination of the named symbolic link.
   156  func (fsys MapFS) ReadLink(name string) (string, error) {
   157  	info, err := fsys.lstat(name)
   158  	if err != nil {
   159  		return "", &fs.PathError{Op: "readlink", Path: name, Err: err}
   160  	}
   161  	if info.f.Mode.Type() != fs.ModeSymlink {
   162  		return "", &fs.PathError{Op: "readlink", Path: name, Err: fs.ErrInvalid}
   163  	}
   164  	return string(info.f.Data), nil
   165  }
   166  
   167  // Lstat returns a FileInfo describing the named file.
   168  // If the file is a symbolic link, the returned FileInfo describes the symbolic link.
   169  // Lstat makes no attempt to follow the link.
   170  func (fsys MapFS) Lstat(name string) (fs.FileInfo, error) {
   171  	info, err := fsys.lstat(name)
   172  	if err != nil {
   173  		return nil, &fs.PathError{Op: "lstat", Path: name, Err: err}
   174  	}
   175  	return info, nil
   176  }
   177  
   178  func (fsys MapFS) lstat(name string) (*mapFileInfo, error) {
   179  	if !fs.ValidPath(name) {
   180  		return nil, fs.ErrNotExist
   181  	}
   182  	realDir, ok := fsys.resolveSymlinks(path.Dir(name))
   183  	if !ok {
   184  		return nil, fs.ErrNotExist
   185  	}
   186  	elem := path.Base(name)
   187  	realName := path.Join(realDir, elem)
   188  
   189  	file := fsys[realName]
   190  	if file != nil {
   191  		return &mapFileInfo{elem, file}, nil
   192  	}
   193  
   194  	if realName == "." {
   195  		return &mapFileInfo{elem, &MapFile{Mode: fs.ModeDir | 0555}}, nil
   196  	}
   197  	// Maybe a directory.
   198  	prefix := realName + "/"
   199  	for fname := range fsys {
   200  		if strings.HasPrefix(fname, prefix) {
   201  			return &mapFileInfo{elem, &MapFile{Mode: fs.ModeDir | 0555}}, nil
   202  		}
   203  	}
   204  	// If the directory name is not in the map,
   205  	// and there are no children of the name in the map,
   206  	// then the directory is treated as not existing.
   207  	return nil, fs.ErrNotExist
   208  }
   209  
   210  // fsOnly is a wrapper that hides all but the fs.FS methods,
   211  // to avoid an infinite recursion when implementing special
   212  // methods in terms of helpers that would use them.
   213  // (In general, implementing these methods using the package fs helpers
   214  // is redundant and unnecessary, but having the methods may make
   215  // MapFS exercise more code paths when used in tests.)
   216  type fsOnly struct{ fs.FS }
   217  
   218  func (fsys MapFS) ReadFile(name string) ([]byte, error) {
   219  	return fs.ReadFile(fsOnly{fsys}, name)
   220  }
   221  
   222  func (fsys MapFS) Stat(name string) (fs.FileInfo, error) {
   223  	return fs.Stat(fsOnly{fsys}, name)
   224  }
   225  
   226  func (fsys MapFS) ReadDir(name string) ([]fs.DirEntry, error) {
   227  	return fs.ReadDir(fsOnly{fsys}, name)
   228  }
   229  
   230  func (fsys MapFS) Glob(pattern string) ([]string, error) {
   231  	return fs.Glob(fsOnly{fsys}, pattern)
   232  }
   233  
   234  type noSub struct {
   235  	MapFS
   236  }
   237  
   238  func (noSub) Sub() {} // not the fs.SubFS signature
   239  
   240  func (fsys MapFS) Sub(dir string) (fs.FS, error) {
   241  	return fs.Sub(noSub{fsys}, dir)
   242  }
   243  
   244  // A mapFileInfo implements fs.FileInfo and fs.DirEntry for a given map file.
   245  type mapFileInfo struct {
   246  	name string
   247  	f    *MapFile
   248  }
   249  
   250  func (i *mapFileInfo) Name() string               { return path.Base(i.name) }
   251  func (i *mapFileInfo) Size() int64                { return int64(len(i.f.Data)) }
   252  func (i *mapFileInfo) Mode() fs.FileMode          { return i.f.Mode }
   253  func (i *mapFileInfo) Type() fs.FileMode          { return i.f.Mode.Type() }
   254  func (i *mapFileInfo) ModTime() time.Time         { return i.f.ModTime }
   255  func (i *mapFileInfo) IsDir() bool                { return i.f.Mode&fs.ModeDir != 0 }
   256  func (i *mapFileInfo) Sys() any                   { return i.f.Sys }
   257  func (i *mapFileInfo) Info() (fs.FileInfo, error) { return i, nil }
   258  
   259  func (i *mapFileInfo) String() string {
   260  	return fs.FormatFileInfo(i)
   261  }
   262  
   263  // An openMapFile is a regular (non-directory) fs.File open for reading.
   264  type openMapFile struct {
   265  	path string
   266  	mapFileInfo
   267  	offset int64
   268  }
   269  
   270  func (f *openMapFile) Stat() (fs.FileInfo, error) { return &f.mapFileInfo, nil }
   271  
   272  func (f *openMapFile) Close() error { return nil }
   273  
   274  func (f *openMapFile) Read(b []byte) (int, error) {
   275  	if f.offset >= int64(len(f.f.Data)) {
   276  		return 0, io.EOF
   277  	}
   278  	if f.offset < 0 {
   279  		return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid}
   280  	}
   281  	n := copy(b, f.f.Data[f.offset:])
   282  	f.offset += int64(n)
   283  	return n, nil
   284  }
   285  
   286  func (f *openMapFile) Seek(offset int64, whence int) (int64, error) {
   287  	switch whence {
   288  	case 0:
   289  		// offset += 0
   290  	case 1:
   291  		offset += f.offset
   292  	case 2:
   293  		offset += int64(len(f.f.Data))
   294  	}
   295  	if offset < 0 || offset > int64(len(f.f.Data)) {
   296  		return 0, &fs.PathError{Op: "seek", Path: f.path, Err: fs.ErrInvalid}
   297  	}
   298  	f.offset = offset
   299  	return offset, nil
   300  }
   301  
   302  func (f *openMapFile) ReadAt(b []byte, offset int64) (int, error) {
   303  	if offset < 0 || offset > int64(len(f.f.Data)) {
   304  		return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid}
   305  	}
   306  	n := copy(b, f.f.Data[offset:])
   307  	if n < len(b) {
   308  		return n, io.EOF
   309  	}
   310  	return n, nil
   311  }
   312  
   313  // A mapDir is a directory fs.File (so also an fs.ReadDirFile) open for reading.
   314  type mapDir struct {
   315  	path string
   316  	mapFileInfo
   317  	entry  []mapFileInfo
   318  	offset int
   319  }
   320  
   321  func (d *mapDir) Stat() (fs.FileInfo, error) { return &d.mapFileInfo, nil }
   322  func (d *mapDir) Close() error               { return nil }
   323  func (d *mapDir) Read(b []byte) (int, error) {
   324  	return 0, &fs.PathError{Op: "read", Path: d.path, Err: fs.ErrInvalid}
   325  }
   326  
   327  func (d *mapDir) ReadDir(count int) ([]fs.DirEntry, error) {
   328  	n := len(d.entry) - d.offset
   329  	if n == 0 && count > 0 {
   330  		return nil, io.EOF
   331  	}
   332  	if count > 0 && n > count {
   333  		n = count
   334  	}
   335  	list := make([]fs.DirEntry, n)
   336  	for i := range list {
   337  		list[i] = &d.entry[d.offset+i]
   338  	}
   339  	d.offset += n
   340  	return list, nil
   341  }
   342  

View as plain text