mirror of
https://github.com/anchore/syft.git
synced 2026-11-03 16:06:36 +01:00
chore: simplify and cleanup
Signed-off-by: Keith Zantow <keith.zantow@anchore.com>
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
|
||||
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||
syftindex "github.com/anchore/syft/internal/index"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
// Index is a file.Resolver over one extracted archive's entries.
|
||||
//
|
||||
// Paths are archive-relative. Nearly every cataloger glob is `**/<name>` or `**/*<suffix>`, so files
|
||||
// are indexed by base name and a pattern whose last segment fixes a name, prefix or suffix is
|
||||
// narrowed to those files before doublestar decides; any other pattern is matched against every file.
|
||||
type Index struct {
|
||||
fileSystemID string
|
||||
archivePath string
|
||||
store *EntryStore
|
||||
|
||||
// byPath holds every file and directory by rooted path, including directories the archive never
|
||||
// listed but that entry paths imply
|
||||
byPath map[string]*node
|
||||
|
||||
// files holds every file the archive listed, in path order
|
||||
files []*node
|
||||
|
||||
// names indexes files by base name
|
||||
names syftindex.PrefixSuffix[[]*node]
|
||||
}
|
||||
|
||||
type node struct {
|
||||
path string // rooted, e.g. "/META-INF/MANIFEST.MF"
|
||||
isDir bool
|
||||
entry *Entry // nil for a directory the archive never listed
|
||||
metadata file.Metadata
|
||||
|
||||
// target is the node whose entry holds this node's content: itself for a regular file, the
|
||||
// resolved node for a link, and nil for a link that leads nowhere inside this archive
|
||||
target *node
|
||||
}
|
||||
|
||||
var _ file.Resolver = (*Index)(nil)
|
||||
|
||||
// NewIndex builds a resolver over the store's entries whose locations carry the given fileSystemID
|
||||
// and archivePath.
|
||||
func NewIndex(store *EntryStore, fileSystemID, archivePath string) *Index {
|
||||
r := &Index{
|
||||
fileSystemID: fileSystemID,
|
||||
archivePath: archivePath,
|
||||
store: store,
|
||||
byPath: map[string]*node{"/": {path: "/", isDir: true}},
|
||||
}
|
||||
for _, entry := range store.Entries() {
|
||||
r.add(entry)
|
||||
}
|
||||
sort.Slice(r.files, func(i, j int) bool { return r.files[i].path < r.files[j].path })
|
||||
for _, n := range r.byPath {
|
||||
n.target = r.linkTarget(n)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// add places an entry at its path, creating the directories the path implies. The first entry at a
|
||||
// path wins.
|
||||
func (r *Index) add(entry *Entry) {
|
||||
entryPath := path.Clean("/" + entry.Header.Name)
|
||||
if entryPath == "/" {
|
||||
return
|
||||
}
|
||||
for i := 1; i < len(entryPath); i++ {
|
||||
if entryPath[i] == '/' {
|
||||
r.nodeAt(entryPath[:i], true)
|
||||
}
|
||||
}
|
||||
|
||||
n := r.nodeAt(entryPath, entry.Header.FileInfo().IsDir())
|
||||
if n.entry != nil {
|
||||
return
|
||||
}
|
||||
n.entry = entry
|
||||
n.metadata = r.metadataOf(entry, entryPath)
|
||||
if n.isDir {
|
||||
return
|
||||
}
|
||||
r.files = append(r.files, n)
|
||||
r.names.Update(path.Base(entryPath), func(current *syftindex.Node[[]*node]) []*node {
|
||||
return append(current.Value(), n)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Index) nodeAt(p string, isDir bool) *node {
|
||||
if n, ok := r.byPath[p]; ok {
|
||||
return n
|
||||
}
|
||||
n := &node{path: p, isDir: isDir}
|
||||
r.byPath[p] = n
|
||||
return n
|
||||
}
|
||||
|
||||
// maxLinkHops bounds a chain of links pointing at links, which is also what ends a cycle.
|
||||
const maxLinkHops = 8
|
||||
|
||||
func (r *Index) linkTarget(n *node) *node {
|
||||
for hops := 0; hops <= maxLinkHops; hops++ {
|
||||
if n.entry == nil || n.isDir {
|
||||
return nil
|
||||
}
|
||||
if n.entry.Header.Linkname == "" {
|
||||
return n
|
||||
}
|
||||
next, ok := r.byPath[linkDestination(n.path, n.entry.Header)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
n = next
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// linkDestination is the rooted path a link points at inside the archive. A hard link names its
|
||||
// target from the archive root, as does an absolute symlink, so "/etc/passwd" is an entry of this
|
||||
// archive and never the host's. A relative symlink is joined onto the link's own directory, and
|
||||
// cleaning against the root keeps "../../x" inside the archive.
|
||||
func linkDestination(linkPath string, hdr tar.Header) string {
|
||||
if hdr.Typeflag == tar.TypeLink || path.IsAbs(hdr.Linkname) {
|
||||
return path.Join("/", hdr.Linkname)
|
||||
}
|
||||
return path.Join(path.Dir(linkPath), hdr.Linkname)
|
||||
}
|
||||
|
||||
func (r *Index) metadataOf(entry *Entry, entryPath string) file.Metadata {
|
||||
metadata := stereoscopeFile.NewMetadata(entry.Header, r.store.Open(entry))
|
||||
// NewMetadata copies the header and points FileInfo into the copy; share the one the store holds
|
||||
metadata.FileInfo = entry.Header.FileInfo()
|
||||
metadata.Path = reportedPath(entryPath)
|
||||
if entry.Header.Linkname != "" {
|
||||
metadata.LinkDestination = reportedPath(linkDestination(entryPath, entry.Header))
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
// reportedPath is how paths leave the resolver: archive-relative with no leading slash.
|
||||
func reportedPath(rooted string) string {
|
||||
return strings.TrimPrefix(rooted, "/")
|
||||
}
|
||||
|
||||
func rootedPath(userPath string) string {
|
||||
return path.Clean("/" + userPath)
|
||||
}
|
||||
|
||||
func (r *Index) coordinates(n *node) file.Coordinates {
|
||||
return file.Coordinates{
|
||||
RealPath: reportedPath(n.path),
|
||||
FileSystemID: r.fileSystemID,
|
||||
ArchivePath: r.archivePath,
|
||||
}
|
||||
}
|
||||
|
||||
// locationOf names a node by its own path.
|
||||
func (r *Index) locationOf(n *node) file.Location {
|
||||
return file.NewLocationFromCoordinates(r.coordinates(n))
|
||||
}
|
||||
|
||||
// resolvedLocation names the file behind a node: a link is reported at its target's path with the
|
||||
// link's own path as the access path, and a link leading nowhere is reported as itself.
|
||||
func (r *Index) resolvedLocation(n *node) file.Location {
|
||||
if n.target == nil || n.target == n {
|
||||
return r.locationOf(n)
|
||||
}
|
||||
return file.NewVirtualLocationFromCoordinates(r.coordinates(n.target), reportedPath(n.path))
|
||||
}
|
||||
|
||||
func (r *Index) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
|
||||
n, ok := r.byPath[rootedPath(location.RealPath)]
|
||||
if !ok || n.entry == nil {
|
||||
return nil, fmt.Errorf("no archive entry for path=%q", location.RealPath)
|
||||
}
|
||||
return entryReader{r.store.Open(n.entry)}, nil
|
||||
}
|
||||
|
||||
func (r *Index) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
|
||||
n, ok := r.byPath[rootedPath(location.RealPath)]
|
||||
if !ok {
|
||||
return file.Metadata{}, fmt.Errorf("no archive entry for path=%q", location.RealPath)
|
||||
}
|
||||
return n.metadata, nil
|
||||
}
|
||||
|
||||
func (r *Index) HasPath(userPath string) bool {
|
||||
_, ok := r.byPath[rootedPath(userPath)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *Index) FilesByPath(paths ...string) ([]file.Location, error) {
|
||||
var out []file.Location
|
||||
seen := map[*node]struct{}{}
|
||||
for _, userPath := range paths {
|
||||
n, ok := r.byPath[rootedPath(userPath)]
|
||||
if !ok || n.isDir || n.entry == nil {
|
||||
continue
|
||||
}
|
||||
if _, dup := seen[n]; dup {
|
||||
continue
|
||||
}
|
||||
seen[n] = struct{}{}
|
||||
out = append(out, r.resolvedLocation(n))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Index) FilesByGlob(patterns ...string) ([]file.Location, error) {
|
||||
matched := map[*node]struct{}{}
|
||||
for _, pattern := range patterns {
|
||||
pattern = strings.TrimLeft(pattern, "/")
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
for _, n := range r.globCandidates(pattern) {
|
||||
ok, err := doublestar.Match(pattern, reportedPath(n.path))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid glob %q: %w", pattern, err)
|
||||
}
|
||||
if ok {
|
||||
matched[n] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var out []file.Location
|
||||
for _, n := range onePathPerFile(matched) {
|
||||
out = append(out, r.resolvedLocation(n))
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].RealPath != out[j].RealPath {
|
||||
return out[i].RealPath < out[j].RealPath
|
||||
}
|
||||
return out[i].AccessPath < out[j].AccessPath
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// globMetacharacters are the glob metacharacters other than `*` itself.
|
||||
const globMetacharacters = `?[]{}\`
|
||||
|
||||
// globCandidates narrows the files a pattern can match by its last segment: an exact name, `*suffix`
|
||||
// or `prefix*` is looked up in the name index. Any other shape has every file as a candidate.
|
||||
func (r *Index) globCandidates(pattern string) []*node {
|
||||
last := pattern[strings.LastIndexByte(pattern, '/')+1:]
|
||||
if strings.ContainsAny(last, globMetacharacters) || strings.Count(last, "*") > 1 {
|
||||
return r.files
|
||||
}
|
||||
switch star := strings.IndexByte(last, '*'); {
|
||||
case star < 0:
|
||||
return r.names.Get(last)
|
||||
case star == 0:
|
||||
return flatten(r.names.BySuffix(last[1:]))
|
||||
case star == len(last)-1:
|
||||
return flatten(r.names.ByPrefix(last[:star]))
|
||||
}
|
||||
return r.files
|
||||
}
|
||||
|
||||
func flatten(groups [][]*node) []*node {
|
||||
var out []*node
|
||||
for _, group := range groups {
|
||||
out = append(out, group...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// onePathPerFile keeps one matched path per file. Several paths can name the same content, such as
|
||||
// a link and its target, and a resolver answers with one: the file's own path when it matched,
|
||||
// otherwise the lowest-sorting link. A link leading nowhere stands as its own file.
|
||||
func onePathPerFile(matched map[*node]struct{}) []*node {
|
||||
best := map[*node]*node{}
|
||||
for n := range matched {
|
||||
key := n.target
|
||||
if key == nil {
|
||||
key = n
|
||||
}
|
||||
current, seen := best[key]
|
||||
if !seen || n == key || (current != key && n.path < current.path) {
|
||||
best[key] = n
|
||||
}
|
||||
}
|
||||
out := make([]*node, 0, len(best))
|
||||
for _, n := range best {
|
||||
out = append(out, n)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Index) FilesByMIMEType(types ...string) ([]file.Location, error) {
|
||||
wanted := map[string]struct{}{}
|
||||
for _, t := range types {
|
||||
wanted[t] = struct{}{}
|
||||
}
|
||||
var out []file.Location
|
||||
for _, n := range r.files {
|
||||
// only a file holding bytes has a type of its own; a link's target answers for itself
|
||||
if n.target != n {
|
||||
continue
|
||||
}
|
||||
if _, ok := wanted[n.metadata.MIMEType]; ok {
|
||||
out = append(out, r.locationOf(n))
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RelativeFileByPath resolves against the archive root, the only frame an archive has.
|
||||
func (r *Index) RelativeFileByPath(_ file.Location, userPath string) *file.Location {
|
||||
locations, _ := r.FilesByPath(userPath)
|
||||
if len(locations) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &locations[0]
|
||||
}
|
||||
|
||||
func (r *Index) AllLocations(ctx context.Context) <-chan file.Location {
|
||||
out := make(chan file.Location)
|
||||
go func() {
|
||||
defer close(out)
|
||||
for _, n := range r.files {
|
||||
select {
|
||||
case out <- r.locationOf(n):
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return out
|
||||
}
|
||||
|
||||
// entryReader gives an entry's reader the Close the resolver interface requires. The store owns the
|
||||
// bytes, so there is nothing to close.
|
||||
type entryReader struct {
|
||||
ReaderAtSeeker
|
||||
}
|
||||
|
||||
func (entryReader) Close() error { return nil }
|
||||
Reference in New Issue
Block a user