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 227f6e3e fix(arrow/array): validate union JSON type codes (#1157)
227f6e3e is described below

commit 227f6e3ea469622d9b3f46924841e2dc4eed943d
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 26 23:00:16 2026 +0200

    fix(arrow/array): validate union JSON type codes (#1157)
    
    ## What
    
    Validate union type IDs before narrowing them to int8 in both sparse and
    dense JSON decoders. Out-of-range, negative, and nonnumeric type IDs are
    rejected.
    
    ## Test
    
    - go test ./arrow/array -run ^TestUnion -count=1
---
 arrow/array/record.go      |  21 +++--
 arrow/array/record_test.go |  44 +++++++++++
 arrow/array/union.go       |  96 ++++++++++++-----------
 arrow/array/union_test.go  | 187 +++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 298 insertions(+), 50 deletions(-)

diff --git a/arrow/array/record.go b/arrow/array/record.go
index 443d3492..704759b1 100644
--- a/arrow/array/record.go
+++ b/arrow/array/record.go
@@ -469,6 +469,12 @@ type builderCheckpoint struct {
        lastStr          *string
 }
 
+func (checkpoint *builderCheckpoint) syncChildren(builders []Builder) {
+       for i := len(checkpoint.children); i < len(builders); i++ {
+               checkpoint.children = append(checkpoint.children, 
newBuilderCheckpoint(builders[i]))
+       }
+}
+
 func newBuilderCheckpoint(builder Builder) *builderCheckpoint {
        checkpoint := &builderCheckpoint{
                builder: builder,
@@ -499,13 +505,9 @@ func newBuilderCheckpoint(builder Builder) 
*builderCheckpoint {
                        checkpoint.children = append(checkpoint.children, 
newBuilderCheckpoint(field))
                }
        case *SparseUnionBuilder:
-               for _, child := range builder.children {
-                       checkpoint.children = append(checkpoint.children, 
newBuilderCheckpoint(child))
-               }
+               checkpoint.syncChildren(builder.children)
        case *DenseUnionBuilder:
-               for _, child := range builder.children {
-                       checkpoint.children = append(checkpoint.children, 
newBuilderCheckpoint(child))
-               }
+               checkpoint.syncChildren(builder.children)
        case storageBuilder:
                checkpoint.children = append(checkpoint.children, 
newBuilderCheckpoint(builder.StorageBuilder()))
        case *RunEndEncodedBuilder:
@@ -520,6 +522,13 @@ func newBuilderCheckpoint(builder Builder) 
*builderCheckpoint {
 }
 
 func (checkpoint *builderCheckpoint) capture() {
+       switch builder := checkpoint.builder.(type) {
+       case *SparseUnionBuilder:
+               checkpoint.syncChildren(builder.children)
+       case *DenseUnionBuilder:
+               checkpoint.syncChildren(builder.children)
+       }
+
        checkpoint.length = checkpoint.builder.Len()
        if checkpoint.state != nil {
                checkpoint.state.capture()
diff --git a/arrow/array/record_test.go b/arrow/array/record_test.go
index 6ad3c7e3..ed7c005d 100644
--- a/arrow/array/record_test.go
+++ b/arrow/array/record_test.go
@@ -560,6 +560,50 @@ func TestRecordBuilderRollsBackRowsAfterDecodeError(t 
*testing.T) {
 
 }
 
+func TestRecordBuilderRollsBackDynamicallyAddedUnionChild(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       defer mem.AssertSize(t, 0)
+
+       for _, tc := range []struct {
+               name string
+               mode arrow.UnionMode
+       }{{name: "dense", mode: arrow.DenseMode}, {name: "sparse", mode: 
arrow.SparseMode}} {
+               t.Run(tc.name, func(t *testing.T) {
+                       unionType := arrow.UnionOf(tc.mode,
+                               []arrow.Field{{Name: "first", Type: 
arrow.PrimitiveTypes.Int32}},
+                               []arrow.UnionTypeCode{0})
+                       schema := arrow.NewSchema([]arrow.Field{
+                               {Name: "value", Type: unionType},
+                               {Name: "other", Type: 
arrow.PrimitiveTypes.Int32},
+                       }, nil)
+                       builder := array.NewRecordBuilder(mem, schema)
+                       defer builder.Release()
+
+                       unionBuilder := builder.Field(0).(array.UnionBuilder)
+                       assert.NoError(t, 
builder.UnmarshalJSON([]byte(`{"value":[0,1],"other":1}`)))
+
+                       secondChild := array.NewInt32Builder(mem)
+                       defer secondChild.Release()
+                       if tc.mode == arrow.SparseMode {
+                               secondChild.AppendNull()
+                       }
+                       assert.EqualValues(t, 1, 
unionBuilder.AppendChild(secondChild, "second"))
+
+                       assert.Error(t, 
builder.UnmarshalJSON([]byte(`{"value":[1,2],"other":"invalid"}`)))
+                       wantChildLen := 0
+                       if tc.mode == arrow.SparseMode {
+                               wantChildLen = 1
+                       }
+                       assert.Equal(t, wantChildLen, 
unionBuilder.Child(1).Len())
+
+                       assert.NoError(t, 
builder.UnmarshalJSON([]byte(`{"value":[1,3],"other":2}`)))
+                       union := unionBuilder.NewArray().(array.Union)
+                       defer union.Release()
+                       assert.Equal(t, `[1,3]`, union.ValueStr(1))
+               })
+       }
+}
+
 func TestRecordBuilderRollsBackVariableWidthState(t *testing.T) {
        mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
        defer mem.AssertSize(t, 0)
diff --git a/arrow/array/union.go b/arrow/array/union.go
index afa4e84c..e6137eae 100644
--- a/arrow/array/union.go
+++ b/arrow/array/union.go
@@ -772,10 +772,32 @@ type unionBuilder struct {
        typeIDtoBuilder []Builder
        typeIDtoChildID []int
        // for all typeID < denseTypeID, typeIDtoBuilder[typeID] != nil
-       denseTypeID  arrow.UnionTypeCode
+       denseTypeID  int
        typesBuilder *int8BufferBuilder
 }
 
+func unionTypeCodeFromJSON(dec *json.Decoder, typeID json.RawMessage, typ 
arrow.DataType) (arrow.UnionTypeCode, error) {
+       id, err := json.Number(string(typeID)).Int64()
+       if err != nil {
+               return 0, &json.UnmarshalTypeError{
+                       Offset: dec.InputOffset(),
+                       Type:   reflect.TypeOf(int8(0)),
+                       Struct: fmt.Sprint(typ),
+                       Value:  "integer",
+               }
+       }
+
+       if id < 0 || id > int64(arrow.MaxUnionTypeCode) {
+               return 0, &json.UnmarshalTypeError{
+                       Offset: dec.InputOffset(),
+                       Type:   reflect.TypeOf(int8(0)),
+                       Struct: fmt.Sprint(typ),
+                       Value:  "integer",
+               }
+       }
+       return arrow.UnionTypeCode(id), nil
+}
+
 func newUnionBuilder(mem memory.Allocator, children []Builder, typ 
arrow.UnionType) *unionBuilder {
        if children == nil {
                children = make([]Builder, 0)
@@ -864,9 +886,9 @@ func (b *unionBuilder) Type() arrow.DataType {
 }
 
 func (b *unionBuilder) AppendChild(newChild Builder, fieldName string) 
arrow.UnionTypeCode {
+       newType := b.nextTypeID()
        newChild.Retain()
        b.children = append(b.children, newChild)
-       newType := b.nextTypeID()
 
        b.typeIDtoChildID[newType] = len(b.children) - 1
        b.typeIDtoBuilder[newType] = newChild
@@ -880,21 +902,23 @@ func (b *unionBuilder) nextTypeID() arrow.UnionTypeCode {
        // find typeID such that typeIDtoBuilder[typeID] == nil
        // use that for the new child. Start searching at denseTypeID
        // since typeIDtoBuilder is densely packed up at least to denseTypeID
-       for ; int(b.denseTypeID) < len(b.typeIDtoBuilder); b.denseTypeID++ {
+       for ; b.denseTypeID < len(b.typeIDtoBuilder); b.denseTypeID++ {
                if b.typeIDtoBuilder[b.denseTypeID] == nil {
                        id := b.denseTypeID
                        b.denseTypeID++
-                       return id
+                       return arrow.UnionTypeCode(id)
                }
        }
 
-       debug.Assert(len(b.typeIDtoBuilder) < int(arrow.MaxUnionTypeCode), "too 
many children typeids")
+       if b.denseTypeID > int(arrow.MaxUnionTypeCode) {
+               panic("arrow/array: too many children typeids")
+       }
        // typeIDtoBuilder is already densely packed, so just append the new 
child
        b.typeIDtoBuilder = append(b.typeIDtoBuilder, nil)
        b.typeIDtoChildID = append(b.typeIDtoChildID, arrow.InvalidUnionChildID)
        id := b.denseTypeID
        b.denseTypeID++
-       return id
+       return arrow.UnionTypeCode(id)
 }
 
 func (b *unionBuilder) newData() *Data {
@@ -1070,6 +1094,7 @@ func (b *SparseUnionBuilder) AppendValueFromString(s 
string) error {
                return nil
        }
        dec := json.NewDecoder(strings.NewReader(s))
+       dec.UseNumber()
        return b.UnmarshalOne(dec)
 }
 
@@ -1082,30 +1107,21 @@ func (b *SparseUnionBuilder) UnmarshalOne(dec 
*json.Decoder) error {
        switch t {
        case json.Delim('['):
                // should be [type_id, Value]
-               typeID, err := dec.Token()
-               if err != nil {
+               var typeID json.RawMessage
+               if err := dec.Decode(&typeID); err != nil {
                        return err
                }
 
-               var typeCode int8
+               typeCode, err := unionTypeCodeFromJSON(dec, typeID, b.Type())
+               if err != nil {
+                       return err
+               }
 
-               switch tid := typeID.(type) {
-               case json.Number:
-                       id, err := tid.Int64()
-                       if err != nil {
-                               return err
-                       }
-                       typeCode = int8(id)
-               case float64:
-                       if tid != float64(int64(tid)) {
-                               return &json.UnmarshalTypeError{
-                                       Offset: dec.InputOffset(),
-                                       Type:   reflect.TypeOf(int8(0)),
-                                       Struct: fmt.Sprint(b.Type()),
-                                       Value:  "float",
-                               }
+               if int(typeCode) >= len(b.typeIDtoChildID) {
+                       return &json.UnmarshalTypeError{
+                               Offset: dec.InputOffset(),
+                               Value:  "invalid type code",
                        }
-                       typeCode = int8(tid)
                }
 
                childNum := b.typeIDtoChildID[typeCode]
@@ -1343,6 +1359,7 @@ func (d *DenseUnionBuilder) AppendValueFromString(s 
string) error {
                return nil
        }
        dec := json.NewDecoder(strings.NewReader(s))
+       dec.UseNumber()
        return d.UnmarshalOne(dec)
 }
 
@@ -1355,30 +1372,21 @@ func (b *DenseUnionBuilder) UnmarshalOne(dec 
*json.Decoder) error {
        switch t {
        case json.Delim('['):
                // should be [type_id, Value]
-               typeID, err := dec.Token()
-               if err != nil {
+               var typeID json.RawMessage
+               if err := dec.Decode(&typeID); err != nil {
                        return err
                }
 
-               var typeCode int8
+               typeCode, err := unionTypeCodeFromJSON(dec, typeID, b.Type())
+               if err != nil {
+                       return err
+               }
 
-               switch tid := typeID.(type) {
-               case json.Number:
-                       id, err := tid.Int64()
-                       if err != nil {
-                               return err
-                       }
-                       typeCode = int8(id)
-               case float64:
-                       if tid != float64(int64(tid)) {
-                               return &json.UnmarshalTypeError{
-                                       Offset: dec.InputOffset(),
-                                       Type:   reflect.TypeOf(int8(0)),
-                                       Struct: fmt.Sprint(b.Type()),
-                                       Value:  "float",
-                               }
+               if int(typeCode) >= len(b.typeIDtoChildID) {
+                       return &json.UnmarshalTypeError{
+                               Offset: dec.InputOffset(),
+                               Value:  "invalid type code",
                        }
-                       typeCode = int8(tid)
                }
 
                childNum := b.typeIDtoChildID[typeCode]
diff --git a/arrow/array/union_test.go b/arrow/array/union_test.go
index 7402f82b..391348ed 100644
--- a/arrow/array/union_test.go
+++ b/arrow/array/union_test.go
@@ -24,6 +24,7 @@ import (
        "github.com/apache/arrow-go/v18/arrow"
        "github.com/apache/arrow-go/v18/arrow/array"
        "github.com/apache/arrow-go/v18/arrow/memory"
+       internaljson "github.com/apache/arrow-go/v18/internal/json"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
        "github.com/stretchr/testify/suite"
@@ -78,6 +79,192 @@ func TestUnionBuilderChildBounds(t *testing.T) {
        }
 }
 
+type unionJSONDecoder struct {
+       name  string
+       apply func(array.UnionBuilder, string) error
+}
+
+func unionJSONDecoders() []unionJSONDecoder {
+       return []unionJSONDecoder{
+               {
+                       name: "UnmarshalJSON",
+                       apply: func(builder array.UnionBuilder, typeCode 
string) error {
+                               return builder.UnmarshalJSON([]byte("[[" + 
typeCode + ", 1]]"))
+                       },
+               },
+               {
+                       name: "AppendValueFromString",
+                       apply: func(builder array.UnionBuilder, typeCode 
string) error {
+                               return builder.AppendValueFromString("[" + 
typeCode + ", 1]")
+                       },
+               },
+               {
+                       name: "UnmarshalOne",
+                       apply: func(builder array.UnionBuilder, typeCode 
string) error {
+                               dec := 
internaljson.NewDecoder(strings.NewReader("[" + typeCode + ", 1]"))
+                               return builder.UnmarshalOne(dec)
+                       },
+               },
+       }
+}
+
+func TestUnionBuilderRejectsInvalidJSONTypeCodes(t *testing.T) {
+       fields := []arrow.Field{{Name: "value", Type: 
arrow.PrimitiveTypes.Int32}}
+       cases := []struct {
+               name string
+               new  func() array.UnionBuilder
+       }{
+               {
+                       name: "dense",
+                       new: func() array.UnionBuilder {
+                               return 
array.NewDenseUnionBuilder(memory.DefaultAllocator, arrow.DenseUnionOf(fields, 
[]arrow.UnionTypeCode{0}))
+                       },
+               },
+               {
+                       name: "sparse",
+                       new: func() array.UnionBuilder {
+                               return 
array.NewSparseUnionBuilder(memory.DefaultAllocator, 
arrow.SparseUnionOf(fields, []arrow.UnionTypeCode{0}))
+                       },
+               },
+       }
+
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       for _, decoder := range unionJSONDecoders() {
+                               t.Run(decoder.name, func(t *testing.T) {
+                                       for _, typeCode := range 
[]string{"256", "-1", "127", "1.5", "null", "-1e-400", "127.00000000000000001"} 
{
+                                               t.Run(typeCode, func(t 
*testing.T) {
+                                                       builder := tc.new()
+                                                       defer builder.Release()
+                                                       assert.Error(t, 
decoder.apply(builder, typeCode))
+                                               })
+                                       }
+                               })
+                       }
+               })
+       }
+}
+
+func TestUnionBuilderRejectsRoundedJSONTypeCodes(t *testing.T) {
+       fields := []arrow.Field{{Name: "value", Type: 
arrow.PrimitiveTypes.Int32}}
+       cases := []struct {
+               name string
+               new  func() array.UnionBuilder
+       }{
+               {
+                       name: "dense",
+                       new: func() array.UnionBuilder {
+                               return 
array.NewDenseUnionBuilder(memory.DefaultAllocator, arrow.DenseUnionOf(fields, 
[]arrow.UnionTypeCode{127}))
+                       },
+               },
+               {
+                       name: "sparse",
+                       new: func() array.UnionBuilder {
+                               return 
array.NewSparseUnionBuilder(memory.DefaultAllocator, 
arrow.SparseUnionOf(fields, []arrow.UnionTypeCode{127}))
+                       },
+               },
+       }
+
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       for _, decoder := range unionJSONDecoders() {
+                               t.Run(decoder.name, func(t *testing.T) {
+                                       builder := tc.new()
+                                       defer builder.Release()
+                                       assert.Error(t, decoder.apply(builder, 
"127.00000000000000001"))
+                               })
+                       }
+               })
+       }
+}
+
+func TestUnionBuilderUnmarshalOnePreservesDecoderConfiguration(t *testing.T) {
+       unionFields := []arrow.Field{{Name: "value", Type: 
arrow.PrimitiveTypes.Int32}}
+       cases := []struct {
+               name string
+               typ  arrow.DataType
+       }{
+               {
+                       name: "dense",
+                       typ:  arrow.DenseUnionOf(unionFields, 
[]arrow.UnionTypeCode{0}),
+               },
+               {
+                       name: "sparse",
+                       typ:  arrow.SparseUnionOf(unionFields, 
[]arrow.UnionTypeCode{0}),
+               },
+       }
+
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       dtype := arrow.StructOf(
+                               arrow.Field{Name: "u", Type: tc.typ},
+                               arrow.Field{Name: "i", Type: 
arrow.PrimitiveTypes.Int32},
+                       )
+                       for _, input := range []struct {
+                               name string
+                               json string
+                       }{
+                               {name: "integer first", json: 
`{"i":1.5,"u":[0,1]}`},
+                               {name: "union first", json: 
`{"u":[0,1],"i":1.5}`},
+                       } {
+                               t.Run(input.name, func(t *testing.T) {
+                                       builder := 
array.NewStructBuilder(memory.DefaultAllocator, dtype)
+                                       defer builder.Release()
+
+                                       dec := 
internaljson.NewDecoder(strings.NewReader(input.json))
+                                       require.NoError(t, 
builder.UnmarshalOne(dec))
+                               })
+                       }
+               })
+       }
+}
+
+func TestUnionBuilderCanAppendMaxTypeCodeChild(t *testing.T) {
+       cases := []struct {
+               name string
+               new  func(memory.Allocator) array.UnionBuilder
+       }{
+               {
+                       name: "dense",
+                       new: func(mem memory.Allocator) array.UnionBuilder {
+                               return array.NewEmptyDenseUnionBuilder(mem)
+                       },
+               },
+               {
+                       name: "sparse",
+                       new: func(mem memory.Allocator) array.UnionBuilder {
+                               return array.NewEmptySparseUnionBuilder(mem)
+                       },
+               },
+       }
+
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       mem := 
memory.NewCheckedAllocator(memory.NewGoAllocator())
+                       defer mem.AssertSize(t, 0)
+
+                       builder := tc.new(mem)
+                       defer builder.Release()
+                       appendChild := func(name string) arrow.UnionTypeCode {
+                               child := array.NewInt32Builder(mem)
+                               defer child.Release()
+                               return builder.AppendChild(child, name)
+                       }
+
+                       for i := 0; i <= int(arrow.MaxUnionTypeCode); i++ {
+                               code := appendChild(fmt.Sprintf("child-%d", i))
+                               assert.EqualValues(t, i, code)
+                       }
+                       assert.Equal(t, int(arrow.MaxUnionTypeCode)+1, 
builder.Type().(arrow.UnionType).NumFields())
+
+                       assert.PanicsWithValue(t, "arrow/array: too many 
children typeids", func() {
+                               appendChild("overflow")
+                       })
+                       assert.Equal(t, int(arrow.MaxUnionTypeCode)+1, 
builder.Type().(arrow.UnionType).NumFields())
+               })
+       }
+}
+
 func TestUnionSliceEquals(t *testing.T) {
        unionFields := []arrow.Field{
                {Name: "u0", Type: arrow.PrimitiveTypes.Int32, Nullable: true},

Reply via email to