From 933c44d4d51922a2e23924ce9d3aa550b45d1e09 Mon Sep 17 00:00:00 2001 From: itsVentie Date: Thu, 23 Jul 2026 06:29:05 +0200 Subject: [PATCH] internal/inspect: handle reads after EOF in trackReader trackReader.Read panicked if called again after a read returned io.EOF, which can happen with readers that do not return EOF consistently, such as a terminal after Ctrl-D. Latch the EOF instead, per the io.Reader contract, so count stays consistent with the file size. Fixes #719 Co-authored-by: Filippo Valsorda --- internal/inspect/inspect.go | 5 ++-- internal/inspect/inspect_test.go | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/internal/inspect/inspect.go b/internal/inspect/inspect.go index b4f7d9b..628979e 100644 --- a/internal/inspect/inspect.go +++ b/internal/inspect/inspect.go @@ -103,12 +103,13 @@ type trackReader struct { } func (tr *trackReader) Read(p []byte) (int, error) { + if tr.done { + return 0, io.EOF + } n, err := tr.r.Read(p) tr.count += int64(n) if err == io.EOF { tr.done = true - } else if tr.done { - panic("non-EOF read after EOF") } return n, err } diff --git a/internal/inspect/inspect_test.go b/internal/inspect/inspect_test.go index f71cbbc..a9e8ac2 100644 --- a/internal/inspect/inspect_test.go +++ b/internal/inspect/inspect_test.go @@ -3,6 +3,7 @@ package inspect import ( "bytes" "fmt" + "io" "testing" "filippo.io/age/internal/format" @@ -52,6 +53,44 @@ func TestInspectTagStanzas(t *testing.T) { } } +// readAfterEOFReader returns io.EOF along with the last of its data, and then +// more data from subsequent Reads, like a terminal that received Ctrl-D +// followed by more input. +type readAfterEOFReader struct { + data []byte + eof bool +} + +func (r *readAfterEOFReader) Read(p []byte) (int, error) { + if r.eof { + return copy(p, "\n"), nil + } + n := copy(p, r.data) + r.data = r.data[n:] + if len(r.data) == 0 { + r.eof = true + return n, io.EOF + } + return n, nil +} + +func TestInspectReadAfterEOF(t *testing.T) { + f := buildFile(t, "X25519") + r := &readAfterEOFReader{data: f} + md, err := Inspect(r, -1) + if err != nil { + t.Fatalf("Inspect: %v", err) + } + if md.Version != "age-encryption.org/v1" { + t.Errorf("Version = %q, want age-encryption.org/v1", md.Version) + } + // If the reads after EOF were counted towards the file size, the extra + // bytes would show up in the payload size. + if md.Sizes.MinPayload != 0 { + t.Errorf("MinPayload = %d, want 0", md.Sizes.MinPayload) + } +} + func TestStreamOverhead(t *testing.T) { tests := []struct { payloadSize int64