R1-F-sdk 5: catalogue.Decode refuses a manifest giving one field twice, naming its path
A decoder keeps the last of two equal keys without a word, so the indexer's judge read a manifest a merge of two branches had given one field twice as if one side never was. mesh-catalog's checks/manifests refuses it already; Decode now refuses it the same way, at any depth. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
+53
-1
@@ -1,7 +1,9 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -9,15 +11,65 @@ import (
|
||||
// contract knows as it declares it, and a field it does not know set aside and named (SetAside), never refused. It
|
||||
// judges nothing: no name, seat, bus or catalogue rule is applied, since the indexer judged the manifest once and its
|
||||
// verdict is in the indexer's state (delivery.VerdictsBucket); a reader takes the verdict beside it
|
||||
// (delivery.TakeVerdict). An error is only bytes that are no manifest: not JSON, or a field of the wrong shape.
|
||||
// (delivery.TakeVerdict). An error is only bytes that are no manifest: not JSON, a field of the wrong shape, or one field
|
||||
// given twice in one object at any depth, named by its path: a decoder keeps the last of two equal keys without a word,
|
||||
// so a merge of two branches that each added the field would lose one side silently (as mesh-catalog's checks/manifests
|
||||
// refuses it).
|
||||
func Decode(raw []byte) (Manifest, error) {
|
||||
var m Manifest
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
return Manifest{}, err
|
||||
}
|
||||
if twice := givenTwice(raw); twice != "" {
|
||||
return Manifest{}, fmt.Errorf("the field %s is given twice; a manifest gives each field once", twice)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// givenTwice is the first field an object of raw gives twice, as its path (`invokes`, `build.artifacts[0].env`), or empty.
|
||||
func givenTwice(raw []byte) string {
|
||||
d := json.NewDecoder(bytes.NewReader(raw))
|
||||
var walk func(path string) string
|
||||
walk = func(path string) string {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
switch t {
|
||||
case json.Delim('{'):
|
||||
seen := map[string]bool{}
|
||||
for d.More() {
|
||||
k, err := d.Token()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
key, _ := k.(string)
|
||||
at := key
|
||||
if path != "" {
|
||||
at = path + "." + key
|
||||
}
|
||||
if seen[key] {
|
||||
return at
|
||||
}
|
||||
seen[key] = true
|
||||
if twice := walk(at); twice != "" {
|
||||
return twice
|
||||
}
|
||||
}
|
||||
_, _ = d.Token()
|
||||
case json.Delim('['):
|
||||
for i := 0; d.More(); i++ {
|
||||
if twice := walk(fmt.Sprintf("%s[%d]", path, i)); twice != "" {
|
||||
return twice
|
||||
}
|
||||
}
|
||||
_, _ = d.Token()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return walk("")
|
||||
}
|
||||
|
||||
// SetAside names what Decode set aside of this manifest: each field it does not know, as the decoder words the first
|
||||
// and with the path of every one it read without, or "" when it knew every field. A reader says it in its own words
|
||||
// (its journal, its finding) and reads the rest; the part that needs the field takes the newer package (ADR 0578 §6).
|
||||
|
||||
@@ -64,21 +64,32 @@ func TestARequirementsLongForm(t *testing.T) {
|
||||
names string
|
||||
busState []BusStateEntry
|
||||
aside []string
|
||||
refused string
|
||||
}{
|
||||
// 1966/AC1: a plain name and a bus-state entry: Requires names the plain one only, since it names provisions,
|
||||
// and BusState holds the entry whole.
|
||||
{"a plain name and a bus-state entry", `["postgres-database",
|
||||
{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":1024}}]`,
|
||||
"postgres-database", []BusStateEntry{records}, nil},
|
||||
"postgres-database", []BusStateEntry{records}, nil, ""},
|
||||
// 1966/AC2: an unknown key is set aside and named, never refused; the entry decodes.
|
||||
{"a bus-state entry with a key it does not know", `[
|
||||
{"name":"records","storage":"ledger","template":"ledger","colour":"red","options":{"max-mib":1024}}]`,
|
||||
"", []BusStateEntry{records}, []string{"requires[0].colour"}},
|
||||
"", []BusStateEntry{records}, []string{"requires[0].colour"}, ""},
|
||||
// 1966/AC3: a ledger type with each of the four effects round-trips through JSON unchanged.
|
||||
{"a ledger with its type", `[{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":1024},
|
||||
"types":[` + string(typeJSON) + `]}]`, "", []BusStateEntry{ledger}, nil},
|
||||
"types":[` + string(typeJSON) + `]}]`, "", []BusStateEntry{ledger}, nil, ""},
|
||||
// A field given twice at any depth is refused, named by its path: a decoder would keep the last without a word.
|
||||
{"a field given twice", `[], "requires": ["postgres-database"]`, "", nil, nil, "the field requires is given twice"},
|
||||
{"a field given twice deeper", `[{"name":"records","storage":"ledger","name":"other","template":"ledger"}]`,
|
||||
"", nil, nil, "the field requires[0].name is given twice"},
|
||||
} {
|
||||
got, err := decodeForTest([]byte(`{"module":"x","version":"1","requires":` + c.requires + `}`))
|
||||
if c.refused != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), c.refused) {
|
||||
t.Errorf("%s: read as %+v, %v; want refused: %s", c.name, got, err, c.refused)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("%s: refused: %v", c.name, err)
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user