package scriptorium import ( "encoding/json" "fmt" "math" "reflect" "strconv" ) const maxSafeJSONInteger = 1<<53 - 1 type jsonVisit struct { typ reflect.Type ptr uintptr } func copyPublicJSONMap(src map[string]any) (map[string]any, error) { if src == nil { return nil, nil } copied, err := copyPublicJSONValue(reflect.ValueOf(src), "extra_params", make(map[jsonVisit]struct{})) 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 copyPublicJSONValue(value reflect.Value, path string, seen map[jsonVisit]struct{}) (any, error) { if !value.IsValid() { return nil, nil } if value.Kind() == reflect.Interface { if value.IsNil() { return nil, nil } return copyPublicJSONValue(value.Elem(), path, seen) } if !value.CanInterface() { return nil, fmt.Errorf("%s: value cannot be copied", path) } if number, ok := value.Interface().(json.Number); ok { 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: f := value.Convert(reflect.TypeOf(float64(0))).Float() if math.IsNaN(f) || math.IsInf(f, 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 } visit := jsonVisit{typ: value.Type(), ptr: value.Pointer()} if _, ok := seen[visit]; ok { return nil, fmt.Errorf("%s: cyclic value is not supported", path) } seen[visit] = struct{}{} defer delete(seen, visit) return copyPublicJSONValue(value.Elem(), path, seen) case reflect.Map: return copyPublicJSONMapValue(value, path, seen) case reflect.Slice: if value.IsNil() { return nil, nil } return copyPublicJSONSequenceValue(value, path, seen) case reflect.Array: return copyPublicJSONSequenceValue(value, path, seen) default: return nil, fmt.Errorf("%s: unsupported JSON value type %s", path, value.Type()) } } func copyPublicJSONMapValue(value reflect.Value, path string, seen map[jsonVisit]struct{}) (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()) } visit := jsonVisit{typ: value.Type(), ptr: value.Pointer()} if _, ok := seen[visit]; ok { return nil, fmt.Errorf("%s: cyclic value is not supported", path) } seen[visit] = struct{}{} defer delete(seen, visit) type entry struct { key reflect.Value name string value any } entries := make([]entry, 0, value.Len()) preserveType := true elemType := value.Type().Elem() iter := value.MapRange() for iter.Next() { key := iter.Key() name := key.String() copied, err := copyPublicJSONValue(iter.Value(), path+"."+name, seen) if err != nil { return nil, err } entries = append(entries, entry{key: key, name: name, value: copied}) if copied == nil { if !canAssignNil(elemType) { preserveType = false } continue } if !reflect.TypeOf(copied).AssignableTo(elemType) { 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(elemType)) 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 copyPublicJSONSequenceValue(value reflect.Value, path string, seen map[jsonVisit]struct{}) (any, error) { var visit jsonVisit if value.Kind() == reflect.Slice { visit = jsonVisit{typ: value.Type(), ptr: value.Pointer()} if _, ok := seen[visit]; ok { return nil, fmt.Errorf("%s: cyclic value is not supported", path) } seen[visit] = struct{}{} defer delete(seen, visit) } values := make([]any, value.Len()) preserveType := true elemType := value.Type().Elem() for i := 0; i < value.Len(); i++ { copied, err := copyPublicJSONValue(value.Index(i), fmt.Sprintf("%s[%d]", path, i), seen) if err != nil { return nil, err } values[i] = copied if copied == nil { if !canAssignNil(elemType) { preserveType = false } continue } if !reflect.TypeOf(copied).AssignableTo(elemType) { 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(elemType)) 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 } }