package catalogue import ( "bytes" "encoding/json" "errors" "fmt" "regexp" ) // A key this controller does not know, in a manifest (novox/hq ADR 0262). // // Registration refuses it, as it always has. A manifest the store already holds was registered by a newer // controller, and is read by this one after a rollback: refusing it there failed the whole catalogue, and // with it every plan and every send. Dropping the key silently would be worse — a module running without // something its manifest says. So the manifest is read, the module is left out of every machine's // declaration by name, and the controller raises a condition until it is updated. // 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 } // UnknownFieldReason is why a module whose stored manifest has a key this controller does not know is // left out of a machine's declaration, or "" when it has none. func UnknownFieldReason(m Manifest) string { if m.unknown == "" { return "" } return m.Module + " uses a field this controller does not know (" + m.unknown + "); it is left out " + "until the controller is updated: nothing of it is changed on its machines and its contributions to " + "other modules and its open ports stop, while its data is still backed up and it still provides " + "what it provides (novox/hq ADR 0262)" } // backupView is what of a left-out module still reaches its machine: its data, so the backup holder // keeps copying it, and the directories and accesses its data items name. No contribution, shell code // or environment of its own: those are what leaving it out stops. func backupView(m Manifest) Manifest { view := Manifest{Module: m.Module, Data: m.Data, Accesses: m.Accesses} for _, r := range m.Resources { if fmt.Sprint(r["type"]) == "directory" { view.Resources = append(view.Resources, r) } } return view } // 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(k, tree[k], unknown.Field, err.Error()) 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 of field k, and says where it was. Every other occurrence is left as it was. func removalThatHelps(k string, v any, key, said string) (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) if err := decodesAlone(k, v); 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 }