This is an automated email from the ASF dual-hosted git repository.
laskoviymishka pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git
The following commit(s) were added to refs/heads/main by this push:
new 7760e064b fix(puffin): reject trailing data in footer payloads (#1580)
7760e064b is described below
commit 7760e064b52a3e4eeab62612ca1db301eba7047e
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 5 18:56:53 2026 +0200
fix(puffin): reject trailing data in footer payloads (#1580)
* fix(puffin): reject trailing data in footer payloads
Signed-off-by: Minh Vu <[email protected]>
* refactor(puffin): detect trailing values without copying
Signed-off-by: Minh Vu <[email protected]>
* fix(puffin): report trailing footer content consistently
* docs(puffin): explain strict footer validation
---------
Signed-off-by: Minh Vu <[email protected]>
---
puffin/puffin_reader.go | 13 ++++++++++++-
puffin/puffin_test.go | 39 +++++++++++++++++++++++++++++++++++++++
2 files changed, 51 insertions(+), 1 deletion(-)
diff --git a/puffin/puffin_reader.go b/puffin/puffin_reader.go
index 9276747b5..75304d541 100644
--- a/puffin/puffin_reader.go
+++ b/puffin/puffin_reader.go
@@ -224,11 +224,22 @@ func (r *Reader) readFooter() error {
}
payloadReader := io.NewSectionReader(r.r, footerStart+MagicSize,
payloadSize)
+ decoder := json.NewDecoder(payloadReader)
var footer Footer
- if err := json.NewDecoder(payloadReader).Decode(&footer); err != nil {
+ if err := decoder.Decode(&footer); err != nil {
return fmt.Errorf("puffin: decode footer JSON: %w", err)
}
+ // FooterPayloadSize defines a single JSON footer object. Reject
trailing
+ // content deliberately, even though some other Iceberg implementations
+ // accept padding or additional values inside the footer payload.
+ if decoder.More() {
+ return errors.New("puffin: unexpected content after footer
JSON")
+ }
+ if _, err := decoder.Token(); !errors.Is(err, io.EOF) {
+ return errors.New("puffin: unexpected content after footer
JSON")
+ }
+
// Validate blob metadata
if err := r.validateBlobs(footer.Blobs, footerStart); err != nil {
return err
diff --git a/puffin/puffin_test.go b/puffin/puffin_test.go
index 4c186ebb4..8d1631970 100644
--- a/puffin/puffin_test.go
+++ b/puffin/puffin_test.go
@@ -19,6 +19,7 @@ package puffin_test
import (
"bytes"
+ "encoding/binary"
"errors"
"math"
"os"
@@ -149,6 +150,15 @@ func validFileWithBlob() []byte {
return buf.Bytes()
}
+func fileWithFooterPayload(payload []byte) []byte {
+ data := append([]byte("PFA1PFA1"), payload...)
+ trailer := make([]byte, 12)
+ binary.LittleEndian.PutUint32(trailer[:4], uint32(len(payload)))
+ copy(trailer[8:], "PFA1")
+
+ return append(data, trailer...)
+}
+
// --- Tests ---
// TestRoundTrip verifies that data written by Writer can be read back by
Reader.
@@ -580,6 +590,35 @@ func TestReaderInvalidFile(t *testing.T) {
})
}
+func TestReaderRejectsTrailingFooterData(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ payload string
+ wantErr string
+ }{
+ {name: "single object", payload: `{"blobs":[]}`},
+ {name: "trailing whitespace", payload: "{\"blobs\":[]} \n\t"},
+ {name: "second JSON value", payload:
`{"blobs":[]}{"blobs":[]}`, wantErr: "unexpected content after footer JSON"},
+ {name: "second scalar value", payload: `{"blobs":[]}42`,
wantErr: "unexpected content after footer JSON"},
+ {name: "trailing garbage", payload: `{"blobs":[]}garbage`,
wantErr: "unexpected content after footer JSON"},
+ {name: "trailing closing delimiter", payload: `{"blobs":[]} ]`,
wantErr: "unexpected content after footer JSON"},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+ _, err :=
puffin.NewReader(bytes.NewReader(fileWithFooterPayload([]byte(test.payload))))
+ if test.wantErr == "" {
+ require.NoError(t, err)
+ } else {
+ require.ErrorContains(t, err, test.wantErr)
+ }
+ })
+ }
+}
+
// TestReaderBlobAccess verifies blob access methods work correctly.
// Tests the primary API for retrieving blob data from puffin files.
func TestReaderBlobAccess(t *testing.T) {