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 f6708819 fix(parquet/metadata): reject malformed fixed-width 
statistics (#1010)
f6708819 is described below

commit f670881919035fa6a07ea10ac70ddf3330eba31c
Author: Minh Vu <[email protected]>
AuthorDate: Mon Jul 27 22:30:38 2026 +0200

    fix(parquet/metadata): reject malformed fixed-width statistics (#1010)
    
    ### Rationale for this change
    
    Fixed-width Parquet statistics must use the exact PLAIN-encoded physical
    width. Padding a shorter value invents a different bound, while
    formatting the raw bytes can panic. Variable-length BYTE_ARRAY bounds
    should remain unchanged.
    
    ### What changes are included in this PR?
    
    - Ignore min/max when a present fixed-width statistic does not match the
    declared physical width.
    - Preserve variable-length BYTE_ARRAY values exactly as encoded.
    - Return malformed scalar bytes unchanged from `GetStatValue` instead of
    padding or indexing past them.
    - Regenerate the typed statistics implementation from its template.
    
    ### Are these changes tested?
    
    Yes. Focused tests cover malformed numeric and FIXED_LEN_BYTE_ARRAY
    values, short scalar formatting, and BYTE_ARRAY preservation. The
    metadata and Parquet file test suites pass.
---
 parquet/metadata/statistics.go                |  30 +++++++
 parquet/metadata/statistics_test.go           |  36 ++++++++
 parquet/metadata/statistics_types.gen.go      | 113 +++++++++++++++++---------
 parquet/metadata/statistics_types.gen.go.tmpl |  14 ++--
 4 files changed, 148 insertions(+), 45 deletions(-)

diff --git a/parquet/metadata/statistics.go b/parquet/metadata/statistics.go
index 0aa9ef46..1ef11653 100644
--- a/parquet/metadata/statistics.go
+++ b/parquet/metadata/statistics.go
@@ -605,7 +605,37 @@ func (FixedLenByteArrayStatistics) cleanStat(minMax 
minmaxPairFixedLenByteArray)
        return &minMax
 }
 
+func fixedStatByteWidth(typ parquet.Type) (int, bool) {
+       switch typ {
+       case parquet.Types.Boolean:
+               return 1, true
+       case parquet.Types.Int32, parquet.Types.Float:
+               return 4, true
+       case parquet.Types.Int64, parquet.Types.Double:
+               return 8, true
+       case parquet.Types.Int96:
+               return 12, true
+       default:
+               return 0, false
+       }
+}
+
+func validEncodedStat(descr *schema.Column, val []byte) bool {
+       if descr.PhysicalType() == parquet.Types.ByteArray {
+               return true
+       }
+       if descr.PhysicalType() == parquet.Types.FixedLenByteArray {
+               return len(val) == int(descr.TypeLength())
+       }
+       width, ok := fixedStatByteWidth(descr.PhysicalType())
+       return ok && len(val) == width
+}
+
 func GetStatValue(typ parquet.Type, val []byte) interface{} {
+       if width, ok := fixedStatByteWidth(typ); ok && len(val) != width {
+               return val
+       }
+
        switch typ {
        case parquet.Types.Boolean:
                return val[0] != 0
diff --git a/parquet/metadata/statistics_test.go 
b/parquet/metadata/statistics_test.go
index f7fcbda6..4aa1607a 100644
--- a/parquet/metadata/statistics_test.go
+++ b/parquet/metadata/statistics_test.go
@@ -858,3 +858,39 @@ func TestByteArrayStatisticsFromEncodedOwnsMinMax(t 
*testing.T) {
        assert.Equal(t, "aaa", string(stats.Min()))
        assert.Equal(t, "zzz", string(stats.Max()))
 }
+
+func TestMalformedFixedWidthStatisticsAreIgnored(t *testing.T) {
+       descr := schema.NewColumn(schema.NewInt32Node("i32", 
parquet.Repetitions.Required, -1), 0, 0)
+       stats := metadata.NewStatisticsFromEncoded(descr, 
memory.DefaultAllocator, 1,
+               &encodedStatProvider{min: []byte{0x34, 0x12}, max: 
[]byte{0x78}}).(*metadata.Int32Statistics)
+
+       assert.False(t, stats.HasMinMax())
+       assert.Equal(t, []byte{0x34, 0x12}, 
metadata.GetStatValue(parquet.Types.Int32, []byte{0x34, 0x12}))
+       assert.Equal(t, []byte(nil), 
metadata.GetStatValue(parquet.Types.Boolean, nil))
+
+       for _, typ := range []parquet.Type{
+               parquet.Types.Boolean,
+               parquet.Types.Int32,
+               parquet.Types.Int64,
+               parquet.Types.Int96,
+               parquet.Types.Float,
+               parquet.Types.Double,
+       } {
+               assert.NotPanics(t, func() {
+                       metadata.GetStatValue(typ, []byte{1})
+               })
+       }
+}
+
+func TestMalformedFixedLenByteArrayStatisticsAreIgnored(t *testing.T) {
+       descr := schema.NewColumn(schema.NewFixedLenByteArrayNode("flba", 
parquet.Repetitions.Required, 8, -1), 0, 0)
+       stats := metadata.NewStatisticsFromEncoded(descr, 
memory.DefaultAllocator, 1,
+               &encodedStatProvider{min: []byte("ab"), max: 
[]byte("xy")}).(*metadata.FixedLenByteArrayStatistics)
+
+       assert.False(t, stats.HasMinMax())
+}
+
+func TestGetStatValuePreservesByteArrays(t *testing.T) {
+       assert.Equal(t, []byte("ab"), 
metadata.GetStatValue(parquet.Types.ByteArray, []byte("ab")))
+       assert.Equal(t, []byte("xy"), 
metadata.GetStatValue(parquet.Types.FixedLenByteArray, []byte("xy")))
+}
diff --git a/parquet/metadata/statistics_types.gen.go 
b/parquet/metadata/statistics_types.gen.go
index e450fb7c..aa4099a5 100644
--- a/parquet/metadata/statistics_types.gen.go
+++ b/parquet/metadata/statistics_types.gen.go
@@ -79,11 +79,16 @@ func NewInt32StatisticsFromEncoded(descr *schema.Column, 
mem memory.Allocator, n
        }
 
        encodedMin := encoded.GetMin()
-       if encodedMin != nil && len(encodedMin) > 0 {
+       encodedMax := encoded.GetMax()
+       if (encoded.IsSetMin() && !validEncodedStat(descr, encodedMin)) ||
+               (encoded.IsSetMax() && !validEncodedStat(descr, encodedMax)) {
+               return ret
+       }
+
+       if encoded.IsSetMin() {
                ret.min = ret.plainDecode(encodedMin)
        }
-       encodedMax := encoded.GetMax()
-       if encodedMax != nil && len(encodedMax) > 0 {
+       if encoded.IsSetMax() {
                ret.max = ret.plainDecode(encodedMax)
        }
        ret.hasMinMax = encoded.IsSetMax() || encoded.IsSetMin()
@@ -105,7 +110,6 @@ func (s *Int32Statistics) plainEncode(src int32) []byte {
 
 func (s *Int32Statistics) plainDecode(src []byte) int32 {
        var buf [1]int32
-
        decoder := encoding.NewDecoder(s.descr.PhysicalType(), 
parquet.Encodings.Plain, s.descr, s.mem)
        decoder.SetData(1, src)
        decoder.(encoding.Int32Decoder).Decode(buf[:])
@@ -379,11 +383,16 @@ func NewInt64StatisticsFromEncoded(descr *schema.Column, 
mem memory.Allocator, n
        }
 
        encodedMin := encoded.GetMin()
-       if encodedMin != nil && len(encodedMin) > 0 {
+       encodedMax := encoded.GetMax()
+       if (encoded.IsSetMin() && !validEncodedStat(descr, encodedMin)) ||
+               (encoded.IsSetMax() && !validEncodedStat(descr, encodedMax)) {
+               return ret
+       }
+
+       if encoded.IsSetMin() {
                ret.min = ret.plainDecode(encodedMin)
        }
-       encodedMax := encoded.GetMax()
-       if encodedMax != nil && len(encodedMax) > 0 {
+       if encoded.IsSetMax() {
                ret.max = ret.plainDecode(encodedMax)
        }
        ret.hasMinMax = encoded.IsSetMax() || encoded.IsSetMin()
@@ -405,7 +414,6 @@ func (s *Int64Statistics) plainEncode(src int64) []byte {
 
 func (s *Int64Statistics) plainDecode(src []byte) int64 {
        var buf [1]int64
-
        decoder := encoding.NewDecoder(s.descr.PhysicalType(), 
parquet.Encodings.Plain, s.descr, s.mem)
        decoder.SetData(1, src)
        decoder.(encoding.Int64Decoder).Decode(buf[:])
@@ -679,11 +687,16 @@ func NewInt96StatisticsFromEncoded(descr *schema.Column, 
mem memory.Allocator, n
        }
 
        encodedMin := encoded.GetMin()
-       if encodedMin != nil && len(encodedMin) > 0 {
+       encodedMax := encoded.GetMax()
+       if (encoded.IsSetMin() && !validEncodedStat(descr, encodedMin)) ||
+               (encoded.IsSetMax() && !validEncodedStat(descr, encodedMax)) {
+               return ret
+       }
+
+       if encoded.IsSetMin() {
                ret.min = ret.plainDecode(encodedMin)
        }
-       encodedMax := encoded.GetMax()
-       if encodedMax != nil && len(encodedMax) > 0 {
+       if encoded.IsSetMax() {
                ret.max = ret.plainDecode(encodedMax)
        }
        ret.hasMinMax = encoded.IsSetMax() || encoded.IsSetMin()
@@ -705,7 +718,6 @@ func (s *Int96Statistics) plainEncode(src parquet.Int96) 
[]byte {
 
 func (s *Int96Statistics) plainDecode(src []byte) parquet.Int96 {
        var buf [1]parquet.Int96
-
        decoder := encoding.NewDecoder(s.descr.PhysicalType(), 
parquet.Encodings.Plain, s.descr, s.mem)
        decoder.SetData(1, src)
        decoder.(encoding.Int96Decoder).Decode(buf[:])
@@ -957,11 +969,16 @@ func NewFloat32StatisticsFromEncoded(descr 
*schema.Column, mem memory.Allocator,
        }
 
        encodedMin := encoded.GetMin()
-       if encodedMin != nil && len(encodedMin) > 0 {
+       encodedMax := encoded.GetMax()
+       if (encoded.IsSetMin() && !validEncodedStat(descr, encodedMin)) ||
+               (encoded.IsSetMax() && !validEncodedStat(descr, encodedMax)) {
+               return ret
+       }
+
+       if encoded.IsSetMin() {
                ret.min = ret.plainDecode(encodedMin)
        }
-       encodedMax := encoded.GetMax()
-       if encodedMax != nil && len(encodedMax) > 0 {
+       if encoded.IsSetMax() {
                ret.max = ret.plainDecode(encodedMax)
        }
        ret.hasMinMax = encoded.IsSetMax() || encoded.IsSetMin()
@@ -983,7 +1000,6 @@ func (s *Float32Statistics) plainEncode(src float32) 
[]byte {
 
 func (s *Float32Statistics) plainDecode(src []byte) float32 {
        var buf [1]float32
-
        decoder := encoding.NewDecoder(s.descr.PhysicalType(), 
parquet.Encodings.Plain, s.descr, s.mem)
        decoder.SetData(1, src)
        decoder.(encoding.Float32Decoder).Decode(buf[:])
@@ -1249,11 +1265,16 @@ func NewFloat64StatisticsFromEncoded(descr 
*schema.Column, mem memory.Allocator,
        }
 
        encodedMin := encoded.GetMin()
-       if encodedMin != nil && len(encodedMin) > 0 {
+       encodedMax := encoded.GetMax()
+       if (encoded.IsSetMin() && !validEncodedStat(descr, encodedMin)) ||
+               (encoded.IsSetMax() && !validEncodedStat(descr, encodedMax)) {
+               return ret
+       }
+
+       if encoded.IsSetMin() {
                ret.min = ret.plainDecode(encodedMin)
        }
-       encodedMax := encoded.GetMax()
-       if encodedMax != nil && len(encodedMax) > 0 {
+       if encoded.IsSetMax() {
                ret.max = ret.plainDecode(encodedMax)
        }
        ret.hasMinMax = encoded.IsSetMax() || encoded.IsSetMin()
@@ -1275,7 +1296,6 @@ func (s *Float64Statistics) plainEncode(src float64) 
[]byte {
 
 func (s *Float64Statistics) plainDecode(src []byte) float64 {
        var buf [1]float64
-
        decoder := encoding.NewDecoder(s.descr.PhysicalType(), 
parquet.Encodings.Plain, s.descr, s.mem)
        decoder.SetData(1, src)
        decoder.(encoding.Float64Decoder).Decode(buf[:])
@@ -1541,11 +1561,16 @@ func NewBooleanStatisticsFromEncoded(descr 
*schema.Column, mem memory.Allocator,
        }
 
        encodedMin := encoded.GetMin()
-       if encodedMin != nil && len(encodedMin) > 0 {
+       encodedMax := encoded.GetMax()
+       if (encoded.IsSetMin() && !validEncodedStat(descr, encodedMin)) ||
+               (encoded.IsSetMax() && !validEncodedStat(descr, encodedMax)) {
+               return ret
+       }
+
+       if encoded.IsSetMin() {
                ret.min = ret.plainDecode(encodedMin)
        }
-       encodedMax := encoded.GetMax()
-       if encodedMax != nil && len(encodedMax) > 0 {
+       if encoded.IsSetMax() {
                ret.max = ret.plainDecode(encodedMax)
        }
        ret.hasMinMax = encoded.IsSetMax() || encoded.IsSetMin()
@@ -1567,7 +1592,6 @@ func (s *BooleanStatistics) plainEncode(src bool) []byte {
 
 func (s *BooleanStatistics) plainDecode(src []byte) bool {
        var buf [1]bool
-
        decoder := encoding.NewDecoder(s.descr.PhysicalType(), 
parquet.Encodings.Plain, s.descr, s.mem)
        decoder.SetData(1, src)
        decoder.(encoding.BooleanDecoder).Decode(buf[:])
@@ -1898,13 +1922,18 @@ func NewByteArrayStatisticsFromEncoded(descr 
*schema.Column, mem memory.Allocato
        }
 
        encodedMin := encoded.GetMin()
-       if encodedMin != nil && len(encodedMin) > 0 {
+       encodedMax := encoded.GetMax()
+       if (encoded.IsSetMin() && !validEncodedStat(descr, encodedMin)) ||
+               (encoded.IsSetMax() && !validEncodedStat(descr, encodedMax)) {
+               return ret
+       }
+
+       if encoded.IsSetMin() {
                // Copy into a statistics-owned buffer so the stored min does 
not alias the
                // encoded metadata buffer; SetMinMax reuses this buffer on 
later updates.
                ret.min = append(ret.min[:0], ret.plainDecode(encodedMin)...)
        }
-       encodedMax := encoded.GetMax()
-       if encodedMax != nil && len(encodedMax) > 0 {
+       if encoded.IsSetMax() {
                ret.max = append(ret.max[:0], ret.plainDecode(encodedMax)...)
        }
        ret.hasMinMax = encoded.IsSetMax() || encoded.IsSetMin()
@@ -2220,13 +2249,18 @@ func NewFixedLenByteArrayStatisticsFromEncoded(descr 
*schema.Column, mem memory.
        }
 
        encodedMin := encoded.GetMin()
-       if encodedMin != nil && len(encodedMin) > 0 {
+       encodedMax := encoded.GetMax()
+       if (encoded.IsSetMin() && !validEncodedStat(descr, encodedMin)) ||
+               (encoded.IsSetMax() && !validEncodedStat(descr, encodedMax)) {
+               return ret
+       }
+
+       if encoded.IsSetMin() {
                // Copy into a statistics-owned buffer so the stored min does 
not alias the
                // encoded metadata buffer; SetMinMax reuses this buffer on 
later updates.
                ret.min = append(ret.min[:0], ret.plainDecode(encodedMin)...)
        }
-       encodedMax := encoded.GetMax()
-       if encodedMax != nil && len(encodedMax) > 0 {
+       if encoded.IsSetMax() {
                ret.max = append(ret.max[:0], ret.plainDecode(encodedMax)...)
        }
        ret.hasMinMax = encoded.IsSetMax() || encoded.IsSetMin()
@@ -2247,12 +2281,7 @@ func (s *FixedLenByteArrayStatistics) plainEncode(src 
parquet.FixedLenByteArray)
 }
 
 func (s *FixedLenByteArrayStatistics) plainDecode(src []byte) 
parquet.FixedLenByteArray {
-       var buf [1]parquet.FixedLenByteArray
-
-       decoder := encoding.NewDecoder(s.descr.PhysicalType(), 
parquet.Encodings.Plain, s.descr, s.mem)
-       decoder.SetData(1, src)
-       decoder.(encoding.FixedLenByteArrayDecoder).Decode(buf[:])
-       return buf[0]
+       return src
 }
 
 func (s *FixedLenByteArrayStatistics) minval(a, b parquet.FixedLenByteArray) 
parquet.FixedLenByteArray {
@@ -2552,13 +2581,18 @@ func NewFloat16StatisticsFromEncoded(descr 
*schema.Column, mem memory.Allocator,
        }
 
        encodedMin := encoded.GetMin()
-       if encodedMin != nil && len(encodedMin) > 0 {
+       encodedMax := encoded.GetMax()
+       if (encoded.IsSetMin() && !validEncodedStat(descr, encodedMin)) ||
+               (encoded.IsSetMax() && !validEncodedStat(descr, encodedMax)) {
+               return ret
+       }
+
+       if encoded.IsSetMin() {
                // Copy into a statistics-owned buffer so the stored min does 
not alias the
                // encoded metadata buffer; SetMinMax reuses this buffer on 
later updates.
                ret.min = append(ret.min[:0], ret.plainDecode(encodedMin)...)
        }
-       encodedMax := encoded.GetMax()
-       if encodedMax != nil && len(encodedMax) > 0 {
+       if encoded.IsSetMax() {
                ret.max = append(ret.max[:0], ret.plainDecode(encodedMax)...)
        }
        ret.hasMinMax = encoded.IsSetMax() || encoded.IsSetMin()
@@ -2580,7 +2614,6 @@ func (s *Float16Statistics) plainEncode(src 
parquet.FixedLenByteArray) []byte {
 
 func (s *Float16Statistics) plainDecode(src []byte) parquet.FixedLenByteArray {
        var buf [1]parquet.FixedLenByteArray
-
        decoder := encoding.NewDecoder(s.descr.PhysicalType(), 
parquet.Encodings.Plain, s.descr, s.mem)
        decoder.SetData(1, src)
        decoder.(encoding.FixedLenByteArrayDecoder).Decode(buf[:])
diff --git a/parquet/metadata/statistics_types.gen.go.tmpl 
b/parquet/metadata/statistics_types.gen.go.tmpl
index 0f0bc377..0938d4ed 100644
--- a/parquet/metadata/statistics_types.gen.go.tmpl
+++ b/parquet/metadata/statistics_types.gen.go.tmpl
@@ -86,7 +86,13 @@ func New{{.Name}}StatisticsFromEncoded(descr *schema.Column, 
mem memory.Allocato
   }
 
   encodedMin := encoded.GetMin()
-  if encodedMin != nil && len(encodedMin) > 0 {
+  encodedMax := encoded.GetMax()
+  if (encoded.IsSetMin() && !validEncodedStat(descr, encodedMin)) ||
+      (encoded.IsSetMax() && !validEncodedStat(descr, encodedMax)) {
+    return ret
+  }
+
+  if encoded.IsSetMin() {
 {{- if or (eq .name "parquet.ByteArray") (eq .name 
"parquet.FixedLenByteArray")}}
     // Copy into a statistics-owned buffer so the stored min does not alias the
     // encoded metadata buffer; SetMinMax reuses this buffer on later updates.
@@ -95,8 +101,7 @@ func New{{.Name}}StatisticsFromEncoded(descr *schema.Column, 
mem memory.Allocato
     ret.min = ret.plainDecode(encodedMin)
 {{- end}}
   }
-  encodedMax := encoded.GetMax()
-  if encodedMax != nil && len(encodedMax) > 0 {
+  if encoded.IsSetMax() {
 {{- if or (eq .name "parquet.ByteArray") (eq .name 
"parquet.FixedLenByteArray")}}
     ret.max = append(ret.max[:0], ret.plainDecode(encodedMax)...)
 {{- else}}
@@ -127,11 +132,10 @@ func (s *{{.Name}}Statistics) plainEncode(src {{.name}}) 
[]byte {
 }
 
 func (s *{{.Name}}Statistics) plainDecode(src []byte) {{.name}} {
-{{- if eq .Name "ByteArray"}}
+{{- if or (eq .Name "ByteArray") (eq .Name "FixedLenByteArray")}}
   return src
 {{- else}}
   var buf [1]{{.name}}
-
   decoder := encoding.NewDecoder(s.descr.PhysicalType(), 
parquet.Encodings.Plain, s.descr, s.mem)
   decoder.SetData(1, src)
   decoder.(encoding.{{if 
.logical}}{{.physical}}{{else}}{{.Name}}{{end}}Decoder).Decode(buf[:])

Reply via email to