github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/internal/fileresolver/unindexed_directory.go (about)

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