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 }