fix(decode): keep the targeted parse on the tree path's contract

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-22 21:15:00 +02:00
parent f7e427ae3f
commit b45f4d65da
2 changed files with 719 additions and 191 deletions
+369 -191
View File
@@ -22,6 +22,14 @@ import (
// document through the tree path, so the observable behaviour is the tree // document through the tree path, so the observable behaviour is the tree
// path's, exactly. A targeted parse either completes with the result the // path's, exactly. A targeted parse either completes with the result the
// tree path would give, or it erases itself. // tree path would give, or it erases itself.
//
// One difference the two paths cannot share: a parse error or a cancellation
// deep in the document leaves the statements before it already written into
// the destination, where the tree path, which parses the whole document
// before it decodes any of it, writes nothing. The value layer shares this
// with encoding/json, whose Unmarshal also leaves a partial destination
// behind a mid-document failure; a destination that must stay untouched on
// error is a destination the caller resets.
var errTargetFallback = errors.New("interpres: targeted decode falls back to the tree path") var errTargetFallback = errors.New("interpres: targeted decode falls back to the tree path")
// targetCache holds whether a destination type may take the targeted parse. // targetCache holds whether a destination type may take the targeted parse.
@@ -33,10 +41,12 @@ var targetCache sync.Map // reflect.Type -> bool
var mapStringAnyType = reflect.TypeFor[map[string]any]() var mapStringAnyType = reflect.TypeFor[map[string]any]()
// typeTargetable reports whether decoding into the struct type t can use the // typeTargetable reports whether decoding into the struct type t can use the
// targeted parse. The one structural ban is untagged embedded maps: their // targeted parse. The structural bans are the shapes whose tree behaviour
// filler-key rule lives in the tree decode, and a targeted document that // the skeleton cannot model: untagged embedded maps, an OrderedMap anywhere a
// meets an unknown table would need a subtree of it. Everything else is safe // table opens, and a custom decode hook on any table the parse would enter
// to attempt, because the value layer is the ordinary decode and every // directly (a struct field, a map field, or the element of a table slice),
// because the tree hands a hook the whole parsed value. Everything else is
// safe to attempt, because the value layer is the ordinary decode and every
// mismatch falls back. // mismatch falls back.
func typeTargetable(t reflect.Type) bool { func typeTargetable(t reflect.Type) bool {
if t == nil || t.Kind() != reflect.Struct || t == orderedMapType { if t == nil || t.Kind() != reflect.Struct || t == orderedMapType {
@@ -59,22 +69,41 @@ func scanTargetable(t reflect.Type, seen map[reflect.Type]bool) bool {
return false return false
} }
for _, loc := range cachedStructSchema(t).byName { for _, loc := range cachedStructSchema(t).byName {
ft := derefType(t.FieldByIndex(loc.index).Type) if !scanTargetableField(derefType(t.FieldByIndex(loc.index).Type), seen) {
return false
}
}
return true
}
// scanTargetableField reports whether one field's type is safe for the
// targeted skeleton to fill directly.
func scanTargetableField(ft reflect.Type, seen map[reflect.Type]bool) bool {
switch ft.Kind() {
case reflect.Struct:
if ft == orderedMapType { if ft == orderedMapType {
return false return false
} }
if ft.Kind() != reflect.Struct || isScalarStruct(ft) { if isScalarStruct(ft) {
continue return true
} }
// A struct field with a custom decode hook receives the whole parsed // A struct the parse enters directly never builds the whole value
// value from the tree decode; the targeted skeleton never builds that // the tree hands a hook, so the hook must win.
// value for a table it enters directly, so the hook must win.
if implementsDecodeHook(ft) || implementsDecodeHook(reflect.PointerTo(ft)) { if implementsDecodeHook(ft) || implementsDecodeHook(reflect.PointerTo(ft)) {
return false return false
} }
if !scanTargetable(ft, seen) { return scanTargetable(ft, seen)
case reflect.Map:
return !implementsDecodeHook(ft) && !implementsDecodeHook(reflect.PointerTo(ft))
case reflect.Slice, reflect.Array:
et := derefType(ft.Elem())
if et == orderedMapType {
return false return false
} }
if et.Kind() == reflect.Struct && !isScalarStruct(et) {
return scanTargetableField(et, seen)
}
return true
} }
return true return true
} }
@@ -110,20 +139,45 @@ func canTargetDecode(v any) bool {
// targetTable is one open table of the targeted parse: the struct (or map) // targetTable is one open table of the targeted parse: the struct (or map)
// value its keys fill, the schema that resolves them (nil for a map or sink // value its keys fill, the schema that resolves them (nil for a map or sink
// destination), the absolute path its errors wrap, and whether it collects // destination), and the absolute path its errors wrap. A sink is the
// the keys no field claims for the strict check. A sink is the destination // destination an unknown subtree gets: its statements parse for the syntax
// an unknown subtree gets: its statements parse for the syntax and definition // and definition contracts, and its values are discarded. The strict and
// contracts, and its values are discarded. // required findings live in the parser's per-address store, not here,
// because the table objects a dotted descent builds are transient while the
// destination is not.
type targetTable struct { type targetTable struct {
rv reflect.Value rv reflect.Value
schema *structSchema schema *structSchema
path []string path []string
sink bool sink bool
keys []string // the keys defined in the table, interned; struct // strict is the strict-decode setting the table was opened with, carried
// tables keep theirs per destination address instead // into the per-address strict state on first sight.
strict bool strict bool
unknown string // strict: the smallest unclaimed key so far // arrayElem marks a sink created as the element of an unknown array of
resolvedSeen map[string]bool // the schema keys resolved so far, for required // tables: a dotted key may not enter it, the tree's own rule for an
// array, while a [sub-table] header may, through the last element.
arrayElem bool
keys []string // the keys defined in a sink, as full path keys
}
// strictState is the strict and required bookkeeping of one struct
// destination, keyed by the value's address.
type strictState struct {
path []string
typ reflect.Type
schema *structSchema
strict bool
unknown string // strict: the smallest unclaimed key so far
resolved map[string]bool
}
// arrayFill tracks how many elements of one fixed-size array the document
// has filled, with what the length mismatch the tree decode reports needs:
// the array's type and its field's path.
type arrayFill struct {
next int
typ reflect.Type
path []string
} }
// targetParser parses a document straight into a struct destination. It // targetParser parses a document straight into a struct destination. It
@@ -139,61 +193,53 @@ type targetParser struct {
rootT *targetTable rootT *targetTable
cur *targetTable cur *targetTable
// arrayNext tracks how many elements of a fixed-size array the document // arrayFills counts the elements of each fixed-size array the document
// has filled, per field address. // has filled, per field address, in the order the arrays were met.
arrayNext map[uintptr]int arrayFills map[uintptr]*arrayFill
fillOrder []*arrayFill
// appendedHere records the slice fields this document's [[headers]] have
// filled: a slice the caller prefilled is replaced by the tree decode,
// not appended to, so the first header over one falls back.
appendedHere map[uintptr]bool
// opened registers every opened table by its path key, sinks included: a // opened registers every opened table by its path key, sinks included: a
// later header or dotted key meets the table the tree already built. // later header or dotted key meets the table the tree already built.
opened map[string]*targetTable opened map[string]*targetTable
// keyAssigned records the fields a key statement assigned directly, per
// table address: an array-of-tables header over such a field is the
// tree's `not an array of tables` error, where a header over a
// header-built array appends.
keyAssignedKeys map[uintptr]map[string]bool
// tableKeysByAddr holds the defined keys of one struct destination, keyed // tableKeysByAddr holds the defined keys of one struct destination, keyed
// by the value's address: the table object a dotted descent builds is // by the value's address: the table object a dotted descent builds is
// transient, the destination is not. // transient, the destination is not.
tableKeysByAddr map[uintptr][]string tableKeysByAddr map[uintptr][]string
}
// markKeyAssigned records that a key statement assigned the field, and // mapKeysByAddr holds the keys the document has defined in one map
// keyAssigned reports that state. Sinks keep no such bookkeeping. // destination, keyed the same way: the destination map the caller
func (tp *targetParser) markKeyAssigned(t *targetTable, key string) { // prefilled is not the parser's state, and a key it holds is not the
if t.sink || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct { // duplicate a key the document repeats is.
return mapKeysByAddr map[uintptr]map[string]bool
}
addr := t.rv.Addr().Pointer()
if tp.keyAssignedKeys == nil {
tp.keyAssignedKeys = make(map[uintptr]map[string]bool, 8)
}
if tp.keyAssignedKeys[addr] == nil {
tp.keyAssignedKeys[addr] = make(map[string]bool, 8)
}
tp.keyAssignedKeys[addr][key] = true
}
func (tp *targetParser) keyAssigned(t *targetTable, key string) bool { // strictByAddr holds each destination's strict and required findings,
if t.sink || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct { // with strictOrder keeping the document order they first appeared in.
return false strictByAddr map[uintptr]*strictState
} strictOrder []uintptr
addr := t.rv.Addr().Pointer()
return tp.keyAssignedKeys[addr][key]
} }
// tableHas reports whether key is already defined in the table. A struct // tableHas reports whether key is already defined in the table. A struct
// table's keys are interned parser strings, so the linear scan compares // table's keys are interned parser strings, so the linear scan compares
// against a handful of short keys, cheaper than hashing a per-table map. // against a handful of short keys, cheaper than hashing a per-table map.
// Struct tables keep their keys by destination address, because the table // Struct tables keep their keys by destination address, and map tables keep
// object a dotted descent builds is transient while the destination is not. // theirs there too, because the table object a dotted descent builds is
// transient while the destination is not, and the destination map's own
// contents are the caller's, not the document's.
func (tp *targetParser) tableHas(t *targetTable, key string) bool { func (tp *targetParser) tableHas(t *targetTable, key string) bool {
switch { switch {
case t.sink: case t.sink:
return slices.Contains(t.keys, key) return slices.Contains(t.keys, key)
case t.schema == nil: case t.schema == nil:
return t.rv.Kind() == reflect.Map && t.rv.MapIndex(reflect.ValueOf(key)).IsValid() if t.rv.Kind() != reflect.Map || !t.rv.CanAddr() {
return false
}
return tp.mapKeysByAddr[t.rv.Addr().Pointer()][key]
default: default:
return slices.Contains(tp.tableKeys(t), key) return slices.Contains(tp.tableKeys(t), key)
} }
@@ -217,6 +263,17 @@ func (tp *targetParser) tableMark(t *targetTable, key string) {
case t.sink: case t.sink:
t.keys = append(t.keys, key) t.keys = append(t.keys, key)
case t.schema == nil: case t.schema == nil:
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Map {
return
}
addr := t.rv.Addr().Pointer()
if tp.mapKeysByAddr == nil {
tp.mapKeysByAddr = make(map[uintptr]map[string]bool, 8)
}
if tp.mapKeysByAddr[addr] == nil {
tp.mapKeysByAddr[addr] = make(map[string]bool, 8)
}
tp.mapKeysByAddr[addr][key] = true
default: default:
if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct { if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
return return
@@ -229,32 +286,49 @@ func (tp *targetParser) tableMark(t *targetTable, key string) {
} }
} }
// resolvedHas reports whether the resolved schema key has been seen, the // strictState returns the strict and required bookkeeping of the struct
// check a required tag runs: the duplicate bookkeeping tracks the key as the // destination t fills, registering it on first sight so a finding recorded
// document wrote it, the required bookkeeping the key as the schema // on a transient table survives the table.
// resolved it. func (tp *targetParser) strictState(t *targetTable) *strictState {
func (t *targetTable) resolvedHas(key string) bool { addr := t.rv.Addr().Pointer()
return t.resolvedSeen[key] if st, ok := tp.strictByAddr[addr]; ok {
return st
}
st := &strictState{
path: slices.Clone(t.path),
typ: t.rv.Type(),
schema: t.schema,
strict: t.strict,
resolved: make(map[string]bool, 8),
}
tp.strictByAddr[addr] = st
tp.strictOrder = append(tp.strictOrder, addr)
return st
} }
func (t *targetTable) markResolved(key string) { // markResolved records that the document resolved the schema key, the check
// a required tag runs: the duplicate bookkeeping tracks the key as the
// document wrote it, the required bookkeeping the key as the schema resolved
// it.
func (tp *targetParser) markResolved(t *targetTable, key string) {
if t.schema == nil || len(t.schema.required) == 0 { if t.schema == nil || len(t.schema.required) == 0 {
return return
} }
if t.resolvedSeen == nil { if !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
t.resolvedSeen = make(map[string]bool, 8) return
} }
t.resolvedSeen[key] = true tp.strictState(t).resolved[key] = true
} }
// recordStrictUnknown remembers the key no field claims when strict decoding // recordStrictUnknown remembers the key no field claims when strict decoding
// is on: the smallest one is reported, the tree decode's own choice. // is on: the smallest one is reported, the tree decode's own choice.
func (t *targetTable) recordStrictUnknown(key string) { func (tp *targetParser) recordStrictUnknown(t *targetTable, key string) {
if !t.strict { if !t.strict || t.schema == nil || !t.rv.CanAddr() || t.rv.Kind() != reflect.Struct {
return return
} }
if t.unknown == "" || key < t.unknown { st := tp.strictState(t)
t.unknown = key if st.unknown == "" || key < st.unknown {
st.unknown = key
} }
} }
@@ -274,12 +348,13 @@ func parseIntoTargeted(ctx context.Context, data []byte, d *decoder, useNumber b
} }
rv := reflect.ValueOf(v) rv := reflect.ValueOf(v)
tp := &targetParser{ tp := &targetParser{
parser: &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, useNumber: useNumber}, parser: &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, useNumber: useNumber},
d: d, d: d,
root: rv.Elem(), root: rv.Elem(),
arrayNext: make(map[uintptr]int, 4), arrayFills: make(map[uintptr]*arrayFill, 4),
keyAssignedKeys: make(map[uintptr]map[string]bool, 8), appendedHere: make(map[uintptr]bool, 4),
opened: make(map[string]*targetTable, 8), opened: make(map[string]*targetTable, 8),
strictByAddr: make(map[uintptr]*strictState, 8),
} }
tp.rootT = &targetTable{rv: tp.root, schema: schemaRef(tp.root.Type()), strict: d.disallowUnknown} tp.rootT = &targetTable{rv: tp.root, schema: schemaRef(tp.root.Type()), strict: d.disallowUnknown}
tp.tables = append(tp.tables, tp.rootT) tp.tables = append(tp.tables, tp.rootT)
@@ -326,35 +401,57 @@ func (tp *targetParser) run() error {
} }
// reportDeferred raises the decode-stage findings in the tree decode's // reportDeferred raises the decode-stage findings in the tree decode's
// order: the root table first, then the opened tables in document order. The // order. A fixed-size array the document under-filled is the length mismatch
// tree decode reports them after a full parse, so a later parse error always // the tree decode raises, and it comes first. Then the unknown keys, before
// won; here the parse has already completed. // the required ones, because the tree decode meets an unknown key while it
// assigns and checks a table's required keys only once the whole table has
// been; within each class the order is the order the destinations first
// appeared in, the document's own order.
func (tp *targetParser) reportDeferred() error { func (tp *targetParser) reportDeferred() error {
for _, t := range append([]*targetTable{tp.rootT}, tp.tables...) { for _, f := range tp.fillOrder {
if t.sink { if f.next != f.typ.Len() {
return wrapTablePath(f.path, fmt.Errorf("interpres: cannot assign %d elements to %s", f.next, f.typ))
}
}
for _, addr := range tp.strictOrder {
if st := tp.strictByAddr[addr]; st.strict && st.unknown != "" {
return wrapTablePath(st.path, fmt.Errorf("interpres: unknown field %q for %s", st.unknown, st.typ))
}
}
for _, addr := range tp.strictOrder {
st := tp.strictByAddr[addr]
if st.schema == nil {
continue continue
} }
if t.strict && t.unknown != "" { for _, key := range st.schema.required {
return tp.wrapTableErr(t, fmt.Errorf("interpres: unknown field %q for %s", t.unknown, t.rv.Type())) if !st.resolved[key] {
} return wrapTablePath(st.path, fmt.Errorf("interpres: missing required key %q", key))
if t.schema != nil {
for _, key := range t.schema.required {
if !t.resolvedHas(key) {
return tp.wrapTableErr(t, fmt.Errorf("interpres: missing required key %q", key))
}
} }
} }
} }
return nil return nil
} }
// wrapTableErr wraps a table's finding the way the tree decode wraps it: the // wrapTablePath wraps a table's finding the way the tree decode wraps it: the
// root speaks for itself, a nested table gains its path. // root speaks for itself, a nested table gains its path.
func (tp *targetParser) wrapTableErr(t *targetTable, err error) error { func wrapTablePath(path []string, err error) error {
if len(t.path) == 0 { if len(path) == 0 {
return err return err
} }
return &DecodeError{Path: Path(slices.Clone(t.path)), Err: err} return &DecodeError{Path: Path(slices.Clone(path)), Err: err}
}
// arrayFillFor returns the fill record of the fixed-size array fv, keyed by
// its address, created with its field's path on first sight.
func (tp *targetParser) arrayFillFor(fv reflect.Value, path []string) *arrayFill {
addr := fv.Addr().Pointer()
if f, ok := tp.arrayFills[addr]; ok {
return f
}
f := &arrayFill{typ: fv.Type(), path: path}
tp.arrayFills[addr] = f
tp.fillOrder = append(tp.fillOrder, f)
return f
} }
// --- headers --------------------------------------------------------------- // --- headers ---------------------------------------------------------------
@@ -474,11 +571,11 @@ func (tp *targetParser) descendOne(parent *targetTable, key string, abs []string
return nil, errTargetFallback return nil, errTargetFallback
} }
if existing := parent.rv.MapIndex(reflect.ValueOf(key)); existing.IsValid() && !existing.IsNil() { if existing := parent.rv.MapIndex(reflect.ValueOf(key)); existing.IsValid() && !existing.IsNil() {
return &targetTable{rv: existing.Elem(), path: abs}, nil return &targetTable{rv: existing, path: slices.Clone(abs)}, nil
} }
next := reflect.MakeMap(elemT) next := reflect.MakeMap(elemT)
parent.rv.SetMapIndex(reflect.ValueOf(key), next) parent.rv.SetMapIndex(reflect.ValueOf(key), next)
return &targetTable{rv: next, path: abs}, nil return &targetTable{rv: next, path: slices.Clone(abs)}, nil
} }
resolved := key resolved := key
loc, ok := parent.schema.byName[key] loc, ok := parent.schema.byName[key]
@@ -487,18 +584,18 @@ func (tp *targetParser) descendOne(parent *targetTable, key string, abs []string
loc, ok = parent.schema.byName[resolved] loc, ok = parent.schema.byName[resolved]
} }
if !ok { if !ok {
parent.recordStrictUnknown(key) tp.recordStrictUnknown(parent, key)
if opened, ok := tp.opened[pathKey(abs)]; ok { if opened, ok := tp.opened[pathKey(abs)]; ok {
return opened, nil return opened, nil
} }
if tp.tableHas(parent, key) { if tp.tableHas(parent, key) {
return nil, tp.errf("key %q is not a table", key) return nil, tp.errf("key %q is not a table", key)
} }
sink := &targetTable{sink: true, path: abs} sink := &targetTable{sink: true, path: slices.Clone(abs)}
tp.opened[pathKey(abs)] = sink tp.opened[pathKey(abs)] = sink
return sink, nil return sink, nil
} }
parent.markResolved(resolved) tp.markResolved(parent, resolved)
fv, err := fieldByIndex(parent.rv, loc.index) fv, err := fieldByIndex(parent.rv, loc.index)
if err != nil { if err != nil {
return nil, errTargetFallback return nil, errTargetFallback
@@ -509,10 +606,12 @@ func (tp *targetParser) descendOne(parent *targetTable, key string, abs []string
// openValueTable opens a table scope over a placed field value, allocating a // openValueTable opens a table scope over a placed field value, allocating a
// nil pointer on the way. The rules mirror the tree decode's own type // nil pointer on the way. The rules mirror the tree decode's own type
// decisions: a struct enters, a map enters (allocated when nil), an array of // decisions: a struct enters, a map enters (allocated when nil), an array of
// tables enters its last element, and anything else is a type mismatch the // tables enters its last filled element, a slice enters the last element of
// tree decode reports, so it falls back. A scalar field the table keys // an array this document's [[headers]] built (a prefilled slice is a table
// already define is the tree's `key is not a table` error, checked against // the tree decode rejects, so it falls back), and anything else is a type
// parent, the table the key belongs to. // mismatch the tree decode reports, so it falls back too. A scalar field the
// table keys already define is the tree's `key is not a table` error,
// checked against parent, the table the key belongs to.
func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, key string, abs []string, strict bool) (*targetTable, error) { func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, key string, abs []string, strict bool) (*targetTable, error) {
if fv.Kind() == reflect.Pointer { if fv.Kind() == reflect.Pointer {
if fv.IsNil() { if fv.IsNil() {
@@ -528,7 +627,7 @@ func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, ke
if isScalarStruct(fv.Type()) { if isScalarStruct(fv.Type()) {
return nil, errTargetFallback return nil, errTargetFallback
} }
return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: abs, strict: strict}, nil return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: slices.Clone(abs), strict: strict}, nil
case reflect.Map: case reflect.Map:
if fv.Type().Key().Kind() != reflect.String { if fv.Type().Key().Kind() != reflect.String {
return nil, errTargetFallback return nil, errTargetFallback
@@ -536,11 +635,12 @@ func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, ke
if fv.IsNil() { if fv.IsNil() {
fv.Set(reflect.MakeMap(fv.Type())) fv.Set(reflect.MakeMap(fv.Type()))
} }
return &targetTable{rv: fv, path: abs}, nil return &targetTable{rv: fv, path: slices.Clone(abs)}, nil
case reflect.Slice: case reflect.Slice:
if fv.Len() == 0 { if !tp.appendedHere[fv.Addr().Pointer()] {
// No [[header]] ever filled it, so the tree holds a map here and // No [[header]] of this document filled it, so the tree holds a
// its decode raises the type mismatch. // map here, whose decode raises the type mismatch; a prefilled
// slice is the tree's replacement case, not a table to enter.
return nil, errTargetFallback return nil, errTargetFallback
} }
et := derefType(fv.Type().Elem()) et := derefType(fv.Type().Elem())
@@ -548,6 +648,22 @@ func (tp *targetParser) openValueTable(fv reflect.Value, parent *targetTable, ke
return nil, errTargetFallback return nil, errTargetFallback
} }
return &targetTable{rv: fv.Index(fv.Len() - 1), schema: schemaRef(et), path: elementPath(abs, key, fv.Len()-1), strict: strict}, nil return &targetTable{rv: fv.Index(fv.Len() - 1), schema: schemaRef(et), path: elementPath(abs, key, fv.Len()-1), strict: strict}, nil
case reflect.Array:
fill := tp.arrayFillFor(fv, append(slices.Clone(parent.path), key))
if fill.next == 0 {
return nil, errTargetFallback
}
et := derefType(fv.Type().Elem())
if et.Kind() == reflect.Struct {
if isScalarStruct(et) {
return nil, errTargetFallback
}
return &targetTable{rv: fv.Index(fill.next - 1), schema: schemaRef(et), path: elementPath(abs, key, fill.next-1), strict: strict}, nil
}
if et.Kind() == reflect.Map && et.Key().Kind() == reflect.String {
return &targetTable{rv: fv.Index(fill.next - 1), path: elementPath(abs, key, fill.next-1)}, nil
}
return nil, errTargetFallback
} }
if tp.tableHas(parent, key) { if tp.tableHas(parent, key) {
return nil, tp.errf("key %q is not a table", key) return nil, tp.errf("key %q is not a table", key)
@@ -578,12 +694,30 @@ func (tp *targetParser) appendArrayTable(key []string) error {
if tp.parser.dotted[pk] || tp.parser.headers[pk] { if tp.parser.dotted[pk] || tp.parser.headers[pk] {
return tp.errf("key %q is not an array of tables", leaf) return tp.errf("key %q is not an array of tables", leaf)
} }
// A leaf the document already defined as a value or a table is the tree
// parser's own parse error, and a leaf an earlier [[header]] defined
// opens a new element; the arrays map, read before this header marks it,
// is what tells the two apart. A sink parent holds no destination state
// worth consulting.
if !parent.sink && !tp.parser.arrays[pk] && tp.tableHas(parent, leaf) {
return tp.errf("key %q is not an array of tables", leaf)
}
// A new element starts a fresh scope, exactly as the tree parser's own
// header does: sub-headers, inline freezes and nested arrays from the
// previous element no longer apply.
tp.parser.resetScopeUnder(key)
tp.parser.markArray(pk) tp.parser.markArray(pk)
elem, err := tp.appendElement(parent, leaf, key) elem, err := tp.appendElement(parent, leaf, key)
if err != nil { if err != nil {
return err return err
} }
if elem.sink {
// A sink the element scope reuses ([[a.b]] over an unknown a, the
// parent sink) starts the new element with no keys, the way a known
// array's element does.
elem.keys = nil
}
tp.tableMark(parent, leaf) tp.tableMark(parent, leaf)
if !elem.sink { if !elem.sink {
tp.tables = append(tp.tables, elem) tp.tables = append(tp.tables, elem)
@@ -593,9 +727,9 @@ func (tp *targetParser) appendArrayTable(key []string) error {
} }
// appendElement appends one element to the array the leaf names in parent // appendElement appends one element to the array the leaf names in parent
// and returns its table. A leaf no field claims sinks; a field whose array // and returns its table. A leaf no field claims sinks, a fresh namespace per
// element kind cannot be a table falls back, the tree decode owning the type // element; a field whose array element kind cannot be a table falls back,
// error. // the tree decode owning the type error.
func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []string) (*targetTable, error) { func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []string) (*targetTable, error) {
if parent.sink { if parent.sink {
return parent, nil return parent, nil
@@ -608,10 +742,15 @@ func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []st
gk := reflect.ValueOf(leaf) gk := reflect.ValueOf(leaf)
var arr reflect.Value var arr reflect.Value
if existing := parent.rv.MapIndex(gk); existing.IsValid() && !existing.IsNil() { if existing := parent.rv.MapIndex(gk); existing.IsValid() && !existing.IsNil() {
if existing.Elem().Kind() != reflect.Slice { // A map[string]any destination boxes its arrays in the
// interface; a typed map hands the slice itself.
if existing.Kind() == reflect.Interface {
existing = existing.Elem()
}
if existing.Kind() != reflect.Slice {
return nil, tp.errf("key %q is not an array of tables", leaf) return nil, tp.errf("key %q is not an array of tables", leaf)
} }
arr = existing.Elem() arr = existing
} }
var elem reflect.Value var elem reflect.Value
switch { switch {
@@ -645,62 +784,64 @@ func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []st
loc, ok = parent.schema.byName[resolved] loc, ok = parent.schema.byName[resolved]
} }
if !ok { if !ok {
parent.recordStrictUnknown(leaf) tp.recordStrictUnknown(parent, leaf)
if opened, ok := tp.opened[pathKey(parent.path)]; ok { // Every element is a fresh namespace, the way a known array's is,
return opened, nil // registered under the header's path so a [sub-table] header reaches
} // the last element, the tree's rule for a header under an array of
if tp.tableHas(parent, leaf) { // tables; a dotted key skips it, the tree's rule for an array.
return nil, tp.errf("key %q is not an array of tables", leaf) sink := &targetTable{sink: true, arrayElem: true, path: slices.Clone(key)}
} tp.opened[pathKey(key)] = sink
sink := &targetTable{sink: true, path: parent.path}
tp.opened[pathKey(parent.path)] = sink
return sink, nil return sink, nil
} }
parent.markResolved(resolved) tp.markResolved(parent, resolved)
if tp.keyAssigned(parent, resolved) {
// A key statement already assigned the field its own value; the tree
// holds a value array there and its header append is the
// `not an array of tables` parse error.
return nil, tp.errf("key %q is not an array of tables", leaf)
}
fv, err := fieldByIndex(parent.rv, loc.index) fv, err := fieldByIndex(parent.rv, loc.index)
if err != nil { if err != nil {
return nil, errTargetFallback return nil, errTargetFallback
} }
fieldPath := append(slices.Clone(parent.path), leaf)
if fv.Kind() == reflect.Array { if fv.Kind() == reflect.Array {
// A fixed-size array fills position by position; one element too many // A fixed-size array fills position by position; one element too many
// is the length mismatch the tree decode reports. // is the length mismatch the tree decode reports, and one too few is
// the same mismatch, checked when the parse completes.
fill := tp.arrayFillFor(fv, fieldPath)
et := derefType(fv.Type().Elem()) et := derefType(fv.Type().Elem())
switch et.Kind() { switch {
case reflect.Struct: case et.Kind() == reflect.Struct && !isScalarStruct(et):
if isScalarStruct(et) { if fill.next >= fv.Len() {
return nil, errTargetFallback return nil, errTargetFallback
} }
addr := fv.Addr().Pointer() n := fill.next
n := tp.arrayNext[addr] fill.next = n + 1
if n >= fv.Len() { return &targetTable{rv: fv.Index(n), schema: schemaRef(et), path: elementPath(parent.path, leaf, n), strict: parent.strict}, nil
case et.Kind() == reflect.Map && et.Key().Kind() == reflect.String:
if fill.next >= fv.Len() {
return nil, errTargetFallback return nil, errTargetFallback
} }
tp.arrayNext[addr] = n + 1 n := fill.next
return &targetTable{rv: fv.Index(n), schema: schemaRef(et), path: parent.path, strict: parent.strict}, nil fill.next = n + 1
case reflect.Map:
if et.Key().Kind() != reflect.String {
return nil, errTargetFallback
}
addr := fv.Addr().Pointer()
n := tp.arrayNext[addr]
if n >= fv.Len() {
return nil, errTargetFallback
}
tp.arrayNext[addr] = n + 1
elem := reflect.MakeMap(et) elem := reflect.MakeMap(et)
fv.Index(n).Set(elem) fv.Index(n).Set(elem)
return &targetTable{rv: elem, path: parent.path}, nil return &targetTable{rv: elem, path: elementPath(parent.path, leaf, n)}, nil
} }
return nil, errTargetFallback return nil, errTargetFallback
} }
if fv.Kind() != reflect.Slice { if fv.Kind() != reflect.Slice {
return nil, tp.errf("key %q is not an array of tables", leaf) if tp.tableHas(parent, leaf) {
return nil, tp.errf("key %q is not an array of tables", leaf)
}
// The tree builds an array here without asking the destination, and
// its decode answers with the type mismatch; the fallback keeps the
// message the tree path gives.
return nil, errTargetFallback
}
addr := fv.Addr().Pointer()
if !tp.appendedHere[addr] {
if fv.Len() > 0 {
// A slice the caller prefilled is replaced by the tree decode,
// not appended to; the fallback runs the document the tree's way.
return nil, errTargetFallback
}
tp.appendedHere[addr] = true
} }
et := derefType(fv.Type().Elem()) et := derefType(fv.Type().Elem())
switch et.Kind() { switch et.Kind() {
@@ -708,16 +849,38 @@ func (tp *targetParser) appendElement(parent *targetTable, leaf string, key []st
if isScalarStruct(et) { if isScalarStruct(et) {
return nil, errTargetFallback return nil, errTargetFallback
} }
grown := reflect.Append(fv, reflect.New(et).Elem()) // A pointer element is appended as the allocated pointer and filled
// through its pointee, so []*T takes the same path []T does. The
// element the table fills is the slice's own: the value a New built
// stands apart from the backing array.
var el, appended reflect.Value
if fv.Type().Elem().Kind() == reflect.Pointer {
p := reflect.New(et)
el, appended = p.Elem(), p
} else {
el = reflect.New(et).Elem()
appended = el
}
grown := reflect.Append(fv, appended)
fv.Set(grown) fv.Set(grown)
return &targetTable{rv: grown.Index(grown.Len() - 1), schema: schemaRef(et), path: elementPath(parent.path, leaf, grown.Len()-1), strict: parent.strict}, nil if fv.Type().Elem().Kind() != reflect.Pointer {
el = grown.Index(grown.Len() - 1)
}
return &targetTable{rv: el, schema: schemaRef(et), path: elementPath(parent.path, leaf, grown.Len()-1), strict: parent.strict}, nil
case reflect.Map: case reflect.Map:
if et.Key().Kind() != reflect.String { if et.Key().Kind() != reflect.String {
return nil, errTargetFallback return nil, errTargetFallback
} }
grown := reflect.Append(fv, reflect.MakeMap(et)) m := reflect.MakeMap(et)
var appended reflect.Value = m
if fv.Type().Elem().Kind() == reflect.Pointer {
p := reflect.New(et)
p.Elem().Set(m)
appended = p
}
grown := reflect.Append(fv, appended)
fv.Set(grown) fv.Set(grown)
return &targetTable{rv: grown.Index(grown.Len() - 1), path: elementPath(parent.path, leaf, grown.Len()-1)}, nil return &targetTable{rv: m, path: elementPath(parent.path, leaf, grown.Len()-1)}, nil
} }
return nil, errTargetFallback return nil, errTargetFallback
} }
@@ -780,22 +943,18 @@ func (tp *targetParser) parseKeyStatement() error {
if verr != nil { if verr != nil {
return verr return verr
} }
dupKey := first full := append([]string{first}, rest...)
if len(dest.path) > 0 || len(rest) > 0 { if len(dest.path) > 0 {
full := make([]string, 0, len(dest.path)+len(rest)+1) full = append(slices.Clone(dest.path), full...)
full = append(full, dest.path...)
full = append(full, first)
full = append(full, rest...)
dupKey = pathKey(full)
} }
if tp.tableHas(leafTable, dupKey) { if tp.tableHas(leafTable, pathKey(full)) {
return p.errf("duplicate key %q", leaf) return p.errf("duplicate key %q", leaf)
} }
tp.tableMark(leafTable, dupKey) tp.tableMark(leafTable, pathKey(full))
if m, isMap := val.(map[string]any); isMap { if m, isMap := val.(map[string]any); isMap {
full := make([]string, 0, len(dest.path)+len(leaf)+1) // An inline table freezes the whole path the statement wrote,
full = append(full, dest.path...) // intermediate segments included, so no later header or dotted
full = append(full, leaf) // key can extend it at any depth.
p.freezeInline(full, m) p.freezeInline(full, m)
} }
return nil return nil
@@ -804,27 +963,29 @@ func (tp *targetParser) parseKeyStatement() error {
return p.errf("duplicate key %q", leaf) return p.errf("duplicate key %q", leaf)
} }
tp.tableMark(leafTable, leaf) tp.tableMark(leafTable, leaf)
if dst.Kind() == reflect.Slice || dst.Kind() == reflect.Array {
// Only a field a later [[header]] could append to needs the
// key-assigned record; everything else never checks it.
et := derefType(dst.Type().Elem())
if et.Kind() == reflect.Struct && !isScalarStruct(et) || et.Kind() == reflect.Map {
tp.markKeyAssigned(leafTable, leaf)
}
}
val, err := tp.parseValueInto(dst) val, err := tp.parseValueInto(dst)
if err != nil { if err != nil {
if errors.Is(err, errTargetFallback) {
return err
}
if _, isSyntax := errors.AsType[*SyntaxError](err); !isSyntax {
// A hook's own failure, which the fallback must not rerun: it
// returns wrapped the way the tree decode wraps a field's.
return newDecodeError(leaf, err)
}
return err return err
} }
if mapDst.IsValid() { if mapDst.IsValid() {
mapDst.SetMapIndex(reflect.ValueOf(leaf), dst) mapDst.SetMapIndex(reflect.ValueOf(leaf), dst)
} }
if m, isMap := val.(map[string]any); isMap { if m, isMap := val.(map[string]any); isMap {
// An inline table freezes its paths; the abs slice is built for it // An inline table freezes the whole path the statement wrote,
// alone, after the parse proved one is needed. // intermediate segments included; the slice is built for it alone,
abs := make([]string, 0, len(dest.path)+len(leaf)+1) // after the parse proved one is needed.
abs := make([]string, 0, len(dest.path)+len(rest)+1)
abs = append(abs, dest.path...) abs = append(abs, dest.path...)
abs = append(abs, leaf) abs = append(abs, first)
abs = append(abs, rest...)
p.freezeInline(abs, m) p.freezeInline(abs, m)
} }
return nil return nil
@@ -847,7 +1008,10 @@ func (tp *targetParser) parseValueInto(dst reflect.Value) (any, error) {
return nil, err return nil, err
} }
if err := tp.d.assign(v, dst); err != nil { if err := tp.d.assign(v, dst); err != nil {
return nil, errTargetFallback // The hook has run; falling back would run it a second time on
// tree path, so its error returns as the tree path's own, for
// the caller to wrap the way the tree decode wraps a field's.
return nil, err
} }
return v, nil return v, nil
} }
@@ -890,7 +1054,16 @@ func (tp *targetParser) parseValueInto(dst reflect.Value) (any, error) {
} }
} }
tok := tp.scanNumberToken() tok := tp.scanNumberToken()
if dtv, dtok := parseDateTime(tok); dtok { if hasHighByte(tok) && invalidUTF8Offset(tok) >= 0 {
// The token route the targeted parse takes validates UTF-8 the
// way the tree scanner does, on the byte that does not decode.
return nil, p.errf("invalid UTF-8 in value at byte offset %d", start+invalidUTF8Offset(tok))
}
dtv, isDT, dterr := parseDateTime(tok)
if dterr != nil {
return nil, p.errf("%s", dterr)
}
if isDT {
if err := tp.d.assign(dtv, dst); err != nil { if err := tp.d.assign(dtv, dst); err != nil {
return nil, errTargetFallback return nil, errTargetFallback
} }
@@ -1091,10 +1264,10 @@ func (tp *targetParser) descendDotted(dest *targetTable, first string, rest []st
loc, found = tbl.schema.byName[resolved] loc, found = tbl.schema.byName[resolved]
} }
if !found { if !found {
tbl.recordStrictUnknown(leaf) tp.recordStrictUnknown(tbl, leaf)
return reflect.Value{}, reflect.Value{}, leafTable, false, nil return reflect.Value{}, reflect.Value{}, leafTable, false, nil
} }
tbl.markResolved(resolved) tp.markResolved(tbl, resolved)
fv, ferr := fieldByIndex(tbl.rv, loc.index) fv, ferr := fieldByIndex(tbl.rv, loc.index)
if ferr != nil { if ferr != nil {
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
@@ -1118,15 +1291,14 @@ func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []strin
return nil, errTargetFallback return nil, errTargetFallback
} }
if existing := tbl.rv.MapIndex(reflect.ValueOf(seg)); existing.IsValid() && !existing.IsNil() { if existing := tbl.rv.MapIndex(reflect.ValueOf(seg)); existing.IsValid() && !existing.IsNil() {
ev := existing.Elem() if existing.Kind() != reflect.Map {
if ev.Kind() != reflect.Map {
return nil, tp.errf("key %q is not a table", seg) return nil, tp.errf("key %q is not a table", seg)
} }
return &targetTable{rv: ev, path: segAbs}, nil return &targetTable{rv: existing, path: slices.Clone(segAbs)}, nil
} }
next := reflect.MakeMap(elemT) next := reflect.MakeMap(elemT)
tbl.rv.SetMapIndex(reflect.ValueOf(seg), next) tbl.rv.SetMapIndex(reflect.ValueOf(seg), next)
return &targetTable{rv: next, path: segAbs}, nil return &targetTable{rv: next, path: slices.Clone(segAbs)}, nil
} }
resolved := seg resolved := seg
loc, ok := tbl.schema.byName[seg] loc, ok := tbl.schema.byName[seg]
@@ -1135,17 +1307,21 @@ func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []strin
loc, ok = tbl.schema.byName[resolved] loc, ok = tbl.schema.byName[resolved]
} }
if !ok { if !ok {
tbl.recordStrictUnknown(seg) tp.recordStrictUnknown(tbl, seg)
if opened, ok := tp.opened[pathKey(segAbs)]; ok { // A sink an array element created is entered by a [sub-table]
// header, through the last element, but never by a dotted key: the
// tree's descendKey rejects an array where tableAt follows it.
if opened, ok := tp.opened[pathKey(segAbs)]; ok && !opened.arrayElem {
return opened, nil return opened, nil
} }
if tp.tableHas(tbl, seg) { if tp.tableHas(tbl, seg) {
return nil, tp.errf("key %q is not a table", seg) return nil, tp.errf("key %q is not a table", seg)
} }
sink := &targetTable{sink: true, path: segAbs} sink := &targetTable{sink: true, path: slices.Clone(segAbs)}
tp.opened[pathKey(segAbs)] = sink tp.opened[pathKey(segAbs)] = sink
return sink, nil return sink, nil
} }
tp.markResolved(tbl, resolved)
fv, ferr := fieldByIndex(tbl.rv, loc.index) fv, ferr := fieldByIndex(tbl.rv, loc.index)
if ferr != nil { if ferr != nil {
return nil, errTargetFallback return nil, errTargetFallback
@@ -1167,7 +1343,7 @@ func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []strin
} }
return nil, errTargetFallback return nil, errTargetFallback
} }
return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: segAbs, strict: tbl.strict}, nil return &targetTable{rv: fv, schema: schemaRef(fv.Type()), path: slices.Clone(segAbs), strict: tbl.strict}, nil
case reflect.Map: case reflect.Map:
if fv.Type().Key().Kind() != reflect.String { if fv.Type().Key().Kind() != reflect.String {
return nil, errTargetFallback return nil, errTargetFallback
@@ -1175,7 +1351,7 @@ func (tp *targetParser) dottedEnter(tbl *targetTable, seg string, segAbs []strin
if fv.IsNil() { if fv.IsNil() {
fv.Set(reflect.MakeMap(fv.Type())) fv.Set(reflect.MakeMap(fv.Type()))
} }
return &targetTable{rv: fv, path: segAbs}, nil return &targetTable{rv: fv, path: slices.Clone(segAbs)}, nil
} }
if tp.tableHas(tbl, seg) { if tp.tableHas(tbl, seg) {
return nil, tp.errf("key %q is not a table", seg) return nil, tp.errf("key %q is not a table", seg)
@@ -1193,7 +1369,9 @@ func (tp *targetParser) leafInTable(dest *targetTable, key string) (dst reflect.
if dest.rv.Kind() != reflect.Map { if dest.rv.Kind() != reflect.Map {
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
} }
if dest.rv.MapIndex(reflect.ValueOf(key)).IsValid() { if tp.tableHas(dest, key) {
// The duplicate check reads the keys the document defined, not
// the destination map's own contents, which are the caller's.
return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("duplicate key %q", key) return reflect.Value{}, reflect.Value{}, leafTable, false, tp.errf("duplicate key %q", key)
} }
elem := reflect.New(dest.rv.Type().Elem()).Elem() elem := reflect.New(dest.rv.Type().Elem()).Elem()
@@ -1206,10 +1384,10 @@ func (tp *targetParser) leafInTable(dest *targetTable, key string) (dst reflect.
loc, ok = dest.schema.byName[resolved] loc, ok = dest.schema.byName[resolved]
} }
if !ok { if !ok {
dest.recordStrictUnknown(key) tp.recordStrictUnknown(dest, key)
return reflect.Value{}, reflect.Value{}, leafTable, false, nil return reflect.Value{}, reflect.Value{}, leafTable, false, nil
} }
dest.markResolved(resolved) tp.markResolved(dest, resolved)
fv, ferr := fieldByIndex(dest.rv, loc.index) fv, ferr := fieldByIndex(dest.rv, loc.index)
if ferr != nil { if ferr != nil {
return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback return reflect.Value{}, reflect.Value{}, leafTable, false, errTargetFallback
+350
View File
@@ -4,9 +4,12 @@
package interpres package interpres
import ( import (
"errors"
"maps"
"net" "net"
"reflect" "reflect"
"strings" "strings"
"sync/atomic"
"testing" "testing"
"time" "time"
) )
@@ -522,3 +525,350 @@ func TestTargetedMapTableShapes(t *testing.T) {
}) })
} }
} }
// TestTargetedNestedMapDescents pins the descents into a map of maps that
// meet entries the document built earlier: a dotted key twice through the
// same sub-table, a header into a dotted-built sub-table, and a typed array
// under a map key. Each shape once panicked on a reflect Elem of a map.
func TestTargetedNestedMapDescents(t *testing.T) {
t.Run("dotted key through one sub-table twice", func(t *testing.T) {
var cfg struct {
M map[string]map[string]any `toml:"m"`
}
err := Unmarshal([]byte("m.a.b = 1\nm.a.c = 2\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.M["a"]["b"] != int64(1) || cfg.M["a"]["c"] != int64(2) {
t.Errorf("m = %#v", cfg.M)
}
})
t.Run("header under a dotted-built sub-table", func(t *testing.T) {
var cfg struct {
M map[string]map[string]any `toml:"m"`
}
err := Unmarshal([]byte("m.a.b = 1\n[m.a.deep]\nx = 2\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.M["a"]["b"] != int64(1) || cfg.M["a"]["deep"].(map[string]any)["x"] != int64(2) {
t.Errorf("m = %#v", cfg.M)
}
})
t.Run("typed array under a map key", func(t *testing.T) {
var cfg struct {
M map[string][]map[string]any `toml:"m"`
}
err := Unmarshal([]byte("[[m.arr]]\nx = 1\n\n[[m.arr]]\ny = 2\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(cfg.M["arr"]) != 2 || cfg.M["arr"][1]["y"] != int64(2) {
t.Errorf("m = %#v", cfg.M)
}
})
}
// TestTargetedPointerElementSlice pins that an array of tables over a slice
// of pointer elements fills the pointed-to structs.
func TestTargetedPointerElementSlice(t *testing.T) {
type item struct {
N int `toml:"n"`
}
var cfg struct {
Items []*item `toml:"items"`
}
err := Unmarshal([]byte("[[items]]\nn = 1\n\n[[items]]\nn = 2\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(cfg.Items) != 2 || cfg.Items[0] == nil || cfg.Items[1].N != 2 {
t.Errorf("items = %#v", cfg.Items)
}
}
// TestTargetedArrayScopeResets pins that a new element of an array of tables
// starts a fresh definition scope, the contract the changelog documents.
func TestTargetedArrayScopeResets(t *testing.T) {
doc := "[[a]]\nb.c = 1\n\n[[a]]\n\n[a.b]\nx = 1\n"
var ref, tgt targetCfg
refErr := treeDecodeInto([]byte(doc), &ref)
if refErr != nil {
t.Fatalf("tree decode: %v", refErr)
}
if err := Unmarshal([]byte(doc), &tgt); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(ref, tgt) {
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
}
}
// TestTargetedUnknownArrayElements pins that every element of an unknown
// array of tables is a fresh namespace, and a sub-table header reaches the
// last element the way the tree parser's does.
func TestTargetedUnknownArrayElements(t *testing.T) {
doc := "[[zz]]\nk = 1\n\n[[zz]]\nk = 2\n\n[zz.sub]\nx = 3\n"
var ref, tgt targetCfg
refErr := treeDecodeInto([]byte(doc), &ref)
tgtErr := Unmarshal([]byte(doc), &tgt)
if (refErr == nil) != (tgtErr == nil) {
t.Fatalf("error presence disagrees: tree %v, targeted %v", refErr, tgtErr)
}
if refErr != nil {
return
}
if !reflect.DeepEqual(ref, tgt) {
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
}
// A dotted key may not enter the array: the tree's own rule.
var dotted targetCfg
dErr := Unmarshal([]byte("[[zz]]\nk = 1\nzz.x = 2\n"), &dotted)
refDotted := treeDecodeInto([]byte("[[zz]]\nk = 1\nzz.x = 2\n"), &dotted)
if (dErr == nil) != (refDotted == nil) {
t.Errorf("dotted into an array: targeted %v, tree %v", dErr, refDotted)
}
}
// TestTargetedFixedArrayUnderFill pins that a fixed-size array the document
// under-fills is the length mismatch the tree decode raises, with the
// field's path.
func TestTargetedFixedArrayUnderFill(t *testing.T) {
type item struct {
N int `toml:"n"`
}
var cfg struct {
Items [2]item `toml:"items"`
}
err := Unmarshal([]byte("[[items]]\nn = 1\n"), &cfg)
if err == nil {
t.Fatal("unmarshal accepted an under-filled array")
}
want := `interpres: items: cannot assign 1 elements to [2]interpres.item`
if err.Error() != want {
t.Errorf("err = %v\nwant %q", err, want)
}
}
// TestTargetedPrefilledSliceReplaced pins that a prefilled slice is replaced
// by the document's elements on both paths, not appended to.
func TestTargetedPrefilledSliceReplaced(t *testing.T) {
type item struct {
N int `toml:"n"`
}
doc := []byte("[[items]]\nn = 1\n")
var ref struct {
Items []item `toml:"items"`
}
ref.Items = []item{{N: 9}}
if err := treeDecodeInto(doc, &ref); err != nil {
t.Fatalf("tree decode: %v", err)
}
var tgt struct {
Items []item `toml:"items"`
}
tgt.Items = []item{{N: 9}}
if err := Unmarshal(doc, &tgt); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(ref, tgt) {
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
}
if len(tgt.Items) != 1 || tgt.Items[0].N != 1 {
t.Errorf("items = %#v, want the prefilled element replaced", tgt.Items)
}
}
// TestTargetedHeaderOverValueArrayKeepsCase pins that a value array assigned
// under a differently cased key than the field's name still blocks the
// array-of-tables header over it, the tree parse error.
func TestTargetedHeaderOverValueArrayKeepsCase(t *testing.T) {
var cfg struct {
Arr []targetNested `toml:"arr"`
}
err := Unmarshal([]byte("Arr = [{x = 1}]\n[[Arr]]\nx = 2\n"), &cfg)
if err == nil || err.Error() != `interpres: line 2: key "Arr" is not an array of tables` {
t.Errorf("err = %v, want the parse error over the assigned field", err)
}
}
// TestTargetedDottedInlineFreezePath pins that an inline table assigned by a
// dotted key freezes the whole path the statement wrote: a later header
// under that path is the extension error, and a key outside it stays free.
func TestTargetedDottedInlineFreezePath(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte("m.a.b = {x = 1}\nb.y = 2\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
err = Unmarshal([]byte("m.a.b = {x = 1}\n[m.a.b]\ny = 2\n"), &cfg)
want := `interpres: line 2: cannot extend inline table "m.a.b"`
if err == nil || err.Error() != want {
t.Errorf("err = %v\nwant %q", err, want)
}
}
// TestTargetedStrictThroughDottedKeys pins that strict and required findings
// survive the transient tables a dotted descent builds.
func TestTargetedStrictThroughDottedKeys(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte("tab.zz = 1\n"), &cfg, RejectUnknownFields(true))
if err == nil || !strings.Contains(err.Error(), `unknown field "zz"`) {
t.Errorf("err = %v, want the strict failure through the dotted key", err)
}
if err == nil || !strings.HasPrefix(err.Error(), "interpres: tab:") {
t.Errorf("err = %v, want the path through the dotted key", err)
}
}
// TestTargetedRequiredThroughDottedKeys pins that a required tag is honoured
// when the table is reached only through dotted keys.
func TestTargetedRequiredThroughDottedKeys(t *testing.T) {
type nested struct {
X int `toml:"x,required"`
Y int `toml:"y"`
}
var cfg struct {
Tab nested `toml:"tab"`
}
err := Unmarshal([]byte("tab.y = 1\n"), &cfg)
if err == nil || !strings.Contains(err.Error(), `missing required key "x"`) {
t.Errorf("err = %v, want the missing required key through the dotted key", err)
}
}
// TestTargetedOrderedMapSliceFallsBack pins that a slice of OrderedMap
// elements takes the tree path, whose fill keeps the written order.
func TestTargetedOrderedMapSliceFallsBack(t *testing.T) {
var cfg struct {
Items []OrderedMap `toml:"items"`
}
err := Unmarshal([]byte("[[items]]\nk = \"v\"\n"), &cfg)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(cfg.Items) != 1 || cfg.Items[0].Keys()[0] != "k" {
t.Errorf("items = %#v, want the element filled in written order", cfg.Items)
}
}
// hookMap is a named map type whose decode hook counts its calls.
type hookMap map[string]any
var hookMapCalls atomic.Int32
func (h *hookMap) UnmarshalTOML(data any) error {
hookMapCalls.Add(1)
m, _ := data.(map[string]any)
if *h == nil {
*h = hookMap{}
}
maps.Copy((*h), m)
return nil
}
// TestTargetedMapFieldHookGetsWholeTable pins that a named map field with a
// decode hook receives the whole parsed table, even in its header form.
func TestTargetedMapFieldHookGetsWholeTable(t *testing.T) {
type cfg struct {
M hookMap `toml:"m"`
}
var c cfg
hookMapCalls.Store(0)
err := Unmarshal([]byte("[m]\na = 1\nb = 2\n"), &c)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if hookMapCalls.Load() != 1 {
t.Errorf("hook calls = %d, want exactly one with the whole table", hookMapCalls.Load())
}
if c.M["a"] != int64(1) || c.M["b"] != int64(2) {
t.Errorf("m = %#v", c.M)
}
}
// errHook fails every decode with a fixed error and counts its calls.
type errHook struct{ calls *int }
func (e *errHook) UnmarshalTOML(any) error {
if e.calls != nil {
*e.calls++
}
return errors.New("boom")
}
// TestTargetedHookErrorRunsOnce pins that a failing hook's error is the
// tree path's own, wrapped with the key, and that the hook is not run a
// second time by a fallback.
func TestTargetedHookErrorRunsOnce(t *testing.T) {
calls := 0
cfg := struct {
F errHook `toml:"f"`
}{F: errHook{calls: &calls}}
err := Unmarshal([]byte("f = 1\n"), &cfg)
if err == nil || err.Error() != "interpres: f: unmarshal: boom" {
t.Errorf("err = %v, want the wrapped hook failure", err)
}
if calls != 1 {
t.Errorf("hook calls = %d, want one", calls)
}
}
// TestTargetedUnknownBeforeRequired pins the report order the tree decode
// produces: an unknown key wins over a missing required one.
func TestTargetedUnknownBeforeRequired(t *testing.T) {
type inner struct {
X int `toml:"x,required"`
}
var cfg struct {
Tab inner `toml:"tab"`
}
err := Unmarshal([]byte("[tab]\nzz = 1\n"), &cfg, RejectUnknownFields(true))
if err == nil || !strings.Contains(err.Error(), `unknown field "zz"`) {
t.Errorf("err = %v, want the unknown key reported before the required one", err)
}
}
// TestTargetedStrictPathStableAcrossHeaders pins that the path a strict
// finding wraps does not alias the parser's key buffer: the table that owns
// the unknown key keeps its name after a later header.
func TestTargetedStrictPathStableAcrossHeaders(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte("[tab]\nzz = 1\n\n[lims]\nx = 1\n"), &cfg, RejectUnknownFields(true))
if err == nil || !strings.HasPrefix(err.Error(), "interpres: tab:") {
t.Errorf("err = %v, want the finding on tab, not the later header", err)
}
}
// TestTargetedPrefilledMapFieldMergesUnderHeader pins that a prefilled map
// field merges the document's header-form table into it on both paths, the
// rule the root map has always followed.
func TestTargetedPrefilledMapFieldMergesUnderHeader(t *testing.T) {
doc := []byte("[lims]\nnew = 3\n")
var ref, tgt targetCfg
ref.Lims = map[string]any{"keep": "yes"}
if err := treeDecodeInto(doc, &ref); err != nil {
t.Fatalf("tree decode: %v", err)
}
tgt.Lims = map[string]any{"keep": "yes"}
if err := Unmarshal(doc, &tgt); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(ref, tgt) {
t.Errorf("targeted = %#v, tree = %#v", tgt, ref)
}
if tgt.Lims["keep"] != "yes" || tgt.Lims["new"] != int64(3) {
t.Errorf("lims = %#v, want the merge", tgt.Lims)
}
}
// TestTargetedNumberTokenValidatesUTF8 pins that the token route the
// targeted parse takes reports invalid UTF-8 with the scanner's own message
// and position.
func TestTargetedNumberTokenValidatesUTF8(t *testing.T) {
var cfg targetCfg
err := Unmarshal([]byte("num = 12\xff\n"), &cfg)
if err == nil || !strings.Contains(err.Error(), "invalid UTF-8 in value at byte offset 8") {
t.Errorf("err = %v, want the UTF-8 complaint on the invalid byte", err)
}
}