github.com/anchore/syft@v1.38.2/syft/internal/fileresolver/unindexed_directory.go (about)

     1  package fileresolver
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"slices"
    13  	"sort"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/bmatcuk/doublestar/v4"
    18  	"github.com/spf13/afero"
    19  
    20  	"github.com/anchore/go-homedir"
    21  	"github.com/anchore/syft/internal/log"
    22  	"github.com/anchore/syft/syft/file"
    23  )
    24  
    25  var _ file.Resolver = (*UnindexedDirectory)(nil)
    26  var _ file.WritableResolver = (*UnindexedDirectory)(nil)
    27  
    28  type UnindexedDirectory struct {
    29  	ls   afero.Lstater
    30  	lr   afero.LinkReader
    31  	base string
    32  	dir  string
    33  	fs   afero.Fs
    34  }
    35  
    36  func NewFromUnindexedDirectory(dir string) file.WritableResolver {
    37  	return NewFromUnindexedDirectoryFS(afero.NewOsFs(), dir, "")
    38  }
    39  
    40  func NewFromRootedUnindexedDirectory(dir string, base string) file.WritableResolver {
    41  	return NewFromUnindexedDirectoryFS(afero.NewOsFs(), dir, base)
    42  }
    43  
    44  func NewFromUnindexedDirectoryFS(fs afero.Fs, dir string, base string) file.WritableResolver {
    45  	ls, ok := fs.(afero.Lstater)
    46  	if !ok {
    47  		panic(fmt.Sprintf("unable to get afero.Lstater interface from: %+v", fs))
    48  	}
    49  	lr, ok := fs.(afero.LinkReader)
    50  	if !ok {
    51  		panic(fmt.Sprintf("unable to get afero.Lstater interface from: %+v", fs))
    52  	}
    53  	expanded, err := homedir.Expand(dir)
    54  	if err == nil {
    55  		dir = expanded
    56  	}
    57  	if base != "" {
    58  		expanded, err = homedir.Expand(base)
    59  		if err == nil {
    60  			base = expanded
    61  		}
    62  	}
    63  	wd, err := os.Getwd()
    64  	if err == nil {
    65  		if !filepath.IsAbs(dir) {
    66  			dir = filepath.Clean(filepath.Join(wd, dir))
    67  		}
    68  		if base != "" && !filepath.IsAbs(base) {
    69  			base = filepath.Clean(filepath.Join(wd, base))
    70  		}
    71  	}
    72  	return UnindexedDirectory{
    73  		base: base,
    74  		dir:  dir,
    75  		fs:   fs,
    76  		ls:   ls,
    77  		lr:   lr,
    78  	}
    79  }
    80  
    81  func (u UnindexedDirectory) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
    82  	p := u.absPath(u.scrubInputPath(location.RealPath))
    83  	f, err := u.fs.Open(p)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	fi, err := f.Stat()
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  	if fi.IsDir() {
    92  		return nil, fmt.Errorf("unable to get contents of directory: %s", location.RealPath)
    93  	}
    94  	return f, nil
    95  }
    96  
    97  // - full symlink resolution should be performed on all requests
    98  // - returns locations for any file or directory
    99  func (u UnindexedDirectory) HasPath(p string) bool {
   100  	locs, err := u.filesByPath(true, true, p)
   101  	return err == nil && len(locs) > 0
   102  }
   103  
   104  func (u UnindexedDirectory) canLstat(p string) bool {
   105  	_, _, err := u.ls.LstatIfPossible(u.absPath(p))
   106  	return err == nil
   107  }
   108  
   109  func (u UnindexedDirectory) isRegularFile(p string) bool {
   110  	fi, _, err := u.ls.LstatIfPossible(u.absPath(p))
   111  	return err == nil && !fi.IsDir()
   112  }
   113  
   114  func (u UnindexedDirectory) scrubInputPath(p string) string {
   115  	if path.IsAbs(p) {
   116  		p = p[1:]
   117  	}
   118  	return path.Clean(p)
   119  }
   120  
   121  func (u UnindexedDirectory) scrubResolutionPath(p string) string {
   122  	if u.base != "" {
   123  		if path.IsAbs(p) {
   124  			p = p[1:]
   125  		}
   126  		for strings.HasPrefix(p, "../") {
   127  			p = p[3:]
   128  		}
   129  	}
   130  	return path.Clean(p)
   131  }
   132  
   133  func (u UnindexedDirectory) absPath(p string) string {
   134  	if u.base != "" {
   135  		if path.IsAbs(p) {
   136  			p = p[1:]
   137  		}
   138  		for strings.HasPrefix(p, "../") {
   139  			p = p[3:]
   140  		}
   141  		p = path.Join(u.base, p)
   142  		return path.Clean(p)
   143  	}
   144  	if path.IsAbs(p) {
   145  		return p
   146  	}
   147  	return path.Clean(path.Join(u.dir, p))
   148  }
   149  
   150  // - full symlink resolution should be performed on all requests
   151  // - only returns locations to files (NOT directories)
   152  func (u UnindexedDirectory) FilesByPath(paths ...string) (out []file.Location, _ error) {
   153  	return u.filesByPath(true, false, paths...)
   154  }
   155  
   156  func (u UnindexedDirectory) filesByPath(resolveLinks bool, includeDirs bool, paths ...string) (out []file.Location, _ error) {
   157  	// sort here for stable output
   158  	sort.Strings(paths)
   159  nextPath:
   160  	for _, p := range paths {
   161  		p = u.scrubInputPath(p)
   162  		if u.canLstat(p) && (includeDirs || u.isRegularFile(p)) {
   163  			l := u.newLocation(p, resolveLinks)
   164  			if l == nil {
   165  				continue
   166  			}
   167  			// only include the first entry we find
   168  			for i := range out {
   169  				existing := &out[i]
   170  				if existing.RealPath == l.RealPath {
   171  					if l.AccessPath == "" {
   172  						existing.AccessPath = ""
   173  					}
   174  					continue nextPath
   175  				}
   176  			}
   177  			out = append(out, *l)
   178  		}
   179  	}
   180  	return
   181  }
   182  
   183  // - full symlink resolution should be performed on all requests
   184  // - if multiple paths to the same file are found, the best single match should be returned
   185  // - only returns locations to files (NOT directories)
   186  func (u UnindexedDirectory) FilesByGlob(patterns ...string) (out []file.Location, _ error) {
   187  	return u.filesByGlob(true, false, patterns...)
   188  }
   189  
   190  func (u UnindexedDirectory) filesByGlob(resolveLinks bool, includeDirs bool, patterns ...string) (out []file.Location, _ error) {
   191  	f := unindexedDirectoryResolverFS{
   192  		u: u,
   193  	}
   194  	var paths []string
   195  	for _, p := range patterns {
   196  		opts := []doublestar.GlobOption{doublestar.WithNoFollow()}
   197  		if !includeDirs {
   198  			opts = append(opts, doublestar.WithFilesOnly())
   199  		}
   200  		found, err := doublestar.Glob(f, p, opts...)
   201  		if err != nil {
   202  			return nil, err
   203  		}
   204  		paths = append(paths, found...)
   205  	}
   206  	return u.filesByPath(resolveLinks, includeDirs, paths...)
   207  }
   208  
   209  func (u UnindexedDirectory) FilesByMIMEType(_ ...string) ([]file.Location, error) {
   210  	panic("FilesByMIMEType unsupported")
   211  }
   212  
   213  // RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference.
   214  // This is helpful when attempting to find a file that is in the same layer or lower as another file.
   215  func (u UnindexedDirectory) RelativeFileByPath(l file.Location, p string) *file.Location {
   216  	p = path.Clean(path.Join(l.RealPath, p))
   217  	locs, err := u.filesByPath(true, false, p)
   218  	if err != nil || len(locs) == 0 {
   219  		return nil
   220  	}
   221  	l = locs[0]
   222  	p = l.RealPath
   223  	if u.isRegularFile(p) {
   224  		return u.newLocation(p, true)
   225  	}
   226  	return nil
   227  }
   228  
   229  // - NO symlink resolution should be performed on results
   230  // - returns locations for any file or directory
   231  func (u UnindexedDirectory) AllLocations(ctx context.Context) <-chan file.Location {
   232  	out := make(chan file.Location)
   233  	errWalkCanceled := fmt.Errorf("walk canceled")
   234  	go func() {
   235  		defer close(out)
   236  		err := afero.Walk(u.fs, u.absPath("."), func(p string, _ fs.FileInfo, _ error) error {
   237  			p = strings.TrimPrefix(p, u.dir)
   238  			if p == "" {
   239  				return nil
   240  			}
   241  			p = strings.TrimPrefix(p, "/")
   242  			select {
   243  			case out <- file.NewLocation(p):
   244  				return nil
   245  			case <-ctx.Done():
   246  				return errWalkCanceled
   247  			}
   248  		})
   249  		if err != nil && !errors.Is(err, errWalkCanceled) {
   250  			log.Debug(err)
   251  		}
   252  	}()
   253  	return out
   254  }
   255  
   256  func (u UnindexedDirectory) FileMetadataByLocation(_ file.Location) (file.Metadata, error) {
   257  	panic("FileMetadataByLocation unsupported")
   258  }
   259  
   260  func (u UnindexedDirectory) Write(location file.Location, reader io.Reader) error {
   261  	filePath := location.RealPath
   262  	if path.IsAbs(filePath) {
   263  		filePath = filePath[1:]
   264  	}
   265  	absPath := u.absPath(filePath)
   266  	return afero.WriteReader(u.fs, absPath, reader)
   267  }
   268  
   269  func (u UnindexedDirectory) newLocation(filePath string, resolveLinks bool) *file.Location {
   270  	filePath = path.Clean(filePath)
   271  
   272  	virtualPath := filePath
   273  	realPath := filePath
   274  
   275  	if resolveLinks {
   276  		paths := u.resolveLinks(filePath)
   277  		if len(paths) > 1 {
   278  			realPath = paths[len(paths)-1]
   279  			// TODO: this is not quite correct, as the equivalent of os.EvalSymlinks needs to be done (in the context of afero)
   280  			if realPath != path.Clean(filePath) {
   281  				virtualPath = paths[0]
   282  			}
   283  		}
   284  		if len(paths) == 0 {
   285  			// this file does not exist, don't return a location
   286  			return nil
   287  		}
   288  	}
   289  
   290  	l := file.NewVirtualLocation(realPath, virtualPath)
   291  	return &l
   292  }
   293  
   294  //nolint:gocognit
   295  func (u UnindexedDirectory) resolveLinks(filePath string) []string {
   296  	var visited []string
   297  
   298  	out := []string{}
   299  
   300  	resolvedPath := ""
   301  
   302  	parts := strings.Split(filePath, "/")
   303  	for i := 0; i < len(parts); i++ {
   304  		part := parts[i]
   305  		if resolvedPath == "" {
   306  			resolvedPath = part
   307  		} else {
   308  			resolvedPath = path.Clean(path.Join(resolvedPath, part))
   309  		}
   310  		resolvedPath = u.scrubResolutionPath(resolvedPath)
   311  		if resolvedPath == ".." {
   312  			resolvedPath = ""
   313  			continue
   314  		}
   315  
   316  		absPath := u.absPath(resolvedPath)
   317  		if slices.Contains(visited, absPath) {
   318  			return nil // circular links can't resolve
   319  		}
   320  		visited = append(visited, absPath)
   321  
   322  		fi, wasLstat, err := u.ls.LstatIfPossible(absPath)
   323  		if fi == nil || err != nil {
   324  			// this file does not exist
   325  			return nil
   326  		}
   327  
   328  		for wasLstat && u.isSymlink(fi) {
   329  			next, err := u.lr.ReadlinkIfPossible(absPath)
   330  			if err == nil {
   331  				if !path.IsAbs(next) {
   332  					next = path.Clean(path.Join(path.Dir(resolvedPath), next))
   333  				}
   334  				next = u.scrubResolutionPath(next)
   335  				absPath = u.absPath(next)
   336  				if slices.Contains(visited, absPath) {
   337  					return nil // circular links can't resolve
   338  				}
   339  				visited = append(visited, absPath)
   340  
   341  				fi, wasLstat, err = u.ls.LstatIfPossible(absPath)
   342  				if fi == nil || err != nil {
   343  					// this file does not exist
   344  					return nil
   345  				}
   346  				if i < len(parts) {
   347  					out = append(out, path.Join(resolvedPath, path.Join(parts[i+1:]...)))
   348  				}
   349  				if u.base != "" && path.IsAbs(next) {
   350  					next = next[1:]
   351  				}
   352  				resolvedPath = next
   353  			}
   354  		}
   355  	}
   356  
   357  	out = append(out, resolvedPath)
   358  
   359  	return out
   360  }
   361  
   362  func (u UnindexedDirectory) isSymlink(fi os.FileInfo) bool {
   363  	return fi.Mode().Type()&fs.ModeSymlink == fs.ModeSymlink
   364  }
   365  
   366  // ------------------------- fs.FS ------------------------------
   367  
   368  // unindexedDirectoryResolverFS wraps the UnindexedDirectory as a fs.FS, fs.ReadDirFS, and fs.StatFS
   369  type unindexedDirectoryResolverFS struct {
   370  	u UnindexedDirectory
   371  }
   372  
   373  // resolve takes a virtual path and returns the resolved absolute or relative path and file info
   374  func (f unindexedDirectoryResolverFS) resolve(filePath string) (resolved string, fi fs.FileInfo, err error) {
   375  	parts := strings.Split(filePath, "/")
   376  	var visited []string
   377  	for i, part := range parts {
   378  		if i > 0 {
   379  			resolved = path.Clean(path.Join(resolved, part))
   380  		} else {
   381  			resolved = part
   382  		}
   383  		abs := f.u.absPath(resolved)
   384  		fi, _, err = f.u.ls.LstatIfPossible(abs)
   385  		if err != nil {
   386  			return resolved, fi, err
   387  		}
   388  		for f.u.isSymlink(fi) {
   389  			if slices.Contains(visited, resolved) {
   390  				return resolved, fi, fmt.Errorf("link cycle detected at: %s", f.u.absPath(resolved))
   391  			}
   392  			visited = append(visited, resolved)
   393  			link, err := f.u.lr.ReadlinkIfPossible(abs)
   394  			if err != nil {
   395  				return resolved, fi, err
   396  			}
   397  			if !path.IsAbs(link) {
   398  				link = path.Clean(path.Join(path.Dir(abs), link))
   399  				link = strings.TrimPrefix(link, abs)
   400  			} else if f.u.base != "" {
   401  				link = path.Clean(path.Join(f.u.base, link[1:]))
   402  			}
   403  			resolved = link
   404  			abs = f.u.absPath(resolved)
   405  			fi, _, err = f.u.ls.LstatIfPossible(abs)
   406  			if err != nil {
   407  				return resolved, fi, err
   408  			}
   409  		}
   410  	}
   411  	return resolved, fi, err
   412  }
   413  
   414  func (f unindexedDirectoryResolverFS) ReadDir(name string) (out []fs.DirEntry, _ error) {
   415  	p, _, err := f.resolve(name)
   416  	if err != nil {
   417  		return nil, err
   418  	}
   419  	entries, err := afero.ReadDir(f.u.fs, f.u.absPath(p))
   420  	if err != nil {
   421  		return nil, err
   422  	}
   423  	for _, e := range entries {
   424  		isDir := e.IsDir()
   425  		_, fi, _ := f.resolve(path.Join(name, e.Name()))
   426  		if fi != nil && fi.IsDir() {
   427  			isDir = true
   428  		}
   429  		out = append(out, unindexedDirectoryResolverDirEntry{
   430  			unindexedDirectoryResolverFileInfo: newFsFileInfo(f.u, e.Name(), isDir, e),
   431  		})
   432  	}
   433  	return out, nil
   434  }
   435  
   436  func (f unindexedDirectoryResolverFS) Stat(name string) (fs.FileInfo, error) {
   437  	fi, err := f.u.fs.Stat(f.u.absPath(name))
   438  	if err != nil {
   439  		return nil, err
   440  	}
   441  	return newFsFileInfo(f.u, name, fi.IsDir(), fi), nil
   442  }
   443  
   444  func (f unindexedDirectoryResolverFS) Open(name string) (fs.File, error) {
   445  	_, err := f.u.fs.Open(f.u.absPath(name))
   446  	if err != nil {
   447  		return nil, err
   448  	}
   449  
   450  	return unindexedDirectoryResolverFile{
   451  		u:    f.u,
   452  		path: name,
   453  	}, nil
   454  }
   455  
   456  var _ fs.FS = (*unindexedDirectoryResolverFS)(nil)
   457  var _ fs.StatFS = (*unindexedDirectoryResolverFS)(nil)
   458  var _ fs.ReadDirFS = (*unindexedDirectoryResolverFS)(nil)
   459  
   460  type unindexedDirectoryResolverDirEntry struct {
   461  	unindexedDirectoryResolverFileInfo
   462  }
   463  
   464  func (f unindexedDirectoryResolverDirEntry) Name() string {
   465  	return f.name
   466  }
   467  
   468  func (f unindexedDirectoryResolverDirEntry) IsDir() bool {
   469  	return f.isDir
   470  }
   471  
   472  func (f unindexedDirectoryResolverDirEntry) Type() fs.FileMode {
   473  	return f.mode
   474  }
   475  
   476  func (f unindexedDirectoryResolverDirEntry) Info() (fs.FileInfo, error) {
   477  	return f, nil
   478  }
   479  
   480  var _ fs.DirEntry = (*unindexedDirectoryResolverDirEntry)(nil)
   481  
   482  type unindexedDirectoryResolverFile struct {
   483  	u    UnindexedDirectory
   484  	path string
   485  }
   486  
   487  func (f unindexedDirectoryResolverFile) Stat() (fs.FileInfo, error) {
   488  	fi, err := f.u.fs.Stat(f.u.absPath(f.path))
   489  	if err != nil {
   490  		return nil, err
   491  	}
   492  	return newFsFileInfo(f.u, fi.Name(), fi.IsDir(), fi), nil
   493  }
   494  
   495  func (f unindexedDirectoryResolverFile) Read(_ []byte) (int, error) {
   496  	panic("Read not implemented")
   497  }
   498  
   499  func (f unindexedDirectoryResolverFile) Close() error {
   500  	panic("Close not implemented")
   501  }
   502  
   503  var _ fs.File = (*unindexedDirectoryResolverFile)(nil)
   504  
   505  type unindexedDirectoryResolverFileInfo struct {
   506  	u       UnindexedDirectory
   507  	name    string
   508  	size    int64
   509  	mode    fs.FileMode
   510  	modTime time.Time
   511  	isDir   bool
   512  	sys     any
   513  }
   514  
   515  func newFsFileInfo(u UnindexedDirectory, name string, isDir bool, fi os.FileInfo) unindexedDirectoryResolverFileInfo {
   516  	return unindexedDirectoryResolverFileInfo{
   517  		u:       u,
   518  		name:    name,
   519  		size:    fi.Size(),
   520  		mode:    fi.Mode() & ^fs.ModeSymlink, // pretend nothing is a symlink
   521  		modTime: fi.ModTime(),
   522  		isDir:   isDir,
   523  		// sys:     fi.Sys(), // what values does this hold?
   524  	}
   525  }
   526  
   527  func (f unindexedDirectoryResolverFileInfo) Name() string {
   528  	return f.name
   529  }
   530  
   531  func (f unindexedDirectoryResolverFileInfo) Size() int64 {
   532  	return f.size
   533  }
   534  
   535  func (f unindexedDirectoryResolverFileInfo) Mode() fs.FileMode {
   536  	return f.mode
   537  }
   538  
   539  func (f unindexedDirectoryResolverFileInfo) ModTime() time.Time {
   540  	return f.modTime
   541  }
   542  
   543  func (f unindexedDirectoryResolverFileInfo) IsDir() bool {
   544  	return f.isDir
   545  }
   546  
   547  func (f unindexedDirectoryResolverFileInfo) Sys() any {
   548  	return f.sys
   549  }
   550  
   551  var _ fs.FileInfo = (*unindexedDirectoryResolverFileInfo)(nil)