Files
notarius/internal/core/source/metadata.go

120 lines
3.6 KiB
Go

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
}