Deep-clone source metadata during chunk materialization
This commit is contained in:
119
internal/core/source/metadata.go
Normal file
119
internal/core/source/metadata.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// CloneMetadata returns an independently owned copy of JSON-shaped metadata.
|
||||
// It preserves concrete map, slice, and array types while rejecting values that
|
||||
// cannot be safely represented as JSON-shaped metadata.
|
||||
func CloneMetadata(metadata map[string]any) (map[string]any, error) {
|
||||
if len(metadata) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
cloned, err := cloneMetadataValue(reflect.ValueOf(metadata), "metadata", make(map[metadataVisit]struct{}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloned.Interface().(map[string]any), nil
|
||||
}
|
||||
|
||||
type metadataVisit struct {
|
||||
typ reflect.Type
|
||||
ptr uintptr
|
||||
}
|
||||
|
||||
func cloneMetadataValue(value reflect.Value, location string, active map[metadataVisit]struct{}) (reflect.Value, error) {
|
||||
if !value.IsValid() {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Interface:
|
||||
if value.IsNil() {
|
||||
return reflect.Zero(value.Type()), nil
|
||||
}
|
||||
cloned, err := cloneMetadataValue(value.Elem(), location, active)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
result := reflect.New(value.Type()).Elem()
|
||||
result.Set(cloned)
|
||||
return result, nil
|
||||
case reflect.Map:
|
||||
if value.IsNil() {
|
||||
return reflect.Zero(value.Type()), nil
|
||||
}
|
||||
if value.Type().Key().Kind() != reflect.String {
|
||||
return reflect.Value{}, fmt.Errorf("%s has unsupported map key type %s", location, value.Type().Key())
|
||||
}
|
||||
leave, err := enterMetadataValue(value, active, location)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
defer leave()
|
||||
|
||||
keys := value.MapKeys()
|
||||
sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
|
||||
result := reflect.MakeMapWithSize(value.Type(), value.Len())
|
||||
for _, key := range keys {
|
||||
cloned, err := cloneMetadataValue(value.MapIndex(key), location+"."+key.String(), active)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
result.SetMapIndex(key, cloned)
|
||||
}
|
||||
return result, nil
|
||||
case reflect.Slice:
|
||||
if value.IsNil() {
|
||||
return reflect.Zero(value.Type()), nil
|
||||
}
|
||||
leave, err := enterMetadataValue(value, active, location)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
defer leave()
|
||||
|
||||
result := reflect.MakeSlice(value.Type(), value.Len(), value.Len())
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
cloned, err := cloneMetadataValue(value.Index(i), fmt.Sprintf("%s[%d]", location, i), active)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
result.Index(i).Set(cloned)
|
||||
}
|
||||
return result, nil
|
||||
case reflect.Array:
|
||||
result := reflect.New(value.Type()).Elem()
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
cloned, err := cloneMetadataValue(value.Index(i), fmt.Sprintf("%s[%d]", location, i), active)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
result.Index(i).Set(cloned)
|
||||
}
|
||||
return result, nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if math.IsNaN(value.Float()) || math.IsInf(value.Float(), 0) {
|
||||
return reflect.Value{}, fmt.Errorf("%s has a non-finite number", location)
|
||||
}
|
||||
return value, nil
|
||||
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.String:
|
||||
return value, nil
|
||||
default:
|
||||
return reflect.Value{}, fmt.Errorf("%s has unsupported type %s", location, value.Type())
|
||||
}
|
||||
}
|
||||
|
||||
func enterMetadataValue(value reflect.Value, active map[metadataVisit]struct{}, location string) (func(), error) {
|
||||
visit := metadataVisit{typ: value.Type(), ptr: value.Pointer()}
|
||||
if _, exists := active[visit]; exists {
|
||||
return nil, fmt.Errorf("%s contains a cycle", location)
|
||||
}
|
||||
active[visit] = struct{}{}
|
||||
return func() { delete(active, visit) }, nil
|
||||
}
|
||||
Reference in New Issue
Block a user