github.com/anchore/syft@v1.38.2/internal/file/zip_file_manifest.go (about)

     1  package file
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/bmatcuk/doublestar/v4"
    10  	"github.com/mholt/archives"
    11  	"github.com/scylladb/go-set/strset"
    12  
    13  	"github.com/anchore/syft/internal/log"
    14  )
    15  
    16  // ZipFileManifest is a collection of paths and their file metadata.
    17  type ZipFileManifest map[string]os.FileInfo
    18  
    19  // NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
    20  func NewZipFileManifest(ctx context.Context, archivePath string) (ZipFileManifest, error) {
    21  	zipReader, err := os.Open(archivePath)
    22  	manifest := make(ZipFileManifest)
    23  	if err != nil {
    24  		log.Debugf("unable to open zip archive (%s): %v", archivePath, err)
    25  		return manifest, err
    26  	}
    27  	defer func() {
    28  		if err = zipReader.Close(); err != nil {
    29  			log.Debugf("unable to close zip archive (%s): %+v", archivePath, err)
    30  		}
    31  	}()
    32  
    33  	err = archives.Zip{}.Extract(ctx, zipReader, func(_ context.Context, file archives.FileInfo) error {
    34  		manifest.Add(file.NameInArchive, file.FileInfo)
    35  		return nil
    36  	})
    37  	if err != nil {
    38  		return manifest, err
    39  	}
    40  	return manifest, nil
    41  }
    42  
    43  // Add a new path and it's file metadata to the collection.
    44  func (z ZipFileManifest) Add(entry string, info os.FileInfo) {
    45  	z[entry] = info
    46  }
    47  
    48  // GlobMatch returns the path keys to files (not directories) that match the given value(s).
    49  func (z ZipFileManifest) GlobMatch(caseInsensitive bool, patterns ...string) []string {
    50  	uniqueMatches := strset.New()
    51  
    52  	for _, pattern := range patterns {
    53  		for entry := range z {
    54  			fileInfo := z[entry]
    55  			if fileInfo != nil && fileInfo.IsDir() {
    56  				continue
    57  			}
    58  
    59  			// We want to match globs as if entries begin with a leading slash (akin to an absolute path)
    60  			// so that glob logic is consistent inside and outside of ZIP archives
    61  			normalizedEntry := normalizeZipEntryName(caseInsensitive, entry)
    62  
    63  			if caseInsensitive {
    64  				pattern = strings.ToLower(pattern)
    65  			}
    66  
    67  			matches, err := doublestar.Match(pattern, normalizedEntry)
    68  			if err != nil {
    69  				log.Debugf("error with match pattern '%s', including by default: %v", pattern, err)
    70  				matches = true
    71  			}
    72  			if matches {
    73  				uniqueMatches.Add(entry)
    74  			}
    75  		}
    76  	}
    77  
    78  	results := uniqueMatches.List()
    79  	sort.Strings(results)
    80  
    81  	return results
    82  }
    83  
    84  // normalizeZipEntryName takes the given path entry and ensures it is prefixed with "/".
    85  func normalizeZipEntryName(caseInsensitive bool, entry string) string {
    86  	if caseInsensitive {
    87  		entry = strings.ToLower(entry)
    88  	}
    89  	if !strings.HasPrefix(entry, "/") {
    90  		return "/" + entry
    91  	}
    92  
    93  	return entry
    94  }