fix: hash each file once in the file digest cataloger (#5491)

Symlinks resolve to their target, so the same coordinates could show up
once per path that reaches them and get hashed again each time. The
results map hid this but the work was repeated. Drop repeated
coordinates before digesting.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-10-29 12:07:01 -04:00
committed by GitHub
parent 12f0e2d837
commit 1998095808
2 changed files with 52 additions and 0 deletions
@@ -50,6 +50,18 @@ func (i *Cataloger) Catalog(ctx context.Context, resolver file.Resolver, coordin
}
}
// symlinks resolve to the same file, so drop repeated coordinates to avoid hashing the same contents more than once
seen := make(map[file.Coordinates]struct{}, len(locations))
unique := locations[:0]
for _, l := range locations {
if _, ok := seen[l.Coordinates]; ok {
continue
}
seen[l.Coordinates] = struct{}{}
unique = append(unique, l)
}
locations = unique
prog := catalogingProgress(int64(len(locations)))
err := sync.Collect(&ctx, cataloging.ExecutorFile, sync.ToSeq(locations), func(location file.Location) ([]file.Digest, error) {
@@ -7,6 +7,7 @@ import (
"io"
"os"
"path/filepath"
"sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -207,6 +208,45 @@ func TestFileDigestCataloger_GivenCoordinates(t *testing.T) {
}
// countingResolver records how many times each coordinate's contents are opened
type countingResolver struct {
file.Resolver
lock sync.Mutex
counts map[file.Coordinates]int
}
func (r *countingResolver) FileContentsByLocation(l file.Location) (io.ReadCloser, error) {
r.lock.Lock()
r.counts[l.Coordinates]++
r.lock.Unlock()
return r.Resolver.FileContentsByLocation(l)
}
func TestDigestsCataloger_hashesEachCoordinateOnce(t *testing.T) {
testImage := "image-file-type-mix"
testutils.SkipWithoutLinuxContainers(t)
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
Reference: testImage,
})
resolver, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
// /symlink-1 and /file-1.txt both resolve to the same coordinates
counter := &countingResolver{Resolver: resolver, counts: make(map[file.Coordinates]int)}
actual, err := NewCataloger([]crypto.Hash{crypto.MD5}).Catalog(context.Background(), counter)
require.NoError(t, err)
require.NotEmpty(t, actual)
for c, n := range counter.counts {
assert.Equal(t, 1, n, "contents of %+v opened %d times", c, n)
}
}
type panicReader struct{}
func (panicReader) Read([]byte) (int, error) { panic("boom") }