Documentation
¶
Overview ¶
Package canonical owns the module's canonical encoding: the framing every identity digest in this module is computed over.
It exists because there is more than one thing worth digesting — a server's catalog (internal/catalog), a binding's configuration identity (pkg/harness) — and they must agree on how bytes are framed. Two encoders that start identical and drift apart are the specific hazard: the drift is invisible (each side's own golden test still passes) and its symptom is two digests that disagree about content they agree on, or worse, agree about content they differ on.
The framing gives every caller two properties that a naive hash of a struct does not have:
- Determinism. The same content produces the same digest however it arrived. Nothing here iterates a map; ordering collections canonically before encoding them is the caller's job, because only the caller knows which orderings are content and which are incidental.
- Unambiguity. No two different inputs encode to the same bytes. Every value is length-delimited or fixed-width, so fields cannot be slid into one another: Str("ab") + Str("") and Str("a") + Str("b") are distinct encodings where a naive concatenation would render both as "ab".
The scheme follows the fingerprint guidance in the session-versioning design: explicit domain, explicit schema version, stable field ordering, length-delimited values, deterministic collection ordering. This package supplies the framing; each caller supplies its own domain tag, its own schema version, and its own field order, because those are the parts that define what a particular digest means.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Encoder ¶
type Encoder struct {
// contains filtered or unexported fields
}
Encoder writes the canonical encoding into a hash. Every method is length-delimited or fixed-width, so the concatenation of any sequence of calls is unambiguous.
It writes to a hash.Hash, which never errors (hash.Hash's Write contract forbids it), so no method here returns one. That is the reason this is not an io.Writer wrapper with error plumbing nobody could act on.
func NewEncoder ¶
NewEncoder returns an Encoder that writes into h.
func (*Encoder) Field ¶
Field writes a field tag. It is a length-delimited string like any other, which is enough to separate fields: the hash sees a different byte sequence for any different tag, and a tag can never be confused with a value because both are framed identically.
A field tag is not needed for unambiguity — the length delimiters already supply that — it is needed for meaning: it pins a value to the field it was read from, so that reordering two same-typed fields changes the digest.
func (*Encoder) Int ¶
Int writes a signed integer as a sign byte followed by a magnitude, rather than by casting a possibly-negative value straight to uint64.
The sign is written separately so that the magnitude is always a genuine count. A bare uint64(i) would encode -1 as 2^64-1, which is a legal encoding of a legal value — so a negative field and an enormous positive one would share a digest, and a configuration error would be indistinguishable from a configuration.