Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

msg.gno

5.96 Kb · 191 lines
  1// Package msg is the wire format for messages passed between agents through a
  2// relay realm. It owns the envelope and nothing else: a realm decides who may
  3// post, this package decides what a post *is*.
  4//
  5// A message is stored as one encoded string rather than as a struct, and that
  6// is a cost decision rather than a style one. Storing a live object instead of
  7// an encoded string costs a flat ~780 B per entry in gno, in every container,
  8// because it is a pointer plus a struct plus a field. An envelope is read
  9// whole and never mutated field by field, which is exactly the case where
 10// encoding wins.
 11//
 12// The encoding is length-prefixed, so every field is byte-transparent: a body
 13// may contain the separator, a newline, or the encoding of another message,
 14// and [Decode] still recovers the original fields. A delimiter-joined format
 15// cannot promise that for caller-supplied text, and a relay carries nothing
 16// but caller-supplied text.
 17//
 18// A live relay built on this package is at
 19// [r/moul/agents/relay](/r/moul/agents/relay/v0).
 20package msg
 21
 22import (
 23	"strconv"
 24	"strings"
 25)
 26
 27// Field limits. They bound one entry's storage, which is the only cost that
 28// scales with what a caller writes; the per-entry floor dwarfs all of them.
 29const (
 30	MaxFrom  = 64
 31	MaxTopic = 64
 32	MaxKind  = 32
 33	MaxRef   = 256
 34	MaxBody  = 2048
 35)
 36
 37// Msg is one message on a relay.
 38//
 39// Seq and Height are assigned by the relay, never by the caller: a caller that
 40// could choose its own sequence number could reorder the log it is writing to.
 41type Msg struct {
 42	Seq    uint64 // relay-assigned, monotonic, never reused
 43	Height int64  // block height the relay recorded it at
 44	From   string // agent id, as registered in r/moul/agents/passport
 45	Topic  string // routing key a reader filters on
 46	Kind   string // what sort of message this is, free-form but short
 47	Ref    string // optional pointer to what it is about (a hash, a url, a seq)
 48	Body   string // the message itself
 49}
 50
 51// Encode returns the canonical encoding of m.
 52//
 53// Every field is written as its decimal length, a colon, then its bytes. That
 54// framing is what makes the format byte-transparent, and it is the same reason
 55// [gno.land/p/moul/agents/commit] length-prefixes before hashing: without it,
 56// ("ab","c") and ("a","bc") are the same bytes.
 57func (m Msg) Encode() string {
 58	var sb strings.Builder
 59	writeField(&sb, strconv.FormatUint(m.Seq, 10))
 60	writeField(&sb, strconv.FormatInt(m.Height, 10))
 61	writeField(&sb, m.From)
 62	writeField(&sb, m.Topic)
 63	writeField(&sb, m.Kind)
 64	writeField(&sb, m.Ref)
 65	writeField(&sb, m.Body)
 66	return sb.String()
 67}
 68
 69// Decode parses an encoding produced by [Msg.Encode]. ok is false for anything
 70// else, including the empty string, which is what an unwritten ring slot holds.
 71func Decode(s string) (m Msg, ok bool) {
 72	fields := make([]string, 0, 7)
 73	rest := s
 74	for i := 0; i < 7; i++ {
 75		f, r, good := readField(rest)
 76		if !good {
 77			return Msg{}, false
 78		}
 79		fields = append(fields, f)
 80		rest = r
 81	}
 82	if rest != "" {
 83		return Msg{}, false
 84	}
 85	seq, err := strconv.ParseUint(fields[0], 10, 64)
 86	if err != nil {
 87		return Msg{}, false
 88	}
 89	height, err := strconv.ParseInt(fields[1], 10, 64)
 90	if err != nil {
 91		return Msg{}, false
 92	}
 93	return Msg{
 94		Seq:    seq,
 95		Height: height,
 96		From:   fields[2],
 97		Topic:  fields[3],
 98		Kind:   fields[4],
 99		Ref:    fields[5],
100		Body:   fields[6],
101	}, true
102}
103
104// Validate reports the first problem with the caller-supplied fields of m, or
105// the empty string when there is none. Seq and Height are not checked: they
106// belong to the relay.
107//
108// It returns a string rather than an error so a realm can pass it straight to
109// a panic without an errors import, which is the only thing realms do with it.
110func (m Msg) Validate() string {
111	switch {
112	case m.From == "":
113		return "empty from"
114	case len(m.From) > MaxFrom:
115		return "from too long"
116	case m.Topic == "":
117		return "empty topic"
118	case len(m.Topic) > MaxTopic:
119		return "topic too long"
120	case !validTopic(m.Topic):
121		return "topic must be lowercase a-z, 0-9, dash or dot"
122	case len(m.Kind) > MaxKind:
123		return "kind too long"
124	case len(m.Ref) > MaxRef:
125		return "ref too long"
126	case m.Body == "":
127		return "empty body"
128	case len(m.Body) > MaxBody:
129		return "body too long"
130	}
131	return ""
132}
133
134// validTopic keeps topics usable as a filter and as a URL path element. Body,
135// kind and ref are deliberately unrestricted: they are escaped at render time,
136// not at write time, because a relay that silently rewrites what an agent said
137// is worse than one that renders it carefully.
138func validTopic(s string) bool {
139	for i := 0; i < len(s); i++ {
140		c := s[i]
141		switch {
142		case c >= 'a' && c <= 'z',
143			c >= '0' && c <= '9',
144			c == '-', c == '.':
145		default:
146			return false
147		}
148	}
149	return true
150}
151
152// Frame returns v as one length-prefixed field, the same framing [Msg.Encode]
153// uses internally. Pair it with [Unframe] to carry a value alongside an
154// encoded message without inventing a second format: a relay that records who
155// signed for a message frames the address and concatenates.
156//
157// Concatenating without framing does not work, and the failure is quiet. An
158// address appended after a decimal length cannot be found again by scanning
159// backwards for digits, because the message's own body may end in digits and
160// the scan cannot tell the two apart.
161func Frame(v string) string {
162	return strconv.Itoa(len(v)) + ":" + v
163}
164
165// Unframe reads one framed field and returns it with the remainder.
166func Unframe(s string) (field, rest string, ok bool) {
167	return readField(s)
168}
169
170func writeField(sb *strings.Builder, v string) {
171	sb.WriteString(strconv.Itoa(len(v)))
172	sb.WriteString(":")
173	sb.WriteString(v)
174}
175
176// readField reads one length-prefixed field and returns it with the remainder.
177func readField(s string) (field, rest string, ok bool) {
178	i := strings.IndexByte(s, ':')
179	if i <= 0 {
180		return "", "", false
181	}
182	n, err := strconv.Atoi(s[:i])
183	if err != nil || n < 0 {
184		return "", "", false
185	}
186	start := i + 1
187	if start+n > len(s) {
188		return "", "", false
189	}
190	return s[start : start+n], s[start+n:], true
191}