From fda2fe4fcc1ad575338e47c4da620cc528c090dd Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Wed, 30 Sep 2026 15:57:48 -0400 Subject: [PATCH] chore: archive memory and disk limits of 0 mean 0 Signed-off-by: Keith Zantow --- cmd/syft/internal/options/pkg.go | 5 +++-- internal/task/archive_nesting_test.go | 2 +- internal/task/archive_tasks.go | 7 +------ internal/task/archive_tasks_test.go | 17 ----------------- syft/cataloging/archive_search.go | 5 +++-- syft/create_sbom_config.go | 6 ++++-- syft/create_sbom_config_test.go | 25 +++++++++++++++++++++++++ 7 files changed, 37 insertions(+), 30 deletions(-) diff --git a/cmd/syft/internal/options/pkg.go b/cmd/syft/internal/options/pkg.go index 5308e2308..182fe961b 100644 --- a/cmd/syft/internal/options/pkg.go +++ b/cmd/syft/internal/options/pkg.go @@ -28,8 +28,9 @@ note: this only applies to the java package cataloger, and is ignored when neste these packages are removed if an overlap with a non-synthetic package is found`) descriptions.Add(&o.NestedArchiveMaxDepth, `maximum depth to recursively catalog nested archives (0 = disabled, -1 = as deep as 16 levels) note: when enabled this replaces the java cataloger's own unbounded jar-in-jar recursion, so archives deeper than this are reported as unknowns rather than cataloged`) - descriptions.Add(&o.NestedArchiveMaxMemoryBytes, `maximum bytes of nested archive content held in memory at once (0 = default, -1 = unbounded)`) - descriptions.Add(&o.NestedArchiveMaxDiskBytes, `maximum bytes of nested archive content on disk at once (0 = default, -1 = unbounded)`) + descriptions.Add(&o.NestedArchiveMaxMemoryBytes, `maximum bytes of nested archive content held in memory at once (0 = none, -1 = unbounded)`) + descriptions.Add(&o.NestedArchiveMaxDiskBytes, `maximum bytes of nested archive content on disk at once (0 = none, -1 = unbounded) +note: this and nested-archive-max-memory-bytes cannot both be 0 when nested-archive-max-depth is not 0`) } func defaultPackageConfig() packageConfig { diff --git a/internal/task/archive_nesting_test.go b/internal/task/archive_nesting_test.go index 1e7d604c4..16dfb48e7 100644 --- a/internal/task/archive_nesting_test.go +++ b/internal/task/archive_nesting_test.go @@ -433,7 +433,7 @@ func Test_mixedFamilyNesting_diskLimitTruncatesTheLevelItCannotAdmit(t *testing. func Test_mixedFamilyNesting_unboundedLimitsEnforceNothing(t *testing.T) { // a caller can opt out of one bound without the other, and opting out of both must still find the - // deepest leaf. Both opt out with a negative value, since zero means the default. + // deepest leaf. Opting out is a negative value; zero forbids the resource. for _, row := range nestingRows() { t.Run(row.name, func(t *testing.T) { scanDir := t.TempDir() diff --git a/internal/task/archive_tasks.go b/internal/task/archive_tasks.go index f3fec0a97..fe98f9143 100644 --- a/internal/task/archive_tasks.go +++ b/internal/task/archive_tasks.go @@ -41,13 +41,8 @@ func NewArchiveCatalogerTask(cfg cataloging.ArchiveSearchConfig, subPipeline []T return newArchiveCatalogerTask(cfg.MaxDepth, archiveLimits(cfg), subPipeline, exclusions) } -// archiveLimits maps the configured limits onto the limiter's. Zero means the default, so the limiter's -// own zero (which forbids the resource) is not reachable from configuration. func archiveLimits(cfg cataloging.ArchiveSearchConfig) archive.Limits { - return archive.Limits{ - MaxMemoryBytes: cmp.Or(cfg.MaxMemoryBytes, int64(cataloging.DefaultArchiveMaxMemoryBytes)), - MaxDiskBytes: cmp.Or(cfg.MaxDiskBytes, int64(cataloging.DefaultArchiveMaxDiskBytes)), - } + return archive.Limits{MaxMemoryBytes: cfg.MaxMemoryBytes, MaxDiskBytes: cfg.MaxDiskBytes} } func newArchiveCatalogerTask(maxDepth int, limits archive.Limits, subPipeline []Task, exclusions []string) Task { diff --git a/internal/task/archive_tasks_test.go b/internal/task/archive_tasks_test.go index 52772daad..c0f05e7e3 100644 --- a/internal/task/archive_tasks_test.go +++ b/internal/task/archive_tasks_test.go @@ -172,23 +172,6 @@ func Test_archiveCataloger_traversalThreading(t *testing.T) { }, seen) } -func Test_archiveCataloger_zeroLimitsMeanDefault(t *testing.T) { - // a config literal naming only the depth must catalog archives, not forbid both memory and disk - rootDir := t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(rootDir, "outer.zip"), makeZip(t, map[string][]byte{"a.txt": []byte("a")}), 0o600)) - - var seen []string - fileCounts := map[string]int{} - tsk := newTestTask(t, cataloging.ArchiveSearchConfig{MaxDepth: 1}, capturingTask(t, &seen, &fileCounts)) - - s := newTestSBOM() - require.NoError(t, tsk.Execute(context.Background(), dirTestResolver{dir: rootDir}, sbomsync.NewBuilder(s))) - - assert.Equal(t, []string{"/outer.zip"}, seen) - assert.Equal(t, 1, fileCounts["/outer.zip"]) - assert.Empty(t, s.Artifacts.Unknowns) -} - func Test_archiveCataloger_truncationStillCatalogs(t *testing.T) { // reaching a limit is a truncation, not a failure: the sub-pipeline still runs over what was stored big := bytes.Repeat([]byte("x"), 4096) diff --git a/syft/cataloging/archive_search.go b/syft/cataloging/archive_search.go index 05906839a..b30ca1555 100644 --- a/syft/cataloging/archive_search.go +++ b/syft/cataloging/archive_search.go @@ -27,11 +27,12 @@ type ArchiveSearchConfig struct { MaxDepth int `yaml:"max-depth" json:"max-depth" mapstructure:"max-depth"` // MaxMemoryBytes bounds the archive content held in memory at once; content that does not fit is - // written to disk. Zero means DefaultArchiveMaxMemoryBytes, negative is unbounded. + // written to disk. Zero holds nothing in memory, negative is unbounded. MaxMemoryBytes int64 `yaml:"max-memory-bytes" json:"max-memory-bytes" mapstructure:"max-memory-bytes"` // MaxDiskBytes bounds the archive content written to disk at once; an archive whose content does - // not fit is skipped or truncated. Zero means DefaultArchiveMaxDiskBytes, negative is unbounded. + // not fit is skipped or truncated. Zero writes nothing to disk, negative is unbounded. MaxMemoryBytes + // and MaxDiskBytes cannot both be zero while MaxDepth is not. MaxDiskBytes int64 `yaml:"max-disk-bytes" json:"max-disk-bytes" mapstructure:"max-disk-bytes"` } diff --git a/syft/create_sbom_config.go b/syft/create_sbom_config.go index e872fbd6d..9a0edb062 100644 --- a/syft/create_sbom_config.go +++ b/syft/create_sbom_config.go @@ -148,8 +148,7 @@ func (c *CreateSBOMConfig) WithFilesConfig(cfg filecataloging.Config) *CreateSBO } // WithArchiveConfig sets how archives are cataloged: how deep to recurse into nested archives (MaxDepth -// 0, the default, does not recurse) and how much extracted content the scan may hold at once (a zero -// limit means the default). +// 0, the default, does not recurse) and how much extracted content the scan may hold at once. func (c *CreateSBOMConfig) WithArchiveConfig(cfg cataloging.ArchiveSearchConfig) *CreateSBOMConfig { c.Archive = cfg return c @@ -515,6 +514,9 @@ func (c *CreateSBOMConfig) validate() error { if j := c.Packages.JavaArchive.ArchiveSearchConfig; j.MaxDepth != 0 || j.MaxMemoryBytes != 0 || j.MaxDiskBytes != 0 { return fmt.Errorf("invalid configuration: nested archive depth and limits are not java cataloger settings, set them with CreateSBOMConfig.Archive") } + if a := c.Archive; a.MaxDepth != 0 && a.MaxMemoryBytes == 0 && a.MaxDiskBytes == 0 { + return fmt.Errorf("invalid configuration: nested archive cataloging is enabled but its memory and disk limits are both 0, leaving nowhere to extract archives") + } return nil } diff --git a/syft/create_sbom_config_test.go b/syft/create_sbom_config_test.go index 12dc46bd3..4b2dd9a46 100644 --- a/syft/create_sbom_config_test.go +++ b/syft/create_sbom_config_test.go @@ -555,6 +555,31 @@ func TestCreateSBOMConfig_validate(t *testing.T) { ), wantErr: assert.Error, }, + { + name: "nested archives enabled with no memory or disk", + cfg: DefaultCreateSBOMConfig().WithArchiveConfig( + cataloging.DefaultArchiveSearchConfig().WithMaxDepth(1).WithMaxMemoryBytes(0).WithMaxDiskBytes(0), + ), + wantErr: assert.Error, + }, + { + name: "nested archives enabled with memory only", + cfg: DefaultCreateSBOMConfig().WithArchiveConfig( + cataloging.DefaultArchiveSearchConfig().WithMaxDepth(1).WithMaxDiskBytes(0), + ), + }, + { + name: "nested archives enabled with disk only", + cfg: DefaultCreateSBOMConfig().WithArchiveConfig( + cataloging.DefaultArchiveSearchConfig().WithMaxDepth(1).WithMaxMemoryBytes(0), + ), + }, + { + name: "nested archives disabled with no memory or disk", + cfg: DefaultCreateSBOMConfig().WithArchiveConfig( + cataloging.DefaultArchiveSearchConfig().WithMaxMemoryBytes(0).WithMaxDiskBytes(0), + ), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {