diff --git a/cmd/syft/internal/commands/attest.go b/cmd/syft/internal/commands/attest.go index 417aa44d2..526303edb 100644 --- a/cmd/syft/internal/commands/attest.go +++ b/cmd/syft/internal/commands/attest.go @@ -253,7 +253,6 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt } src, err := getSource(ctx, opts, userInput, stereoscope.RegistryTag) - if err != nil { return nil, err } diff --git a/cmd/syft/internal/commands/scan.go b/cmd/syft/internal/commands/scan.go index c44b66d19..bc510948a 100644 --- a/cmd/syft/internal/commands/scan.go +++ b/cmd/syft/internal/commands/scan.go @@ -185,7 +185,6 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use } src, err := getSource(ctx, &opts.Catalog, userInput, sources...) - if err != nil { return err } diff --git a/cmd/syft/internal/test/integration/catalog_packages_test.go b/cmd/syft/internal/test/integration/catalog_packages_test.go index 9fc65fdb0..c11be21c5 100644 --- a/cmd/syft/internal/test/integration/catalog_packages_test.go +++ b/cmd/syft/internal/test/integration/catalog_packages_test.go @@ -25,7 +25,6 @@ func BenchmarkImagePackageCatalogers(b *testing.B) { // get the source object for the image theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive")) require.NoError(b, err) - b.Cleanup(func() { require.NoError(b, theSource.Close()) }) diff --git a/cmd/syft/internal/test/integration/utils_test.go b/cmd/syft/internal/test/integration/utils_test.go index 978b8a7cc..7781fd89f 100644 --- a/cmd/syft/internal/test/integration/utils_test.go +++ b/cmd/syft/internal/test/integration/utils_test.go @@ -38,11 +38,11 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s // get the source to build an SBOM against theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive")) require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, theSource.Close()) }) + // build the SBOM s, err := syft.CreateSBOM(context.Background(), theSource, cfg) require.NoError(t, err) @@ -66,7 +66,7 @@ func catalogDirectory(t *testing.T, dir string, catalogerSelection ...string) (s func catalogDirectoryWithConfig(t *testing.T, dir string, cfg *syft.CreateSBOMConfig) (sbom.SBOM, source.Source) { cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag) - // get the source to build an sbom against + // get the source to build an SBOM against theSource, err := syft.GetSource(context.Background(), dir, syft.DefaultGetSourceConfig().WithSources("dir")) require.NoError(t, err) t.Cleanup(func() { diff --git a/examples/create_custom_sbom/main.go b/examples/create_custom_sbom/main.go index 459bb632c..5de359d89 100644 --- a/examples/create_custom_sbom/main.go +++ b/examples/create_custom_sbom/main.go @@ -23,6 +23,7 @@ const defaultImage = "alpine:3.19" func main() { // automagically get a source.Source for arbitrary string input src := getSource(imageReference()) + defer src.Close() // will catalog the given source and return a SBOM keeping in mind several configurable options sbom := getSBOM(src) @@ -46,7 +47,6 @@ func getSource(input string) source.Source { fmt.Println("detecting source type for input:", input, "...") src, err := syft.GetSource(context.Background(), input, nil) - if err != nil { panic(err) } diff --git a/examples/create_simple_sbom/main.go b/examples/create_simple_sbom/main.go index 788e8e530..59f365bfd 100644 --- a/examples/create_simple_sbom/main.go +++ b/examples/create_simple_sbom/main.go @@ -19,6 +19,7 @@ const defaultImage = "alpine:3.19" func main() { // automagically get a source.Source for arbitrary string input src := getSource(imageReference()) + defer src.Close() // catalog the given source and return a SBOM sbom := getSBOM(src) @@ -40,7 +41,6 @@ func imageReference() string { func getSource(input string) source.Source { src, err := syft.GetSource(context.Background(), input, nil) - if err != nil { panic(err) } diff --git a/examples/select_catalogers/main.go b/examples/select_catalogers/main.go index bc8c6c813..fed786fb1 100644 --- a/examples/select_catalogers/main.go +++ b/examples/select_catalogers/main.go @@ -19,6 +19,7 @@ const defaultImage = "alpine:3.19" func main() { // automagically get a source.Source for arbitrary string input src := getSource(imageReference()) + defer src.Close() // catalog the given source and return a SBOM // let's explicitly use catalogers that are: @@ -44,7 +45,6 @@ func imageReference() string { func getSource(input string) source.Source { src, err := syft.GetSource(context.Background(), input, nil) - if err != nil { panic(err) } diff --git a/examples/source_from_registry/main.go b/examples/source_from_registry/main.go index c43ad8afd..894d7cfae 100644 --- a/examples/source_from_registry/main.go +++ b/examples/source_from_registry/main.go @@ -15,6 +15,7 @@ func main() { image := "alpine:3.19" src, _ := syft.GetSource(context.Background(), image, syft.DefaultGetSourceConfig().WithSources("registry")) + defer src.Close() sbom, _ := syft.CreateSBOM(context.Background(), src, syft.DefaultCreateSBOMConfig()) diff --git a/internal/os/feature_detection_test.go b/internal/os/feature_detection_test.go index f7622e7ff..9a1fe32f9 100644 --- a/internal/os/feature_detection_test.go +++ b/internal/os/feature_detection_test.go @@ -81,6 +81,10 @@ func Test_EnvironmentTask(t *testing.T) { // get the source theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive")) require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, theSource.Close()) + }) + resolver, err := theSource.FileResolver(source.SquashedScope) require.NoError(t, err) diff --git a/syft/internal/fileresolver/directory.go b/syft/internal/fileresolver/directory.go index 55882b802..750cfffd9 100644 --- a/syft/internal/fileresolver/directory.go +++ b/syft/internal/fileresolver/directory.go @@ -19,16 +19,16 @@ type Directory struct { indexer *directoryIndexer } -func NewFromDirectory(root string, base string, pathFilters ...PathIndexVisitor) (*Directory, error) { - r, err := newFromDirectoryWithoutIndex(root, base, pathFilters...) +func NewFromDirectory(root, base string, pathFilters ...PathIndexVisitor) (*Directory, error) { + resolver, err := newFromDirectoryWithoutIndex(root, base, pathFilters...) if err != nil { return nil, err } - return r, r.buildIndex() + return resolver, resolver.buildIndex() } -func newFromDirectoryWithoutIndex(root string, base string, pathFilters ...PathIndexVisitor) (*Directory, error) { +func newFromDirectoryWithoutIndex(root, base string, pathFilters ...PathIndexVisitor) (*Directory, error) { chroot, err := NewChrootContextFromCWD(root, base) if err != nil { return nil, fmt.Errorf("unable to interpret chroot context: %w", err) @@ -66,6 +66,6 @@ func (r *Directory) buildIndex() error { } // Stringer to represent a directory path data source -func (r Directory) String() string { +func (r *Directory) String() string { return fmt.Sprintf("dir:%s", r.path) } diff --git a/syft/internal/fileresolver/file.go b/syft/internal/fileresolver/file.go index 114612f82..16ffadffe 100644 --- a/syft/internal/fileresolver/file.go +++ b/syft/internal/fileresolver/file.go @@ -17,17 +17,31 @@ type File struct { indexer *fileIndexer } -// parent should be the symlink free absolute path to the parent directory +// NewFromFile single file analyser // path is the filepath of the file we're creating content access for -func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, error) { - chroot, err := NewChrootContextFromCWD(parent, parent) +func NewFromFile(path string, pathFilters ...PathIndexVisitor) (*File, error) { + resolver, err := newFromFileWithoutIndex(path, pathFilters...) + if err != nil { + return nil, err + } + + return resolver, resolver.buildIndex() +} + +func newFromFileWithoutIndex(path string, pathFilters ...PathIndexVisitor) (*File, error) { + absParentDir, err := absoluteSymlinkFreePathToParent(path) + if err != nil { + return nil, err + } + + chroot, err := NewChrootContextFromCWD(absParentDir, absParentDir) if err != nil { return nil, fmt.Errorf("unable to interpret chroot context: %w", err) } cleanBase := chroot.Base() - file := &File{ + return &File{ path: path, FiletreeResolver: FiletreeResolver{ Chroot: *chroot, @@ -36,9 +50,7 @@ func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, e Opener: nativeOSFileOpener, }, indexer: newFileIndexer(path, cleanBase, pathFilters...), - } - - return file, file.buildIndex() + }, nil } func (r *File) buildIndex() error { @@ -58,6 +70,6 @@ func (r *File) buildIndex() error { } // Stringer to represent a file path data source -func (r File) String() string { +func (r *File) String() string { return fmt.Sprintf("file:%s", r.path) } diff --git a/syft/internal/fileresolver/filetree_resolver_test.go b/syft/internal/fileresolver/filetree_resolver_test.go index a3c5209e1..d2ad67990 100644 --- a/syft/internal/fileresolver/filetree_resolver_test.go +++ b/syft/internal/fileresolver/filetree_resolver_test.go @@ -1384,9 +1384,10 @@ func TestFileResolver_FilesByPath(t *testing.T) { require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, tt.filePath) + resolver, err := NewFromFile(tt.filePath) require.NoError(t, err) require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) refs, err := resolver.FilesByPath(tt.fileByPathInput) require.NoError(t, err) @@ -1431,8 +1432,11 @@ func TestFileResolver_MultipleFilesByPath(t *testing.T) { require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + resolver, err := NewFromFile(filePath) assert.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) + refs, err := resolver.FilesByPath(tt.input...) assert.NoError(t, err) @@ -1449,8 +1453,11 @@ func TestFileResolver_FilesByGlob(t *testing.T) { require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + resolver, err := NewFromFile(filePath) assert.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) + refs, err := resolver.FilesByGlob("**/*.txt") assert.NoError(t, err) @@ -1476,8 +1483,11 @@ func Test_fileResolver_FilesByMIMEType(t *testing.T) { require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + resolver, err := NewFromFile(filePath) assert.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) + locations, err := resolver.FilesByMIMEType(test.mimeType) assert.NoError(t, err) assert.Equal(t, test.expectedPaths.Size(), len(locations)) @@ -1497,10 +1507,12 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) { require.NoError(t, err) require.NotNil(t, parentPath) - r, err := NewFromFile(parentPath, filePath) + resolver, err := NewFromFile(filePath) require.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) - exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt"))) + exists, existingPath, err := resolver.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()) @@ -1525,7 +1537,7 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual, err := r.FileContentsByLocation(test.location) + actual, err := resolver.FileContentsByLocation(test.location) if test.err { require.Error(t, err) return @@ -1546,8 +1558,11 @@ func TestFileResolver_AllLocations_errorOnDirRequest(t *testing.T) { parentPath, err := absoluteSymlinkFreePathToParent(filePath) require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + + resolver, err := NewFromFile(filePath) require.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) var dirLoc *file.Location ctx, cancel := context.WithCancel(context.Background()) @@ -1575,8 +1590,11 @@ func TestFileResolver_AllLocations(t *testing.T) { parentPath, err := absoluteSymlinkFreePathToParent(filePath) require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + + resolver, err := NewFromFile(filePath) require.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) paths := strset.New() for loc := range resolver.AllLocations(context.Background()) { @@ -1600,8 +1618,11 @@ func Test_FileResolver_AllLocationsDoesNotLeakGoRoutine(t *testing.T) { parentPath, err := absoluteSymlinkFreePathToParent(filePath) require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + + resolver, err := NewFromFile(filePath) require.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) diff --git a/syft/pkg/cataloger/redhat/sqlitetest/no_sqlite_driver_test.go b/syft/pkg/cataloger/redhat/sqlitetest/no_sqlite_driver_test.go index f1a3f17e4..894569200 100644 --- a/syft/pkg/cataloger/redhat/sqlitetest/no_sqlite_driver_test.go +++ b/syft/pkg/cataloger/redhat/sqlitetest/no_sqlite_driver_test.go @@ -13,7 +13,7 @@ import ( func Test_noSQLiteDriverError(t *testing.T) { // this test package does must not import the sqlite library file := "../test-fixtures/Packages" - resolver, err := fileresolver.NewFromFile(file, file) + resolver, err := fileresolver.NewFromFile(file) require.NoError(t, err) cataloger := redhat.NewDBCataloger() diff --git a/syft/source/directorysource/directory_source.go b/syft/source/directorysource/directory_source.go index d45084a4e..e4f79eee4 100644 --- a/syft/source/directorysource/directory_source.go +++ b/syft/source/directorysource/directory_source.go @@ -30,24 +30,21 @@ type Config struct { type directorySource struct { id artifact.ID config Config - resolver *fileresolver.Directory + resolver file.Resolver mutex *sync.Mutex } func NewFromPath(path string) (source.Source, error) { - cfg := Config{ - Path: path, - } - return New(cfg) + return New(Config{Path: path}) } func New(cfg Config) (source.Source, error) { - fi, err := os.Stat(cfg.Path) + fileMeta, err := os.Stat(cfg.Path) if err != nil { return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err) } - if !fi.IsDir() { + if !fileMeta.IsDir() { return nil, fmt.Errorf("given path is not a directory: %q", cfg.Path) } @@ -58,53 +55,6 @@ func New(cfg Config) (source.Source, error) { }, nil } -// deriveIDFromDirectory generates an artifact ID from the given directory config. If an alias is provided, then -// the artifact ID is derived exclusively from the alias name and version. Otherwise, the artifact ID is derived -// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory -// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without -// consideration for the path. -func deriveIDFromDirectory(cfg Config) artifact.ID { - var info string - if !cfg.Alias.IsEmpty() { - // don't use any of the path information -- instead use the alias name and version as the artifact ID. - // why? this allows the user to set a dependable stable value for the artifact ID in case the - // scanning root changes (e.g. a user scans a directory, then moves it to a new location and scans again). - info = fmt.Sprintf("%s@%s", cfg.Alias.Name, cfg.Alias.Version) - } else { - log.Warn("no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not ideal)") - info = cleanDirPath(cfg.Path, cfg.Base) - } - - return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String()) -} - -func cleanDirPath(path, base string) string { - if path == base { - return path - } - - if base != "" { - cleanRoot, rootErr := fileresolver.NormalizeRootDirectory(path) - cleanBase, baseErr := fileresolver.NormalizeBaseDirectory(base) - - if rootErr == nil && baseErr == nil { - // allows for normalizing inputs: - // cleanRoot: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001/some/path - // cleanBase: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001 - // normalized: some/path - - relPath, err := filepath.Rel(cleanBase, cleanRoot) - if err == nil { - path = relPath - } - // this is odd, but this means we can't use base - } - // if the base is not a valid chroot, then just use the path as-is - } - - return path -} - func (s directorySource) ID() artifact.ID { return s.id } @@ -118,9 +68,11 @@ func (s directorySource) Describe() source.Description { if a.Name != "" { name = a.Name } + if a.Version != "" { version = a.Version } + if a.Supplier != "" { supplier = a.Supplier } @@ -141,29 +93,31 @@ func (s *directorySource) FileResolver(_ source.Scope) (file.Resolver, error) { s.mutex.Lock() defer s.mutex.Unlock() - if s.resolver == nil { - exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths) - if err != nil { - return nil, err - } - - // this should be the only file resolver that might have overlap with where files are cached - exclusionFunctions = append(exclusionFunctions, excludeCachePathVisitors()...) - - res, err := fileresolver.NewFromDirectory(s.config.Path, s.config.Base, exclusionFunctions...) - if err != nil { - return nil, fmt.Errorf("unable to create directory resolver: %w", err) - } - - s.resolver = res + if s.resolver != nil { + return s.resolver, nil } + exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths) + if err != nil { + return nil, err + } + + // this should be the only file resolver that might have overlap with where files are cached + exclusionFunctions = append(exclusionFunctions, excludeCachePathVisitors()...) + + res, err := fileresolver.NewFromDirectory(s.config.Path, s.config.Base, exclusionFunctions...) + if err != nil { + return nil, fmt.Errorf("unable to create directory resolver: %w", err) + } + + s.resolver = res return s.resolver, nil } func (s *directorySource) Close() error { s.mutex.Lock() defer s.mutex.Unlock() + s.resolver = nil return nil } @@ -221,3 +175,50 @@ func GetDirectoryExclusionFunctions(root string, exclusions []string) ([]fileres }, }, nil } + +// deriveIDFromDirectory generates an artifact ID from the given directory config. If an alias is provided, then +// the artifact ID is derived exclusively from the alias name and version. Otherwise, the artifact ID is derived +// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory +// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without +// consideration for the path. +func deriveIDFromDirectory(cfg Config) artifact.ID { + var info string + if !cfg.Alias.IsEmpty() { + // don't use any of the path information -- instead use the alias name and version as the artifact ID. + // why? this allows the user to set a dependable stable value for the artifact ID in case the + // scanning root changes (e.g. a user scans a directory, then moves it to a new location and scans again). + info = fmt.Sprintf("%s@%s", cfg.Alias.Name, cfg.Alias.Version) + } else { + log.Warn("no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not ideal)") + info = cleanDirPath(cfg.Path, cfg.Base) + } + + return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String()) +} + +func cleanDirPath(path, base string) string { + if path == base { + return path + } + + if base != "" { + cleanRoot, rootErr := fileresolver.NormalizeRootDirectory(path) + cleanBase, baseErr := fileresolver.NormalizeBaseDirectory(base) + + if rootErr == nil && baseErr == nil { + // allows for normalizing inputs: + // cleanRoot: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001/some/path + // cleanBase: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001 + // normalized: some/path + + relPath, err := filepath.Rel(cleanBase, cleanRoot) + if err == nil { + path = relPath + } + // this is odd, but this means we can't use base + } + // if the base is not a valid chroot, then just use the path as-is + } + + return path +} diff --git a/syft/source/filesource/file_source.go b/syft/source/filesource/file_source.go index f74b0c089..da2be0e19 100644 --- a/syft/source/filesource/file_source.go +++ b/syft/source/filesource/file_source.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path" - "path/filepath" "sync" "github.com/opencontainers/go-digest" @@ -50,7 +49,13 @@ func NewFromPath(path string) (source.Source, error) { } func New(cfg Config) (source.Source, error) { - fileMeta, err := os.Stat(cfg.Path) + f, err := os.Open(cfg.Path) + if err != nil { + return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err) + } + defer f.Close() + + fileMeta, err := f.Stat() if err != nil { return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err) } @@ -59,33 +64,19 @@ func New(cfg Config) (source.Source, error) { return nil, fmt.Errorf("given path is a directory: %q", cfg.Path) } - analysisPath, cleanupFn, err := fileAnalysisPath(cfg.Path, cfg.SkipExtractArchive) - if err != nil { - return nil, fmt.Errorf("unable to extract file analysis path=%q: %w", cfg.Path, err) - } - var digests []file.Digest if len(cfg.DigestAlgorithms) > 0 { - fh, err := os.Open(cfg.Path) - if err != nil { - return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err) - } - - defer fh.Close() - - digests, err = intFile.NewDigestsFromFile(context.TODO(), fh, cfg.DigestAlgorithms) + digests, err = intFile.NewDigestsFromFile(context.TODO(), f, cfg.DigestAlgorithms) if err != nil { return nil, fmt.Errorf("unable to calculate digests for file=%q: %w", cfg.Path, err) } } - fh, err := os.Open(cfg.Path) + analysisPath, cleanupFn, err := fileAnalysisPath(cfg.Path, cfg.SkipExtractArchive) if err != nil { - return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err) + return nil, fmt.Errorf("unable to extract file analysis path=%q: %w", cfg.Path, err) } - defer fh.Close() - id, versionDigest := deriveIDFromFile(cfg) return &fileSource{ @@ -96,26 +87,10 @@ func New(cfg Config) (source.Source, error) { analysisPath: analysisPath, digestForVersion: versionDigest, digests: digests, - mimeType: stereoFile.MIMEType(fh), + mimeType: stereoFile.MIMEType(f), }, nil } -// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included -// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be -// logically different, then ID will express that. -func deriveIDFromFile(cfg Config) (artifact.ID, string) { - d := digestOfFileContents(cfg.Path) - info := d - - if !cfg.Alias.IsEmpty() { - // if the user provided an alias, we want to consider that in the artifact ID. This way if the user - // scans the same item but is considered to be logically different, then ID will express that. - info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version) - } - - return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d -} - func (s fileSource) ID() artifact.ID { return s.id } @@ -168,52 +143,56 @@ func (s fileSource) FileResolver(_ source.Scope) (file.Resolver, error) { if err != nil { return nil, fmt.Errorf("unable to stat path=%q: %w", s.analysisPath, err) } - isArchiveAnalysis := fi.IsDir() - absParentDir, err := absoluteSymlinkFreePathToParent(s.analysisPath) - if err != nil { - return nil, err - } - - if isArchiveAnalysis { + if isArchiveAnalysis := fi.IsDir(); isArchiveAnalysis { // this is an analysis of an archive file... we should scan the directory where the archive contents res, err := fileresolver.NewFromDirectory(s.analysisPath, "", exclusionFunctions...) if err != nil { return nil, fmt.Errorf("unable to create directory resolver: %w", err) } + s.resolver = res return s.resolver, nil } // This is analysis of a single file. Use file indexer. - res, err := fileresolver.NewFromFile(absParentDir, s.analysisPath, exclusionFunctions...) + res, err := fileresolver.NewFromFile(s.analysisPath, exclusionFunctions...) if err != nil { return nil, fmt.Errorf("unable to create file resolver: %w", err) } + s.resolver = res return s.resolver, nil } -func absoluteSymlinkFreePathToParent(path string) (string, error) { - absAnalysisPath, err := filepath.Abs(path) - if err != nil { - return "", fmt.Errorf("unable to get absolute path for analysis path=%q: %w", path, err) - } - dereferencedAbsAnalysisPath, err := filepath.EvalSymlinks(absAnalysisPath) - if err != nil { - return "", fmt.Errorf("unable to get absolute path for analysis path=%q: %w", path, err) - } - return filepath.Dir(dereferencedAbsAnalysisPath), nil -} - func (s *fileSource) Close() error { + s.mutex.Lock() + defer s.mutex.Unlock() + if s.closer == nil { return nil } + s.resolver = nil return s.closer() } +// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included +// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be +// logically different, then ID will express that. +func deriveIDFromFile(cfg Config) (artifact.ID, string) { + d := digestOfFileContents(cfg.Path) + info := d + + if !cfg.Alias.IsEmpty() { + // if the user provided an alias, we want to consider that in the artifact ID. This way if the user + // scans the same item but is considered to be logically different, then ID will express that. + info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version) + } + + return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d +} + // fileAnalysisPath returns the path given, or in the case the path is an archive, the location where the archive // contents have been made available. A cleanup function is provided for any temp files created (if any). // Users can disable unpacking archives, allowing individual cataloguers to extract them instead (where @@ -253,15 +232,17 @@ func fileAnalysisPath(path string, skipExtractArchive bool) (string, func() erro } func digestOfFileContents(path string) string { - file, err := os.Open(path) + f, err := os.Open(path) if err != nil { return digest.SHA256.FromString(path).String() } - defer file.Close() - di, err := digest.SHA256.FromReader(file) + defer f.Close() + + di, err := digest.SHA256.FromReader(f) if err != nil { return digest.SHA256.FromString(path).String() } + return di.String() }