diff --git a/cmd/syft/internal/options/pkg.go b/cmd/syft/internal/options/pkg.go index 1cc238a01..c79027a2a 100644 --- a/cmd/syft/internal/options/pkg.go +++ b/cmd/syft/internal/options/pkg.go @@ -27,8 +27,8 @@ note: for now this only applies to the java package cataloger`) descriptions.Add(&o.ExcludeBinaryOverlapByOwnership, `allows users to exclude synthetic binary packages from the sbom 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 = unlimited)`) - descriptions.Add(&o.NestedArchiveMaxMemoryBytes, `maximum bytes of nested archive content held in memory at once (0 = none, always overflow to disk; -1 = unbounded)`) - descriptions.Add(&o.NestedArchiveMaxDiskBytes, `maximum bytes of nested archive content on disk at once (0 = none, nothing overflows; -1 = unbounded)`) + 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)`) } func defaultPackageConfig() packageConfig { diff --git a/internal/task/archive_nesting_test.go b/internal/task/archive_nesting_test.go index 31a3aba17..fc0736045 100644 --- a/internal/task/archive_nesting_test.go +++ b/internal/task/archive_nesting_test.go @@ -113,7 +113,7 @@ func Test_mixedFamilyNesting_leafIsCatalogedOnceWithTheFullChain(t *testing.T) { nested := writeNestedArchive(t, scanDir, nestPlan{families: row.families, leaf: markerLeaf()}) probe := &nestingProbe{} - s := runNesting(t, scanDir, 3, cataloging.DefaultArchiveSearchConfig(), markerTask(), probe.task()) + s := runNesting(t, scanDir, 3, defaultLimits(), markerTask(), probe.task()) assert.Equal(t, 1, packageCount(s, "marker-pkg"), "%s: the leaf package must be cataloged exactly once", row.why) @@ -139,7 +139,7 @@ func Test_mixedFamilyNesting_containsEdgesChainAtEveryLevel(t *testing.T) { scanDir := t.TempDir() nested := writeNestedArchive(t, scanDir, nestPlan{families: row.families, leaf: markerLeaf()}) - s := runNesting(t, scanDir, 3, cataloging.DefaultArchiveSearchConfig(), markerTask(), fileMetadataTask()) + s := runNesting(t, scanDir, 3, defaultLimits(), markerTask(), fileMetadataTask()) // each archive's coordinates as seen in its parent; the outermost sits in the scanned // directory, so it carries no filesystem id of its own @@ -197,7 +197,7 @@ func Test_mixedFamilyNesting_javaLeafVirtualPathIsTheColonJoinedChain(t *testing scanDir := t.TempDir() nested := writeNestedArchive(t, scanDir, nestPlan{families: row.families, leaf: markerLeaf()}) - s := runNesting(t, scanDir, 3, cataloging.DefaultArchiveSearchConfig(), markerTask(), javaTask()) + s := runNesting(t, scanDir, 3, defaultLimits(), markerTask(), javaTask()) // the innermost archive is the jar, so its own package is the java leaf and the chain of // all three archives is exactly its virtual path @@ -228,7 +228,7 @@ func Test_mixedFamilyNesting_jarIsExercisedAsAContainer(t *testing.T) { nested := writeNestedArchive(t, scanDir, nestPlan{families: row.families, leaf: markerLeaf()}) probe := &nestingProbe{} - s := runNesting(t, scanDir, 3, cataloging.DefaultArchiveSearchConfig(), markerTask(), javaTask(), probe.task()) + s := runNesting(t, scanDir, 3, defaultLimits(), markerTask(), javaTask(), probe.task()) for _, level := range containerLevels { name := fmt.Sprintf("level%d", level) @@ -256,7 +256,7 @@ func Test_mixedFamilyNesting_depthCountsLevelsNotFamilies(t *testing.T) { nested := writeNestedArchive(t, scanDir, nestPlan{families: row.families, leaf: markerLeaf()}) probe := &nestingProbe{} - s := runNesting(t, scanDir, 2, cataloging.DefaultArchiveSearchConfig(), markerTask(), fileMetadataTask(), probe.task()) + s := runNesting(t, scanDir, 2, defaultLimits(), markerTask(), fileMetadataTask(), probe.task()) assert.Equal(t, nested.fileSystemIDs[:2], probe.archiveFileSystemIDs(), "at depth 2 the outer and middle archives are cataloged and the innermost is not") @@ -287,7 +287,7 @@ func Test_mixedFamilyNesting_defaultBoundsCatalogEveryRow(t *testing.T) { nested := writeNestedArchive(t, scanDir, nestPlan{families: row.families, leaf: markerLeaf()}) probe := &nestingProbe{} - s := runNesting(t, scanDir, 3, bounds, markerTask(), probe.task()) + s := runNesting(t, scanDir, 3, archiveLimits(bounds), markerTask(), probe.task()) assert.Equal(t, nested.fileSystemIDs, probe.archiveFileSystemIDs(), "every level of the chain must be cataloged at the default bounds") @@ -306,8 +306,8 @@ func Test_mixedFamilyNesting_memoryPressureOverflowsRatherThanFailing(t *testing t.Run("with a generous memory limit nothing is written to disk", func(t *testing.T) { probe := &nestingProbe{tempDir: tempDir} - bounds := cataloging.DefaultArchiveSearchConfig(). - WithMaxMemoryBytes(int64(2 * (nested.sizes[0] + nested.sizes[1] + nested.sizes[2]))) + bounds := defaultLimits() + bounds.MaxMemoryBytes = int64(2 * (nested.sizes[0] + nested.sizes[1] + nested.sizes[2])) runNesting(t, scanDir, 3, bounds, markerTask(), probe.task()) deepest := probe.deepestVisit() @@ -318,9 +318,7 @@ func Test_mixedFamilyNesting_memoryPressureOverflowsRatherThanFailing(t *testing t.Run("with a memory limit the outermost archive's entries do not fit in", func(t *testing.T) { // room for every level's index estimate, which must stay in memory, but not the outer entries probe := &nestingProbe{tempDir: tempDir} - bounds := cataloging.DefaultArchiveSearchConfig(). - WithMaxMemoryBytes(int64(nested.sizes[2] + 32*1024)). - WithMaxDiskBytes(-1) + bounds := archive.Limits{MaxMemoryBytes: int64(nested.sizes[2] + 32*1024), MaxDiskBytes: -1} s := runNesting(t, scanDir, 3, bounds, markerTask(), probe.task()) deepest := probe.deepestVisit() @@ -359,9 +357,7 @@ func Test_mixedFamilyNesting_aNestedArchiveIsReadWhereItLies(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { probe := &nestingProbe{tempDir: tempDir} - bounds := cataloging.DefaultArchiveSearchConfig(). - WithMaxMemoryBytes(tc.memoryBytes). - WithMaxDiskBytes(-1) + bounds := archive.Limits{MaxMemoryBytes: tc.memoryBytes, MaxDiskBytes: -1} s := runNesting(t, scanDir, 3, bounds, markerTask(), probe.task()) @@ -397,9 +393,7 @@ func Test_mixedFamilyNesting_diskLimitTruncatesTheLevelItCannotAdmit(t *testing. memoryLimit := int64(nested.sizes[0] + nested.sizes[1] + deepChainPayloadBytes/2) probe := &nestingProbe{} - bounds := cataloging.DefaultArchiveSearchConfig(). - WithMaxMemoryBytes(memoryLimit). - WithMaxDiskBytes(0) + bounds := archive.Limits{MaxMemoryBytes: memoryLimit, MaxDiskBytes: 0} s := runNesting(t, scanDir, 3, bounds, markerTask(), probe.task()) @@ -428,16 +422,14 @@ 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 "none of that resource". + // deepest leaf. Both opt out with a negative value, since zero means the default. for _, row := range nestingRows() { t.Run(row.name, func(t *testing.T) { scanDir := t.TempDir() nested := writeNestedArchive(t, scanDir, nestPlan{families: row.families, leaf: markerLeaf()}) probe := &nestingProbe{} - bounds := cataloging.DefaultArchiveSearchConfig(). - WithMaxMemoryBytes(-1). - WithMaxDiskBytes(-1) + bounds := archive.Limits{MaxMemoryBytes: -1, MaxDiskBytes: -1} s := runNesting(t, scanDir, 3, bounds, markerTask(), probe.task()) @@ -449,25 +441,8 @@ func Test_mixedFamilyNesting_unboundedLimitsEnforceNothing(t *testing.T) { } } -func Test_mixedFamilyNesting_bothLimitsZeroCatalogsNothingInsideAnyArchive(t *testing.T) { - // unlike negative (unbounded), zero on both limits admits no entry anywhere: the outermost archive is - // cataloged from nothing, so the level inside it is never found, and the scan still succeeds - for _, row := range nestingRows() { - t.Run(row.name, func(t *testing.T) { - scanDir := t.TempDir() - nested := writeNestedArchive(t, scanDir, nestPlan{families: row.families, leaf: markerLeaf()}) - - probe := &nestingProbe{} - bounds := cataloging.DefaultArchiveSearchConfig(). - WithMaxMemoryBytes(0). - WithMaxDiskBytes(0) - - s := runNesting(t, scanDir, 3, bounds, markerTask(), probe.task()) - - assert.Equal(t, nested.fileSystemIDs[:1], probe.archiveFileSystemIDs()) - assert.Empty(t, leafLocations(s)) - }) - } +func defaultLimits() archive.Limits { + return archiveLimits(cataloging.DefaultArchiveSearchConfig()) } // runNesting runs the archive cataloger task over scanDir with the given sub-pipeline and returns the @@ -476,9 +451,9 @@ func Test_mixedFamilyNesting_bothLimitsZeroCatalogsNothingInsideAnyArchive(t *te // The depth-3 limit cases need the timeout: the walk descends before it unwinds, so the outer and // middle archives still hold their content when the inner one is refused. An implementation that // waited for capacity would deadlock, and a test asserting only the result would hang rather than fail. -func runNesting(t *testing.T, scanDir string, depth int, bounds cataloging.ArchiveSearchConfig, subPipeline ...Task) *sbom.SBOM { +func runNesting(t *testing.T, scanDir string, depth int, bounds archive.Limits, subPipeline ...Task) *sbom.SBOM { t.Helper() - tsk := newTestTask(t, bounds.WithMaxDepth(depth), subPipeline...) + tsk := newLimitedTestTask(t, depth, bounds, subPipeline...) s := &sbom.SBOM{Artifacts: sbom.Artifacts{ Packages: pkg.NewCollection(), FileMetadata: map[file.Coordinates]file.Metadata{}, diff --git a/internal/task/archive_tasks.go b/internal/task/archive_tasks.go index f8b0574d9..c2d7e7f31 100644 --- a/internal/task/archive_tasks.go +++ b/internal/task/archive_tasks.go @@ -1,6 +1,7 @@ package task import ( + "cmp" "context" "errors" "fmt" @@ -36,7 +37,20 @@ const ArchiveCatalogerTaskName = "archive-cataloger" // exclusions are the scan's exclusion patterns, applied inside each archive. Returns nil when archive // cataloging is disabled (MaxDepth == 0; negative means unbounded). func NewArchiveCatalogerTask(cfg cataloging.ArchiveSearchConfig, subPipeline []Task, exclusions []string) Task { - if cfg.MaxDepth == 0 { + 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)), + } +} + +func newArchiveCatalogerTask(maxDepth int, limits archive.Limits, subPipeline []Task, exclusions []string) Task { + if maxDepth == 0 { return nil } subPipeline = withoutArchiveCataloger(subPipeline) @@ -49,9 +63,9 @@ func NewArchiveCatalogerTask(cfg cataloging.ArchiveSearchConfig, subPipeline []T ctx = bus.WithCatalogerTaskRegistry(ctx) c := &archiveCataloger{ - maxDepth: cfg.MaxDepth, + maxDepth: maxDepth, subPipeline: subPipeline, - limiter: archive.NewLimiter(archive.Limits{MaxMemoryBytes: cfg.MaxMemoryBytes, MaxDiskBytes: cfg.MaxDiskBytes}), + limiter: archive.NewLimiter(limits), exclusions: archive.NewExclusions(exclusions), progress: bus.StartCatalogerTask(ctx, archiveCatalogerProgressInfo(), -1, ""), } diff --git a/internal/task/archive_tasks_test.go b/internal/task/archive_tasks_test.go index 83de81351..1e9a6b71f 100644 --- a/internal/task/archive_tasks_test.go +++ b/internal/task/archive_tasks_test.go @@ -70,6 +70,14 @@ func newTestTask(t *testing.T, cfg cataloging.ArchiveSearchConfig, subPipeline . return tsk } +// newLimitedTestTask builds the task with the limiter's own limits, where zero forbids the resource. +func newLimitedTestTask(t *testing.T, maxDepth int, limits archive.Limits, subPipeline ...Task) Task { + t.Helper() + tsk := newArchiveCatalogerTask(maxDepth, limits, subPipeline, nil) + require.NotNil(t, tsk) + return tsk +} + func newTestSBOM() *sbom.SBOM { return &sbom.SBOM{Artifacts: sbom.Artifacts{Packages: pkg.NewCollection()}} } @@ -122,6 +130,23 @@ 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) @@ -134,14 +159,11 @@ func Test_archiveCataloger_truncationStillCatalogs(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(rootDir, "outer.zip"), outerZip, 0o600)) // nothing is held in memory, so each entry is charged to disk: small.txt lands, big.txt is refused - cfg := cataloging.DefaultArchiveSearchConfig(). - WithMaxDepth(1). - WithMaxMemoryBytes(0). - WithMaxDiskBytes(6000) + limits := archive.Limits{MaxMemoryBytes: 0, MaxDiskBytes: 6000} var seen []string fileCounts := map[string]int{} - tsk := newTestTask(t, cfg, capturingTask(t, &seen, &fileCounts)) + tsk := newLimitedTestTask(t, 1, limits, capturingTask(t, &seen, &fileCounts)) s := newTestSBOM() err := tsk.Execute(context.Background(), dirTestResolver{dir: rootDir}, sbomsync.NewBuilder(s)) @@ -462,14 +484,11 @@ func Test_archiveCataloger_manySmallArchivesAreEachCataloged(t *testing.T) { } // room for one archive's entries and their index cost, not for two - cfg := cataloging.DefaultArchiveSearchConfig(). - WithMaxDepth(1). - WithMaxMemoryBytes(0). - WithMaxDiskBytes(20000) + limits := archive.Limits{MaxMemoryBytes: 0, MaxDiskBytes: 20000} var seen []string fileCounts := map[string]int{} - tsk := newTestTask(t, cfg, capturingTask(t, &seen, &fileCounts)) + tsk := newLimitedTestTask(t, 1, limits, capturingTask(t, &seen, &fileCounts)) s := newTestSBOM() require.NoError(t, tsk.Execute(context.Background(), dirTestResolver{dir: rootDir}, sbomsync.NewBuilder(s))) @@ -544,14 +563,11 @@ func Test_archiveCataloger_archiveWhoseOwnBytesExceedTheDiskLimitIsSkipped(t *te diskLimit := int64(len(small)) + 3000 require.Greater(t, int64(len(oversized)), diskLimit, "the fixture must actually exceed the limit") - cfg := cataloging.DefaultArchiveSearchConfig(). - WithMaxDepth(1). - WithMaxMemoryBytes(0). - WithMaxDiskBytes(diskLimit) + limits := archive.Limits{MaxMemoryBytes: 0, MaxDiskBytes: diskLimit} var seen []string fileCounts := map[string]int{} - tsk := newTestTask(t, cfg, capturingTask(t, &seen, &fileCounts)) + tsk := newLimitedTestTask(t, 1, limits, capturingTask(t, &seen, &fileCounts)) s := newTestSBOM() require.NoError(t, tsk.Execute(context.Background(), streamingDirResolver{dirTestResolver{dir: rootDir}}, sbomsync.NewBuilder(s)), @@ -576,14 +592,11 @@ func Test_archiveCataloger_zeroDiskLimitTruncatesWhatWillNotFitInMemory(t *testi require.NoError(t, os.WriteFile(filepath.Join(rootDir, "small.zip"), small, 0o600)) // room for the small archive's entry and its index cost, not for the oversized one's - cfg := cataloging.DefaultArchiveSearchConfig(). - WithMaxDepth(1). - WithMaxMemoryBytes(3000). - WithMaxDiskBytes(0) + limits := archive.Limits{MaxMemoryBytes: 3000, MaxDiskBytes: 0} var seen []string fileCounts := map[string]int{} - tsk := newTestTask(t, cfg, capturingTask(t, &seen, &fileCounts)) + tsk := newLimitedTestTask(t, 1, limits, capturingTask(t, &seen, &fileCounts)) s := newTestSBOM() require.NoError(t, tsk.Execute(context.Background(), dirTestResolver{dir: rootDir}, sbomsync.NewBuilder(s)), @@ -597,33 +610,6 @@ func Test_archiveCataloger_zeroDiskLimitTruncatesWhatWillNotFitInMemory(t *testi assert.NotContains(t, s.Artifacts.Unknowns, file.Coordinates{RealPath: "/small.zip"}) } -func Test_archiveCataloger_bothLimitsZeroTruncatesEveryArchiveAndSucceeds(t *testing.T) { - entries := map[string][]byte{"ok.txt": []byte("ok")} - rootDir := t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(rootDir, "one.zip"), makeZip(t, entries), 0o600)) - require.NoError(t, os.WriteFile(filepath.Join(rootDir, "two.zip"), makeZip(t, entries), 0o600)) - - cfg := cataloging.DefaultArchiveSearchConfig(). - WithMaxDepth(1). - WithMaxMemoryBytes(0). - WithMaxDiskBytes(0) - - var seen []string - fileCounts := map[string]int{} - tsk := newTestTask(t, cfg, capturingTask(t, &seen, &fileCounts)) - - s := newTestSBOM() - require.NoError(t, tsk.Execute(context.Background(), dirTestResolver{dir: rootDir}, sbomsync.NewBuilder(s)), - "both limits at zero is a real configuration, not a failure") - - assert.ElementsMatch(t, []string{"/one.zip", "/two.zip"}, seen) - for _, archivePath := range seen { - assert.Equal(t, 0, fileCounts[archivePath], "no entry can be admitted anywhere") - assert.Contains(t, s.Artifacts.Unknowns, file.Coordinates{RealPath: archivePath}) - } - assert.Empty(t, s.Artifacts.Packages.Sorted()) -} - func Test_archiveCataloger_nestedArchiveExceedingALimitIsTruncatedNotBlocked(t *testing.T) { // the walk descends before it unwinds, so nothing is released while a child waits; a child that does // not fit is truncated and the scan finishes @@ -638,14 +624,11 @@ func Test_archiveCataloger_nestedArchiveExceedingALimitIsTruncatedNotBlocked(t * diskLimit := int64(len(inner)) + 2*2100 + 500 require.Greater(t, 3000, 500, "the inner payload must not fit in the slack") - cfg := cataloging.DefaultArchiveSearchConfig(). - WithMaxDepth(2). - WithMaxMemoryBytes(0). - WithMaxDiskBytes(diskLimit) + limits := archive.Limits{MaxMemoryBytes: 0, MaxDiskBytes: diskLimit} var seen []string fileCounts := map[string]int{} - tsk := newTestTask(t, cfg, capturingTask(t, &seen, &fileCounts)) + tsk := newLimitedTestTask(t, 2, limits, capturingTask(t, &seen, &fileCounts)) s := newTestSBOM() require.NoError(t, tsk.Execute(context.Background(), dirTestResolver{dir: rootDir}, sbomsync.NewBuilder(s))) diff --git a/syft/cataloging/archive_search.go b/syft/cataloging/archive_search.go index 08dd0e493..3d1db2783 100644 --- a/syft/cataloging/archive_search.go +++ b/syft/cataloging/archive_search.go @@ -27,11 +27,11 @@ 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 writes everything to disk, negative is unbounded. + // written to disk. Zero means DefaultArchiveMaxMemoryBytes, 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 writes nothing to disk, negative is unbounded. + // not fit is skipped or truncated. Zero means DefaultArchiveMaxDiskBytes, negative is unbounded. 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 96fa0605d..3b373faaf 100644 --- a/syft/create_sbom_config.go +++ b/syft/create_sbom_config.go @@ -148,7 +148,8 @@ 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. +// 0, the default, does not recurse) and how much extracted content the scan may hold at once (a zero +// limit means the default). func (c *CreateSBOMConfig) WithArchiveConfig(cfg cataloging.ArchiveSearchConfig) *CreateSBOMConfig { c.Archive = cfg return c diff --git a/syft/pkg/cataloger/java/capabilities.yaml b/syft/pkg/cataloger/java/capabilities.yaml index a05bcaa6d..bde560445 100644 --- a/syft/pkg/cataloger/java/capabilities.yaml +++ b/syft/pkg/cataloger/java/capabilities.yaml @@ -10,9 +10,9 @@ configs: # AUTO-GENERATED - config structs and their fields - key: MaxDepth description: 'MaxDepth is how many levels of nested archives to catalog: 0 disables it, negative is unbounded.' - key: MaxMemoryBytes - description: MaxMemoryBytes bounds the archive content held in memory at once; content that does not fit is written to disk. Zero writes everything to disk, negative is unbounded. + description: 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. - key: MaxDiskBytes - description: MaxDiskBytes bounds the archive content written to disk at once; an archive whose content does not fit is skipped or truncated. Zero writes nothing to disk, negative is unbounded. + description: 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. - key: UseNetwork description: UseNetwork enables network operations for java package metadata enrichment, such as fetching parent POMs and license information. app_key: java.use-network