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 22da919b fix(arrow/array): validate struct child lengths (#1021)
22da919b is described below

commit 22da919b5f507d89c7ca7c643bf00679c58297a7
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 5 17:33:55 2026 +0200

    fix(arrow/array): validate struct child lengths (#1021)
    
    A struct child shorter than the parent extent can produce an array that
    panics when the missing values are accessed. Because structural validation 
in
    `arrow/array` is opt-in, report this through `Validate`/`ValidateFull` 
rather
    than changing the `StructBuilder` construction contract.
    
    - Add `Validate`/`ValidateFull` to `Struct`, checking each raw child against
      the full `offset + length` extent (offset-aware, so slicing cannot defeat
      it), mirroring the sparse-union and fixed-size-list precedents.
    - Clamp constructed child views to the available data so malformed arrays
      remain inspectable without substituting values from the wrong offset.
    - Guard against `offset + length` overflow.
    - Preserve the valid child-only builder pattern (e.g. `MapScalar`) and 
normal
      struct slicing.
---
 arrow/array/struct.go      | 38 +++++++++++++++++++++++-
 arrow/array/struct_test.go | 73 +++++++++++++++++++++++++++++++---------------
 2 files changed, 86 insertions(+), 25 deletions(-)

diff --git a/arrow/array/struct.go b/arrow/array/struct.go
index 47e30071..7a3622bb 100644
--- a/arrow/array/struct.go
+++ b/arrow/array/struct.go
@@ -20,6 +20,7 @@ import (
        "bytes"
        "errors"
        "fmt"
+       "math"
        "strings"
 
        "github.com/apache/arrow-go/v18/arrow"
@@ -143,6 +144,22 @@ func NewStructData(data arrow.ArrayData) *Struct {
 func (a *Struct) NumField() int           { return len(a.fields) }
 func (a *Struct) Field(i int) arrow.Array { return a.fields[i] }
 
+func (a *Struct) Validate() error {
+       if a.data.offset < 0 || a.data.length < 0 || int64(a.data.offset) > 
math.MaxInt64-int64(a.data.length) {
+               return fmt.Errorf("%w: arrow/array: struct offset and length 
overflow", arrow.ErrInvalid)
+       }
+       expectedLength := a.data.offset + a.data.length
+       for i, child := range a.data.childData {
+               if child.Len() < expectedLength {
+                       return fmt.Errorf("%w: arrow/array: struct child array 
#%d has length smaller than expected for struct array (%d < %d)",
+                               arrow.ErrInvalid, i, child.Len(), 
expectedLength)
+               }
+       }
+       return nil
+}
+
+func (a *Struct) ValidateFull() error { return a.Validate() }
+
 // ValueStr returns the string representation (as json) of the value at index 
i.
 func (a *Struct) ValueStr(i int) string {
        if a.IsNull(i) {
@@ -222,7 +239,26 @@ func (a *Struct) setData(data *Data) {
        a.fields = make([]arrow.Array, len(data.childData))
        for i, child := range data.childData {
                if data.offset != 0 || child.Len() != data.length {
-                       sub := NewSliceData(child, int64(data.offset), 
int64(data.offset+data.length))
+                       childLen := int64(child.Len())
+                       start := max(int64(data.offset), int64(0))
+                       if start > childLen {
+                               start = childLen
+                       }
+                       var end int64
+                       offset, length := int64(data.offset), int64(data.length)
+                       switch {
+                       case length > 0 && offset > math.MaxInt64-length:
+                               end = math.MaxInt64
+                       case length < 0 && offset < math.MinInt64-length:
+                               end = math.MinInt64
+                       default:
+                               end = offset + length
+                       }
+                       end = max(end, start)
+                       if end > childLen {
+                               end = childLen
+                       }
+                       sub := NewSliceData(child, start, end)
                        a.fields[i] = MakeFromData(sub)
                        sub.Release()
                } else {
diff --git a/arrow/array/struct_test.go b/arrow/array/struct_test.go
index a6d4f1c7..74fda503 100644
--- a/arrow/array/struct_test.go
+++ b/arrow/array/struct_test.go
@@ -17,6 +17,7 @@
 package array_test
 
 import (
+       "math"
        "reflect"
        "testing"
 
@@ -135,6 +136,46 @@ func TestStructArray(t *testing.T) {
        }
 }
 
+func TestStructValidateFullRejectsShortField(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       defer mem.AssertSize(t, 0)
+
+       dt := arrow.StructOf(
+               arrow.Field{Name: "a", Type: arrow.PrimitiveTypes.Int32},
+               arrow.Field{Name: "b", Type: arrow.PrimitiveTypes.Int32},
+       )
+       b := array.NewStructBuilder(mem, dt)
+       defer b.Release()
+       b.Append(true)
+       b.Append(true)
+       b.FieldBuilder(0).(*array.Int32Builder).Append(1)
+       b.FieldBuilder(1).(*array.Int32Builder).AppendValues([]int32{2, 3}, nil)
+
+       arr := b.NewStructArray()
+       defer arr.Release()
+       assert.ErrorIs(t, arr.Validate(), arrow.ErrInvalid)
+       assert.ErrorIs(t, arr.ValidateFull(), arrow.ErrInvalid)
+       assert.ErrorIs(t, array.ValidateFull(arr), arrow.ErrInvalid)
+
+       sliced := array.NewSlice(arr, 1, 2).(*array.Struct)
+       defer sliced.Release()
+       assert.Zero(t, sliced.Field(0).Len())
+       assert.ErrorIs(t, sliced.Validate(), arrow.ErrInvalid)
+       assert.ErrorIs(t, sliced.ValidateFull(), arrow.ErrInvalid)
+}
+
+func TestStructValidateRejectsOffsetLengthOverflow(t *testing.T) {
+       childData := array.NewData(arrow.PrimitiveTypes.Int32, 0, 
[]*memory.Buffer{nil, nil}, nil, 0, 0)
+       defer childData.Release()
+       data := array.NewData(arrow.StructOf(arrow.Field{Name: "value", Type: 
arrow.PrimitiveTypes.Int32}),
+               math.MaxInt, nil, []arrow.ArrayData{childData}, 0, 1)
+       defer data.Release()
+
+       arr := array.NewStructData(data)
+       defer arr.Release()
+       require.ErrorIs(t, arr.Validate(), arrow.ErrInvalid)
+}
+
 func TestStructStringRoundTrip(t *testing.T) {
        // 1. create array
        mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
@@ -615,18 +656,18 @@ func TestStructArrayUnmarshalJSONMissingFields(t 
*testing.T) {
                name      string
                jsonInput string
                want      string
-               panic     bool
+               invalid   bool
        }{
                {
                        name:      "missing required field",
                        jsonInput: `[{"f2": 3, "f3": {"f3_1": "test"}}]`,
-                       panic:     true,
+                       invalid:   true,
                        want:      "",
                },
                {
                        name:      "missing optional fields",
                        jsonInput: `[{"f2": 3, "f3": {"f3_3": "test"}}]`,
-                       panic:     false,
+                       invalid:   false,
                        want:      `{[(null)] [3] {[(null)] [(null)] 
["test"]}}`,
                },
        }
@@ -634,30 +675,9 @@ func TestStructArrayUnmarshalJSONMissingFields(t 
*testing.T) {
        for _, tc := range tests {
                t.Run(
                        tc.name, func(t *testing.T) {
-
-                               var val bool
-
                                sb := array.NewStructBuilder(pool, dtype)
                                defer sb.Release()
 
-                               if tc.panic {
-                                       defer func() {
-                                               e := recover()
-                                               if e == nil {
-                                                       t.Fatalf("this should 
have panicked, but did not; slice value %v", val)
-                                               }
-                                               if got, want := e.(string), 
"arrow/array: index out of range"; got != want {
-                                                       t.Fatalf("invalid 
error. got=%q, want=%q", got, want)
-                                               }
-                                       }()
-                               } else {
-                                       defer func() {
-                                               if e := recover(); e != nil {
-                                                       t.Fatalf("unexpected 
panic: %v", e)
-                                               }
-                                       }()
-                               }
-
                                err := sb.UnmarshalJSON([]byte(tc.jsonInput))
                                if err != nil {
                                        t.Fatal(err)
@@ -665,6 +685,11 @@ func TestStructArrayUnmarshalJSONMissingFields(t 
*testing.T) {
 
                                arr := sb.NewArray().(*array.Struct)
                                defer arr.Release()
+                               if tc.invalid {
+                                       require.ErrorIs(t, 
array.ValidateFull(arr), arrow.ErrInvalid)
+                                       return
+                               }
+                               require.NoError(t, array.ValidateFull(arr))
 
                                got := arr.String()
                                if got != tc.want {

Reply via email to