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 69c2fd50 fix(array): validate binary_view and string_view payloads
(#994)
69c2fd50 is described below
commit 69c2fd50fa2fc1f122b7abce66c6863e4302a6e8
Author: Minh Vu <[email protected]>
AuthorDate: Fri Jul 24 20:16:44 2026 +0200
fix(array): validate binary_view and string_view payloads (#994)
## Summary
- add BinaryView and StringView validators
- validate view header ranges, referenced buffers, prefixes, and inline
padding during ValidateFull
- validate UTF-8 for StringView payloads and add regressions for
malformed headers
## Why
BinaryView and StringView accessors currently trust header metadata
directly. Malformed view headers can pass validation today even when
they reference missing data or carry invalid payload metadata.
## Validation
- go test ./arrow/array
- go test ./arrow/compute/...
---
arrow/array/binary.go | 94 ++++++++++++++++++++
arrow/array/string.go | 16 ++++
arrow/array/validate_test.go | 207 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 317 insertions(+)
diff --git a/arrow/array/binary.go b/arrow/array/binary.go
index a8e77ae9..d23e2a0b 100644
--- a/arrow/array/binary.go
+++ b/arrow/array/binary.go
@@ -482,6 +482,17 @@ func (a *BinaryView) ValueLen(i int) int {
return s.Len()
}
+func (a *BinaryView) Validate() error {
+ return validateViewLayout(a, "binary view")
+}
+
+func (a *BinaryView) ValidateFull() error {
+ if err := a.Validate(); err != nil {
+ return err
+ }
+ return validateViewValues(a, a.dataBuffers, nil)
+}
+
// ValueString returns the value at index i as a string instead of
// a byte slice, without copying the underlying data.
func (a *BinaryView) ValueString(i int) string {
@@ -552,6 +563,89 @@ func arrayEqualBinaryView(left, right *BinaryView) bool {
return true
}
+func validateViewLayout(arr ViewLike, kind string) error {
+ data := arr.Data().(*Data)
+ if data.length == 0 {
+ return nil
+ }
+ if data.buffers[1] == nil {
+ return fmt.Errorf("arrow/array: non-empty %s array has no view
buffer", kind)
+ }
+
+ expNumViews := data.offset + data.length
+ if len(data.buffers[1].Bytes())/arrow.ViewHeaderSizeBytes < expNumViews
{
+ return fmt.Errorf("arrow/array: %s buffer must have at least %d
view values", kind, expNumViews)
+ }
+ return nil
+}
+
+func validateViewValues(arr ViewLike, dataBuffers []*memory.Buffer,
validateValue func(int, []byte) error) error {
+ data := arr.Data().(*Data)
+ if data.length == 0 {
+ return nil
+ }
+ rawViews := data.buffers[1].Bytes()
+ for i := 0; i < data.length; i++ {
+ if arr.IsNull(i) {
+ continue
+ }
+
+ view := arr.ValueHeader(i)
+ if view.Len() < 0 {
+ return fmt.Errorf("arrow/array: view at slot %d has
negative size %d", i, view.Len())
+ }
+
+ if view.IsInline() {
+ rawOffset := (data.offset + i) *
arrow.ViewHeaderSizeBytes
+ raw := rawViews[rawOffset :
rawOffset+arrow.ViewHeaderSizeBytes]
+ for _, b := range raw[4+view.Len() :
arrow.ViewHeaderSizeBytes] {
+ if b != 0 {
+ return fmt.Errorf("arrow/array: view at
slot %d was inline with size %d but its padding bytes were not all zero", i,
view.Len())
+ }
+ }
+ if validateValue != nil {
+ if err := validateValue(i, view.InlineBytes());
err != nil {
+ return err
+ }
+ }
+ continue
+ }
+
+ if view.BufferIndex() < 0 {
+ return fmt.Errorf("arrow/array: view at slot %d has
negative buffer index %d", i, view.BufferIndex())
+ }
+ if view.BufferOffset() < 0 {
+ return fmt.Errorf("arrow/array: view at slot %d has
negative offset %d", i, view.BufferOffset())
+ }
+ if int(view.BufferIndex()) >= len(dataBuffers) {
+ return fmt.Errorf("arrow/array: view at slot %d
references buffer %d but there are only %d data buffers", i,
view.BufferIndex(), len(dataBuffers))
+ }
+
+ buf := dataBuffers[view.BufferIndex()]
+ if buf == nil {
+ return fmt.Errorf("arrow/array: view at slot %d
references nil data buffer %d", i, view.BufferIndex())
+ }
+
+ offset := int(view.BufferOffset())
+ end := offset + view.Len()
+ if end > buf.Len() {
+ return fmt.Errorf("arrow/array: view at slot %d
references range %d-%d of buffer %d but that buffer is only %d bytes long", i,
offset, end, view.BufferIndex(), buf.Len())
+ }
+
+ value := buf.Bytes()[offset:end]
+ prefix := view.Prefix()
+ if !bytes.Equal(value[:arrow.ViewPrefixLen], prefix[:]) {
+ return fmt.Errorf("arrow/array: view at slot %d has
inlined prefix %x but the out-of-line data begins with %x", i, prefix,
value[:arrow.ViewPrefixLen])
+ }
+ if validateValue != nil {
+ if err := validateValue(i, value); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
var (
_ arrow.Array = (*Binary)(nil)
_ arrow.Array = (*LargeBinary)(nil)
diff --git a/arrow/array/string.go b/arrow/array/string.go
index 7c2ab074..519b2cae 100644
--- a/arrow/array/string.go
+++ b/arrow/array/string.go
@@ -501,6 +501,22 @@ func (a *StringView) ValueLen(i int) int {
return s.Len()
}
+func (a *StringView) Validate() error {
+ return validateViewLayout(a, "string view")
+}
+
+func (a *StringView) ValidateFull() error {
+ if err := a.Validate(); err != nil {
+ return err
+ }
+ return validateViewValues(a, a.dataBuffers, func(i int, value []byte)
error {
+ if !utf8.Valid(value) {
+ return fmt.Errorf("arrow/array: string view at slot %d
is not valid utf8", i)
+ }
+ return nil
+ })
+}
+
func (a *StringView) String() string {
var o strings.Builder
o.WriteString("[")
diff --git a/arrow/array/validate_test.go b/arrow/array/validate_test.go
index 7b83f7a4..ac641787 100644
--- a/arrow/array/validate_test.go
+++ b/arrow/array/validate_test.go
@@ -22,6 +22,7 @@ import (
"github.com/apache/arrow-go/v18/arrow"
"github.com/apache/arrow-go/v18/arrow/bitutil"
+ "github.com/apache/arrow-go/v18/arrow/endian"
"github.com/apache/arrow-go/v18/arrow/memory"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -77,6 +78,42 @@ func makeInt32ArrayRaw(t *testing.T, values []int32,
validity []byte, nulls, len
return arr
}
+func makeBinaryViewArrayRaw(t *testing.T, headerBytes []byte, dataBuffers
[]*memory.Buffer, validity []byte, nulls, length, offset int) *BinaryView {
+ t.Helper()
+ var validityBuf *memory.Buffer
+ if validity != nil {
+ validityBuf = memory.NewBufferBytes(validity)
+ }
+ viewBuf := memory.NewBufferBytes(headerBytes)
+ buffers := append([]*memory.Buffer{validityBuf, viewBuf},
dataBuffers...)
+ data := NewData(arrow.BinaryTypes.BinaryView, length, buffers, nil,
nulls, offset)
+ arr := NewBinaryViewData(data)
+ data.Release()
+ return arr
+}
+
+func makeStringViewArrayRaw(t *testing.T, headerBytes []byte, dataBuffers
[]*memory.Buffer, validity []byte, nulls, length, offset int) *StringView {
+ t.Helper()
+ var validityBuf *memory.Buffer
+ if validity != nil {
+ validityBuf = memory.NewBufferBytes(validity)
+ }
+ viewBuf := memory.NewBufferBytes(headerBytes)
+ buffers := append([]*memory.Buffer{validityBuf, viewBuf},
dataBuffers...)
+ data := NewData(arrow.BinaryTypes.StringView, length, buffers, nil,
nulls, offset)
+ arr := NewStringViewData(data)
+ data.Release()
+ return arr
+}
+
+func setViewHeaderBufferIndex(raw []byte, idx int32) {
+ endian.Native.PutUint32(raw[8:12], uint32(idx))
+}
+
+func setViewHeaderOffset(raw []byte, offset int32) {
+ endian.Native.PutUint32(raw[12:16], uint32(offset))
+}
+
func TestBinaryValidate(t *testing.T) {
t.Run("valid array passes", func(t *testing.T) {
// offsets [0,3,6,9], data "abcdefghi" — 3 elements of 3 bytes
each
@@ -201,6 +238,176 @@ func TestLargeStringValidate(t *testing.T) {
})
}
+func TestBinaryViewValidate(t *testing.T) {
+ t.Run("empty arrays pass top level validation", func(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+
+ binaryArr := NewBinaryViewBuilder(mem).NewBinaryViewArray()
+ defer binaryArr.Release()
+ stringArr := NewStringViewBuilder(mem).NewStringViewArray()
+ defer stringArr.Release()
+
+ assert.NoError(t, Validate(binaryArr))
+ assert.NoError(t, ValidateFull(binaryArr))
+ assert.NoError(t, Validate(stringArr))
+ assert.NoError(t, ValidateFull(stringArr))
+ })
+
+ t.Run("valid array passes", func(t *testing.T) {
+ var headers [1]arrow.ViewHeader
+ headers[0].SetBytes([]byte("hello"))
+ arr := makeBinaryViewArrayRaw(t,
arrow.ViewHeaderTraits.CastToBytes(headers[:]), nil, nil, 0, 1, 0)
+ defer arr.Release()
+
+ assert.NoError(t, arr.Validate())
+ assert.NoError(t, arr.ValidateFull())
+ })
+
+ t.Run("out of line values pass top level validation", func(t
*testing.T) {
+ value := []byte("this payload is out of line")
+ var headers [1]arrow.ViewHeader
+ headers[0].SetBytes(value)
+ headers[0].SetIndexOffset(0, 0)
+ dataBuf := memory.NewBufferBytes(value)
+ arr := makeBinaryViewArrayRaw(t,
arrow.ViewHeaderTraits.CastToBytes(headers[:]), []*memory.Buffer{dataBuf}, nil,
0, 1, 0)
+ defer arr.Release()
+
+ assert.NoError(t, Validate(arr))
+ assert.NoError(t, ValidateFull(arr))
+ })
+
+ t.Run("offset arrays use the correct view slot", func(t *testing.T) {
+ headers := [2]arrow.ViewHeader{}
+ headers[0].SetBytes([]byte("skip"))
+ headers[1].SetBytes([]byte("keep"))
+ arr := makeBinaryViewArrayRaw(t,
arrow.ViewHeaderTraits.CastToBytes(headers[:]), nil, nil, 0, 1, 1)
+ defer arr.Release()
+
+ assert.NoError(t, Validate(arr))
+ assert.NoError(t, ValidateFull(arr))
+ })
+
+ t.Run("negative size passes Validate but fails ValidateFull", func(t
*testing.T) {
+ headerBytes := make([]byte, arrow.ViewHeaderSizeBytes)
+ endian.Native.PutUint32(headerBytes[:4], ^uint32(0))
+ arr := makeBinaryViewArrayRaw(t, headerBytes, nil, nil, 0, 1, 0)
+ defer arr.Release()
+
+ assert.NoError(t, arr.Validate())
+ err := arr.ValidateFull()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "negative size")
+ })
+
+ t.Run("missing referenced buffer passes Validate but fails
ValidateFull", func(t *testing.T) {
+ var headers [1]arrow.ViewHeader
+ headers[0].SetBytes([]byte("this is longer than twelve"))
+ headers[0].SetIndexOffset(0, 0)
+ arr := makeBinaryViewArrayRaw(t,
arrow.ViewHeaderTraits.CastToBytes(headers[:]), nil, nil, 0, 1, 0)
+ defer arr.Release()
+
+ assert.NoError(t, arr.Validate())
+ err := arr.ValidateFull()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "references buffer 0")
+ })
+
+ t.Run("prefix mismatch passes Validate but fails ValidateFull", func(t
*testing.T) {
+ value := []byte("this payload is out of line")
+ var headers [1]arrow.ViewHeader
+ headers[0].SetBytes(value)
+ headers[0].SetIndexOffset(0, 0)
+ headerBytes := append([]byte(nil),
arrow.ViewHeaderTraits.CastToBytes(headers[:])...)
+ headerBytes[4] ^= 0xff
+ dataBuf := memory.NewBufferBytes(value)
+ arr := makeBinaryViewArrayRaw(t, headerBytes,
[]*memory.Buffer{dataBuf}, nil, 0, 1, 0)
+ defer arr.Release()
+
+ assert.NoError(t, Validate(arr))
+ err := ValidateFull(arr)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "out-of-line data begins with")
+ })
+
+ t.Run("negative buffer offset passes Validate but fails ValidateFull",
func(t *testing.T) {
+ value := []byte("this payload is out of line")
+ var headers [1]arrow.ViewHeader
+ headers[0].SetBytes(value)
+ headers[0].SetIndexOffset(0, 0)
+ headerBytes := append([]byte(nil),
arrow.ViewHeaderTraits.CastToBytes(headers[:])...)
+ setViewHeaderOffset(headerBytes, -1)
+ dataBuf := memory.NewBufferBytes(value)
+ arr := makeBinaryViewArrayRaw(t, headerBytes,
[]*memory.Buffer{dataBuf}, nil, 0, 1, 0)
+ defer arr.Release()
+
+ assert.NoError(t, Validate(arr))
+ err := ValidateFull(arr)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "negative offset")
+ })
+
+ t.Run("negative buffer index passes Validate but fails ValidateFull",
func(t *testing.T) {
+ value := []byte("this payload is out of line")
+ var headers [1]arrow.ViewHeader
+ headers[0].SetBytes(value)
+ headers[0].SetIndexOffset(0, 0)
+ headerBytes := append([]byte(nil),
arrow.ViewHeaderTraits.CastToBytes(headers[:])...)
+ setViewHeaderBufferIndex(headerBytes, -1)
+ dataBuf := memory.NewBufferBytes(value)
+ arr := makeBinaryViewArrayRaw(t, headerBytes,
[]*memory.Buffer{dataBuf}, nil, 0, 1, 0)
+ defer arr.Release()
+
+ assert.NoError(t, Validate(arr))
+ err := ValidateFull(arr)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "negative buffer index")
+ })
+
+ t.Run("referenced range beyond buffer length passes Validate but fails
ValidateFull", func(t *testing.T) {
+ value := []byte("this payload is out of line")
+ var headers [1]arrow.ViewHeader
+ headers[0].SetBytes(value)
+ headers[0].SetIndexOffset(0, 2)
+ dataBuf := memory.NewBufferBytes(value)
+ arr := makeBinaryViewArrayRaw(t,
arrow.ViewHeaderTraits.CastToBytes(headers[:]), []*memory.Buffer{dataBuf}, nil,
0, 1, 0)
+ defer arr.Release()
+
+ assert.NoError(t, Validate(arr))
+ err := ValidateFull(arr)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "references range")
+ })
+
+ t.Run("inline padding bytes fail ValidateFull", func(t *testing.T) {
+ var headers [1]arrow.ViewHeader
+ headers[0].SetBytes([]byte("x"))
+ headerBytes := append([]byte(nil),
arrow.ViewHeaderTraits.CastToBytes(headers[:])...)
+ headerBytes[8] = 1
+ arr := makeBinaryViewArrayRaw(t, headerBytes, nil, nil, 0, 1, 0)
+ defer arr.Release()
+
+ assert.NoError(t, arr.Validate())
+ err := arr.ValidateFull()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "padding bytes were not all
zero")
+ })
+}
+
+func TestStringViewValidate(t *testing.T) {
+ t.Run("invalid utf8 passes Validate but fails ValidateFull", func(t
*testing.T) {
+ var headers [1]arrow.ViewHeader
+ headers[0].SetBytes([]byte{0xff})
+ arr := makeStringViewArrayRaw(t,
arrow.ViewHeaderTraits.CastToBytes(headers[:]), nil, nil, 0, 1, 0)
+ defer arr.Release()
+
+ assert.NoError(t, arr.Validate())
+ err := arr.ValidateFull()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "not valid utf8")
+ })
+}
+
func TestTopLevelValidate(t *testing.T) {
t.Run("Validate dispatches to Validator", func(t *testing.T) {
// non-monotonic string array: passes setData but ValidateFull
must fail