github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/mapfs/fs.go (about)

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