mirror of
https://github.com/anchore/syft.git
synced 2026-11-02 07:26:34 +01:00
chore: truncation reason to error type
Signed-off-by: Keith Zantow <keith.zantow@anchore.com>
This commit is contained in:
@@ -149,16 +149,16 @@ func (r *Resolver) extract(ctx context.Context, format archives.Extractor, conte
|
|||||||
})
|
})
|
||||||
r.finish()
|
r.finish()
|
||||||
if errors.Is(err, ErrDiskLimitReached) {
|
if errors.Is(err, ErrDiskLimitReached) {
|
||||||
r.truncate("extraction stopped at the configured memory or disk limit")
|
r.truncate(fmt.Errorf("extraction stopped: %w", err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if errors.Is(err, errBudgetSpent) {
|
if errors.Is(err, errBudgetSpent) {
|
||||||
r.truncate("extraction stopped: the archive and those nested in it decompressed to far more than its size (possible decompression bomb)")
|
r.truncate(fmt.Errorf("extraction stopped, possible decompression bomb: %w", err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil && ctx.Err() == nil && len(r.files) > 0 {
|
if err != nil && ctx.Err() == nil && len(r.files) > 0 {
|
||||||
// the stream broke partway (a truncated .tar.gz); what was read before that is still good
|
// the stream broke partway (a truncated .tar.gz); what was read before that is still good
|
||||||
r.truncate(fmt.Sprintf("extraction stopped early: %v", err))
|
r.truncate(fmt.Errorf("extraction stopped early: %w", err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func TestResolver_extract_storesEveryEntryInMemoryWhenUnbounded(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
r, root := extractBytes(t, data, name, nil)
|
r, root := extractBytes(t, data, name, nil)
|
||||||
assert.False(t, r.Truncated)
|
assert.NoError(t, r.Truncated)
|
||||||
assert.Zero(t, r.written)
|
assert.Zero(t, r.written)
|
||||||
assert.Empty(t, filesIn(t, root))
|
assert.Empty(t, filesIn(t, root))
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ func TestResolver_extract_diskLimitTruncatesAtTheEntryThatDoesNotFit(t *testing.
|
|||||||
|
|
||||||
r, _ := extractBytes(t, data, "test.zip", diskCharge(2*(record+1)+record))
|
r, _ := extractBytes(t, data, "test.zip", diskCharge(2*(record+1)+record))
|
||||||
|
|
||||||
assert.True(t, r.Truncated)
|
assert.Error(t, r.Truncated)
|
||||||
assert.Equal(t, []string{"file1.txt", "file2.txt"}, storedNames(t, r), "the refused entry is not stored")
|
assert.Equal(t, []string{"file1.txt", "file2.txt"}, storedNames(t, r), "the refused entry is not stored")
|
||||||
entries := readStore(t, r)
|
entries := readStore(t, r)
|
||||||
assert.Equal(t, "a", entries["file1.txt"].body)
|
assert.Equal(t, "a", entries["file1.txt"].body)
|
||||||
@@ -105,7 +105,7 @@ func TestResolver_extract_diskLimitBitesMidEntry(t *testing.T) {
|
|||||||
|
|
||||||
r, root := extractBytes(t, data, "test.tar.gz", charge)
|
r, root := extractBytes(t, data, "test.tar.gz", charge)
|
||||||
|
|
||||||
assert.True(t, r.Truncated)
|
assert.Error(t, r.Truncated)
|
||||||
assert.Equal(t, []string{"a/first.txt"}, storedNames(t, r))
|
assert.Equal(t, []string{"a/first.txt"}, storedNames(t, r))
|
||||||
assert.Greater(t, r.written, int64(40*1024), "the first entry landed whole and the second part way")
|
assert.Greater(t, r.written, int64(40*1024), "the first entry landed whole and the second part way")
|
||||||
_, disk := charge.held()
|
_, disk := charge.held()
|
||||||
@@ -123,11 +123,11 @@ func TestResolver_extract_diskLimitFallsWhenAnArchiveIsReleased(t *testing.T) {
|
|||||||
limiter := NewLimiter(Limits{MaxDiskBytes: oneArchive * 3 / 2})
|
limiter := NewLimiter(Limits{MaxDiskBytes: oneArchive * 3 / 2})
|
||||||
|
|
||||||
first, _ := extractBytes(t, data, "test.zip", limiter.charge())
|
first, _ := extractBytes(t, data, "test.zip", limiter.charge())
|
||||||
require.False(t, first.Truncated)
|
require.NoError(t, first.Truncated)
|
||||||
require.Equal(t, int64(200), first.written)
|
require.Equal(t, int64(200), first.written)
|
||||||
|
|
||||||
second, _ := extractBytes(t, data, "test.zip", limiter.charge())
|
second, _ := extractBytes(t, data, "test.zip", limiter.charge())
|
||||||
assert.True(t, second.Truncated, "a second archive is bounded while the first is still held")
|
assert.Error(t, second.Truncated, "a second archive is bounded while the first is still held")
|
||||||
second.Cleanup()
|
second.Cleanup()
|
||||||
|
|
||||||
first.Cleanup()
|
first.Cleanup()
|
||||||
@@ -135,7 +135,7 @@ func TestResolver_extract_diskLimitFallsWhenAnArchiveIsReleased(t *testing.T) {
|
|||||||
require.Zero(t, disk)
|
require.Zero(t, disk)
|
||||||
|
|
||||||
third, _ := extractBytes(t, data, "test.zip", limiter.charge())
|
third, _ := extractBytes(t, data, "test.zip", limiter.charge())
|
||||||
assert.False(t, third.Truncated, "and extracts in full once the first is released")
|
assert.NoError(t, third.Truncated, "and extracts in full once the first is released")
|
||||||
assert.Equal(t, int64(200), third.written)
|
assert.Equal(t, int64(200), third.written)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ func TestResolver_extract_directoryOnlyArchivesAreBoundedByTheIndexEstimate(t *t
|
|||||||
} {
|
} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
r, _ := extractBytes(t, data, name, diskCharge(room))
|
r, _ := extractBytes(t, data, name, diskCharge(room))
|
||||||
assert.True(t, r.Truncated)
|
assert.Error(t, r.Truncated)
|
||||||
assert.Len(t, storedNames(t, r), 10)
|
assert.Len(t, storedNames(t, r), 10)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ func TestResolver_extract_entryNamesAreCleanedAndNothingIsWrittenUnderThem(t *te
|
|||||||
} {
|
} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
r, root := extractBytes(t, data, name, nil)
|
r, root := extractBytes(t, data, name, nil)
|
||||||
require.False(t, r.Truncated)
|
require.NoError(t, r.Truncated)
|
||||||
|
|
||||||
assert.Equal(t, []string{"META-INF", "META-INF/MANIFEST.MF", "bin/link.txt", "etc/hosts", "etc/passwd", "passwd", "shadow"},
|
assert.Equal(t, []string{"META-INF", "META-INF/MANIFEST.MF", "bin/link.txt", "etc/hosts", "etc/passwd", "passwd", "shadow"},
|
||||||
storedNames(t, r))
|
storedNames(t, r))
|
||||||
@@ -205,7 +205,7 @@ func TestResolver_extract_dataEdgeCases(t *testing.T) {
|
|||||||
t.Run("an empty archive is not an error", func(t *testing.T) {
|
t.Run("an empty archive is not an error", func(t *testing.T) {
|
||||||
r, _ := extractBytes(t, zipBytes(t, map[string]string{}), "test.zip", nil)
|
r, _ := extractBytes(t, zipBytes(t, map[string]string{}), "test.zip", nil)
|
||||||
assert.Empty(t, r.files)
|
assert.Empty(t, r.files)
|
||||||
assert.False(t, r.Truncated)
|
assert.NoError(t, r.Truncated)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("a zero-byte entry costs no disk", func(t *testing.T) {
|
t.Run("a zero-byte entry costs no disk", func(t *testing.T) {
|
||||||
@@ -251,7 +251,7 @@ func TestResolver_extract_excludedEntriesAreNeverStored(t *testing.T) {
|
|||||||
err := r.extract(context.Background(), identifyFormat(context.Background(), "test.zip", content), content,
|
err := r.extract(context.Background(), identifyFormat(context.Background(), "test.zip", content), content,
|
||||||
NewExclusions([]string{"**/vendor", "**/*.rpm"}))
|
NewExclusions([]string{"**/vendor", "**/*.rpm"}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.False(t, r.Truncated)
|
require.NoError(t, r.Truncated)
|
||||||
|
|
||||||
assert.Equal(t, []string{"keep.txt"}, storedNames(t, r))
|
assert.Equal(t, []string{"keep.txt"}, storedNames(t, r))
|
||||||
mem, _ := limiter.InUse()
|
mem, _ := limiter.InUse()
|
||||||
@@ -266,7 +266,7 @@ func TestExtract_readsEntriesAndDigestsTheArchive(t *testing.T) {
|
|||||||
require.NotNil(t, extracted)
|
require.NotNil(t, extracted)
|
||||||
t.Cleanup(extracted.Cleanup)
|
t.Cleanup(extracted.Cleanup)
|
||||||
|
|
||||||
assert.False(t, extracted.Truncated)
|
assert.NoError(t, extracted.Truncated)
|
||||||
require.Len(t, extracted.Digests, 1)
|
require.Len(t, extracted.Digests, 1)
|
||||||
assert.Equal(t, "sha1", extracted.Digests[0].Algorithm)
|
assert.Equal(t, "sha1", extracted.Digests[0].Algorithm)
|
||||||
|
|
||||||
@@ -358,7 +358,7 @@ func TestExtract_aTruncatedStreamKeepsWhatWasRead(t *testing.T) {
|
|||||||
require.NotNil(t, extracted)
|
require.NotNil(t, extracted)
|
||||||
t.Cleanup(extracted.Cleanup)
|
t.Cleanup(extracted.Cleanup)
|
||||||
|
|
||||||
assert.True(t, extracted.Truncated)
|
assert.Error(t, extracted.Truncated)
|
||||||
locations, err := extracted.FilesByGlob("**")
|
locations, err := extracted.FilesByGlob("**")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []string{"a.txt"}, realPaths(locations))
|
assert.Equal(t, []string{"a.txt"}, realPaths(locations))
|
||||||
@@ -404,7 +404,7 @@ func TestExtract_archiveBytesThatCannotBePlacedAreSkipped(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, extracted)
|
require.NotNil(t, extracted)
|
||||||
t.Cleanup(extracted.Cleanup)
|
t.Cleanup(extracted.Cleanup)
|
||||||
assert.True(t, extracted.Truncated)
|
assert.Error(t, extracted.Truncated)
|
||||||
locations, err := extracted.FilesByGlob("**/*")
|
locations, err := extracted.FilesByGlob("**/*")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Empty(t, locations)
|
assert.Empty(t, locations)
|
||||||
@@ -422,7 +422,7 @@ func TestExtract_aZipBehindALauncherScript(t *testing.T) {
|
|||||||
require.NotNil(t, extracted)
|
require.NotNil(t, extracted)
|
||||||
t.Cleanup(extracted.Cleanup)
|
t.Cleanup(extracted.Cleanup)
|
||||||
|
|
||||||
assert.False(t, extracted.Truncated)
|
assert.NoError(t, extracted.Truncated)
|
||||||
locations, err := extracted.FilesByGlob("**/*")
|
locations, err := extracted.FilesByGlob("**/*")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, locations, 2)
|
assert.Len(t, locations, 2)
|
||||||
@@ -525,7 +525,7 @@ func TestExtract_decompressionBombIsBoundedByTheLimits(t *testing.T) {
|
|||||||
require.NotNil(t, r)
|
require.NotNil(t, r)
|
||||||
t.Cleanup(r.Cleanup)
|
t.Cleanup(r.Cleanup)
|
||||||
|
|
||||||
assert.True(t, r.Truncated)
|
assert.Error(t, r.Truncated)
|
||||||
locations, err := r.FilesByGlob("**/*")
|
locations, err := r.FilesByGlob("**/*")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Empty(t, locations, "the entry that did not fit is not cataloged in part")
|
assert.Empty(t, locations, "the entry that did not fit is not cataloged in part")
|
||||||
@@ -550,7 +550,7 @@ func TestExtract_entryCountBombIsBoundedByTheMemoryLimit(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(r.Cleanup)
|
t.Cleanup(r.Cleanup)
|
||||||
|
|
||||||
assert.True(t, r.Truncated)
|
assert.Error(t, r.Truncated)
|
||||||
assert.Less(t, len(r.files), 20_000)
|
assert.Less(t, len(r.files), 20_000)
|
||||||
assert.LessOrEqual(t, int64(len(r.byPath))*approxIndexBytesPerEntry, limits.MaxMemoryBytes+approxIndexBytesPerEntry,
|
assert.LessOrEqual(t, int64(len(r.byPath))*approxIndexBytesPerEntry, limits.MaxMemoryBytes+approxIndexBytesPerEntry,
|
||||||
"nodes held must not exceed what the memory limit paid for")
|
"nodes held must not exceed what the memory limit paid for")
|
||||||
@@ -578,7 +578,7 @@ func TestExtract_deepPathBombIsBoundedByTheMemoryLimit(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(r.Cleanup)
|
t.Cleanup(r.Cleanup)
|
||||||
|
|
||||||
assert.True(t, r.Truncated)
|
assert.Error(t, r.Truncated)
|
||||||
assert.LessOrEqual(t, int64(len(r.byPath))*approxIndexBytesPerEntry, limits.MaxMemoryBytes+approxIndexBytesPerEntry)
|
assert.LessOrEqual(t, int64(len(r.byPath))*approxIndexBytesPerEntry, limits.MaxMemoryBytes+approxIndexBytesPerEntry)
|
||||||
peakMemory, _ := limiter.Peak()
|
peakMemory, _ := limiter.Peak()
|
||||||
assert.LessOrEqual(t, peakMemory, limits.MaxMemoryBytes)
|
assert.LessOrEqual(t, peakMemory, limits.MaxMemoryBytes)
|
||||||
|
|||||||
@@ -34,10 +34,9 @@ type Resolver struct {
|
|||||||
// Digests are of the archive file itself.
|
// Digests are of the archive file itself.
|
||||||
Digests []file.Digest
|
Digests []file.Digest
|
||||||
|
|
||||||
// Truncated reports that the resolver covers only part of the archive, and TruncatedReason says why:
|
// Truncated is non-nil when the resolver covers only part of the archive, and says why: the disk limit
|
||||||
// the disk limit or the decompression budget stopped extraction early, or an entry was too large.
|
// or the decompression budget stopped extraction early, or an entry was too large or unreadable.
|
||||||
Truncated bool
|
Truncated error
|
||||||
TruncatedReason string
|
|
||||||
|
|
||||||
fileSystemID string
|
fileSystemID string
|
||||||
archivePath string
|
archivePath string
|
||||||
@@ -164,14 +163,14 @@ func (r *Resolver) add(hdr tar.Header, content io.Reader) error {
|
|||||||
delete(r.byPath, entryPath)
|
delete(r.byPath, entryPath)
|
||||||
}
|
}
|
||||||
if errors.Is(err, errEntryTooLarge) {
|
if errors.Is(err, errEntryTooLarge) {
|
||||||
r.truncate(fmt.Sprintf("an entry larger than %s was skipped", humanize.IBytes(uint64(maxEntryBytes))))
|
r.truncate(fmt.Errorf("an entry was skipped: %w of %s", err, humanize.IBytes(uint64(maxEntryBytes))))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrDiskLimitReached) || errors.Is(err, errBudgetSpent) {
|
if errors.Is(err, ErrDiskLimitReached) || errors.Is(err, errBudgetSpent) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// a damaged entry (a CRC mismatch, a corrupt deflate stream) costs only that entry
|
// a damaged entry (a CRC mismatch, a corrupt deflate stream) costs only that entry
|
||||||
r.truncate(fmt.Sprintf("entry %q could not be read: %v", hdr.Name, err))
|
r.truncate(fmt.Errorf("entry %q could not be read: %w", hdr.Name, err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,9 +194,9 @@ var maxEntryBytes int64 = intFile.PerFileReadLimit
|
|||||||
var errEntryTooLarge = errors.New("archive entry exceeds the per-entry size cap")
|
var errEntryTooLarge = errors.New("archive entry exceeds the per-entry size cap")
|
||||||
|
|
||||||
// truncate marks the resolver as covering part of the archive, keeping the first reason given.
|
// truncate marks the resolver as covering part of the archive, keeping the first reason given.
|
||||||
func (r *Resolver) truncate(reason string) {
|
func (r *Resolver) truncate(err error) {
|
||||||
if !r.Truncated {
|
if r.Truncated == nil {
|
||||||
r.Truncated, r.TruncatedReason = true, reason
|
r.Truncated = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -525,8 +525,8 @@ func TestResolver_add_anEntryOverTheCapIsSkippedAndReadNoFurther(t *testing.T) {
|
|||||||
r.finish()
|
r.finish()
|
||||||
|
|
||||||
assert.LessOrEqual(t, endless.n, 1024+copyChunkSize, "reading stops just past the cap")
|
assert.LessOrEqual(t, endless.n, 1024+copyChunkSize, "reading stops just past the cap")
|
||||||
assert.True(t, r.Truncated)
|
assert.ErrorIs(t, r.Truncated, errEntryTooLarge)
|
||||||
assert.Contains(t, r.TruncatedReason, "an entry larger than 1.0 KiB was skipped")
|
assert.ErrorContains(t, r.Truncated, "per-entry size cap of 1.0 KiB")
|
||||||
locations, err := r.FilesByGlob("**")
|
locations, err := r.FilesByGlob("**")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []string{"ok.txt"}, realPaths(locations))
|
assert.Equal(t, []string{"ok.txt"}, realPaths(locations))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package task
|
package task
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -231,8 +230,8 @@ func (c *archiveCataloger) processArchive(ctx context.Context, parentResolver fi
|
|||||||
c.ancestors = append(c.ancestors, extracted.Digests...)
|
c.ancestors = append(c.ancestors, extracted.Digests...)
|
||||||
defer func() { c.ancestors = c.ancestors[:len(c.ancestors)-len(extracted.Digests)] }()
|
defer func() { c.ancestors = c.ancestors[:len(c.ancestors)-len(extracted.Digests)] }()
|
||||||
|
|
||||||
if extracted.Truncated {
|
if extracted.Truncated != nil {
|
||||||
recordArchiveUnknown(builder, location.Coordinates, "archive cataloged from part of its contents: "+extracted.TruncatedReason)
|
recordArchiveUnknown(builder, location.Coordinates, "archive cataloged from part of its contents: "+extracted.Truncated.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
traversal.Digests = extracted.Digests
|
traversal.Digests = extracted.Digests
|
||||||
|
|||||||
Reference in New Issue
Block a user