// Package jsonvalue validates and defensively copies JSON-compatible value // trees used by configuration, request, and prepared-state boundaries. package jsonvalue import ( "encoding/json" "fmt" "math" "reflect" "sort" "strconv" ) const maxSafeJSONInteger = 1<<53 - 1 type visit struct { typ reflect.Type ptr uintptr } // Copy validates and deeply copies a JSON-compatible value while preserving // compatible concrete map, slice, array, scalar, and number types. func Copy(src any) (any, error) { return copyValue(reflect.ValueOf(src), "value", make(map[visit]struct{}), true) } // CopyMap validates and deeply copies an extra-parameter map while preserving // compatible concrete map, slice, array, scalar, and number types. func CopyMap(src map[string]any) (map[string]any, error) { if src == nil { return nil, nil } copied, err := copyValue(reflect.ValueOf(src), "extra_params", make(map[visit]struct{}), false) 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, seen map[visit]struct{}, allowEmptyMapKeys bool, ) (any, error) { if !value.IsValid() { return nil, nil } if value.Kind() == reflect.Interface { if value.IsNil() { return nil, nil } return copyValue(value.Elem(), path, seen, allowEmptyMapKeys) } if !value.CanInterface() { return nil, fmt.Errorf("%s: value cannot be copied", path) } if number, ok := value.Interface().(json.Number); ok { if _, err := json.Marshal(number); err != nil { return nil, fmt.Errorf("%s: invalid JSON number", path) } f, err := strconv.ParseFloat(number.String(), 64) if err != nil || math.IsNaN(f) || math.IsInf(f, 0) { return nil, fmt.Errorf("%s: invalid JSON number", path) } return number, nil } switch value.Kind() { case reflect.Bool, reflect.String: return value.Interface(), nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if value.Int() < -maxSafeJSONInteger || value.Int() > maxSafeJSONInteger { return nil, fmt.Errorf("%s: integer is outside the JSON-safe range", path) } return value.Interface(), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: if value.Uint() > maxSafeJSONInteger { return nil, fmt.Errorf("%s: integer is outside the JSON-safe range", path) } return value.Interface(), nil case reflect.Float32, reflect.Float64: number := value.Convert(reflect.TypeOf(float64(0))).Float() if math.IsNaN(number) || math.IsInf(number, 0) { return nil, fmt.Errorf("%s: floating-point value must be finite", path) } return value.Interface(), nil case reflect.Pointer: if value.IsNil() { return nil, nil } current := visit{typ: value.Type(), ptr: value.Pointer()} if _, ok := seen[current]; ok { return nil, fmt.Errorf("%s: cyclic value is not supported", path) } seen[current] = struct{}{} defer delete(seen, current) return copyValue(value.Elem(), path, seen, allowEmptyMapKeys) case reflect.Map: return copyMapValue(value, path, seen, allowEmptyMapKeys) case reflect.Slice: if value.IsNil() { return nil, nil } return copySequenceValue(value, path, seen, allowEmptyMapKeys) case reflect.Array: return copySequenceValue(value, path, seen, allowEmptyMapKeys) default: return nil, fmt.Errorf("%s: unsupported JSON value type %s", path, value.Type()) } } func copyMapValue( value reflect.Value, path string, seen map[visit]struct{}, allowEmptyMapKeys bool, ) (any, error) { if value.IsNil() { return nil, nil } if value.Type().Key().Kind() != reflect.String { return nil, fmt.Errorf("%s: map key type %s is not supported", path, value.Type().Key()) } current := visit{typ: value.Type(), ptr: value.Pointer()} if _, ok := seen[current]; ok { return nil, fmt.Errorf("%s: cyclic value is not supported", path) } seen[current] = struct{}{} defer delete(seen, 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, seen, allowEmptyMapKeys) 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, seen map[visit]struct{}, allowEmptyMapKeys bool, ) (any, error) { var current visit if value.Kind() == reflect.Slice { current = visit{typ: value.Type(), ptr: value.Pointer()} if _, ok := seen[current]; ok { return nil, fmt.Errorf("%s: cyclic value is not supported", path) } seen[current] = struct{}{} defer delete(seen, 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), seen, allowEmptyMapKeys, ) 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 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 } }