mirror of
https://github.com/anchore/syft.git
synced 2026-11-02 15:36:41 +01:00
Add ability to scan snaps (as a source) (#3929)
This commit is contained in:
@@ -14,7 +14,7 @@ var _ file.Resolver = (*Directory)(nil)
|
||||
|
||||
// Directory implements path and content access for the directory data source.
|
||||
type Directory struct {
|
||||
filetreeResolver
|
||||
FiletreeResolver
|
||||
path string
|
||||
indexer *directoryIndexer
|
||||
}
|
||||
@@ -39,10 +39,11 @@ func newFromDirectoryWithoutIndex(root string, base string, pathFilters ...PathI
|
||||
|
||||
return &Directory{
|
||||
path: cleanRoot,
|
||||
filetreeResolver: filetreeResolver{
|
||||
chroot: *chroot,
|
||||
tree: filetree.New(),
|
||||
index: filetree.NewIndex(),
|
||||
FiletreeResolver: FiletreeResolver{
|
||||
Chroot: *chroot,
|
||||
Tree: filetree.New(),
|
||||
Index: filetree.NewIndex(),
|
||||
Opener: nativeOSFileOpener,
|
||||
},
|
||||
indexer: newDirectoryIndexer(cleanRoot, cleanBase, pathFilters...),
|
||||
}, nil
|
||||
@@ -57,9 +58,9 @@ func (r *Directory) buildIndex() error {
|
||||
return err
|
||||
}
|
||||
|
||||
r.tree = tree
|
||||
r.index = index
|
||||
r.searchContext = filetree.NewSearchContext(tree, index)
|
||||
r.Tree = tree
|
||||
r.Index = index
|
||||
r.SearchContext = filetree.NewSearchContext(tree, index)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,14 +9,12 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/internal/windows"
|
||||
)
|
||||
|
||||
@@ -64,14 +62,14 @@ func (r *directoryIndexer) build() (filetree.Reader, filetree.IndexReader, error
|
||||
return r.tree, r.index, indexAllRoots(r.path, r.indexTree)
|
||||
}
|
||||
|
||||
func indexAllRoots(root string, indexer func(string, *progress.Stage) ([]string, error)) error {
|
||||
func indexAllRoots(root string, indexer func(string, *progress.AtomicStage) ([]string, error)) error {
|
||||
// why account for multiple roots? To cover cases when there is a symlink that references above the root path,
|
||||
// in which case we need to additionally index where the link resolves to. it's for this reason why the filetree
|
||||
// must be relative to the root of the filesystem (and not just relative to the given path).
|
||||
pathsToIndex := []string{root}
|
||||
fullPathsMap := map[string]struct{}{}
|
||||
|
||||
stager, prog := indexingProgress(root)
|
||||
prog := bus.StartIndexingFiles(root)
|
||||
defer prog.SetCompleted()
|
||||
loop:
|
||||
for {
|
||||
@@ -85,7 +83,7 @@ loop:
|
||||
currentPath, pathsToIndex = pathsToIndex[0], pathsToIndex[1:]
|
||||
}
|
||||
|
||||
additionalRoots, err := indexer(currentPath, stager)
|
||||
additionalRoots, err := indexer(currentPath, prog.AtomicStage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to index filesystem path=%q: %w", currentPath, err)
|
||||
}
|
||||
@@ -101,7 +99,7 @@ loop:
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *directoryIndexer) indexTree(root string, stager *progress.Stage) ([]string, error) {
|
||||
func (r *directoryIndexer) indexTree(root string, stager *progress.AtomicStage) ([]string, error) {
|
||||
log.WithFields("path", root).Trace("indexing filetree")
|
||||
|
||||
var roots []string
|
||||
@@ -144,7 +142,7 @@ func (r *directoryIndexer) indexTree(root string, stager *progress.Stage) ([]str
|
||||
|
||||
err = filepath.Walk(root,
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
stager.Current = path
|
||||
stager.Set(path)
|
||||
|
||||
newRoot, err := r.indexPath(path, info, err)
|
||||
|
||||
@@ -179,7 +177,7 @@ func isRealPath(root string) (bool, error) {
|
||||
return rootParent == realRootParent, nil
|
||||
}
|
||||
|
||||
func (r *directoryIndexer) indexBranch(root string, stager *progress.Stage) ([]string, error) {
|
||||
func (r *directoryIndexer) indexBranch(root string, stager *progress.AtomicStage) ([]string, error) {
|
||||
rootRealPath, err := filepath.EvalSymlinks(root)
|
||||
if err != nil {
|
||||
var pathErr *os.PathError
|
||||
@@ -213,7 +211,7 @@ func (r *directoryIndexer) indexBranch(root string, stager *progress.Stage) ([]s
|
||||
targetPath = p
|
||||
}
|
||||
|
||||
stager.Current = targetPath
|
||||
stager.Set(targetPath)
|
||||
|
||||
lstat, err := os.Lstat(targetPath)
|
||||
newRoot, err := r.indexPath(targetPath, lstat, err)
|
||||
@@ -492,22 +490,3 @@ func requireFileInfo(_, _ string, info os.FileInfo, _ error) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func indexingProgress(path string) (*progress.Stage, *progress.Manual) {
|
||||
stage := &progress.Stage{}
|
||||
prog := progress.NewManual(-1)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.FileIndexingStarted,
|
||||
Source: path,
|
||||
Value: struct {
|
||||
progress.Stager
|
||||
progress.Progressable
|
||||
}{
|
||||
Stager: progress.Stager(stage),
|
||||
Progressable: prog,
|
||||
},
|
||||
})
|
||||
|
||||
return stage, prog
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type indexerMock struct {
|
||||
additionalRoots map[string][]string
|
||||
}
|
||||
|
||||
func (m *indexerMock) indexer(s string, _ *progress.Stage) ([]string, error) {
|
||||
func (m *indexerMock) indexer(s string, _ *progress.AtomicStage) ([]string, error) {
|
||||
m.observedRoots = append(m.observedRoots, s)
|
||||
return m.additionalRoots[s], nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ var _ file.Resolver = (*File)(nil)
|
||||
|
||||
// File implements path and content access for the file data source.
|
||||
type File struct {
|
||||
filetreeResolver
|
||||
FiletreeResolver
|
||||
path string
|
||||
indexer *fileIndexer
|
||||
}
|
||||
@@ -29,10 +29,11 @@ func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, e
|
||||
|
||||
file := &File{
|
||||
path: path,
|
||||
filetreeResolver: filetreeResolver{
|
||||
chroot: *chroot,
|
||||
tree: filetree.New(),
|
||||
index: filetree.NewIndex(),
|
||||
FiletreeResolver: FiletreeResolver{
|
||||
Chroot: *chroot,
|
||||
Tree: filetree.New(),
|
||||
Index: filetree.NewIndex(),
|
||||
Opener: nativeOSFileOpener,
|
||||
},
|
||||
indexer: newFileIndexer(path, cleanBase, pathFilters...),
|
||||
}
|
||||
@@ -49,9 +50,9 @@ func (r *File) buildIndex() error {
|
||||
return err
|
||||
}
|
||||
|
||||
r.tree = tree
|
||||
r.index = index
|
||||
r.searchContext = filetree.NewSearchContext(tree, index)
|
||||
r.Tree = tree
|
||||
r.Index = index
|
||||
r.SearchContext = filetree.NewSearchContext(tree, index)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/internal/windows"
|
||||
)
|
||||
@@ -49,14 +50,14 @@ func (r *fileIndexer) build() (filetree.Reader, filetree.IndexReader, error) {
|
||||
|
||||
// Index file at the given path
|
||||
// A file indexer simply indexes the file and its directory.
|
||||
func index(path string, indexer func(string, *progress.Stage) error) error {
|
||||
func index(path string, indexer func(string, *progress.AtomicStage) error) error {
|
||||
// We want to index the file at the provided path and its parent directory.
|
||||
// We need to probably check that we have file access
|
||||
// We also need to determine what to do when the file itself is a symlink.
|
||||
stager, prog := indexingProgress(path)
|
||||
prog := bus.StartIndexingFiles(path)
|
||||
defer prog.SetCompleted()
|
||||
|
||||
err := indexer(path, stager)
|
||||
err := indexer(path, prog.AtomicStage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to index filesystem path=%q: %w", path, err)
|
||||
}
|
||||
@@ -70,7 +71,7 @@ func index(path string, indexer func(string, *progress.Stage) error) error {
|
||||
// permissions errors on the file at path or its parent directory will return an error.
|
||||
// Filter functions provided to the indexer are honoured, so if the path provided (or its parent
|
||||
// directory) is filtered by a filter function, an error is returned.
|
||||
func (r *fileIndexer) indexPath(path string, stager *progress.Stage) error {
|
||||
func (r *fileIndexer) indexPath(path string, stager *progress.AtomicStage) error {
|
||||
log.WithFields("path", path).Trace("indexing file path")
|
||||
|
||||
absPath, err := filepath.Abs(path)
|
||||
@@ -105,14 +106,14 @@ func (r *fileIndexer) indexPath(path string, stager *progress.Stage) error {
|
||||
return fmt.Errorf("unable to stat parent of file=%q: %w", absSymlinkFreeParent, err)
|
||||
}
|
||||
|
||||
stager.Current = absSymlinkFreeParent
|
||||
stager.Set(absSymlinkFreeParent)
|
||||
indexParentErr := r.filterAndIndex(absSymlinkFreeParent, parentFi)
|
||||
if indexParentErr != nil {
|
||||
return indexParentErr
|
||||
}
|
||||
|
||||
// We have indexed the parent successfully, now attempt to index the file.
|
||||
stager.Current = absSymlinkFreeFilePath
|
||||
stager.Set(absSymlinkFreeFilePath)
|
||||
indexFileErr := r.filterAndIndex(absSymlinkFreeFilePath, fi)
|
||||
if indexFileErr != nil {
|
||||
return indexFileErr
|
||||
|
||||
@@ -14,33 +14,46 @@ import (
|
||||
"github.com/anchore/syft/syft/internal/windows"
|
||||
)
|
||||
|
||||
type filetreeResolver struct {
|
||||
chroot ChrootContext
|
||||
tree filetree.Reader
|
||||
index filetree.IndexReader
|
||||
searchContext filetree.Searcher
|
||||
// TODO: consider making a constructor for this
|
||||
type FiletreeResolver struct {
|
||||
Chroot ChrootContext
|
||||
Tree filetree.Reader
|
||||
Index filetree.IndexReader
|
||||
SearchContext filetree.Searcher
|
||||
Opener func(stereoscopeFile.Reference) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func (r *filetreeResolver) requestPath(userPath string) (string, error) {
|
||||
return r.chroot.ToNativePath(userPath)
|
||||
func nativeOSFileOpener(ref stereoscopeFile.Reference) (io.ReadCloser, error) {
|
||||
// RealPath is posix so for windows file resolver we need to translate
|
||||
// to its true on disk path.
|
||||
filePath := string(ref.RealPath)
|
||||
if windows.HostRunningOnWindows() {
|
||||
filePath = windows.FromPosix(filePath)
|
||||
}
|
||||
|
||||
return stereoscopeFile.NewLazyReadCloser(filePath), nil
|
||||
}
|
||||
|
||||
func (r *FiletreeResolver) requestPath(userPath string) (string, error) {
|
||||
return r.Chroot.ToNativePath(userPath)
|
||||
}
|
||||
|
||||
// responsePath takes a path from the underlying fs domain and converts it to a path that is relative to the root of the file resolver.
|
||||
func (r filetreeResolver) responsePath(path string) string {
|
||||
return r.chroot.ToChrootPath(path)
|
||||
func (r FiletreeResolver) responsePath(path string) string {
|
||||
return r.Chroot.ToChrootPath(path)
|
||||
}
|
||||
|
||||
// HasPath indicates if the given path exists in the underlying source.
|
||||
func (r *filetreeResolver) HasPath(userPath string) bool {
|
||||
func (r *FiletreeResolver) HasPath(userPath string) bool {
|
||||
requestPath, err := r.requestPath(userPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return r.tree.HasPath(stereoscopeFile.Path(requestPath))
|
||||
return r.Tree.HasPath(stereoscopeFile.Path(requestPath))
|
||||
}
|
||||
|
||||
// FilesByPath returns all file.References that match the given paths from the file index.
|
||||
func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, error) {
|
||||
func (r FiletreeResolver) FilesByPath(userPaths ...string) ([]file.Location, error) {
|
||||
var references = make([]file.Location, 0)
|
||||
|
||||
for _, userPath := range userPaths {
|
||||
@@ -51,7 +64,7 @@ func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
|
||||
}
|
||||
|
||||
// we should be resolving symlinks and preserving this information as a AccessPath to the real file
|
||||
ref, err := r.searchContext.SearchByPath(userStrPath, filetree.FollowBasenameLinks)
|
||||
ref, err := r.SearchContext.SearchByPath(userStrPath, filetree.FollowBasenameLinks)
|
||||
if err != nil {
|
||||
log.Tracef("unable to evaluate symlink for path=%q : %+v", userPath, err)
|
||||
continue
|
||||
@@ -61,7 +74,7 @@ func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
|
||||
continue
|
||||
}
|
||||
|
||||
entry, err := r.index.Get(*ref.Reference)
|
||||
entry, err := r.Index.Get(*ref.Reference)
|
||||
if err != nil {
|
||||
log.Warnf("unable to get file by path=%q : %+v", userPath, err)
|
||||
continue
|
||||
@@ -90,12 +103,12 @@ func (r filetreeResolver) FilesByPath(userPaths ...string) ([]file.Location, err
|
||||
return references, nil
|
||||
}
|
||||
|
||||
func (r filetreeResolver) requestGlob(pattern string) (string, error) {
|
||||
return r.chroot.ToNativeGlob(pattern)
|
||||
func (r FiletreeResolver) requestGlob(pattern string) (string, error) {
|
||||
return r.Chroot.ToNativeGlob(pattern)
|
||||
}
|
||||
|
||||
// FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image.
|
||||
func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
|
||||
func (r FiletreeResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
|
||||
uniqueFileIDs := stereoscopeFile.NewFileReferenceSet()
|
||||
uniqueLocations := make([]file.Location, 0)
|
||||
|
||||
@@ -104,7 +117,7 @@ func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refVias, err := r.searchContext.SearchByGlob(requestGlob, filetree.FollowBasenameLinks)
|
||||
refVias, err := r.SearchContext.SearchByGlob(requestGlob, filetree.FollowBasenameLinks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -112,7 +125,7 @@ func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, erro
|
||||
if !refVia.HasReference() || uniqueFileIDs.Contains(*refVia.Reference) {
|
||||
continue
|
||||
}
|
||||
entry, err := r.index.Get(*refVia.Reference)
|
||||
entry, err := r.Index.Get(*refVia.Reference)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get file metadata for reference %s: %w", refVia.RealPath, err)
|
||||
}
|
||||
@@ -137,7 +150,7 @@ func (r filetreeResolver) FilesByGlob(patterns ...string) ([]file.Location, erro
|
||||
|
||||
// RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference.
|
||||
// This is helpful when attempting to find a file that is in the same layer or lower as another file.
|
||||
func (r *filetreeResolver) RelativeFileByPath(_ file.Location, path string) *file.Location {
|
||||
func (r *FiletreeResolver) RelativeFileByPath(_ file.Location, path string) *file.Location {
|
||||
paths, err := r.FilesByPath(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -151,12 +164,12 @@ func (r *filetreeResolver) RelativeFileByPath(_ file.Location, path string) *fil
|
||||
|
||||
// FileContentsByLocation fetches file contents for a single file reference relative to a directory.
|
||||
// If the path does not exist an error is returned.
|
||||
func (r filetreeResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
|
||||
func (r FiletreeResolver) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
|
||||
if location.RealPath == "" {
|
||||
return nil, errors.New("empty path given")
|
||||
}
|
||||
|
||||
entry, err := r.index.Get(location.Reference())
|
||||
entry, err := r.Index.Get(location.Reference())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -166,21 +179,14 @@ func (r filetreeResolver) FileContentsByLocation(location file.Location) (io.Rea
|
||||
return nil, fmt.Errorf("cannot read contents of non-file %q", location.Reference().RealPath)
|
||||
}
|
||||
|
||||
// RealPath is posix so for windows file resolver we need to translate
|
||||
// to its true on disk path.
|
||||
filePath := string(location.Reference().RealPath)
|
||||
if windows.HostRunningOnWindows() {
|
||||
filePath = windows.FromPosix(filePath)
|
||||
}
|
||||
|
||||
return stereoscopeFile.NewLazyReadCloser(filePath), nil
|
||||
return r.Opener(location.Reference())
|
||||
}
|
||||
|
||||
func (r *filetreeResolver) AllLocations(ctx context.Context) <-chan file.Location {
|
||||
func (r *FiletreeResolver) AllLocations(ctx context.Context) <-chan file.Location {
|
||||
results := make(chan file.Location)
|
||||
go func() {
|
||||
defer close(results)
|
||||
for _, ref := range r.tree.AllFiles(stereoscopeFile.AllTypes()...) {
|
||||
for _, ref := range r.Tree.AllFiles(stereoscopeFile.AllTypes()...) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
@@ -192,8 +198,8 @@ func (r *filetreeResolver) AllLocations(ctx context.Context) <-chan file.Locatio
|
||||
return results
|
||||
}
|
||||
|
||||
func (r *filetreeResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
|
||||
entry, err := r.index.Get(location.Reference())
|
||||
func (r *FiletreeResolver) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
|
||||
entry, err := r.Index.Get(location.Reference())
|
||||
if err != nil {
|
||||
return file.Metadata{}, fmt.Errorf("location: %+v : %w", location, os.ErrNotExist)
|
||||
}
|
||||
@@ -201,11 +207,11 @@ func (r *filetreeResolver) FileMetadataByLocation(location file.Location) (file.
|
||||
return entry.Metadata, nil
|
||||
}
|
||||
|
||||
func (r *filetreeResolver) FilesByMIMEType(types ...string) ([]file.Location, error) {
|
||||
func (r *FiletreeResolver) FilesByMIMEType(types ...string) ([]file.Location, error) {
|
||||
uniqueFileIDs := stereoscopeFile.NewFileReferenceSet()
|
||||
uniqueLocations := make([]file.Location, 0)
|
||||
|
||||
refVias, err := r.searchContext.SearchByMIMEType(types...)
|
||||
refVias, err := r.SearchContext.SearchByMIMEType(types...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -971,7 +971,7 @@ func Test_directoryResolver_FileContentsByLocation(t *testing.T) {
|
||||
r, err := NewFromDirectory(".", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
exists, existingPath, err := r.tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
|
||||
exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
|
||||
require.True(t, exists)
|
||||
require.NoError(t, err)
|
||||
require.True(t, existingPath.HasReference())
|
||||
@@ -1271,7 +1271,7 @@ func TestDirectoryResolver_FilesContents_errorOnDirRequest(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
for loc := range resolver.AllLocations(ctx) {
|
||||
entry, err := resolver.index.Get(loc.Reference())
|
||||
entry, err := resolver.Index.Get(loc.Reference())
|
||||
require.NoError(t, err)
|
||||
if entry.Metadata.IsDir() {
|
||||
dirLoc = &loc
|
||||
@@ -1500,7 +1500,7 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) {
|
||||
r, err := NewFromFile(parentPath, filePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
exists, existingPath, err := r.tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
|
||||
exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt")))
|
||||
require.True(t, exists)
|
||||
require.NoError(t, err)
|
||||
require.True(t, existingPath.HasReference())
|
||||
@@ -1553,7 +1553,7 @@ func TestFileResolver_AllLocations_errorOnDirRequest(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
for loc := range resolver.AllLocations(ctx) {
|
||||
entry, err := resolver.index.Get(loc.Reference())
|
||||
entry, err := resolver.Index.Get(loc.Reference())
|
||||
require.NoError(t, err)
|
||||
if dirLoc == nil && entry.Metadata.IsDir() {
|
||||
dirLoc = &loc
|
||||
|
||||
@@ -6,5 +6,5 @@ import "github.com/anchore/syft/syft/source"
|
||||
|
||||
// AllTypes returns a list of all source metadata types that syft supports (that are represented in the source.Description.Metadata field).
|
||||
func AllTypes() []any {
|
||||
return []any{source.DirectoryMetadata{}, source.FileMetadata{}, source.ImageMetadata{}}
|
||||
return []any{source.DirectoryMetadata{}, source.FileMetadata{}, source.ImageMetadata{}, source.SnapMetadata{}}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ var jsonNameFromType = map[reflect.Type][]string{
|
||||
reflect.TypeOf(source.DirectoryMetadata{}): {"directory", "dir"},
|
||||
reflect.TypeOf(source.FileMetadata{}): {"file"},
|
||||
reflect.TypeOf(source.ImageMetadata{}): {"image"},
|
||||
reflect.TypeOf(source.SnapMetadata{}): {"snap"},
|
||||
}
|
||||
|
||||
func AllTypeNames() []string {
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/diskfs/go-diskfs/filesystem/squashfs"
|
||||
|
||||
macho "github.com/anchore/go-macholibre"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -48,12 +51,15 @@ func GetUnionReader(readerCloser io.ReadCloser) (UnionReader, error) {
|
||||
// file.LocationReadCloser embeds a ReadCloser, which is likely
|
||||
// to implement UnionReader. Check whether the embedded read closer
|
||||
// implements UnionReader, and just return that if so.
|
||||
r, ok := readerCloser.(file.LocationReadCloser)
|
||||
if ok {
|
||||
ur, ok := r.ReadCloser.(UnionReader)
|
||||
if ok {
|
||||
return ur, nil
|
||||
}
|
||||
|
||||
if r, ok := readerCloser.(file.LocationReadCloser); ok {
|
||||
return GetUnionReader(r.ReadCloser)
|
||||
}
|
||||
|
||||
if r, ok := readerCloser.(*squashfs.File); ok {
|
||||
// seeking is implemented, but not io.ReaderAt. Lets wrap it to prevent from degrading performance
|
||||
// by copying all data.
|
||||
return newReaderAtAdapter(r), nil
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(readerCloser)
|
||||
@@ -75,3 +81,58 @@ func GetUnionReader(readerCloser io.ReadCloser) (UnionReader, error) {
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
type readerAtAdapter struct {
|
||||
io.ReadSeekCloser
|
||||
mu *sync.Mutex
|
||||
}
|
||||
|
||||
func newReaderAtAdapter(rs io.ReadSeekCloser) UnionReader {
|
||||
return &readerAtAdapter{
|
||||
ReadSeekCloser: rs,
|
||||
mu: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *readerAtAdapter) Read(p []byte) (n int, err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.ReadSeekCloser.Read(p)
|
||||
}
|
||||
|
||||
func (r *readerAtAdapter) Seek(offset int64, whence int) (int64, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.ReadSeekCloser.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (r *readerAtAdapter) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
currentPos, err := r.ReadSeekCloser.Seek(0, io.SeekCurrent) // save current pos
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_, err = r.ReadSeekCloser.Seek(off, io.SeekStart) // seek to absolute position `off`
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err = r.ReadSeekCloser.Read(p) // read from that absolute position
|
||||
|
||||
// restore the position for the stateful read/seek operations
|
||||
if restoreErr := r.restorePosition(currentPos); restoreErr != nil {
|
||||
if err == nil {
|
||||
err = restoreErr
|
||||
}
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *readerAtAdapter) restorePosition(pos int64) error {
|
||||
_, err := r.ReadSeekCloser.Seek(pos, io.SeekStart)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package unionreader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -61,3 +63,275 @@ func Test_getUnionReader_fileLocationReadCloser(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, p, ur)
|
||||
}
|
||||
|
||||
func TestReaderAtAdapter_ReadAt(t *testing.T) {
|
||||
testData := "Hello, World! This is a test string for ReadAt."
|
||||
|
||||
t.Run("basic functionality", func(t *testing.T) {
|
||||
reader := newReadSeekCloser(strings.NewReader(testData))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
offset int64
|
||||
length int
|
||||
expected string
|
||||
}{
|
||||
{name: "read from beginning", offset: 0, length: 5, expected: "Hello"},
|
||||
{name: "read from middle", offset: 7, length: 5, expected: "World"},
|
||||
{name: "read from end", offset: int64(len(testData) - 4), length: 4, expected: "dAt."},
|
||||
{name: "read single character", offset: 12, length: 1, expected: "!"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf := make([]byte, tt.length)
|
||||
n, err := adapter.ReadAt(buf, tt.offset)
|
||||
|
||||
if err != nil && err != io.EOF {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := string(buf[:n])
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %q, got %q", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge cases", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
offset int64
|
||||
bufSize int
|
||||
expectedN int
|
||||
expectedErr error
|
||||
expectedStr string
|
||||
}{
|
||||
{
|
||||
name: "beyond EOF",
|
||||
data: "Hello",
|
||||
offset: 10,
|
||||
bufSize: 5,
|
||||
expectedN: 0,
|
||||
expectedErr: io.EOF,
|
||||
expectedStr: "",
|
||||
},
|
||||
{
|
||||
name: "partial read",
|
||||
data: "Hello",
|
||||
offset: 2,
|
||||
bufSize: 10,
|
||||
expectedN: 3,
|
||||
expectedErr: nil,
|
||||
expectedStr: "llo",
|
||||
},
|
||||
{
|
||||
name: "empty buffer",
|
||||
data: "Hello",
|
||||
offset: 0,
|
||||
bufSize: 0,
|
||||
expectedN: 0,
|
||||
expectedErr: nil,
|
||||
expectedStr: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := newReadSeekCloser(strings.NewReader(tt.data))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
buf := make([]byte, tt.bufSize)
|
||||
n, err := adapter.ReadAt(buf, tt.offset)
|
||||
|
||||
if err != tt.expectedErr {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedErr, err)
|
||||
}
|
||||
|
||||
if n != tt.expectedN {
|
||||
t.Errorf("Expected %d bytes read, got %d", tt.expectedN, n)
|
||||
}
|
||||
|
||||
result := string(buf[:n])
|
||||
if result != tt.expectedStr {
|
||||
t.Errorf("Expected %q, got %q", tt.expectedStr, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple reads from same position", func(t *testing.T) {
|
||||
reader := newReadSeekCloser(strings.NewReader(testData))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
// read the same data multiple times
|
||||
for i := 0; i < 3; i++ {
|
||||
buf := make([]byte, 5)
|
||||
n, err := adapter.ReadAt(buf, 7)
|
||||
|
||||
if err != nil && err != io.EOF {
|
||||
t.Fatalf("ReadAt %d failed: %v", i, err)
|
||||
}
|
||||
|
||||
result := string(buf[:n])
|
||||
if result != "World" {
|
||||
t.Errorf("ReadAt %d: expected 'World', got %q", i, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("concurrent access", func(t *testing.T) {
|
||||
td := "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
reader := newReadSeekCloser(strings.NewReader(td))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
const numGoroutines = 10
|
||||
const numReads = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make(chan bool, numGoroutines*numReads)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < numReads; j++ {
|
||||
offset := int64(goroutineID % len(td))
|
||||
buf := make([]byte, 1)
|
||||
|
||||
n, err := adapter.ReadAt(buf, offset)
|
||||
if err != nil && err != io.EOF {
|
||||
results <- false
|
||||
return
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
expected := td[offset]
|
||||
if buf[0] != expected {
|
||||
results <- false
|
||||
return
|
||||
}
|
||||
}
|
||||
results <- true
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
successCount := 0
|
||||
totalCount := 0
|
||||
for success := range results {
|
||||
totalCount++
|
||||
if success {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
if successCount != totalCount {
|
||||
t.Errorf("Concurrent reads failed: %d/%d successful", successCount, totalCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReaderAtAdapter_PositionHandling(t *testing.T) {
|
||||
testData := "Hello, World!"
|
||||
|
||||
t.Run("preserves position after ReadAt", func(t *testing.T) {
|
||||
reader := newReadSeekCloser(strings.NewReader(testData))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
// move to a specific position
|
||||
initialPos := int64(7)
|
||||
_, err := adapter.Seek(initialPos, io.SeekStart)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to seek: %v", err)
|
||||
}
|
||||
|
||||
// read using ReadAt
|
||||
buf := make([]byte, 5)
|
||||
_, err = adapter.ReadAt(buf, 0)
|
||||
if err != nil && err != io.EOF {
|
||||
t.Fatalf("ReadAt failed: %v", err)
|
||||
}
|
||||
|
||||
// verify position is preserved
|
||||
currentPos, err := adapter.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current position: %v", err)
|
||||
}
|
||||
|
||||
if currentPos != initialPos {
|
||||
t.Errorf("Position not preserved. Expected %d, got %d", initialPos, currentPos)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not affect regular reads", func(t *testing.T) {
|
||||
reader := newReadSeekCloser(strings.NewReader(testData))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
// read first few bytes normally
|
||||
normalBuf := make([]byte, 5)
|
||||
n, err := adapter.Read(normalBuf)
|
||||
if err != nil {
|
||||
t.Fatalf("Normal read failed: %v", err)
|
||||
}
|
||||
if string(normalBuf[:n]) != "Hello" {
|
||||
t.Errorf("Expected 'Hello', got %q", string(normalBuf[:n]))
|
||||
}
|
||||
|
||||
// use ReadAt to read from a different position
|
||||
readAtBuf := make([]byte, 5)
|
||||
n, err = adapter.ReadAt(readAtBuf, 7)
|
||||
if err != nil && err != io.EOF {
|
||||
t.Fatalf("ReadAt failed: %v", err)
|
||||
}
|
||||
if string(readAtBuf[:n]) != "World" {
|
||||
t.Errorf("Expected 'World', got %q", string(readAtBuf[:n]))
|
||||
}
|
||||
|
||||
// continue normal reading - should pick up where we left off
|
||||
continueBuf := make([]byte, 2)
|
||||
n, err = adapter.Read(continueBuf)
|
||||
if err != nil {
|
||||
t.Fatalf("Continue read failed: %v", err)
|
||||
}
|
||||
if string(continueBuf[:n]) != ", " {
|
||||
t.Errorf("Expected ', ', got %q", string(continueBuf[:n]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReaderAtAdapter_Close(t *testing.T) {
|
||||
reader := newReadSeekCloser(bytes.NewReader([]byte("test data")))
|
||||
adapter := newReaderAtAdapter(reader)
|
||||
|
||||
// test that adapter can be closed
|
||||
err := adapter.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
if !reader.closed {
|
||||
t.Error("Underlying reader was not closed")
|
||||
}
|
||||
}
|
||||
|
||||
type readSeekCloser struct {
|
||||
io.ReadSeeker
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newReadSeekCloser(rs io.ReadSeeker) *readSeekCloser {
|
||||
return &readSeekCloser{ReadSeeker: rs}
|
||||
}
|
||||
|
||||
func (r *readSeekCloser) Close() error {
|
||||
r.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user