feat: bound the encoder walk and add UnmarshalWithOptions
Test / test (push) Successful in 1m34s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-22 00:21:46 +02:00
parent eaa69dc6f6
commit 10391a090f
5 changed files with 219 additions and 18 deletions
+63 -14
View File
@@ -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)
}