374 lines
9.3 KiB
Go
374 lines
9.3 KiB
Go
// Package jsonvalue validates and defensively copies bounded JSON-compatible
|
|
// value trees used by configuration, request, and prepared-state boundaries.
|
|
package jsonvalue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"sort"
|
|
)
|
|
|
|
const (
|
|
maxContainerDepth = 100
|
|
maxProducedNodes = 100_000
|
|
)
|
|
|
|
type visit struct {
|
|
typ reflect.Type
|
|
ptr uintptr
|
|
}
|
|
|
|
type traversalState struct {
|
|
active map[visit]struct{}
|
|
producedNodes int
|
|
}
|
|
|
|
// Copy validates and deeply copies a JSON-compatible value while preserving
|
|
// compatible concrete map, slice, array, scalar, and number types. It rejects
|
|
// cycles and values that exceed the package's traversal limits.
|
|
func Copy(src any) (any, error) {
|
|
return copyValue(reflect.ValueOf(src), "value", newTraversalState(), true, 0)
|
|
}
|
|
|
|
// CopyMap validates and deeply copies an extra-parameter map while preserving
|
|
// compatible concrete map, slice, array, scalar, and number types. It rejects
|
|
// empty object keys, cycles, and values that exceed the package's traversal
|
|
// limits.
|
|
func CopyMap(src map[string]any) (map[string]any, error) {
|
|
if src == nil {
|
|
return nil, nil
|
|
}
|
|
copied, err := copyValue(reflect.ValueOf(src), "extra_params", newTraversalState(), false, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, ok := copied.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("extra_params: expected object")
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func copyValue(
|
|
value reflect.Value,
|
|
path string,
|
|
state *traversalState,
|
|
allowEmptyMapKeys bool,
|
|
containerDepth int,
|
|
) (any, error) {
|
|
resolved, cleanup, isNull, err := state.resolveIndirection(value, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer cleanup()
|
|
if isNull {
|
|
if err := state.produceNode(path); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, nil
|
|
}
|
|
value = resolved
|
|
if !value.CanInterface() {
|
|
return nil, fmt.Errorf("%s: value cannot be copied", path)
|
|
}
|
|
if number, ok := value.Interface().(json.Number); ok {
|
|
if !validJSONNumber(number) {
|
|
return nil, fmt.Errorf("%s: invalid JSON number", path)
|
|
}
|
|
if err := state.produceNode(path); err != nil {
|
|
return nil, err
|
|
}
|
|
return number, nil
|
|
}
|
|
|
|
switch value.Kind() {
|
|
case reflect.Bool, reflect.String:
|
|
if err := state.produceNode(path); err != nil {
|
|
return nil, err
|
|
}
|
|
return value.Interface(), nil
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
if err := state.produceNode(path); err != nil {
|
|
return nil, err
|
|
}
|
|
return value.Interface(), nil
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
if err := state.produceNode(path); err != nil {
|
|
return nil, err
|
|
}
|
|
return value.Interface(), nil
|
|
case reflect.Float32, reflect.Float64:
|
|
number := value.Float()
|
|
if math.IsNaN(number) || math.IsInf(number, 0) {
|
|
return nil, fmt.Errorf("%s: floating-point value must be finite", path)
|
|
}
|
|
if err := state.produceNode(path); err != nil {
|
|
return nil, err
|
|
}
|
|
return value.Interface(), nil
|
|
case reflect.Map:
|
|
if value.IsNil() {
|
|
if err := state.produceNode(path); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, nil
|
|
}
|
|
nextDepth, err := state.enterContainer(path, containerDepth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return copyMapValue(value, path, state, allowEmptyMapKeys, nextDepth)
|
|
case reflect.Slice:
|
|
if value.IsNil() {
|
|
if err := state.produceNode(path); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, nil
|
|
}
|
|
nextDepth, err := state.enterContainer(path, containerDepth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return copySequenceValue(value, path, state, allowEmptyMapKeys, nextDepth)
|
|
case reflect.Array:
|
|
nextDepth, err := state.enterContainer(path, containerDepth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return copySequenceValue(value, path, state, allowEmptyMapKeys, nextDepth)
|
|
default:
|
|
return nil, fmt.Errorf("%s: unsupported JSON value type %s", path, value.Type())
|
|
}
|
|
}
|
|
|
|
func copyMapValue(
|
|
value reflect.Value,
|
|
path string,
|
|
state *traversalState,
|
|
allowEmptyMapKeys bool,
|
|
containerDepth int,
|
|
) (any, error) {
|
|
if value.Type().Key().Kind() != reflect.String {
|
|
return nil, fmt.Errorf("%s: map key type %s is not supported", path, value.Type().Key())
|
|
}
|
|
if err := state.produceNode(path); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := state.ensureChildCapacity(path, value.Len()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
current := visit{typ: value.Type(), ptr: value.Pointer()}
|
|
if _, ok := state.active[current]; ok {
|
|
return nil, fmt.Errorf("%s: cyclic value is not supported", path)
|
|
}
|
|
state.active[current] = struct{}{}
|
|
defer delete(state.active, current)
|
|
|
|
keys := value.MapKeys()
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
return keys[i].String() < keys[j].String()
|
|
})
|
|
|
|
type entry struct {
|
|
key reflect.Value
|
|
name string
|
|
value any
|
|
}
|
|
entries := make([]entry, 0, len(keys))
|
|
preserveType := true
|
|
elementType := value.Type().Elem()
|
|
for _, key := range keys {
|
|
name := key.String()
|
|
if name == "" && !allowEmptyMapKeys {
|
|
return nil, fmt.Errorf("%s: map key must not be empty", path)
|
|
}
|
|
copied, err := copyValue(
|
|
value.MapIndex(key),
|
|
path+"."+name,
|
|
state,
|
|
allowEmptyMapKeys,
|
|
containerDepth,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries = append(entries, entry{key: key, name: name, value: copied})
|
|
if copied == nil {
|
|
if !canAssignNil(elementType) {
|
|
preserveType = false
|
|
}
|
|
continue
|
|
}
|
|
if !reflect.TypeOf(copied).AssignableTo(elementType) {
|
|
preserveType = false
|
|
}
|
|
}
|
|
|
|
if preserveType {
|
|
out := reflect.MakeMapWithSize(value.Type(), len(entries))
|
|
for _, entry := range entries {
|
|
if entry.value == nil {
|
|
out.SetMapIndex(entry.key, reflect.Zero(elementType))
|
|
continue
|
|
}
|
|
out.SetMapIndex(entry.key, reflect.ValueOf(entry.value))
|
|
}
|
|
return out.Interface(), nil
|
|
}
|
|
|
|
out := make(map[string]any, len(entries))
|
|
for _, entry := range entries {
|
|
out[entry.name] = entry.value
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func copySequenceValue(
|
|
value reflect.Value,
|
|
path string,
|
|
state *traversalState,
|
|
allowEmptyMapKeys bool,
|
|
containerDepth int,
|
|
) (any, error) {
|
|
if err := state.produceNode(path); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := state.ensureChildCapacity(path, value.Len()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var current visit
|
|
if value.Kind() == reflect.Slice {
|
|
current = visit{typ: value.Type(), ptr: value.Pointer()}
|
|
if _, ok := state.active[current]; ok {
|
|
return nil, fmt.Errorf("%s: cyclic value is not supported", path)
|
|
}
|
|
state.active[current] = struct{}{}
|
|
defer delete(state.active, current)
|
|
}
|
|
|
|
values := make([]any, value.Len())
|
|
preserveType := true
|
|
elementType := value.Type().Elem()
|
|
for i := 0; i < value.Len(); i++ {
|
|
copied, err := copyValue(
|
|
value.Index(i),
|
|
fmt.Sprintf("%s[%d]", path, i),
|
|
state,
|
|
allowEmptyMapKeys,
|
|
containerDepth,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values[i] = copied
|
|
if copied == nil {
|
|
if !canAssignNil(elementType) {
|
|
preserveType = false
|
|
}
|
|
continue
|
|
}
|
|
if !reflect.TypeOf(copied).AssignableTo(elementType) {
|
|
preserveType = false
|
|
}
|
|
}
|
|
|
|
if preserveType {
|
|
out := reflect.New(value.Type()).Elem()
|
|
if value.Kind() == reflect.Slice {
|
|
out = reflect.MakeSlice(value.Type(), value.Len(), value.Len())
|
|
}
|
|
for i, copied := range values {
|
|
if copied == nil {
|
|
out.Index(i).Set(reflect.Zero(elementType))
|
|
continue
|
|
}
|
|
out.Index(i).Set(reflect.ValueOf(copied))
|
|
}
|
|
return out.Interface(), nil
|
|
}
|
|
|
|
out := make([]any, len(values))
|
|
copy(out, values)
|
|
return out, nil
|
|
}
|
|
|
|
func validJSONNumber(number json.Number) bool {
|
|
var parsed json.Number
|
|
if err := json.Unmarshal([]byte(number.String()), &parsed); err != nil {
|
|
return false
|
|
}
|
|
return parsed.String() == number.String()
|
|
}
|
|
|
|
func newTraversalState() *traversalState {
|
|
return &traversalState{active: make(map[visit]struct{})}
|
|
}
|
|
|
|
func (state *traversalState) produceNode(path string) error {
|
|
if state.producedNodes >= maxProducedNodes {
|
|
return fmt.Errorf("%s: JSON value work limit exceeded", path)
|
|
}
|
|
state.producedNodes++
|
|
return nil
|
|
}
|
|
|
|
func (state *traversalState) enterContainer(path string, depth int) (int, error) {
|
|
depth++
|
|
if depth > maxContainerDepth {
|
|
return 0, fmt.Errorf("%s: JSON container depth limit exceeded", path)
|
|
}
|
|
return depth, nil
|
|
}
|
|
|
|
func (state *traversalState) ensureChildCapacity(path string, count int) error {
|
|
if count > maxProducedNodes-state.producedNodes {
|
|
return fmt.Errorf("%s: JSON value work limit exceeded", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (state *traversalState) resolveIndirection(
|
|
value reflect.Value,
|
|
path string,
|
|
) (reflect.Value, func(), bool, error) {
|
|
var visits []visit
|
|
cleanup := func() {
|
|
for _, current := range visits {
|
|
delete(state.active, current)
|
|
}
|
|
}
|
|
|
|
for value.IsValid() && (value.Kind() == reflect.Interface || value.Kind() == reflect.Pointer) {
|
|
if value.IsNil() {
|
|
return reflect.Value{}, cleanup, true, nil
|
|
}
|
|
if value.Kind() == reflect.Pointer {
|
|
current := visit{typ: value.Type(), ptr: value.Pointer()}
|
|
if _, ok := state.active[current]; ok {
|
|
cleanup()
|
|
return reflect.Value{}, nil, false, fmt.Errorf("%s: cyclic value is not supported", path)
|
|
}
|
|
state.active[current] = struct{}{}
|
|
visits = append(visits, current)
|
|
}
|
|
value = value.Elem()
|
|
}
|
|
if !value.IsValid() {
|
|
return reflect.Value{}, cleanup, true, nil
|
|
}
|
|
return value, cleanup, false, nil
|
|
}
|
|
|
|
func canAssignNil(typ reflect.Type) bool {
|
|
switch typ.Kind() {
|
|
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|