v0.2.0 toml-only, harden concurrency and env export, dep updates
Test / test (push) Canceled after 0s
Test / test (push) Canceled after 0s
This commit is contained in:
1 parent
3c35601179
commit
e08467bf38
42 files changed
+2533
-4274
No files matched your search
@@ -0,0 +1,162 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const maxValueDepth = 128
|
||||
|
||||
var (
|
||||
timeType = reflect.TypeFor[time.Time]()
|
||||
durationType = reflect.TypeFor[time.Duration]()
|
||||
ipType = reflect.TypeFor[net.IP]()
|
||||
ipNetType = reflect.TypeFor[net.IPNet]()
|
||||
urlType = reflect.TypeFor[url.URL]()
|
||||
userInfoType = reflect.TypeFor[url.Userinfo]()
|
||||
)
|
||||
|
||||
func atomicType(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
return t == timeType || t == ipType || t == ipNetType || t == urlType
|
||||
}
|
||||
|
||||
// copyValue validates the supported value graph and detaches mutable containers.
|
||||
// The depth bound rejects cycles without retaining a global identity cache.
|
||||
func copyValue(v any) (any, error) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out, err := copyReflect(reflect.ValueOf(v), 0)
|
||||
if err != nil {
|
||||
return nil, wrapError(ErrTypeMismatch, err)
|
||||
}
|
||||
return out.Interface(), nil
|
||||
}
|
||||
|
||||
func copyReflect(v reflect.Value, depth int) (reflect.Value, error) {
|
||||
if depth > maxValueDepth {
|
||||
return reflect.Value{}, fmt.Errorf("value nesting exceeds %d (possible cycle)", maxValueDepth)
|
||||
}
|
||||
if v.Type() == ipType && v.Len() != 0 && v.Interface().(net.IP).To16() == nil {
|
||||
return reflect.Value{}, fmt.Errorf("invalid IP value")
|
||||
}
|
||||
if v.Type() == ipNetType {
|
||||
n := v.Interface().(net.IPNet)
|
||||
_, bits := n.Mask.Size()
|
||||
if (len(n.IP) != 0 || len(n.Mask) != 0) && (n.IP.To16() == nil || bits == 0 || bits == 32 && n.IP.To4() == nil) {
|
||||
return reflect.Value{}, fmt.Errorf("invalid CIDR value")
|
||||
}
|
||||
}
|
||||
if v.Type() == timeType {
|
||||
if _, err := v.Interface().(time.Time).MarshalText(); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
}
|
||||
out := reflect.New(v.Type()).Elem()
|
||||
switch v.Kind() {
|
||||
case reflect.Interface, reflect.Pointer:
|
||||
if v.IsNil() {
|
||||
return out, nil
|
||||
}
|
||||
elem, err := copyReflect(v.Elem(), depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
if v.Kind() == reflect.Pointer {
|
||||
out.Set(reflect.New(v.Type().Elem()))
|
||||
out.Elem().Set(elem)
|
||||
} else {
|
||||
out.Set(elem)
|
||||
}
|
||||
case reflect.Map:
|
||||
if v.Type().Key().Kind() != reflect.String {
|
||||
return reflect.Value{}, fmt.Errorf("map keys must be strings")
|
||||
}
|
||||
if v.IsNil() {
|
||||
return out, nil
|
||||
}
|
||||
out.Set(reflect.MakeMapWithSize(v.Type(), v.Len()))
|
||||
it := v.MapRange()
|
||||
for it.Next() {
|
||||
if !utf8.ValidString(it.Key().String()) {
|
||||
return reflect.Value{}, fmt.Errorf("invalid UTF-8 map key")
|
||||
}
|
||||
x, err := copyReflect(it.Value(), depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out.SetMapIndex(it.Key(), x)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
if v.Kind() == reflect.Slice {
|
||||
if v.IsNil() {
|
||||
return out, nil
|
||||
}
|
||||
out.Set(reflect.MakeSlice(v.Type(), v.Len(), v.Len()))
|
||||
}
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
x, err := copyReflect(v.Index(i), depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out.Index(i).Set(x)
|
||||
}
|
||||
case reflect.Struct:
|
||||
// These standard-library values contain immutable private state.
|
||||
if v.Type() == timeType || v.Type() == userInfoType {
|
||||
out.Set(v)
|
||||
return out, nil
|
||||
}
|
||||
exported := false
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if !v.Type().Field(i).IsExported() {
|
||||
continue
|
||||
}
|
||||
exported = true
|
||||
x, err := copyReflect(v.Field(i), depth+1)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
out.Field(i).Set(x)
|
||||
}
|
||||
if !exported && v.NumField() != 0 {
|
||||
return reflect.Value{}, fmt.Errorf("unsupported private state in %s", v.Type())
|
||||
}
|
||||
case reflect.String:
|
||||
if len(v.String()) > MaxValueSize {
|
||||
return reflect.Value{}, ErrValueSize
|
||||
}
|
||||
if !utf8.ValidString(v.String()) {
|
||||
return reflect.Value{}, fmt.Errorf("invalid UTF-8 string")
|
||||
}
|
||||
out.Set(v)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if math.IsNaN(v.Float()) || math.IsInf(v.Float(), 0) {
|
||||
return reflect.Value{}, fmt.Errorf("non-finite float")
|
||||
}
|
||||
out.Set(v)
|
||||
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
out.Set(v)
|
||||
default:
|
||||
return reflect.Value{}, fmt.Errorf("unsupported value type %s", v.Type())
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// cloneOwned is used only on values already accepted by copyValue.
|
||||
func cloneOwned(v any) any {
|
||||
out, err := copyValue(v)
|
||||
if err != nil {
|
||||
panic("config: invalid internal value: " + err.Error())
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user