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.
173 lines
5.1 KiB
Go
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
|
|
}
|