laskoviymishka commented on code in PR #1678:
URL: https://github.com/apache/iceberg-go/pull/1678#discussion_r3740296270


##########
puffin/puffin_reader.go:
##########
@@ -224,11 +238,32 @@ func (r *Reader) readFooter() error {
        }
 
        payloadReader := io.NewSectionReader(r.r, footerStart+MagicSize, 
payloadSize)
-       decoder := json.NewDecoder(payloadReader)
+       var footerReader io.Reader = payloadReader
+       if flags&FooterFlagCompressed != 0 {
+               lz4Reader := lz4.NewReader(payloadReader)
+               var firstByte [1]byte
+               n, err := lz4Reader.Read(firstByte[:])
+               if err != nil && !errors.Is(err, io.EOF) {
+                       return fmt.Errorf("puffin: initialize compressed 
footer: %w", err)
+               }
+               if lz4Reader.Size() == 0 {
+                       return errors.New("puffin: compressed footer LZ4 frame 
is missing content size")
+               }
+               if int64(lz4Reader.Size()) > r.maxFooterSize {

Review Comment:
   `lz4Reader.Size()` is a `uint64`; casting to `int64` first means a frame 
advertising a content size above `math.MaxInt64` wraps negative and sails 
straight past this guard. The `LimitedReader` still catches it downstream, but 
this early check is exactly the fail-fast-before-allocation gate, and a one-bit 
flip in the frame header disables it.
   
   Comparing in uint64 space closes it, since `r.maxFooterSize` is already 
guaranteed positive above:
   
   ```go
   if lz4Reader.Size() > uint64(r.maxFooterSize) {
   ```



##########
puffin/puffin_reader.go:
##########
@@ -224,11 +238,32 @@ func (r *Reader) readFooter() error {
        }
 
        payloadReader := io.NewSectionReader(r.r, footerStart+MagicSize, 
payloadSize)
-       decoder := json.NewDecoder(payloadReader)
+       var footerReader io.Reader = payloadReader
+       if flags&FooterFlagCompressed != 0 {
+               lz4Reader := lz4.NewReader(payloadReader)
+               var firstByte [1]byte
+               n, err := lz4Reader.Read(firstByte[:])
+               if err != nil && !errors.Is(err, io.EOF) {
+                       return fmt.Errorf("puffin: initialize compressed 
footer: %w", err)
+               }
+               if lz4Reader.Size() == 0 {

Review Comment:
   This leans on the 1-byte `Read()` having populated `Size()` as a side 
effect. pierrec parses the frame descriptor on the first Read today, but that 
ordering isn't part of the documented contract. A lazy-parsing version, or a 
different lz4 library, would return `Size() == 0` for a perfectly valid frame 
and we'd reject every Trino-written file.
   
   `Size() == 0` also can't tell "no content-size field" (the case the spec 
actually prohibits) from "field present, declared as 0", so the "missing 
content size" error can misfire on the degenerate case.
   
   I'd read the frame descriptor explicitly via the lz4 header API instead of 
the peek-and-reconstruct dance, or just decompress into a bounded buffer and 
let the `LimitedReader` be the only size gate. wdyt?



##########
puffin/puffin_test.go:
##########
@@ -619,6 +638,60 @@ func TestReaderRejectsTrailingFooterData(t *testing.T) {
        }
 }
 
+func TestReaderReadsLZ4CompressedFooter(t *testing.T) {
+       data := fileWithCompressedFooterPayload(t, 
[]byte(`{"blobs":[],"properties":{"source":"lz4"}}`))
+
+       r, err := puffin.NewReader(bytes.NewReader(data))
+       require.NoError(t, err)
+       assert.Equal(t, "lz4", r.Properties()["source"])
+       assert.Empty(t, r.Blobs())
+}
+
+func TestReaderRejectsLZ4CompressedFooterWithoutContentSize(t *testing.T) {
+       payload := []byte(`{"blobs":[]}`)

Review Comment:
   This inlines the same frame-building as `fileWithCompressedFooterPayload` 
minus the `SizeOption` call, so if the helper ever regressed in a way that 
dropped `SizeOption` this test would still pass green. I'd factor out a 
`fileWithCompressedFooterPayloadNoSize(t, payload)` so the two stay in sync.



##########
puffin/puffin_reader.go:
##########
@@ -224,11 +238,32 @@ func (r *Reader) readFooter() error {
        }
 
        payloadReader := io.NewSectionReader(r.r, footerStart+MagicSize, 
payloadSize)
-       decoder := json.NewDecoder(payloadReader)
+       var footerReader io.Reader = payloadReader
+       if flags&FooterFlagCompressed != 0 {
+               lz4Reader := lz4.NewReader(payloadReader)
+               var firstByte [1]byte
+               n, err := lz4Reader.Read(firstByte[:])
+               if err != nil && !errors.Is(err, io.EOF) {
+                       return fmt.Errorf("puffin: initialize compressed 
footer: %w", err)
+               }
+               if lz4Reader.Size() == 0 {
+                       return errors.New("puffin: compressed footer LZ4 frame 
is missing content size")
+               }
+               if int64(lz4Reader.Size()) > r.maxFooterSize {
+                       return fmt.Errorf("puffin: footer exceeds maximum size 
%d", r.maxFooterSize)
+               }
+               footerReader = io.MultiReader(bytes.NewReader(firstByte[:n]), 
lz4Reader)
+       }
+
+       limitedFooter := &io.LimitedReader{R: footerReader, N: r.maxFooterSize 
+ 1}

Review Comment:
   Small edge: if a caller passes `WithMaxFooterSize(math.MaxInt64)`, `N: 
r.maxFooterSize + 1` overflows to `MinInt64`, `LimitedReader` treats `N <= 0` 
as an empty stream, and the decoder fails with "unexpected end of JSON input", 
which points nowhere near the actual misconfig. Worth capping `N` at the 
sentinel when `maxFooterSize` is near `MaxInt64`.



##########
puffin/puffin_reader.go:
##########
@@ -224,11 +238,32 @@ func (r *Reader) readFooter() error {
        }
 
        payloadReader := io.NewSectionReader(r.r, footerStart+MagicSize, 
payloadSize)
-       decoder := json.NewDecoder(payloadReader)
+       var footerReader io.Reader = payloadReader
+       if flags&FooterFlagCompressed != 0 {
+               lz4Reader := lz4.NewReader(payloadReader)
+               var firstByte [1]byte
+               n, err := lz4Reader.Read(firstByte[:])
+               if err != nil && !errors.Is(err, io.EOF) {
+                       return fmt.Errorf("puffin: initialize compressed 
footer: %w", err)
+               }
+               if lz4Reader.Size() == 0 {
+                       return errors.New("puffin: compressed footer LZ4 frame 
is missing content size")
+               }
+               if int64(lz4Reader.Size()) > r.maxFooterSize {
+                       return fmt.Errorf("puffin: footer exceeds maximum size 
%d", r.maxFooterSize)
+               }
+               footerReader = io.MultiReader(bytes.NewReader(firstByte[:n]), 
lz4Reader)
+       }
+
+       limitedFooter := &io.LimitedReader{R: footerReader, N: r.maxFooterSize 
+ 1}
+       decoder := json.NewDecoder(limitedFooter)
        var footer Footer
        if err := decoder.Decode(&footer); err != nil {
                return fmt.Errorf("puffin: decode footer JSON: %w", err)
        }
+       if limitedFooter.N == 0 {

Review Comment:
   `json.Decoder` reads ahead into its own buffer, so by the time `Decode` 
returns `N` may already be 0 because the decoder pulled in trailing bytes past 
the JSON value, not because the footer itself overflowed. In that case this 
reports "footer exceeds maximum size" when the real problem is trailing 
content, and the deliberate trailing-content check just below never gets a 
chance to fire.
   
   I'd move the `N == 0` guard to wrap the trailing-content read rather than 
sit between `Decode` and it, or check `decoder.Buffered()` to tell the two 
apart.



##########
puffin/puffin_test.go:
##########
@@ -619,6 +638,60 @@ func TestReaderRejectsTrailingFooterData(t *testing.T) {
        }
 }
 
+func TestReaderReadsLZ4CompressedFooter(t *testing.T) {
+       data := fileWithCompressedFooterPayload(t, 
[]byte(`{"blobs":[],"properties":{"source":"lz4"}}`))
+
+       r, err := puffin.NewReader(bytes.NewReader(data))
+       require.NoError(t, err)
+       assert.Equal(t, "lz4", r.Properties()["source"])
+       assert.Empty(t, r.Blobs())
+}
+
+func TestReaderRejectsLZ4CompressedFooterWithoutContentSize(t *testing.T) {
+       payload := []byte(`{"blobs":[]}`)
+       var compressed bytes.Buffer
+       writer := lz4.NewWriter(&compressed)
+       _, err := writer.Write(payload)
+       require.NoError(t, err)
+       require.NoError(t, writer.Close())
+
+       data := append([]byte("PFA1PFA1"), compressed.Bytes()...)
+       trailer := make([]byte, 12)
+       binary.LittleEndian.PutUint32(trailer[:4], uint32(compressed.Len()))
+       binary.LittleEndian.PutUint32(trailer[4:8], puffin.FooterFlagCompressed)
+       copy(trailer[8:], "PFA1")
+       data = append(data, trailer...)
+
+       _, err = puffin.NewReader(bytes.NewReader(data))
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "missing content size")
+}
+
+func TestReaderEnforcesFooterSizeLimit(t *testing.T) {
+       t.Run("json payload", func(t *testing.T) {
+               data := 
fileWithFooterPayload([]byte(`{"blobs":[],"properties":{"large":"0123456789"}}`))
+               _, err := puffin.NewReader(bytes.NewReader(data), 
puffin.WithMaxFooterSize(16))
+               require.Error(t, err)
+               assert.Contains(t, err.Error(), "footer")

Review Comment:
   `assert.Contains(t, err.Error(), "footer")` would also pass on unrelated 
errors like "invalid footer start magic". The two sibling subtests assert the 
specific "footer exceeds maximum size", so I'd match that here too.



##########
puffin/puffin_test.go:
##########
@@ -619,6 +638,60 @@ func TestReaderRejectsTrailingFooterData(t *testing.T) {
        }
 }
 
+func TestReaderReadsLZ4CompressedFooter(t *testing.T) {
+       data := fileWithCompressedFooterPayload(t, 
[]byte(`{"blobs":[],"properties":{"source":"lz4"}}`))
+
+       r, err := puffin.NewReader(bytes.NewReader(data))
+       require.NoError(t, err)
+       assert.Equal(t, "lz4", r.Properties()["source"])
+       assert.Empty(t, r.Blobs())
+}
+
+func TestReaderRejectsLZ4CompressedFooterWithoutContentSize(t *testing.T) {
+       payload := []byte(`{"blobs":[]}`)
+       var compressed bytes.Buffer
+       writer := lz4.NewWriter(&compressed)
+       _, err := writer.Write(payload)
+       require.NoError(t, err)
+       require.NoError(t, writer.Close())
+
+       data := append([]byte("PFA1PFA1"), compressed.Bytes()...)
+       trailer := make([]byte, 12)
+       binary.LittleEndian.PutUint32(trailer[:4], uint32(compressed.Len()))
+       binary.LittleEndian.PutUint32(trailer[4:8], puffin.FooterFlagCompressed)
+       copy(trailer[8:], "PFA1")
+       data = append(data, trailer...)
+
+       _, err = puffin.NewReader(bytes.NewReader(data))
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "missing content size")
+}
+
+func TestReaderEnforcesFooterSizeLimit(t *testing.T) {
+       t.Run("json payload", func(t *testing.T) {
+               data := 
fileWithFooterPayload([]byte(`{"blobs":[],"properties":{"large":"0123456789"}}`))
+               _, err := puffin.NewReader(bytes.NewReader(data), 
puffin.WithMaxFooterSize(16))
+               require.Error(t, err)
+               assert.Contains(t, err.Error(), "footer")
+       })
+
+       t.Run("trailing content beyond limit", func(t *testing.T) {
+               payload := append([]byte(`{"blobs":[]}`), bytes.Repeat([]byte{' 
'}, 32)...)
+               data := fileWithFooterPayload(payload)
+               _, err := puffin.NewReader(bytes.NewReader(data), 
puffin.WithMaxFooterSize(16))
+               require.Error(t, err)
+               assert.Contains(t, err.Error(), "footer exceeds maximum size")
+       })
+
+       t.Run("compressed advertised size", func(t *testing.T) {

Review Comment:
   All three size-limit subtests hit the early advertised-size rejection; none 
exercise the `LimitedReader` as the actual backstop, i.e. a frame that 
advertises a size within the limit but decompresses to more than it. That's the 
one path that matters if the early check is ever bypassed (see the uint64 issue 
above), and lz4 won't always validate actual output against the advertised size.
   
   I'd add a subtest that builds a frame with `SizeOption(<= maxFooterSize)` 
but writes a payload that decompresses past the limit, and assert it still 
errors with "footer exceeds maximum size".



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to