mirror of
https://github.com/crazy-max/diun.git
synced 2024-12-22 19:38:28 +00:00
614 lines
17 KiB
Go
614 lines
17 KiB
Go
package toml
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"math"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Unmarshaler is the interface implemented by objects that can unmarshal a
|
|
// TOML description of themselves.
|
|
type Unmarshaler interface {
|
|
UnmarshalTOML(any) error
|
|
}
|
|
|
|
// Unmarshal decodes the contents of data in TOML format into a pointer v.
|
|
//
|
|
// See [Decoder] for a description of the decoding process.
|
|
func Unmarshal(data []byte, v any) error {
|
|
_, err := NewDecoder(bytes.NewReader(data)).Decode(v)
|
|
return err
|
|
}
|
|
|
|
// Decode the TOML data in to the pointer v.
|
|
//
|
|
// See [Decoder] for a description of the decoding process.
|
|
func Decode(data string, v any) (MetaData, error) {
|
|
return NewDecoder(strings.NewReader(data)).Decode(v)
|
|
}
|
|
|
|
// DecodeFile reads the contents of a file and decodes it with [Decode].
|
|
func DecodeFile(path string, v any) (MetaData, error) {
|
|
fp, err := os.Open(path)
|
|
if err != nil {
|
|
return MetaData{}, err
|
|
}
|
|
defer fp.Close()
|
|
return NewDecoder(fp).Decode(v)
|
|
}
|
|
|
|
// DecodeFS reads the contents of a file from [fs.FS] and decodes it with
|
|
// [Decode].
|
|
func DecodeFS(fsys fs.FS, path string, v any) (MetaData, error) {
|
|
fp, err := fsys.Open(path)
|
|
if err != nil {
|
|
return MetaData{}, err
|
|
}
|
|
defer fp.Close()
|
|
return NewDecoder(fp).Decode(v)
|
|
}
|
|
|
|
// Primitive is a TOML value that hasn't been decoded into a Go value.
|
|
//
|
|
// This type can be used for any value, which will cause decoding to be delayed.
|
|
// You can use [PrimitiveDecode] to "manually" decode these values.
|
|
//
|
|
// NOTE: The underlying representation of a `Primitive` value is subject to
|
|
// change. Do not rely on it.
|
|
//
|
|
// NOTE: Primitive values are still parsed, so using them will only avoid the
|
|
// overhead of reflection. They can be useful when you don't know the exact type
|
|
// of TOML data until runtime.
|
|
type Primitive struct {
|
|
undecoded any
|
|
context Key
|
|
}
|
|
|
|
// The significand precision for float32 and float64 is 24 and 53 bits; this is
|
|
// the range a natural number can be stored in a float without loss of data.
|
|
const (
|
|
maxSafeFloat32Int = 16777215 // 2^24-1
|
|
maxSafeFloat64Int = int64(9007199254740991) // 2^53-1
|
|
)
|
|
|
|
// Decoder decodes TOML data.
|
|
//
|
|
// TOML tables correspond to Go structs or maps; they can be used
|
|
// interchangeably, but structs offer better type safety.
|
|
//
|
|
// TOML table arrays correspond to either a slice of structs or a slice of maps.
|
|
//
|
|
// TOML datetimes correspond to [time.Time]. Local datetimes are parsed in the
|
|
// local timezone.
|
|
//
|
|
// [time.Duration] types are treated as nanoseconds if the TOML value is an
|
|
// integer, or they're parsed with time.ParseDuration() if they're strings.
|
|
//
|
|
// All other TOML types (float, string, int, bool and array) correspond to the
|
|
// obvious Go types.
|
|
//
|
|
// An exception to the above rules is if a type implements the TextUnmarshaler
|
|
// interface, in which case any primitive TOML value (floats, strings, integers,
|
|
// booleans, datetimes) will be converted to a []byte and given to the value's
|
|
// UnmarshalText method. See the Unmarshaler example for a demonstration with
|
|
// email addresses.
|
|
//
|
|
// # Key mapping
|
|
//
|
|
// TOML keys can map to either keys in a Go map or field names in a Go struct.
|
|
// The special `toml` struct tag can be used to map TOML keys to struct fields
|
|
// that don't match the key name exactly (see the example). A case insensitive
|
|
// match to struct names will be tried if an exact match can't be found.
|
|
//
|
|
// The mapping between TOML values and Go values is loose. That is, there may
|
|
// exist TOML values that cannot be placed into your representation, and there
|
|
// may be parts of your representation that do not correspond to TOML values.
|
|
// This loose mapping can be made stricter by using the IsDefined and/or
|
|
// Undecoded methods on the MetaData returned.
|
|
//
|
|
// This decoder does not handle cyclic types. Decode will not terminate if a
|
|
// cyclic type is passed.
|
|
type Decoder struct {
|
|
r io.Reader
|
|
}
|
|
|
|
// NewDecoder creates a new Decoder.
|
|
func NewDecoder(r io.Reader) *Decoder {
|
|
return &Decoder{r: r}
|
|
}
|
|
|
|
var (
|
|
unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
|
|
unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
|
primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem()
|
|
)
|
|
|
|
// Decode TOML data in to the pointer `v`.
|
|
func (dec *Decoder) Decode(v any) (MetaData, error) {
|
|
rv := reflect.ValueOf(v)
|
|
if rv.Kind() != reflect.Ptr {
|
|
s := "%q"
|
|
if reflect.TypeOf(v) == nil {
|
|
s = "%v"
|
|
}
|
|
|
|
return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v))
|
|
}
|
|
if rv.IsNil() {
|
|
return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v))
|
|
}
|
|
|
|
// Check if this is a supported type: struct, map, any, or something that
|
|
// implements UnmarshalTOML or UnmarshalText.
|
|
rv = indirect(rv)
|
|
rt := rv.Type()
|
|
if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map &&
|
|
!(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) &&
|
|
!rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) {
|
|
return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt)
|
|
}
|
|
|
|
// TODO: parser should read from io.Reader? Or at the very least, make it
|
|
// read from []byte rather than string
|
|
data, err := io.ReadAll(dec.r)
|
|
if err != nil {
|
|
return MetaData{}, err
|
|
}
|
|
|
|
p, err := parse(string(data))
|
|
if err != nil {
|
|
return MetaData{}, err
|
|
}
|
|
|
|
md := MetaData{
|
|
mapping: p.mapping,
|
|
keyInfo: p.keyInfo,
|
|
keys: p.ordered,
|
|
decoded: make(map[string]struct{}, len(p.ordered)),
|
|
context: nil,
|
|
data: data,
|
|
}
|
|
return md, md.unify(p.mapping, rv)
|
|
}
|
|
|
|
// PrimitiveDecode is just like the other Decode* functions, except it decodes a
|
|
// TOML value that has already been parsed. Valid primitive values can *only* be
|
|
// obtained from values filled by the decoder functions, including this method.
|
|
// (i.e., v may contain more [Primitive] values.)
|
|
//
|
|
// Meta data for primitive values is included in the meta data returned by the
|
|
// Decode* functions with one exception: keys returned by the Undecoded method
|
|
// will only reflect keys that were decoded. Namely, any keys hidden behind a
|
|
// Primitive will be considered undecoded. Executing this method will update the
|
|
// undecoded keys in the meta data. (See the example.)
|
|
func (md *MetaData) PrimitiveDecode(primValue Primitive, v any) error {
|
|
md.context = primValue.context
|
|
defer func() { md.context = nil }()
|
|
return md.unify(primValue.undecoded, rvalue(v))
|
|
}
|
|
|
|
// unify performs a sort of type unification based on the structure of `rv`,
|
|
// which is the client representation.
|
|
//
|
|
// Any type mismatch produces an error. Finding a type that we don't know
|
|
// how to handle produces an unsupported type error.
|
|
func (md *MetaData) unify(data any, rv reflect.Value) error {
|
|
// Special case. Look for a `Primitive` value.
|
|
// TODO: #76 would make this superfluous after implemented.
|
|
if rv.Type() == primitiveType {
|
|
// Save the undecoded data and the key context into the primitive
|
|
// value.
|
|
context := make(Key, len(md.context))
|
|
copy(context, md.context)
|
|
rv.Set(reflect.ValueOf(Primitive{
|
|
undecoded: data,
|
|
context: context,
|
|
}))
|
|
return nil
|
|
}
|
|
|
|
rvi := rv.Interface()
|
|
if v, ok := rvi.(Unmarshaler); ok {
|
|
err := v.UnmarshalTOML(data)
|
|
if err != nil {
|
|
return md.parseErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
if v, ok := rvi.(encoding.TextUnmarshaler); ok {
|
|
return md.unifyText(data, v)
|
|
}
|
|
|
|
// TODO:
|
|
// The behavior here is incorrect whenever a Go type satisfies the
|
|
// encoding.TextUnmarshaler interface but also corresponds to a TOML hash or
|
|
// array. In particular, the unmarshaler should only be applied to primitive
|
|
// TOML values. But at this point, it will be applied to all kinds of values
|
|
// and produce an incorrect error whenever those values are hashes or arrays
|
|
// (including arrays of tables).
|
|
|
|
k := rv.Kind()
|
|
|
|
if k >= reflect.Int && k <= reflect.Uint64 {
|
|
return md.unifyInt(data, rv)
|
|
}
|
|
switch k {
|
|
case reflect.Struct:
|
|
return md.unifyStruct(data, rv)
|
|
case reflect.Map:
|
|
return md.unifyMap(data, rv)
|
|
case reflect.Array:
|
|
return md.unifyArray(data, rv)
|
|
case reflect.Slice:
|
|
return md.unifySlice(data, rv)
|
|
case reflect.String:
|
|
return md.unifyString(data, rv)
|
|
case reflect.Bool:
|
|
return md.unifyBool(data, rv)
|
|
case reflect.Interface:
|
|
if rv.NumMethod() > 0 { /// Only empty interfaces are supported.
|
|
return md.e("unsupported type %s", rv.Type())
|
|
}
|
|
return md.unifyAnything(data, rv)
|
|
case reflect.Float32, reflect.Float64:
|
|
return md.unifyFloat64(data, rv)
|
|
}
|
|
return md.e("unsupported type %s", rv.Kind())
|
|
}
|
|
|
|
func (md *MetaData) unifyStruct(mapping any, rv reflect.Value) error {
|
|
tmap, ok := mapping.(map[string]any)
|
|
if !ok {
|
|
if mapping == nil {
|
|
return nil
|
|
}
|
|
return md.e("type mismatch for %s: expected table but found %s", rv.Type().String(), fmtType(mapping))
|
|
}
|
|
|
|
for key, datum := range tmap {
|
|
var f *field
|
|
fields := cachedTypeFields(rv.Type())
|
|
for i := range fields {
|
|
ff := &fields[i]
|
|
if ff.name == key {
|
|
f = ff
|
|
break
|
|
}
|
|
if f == nil && strings.EqualFold(ff.name, key) {
|
|
f = ff
|
|
}
|
|
}
|
|
if f != nil {
|
|
subv := rv
|
|
for _, i := range f.index {
|
|
subv = indirect(subv.Field(i))
|
|
}
|
|
|
|
if isUnifiable(subv) {
|
|
md.decoded[md.context.add(key).String()] = struct{}{}
|
|
md.context = append(md.context, key)
|
|
|
|
err := md.unify(datum, subv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
md.context = md.context[0 : len(md.context)-1]
|
|
} else if f.name != "" {
|
|
return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (md *MetaData) unifyMap(mapping any, rv reflect.Value) error {
|
|
keyType := rv.Type().Key().Kind()
|
|
if keyType != reflect.String && keyType != reflect.Interface {
|
|
return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)",
|
|
keyType, rv.Type())
|
|
}
|
|
|
|
tmap, ok := mapping.(map[string]any)
|
|
if !ok {
|
|
if tmap == nil {
|
|
return nil
|
|
}
|
|
return md.badtype("map", mapping)
|
|
}
|
|
if rv.IsNil() {
|
|
rv.Set(reflect.MakeMap(rv.Type()))
|
|
}
|
|
for k, v := range tmap {
|
|
md.decoded[md.context.add(k).String()] = struct{}{}
|
|
md.context = append(md.context, k)
|
|
|
|
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
|
|
|
|
err := md.unify(v, indirect(rvval))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
md.context = md.context[0 : len(md.context)-1]
|
|
|
|
rvkey := indirect(reflect.New(rv.Type().Key()))
|
|
|
|
switch keyType {
|
|
case reflect.Interface:
|
|
rvkey.Set(reflect.ValueOf(k))
|
|
case reflect.String:
|
|
rvkey.SetString(k)
|
|
}
|
|
|
|
rv.SetMapIndex(rvkey, rvval)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (md *MetaData) unifyArray(data any, rv reflect.Value) error {
|
|
datav := reflect.ValueOf(data)
|
|
if datav.Kind() != reflect.Slice {
|
|
if !datav.IsValid() {
|
|
return nil
|
|
}
|
|
return md.badtype("slice", data)
|
|
}
|
|
if l := datav.Len(); l != rv.Len() {
|
|
return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l)
|
|
}
|
|
return md.unifySliceArray(datav, rv)
|
|
}
|
|
|
|
func (md *MetaData) unifySlice(data any, rv reflect.Value) error {
|
|
datav := reflect.ValueOf(data)
|
|
if datav.Kind() != reflect.Slice {
|
|
if !datav.IsValid() {
|
|
return nil
|
|
}
|
|
return md.badtype("slice", data)
|
|
}
|
|
n := datav.Len()
|
|
if rv.IsNil() || rv.Cap() < n {
|
|
rv.Set(reflect.MakeSlice(rv.Type(), n, n))
|
|
}
|
|
rv.SetLen(n)
|
|
return md.unifySliceArray(datav, rv)
|
|
}
|
|
|
|
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
|
|
l := data.Len()
|
|
for i := 0; i < l; i++ {
|
|
err := md.unify(data.Index(i).Interface(), indirect(rv.Index(i)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (md *MetaData) unifyString(data any, rv reflect.Value) error {
|
|
_, ok := rv.Interface().(json.Number)
|
|
if ok {
|
|
if i, ok := data.(int64); ok {
|
|
rv.SetString(strconv.FormatInt(i, 10))
|
|
} else if f, ok := data.(float64); ok {
|
|
rv.SetString(strconv.FormatFloat(f, 'f', -1, 64))
|
|
} else {
|
|
return md.badtype("string", data)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if s, ok := data.(string); ok {
|
|
rv.SetString(s)
|
|
return nil
|
|
}
|
|
return md.badtype("string", data)
|
|
}
|
|
|
|
func (md *MetaData) unifyFloat64(data any, rv reflect.Value) error {
|
|
rvk := rv.Kind()
|
|
|
|
if num, ok := data.(float64); ok {
|
|
switch rvk {
|
|
case reflect.Float32:
|
|
if num < -math.MaxFloat32 || num > math.MaxFloat32 {
|
|
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
|
}
|
|
fallthrough
|
|
case reflect.Float64:
|
|
rv.SetFloat(num)
|
|
default:
|
|
panic("bug")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if num, ok := data.(int64); ok {
|
|
if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) ||
|
|
(rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) {
|
|
return md.parseErr(errUnsafeFloat{i: num, size: rvk.String()})
|
|
}
|
|
rv.SetFloat(float64(num))
|
|
return nil
|
|
}
|
|
|
|
return md.badtype("float", data)
|
|
}
|
|
|
|
func (md *MetaData) unifyInt(data any, rv reflect.Value) error {
|
|
_, ok := rv.Interface().(time.Duration)
|
|
if ok {
|
|
// Parse as string duration, and fall back to regular integer parsing
|
|
// (as nanosecond) if this is not a string.
|
|
if s, ok := data.(string); ok {
|
|
dur, err := time.ParseDuration(s)
|
|
if err != nil {
|
|
return md.parseErr(errParseDuration{s})
|
|
}
|
|
rv.SetInt(int64(dur))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
num, ok := data.(int64)
|
|
if !ok {
|
|
return md.badtype("integer", data)
|
|
}
|
|
|
|
rvk := rv.Kind()
|
|
switch {
|
|
case rvk >= reflect.Int && rvk <= reflect.Int64:
|
|
if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) ||
|
|
(rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) ||
|
|
(rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) {
|
|
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
|
}
|
|
rv.SetInt(num)
|
|
case rvk >= reflect.Uint && rvk <= reflect.Uint64:
|
|
unum := uint64(num)
|
|
if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) ||
|
|
rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) ||
|
|
rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) {
|
|
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
|
}
|
|
rv.SetUint(unum)
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (md *MetaData) unifyBool(data any, rv reflect.Value) error {
|
|
if b, ok := data.(bool); ok {
|
|
rv.SetBool(b)
|
|
return nil
|
|
}
|
|
return md.badtype("boolean", data)
|
|
}
|
|
|
|
func (md *MetaData) unifyAnything(data any, rv reflect.Value) error {
|
|
rv.Set(reflect.ValueOf(data))
|
|
return nil
|
|
}
|
|
|
|
func (md *MetaData) unifyText(data any, v encoding.TextUnmarshaler) error {
|
|
var s string
|
|
switch sdata := data.(type) {
|
|
case Marshaler:
|
|
text, err := sdata.MarshalTOML()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s = string(text)
|
|
case encoding.TextMarshaler:
|
|
text, err := sdata.MarshalText()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s = string(text)
|
|
case fmt.Stringer:
|
|
s = sdata.String()
|
|
case string:
|
|
s = sdata
|
|
case bool:
|
|
s = fmt.Sprintf("%v", sdata)
|
|
case int64:
|
|
s = fmt.Sprintf("%d", sdata)
|
|
case float64:
|
|
s = fmt.Sprintf("%f", sdata)
|
|
default:
|
|
return md.badtype("primitive (string-like)", data)
|
|
}
|
|
if err := v.UnmarshalText([]byte(s)); err != nil {
|
|
return md.parseErr(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (md *MetaData) badtype(dst string, data any) error {
|
|
return md.e("incompatible types: TOML value has type %s; destination has type %s", fmtType(data), dst)
|
|
}
|
|
|
|
func (md *MetaData) parseErr(err error) error {
|
|
k := md.context.String()
|
|
return ParseError{
|
|
LastKey: k,
|
|
Position: md.keyInfo[k].pos,
|
|
Line: md.keyInfo[k].pos.Line,
|
|
err: err,
|
|
input: string(md.data),
|
|
}
|
|
}
|
|
|
|
func (md *MetaData) e(format string, args ...any) error {
|
|
f := "toml: "
|
|
if len(md.context) > 0 {
|
|
f = fmt.Sprintf("toml: (last key %q): ", md.context)
|
|
p := md.keyInfo[md.context.String()].pos
|
|
if p.Line > 0 {
|
|
f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context)
|
|
}
|
|
}
|
|
return fmt.Errorf(f+format, args...)
|
|
}
|
|
|
|
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
|
|
func rvalue(v any) reflect.Value {
|
|
return indirect(reflect.ValueOf(v))
|
|
}
|
|
|
|
// indirect returns the value pointed to by a pointer.
|
|
//
|
|
// Pointers are followed until the value is not a pointer. New values are
|
|
// allocated for each nil pointer.
|
|
//
|
|
// An exception to this rule is if the value satisfies an interface of interest
|
|
// to us (like encoding.TextUnmarshaler).
|
|
func indirect(v reflect.Value) reflect.Value {
|
|
if v.Kind() != reflect.Ptr {
|
|
if v.CanSet() {
|
|
pv := v.Addr()
|
|
pvi := pv.Interface()
|
|
if _, ok := pvi.(encoding.TextUnmarshaler); ok {
|
|
return pv
|
|
}
|
|
if _, ok := pvi.(Unmarshaler); ok {
|
|
return pv
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
if v.IsNil() {
|
|
v.Set(reflect.New(v.Type().Elem()))
|
|
}
|
|
return indirect(reflect.Indirect(v))
|
|
}
|
|
|
|
func isUnifiable(rv reflect.Value) bool {
|
|
if rv.CanSet() {
|
|
return true
|
|
}
|
|
rvi := rv.Interface()
|
|
if _, ok := rvi.(encoding.TextUnmarshaler); ok {
|
|
return true
|
|
}
|
|
if _, ok := rvi.(Unmarshaler); ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// fmt %T with "interface {}" replaced with "any", which is far more readable.
|
|
func fmtType(t any) string {
|
|
return strings.ReplaceAll(fmt.Sprintf("%T", t), "interface {}", "any")
|
|
}
|