From 5933eb757d443293631be2bed27a96288c2348d8 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 13 Oct 2026 20:29:24 +0200 Subject: [PATCH] go/asks: whole details never digest as another ask's Whole and Details (the review of #25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WholeDetails was written after a tag, so an ask with only whole details "+ x: 1" encoded as one whose Whole was "whole-details" and Details "+ x: 1". Whole and Details are now written whenever whole details are given, then the whole details, untagged: the number of trailing values tells the three shapes apart, and an ask without whole details digests as before. The note's comment says what ADR 0347 §5 holds of cuts. --- go/asks/asks.go | 14 +++++++++----- go/asks/asks_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/go/asks/asks.go b/go/asks/asks.go index 8915af8..596bb8f 100644 --- a/go/asks/asks.go +++ b/go/asks/asks.go @@ -170,14 +170,16 @@ func (a Ask) Digest() string { canonical(&b, v) } } - if a.Whole != "" || a.Details != "" { + // Whole and Details are written whenever either or WholeDetails is given, and WholeDetails after them whenever + // it is given (novox/hq ADR 0347): the fields after the options are then always one of three fixed shapes — + // none, the two, or the three — and the count of values the sequence holds tells them apart, so no two asks + // encode the same (the review of mesh-sdk #25). An ask with none of them digests as before they existed, and + // one without WholeDetails as before it existed. + if a.Whole != "" || a.Details != "" || a.WholeDetails != "" { canonical(&b, a.Whole) canonical(&b, a.Details) } - // WholeDetails (novox/hq issue 383, the review of mesh-controller #304) follows only when given, so every - // ask without it digests as it did before it existed. if a.WholeDetails != "" { - canonical(&b, "whole-details") canonical(&b, a.WholeDetails) } sum := sha256.Sum256([]byte(b.String())) @@ -493,7 +495,9 @@ type Message struct { Buttons []Button `json:"buttons,omitempty"` // Note is the message's secondary words, after its body and its buttons: who asked and when, what each // answer does, when it expires, and the ask's Details. A channel shows it smaller, collapsed or set apart - // where it can, and after the body where it cannot; it is never left out (novox/hq issue 639). + // where it can, and after the body where it cannot (novox/hq issue 639). A channel that bounds a message's + // length cuts the body first and the note only when the title and the note alone do not fit, saying each + // cut (novox/hq ADR 0347 §5). Note string `json:"note,omitempty"` Urgent bool `json:"urgent,omitempty"` Silent bool `json:"silent,omitempty"` diff --git a/go/asks/asks_test.go b/go/asks/asks_test.go index 3ce66d6..b9a7fdc 100644 --- a/go/asks/asks_test.go +++ b/go/asks/asks_test.go @@ -337,3 +337,31 @@ func TestWholeDetailsAreDigestedOnlyWhenGiven(t *testing.T) { t.Fatal("an ask without whole details digests otherwise than before") } } + +// No two asks encode the same (the review of mesh-sdk #25): whole details given beside nothing else are not read as +// a Whole and Details that happen to spell the same values, nor as any other split of the three fields. +func TestWholeDetailsCollideWithNoOtherAsk(t *testing.T) { + base := Ask{ID: "a", Headline: "h", Explanation: "e"} + shapes := []Ask{} + for _, f := range []struct{ whole, details, wholeDetails string }{ + {"", "", "+ x: 1"}, + {"whole-details", "+ x: 1", ""}, + {"", "+ x: 1", ""}, + {"+ x: 1", "", ""}, + {"", "", "whole-details"}, + {"", "+ x: 1", "+ x: 1"}, + {"+ x: 1", "+ x: 1", ""}, + {"", "", ""}, + } { + a := base + a.Whole, a.Details, a.WholeDetails = f.whole, f.details, f.wholeDetails + shapes = append(shapes, a) + } + seen := map[string]int{} + for i, a := range shapes { + if j, ok := seen[a.Digest()]; ok { + t.Errorf("asks %d and %d digest the same: %+v / %+v", j, i, shapes[j], a) + } + seen[a.Digest()] = i + } +}