This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git
The following commit(s) were added to refs/heads/main by this push:
new 1476769e fix(parquet): reject truncated bit-packed batches (#943)
1476769e is described below
commit 1476769ecda35b4527f2d8f1adc276257a9f19f4
Author: Minh Vu <[email protected]>
AuthorDate: Thu Jul 16 20:20:20 2026 +0200
fix(parquet): reject truncated bit-packed batches (#943)
---
parquet/file/column_reader.go | 49 ++++---
parquet/file/column_writer_v2_row_boundary_test.go | 6 +-
parquet/file/record_reader.go | 17 ++-
parquet/internal/encoding/boolean_decoder.go | 19 ++-
parquet/internal/encoding/decoder.go | 4 +-
parquet/internal/encoding/encoding_test.go | 20 +++
parquet/internal/encoding/levels.go | 20 ++-
parquet/internal/encoding/levels_test.go | 32 ++++-
parquet/internal/utils/bit_packing_avx2_amd64.go | 15 ++-
parquet/internal/utils/bit_packing_default.go | 16 ++-
parquet/internal/utils/bit_packing_neon_arm64.go | 15 ++-
parquet/internal/utils/bit_reader.go | 16 ++-
parquet/internal/utils/bit_reader_test.go | 149 ++++++++++++++++++++-
parquet/internal/utils/rle.go | 29 ++--
parquet/internal/utils/typed_rle_dict.go | 10 +-
15 files changed, 342 insertions(+), 75 deletions(-)
diff --git a/parquet/file/column_reader.go b/parquet/file/column_reader.go
index 53cc284a..cb4c5930 100644
--- a/parquet/file/column_reader.go
+++ b/parquet/file/column_reader.go
@@ -126,12 +126,14 @@ type ColumnChunkReader interface {
// when this returns 0, you're at the end of a page.
numAvailValues() int64
// read the definition levels and return the number of definitions,
- // and the number of values to be read (number of def levels == maxdef
level)
+ // the number of values to be read (number of def levels == maxdef
level),
+ // and any decoding error.
// it also populates the passed in slice which should be sized
appropriately.
- readDefinitionLevels(levels []int16) (int, int64)
+ readDefinitionLevels(levels []int16) (int, int64, error)
// read the repetition levels and return the number of repetition
levels read
+ // and any decoding error.
// also populates the passed in slice, which should be sized
appropriately.
- readRepetitionLevels(levels []int16) int
+ readRepetitionLevels(levels []int16) (int, error)
// a column is made up of potentially multiple pages across potentially
multiple
// row groups. A PageReader allows looping through the pages in a
single row group.
// When moving to another row group for reading, use setPageReader to
re-use the
@@ -531,10 +533,10 @@ func (c *columnChunkReader) setDecoderData(page DataPage,
lvlByteLen int64) erro
// (the number of levels that are equal to the max definition level).
//
// If the max definition level is 0, the assumption is that there no nulls in
the
-// column and therefore no definition levels to read, so it will always return
0, 0
-func (c *columnChunkReader) readDefinitionLevels(levels []int16) (totalDecoded
int, valuesToRead int64) {
+// column and therefore no definition levels to read, so it will always return
0, 0, nil.
+func (c *columnChunkReader) readDefinitionLevels(levels []int16) (totalDecoded
int, valuesToRead int64, err error) {
if c.descr.MaxDefinitionLevel() == 0 {
- return 0, 0
+ return 0, 0, nil
}
return c.definitionDecoder.Decode(levels)
@@ -545,17 +547,17 @@ func (c *columnChunkReader) readDefinitionLevels(levels
[]int16) (totalDecoded i
// slice).
//
// If max repetition level is 0, it is assumed there are no repetition levels,
-// and thus will always return 0.
-func (c *columnChunkReader) readRepetitionLevels(levels []int16) int {
+// and thus will always return 0, nil.
+func (c *columnChunkReader) readRepetitionLevels(levels []int16) (int, error) {
if c.descr.MaxRepetitionLevel() == 0 {
- return 0
+ return 0, nil
}
if len(c.repLvlBuffer) > 0 {
- return copy(levels, c.repLvlBuffer[c.numDecoded:])
+ return copy(levels, c.repLvlBuffer[c.numDecoded:]), nil
}
- nlevels, _ := c.repetitionDecoder.Decode(levels)
- return nlevels
+ nlevels, _, err := c.repetitionDecoder.Decode(levels)
+ return nlevels, err
}
// determineNumToRead reads the definition levels (and optionally populates
the repetition levels)
@@ -580,13 +582,20 @@ func (c *columnChunkReader) determineNumToRead(batchLen
int64, defLvls, repLvls
if defLvls == nil {
defLvls = c.getDefLvlBuffer(size)
}
- ndefs, toRead = c.readDefinitionLevels(defLvls[:size])
+ ndefs, toRead, err = c.readDefinitionLevels(defLvls[:size])
+ if err != nil {
+ return
+ }
} else {
toRead = size
}
if c.descr.MaxRepetitionLevel() > 0 && repLvls != nil {
- nreps := c.readRepetitionLevels(repLvls[:size])
+ nreps, repErr := c.readRepetitionLevels(repLvls[:size])
+ if repErr != nil {
+ err = repErr
+ return
+ }
if defLvls != nil && ndefs != nreps {
err = errors.New("parquet: number of decoded rep/def
levels did not match")
}
@@ -663,7 +672,11 @@ func (c *columnChunkReader) skipRows(nrows int64) error {
// the repetition levels for the entire page at once
and then go
// through them to find the right row.
repLvls := c.getRepLvlBuffer(c.numBuffered)
- nreps, _ := c.repetitionDecoder.Decode(repLvls)
+ nreps, _, err := c.repetitionDecoder.Decode(repLvls)
+ if err != nil {
+ c.err = err
+ return err
+ }
rowsSkipped := int64(0)
levelsToSkip := -1
@@ -686,7 +699,11 @@ func (c *columnChunkReader) skipRows(nrows int64) error {
var valuesToSkip int64
if c.descr.MaxDefinitionLevel() > 0 {
defLvls :=
c.getDefLvlBuffer(int64(levelsToSkip))
- _, valuesToSkip =
c.readDefinitionLevels(defLvls)
+ _, valuesToSkip, err =
c.readDefinitionLevels(defLvls)
+ if err != nil {
+ c.err = err
+ return err
+ }
} else {
valuesToSkip = int64(levelsToSkip)
}
diff --git a/parquet/file/column_writer_v2_row_boundary_test.go
b/parquet/file/column_writer_v2_row_boundary_test.go
index aa8e4b39..1492a937 100644
--- a/parquet/file/column_writer_v2_row_boundary_test.go
+++ b/parquet/file/column_writer_v2_row_boundary_test.go
@@ -133,7 +133,8 @@ func assertPagesStartOnRowBoundary(t *testing.T, raw
[]byte) {
require.NoError(t, dec.SetDataV2(v2.RepetitionLevelByteLen(),
maxRep, int(v2.NumValues()), v2.Data()))
levels := make([]int16, v2.NumValues())
- n, _ := dec.Decode(levels)
+ n, _, err := dec.Decode(levels)
+ require.NoError(t, err)
require.Greater(t, n, 0, "page %d decoded zero repetition
levels", v2Count)
require.Zerof(t, levels[0],
"DataPageV2 #%d starts mid-row (first repetition level
= %d, want 0)", v2Count, levels[0])
@@ -225,7 +226,8 @@ func TestWriteSpacedV2NullableListWideRow(t *testing.T) {
var dec encoding.LevelDecoder
require.NoError(t, dec.SetDataV2(v2.RepetitionLevelByteLen(),
maxRep, int(v2.NumValues()), v2.Data()))
levels := make([]int16, v2.NumValues())
- n, _ := dec.Decode(levels)
+ n, _, err := dec.Decode(levels)
+ require.NoError(t, err)
require.Greater(t, n, 0)
require.Zero(t, levels[0], "wide row page starts mid-row")
}
diff --git a/parquet/file/record_reader.go b/parquet/file/record_reader.go
index 3b98cdf0..87ae20f3 100644
--- a/parquet/file/record_reader.go
+++ b/parquet/file/record_reader.go
@@ -664,15 +664,26 @@ func (rr *recordReader) ReadRecords(numRecords int64)
(int64, error) {
defLevels := rr.DefLevels()[int(rr.levelsWritten):]
levelsRead := 0
+ var err error
// not present for non-repeated fields
if rr.Descriptor().MaxRepetitionLevel() > 0 {
repLevels :=
rr.RepLevels()[int(rr.levelsWritten):]
- levelsRead, _ =
rr.readDefinitionLevels(defLevels[:batchSize])
- if
rr.readRepetitionLevels(repLevels[:batchSize]) != levelsRead {
+ levelsRead, _, err =
rr.readDefinitionLevels(defLevels[:batchSize])
+ if err != nil {
+ return recordsRead, err
+ }
+ repetitionLevelsRead, err :=
rr.readRepetitionLevels(repLevels[:batchSize])
+ if err != nil {
+ return recordsRead, err
+ }
+ if repetitionLevelsRead != levelsRead {
return 0, errors.New("number of decoded
rep/def levels did not match")
}
} else if rr.Descriptor().MaxDefinitionLevel() > 0 {
- levelsRead, _ =
rr.readDefinitionLevels(defLevels[:batchSize])
+ levelsRead, _, err =
rr.readDefinitionLevels(defLevels[:batchSize])
+ if err != nil {
+ return recordsRead, err
+ }
}
if levelsRead == 0 {
diff --git a/parquet/internal/encoding/boolean_decoder.go
b/parquet/internal/encoding/boolean_decoder.go
index 4d1fa98b..f46a8288 100644
--- a/parquet/internal/encoding/boolean_decoder.go
+++ b/parquet/internal/encoding/boolean_decoder.go
@@ -235,16 +235,21 @@ func (dec *RleBooleanDecoder) Decode(out []bool) (int,
error) {
for n > 0 {
batch := shared_utils.Min(len(buf), n)
- decoded := dec.rleDec.GetBatch(buf[:batch])
- if decoded != batch {
- return max - n, io.ErrUnexpectedEOF
- }
+ decoded, err := dec.rleDec.GetBatch(buf[:batch])
- for i := 0; i < batch; i++ {
+ for i := 0; i < decoded; i++ {
out[i] = buf[i] != 0
}
- n -= batch
- out = out[batch:]
+ n -= decoded
+ out = out[decoded:]
+ if err != nil {
+ dec.nvals -= max - n
+ return max - n, err
+ }
+ if decoded != batch {
+ dec.nvals -= max - n
+ return max - n, io.ErrUnexpectedEOF
+ }
}
dec.nvals -= max
diff --git a/parquet/internal/encoding/decoder.go
b/parquet/internal/encoding/decoder.go
index c45b021d..9c124da2 100644
--- a/parquet/internal/encoding/decoder.go
+++ b/parquet/internal/encoding/decoder.go
@@ -167,7 +167,7 @@ func (d *dictDecoder[T]) DecodeIndices(numValues int, bldr
array.Builder) (int,
d.idxScratchSpace = d.idxScratchSpace[:n]
}
- n = d.idxDecoder.GetBatch(d.idxScratchSpace)
+ n, err := d.idxDecoder.GetBatch(d.idxScratchSpace)
toAppend := make([]int, n)
for i, v := range d.idxScratchSpace {
@@ -175,7 +175,7 @@ func (d *dictDecoder[T]) DecodeIndices(numValues int, bldr
array.Builder) (int,
}
bldr.(*array.BinaryDictionaryBuilder).AppendIndices(toAppend, nil)
d.nvals -= n
- return n, nil
+ return n, err
}
func (d *dictDecoder[T]) DecodeIndicesSpaced(numValues, nullCount int,
validBits []byte, offset int64, bldr array.Builder) (int, error) {
diff --git a/parquet/internal/encoding/encoding_test.go
b/parquet/internal/encoding/encoding_test.go
index 26e87aa6..3ec9c4d2 100644
--- a/parquet/internal/encoding/encoding_test.go
+++ b/parquet/internal/encoding/encoding_test.go
@@ -18,7 +18,9 @@ package encoding_test
import (
"bufio"
+ "encoding/binary"
"fmt"
+ "io"
"os"
"path"
"reflect"
@@ -605,6 +607,24 @@ func TestDictEncoding(t *testing.T) {
}
}
+func TestDictionaryDecoderRejectsTruncatedPackedLiteralRun(t *testing.T) {
+ column := schema.NewColumn(schema.NewInt32Node("int32",
parquet.Repetitions.Required, -1), 0, 0)
+ dictionaryData := make([]byte, 8)
+ binary.LittleEndian.PutUint32(dictionaryData, 10)
+ binary.LittleEndian.PutUint32(dictionaryData[4:], 20)
+
+ dictionary := encoding.NewDecoder(parquet.Types.Int32,
parquet.Encodings.Plain, column, memory.DefaultAllocator)
+ require.NoError(t, dictionary.SetData(2, dictionaryData))
+
+ decoder := encoding.NewDictDecoder(parquet.Types.Int32, column,
memory.DefaultAllocator)
+ decoder.SetDict(dictionary)
+ indices := append([]byte{1, 17}, make([]byte, 7)...)
+ require.NoError(t, decoder.SetData(64, indices))
+
+ _, err := decoder.(encoding.Int32Decoder).Decode(make([]int32, 64))
+ assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
+}
+
func TestWriteDeltaBitPackedInt32(t *testing.T) {
column := schema.NewColumn(schema.NewInt32Node("int32",
parquet.Repetitions.Required, -1), 0, 0)
diff --git a/parquet/internal/encoding/levels.go
b/parquet/internal/encoding/levels.go
index a63e9ab8..86c9b84f 100644
--- a/parquet/internal/encoding/levels.go
+++ b/parquet/internal/encoding/levels.go
@@ -21,6 +21,7 @@ import (
"encoding/binary"
"errors"
"fmt"
+ "io"
"math/bits"
"github.com/apache/arrow-go/v18/arrow/bitutil"
@@ -252,13 +253,14 @@ func (l *LevelDecoder) SetDataV2(nbytes int32, maxLvl
int16, nbuffered int, data
// Decode decodes the bytes that were set with SetData into the slice of levels
// returning the total number of levels that were decoded and the number of
// values which had a level equal to the max level, indicating how many
physical
-// values exist to be read.
-func (l *LevelDecoder) Decode(levels []int16) (int, int64) {
+// values exist to be read, along with any decoding error.
+func (l *LevelDecoder) Decode(levels []int16) (int, int64, error) {
var (
buf [1024]uint64
totaldecoded int
decoded int
valsToRead int64
+ err error
)
n := shared_utils.Min(int64(l.remaining), int64(len(levels)))
@@ -266,13 +268,13 @@ func (l *LevelDecoder) Decode(levels []int16) (int,
int64) {
batch := shared_utils.Min(1024, n)
switch l.encoding {
case format.Encoding_RLE:
- decoded = l.rle.GetBatch(buf[:batch])
+ decoded, err = l.rle.GetBatch(buf[:batch])
case format.Encoding_BIT_PACKED:
- decoded, _ = l.bit.GetBatch(uint(l.bitWidth),
buf[:batch])
+ decoded, err = l.bit.GetBatch(uint(l.bitWidth),
buf[:batch])
}
l.remaining -= decoded
totaldecoded += decoded
- n -= batch
+ n -= int64(decoded)
for idx, val := range buf[:decoded] {
lvl := int16(val)
@@ -282,7 +284,13 @@ func (l *LevelDecoder) Decode(levels []int16) (int, int64)
{
}
}
levels = levels[decoded:]
+ if err != nil {
+ return totaldecoded, valsToRead, err
+ }
+ if decoded != int(batch) {
+ return totaldecoded, valsToRead, io.ErrUnexpectedEOF
+ }
}
- return totaldecoded, valsToRead
+ return totaldecoded, valsToRead, nil
}
diff --git a/parquet/internal/encoding/levels_test.go
b/parquet/internal/encoding/levels_test.go
index b9378b66..52827d86 100644
--- a/parquet/internal/encoding/levels_test.go
+++ b/parquet/internal/encoding/levels_test.go
@@ -18,6 +18,7 @@ package encoding_test
import (
"encoding/binary"
+ "io"
"strconv"
"testing"
@@ -89,7 +90,8 @@ func verifyDecodingLvls(t *testing.T, enc parquet.Encoding,
maxLvl int16, input
// try multiple decoding on a single setdata call
for ct := 0; ct < decodeCount; ct++ {
offset := ct * numInnerLevels
- lvlCount, _ = decoder.Decode(output[:numInnerLevels])
+ lvlCount, _, err = decoder.Decode(output[:numInnerLevels])
+ assert.NoError(t, err)
assert.Equal(t, numInnerLevels, lvlCount)
assert.Equal(t, input[offset:offset+numInnerLevels],
output[:numInnerLevels])
}
@@ -101,12 +103,14 @@ func verifyDecodingLvls(t *testing.T, enc
parquet.Encoding, maxLvl int16, input
)
if remaining > 0 {
- lvlCount, _ = decoder.Decode(output[:remaining])
+ lvlCount, _, err = decoder.Decode(output[:remaining])
+ assert.NoError(t, err)
assert.Equal(t, remaining, lvlCount)
assert.Equal(t, input[levelsCompleted:], output[:remaining])
}
// test decode zero values
- lvlCount, _ = decoder.Decode(output[:1])
+ lvlCount, _, err = decoder.Decode(output[:1])
+ assert.NoError(t, err)
assert.Zero(t, lvlCount)
}
@@ -124,7 +128,8 @@ func verifyDecodingMultipleSetData(t *testing.T, enc
parquet.Encoding, max int16
assert.Len(t, output, numLevels)
_, err := decoder.SetData(enc, max, numLevels, buf[ct])
assert.NoError(t, err)
- lvlCount, _ = decoder.Decode(output)
+ lvlCount, _, err = decoder.Decode(output)
+ assert.NoError(t, err)
assert.Equal(t, numLevels, lvlCount)
assert.Equal(t, input[offset:offset+numLevels], output)
}
@@ -167,6 +172,22 @@ func TestLevelsDecodeMultipleBitWidth(t *testing.T) {
}
}
+func TestLevelDecoderPropagatesTruncatedRleLiteralRun(t *testing.T) {
+ encoded := append([]byte{17}, make([]byte, 7)...)
+ data := make([]byte, 4, 4+len(encoded))
+ binary.LittleEndian.PutUint32(data, uint32(len(encoded)))
+ data = append(data, encoded...)
+
+ var decoder encoding.LevelDecoder
+ _, err := decoder.SetData(parquet.Encodings.RLE, 1, 64, data)
+ assert.NoError(t, err)
+
+ levels := make([]int16, 64)
+ n, _, err := decoder.Decode(levels)
+ assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
+ assert.Equal(t, 32, n)
+}
+
func TestLevelsDecodeMultipleSetData(t *testing.T) {
t.Parallel()
@@ -286,7 +307,8 @@ func TestEncodeDecodeLevels(t *testing.T) {
assert.NoError(t, err)
var levelOut [numToEncode]int16
- total, vals := decoder.Decode(levelOut[:])
+ total, vals, err := decoder.Decode(levelOut[:])
+ assert.NoError(t, err)
assert.EqualValues(t, numToEncode, total)
assert.EqualValues(t, numones, vals)
assert.Equal(t, levels, levelOut[:])
diff --git a/parquet/internal/utils/bit_packing_avx2_amd64.go
b/parquet/internal/utils/bit_packing_avx2_amd64.go
index 5f1923fa..64fa86d9 100644
--- a/parquet/internal/utils/bit_packing_avx2_amd64.go
+++ b/parquet/internal/utils/bit_packing_avx2_amd64.go
@@ -31,23 +31,30 @@ var bufferPool = sync.Pool{New: func() interface{} { return
&bytes.Buffer{} }}
//go:noescape
func _unpack32_avx2(in, out unsafe.Pointer, batchSize, nbits int) (num int)
-func unpack32Avx2(in io.Reader, out []uint32, nbits int) int {
+func unpack32Avx2(in io.Reader, out []uint32, nbits int) (int, error) {
batch := len(out) / 32 * 32
n := batch * nbits / 8
if n <= 0 {
- return 0
+ return 0, nil
}
buffer := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buffer)
buffer.Reset()
buffer.Grow(n)
- io.CopyN(buffer, in, int64(n))
+ nread, err := io.CopyN(buffer, in, int64(n))
+ if err == io.EOF && int(nread)%(nbits*4) != 0 {
+ err = io.ErrUnexpectedEOF
+ }
+ completeBatch := int(nread) * 8 / nbits / 32 * 32
+ if completeBatch == 0 {
+ return 0, err
+ }
var (
input = unsafe.Pointer(&buffer.Bytes()[0])
output = unsafe.Pointer(&out[0])
)
- return _unpack32_avx2(input, output, len(out), nbits)
+ return _unpack32_avx2(input, output, completeBatch, nbits), err
}
diff --git a/parquet/internal/utils/bit_packing_default.go
b/parquet/internal/utils/bit_packing_default.go
index fa814f63..27f4b275 100644
--- a/parquet/internal/utils/bit_packing_default.go
+++ b/parquet/internal/utils/bit_packing_default.go
@@ -17,11 +17,12 @@
package utils
import (
+ "bytes"
"encoding/binary"
"io"
)
-var unpack32 func(io.Reader, []uint32, int) int = unpack32Default
+var unpack32 func(io.Reader, []uint32, int) (int, error) = unpack32Default
type unpackFunc func(in io.Reader, out []uint32)
@@ -1860,7 +1861,7 @@ func nullunpack32(_ io.Reader, out []uint32) {
}
}
-func unpack32Default(in io.Reader, out []uint32, nbits int) int {
+func unpack32Default(in io.Reader, out []uint32, nbits int) (int, error) {
batch := len(out) / 32 * 32
nloops := batch / 32
@@ -1933,11 +1934,16 @@ func unpack32Default(in io.Reader, out []uint32, nbits
int) int {
case 32:
f = unpack32_32
default:
- return 0
+ return 0, nil
}
+ var packed [128]byte
+ packedBytes := nbits * 4
for i := 0; i < nloops; i++ {
- f(in, out[i*32:])
+ if _, err := io.ReadFull(in, packed[:packedBytes]); err != nil {
+ return i * 32, err
+ }
+ f(bytes.NewReader(packed[:packedBytes]), out[i*32:])
}
- return batch
+ return batch, nil
}
diff --git a/parquet/internal/utils/bit_packing_neon_arm64.go
b/parquet/internal/utils/bit_packing_neon_arm64.go
index 580f9a1f..e474f64c 100644
--- a/parquet/internal/utils/bit_packing_neon_arm64.go
+++ b/parquet/internal/utils/bit_packing_neon_arm64.go
@@ -31,23 +31,30 @@ var bufferPool = sync.Pool{New: func() interface{} { return
&bytes.Buffer{} }}
//go:noescape
func _unpack32_neon(in, out unsafe.Pointer, batchSize, nbits int) (num int)
-func unpack32NEON(in io.Reader, out []uint32, nbits int) int {
+func unpack32NEON(in io.Reader, out []uint32, nbits int) (int, error) {
batch := len(out) / 32 * 32
n := batch * nbits / 8
if n <= 0 {
- return 0
+ return 0, nil
}
buffer := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buffer)
buffer.Reset()
buffer.Grow(n)
- io.CopyN(buffer, in, int64(n))
+ nread, err := io.CopyN(buffer, in, int64(n))
+ if err == io.EOF && int(nread)%(nbits*4) != 0 {
+ err = io.ErrUnexpectedEOF
+ }
+ completeBatch := int(nread) * 8 / nbits / 32 * 32
+ if completeBatch == 0 {
+ return 0, err
+ }
var (
input = unsafe.Pointer(&buffer.Bytes()[0])
output = unsafe.Pointer(&out[0])
)
- return _unpack32_neon(input, output, len(out), nbits)
+ return _unpack32_neon(input, output, completeBatch, nbits), err
}
diff --git a/parquet/internal/utils/bit_reader.go
b/parquet/internal/utils/bit_reader.go
index 5feaa8fa..c7e2e79c 100644
--- a/parquet/internal/utils/bit_reader.go
+++ b/parquet/internal/utils/bit_reader.go
@@ -287,9 +287,12 @@ func (b *BitReader) GetBatchIndex(bits uint, out
[]IndexType) (i int, err error)
b.reader.Seek(b.byteoffset, io.SeekStart)
// grab as many 32 byte chunks as possible in one shot
if i < length { // IndexType should be a 32 bit value so we can do
quick unpacking right into the output
- numUnpacked := unpack32(b.reader,
(*(*[]uint32)(unsafe.Pointer(&out)))[i:], int(bits))
+ numUnpacked, unpackErr := unpack32(b.reader,
(*(*[]uint32)(unsafe.Pointer(&out)))[i:], int(bits))
i += numUnpacked
b.byteoffset += int64(numUnpacked * int(bits) / 8)
+ if unpackErr != nil {
+ return i, unpackErr
+ }
}
// re-fill our buffer just in case.
@@ -455,16 +458,19 @@ func (b *BitReader) GetBatch(bits uint, out []uint64)
(int, error) {
for i < length {
// unpack groups of 32 bytes at a time into a buffer since it's
more efficient
unpackSize := utils.Min(buflen, length-i)
- numUnpacked := unpack32(b.reader, b.unpackBuf[:unpackSize],
int(bits))
- if numUnpacked == 0 {
- break
- }
+ numUnpacked, err := unpack32(b.reader,
b.unpackBuf[:unpackSize], int(bits))
for k := 0; k < numUnpacked; k++ {
out[i+k] = uint64(b.unpackBuf[k])
}
i += numUnpacked
b.byteoffset += int64(numUnpacked * int(bits) / 8)
+ if err != nil {
+ return i, err
+ }
+ if numUnpacked == 0 {
+ break
+ }
}
b.fillbuffer()
diff --git a/parquet/internal/utils/bit_reader_test.go
b/parquet/internal/utils/bit_reader_test.go
index b0384971..9df2f617 100644
--- a/parquet/internal/utils/bit_reader_test.go
+++ b/parquet/internal/utils/bit_reader_test.go
@@ -252,6 +252,147 @@ func TestBitArrayVals(t *testing.T) {
}
}
+func TestBitReaderRejectsTruncatedPackedBatches(t *testing.T) {
+ for width := 1; width <= 32; width++ {
+ for _, batchSize := range []int{32, 64} {
+ t.Run(fmt.Sprintf("width=%d/batch=%d", width,
batchSize), func(t *testing.T) {
+ input := bytes.Repeat([]byte{0xFF},
width*batchSize/8-1)
+ expected := batchSize - 32
+ want := uint64(1<<uint(width)) - 1
+
+ values := make([]uint64, batchSize)
+ for i := range values {
+ values[i] = math.MaxUint64
+ }
+ reader :=
utils.NewBitReader(bytes.NewReader(input))
+ n, err := reader.GetBatch(uint(width), values)
+ assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
+ assert.Equal(t, expected, n)
+ for _, got := range values[:n] {
+ assert.Equal(t, want, got)
+ }
+ for _, got := range values[n:] {
+ assert.Equal(t, uint64(math.MaxUint64),
got)
+ }
+
+ indices := make([]utils.IndexType, batchSize)
+ for i := range indices {
+ indices[i] = math.MinInt32
+ }
+ reader =
utils.NewBitReader(bytes.NewReader(input))
+ n, err = reader.GetBatchIndex(uint(width),
indices)
+ assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
+ assert.Equal(t, expected, n)
+ for _, got := range indices[:n] {
+ assert.Equal(t, int32(want), got)
+ }
+ for _, got := range indices[n:] {
+ assert.Equal(t, int32(math.MinInt32),
got)
+ }
+ })
+ }
+ }
+}
+
+func TestBitReaderStopsAtPackedGroupBoundary(t *testing.T) {
+ for width := 1; width <= 32; width++ {
+ t.Run(fmt.Sprintf("width=%d", width), func(t *testing.T) {
+ input := bytes.Repeat([]byte{0xFF}, width*4)
+
+ reader := utils.NewBitReader(bytes.NewReader(input))
+ values := make([]uint64, 64)
+ n, err := reader.GetBatch(uint(width), values)
+ assert.ErrorIs(t, err, io.EOF)
+ assert.Equal(t, 32, n)
+
+ reader = utils.NewBitReader(bytes.NewReader(input))
+ indices := make([]utils.IndexType, 64)
+ n, err = reader.GetBatchIndex(uint(width), indices)
+ assert.ErrorIs(t, err, io.EOF)
+ assert.Equal(t, 32, n)
+ })
+ }
+}
+
+type identityDictionaryConverter struct{}
+
+func (identityDictionaryConverter) Copy(out []uint64, indices
[]utils.IndexType) error {
+ for i, idx := range indices {
+ out[i] = uint64(idx)
+ }
+ return nil
+}
+
+func (identityDictionaryConverter) Fill(out []uint64, index utils.IndexType)
error {
+ for i := range out {
+ out[i] = uint64(index)
+ }
+ return nil
+}
+
+func (identityDictionaryConverter) FillZero(out []uint64) {
+ clear(out)
+}
+
+func (identityDictionaryConverter) IsValid(...utils.IndexType) bool { return
true }
+
+func (identityDictionaryConverter) IsValidSingle(utils.IndexType) bool {
return true }
+
+func TestRleDecoderPropagatesTruncatedPackedRun(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ payload int
+ err error
+ }{
+ {name: "partial group", payload: 7, err: io.ErrUnexpectedEOF},
+ {name: "group boundary", payload: 4, err: io.EOF},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ input := append([]byte{17}, bytes.Repeat([]byte{0xFF},
tc.payload)...)
+ decoder := utils.NewRleDecoder(bytes.NewReader(input),
1)
+
+ values := make([]uint64, 64)
+ n, err := decoder.GetBatch(values)
+ assert.ErrorIs(t, err, tc.err)
+ assert.Equal(t, 32, n)
+ for _, value := range values[:n] {
+ assert.Equal(t, uint64(1), value)
+ }
+ })
+ }
+}
+
+func TestTypedRleDecoderPropagatesTruncatedPackedRun(t *testing.T) {
+ converter := identityDictionaryConverter{}
+ for _, tc := range []struct {
+ name string
+ payload int
+ err error
+ }{
+ {name: "partial group", payload: 7, err: io.ErrUnexpectedEOF},
+ {name: "group boundary", payload: 4, err: io.EOF},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ input := append([]byte{17}, bytes.Repeat([]byte{0xFF},
tc.payload)...)
+
+ t.Run("unspaced", func(t *testing.T) {
+ decoder :=
utils.NewTypedRleDecoder[uint64](bytes.NewReader(input), 1)
+ _, err := decoder.GetBatchWithDict(converter,
make([]uint64, 64))
+ assert.ErrorIs(t, err, tc.err)
+ })
+
+ t.Run("spaced", func(t *testing.T) {
+ decoder :=
utils.NewTypedRleDecoder[uint64](bytes.NewReader(input), 1)
+ validBits := bytes.Repeat([]byte{0xFF}, 9)
+ bitutil.ClearBit(validBits, 0)
+
+ _, err :=
decoder.GetBatchWithDictSpaced(converter, make([]uint64, 65), 1, validBits, 0)
+ assert.ErrorIs(t, err, tc.err)
+ })
+ })
+ }
+}
+
func TestMixedValues(t *testing.T) {
const buflen = 1024
buf := make([]byte, buflen)
@@ -381,7 +522,9 @@ func (r *RLETestSuite) ValidateRle(vals []uint64, width
int, expected []byte, ex
r.Run("decode batch read", func() {
dec := utils.NewRleDecoder(bytes.NewReader(buf), width)
check := make([]uint64, len(vals))
- r.Equal(len(vals), dec.GetBatch(check))
+ n, err := dec.GetBatch(check)
+ r.NoError(err)
+ r.Equal(len(vals), n)
r.Equal(vals, check)
})
}
@@ -603,7 +746,9 @@ func (r *RLERandomSuite) checkRoundTrip(vals []uint64,
width int) bool {
res = res && r.Run("batch decode", func() {
dec := utils.NewRleDecoder(bytes.NewReader(buf[:encoded]),
width)
read := make([]uint64, len(vals))
- r.Require().Equal(len(vals), dec.GetBatch(read))
+ n, err := dec.GetBatch(read)
+ r.Require().NoError(err)
+ r.Require().Equal(len(vals), n)
r.Equal(vals, read)
})
diff --git a/parquet/internal/utils/rle.go b/parquet/internal/utils/rle.go
index 23a86598..596f2018 100644
--- a/parquet/internal/utils/rle.go
+++ b/parquet/internal/utils/rle.go
@@ -170,7 +170,7 @@ func (r *RleDecoder) Next() bool {
func (r *RleDecoder) GetValue() (uint64, bool) {
vals := make([]uint64, 1)
- n := r.GetBatch(vals)
+ n, _ := r.GetBatch(vals)
return vals[0], n == 1
}
@@ -201,7 +201,7 @@ func (r *RleDecoder) Discard(n int) int {
return read
}
-func (r *RleDecoder) GetBatch(values []uint64) int {
+func (r *RleDecoder) GetBatch(values []uint64) (int, error) {
read := 0
size := len(values)
@@ -220,26 +220,28 @@ func (r *RleDecoder) GetBatch(values []uint64) int {
out = out[repbatch:]
} else if r.litCount > 0 {
litbatch := min(remain, int(r.litCount))
- n, _ := r.r.GetBatch(uint(r.bitWidth), out[:litbatch])
+ n, err := r.r.GetBatch(uint(r.bitWidth), out[:litbatch])
+ r.litCount -= int32(n)
+ read += n
+ out = out[n:]
+ if err != nil {
+ return read, err
+ }
if n != litbatch {
- return read
+ return read, nil
}
-
- r.litCount -= int32(litbatch)
- read += litbatch
- out = out[litbatch:]
} else {
if !r.Next() {
- return read
+ return read, nil
}
}
}
- return read
+ return read, nil
}
func (r *RleDecoder) GetBatchSpaced(vals []uint64, nullcount int, validBits
[]byte, validBitsOffset int64) (int, error) {
if nullcount == 0 {
- return r.GetBatch(vals), nil
+ return r.GetBatch(vals)
}
converter := plainConverter[uint64]{}
@@ -259,7 +261,7 @@ func (r *RleDecoder) GetBatchSpaced(vals []uint64,
nullcount int, validBits []by
}
if block.AllSet() {
- processed = r.GetBatch(vals[:block.Len])
+ processed, err = r.GetBatch(vals[:block.Len])
} else if block.NoneSet() {
converter.FillZero(vals[:block.Len])
processed = int(block.Len)
@@ -271,6 +273,9 @@ func (r *RleDecoder) GetBatchSpaced(vals []uint64,
nullcount int, validBits []by
}
totalProcessed += processed
+ if err != nil {
+ return totalProcessed, err
+ }
vals = vals[int(block.Len):]
validBitsOffset += int64(block.Len)
diff --git a/parquet/internal/utils/typed_rle_dict.go
b/parquet/internal/utils/typed_rle_dict.go
index eafae2b8..07785cc2 100644
--- a/parquet/internal/utils/typed_rle_dict.go
+++ b/parquet/internal/utils/typed_rle_dict.go
@@ -91,7 +91,10 @@ func consumeLiterals[T parquet.ColumnTypes | uint64](r
*RleDecoder, dc Dictionar
batch := min(remain, int(r.litCount), len(buf))
buf = buf[:batch]
- n, _ := r.r.GetBatchIndex(uint(r.bitWidth), buf)
+ n, err := r.r.GetBatchIndex(uint(r.bitWidth), buf)
+ if err != nil {
+ return 0, 0, run, err
+ }
if n != batch {
return 0, 0, run, errors.New("was not able to retrieve correct
number of indexes")
}
@@ -163,7 +166,10 @@ func (r *TypedRleDecoder[T]) GetBatchWithDict(dc
DictionaryConverter[T], vals []
case r.litCount > 0:
litbatch := min(remain, int(r.litCount), 1024)
buf := indexbuffer[:litbatch]
- n, _ := r.r.GetBatchIndex(uint(r.bitWidth), buf)
+ n, err := r.r.GetBatchIndex(uint(r.bitWidth), buf)
+ if err != nil {
+ return read, err
+ }
if n != litbatch {
return read, nil
}