Files
mesh-sdk/go/catalogue/unknown_field.go
T
jschoubben 779ad9ac10 A requires entry naming a storage type asks for bus state, read leniently beside the plain names (issue 1966 S1)
ADR 0595 §1: the typed long form {name, storage, template, options,
configuration} decodes into Manifest.BusState, its name also on Requires;
a key it does not know is set aside and named, never refused (ADR 0578 §3).
The requirements tests are one table.
2026-11-03 13:19:40 +01:00

173 lines
5.1 KiB
Go

package catalogue
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"regexp"
)
// UnknownFieldError is a key a manifest has that this controller does not know, wherever it is: at the
// top of the manifest or inside a block (a state, an offer, a data item's backup, an own secret, a verb).
// Typed, because the blocks' decoders wrap the decoder's words in their own.
type UnknownFieldError struct {
Field string
err error
}
func (e *UnknownFieldError) Error() string { return e.err.Error() }
func (e *UnknownFieldError) Unwrap() error { return e.err }
// unknownFieldText is how the JSON decoder words a key its target does not have.
var unknownFieldText = regexp.MustCompile(`^json: unknown field "([^"]*)"$`)
// typedUnknown is err as an UnknownFieldError when it is the decoder refusing an unknown key, else err.
func typedUnknown(err error) error {
if err == nil {
return nil
}
if m := unknownFieldText.FindStringSubmatch(err.Error()); m != nil {
return &UnknownFieldError{Field: m[1], err: err}
}
return err
}
// asUnknownField is the unknown key err is about, at any depth, or nil.
func asUnknownField(err error) *UnknownFieldError {
var u *UnknownFieldError
if errors.As(typedUnknown(err), &u) {
return u
}
return nil
}
// prunedFields is a stored manifest's fields with every key this controller does not know taken out,
// and the keys taken out (novox/hq ADR 0262). A second pass, after the strict one refused: each
// top-level field is decoded alone, and where an entry inside it has an unknown key, the one
// occurrence whose removal moves the decoder past it is removed — never a key of the same name that
// the entry around it knows. So a left-out module still provides what it provides, and its data and
// directories are still read, which its backup lines are made from.
func prunedFields(keys map[string]json.RawMessage) (manifestFields, []string, error) {
var removed []string
tree := map[string]any{}
for k, raw := range keys {
var v any
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()
if err := dec.Decode(&v); err != nil {
return manifestFields{}, nil, err
}
tree[k] = v
}
for _, k := range sortedAnyKeys(tree) {
for tries := 0; ; tries++ {
err := decodesAlone(k, tree[k])
if err == nil {
break
}
unknown := asUnknownField(err)
if unknown == nil || tries > 64 {
return manifestFields{}, nil, err
}
if unknown.Field == k {
delete(tree, k)
removed = append(removed, k)
break
}
path, ok := removalThatHelps(tree[k], unknown.Field, err.Error(), func() error { return decodesAlone(k, tree[k]) })
if !ok {
// The same key unknown in two entries alike: no one removal changes the words.
// Every occurrence goes, and the field is judged again.
if removeEvery(tree[k], unknown.Field) == 0 {
return manifestFields{}, nil, err
}
path = "…." + unknown.Field
}
removed = append(removed, k+path)
}
}
raw, err := json.Marshal(tree)
if err != nil {
return manifestFields{}, nil, err
}
var fields manifestFields
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&fields); err != nil {
return manifestFields{}, nil, err
}
return fields, removed, nil
}
// decodesAlone is whether one top-level field decodes strictly on its own.
func decodesAlone(k string, v any) error {
raw, err := json.Marshal(map[string]any{k: v})
if err != nil {
return err
}
var fields manifestFields
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
return dec.Decode(&fields)
}
// removalThatHelps removes, from v, the one occurrence of key whose removal changes what the strict
// decoder says (decodes, which decodes v strictly), and says where it was. Every other occurrence is left as it was.
func removalThatHelps(v any, key, said string, decodes func() error) (string, bool) {
var found bool
var where string
var walk func(node any, at string) bool
walk = func(node any, at string) bool {
switch n := node.(type) {
case map[string]any:
if value, has := n[key]; has {
delete(n, key)
// Accepted only when the decoder is past it: nothing left, or an unknown key said
// elsewhere. A different kind of error means the removal broke the entry; the same
// words mean this was not the occurrence it refused.
err := decodes()
if err == nil || (asUnknownField(err) != nil && err.Error() != said) {
found, where = true, at+"."+key
return true
}
n[key] = value
}
for _, sub := range sortedAnyKeys(n) {
if walk(n[sub], at+"."+sub) {
return true
}
}
case []any:
for i, item := range n {
if walk(item, fmt.Sprintf("%s[%d]", at, i)) {
return true
}
}
}
return false
}
walk(v, "")
return where, found
}
// removeEvery removes key from every object in v, and says how many it removed.
func removeEvery(v any, key string) int {
n := 0
switch node := v.(type) {
case map[string]any:
if _, has := node[key]; has {
delete(node, key)
n++
}
for _, sub := range node {
n += removeEvery(sub, key)
}
case []any:
for _, item := range node {
n += removeEvery(item, key)
}
}
return n
}