fix: a damaged entry truncates the archive instead of failing it

a read error while copying one entry (a zip CRC mismatch, a corrupt deflate stream) was returned from `add`. That aborted the walk, and `Extract` threw away every entry already stored. So a large war with one damaged, irrelevant entry produced zero packages with the feature on, while java's legacy path (which reads only the entries it needs) still cataloged it.

- a read error on one entry now drops that entry and marks the resolver truncated (`entry "x" could not be read: ...`), and the walk continues
- if the stream itself breaks partway (a truncated `.tar.gz`), whatever was read before the break is kept and the archive is truncated. With nothing read it is still an error, as before
- disk limit and decompression budget errors still stop the walk

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-09-24 13:28:53 -04:00
parent 14bae1a0d9
commit 0152e5b59c
4 changed files with 44 additions and 22 deletions
+20
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"io/fs"
"math/rand"
"os"
"path/filepath"
"strings"
@@ -344,6 +345,25 @@ func TestExtract_heldContentWrittenToDiskStaysUntilCleanup(t *testing.T) {
assert.Zero(t, disk)
}
func TestExtract_aTruncatedStreamKeepsWhatWasRead(t *testing.T) {
// a .tar.gz cut off partway breaks the stream for every entry after the cut; the ones before it are
// still good and must not be thrown away
noise := make([]byte, 64*1024)
rand.New(rand.NewSource(1)).Read(noise)
data := tarGzBytes(t, map[string]string{"a.txt": "first", "b.bin": string(noise)})
cut := data[:len(data)/2]
extracted, err := Extract(context.Background(), bytes.NewReader(cut), "", "app.tar.gz", NewLimiter(unboundedLimits), nil)
require.NoError(t, err)
require.NotNil(t, extracted)
t.Cleanup(extracted.Cleanup)
assert.True(t, extracted.Truncated)
locations, err := extracted.FilesByGlob("**")
require.NoError(t, err)
assert.Equal(t, []string{"a.txt"}, realPaths(locations))
}
func TestExtract_aPanicStillReleasesEverything(t *testing.T) {
// a decoder panicking on hostile bytes must not leak the charge or the spill file
ctx, root := scanContext(t)