diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go index 3d34136989..23a29b51dc 100644 --- a/internal/fingerprint/sources_checksum.go +++ b/internal/fingerprint/sources_checksum.go @@ -88,6 +88,10 @@ func (*ChecksumChecker) Kind() string { return "checksum" } +// readerOnly hides any WriterTo/ReaderFrom implementation of the wrapped +// reader, forcing io.CopyBuffer to use the caller-provided buffer. +type readerOnly struct{ io.Reader } + func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) { sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore()) if err != nil { @@ -101,14 +105,18 @@ func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) { if _, err := io.CopyBuffer(h, strings.NewReader(filepath.Base(f)), buf); err != nil { return "", err } - f, err := os.Open(f) + file, err := os.Open(f) if err != nil { return "", err } - if _, err = io.CopyBuffer(h, f, buf); err != nil { + // Wrap the file in a plain io.Reader so io.CopyBuffer cannot take the + // (*os.File).WriteTo fast path, which ignores buf and allocates a fresh + // 32KiB buffer for every file. Reusing buf keeps this loop allocation-free. + if _, err = io.CopyBuffer(h, readerOnly{file}, buf); err != nil { + file.Close() return "", err } - f.Close() + file.Close() } hash := h.Sum128()