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 70e01f47a refactor(puffin): isolate LZ4 compressed footer handling 
(#2299)
70e01f47a is described below

commit 70e01f47a38c63b97e7ad2fbfa98def0097e3e50
Author: Minh Vu <[email protected]>
AuthorDate: Sun Oct 25 10:10:39 2026 +0100

    refactor(puffin): isolate LZ4 compressed footer handling (#2299)
---
 puffin/lz4_frame.go                   | 262 ++++++++++++++++++++++++++++++++++
 puffin/puffin_reader.go               | 237 +-----------------------------
 puffin/puffin_reader_internal_test.go |  29 ++++
 3 files changed, 296 insertions(+), 232 deletions(-)

diff --git a/puffin/lz4_frame.go b/puffin/lz4_frame.go
new file mode 100644
index 000000000..9b71e3627
--- /dev/null
+++ b/puffin/lz4_frame.go
@@ -0,0 +1,262 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package puffin
+
+import (
+       "bufio"
+       "encoding/binary"
+       "errors"
+       "fmt"
+       "io"
+       "math"
+
+       "github.com/pierrec/lz4/v4"
+)
+
+const (
+       lz4FrameMagic                 uint32 = 0x184d2204
+       lz4FrameHeaderPrefixSize             = 6 // magic (4) + FLG (1) + BD (1)
+       lz4FrameHeaderSize                   = 7 // prefix + header checksum
+       lz4FrameHeaderSizeWithContent        = 15
+       lz4FrameContentSizeFlag       byte   = 1 << 3
+       lz4FrameContentChecksumFlag   byte   = 1 << 2
+       lz4FrameBlockChecksumFlag     byte   = 1 << 4
+       lz4FrameVersionMask           byte   = 0xc0
+       lz4FrameVersionOne            byte   = 0x40
+       lz4FrameReservedFLGFlag       byte   = 1 << 1
+       lz4FrameDictionaryIDFlag      byte   = 1
+       lz4FrameReservedBDFlags       byte   = 0x8f
+       lz4FrameBlockSizeMask         uint32 = 0x7fffffff
+       lz4FrameReadBufferSize               = 4 << 20 // LZ4 maximum block 
size.
+)
+
+type countingReader struct {
+       reader io.Reader
+       count  int64
+       err    error
+}
+
+func (r *countingReader) Read(p []byte) (int, error) {
+       n, err := r.reader.Read(p)
+       r.count += int64(n)
+       if err != nil && !errors.Is(err, io.EOF) {
+               r.err = err
+       }
+
+       return n, err
+}
+
+type lz4FrameHeader struct {
+       flags          byte
+       headerSize     int
+       contentSize    uint64
+       blockMaxSize   int64
+       hasContentSize bool
+}
+
+func lz4BlockMaxSize(bd byte) (int64, error) {
+       switch (bd >> 4) & 0x7 {
+       case 4:
+               return 64 << 10, nil
+       case 5:
+               return 256 << 10, nil
+       case 6:
+               return 1 << 20, nil
+       case 7:
+               return 4 << 20, nil
+       default:
+               return 0, errors.New("invalid LZ4 frame block maximum size")
+       }
+}
+
+// readLZ4FrameHeader reads and validates the LZ4 frame descriptor without
+// consuming the payload. The content size is required by the Puffin format
+// for compressed footer payloads.
+func readLZ4FrameHeader(r io.ReaderAt) (lz4FrameHeader, error) {
+       var header [lz4FrameHeaderSizeWithContent]byte
+       if _, err := r.ReadAt(header[:lz4FrameHeaderSize], 0); err != nil {
+               return lz4FrameHeader{}, err
+       }
+
+       if binary.LittleEndian.Uint32(header[:4]) != lz4FrameMagic {
+               return lz4FrameHeader{}, errors.New("invalid LZ4 frame magic")
+       }
+
+       hasContentSize := header[4]&lz4FrameContentSizeFlag != 0
+       if header[4]&lz4FrameVersionMask != lz4FrameVersionOne {
+               return lz4FrameHeader{}, errors.New("invalid LZ4 frame version")
+       }
+       if header[4]&lz4FrameReservedFLGFlag != 0 {
+               return lz4FrameHeader{}, errors.New("invalid LZ4 frame reserved 
flag")
+       }
+       if header[4]&lz4FrameDictionaryIDFlag != 0 {
+               return lz4FrameHeader{}, errors.New("LZ4 frame dictionary ID is 
not supported")
+       }
+       if header[5]&lz4FrameReservedBDFlags != 0 {
+               return lz4FrameHeader{}, errors.New("invalid LZ4 frame reserved 
block descriptor flags")
+       }
+       blockMaxSize, err := lz4BlockMaxSize(header[5])
+       if err != nil {
+               return lz4FrameHeader{}, err
+       }
+       if hasContentSize {
+               if _, err := r.ReadAt(header[lz4FrameHeaderSize:], 
lz4FrameHeaderSize); err != nil {
+                       return lz4FrameHeader{}, err
+               }
+       }
+
+       headerSize := lz4FrameHeaderSize
+       if hasContentSize {
+               headerSize = lz4FrameHeaderSizeWithContent
+       }
+       valid, err := lz4.ValidFrameHeader(header[:headerSize])
+       if err != nil {
+               return lz4FrameHeader{}, err
+       }
+       if !valid {
+               return lz4FrameHeader{}, errors.New("invalid frame header")
+       }
+
+       frameHeader := lz4FrameHeader{
+               flags:          header[4],
+               headerSize:     headerSize,
+               blockMaxSize:   blockMaxSize,
+               hasContentSize: hasContentSize,
+       }
+       if hasContentSize {
+               frameHeader.contentSize = 
binary.LittleEndian.Uint64(header[lz4FrameHeaderPrefixSize : 
lz4FrameHeaderPrefixSize+8])
+       }
+
+       return frameHeader, nil
+}
+
+func validateLZ4FrameEnvelope(
+       r io.ReaderAt,
+       payloadSize int64,
+       flags byte,
+       headerSize int,
+       blockMaxSize int64,
+) error {
+       if payloadSize < int64(headerSize) {
+               return errors.New("LZ4 frame is shorter than its header")
+       }
+       if payloadSize-int64(headerSize) < 4 {
+               return errors.New("LZ4 frame is missing its end mark")
+       }
+
+       firstBlock := make([]byte, headerSize+4)
+       if _, err := r.ReadAt(firstBlock, 0); err != nil {
+               return fmt.Errorf("read LZ4 frame header and block header: %w", 
err)
+       }
+
+       blockSize := binary.LittleEndian.Uint32(firstBlock[headerSize:])
+       offset := int64(headerSize + 4)
+       reader := bufio.NewReaderSize(
+               io.NewSectionReader(r, offset, payloadSize-offset),
+               int(min(payloadSize-offset, lz4FrameReadBufferSize)),
+       )
+       var blockHeader [4]byte
+
+       for blockSize != 0 {
+               dataSize := int64(blockSize & lz4FrameBlockSizeMask)
+               if dataSize > blockMaxSize {
+                       return errors.New("LZ4 frame block exceeds declared 
maximum")
+               }
+               if dataSize > payloadSize-offset {
+                       return errors.New("LZ4 frame block exceeds its payload")
+               }
+               if _, err := io.CopyN(io.Discard, reader, dataSize); err != nil 
{
+                       return fmt.Errorf("read LZ4 frame block: %w", err)
+               }
+               offset += dataSize
+
+               if flags&lz4FrameBlockChecksumFlag != 0 {
+                       if payloadSize-offset < 4 {
+                               return errors.New("LZ4 frame block checksum is 
truncated")
+                       }
+                       if _, err := io.CopyN(io.Discard, reader, 4); err != 
nil {
+                               return fmt.Errorf("read LZ4 frame block 
checksum: %w", err)
+                       }
+                       offset += 4
+               }
+
+               if payloadSize-offset < int64(len(blockHeader)) {
+                       return errors.New("LZ4 frame is missing its end mark")
+               }
+               if _, err := io.ReadFull(reader, blockHeader[:]); err != nil {
+                       return fmt.Errorf("read LZ4 frame block header: %w", 
err)
+               }
+               offset += int64(len(blockHeader))
+
+               blockSize = binary.LittleEndian.Uint32(blockHeader[:])
+       }
+
+       if flags&lz4FrameContentChecksumFlag != 0 {
+               if payloadSize-offset < 4 {
+                       return errors.New("LZ4 frame content checksum is 
truncated")
+               }
+               if _, err := io.CopyN(io.Discard, reader, 4); err != nil {
+                       return fmt.Errorf("read LZ4 frame content checksum: 
%w", err)
+               }
+               offset += 4
+       }
+
+       if offset != payloadSize {
+               return errors.New("LZ4 frame has trailing data")
+       }
+
+       return nil
+}
+
+// newCompressedFooterReader validates a compressed footer's LZ4 envelope
+// before building a bounded decompression stream.
+func newCompressedFooterReader(payloadReader *io.SectionReader, payloadSize, 
maxFooterSize int64) (*countingReader, *io.LimitedReader, uint64, error) {
+       // Java, PyIceberg, and Rust currently reject compressed Puffin footers;
+       // keep this flag reader-only until those implementations catch up.
+       frameHeader, err := readLZ4FrameHeader(payloadReader)
+       if err != nil {
+               return nil, nil, 0, fmt.Errorf("puffin: inspect compressed 
footer LZ4 frame: %w", err)
+       }
+       if !frameHeader.hasContentSize {
+               return nil, nil, 0, errors.New("puffin: compressed footer LZ4 
frame is missing content size")
+       }
+       if frameHeader.contentSize > uint64(maxFooterSize) {
+               return nil, nil, 0, fmt.Errorf("puffin: footer exceeds maximum 
size %d", maxFooterSize)
+       }
+       if err := validateLZ4FrameEnvelope(
+               payloadReader,
+               payloadSize,
+               frameHeader.flags,
+               frameHeader.headerSize,
+               frameHeader.blockMaxSize,
+       ); err != nil {
+               return nil, nil, 0, fmt.Errorf("puffin: inspect compressed 
footer LZ4 frame: %w", err)
+       }
+       compressedPayloadReader := bufio.NewReaderSize(
+               io.NewSectionReader(payloadReader, 0, payloadSize),
+               int(min(payloadSize, lz4FrameReadBufferSize)),
+       )
+       compressed := &countingReader{reader: 
lz4.NewReader(compressedPayloadReader)}
+       limit := maxFooterSize
+       if limit < math.MaxInt64 {
+               limit++
+       }
+       limited := &io.LimitedReader{R: compressed, N: limit}
+
+       return compressed, limited, frameHeader.contentSize, nil
+}
diff --git a/puffin/puffin_reader.go b/puffin/puffin_reader.go
index 951961456..0a5b0987f 100644
--- a/puffin/puffin_reader.go
+++ b/puffin/puffin_reader.go
@@ -18,7 +18,6 @@
 package puffin
 
 import (
-       "bufio"
        "bytes"
        "cmp"
        "encoding/binary"
@@ -27,10 +26,7 @@ import (
        "fmt"
        "io"
        "maps"
-       "math"
        "slices"
-
-       "github.com/pierrec/lz4/v4"
 )
 
 // ErrNotPuffinFile is returned by NewReader when the input does not carry a
@@ -189,201 +185,6 @@ func cloneBlobMetadata(blob BlobMetadata) BlobMetadata {
 // in one read, reducing round-trips on cloud object storage.
 const defaultFooterReadSize = 8 * 1024 // 8 KB
 
-const (
-       lz4FrameMagic                 uint32 = 0x184d2204
-       lz4FrameHeaderPrefixSize             = 6 // magic (4) + FLG (1) + BD (1)
-       lz4FrameHeaderSize                   = 7 // prefix + header checksum
-       lz4FrameHeaderSizeWithContent        = 15
-       lz4FrameContentSizeFlag       byte   = 1 << 3
-       lz4FrameContentChecksumFlag   byte   = 1 << 2
-       lz4FrameBlockChecksumFlag     byte   = 1 << 4
-       lz4FrameVersionMask           byte   = 0xc0
-       lz4FrameVersionOne            byte   = 0x40
-       lz4FrameReservedFLGFlag       byte   = 1 << 1
-       lz4FrameDictionaryIDFlag      byte   = 1
-       lz4FrameReservedBDFlags       byte   = 0x8f
-       lz4FrameBlockSizeMask         uint32 = 0x7fffffff
-       lz4FrameReadBufferSize               = 4 << 20 // LZ4 maximum block 
size.
-)
-
-type countingReader struct {
-       reader io.Reader
-       count  int64
-       err    error
-}
-
-func (r *countingReader) Read(p []byte) (int, error) {
-       n, err := r.reader.Read(p)
-       r.count += int64(n)
-       if err != nil && !errors.Is(err, io.EOF) {
-               r.err = err
-       }
-
-       return n, err
-}
-
-type lz4FrameHeader struct {
-       flags          byte
-       headerSize     int
-       contentSize    uint64
-       blockMaxSize   int64
-       hasContentSize bool
-}
-
-func lz4BlockMaxSize(bd byte) (int64, error) {
-       switch (bd >> 4) & 0x7 {
-       case 4:
-               return 64 << 10, nil
-       case 5:
-               return 256 << 10, nil
-       case 6:
-               return 1 << 20, nil
-       case 7:
-               return 4 << 20, nil
-       default:
-               return 0, errors.New("invalid LZ4 frame block maximum size")
-       }
-}
-
-// readLZ4FrameHeader reads and validates the LZ4 frame descriptor without
-// consuming the payload. The content size is required by the Puffin format
-// for compressed footer payloads.
-func readLZ4FrameHeader(r io.ReaderAt) (lz4FrameHeader, error) {
-       var header [lz4FrameHeaderSizeWithContent]byte
-       if _, err := r.ReadAt(header[:lz4FrameHeaderSize], 0); err != nil {
-               return lz4FrameHeader{}, err
-       }
-
-       if binary.LittleEndian.Uint32(header[:4]) != lz4FrameMagic {
-               return lz4FrameHeader{}, errors.New("invalid LZ4 frame magic")
-       }
-
-       hasContentSize := header[4]&lz4FrameContentSizeFlag != 0
-       if header[4]&lz4FrameVersionMask != lz4FrameVersionOne {
-               return lz4FrameHeader{}, errors.New("invalid LZ4 frame version")
-       }
-       if header[4]&lz4FrameReservedFLGFlag != 0 {
-               return lz4FrameHeader{}, errors.New("invalid LZ4 frame reserved 
flag")
-       }
-       if header[4]&lz4FrameDictionaryIDFlag != 0 {
-               return lz4FrameHeader{}, errors.New("LZ4 frame dictionary ID is 
not supported")
-       }
-       if header[5]&lz4FrameReservedBDFlags != 0 {
-               return lz4FrameHeader{}, errors.New("invalid LZ4 frame reserved 
block descriptor flags")
-       }
-       blockMaxSize, err := lz4BlockMaxSize(header[5])
-       if err != nil {
-               return lz4FrameHeader{}, err
-       }
-       if hasContentSize {
-               if _, err := r.ReadAt(header[lz4FrameHeaderSize:], 
lz4FrameHeaderSize); err != nil {
-                       return lz4FrameHeader{}, err
-               }
-       }
-
-       headerSize := lz4FrameHeaderSize
-       if hasContentSize {
-               headerSize = lz4FrameHeaderSizeWithContent
-       }
-       valid, err := lz4.ValidFrameHeader(header[:headerSize])
-       if err != nil {
-               return lz4FrameHeader{}, err
-       }
-       if !valid {
-               return lz4FrameHeader{}, errors.New("invalid frame header")
-       }
-
-       frameHeader := lz4FrameHeader{
-               flags:          header[4],
-               headerSize:     headerSize,
-               blockMaxSize:   blockMaxSize,
-               hasContentSize: hasContentSize,
-       }
-       if hasContentSize {
-               frameHeader.contentSize = 
binary.LittleEndian.Uint64(header[lz4FrameHeaderPrefixSize : 
lz4FrameHeaderPrefixSize+8])
-       }
-
-       return frameHeader, nil
-}
-
-func validateLZ4FrameEnvelope(
-       r io.ReaderAt,
-       payloadSize int64,
-       flags byte,
-       headerSize int,
-       blockMaxSize int64,
-) error {
-       if payloadSize < int64(headerSize) {
-               return errors.New("LZ4 frame is shorter than its header")
-       }
-       if payloadSize-int64(headerSize) < 4 {
-               return errors.New("LZ4 frame is missing its end mark")
-       }
-
-       firstBlock := make([]byte, headerSize+4)
-       if _, err := r.ReadAt(firstBlock, 0); err != nil {
-               return fmt.Errorf("read LZ4 frame header and block header: %w", 
err)
-       }
-
-       blockSize := binary.LittleEndian.Uint32(firstBlock[headerSize:])
-       offset := int64(headerSize + 4)
-       reader := bufio.NewReaderSize(
-               io.NewSectionReader(r, offset, payloadSize-offset),
-               lz4FrameReadBufferSize,
-       )
-       var blockHeader [4]byte
-
-       for blockSize != 0 {
-               dataSize := int64(blockSize & lz4FrameBlockSizeMask)
-               if dataSize > blockMaxSize {
-                       return errors.New("LZ4 frame block exceeds declared 
maximum")
-               }
-               if dataSize > payloadSize-offset {
-                       return errors.New("LZ4 frame block exceeds its payload")
-               }
-               if _, err := io.CopyN(io.Discard, reader, dataSize); err != nil 
{
-                       return fmt.Errorf("read LZ4 frame block: %w", err)
-               }
-               offset += dataSize
-
-               if flags&lz4FrameBlockChecksumFlag != 0 {
-                       if payloadSize-offset < 4 {
-                               return errors.New("LZ4 frame block checksum is 
truncated")
-                       }
-                       if _, err := io.CopyN(io.Discard, reader, 4); err != 
nil {
-                               return fmt.Errorf("read LZ4 frame block 
checksum: %w", err)
-                       }
-                       offset += 4
-               }
-
-               if payloadSize-offset < int64(len(blockHeader)) {
-                       return errors.New("LZ4 frame is missing its end mark")
-               }
-               if _, err := io.ReadFull(reader, blockHeader[:]); err != nil {
-                       return fmt.Errorf("read LZ4 frame block header: %w", 
err)
-               }
-               offset += int64(len(blockHeader))
-
-               blockSize = binary.LittleEndian.Uint32(blockHeader[:])
-       }
-
-       if flags&lz4FrameContentChecksumFlag != 0 {
-               if payloadSize-offset < 4 {
-                       return errors.New("LZ4 frame content checksum is 
truncated")
-               }
-               if _, err := io.CopyN(io.Discard, reader, 4); err != nil {
-                       return fmt.Errorf("read LZ4 frame content checksum: 
%w", err)
-               }
-               offset += 4
-       }
-
-       if offset != payloadSize {
-               return errors.New("LZ4 frame has trailing data")
-       }
-
-       return nil
-}
-
 // readFooter reads and parses the footer from the Puffin file.
 func (r *Reader) readFooter() error {
        // Read a larger chunk from the end to minimize round-trips on cloud 
storage.
@@ -466,42 +267,14 @@ func (r *Reader) readFooter() error {
        var compressedContentSize uint64
        var limitedFooter *io.LimitedReader
        if flags&FooterFlagCompressed != 0 {
-               // Java, PyIceberg, and Rust currently reject compressed Puffin 
footers;
-               // keep this flag reader-only until those implementations catch 
up.
-
-               frameHeader, err := readLZ4FrameHeader(payloadReader)
+               var err error
+               compressedFooter, limitedFooter, compressedContentSize, err = 
newCompressedFooterReader(
+                       payloadReader, payloadSize, r.maxFooterSize,
+               )
                if err != nil {
-                       return fmt.Errorf("puffin: inspect compressed footer 
LZ4 frame: %w", err)
-               }
-               if !frameHeader.hasContentSize {
-                       return errors.New("puffin: compressed footer LZ4 frame 
is missing content size")
-               }
-               if frameHeader.contentSize > uint64(r.maxFooterSize) {
-                       return fmt.Errorf("puffin: footer exceeds maximum size 
%d", r.maxFooterSize)
-               }
-               if err := validateLZ4FrameEnvelope(
-                       payloadReader,
-                       payloadSize,
-                       frameHeader.flags,
-                       frameHeader.headerSize,
-                       frameHeader.blockMaxSize,
-               ); err != nil {
-                       return fmt.Errorf("puffin: inspect compressed footer 
LZ4 frame: %w", err)
+                       return err
                }
 
-               compressedContentSize = frameHeader.contentSize
-               compressedPayloadReader := bufio.NewReaderSize(
-                       io.NewSectionReader(payloadReader, 0, payloadSize),
-                       lz4FrameReadBufferSize,
-               )
-               compressedFooter = &countingReader{reader: 
lz4.NewReader(compressedPayloadReader)}
-               footerReader = compressedFooter
-
-               limitedFooterSize := r.maxFooterSize
-               if limitedFooterSize < math.MaxInt64 {
-                       limitedFooterSize++
-               }
-               limitedFooter = &io.LimitedReader{R: footerReader, N: 
limitedFooterSize}
                footerReader = limitedFooter
        }
 
diff --git a/puffin/puffin_reader_internal_test.go 
b/puffin/puffin_reader_internal_test.go
index 1508f5f02..d35bb3dfd 100644
--- a/puffin/puffin_reader_internal_test.go
+++ b/puffin/puffin_reader_internal_test.go
@@ -142,3 +142,32 @@ func TestReaderPreservesLZ4BlockChecksumError(t 
*testing.T) {
        _, err := NewReader(bytes.NewReader(data))
        require.ErrorContains(t, err, "invalid block checksum")
 }
+
+func TestNewCompressedFooterReaderReturnsBoundedStream(t *testing.T) {
+       payload := []byte(`{"blobs":[]}`)
+       frame := compressedReaderTestFrame(t, payload, 
lz4.ChecksumOption(false))
+       payloadReader := io.NewSectionReader(bytes.NewReader(frame), 0, 
int64(len(frame)))
+
+       compressed, limited, contentSize, err := newCompressedFooterReader(
+               payloadReader, int64(len(frame)), int64(len(payload)),
+       )
+       require.NoError(t, err)
+       require.Equal(t, uint64(len(payload)), contentSize)
+
+       decoded, err := io.ReadAll(limited)
+       require.NoError(t, err)
+       require.Equal(t, payload, decoded)
+       require.Equal(t, int64(len(payload)), compressed.count)
+       require.Equal(t, int64(1), limited.N)
+}
+
+func TestNewCompressedFooterReaderRejectsOversizedDeclaredContent(t 
*testing.T) {
+       payload := []byte(`{"blobs":[]}`)
+       frame := compressedReaderTestFrame(t, payload)
+       payloadReader := io.NewSectionReader(bytes.NewReader(frame), 0, 
int64(len(frame)))
+
+       _, _, _, err := newCompressedFooterReader(
+               payloadReader, int64(len(frame)), int64(len(payload)-1),
+       )
+       require.ErrorContains(t, err, "footer exceeds maximum size")
+}

Reply via email to