Centralize trusted JSON value cloning
This commit is contained in:
97
internal/jsonvalue/trusted.go
Normal file
97
internal/jsonvalue/trusted.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package jsonvalue
|
||||
|
||||
import "reflect"
|
||||
|
||||
func cloneTrustedMap(src map[string]any) map[string]any {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]any, len(src))
|
||||
for k, v := range src {
|
||||
out[k] = CloneTrusted(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// CloneTrusted detaches a previously accepted value without revalidating it.
|
||||
func CloneTrusted(value any) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
return cloneTrustedMap(v)
|
||||
case []any:
|
||||
out := make([]any, len(v))
|
||||
for i, item := range v {
|
||||
out[i] = CloneTrusted(item)
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
if v == nil {
|
||||
return []string(nil)
|
||||
}
|
||||
return append([]string{}, v...)
|
||||
case []byte:
|
||||
if v == nil {
|
||||
return []byte(nil)
|
||||
}
|
||||
return append([]byte{}, v...)
|
||||
default:
|
||||
return cloneTrustedReflect(reflect.ValueOf(value)).Interface()
|
||||
}
|
||||
}
|
||||
|
||||
func cloneTrustedReflect(value reflect.Value) reflect.Value {
|
||||
if !value.IsValid() {
|
||||
return value
|
||||
}
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.Interface:
|
||||
if value.IsNil() {
|
||||
return reflect.Zero(value.Type())
|
||||
}
|
||||
copied := cloneTrustedReflect(value.Elem())
|
||||
if copied.IsValid() && copied.Type().AssignableTo(value.Type()) {
|
||||
return copied
|
||||
}
|
||||
out := reflect.New(value.Type()).Elem()
|
||||
out.Set(copied)
|
||||
return out
|
||||
case reflect.Pointer:
|
||||
if value.IsNil() {
|
||||
return reflect.Zero(value.Type())
|
||||
}
|
||||
out := reflect.New(value.Type().Elem())
|
||||
out.Elem().Set(cloneTrustedReflect(value.Elem()))
|
||||
return out
|
||||
case reflect.Map:
|
||||
if value.IsNil() {
|
||||
return reflect.Zero(value.Type())
|
||||
}
|
||||
out := reflect.MakeMapWithSize(value.Type(), value.Len())
|
||||
iter := value.MapRange()
|
||||
for iter.Next() {
|
||||
out.SetMapIndex(cloneTrustedReflect(iter.Key()), cloneTrustedReflect(iter.Value()))
|
||||
}
|
||||
return out
|
||||
case reflect.Slice:
|
||||
if value.IsNil() {
|
||||
return reflect.Zero(value.Type())
|
||||
}
|
||||
out := reflect.MakeSlice(value.Type(), value.Len(), value.Cap())
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
out.Index(i).Set(cloneTrustedReflect(value.Index(i)))
|
||||
}
|
||||
return out
|
||||
case reflect.Array:
|
||||
out := reflect.New(value.Type()).Elem()
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
out.Index(i).Set(cloneTrustedReflect(value.Index(i)))
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
27
internal/jsonvalue/trusted_test.go
Normal file
27
internal/jsonvalue/trusted_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package jsonvalue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type trustedNamed string
|
||||
|
||||
func TestCloneTrustedPreservesTypesAndDetaches(t *testing.T) {
|
||||
named := trustedNamed("value")
|
||||
original := map[string]any{"": map[string]any{"number": json.Number("9007199254740993")}, "typed": []string{"original"}, "pointer": &named, "nilmap": map[string]any(nil), "nilslice": []string(nil), "empty": []any{}}
|
||||
copy := CloneTrusted(original).(map[string]any)
|
||||
if !reflect.DeepEqual(copy, original) {
|
||||
t.Fatalf("clone = %#v", copy)
|
||||
}
|
||||
copy[""].(map[string]any)["number"] = json.Number("1")
|
||||
copy["typed"].([]string)[0] = "changed"
|
||||
*copy["pointer"].(*trustedNamed) = "changed"
|
||||
if original[""].(map[string]any)["number"] != json.Number("9007199254740993") || original["typed"].([]string)[0] != "original" || named != "value" {
|
||||
t.Fatal("clone shares mutable storage")
|
||||
}
|
||||
if copy["nilmap"].(map[string]any) != nil || copy["nilslice"].([]string) != nil || copy["empty"].([]any) == nil {
|
||||
t.Fatal("nil/empty shape changed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user