github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/internal/fileresolver/unindexed_directory.go (about)

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