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 }