v0.1.1 decode hardening, round trip support, doc and test update
Test / test (push) Canceled after 0s
Test / test (push) Canceled after 0s
This commit is contained in:
15 files changed
+874
-651
No files matched your search
@@ -17,18 +17,33 @@ func Unmarshal(data []byte, v any) error {
|
||||
return Decode(parsedMap, v)
|
||||
}
|
||||
|
||||
// Decode maps a generic map[string]any to a struct/slice/etc using reflection.
|
||||
// It prioritizes `toml` tags and falls back to field names.
|
||||
// Decode maps parser values to typed destinations using reflection.
|
||||
// It uses toml tags, falling back to field names, and leaves v unchanged on error.
|
||||
// Interface destinations retain the input value, including container references.
|
||||
func Decode(data any, v any) error {
|
||||
val := reflect.ValueOf(v)
|
||||
if val.Kind() != reflect.Ptr || val.IsNil() {
|
||||
return fmt.Errorf("target must be a non-nil pointer")
|
||||
}
|
||||
|
||||
return decodeValue(data, val.Elem())
|
||||
// Stage changes; pointer, map and slice branches allocate before writing.
|
||||
next := reflect.New(val.Elem().Type()).Elem()
|
||||
next.Set(val.Elem())
|
||||
if err := decodeValue(data, next); err != nil {
|
||||
return err
|
||||
}
|
||||
val.Elem().Set(next)
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeValue(data any, val reflect.Value) error {
|
||||
return decodeAt(data, val, 0)
|
||||
}
|
||||
|
||||
func decodeAt(data any, val reflect.Value, depth int) error {
|
||||
if depth > maxValueDepth {
|
||||
return fmt.Errorf("decode nesting exceeds %d", maxValueDepth)
|
||||
}
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -37,7 +52,10 @@ func decodeValue(data any, val reflect.Value) error {
|
||||
case reflect.Ptr:
|
||||
elemType := val.Type().Elem()
|
||||
newVal := reflect.New(elemType)
|
||||
if err := decodeValue(data, newVal.Elem()); err != nil {
|
||||
if !val.IsNil() {
|
||||
newVal.Elem().Set(val.Elem())
|
||||
}
|
||||
if err := decodeAt(data, newVal.Elem(), depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
val.Set(newVal)
|
||||
@@ -47,9 +65,9 @@ func decodeValue(data any, val reflect.Value) error {
|
||||
if !ok {
|
||||
return fmt.Errorf("expected map for struct, got %T", data)
|
||||
}
|
||||
return decodeStruct(dataMap, val)
|
||||
return decodeStruct(dataMap, val, depth+1)
|
||||
|
||||
case reflect.Slice:
|
||||
case reflect.Slice, reflect.Array:
|
||||
dataSlice, ok := data.([]any)
|
||||
if !ok {
|
||||
if mapSlice, ok := data.([]map[string]any); ok {
|
||||
@@ -62,10 +80,18 @@ func decodeValue(data any, val reflect.Value) error {
|
||||
}
|
||||
}
|
||||
|
||||
newSlice := reflect.MakeSlice(val.Type(), len(dataSlice), len(dataSlice))
|
||||
var newSlice reflect.Value
|
||||
if val.Kind() == reflect.Array {
|
||||
if len(dataSlice) != val.Len() {
|
||||
return fmt.Errorf("expected array length %d, got %d", val.Len(), len(dataSlice))
|
||||
}
|
||||
newSlice = reflect.New(val.Type()).Elem()
|
||||
} else {
|
||||
newSlice = reflect.MakeSlice(val.Type(), len(dataSlice), len(dataSlice))
|
||||
}
|
||||
for i := 0; i < len(dataSlice); i++ {
|
||||
if err := decodeValue(dataSlice[i], newSlice.Index(i)); err != nil {
|
||||
return err
|
||||
if err := decodeAt(dataSlice[i], newSlice.Index(i), depth+1); err != nil {
|
||||
return fmt.Errorf("index %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
val.Set(newSlice)
|
||||
@@ -78,14 +104,14 @@ func decodeValue(data any, val reflect.Value) error {
|
||||
if !ok {
|
||||
return fmt.Errorf("expected map, got %T", data)
|
||||
}
|
||||
newMap := reflect.MakeMap(val.Type())
|
||||
newMap := reflect.MakeMapWithSize(val.Type(), len(dataMap))
|
||||
elemType := val.Type().Elem()
|
||||
for k, vData := range dataMap {
|
||||
newVal := reflect.New(elemType).Elem()
|
||||
if err := decodeValue(vData, newVal); err != nil {
|
||||
if err := decodeAt(vData, newVal, depth+1); err != nil {
|
||||
return fmt.Errorf("map key %s: %w", k, err)
|
||||
}
|
||||
newMap.SetMapIndex(reflect.ValueOf(k), newVal)
|
||||
newMap.SetMapIndex(reflect.ValueOf(k).Convert(val.Type().Key()), newVal)
|
||||
}
|
||||
val.Set(newMap)
|
||||
|
||||
@@ -109,21 +135,19 @@ func decodeValue(data any, val reflect.Value) error {
|
||||
val.SetInt(i)
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
i, ok := toInt64(data)
|
||||
u, ok := toUint64(data)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot convert %T to uint", data)
|
||||
}
|
||||
if i < 0 {
|
||||
return fmt.Errorf("cannot convert negative value %d to uint", i)
|
||||
// Formatting a rejected container's value could recurse through a cycle.
|
||||
return fmt.Errorf("cannot convert %T to uint without loss", data)
|
||||
}
|
||||
// Overflow check
|
||||
if val.OverflowUint(uint64(i)) {
|
||||
return fmt.Errorf("value %d overflows %s", i, val.Type())
|
||||
if val.OverflowUint(u) {
|
||||
return fmt.Errorf("value %d overflows %s", u, val.Type())
|
||||
}
|
||||
val.SetUint(uint64(i))
|
||||
val.SetUint(u)
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f, ok := toFloat(data)
|
||||
f, ok := toFloat(data, val.Type().Bits())
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot convert %T to float", data)
|
||||
}
|
||||
@@ -155,7 +179,7 @@ func decodeValue(data any, val reflect.Value) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeStruct(data map[string]any, val reflect.Value) error {
|
||||
func decodeStruct(data map[string]any, val reflect.Value, depth int) error {
|
||||
typ := val.Type()
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
@@ -174,12 +198,14 @@ func decodeStruct(data map[string]any, val reflect.Value) error {
|
||||
if parts[0] == "-" {
|
||||
continue
|
||||
}
|
||||
key = parts[0]
|
||||
if parts[0] != "" {
|
||||
key = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Look up in data map (case sensitive)
|
||||
if vData, ok := data[key]; ok {
|
||||
if err := decodeValue(vData, field); err != nil {
|
||||
if err := decodeAt(vData, field, depth+1); err != nil {
|
||||
return fmt.Errorf("%s.%s: %w", typ.Name(), fieldType.Name, err)
|
||||
}
|
||||
}
|
||||
@@ -187,67 +213,79 @@ func decodeStruct(data map[string]any, val reflect.Value) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// toInt64 converts numeric types to int64
|
||||
// Numeric conversions check before conversion: rounded float64(MaxInt64) is
|
||||
// 2^63, so the upper bound must be exclusive. No float intermediary for integers.
|
||||
func toInt64(v any) (int64, bool) {
|
||||
switch i := v.(type) {
|
||||
case int:
|
||||
return int64(i), true
|
||||
case int8:
|
||||
return int64(i), true
|
||||
case int16:
|
||||
return int64(i), true
|
||||
case int32:
|
||||
return int64(i), true
|
||||
case int64:
|
||||
return i, true
|
||||
case uint:
|
||||
// Reject wrap
|
||||
if uint64(i) > math.MaxInt64 {
|
||||
r := reflect.ValueOf(v)
|
||||
if !r.IsValid() {
|
||||
return 0, false
|
||||
}
|
||||
switch r.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return r.Int(), true
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
u := r.Uint()
|
||||
return int64(u), u <= math.MaxInt64
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f := r.Float()
|
||||
if math.IsNaN(f) || f < -0x1p63 || f >= 0x1p63 || math.Trunc(f) != f {
|
||||
return 0, false
|
||||
}
|
||||
case uint8:
|
||||
return int64(i), true
|
||||
case uint16:
|
||||
return int64(i), true
|
||||
case uint32:
|
||||
return int64(i), true
|
||||
case uint64:
|
||||
// Reject values that wrap negative through int64
|
||||
if i > math.MaxInt64 {
|
||||
return 0, false
|
||||
}
|
||||
return int64(i), true
|
||||
case float64:
|
||||
return int64(i), true
|
||||
return int64(f), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func toFloat(v any) (float64, bool) {
|
||||
switch i := v.(type) {
|
||||
case int:
|
||||
return float64(i), true
|
||||
case int8:
|
||||
return float64(i), true
|
||||
case int16:
|
||||
return float64(i), true
|
||||
case int32:
|
||||
return float64(i), true
|
||||
case int64:
|
||||
return float64(i), true
|
||||
case uint:
|
||||
return float64(i), true
|
||||
case uint8:
|
||||
return float64(i), true
|
||||
case uint16:
|
||||
return float64(i), true
|
||||
case uint32:
|
||||
return float64(i), true
|
||||
case uint64:
|
||||
return float64(i), true
|
||||
case float64:
|
||||
return i, true
|
||||
func toUint64(v any) (uint64, bool) {
|
||||
r := reflect.ValueOf(v)
|
||||
if !r.IsValid() {
|
||||
return 0, false
|
||||
}
|
||||
switch r.Kind() {
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return r.Uint(), true
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
i := r.Int()
|
||||
return uint64(i), i >= 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f := r.Float()
|
||||
if math.IsNaN(f) || f < 0 || f >= 0x1p64 || math.Trunc(f) != f {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(f), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func toFloat(v any, width int) (float64, bool) {
|
||||
r := reflect.ValueOf(v)
|
||||
if !r.IsValid() {
|
||||
return 0, false
|
||||
}
|
||||
var f float64
|
||||
switch r.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
i := r.Int()
|
||||
f = float64(i)
|
||||
if width == 32 {
|
||||
f = float64(float32(f))
|
||||
}
|
||||
if f >= 0x1p63 || f < -0x1p63 || int64(f) != i {
|
||||
return 0, false
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
u := r.Uint()
|
||||
f = float64(u)
|
||||
if width == 32 {
|
||||
f = float64(float32(f))
|
||||
}
|
||||
if f >= 0x1p64 || uint64(f) != u {
|
||||
return 0, false
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f = r.Float()
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
return f, !math.IsNaN(f) && !math.IsInf(f, 0)
|
||||
}
|
||||
Reference in new issue
Block a user