Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -52,6 +52,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
order on decode and sorts on encode. It works as a decode target on its
|
||||
own, in a struct field, and as the element of an array of tables; its
|
||||
values are untyped, so a nested table stays a `map[string]any`.
|
||||
- `UnmarshalWithOptions(data, v, opts)` decodes with a `DecodeOptions` struct
|
||||
in one call, the options a `Decoder` sets without building one: unknown
|
||||
keys, `Number` literals, and the parse limits.
|
||||
- `Marshal` carries a nesting limit of 10000 levels, the parser's own figure:
|
||||
cyclic data, which used to run the stack out, is now rejected with an error
|
||||
that names the limit and the path it was met at.
|
||||
- The `toml` tag gained the `required` option: a field tagged
|
||||
`toml:"host,required"` makes the decode fail with
|
||||
`missing required key "host"` when the document carries no key that
|
||||
|
||||
+16
-2
@@ -73,6 +73,13 @@ if err := interpres.Valid(data); err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
### `func UnmarshalWithOptions(data []byte, v any, opts DecodeOptions) error`
|
||||
|
||||
The one-shot form of a configured `Decoder`: the same options as
|
||||
`NewDecoder` sets, in a `DecodeOptions` struct, applied to a single call.
|
||||
The zero value takes the defaults: unknown keys ignored, numbers evaluated
|
||||
as `int64` and `float64`, no size limit and the 10000-level nesting default.
|
||||
|
||||
## Documents
|
||||
|
||||
`Parse` returns a `Document`: the value tree together with what a map cannot
|
||||
@@ -443,6 +450,11 @@ different top-level value fails:
|
||||
| a nil `any` | `interpres: cannot marshal nil value` |
|
||||
| a nil pointer | `interpres: cannot marshal nil pointer` |
|
||||
|
||||
The encoding walk carries a nesting limit of 10000 levels, the parser's own
|
||||
figure: a value that nests deeper, which cyclic data always does, is rejected
|
||||
with an error that names the limit and suggests the cycle, instead of running
|
||||
the stack out.
|
||||
|
||||
### Field matching
|
||||
|
||||
Struct fields become TOML keys as follows:
|
||||
@@ -739,9 +751,11 @@ same way.
|
||||
### `type Decoder`
|
||||
|
||||
Configurable strictness for decoding, constructed with `NewDecoder`. Set up
|
||||
with `DisallowUnknownFields`, then call `Decode` or `DecodeContext` any number
|
||||
with the chainable methods, then call `Decode` or `DecodeContext` any number
|
||||
of times. A configured `Decoder` holds no per-call state and is safe for
|
||||
concurrent use.
|
||||
concurrent use. For a single document, `UnmarshalWithOptions(data, v,
|
||||
DecodeOptions{...})` sets the same options without the Decoder; its zero
|
||||
value takes the defaults.
|
||||
|
||||
| Method | Default | Effect |
|
||||
|---|---|---|
|
||||
|
||||
@@ -247,7 +247,10 @@ func buildOrderedDoc(om *OrderedMap, doc *tomlDoc, path encPath) error {
|
||||
val := om.values[key]
|
||||
switch x := val.(type) {
|
||||
case OrderedMap:
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
|
||||
if atDepthLimit(sub.depth) {
|
||||
return &EncodeError{Path: path.key(key).String(), Err: errDepthLimit()}
|
||||
}
|
||||
if err := buildOrderedDoc(&x, sub, path.key(key)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -257,7 +260,10 @@ func buildOrderedDoc(om *OrderedMap, doc *tomlDoc, path encPath) error {
|
||||
if x == nil {
|
||||
continue
|
||||
}
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
|
||||
if atDepthLimit(sub.depth) {
|
||||
return &EncodeError{Path: path.key(key).String(), Err: errDepthLimit()}
|
||||
}
|
||||
if err := buildOrderedDoc(x, sub, path.key(key)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -304,11 +310,30 @@ type entry struct {
|
||||
emitted bool
|
||||
}
|
||||
|
||||
// tomlDoc holds the entries of one TOML table in declaration order.
|
||||
// tomlDoc holds the entries of one TOML table in declaration order. depth is
|
||||
// the nesting level the table sits at, which bounds the walk: cyclic data
|
||||
// would recurse forever, and hits the limit instead of the stack.
|
||||
type tomlDoc struct {
|
||||
entries []entry
|
||||
ctx context.Context // inherited from encoder; nil-safe
|
||||
opts Encoder // inherited from encoder; options drive emit-time behaviour
|
||||
depth int
|
||||
}
|
||||
|
||||
// maxEncodeDepth bounds the encoding walk. It matches the parser's nesting
|
||||
// limit: a document that deep cannot be written by this encoder either, and
|
||||
// a value that nests that far without being an array or a table is cyclic.
|
||||
const maxEncodeDepth = maxNestingDepth
|
||||
|
||||
// atDepthLimit reports whether a table nested depth levels is past the walk's
|
||||
// limit.
|
||||
func atDepthLimit(depth int) bool {
|
||||
return depth > maxEncodeDepth
|
||||
}
|
||||
|
||||
// errDepthLimit is the failure a cyclic value walks into.
|
||||
func errDepthLimit() error {
|
||||
return fmt.Errorf("value nests deeper than the limit of %d levels; the value may be cyclic", maxEncodeDepth)
|
||||
}
|
||||
|
||||
func (d *tomlDoc) checkCtx() error {
|
||||
@@ -385,7 +410,7 @@ func (p encPath) String() string {
|
||||
// --- reflection walk: struct ---------------------------------------------
|
||||
|
||||
func buildStructDoc(v reflect.Value, doc *tomlDoc, path encPath) error {
|
||||
return walkStructDoc(v, doc, path, nil, cachedStructSchema(v.Type()))
|
||||
return walkStructDoc(v, doc, path, nil, cachedStructSchema(v.Type()), doc.depth)
|
||||
}
|
||||
|
||||
// walkStructDoc emits the fields of v into doc. prefix is v's index path from
|
||||
@@ -393,8 +418,13 @@ func buildStructDoc(v reflect.Value, doc *tomlDoc, path encPath) error {
|
||||
// with the outer schema and a longer prefix, so every leaf competes under the
|
||||
// decoder's rule: the shallower field wins, the later declaration at equal
|
||||
// depth. A field another field shadows is skipped, because emitting both
|
||||
// would duplicate the key and the output would not re-parse.
|
||||
func walkStructDoc(v reflect.Value, doc *tomlDoc, path encPath, prefix []int, schema structSchema) error {
|
||||
// would duplicate the key and the output would not re-parse. depth is the
|
||||
// nesting the walk has reached, which an embedded struct raises; a cycle
|
||||
// through embedded pointers ends at the limit instead of the stack.
|
||||
func walkStructDoc(v reflect.Value, doc *tomlDoc, path encPath, prefix []int, schema structSchema, depth int) error {
|
||||
if atDepthLimit(depth) {
|
||||
return fmt.Errorf("interpres: %s: %w", path.String(), errDepthLimit())
|
||||
}
|
||||
t := v.Type()
|
||||
if cap(doc.entries) == 0 {
|
||||
doc.entries = make([]entry, 0, t.NumField())
|
||||
@@ -430,7 +460,7 @@ func walkStructDoc(v reflect.Value, doc *tomlDoc, path encPath, prefix []int, sc
|
||||
doc.addScalar(name, fv.Interface())
|
||||
continue
|
||||
}
|
||||
if err := walkStructDoc(fv, doc, path, fpath, schema); err != nil {
|
||||
if err := walkStructDoc(fv, doc, path, fpath, schema, depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
@@ -520,6 +550,9 @@ func fieldName(f reflect.StructField) string {
|
||||
// --- reflection walk: map ------------------------------------------------
|
||||
|
||||
func buildMapDoc(v reflect.Value, doc *tomlDoc, path encPath) error {
|
||||
if atDepthLimit(doc.depth) {
|
||||
return fmt.Errorf("interpres: %s: %w", path.String(), errDepthLimit())
|
||||
}
|
||||
if v.Type().Key().Kind() != reflect.String {
|
||||
return fmt.Errorf("interpres: map key must be string, got %s", v.Type().Key())
|
||||
}
|
||||
@@ -584,7 +617,10 @@ func addField(doc *tomlDoc, name string, v reflect.Value, path encPath) error {
|
||||
}
|
||||
if v.Type() == orderedMapType {
|
||||
om := v.Interface().(OrderedMap)
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
|
||||
if atDepthLimit(sub.depth) {
|
||||
return &EncodeError{Path: path.key(name).String(), Err: errDepthLimit()}
|
||||
}
|
||||
if err := buildOrderedDoc(&om, sub, path.key(name)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -613,7 +649,10 @@ func addField(doc *tomlDoc, name string, v reflect.Value, path encPath) error {
|
||||
}
|
||||
|
||||
func addSubTable(doc *tomlDoc, name string, v reflect.Value, path encPath) error {
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
|
||||
if atDepthLimit(sub.depth) {
|
||||
return &EncodeError{Path: path.key(name).String(), Err: errDepthLimit()}
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
if err := buildStructDoc(v, sub, path.key(name)); err != nil {
|
||||
@@ -692,7 +731,10 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, path encPath) err
|
||||
return err
|
||||
}
|
||||
}
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts}
|
||||
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
|
||||
if atDepthLimit(sub.depth) {
|
||||
return &EncodeError{Path: apath.elem(i).String(), Err: errDepthLimit()}
|
||||
}
|
||||
switch {
|
||||
case ev.Type() == orderedMapType:
|
||||
om := ev.Interface().(OrderedMap)
|
||||
@@ -795,6 +837,13 @@ func resolveElement(v reflect.Value, path encPath) (reflect.Value, error) {
|
||||
// nested-array representations the emitter understands. Slices and arrays are
|
||||
// recursively normalised so that nested arrays (e.g. [][]int) work.
|
||||
func normaliseValue(v reflect.Value) (any, error) {
|
||||
return normaliseValueAt(v, 0)
|
||||
}
|
||||
|
||||
func normaliseValueAt(v reflect.Value, depth int) (any, error) {
|
||||
if atDepthLimit(depth) {
|
||||
return nil, errDepthLimit()
|
||||
}
|
||||
if !v.IsValid() {
|
||||
return nil, fmt.Errorf("cannot encode nil value")
|
||||
}
|
||||
@@ -818,7 +867,7 @@ func normaliseValue(v reflect.Value) (any, error) {
|
||||
// receiver's own type is written as it is, because recursing into it
|
||||
// would never end.
|
||||
if rv := reflect.ValueOf(mv); rv.Type() != v.Type() {
|
||||
return normaliseValue(rv)
|
||||
return normaliseValueAt(rv, depth+1)
|
||||
}
|
||||
return mv, nil
|
||||
}
|
||||
@@ -842,7 +891,7 @@ func normaliseValue(v reflect.Value) (any, error) {
|
||||
om := v.Interface().(OrderedMap)
|
||||
out := make(map[string]any, om.Len())
|
||||
for _, k := range om.Keys() {
|
||||
val, err := normaliseValue(reflect.ValueOf(om.values[k]))
|
||||
val, err := normaliseValueAt(reflect.ValueOf(om.values[k]), depth+1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s]: %w", k, err)
|
||||
}
|
||||
@@ -882,7 +931,7 @@ func normaliseValue(v reflect.Value) (any, error) {
|
||||
}
|
||||
out := make(map[string]any, v.Len())
|
||||
for _, k := range v.MapKeys() {
|
||||
val, err := normaliseValue(v.MapIndex(k))
|
||||
val, err := normaliseValueAt(v.MapIndex(k), depth+1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s]: %w", k.String(), err)
|
||||
}
|
||||
@@ -892,7 +941,7 @@ func normaliseValue(v reflect.Value) (any, error) {
|
||||
case reflect.Slice, reflect.Array:
|
||||
items := make([]any, v.Len())
|
||||
for i := range v.Len() {
|
||||
val, err := normaliseValue(v.Index(i))
|
||||
val, err := normaliseValueAt(v.Index(i), depth+1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%d]: %w", i, err)
|
||||
}
|
||||
|
||||
+102
@@ -2024,3 +2024,105 @@ func TestMarshalAppend(t *testing.T) {
|
||||
t.Errorf("MarshalAppend with an unencodable value = %q, want an error", out3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalCyclicData(t *testing.T) {
|
||||
t.Run("a cyclic struct is an error, not a crash", func(t *testing.T) {
|
||||
type Node struct {
|
||||
Name string `toml:"name"`
|
||||
Next *Node `toml:"next"`
|
||||
}
|
||||
a := &Node{Name: "a"}
|
||||
b := &Node{Name: "b"}
|
||||
a.Next = b
|
||||
b.Next = a
|
||||
_, err := Marshal(a)
|
||||
if err == nil {
|
||||
t.Fatal("Marshal(cyclic) succeeded, want an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "may be cyclic") {
|
||||
t.Errorf("err = %v, want it to name the cycle", err)
|
||||
}
|
||||
})
|
||||
t.Run("a cyclic map is an error", func(t *testing.T) {
|
||||
m := map[string]any{}
|
||||
m["self"] = m
|
||||
if _, err := Marshal(m); err == nil {
|
||||
t.Fatal("Marshal(cyclic map) succeeded, want an error")
|
||||
}
|
||||
})
|
||||
t.Run("a cyclic value array is an error", func(t *testing.T) {
|
||||
m := map[string]any{}
|
||||
m["items"] = []any{int64(1), m}
|
||||
if _, err := Marshal(map[string]any{"outer": m}); err == nil {
|
||||
t.Fatal("Marshal(cyclic array) succeeded, want an error")
|
||||
}
|
||||
})
|
||||
t.Run("a deeply nested but finite value encodes", func(t *testing.T) {
|
||||
type Node struct {
|
||||
Next *Node `toml:"next"`
|
||||
}
|
||||
root := &Node{}
|
||||
cur := root
|
||||
for range 5000 {
|
||||
cur.Next = &Node{}
|
||||
cur = cur.Next
|
||||
}
|
||||
if _, err := Marshal(root); err != nil {
|
||||
t.Errorf("Marshal(deep) = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnmarshalWithOptions(t *testing.T) {
|
||||
data := []byte("host = \"db\"\nextra = 1\n")
|
||||
type Config struct {
|
||||
Host string `toml:"host,required"`
|
||||
}
|
||||
t.Run("the zero value takes the defaults", func(t *testing.T) {
|
||||
var cfg struct {
|
||||
Host string `toml:"host"`
|
||||
Extra int `toml:"extra"`
|
||||
}
|
||||
if err := UnmarshalWithOptions(data, &cfg, DecodeOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Host != "db" || cfg.Extra != 1 {
|
||||
t.Errorf("decoded %+v", cfg)
|
||||
}
|
||||
})
|
||||
t.Run("strict and required work in one call", func(t *testing.T) {
|
||||
err := UnmarshalWithOptions(data, &Config{}, DecodeOptions{DisallowUnknownFields: true})
|
||||
want := `interpres: unknown field "extra" for interpres.Config`
|
||||
if err == nil || err.Error() != want {
|
||||
t.Errorf("err = %v, want %q", err, want)
|
||||
}
|
||||
})
|
||||
t.Run("UseNumber keeps the literal", func(t *testing.T) {
|
||||
var tree map[string]any
|
||||
in := []byte("n = 1_000\n")
|
||||
if err := UnmarshalWithOptions(in, &tree, DecodeOptions{UseNumber: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, ok := tree["n"].(Number); !ok || got != "1_000" {
|
||||
t.Errorf("n = %#v, want Number(\"1_000\")", tree["n"])
|
||||
}
|
||||
})
|
||||
t.Run("the limits apply", func(t *testing.T) {
|
||||
var nested strings.Builder
|
||||
nested.WriteString("x = ")
|
||||
for range 20 {
|
||||
nested.WriteString("[")
|
||||
}
|
||||
nested.WriteString("1")
|
||||
for range 20 {
|
||||
nested.WriteString("]")
|
||||
}
|
||||
var tree map[string]any
|
||||
if err := UnmarshalWithOptions([]byte(nested.String()), &tree, DecodeOptions{MaxDepth: 10}); err == nil {
|
||||
t.Error("a document over MaxDepth decoded, want an error")
|
||||
}
|
||||
if err := UnmarshalWithOptions([]byte("a = 1\n"), &tree, DecodeOptions{MaxInputSize: 2}); err == nil {
|
||||
t.Error("a document over MaxInputSize decoded, want an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+32
-2
@@ -327,6 +327,35 @@ func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error {
|
||||
return dec.decode(tree, v)
|
||||
}
|
||||
|
||||
// DecodeOptions gathers the options a one-shot decode call can set, the
|
||||
// struct-shaped alternative to building a Decoder for a single document. The
|
||||
// zero value decodes with the defaults: unknown keys ignored, numbers
|
||||
// evaluated, and no limit beyond the nesting default.
|
||||
type DecodeOptions struct {
|
||||
// DisallowUnknownFields rejects a key with no matching struct field.
|
||||
DisallowUnknownFields bool
|
||||
// UseNumber keeps the numbers of the document as Number literals.
|
||||
UseNumber bool
|
||||
// MaxDepth bounds how deeply arrays and inline tables may nest; 0 takes
|
||||
// the default of 10000.
|
||||
MaxDepth int
|
||||
// MaxInputSize bounds the document size in bytes; 0 takes no limit.
|
||||
MaxInputSize int
|
||||
}
|
||||
|
||||
// UnmarshalWithOptions decodes data into v with the options set, the one-shot
|
||||
// form of building a Decoder. See DecodeOptions for the fields and their
|
||||
// defaults.
|
||||
func UnmarshalWithOptions(data []byte, v any, opts DecodeOptions) error {
|
||||
dec := &Decoder{
|
||||
disallowUnknown: opts.DisallowUnknownFields,
|
||||
useNumber: opts.UseNumber,
|
||||
maxDepth: opts.MaxDepth,
|
||||
maxInputSize: opts.MaxInputSize,
|
||||
}
|
||||
return dec.DecodeContext(context.Background(), data, v)
|
||||
}
|
||||
|
||||
// Marshaler is the interface implemented by types that can produce a custom
|
||||
// TOML representation of themselves. MarshalTOML returns a value that Marshal
|
||||
// then encodes as if the returned value had been passed in its place, which
|
||||
@@ -407,8 +436,9 @@ type UnmarshalerContext interface {
|
||||
// returns. time.Duration is written in its canonical Go form, `1h30m0s`.
|
||||
// - nil pointer fields are omitted.
|
||||
//
|
||||
// Marshal cannot encode cyclic data structures; passing one will loop until
|
||||
// the stack overflows. The output is not guaranteed to be byte-identical to
|
||||
// Marshal rejects a value that nests deeper than 10000 levels with an error
|
||||
// naming the limit, so cyclic data is reported instead of running the stack
|
||||
// out. The output is not guaranteed to be byte-identical to
|
||||
// the input that produced v: comments, whitespace, key order (for maps),
|
||||
// string quoting style, and the choice between `[table]` headers and inline
|
||||
// tables are not preserved.
|
||||
|
||||
Reference in New Issue
Block a user