From b1391867a882333caa3e9333e9cf481c415304da Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 10 Nov 2026 16:34:14 +0100 Subject: [PATCH] O-2: C-S2: stateobjects.Ref, a body exactly {mesh-object: Ref} is a reference and anything else a value (milestone 47/G, issue 1966, ADR 0666 section 16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §57 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- go/packages_list_test.go | 2 +- go/stateobjects/ref.go | 71 +++++++++++++++++++++++++++++++++++++ go/stateobjects/ref_test.go | 31 ++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 go/stateobjects/ref.go create mode 100644 go/stateobjects/ref_test.go diff --git a/go/packages_list_test.go b/go/packages_list_test.go index 35dfb78d..109974d4 100644 --- a/go/packages_list_test.go +++ b/go/packages_list_test.go @@ -26,7 +26,7 @@ var meshSDKPackages = []string{ ".", "announce", "announcetest", "buscalls", "buscompose", "busstate", "busretry", "calladdress", "catalogue", "declaration", "forgekey", "health", "internal/beside", "keys", "kvlimit", "membership", "motion", "objectlink", "outward", "pernode", "process", - "sdkversion", "seal", "seatdef", "seatlist", "seatlist/seatlisttest", "testbus", "views", "views/viewstest", + "sdkversion", "seal", "seatdef", "seatlist", "seatlist/seatlisttest", "stateobjects", "testbus", "views", "views/viewstest", } // clientSeats are the seats whose logic is their client's in mesh-contracts (novox/hq ADR 0627 §1, §4): mesh-sdk names diff --git a/go/stateobjects/ref.go b/go/stateobjects/ref.go new file mode 100644 index 00000000..1c76fc79 --- /dev/null +++ b/go/stateobjects/ref.go @@ -0,0 +1,71 @@ +// Package stateobjects keeps values of any size on the bus (novox/hq ADR 0666 §16, ADR 0595 §9): a value over the bus's +// limit for one value, or a verb reply or an event over the limit for one message, is put in an object store and only its +// reference travels. It is a small package of its own, imported only by the modules that use it: the module library (the +// root package) and the process package do not import it, so their importers are not rebuilt when it changes. It names no +// seat and no module, and imports no contracts module. +package stateobjects + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" +) + +// FormVersion is the version of the reference form; a reader refuses a reference of another. +const FormVersion = 1 + +// refField is the one field of a body that is a reference. +const refField = "mesh-object" + +// Ref names an object in an object store: a body that is exactly `{"mesh-object": Ref}` is a reference, and anything +// else is a value. +type Ref struct { + // Store is the object store's name as its reader asks for it: a bundle's local name, or the name a process opens. + Store string `json:"store"` + // Object is the object's name in that store. + Object string `json:"object"` + // Size is the object's size in bytes. + Size int64 `json:"size"` + // Digest is the hex SHA-256 of the object's bytes; a reader checks it after the read. + Digest string `json:"sha256"` + // Form is FormVersion. + Form int `json:"form"` +} + +func (r Ref) valid() bool { + if r.Store == "" || r.Object == "" || r.Size < 0 || r.Form != FormVersion || len(r.Digest) != 64 { + return false + } + _, err := hex.DecodeString(r.Digest) + return err == nil +} + +// Wrap is the body that holds r. +func Wrap(r Ref) []byte { + raw, _ := json.Marshal(map[string]Ref{refField: r}) + return raw +} + +// ReadRef reads a body as a reference: ok is false for a value, including one that only looks like a reference (another +// field beside it, a field it does not know, a digest that is no digest, another form). +func ReadRef(body []byte) (Ref, bool) { + var outer map[string]json.RawMessage + if err := json.Unmarshal(body, &outer); err != nil || len(outer) != 1 { + return Ref{}, false + } + raw, ok := outer[refField] + if !ok { + return Ref{}, false + } + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + var r Ref + if err := dec.Decode(&r); err != nil || !r.valid() { + return Ref{}, false + } + return r, true +} + +// errLooksLikeRef refuses a value that would be read back as a reference. +var errLooksLikeRef = fmt.Errorf("the value is exactly a reference to an object (a body of the one field %q): it would be read as one", refField) diff --git a/go/stateobjects/ref_test.go b/go/stateobjects/ref_test.go new file mode 100644 index 00000000..1fe64ae8 --- /dev/null +++ b/go/stateobjects/ref_test.go @@ -0,0 +1,31 @@ +package stateobjects + +import ( + "strings" + "testing" +) + +// ADR 0666 §16: a body is a reference when it is exactly `{"mesh-object": Ref}`; anything else is a value. +func TestABodyIsAReferenceOnlyWhenItIsExactlyOne(t *testing.T) { + good := Ref{Store: "views-objects", Object: "k." + strings.Repeat("a", 64), Size: 3 << 20, Digest: strings.Repeat("a", 64), Form: FormVersion} + got, ok := ReadRef(Wrap(good)) + if !ok || got != good { + t.Fatalf("round trip: %+v, %v", got, ok) + } + d := strings.Repeat("a", 64) + rows := []struct{ name, body string }{ + {"a value", `{"a":1}`}, + {"a list", `[1]`}, + {"not JSON", `nope`}, + {"another field beside it", `{"mesh-object":{"store":"s","object":"o","size":1,"sha256":"` + d + `","form":1},"x":1}`}, + {"a field the reference does not know", `{"mesh-object":{"store":"s","object":"o","size":1,"sha256":"` + d + `","form":1,"x":1}}`}, + {"a digest that is no digest", `{"mesh-object":{"store":"s","object":"o","size":1,"sha256":"zz","form":1}}`}, + {"another form", `{"mesh-object":{"store":"s","object":"o","size":1,"sha256":"` + d + `","form":2}}`}, + {"no store", `{"mesh-object":{"object":"o","size":1,"sha256":"` + d + `","form":1}}`}, + } + for _, r := range rows { + if _, ok := ReadRef([]byte(r.body)); ok { + t.Errorf("%s: read as a reference", r.name) + } + } +}