bnb/guide/dispatch.rs
1//! `#[bin]` on an enum — tagged-union dispatch.
2//!
3//! A protocol union picks one of several payloads. `#[bin]` on an enum expresses that
4//! with two **orthogonal** concepts:
5//!
6//! - **`magic`** — a wire constant that is *read and written*: a byte string (`b"IHDR"`)
7//! or a width-suffixed unsigned integer (`0x01u16`). It is the discriminant under
8//! *magic dispatch*, or a verified signature on a tag-variant.
9//! - **`tag`** — a read-only **selector** taken from `ctx`. It picks the variant but is
10//! **never** on the wire.
11//!
12//! `#[catch_all]` preserves an unknown discriminant (dual-use); without one, a magic enum
13//! is a *closed set* and an unknown discriminant is a decode error.
14//!
15//! A tagged-union enum encodes **verbatim** — the canonical/`validate`
16//! surface that a [`#[bin]` struct](super::bin_codec#two-encode-forms-verbatim-vs-canonical)
17//! gets for a `reserved`/`calc` field is **struct-only**. Those are properties of a concrete
18//! record; for a union they belong to whichever variant payload is selected (each variant is
19//! itself a `#[bin]`-style record), not to the dispatch. So canonicalize / validate the
20//! payload type, then dispatch.
21//!
22//! # Magic dispatch — the discriminant is on the wire
23//!
24//! With no `tag`, each variant's `magic` is its discriminant: decode reads it once and
25//! matches; encode writes it. Variants may be unit, tuple, or named.
26//!
27//! ```
28//! use bnb::bin;
29//!
30//! #[bin(big)]
31//! #[derive(Debug, PartialEq)]
32//! enum Rdata {
33//! #[bin(magic = 1u16)] A(u32),
34//! #[bin(magic = 2u16)] Port { lo: u8, hi: u8 },
35//! #[bin(magic = 0u16)] Ping,
36//! #[catch_all]
37//! Other { magic: u16, #[br(count = 2)] raw: Vec<u8> }, // captures an unknown magic
38//! }
39//!
40//! assert_eq!(Rdata::A(0x0808_0808).to_bytes().unwrap(), [0x00, 0x01, 8, 8, 8, 8]);
41//! assert_eq!(Rdata::decode_exact(&[0x00, 0x09, 0xAA, 0xBB]).unwrap(),
42//! Rdata::Other { magic: 9, raw: vec![0xAA, 0xBB] });
43//! assert_eq!(Rdata::A(0).magic(), 1); // the `magic()` accessor
44//! ```
45//!
46//! Byte-string magics work the same way — the natural fit for PNG/RIFF-style signatures:
47//!
48//! ```
49//! use bnb::bin;
50//!
51//! #[bin(big)]
52//! #[derive(Debug, PartialEq)]
53//! enum Chunk {
54//! #[bin(magic = b"IHDR")] Header { width: u16, height: u16 },
55//! #[bin(magic = b"IDAT")] Data(u8),
56//! #[catch_all] Other { magic: [u8; 4], #[br(count = 1)] rest: Vec<u8> },
57//! }
58//!
59//! let hdr = [b'I', b'H', b'D', b'R', 0x00, 0x10, 0x00, 0x20];
60//! assert_eq!(Chunk::decode_exact(&hdr).unwrap(), Chunk::Header { width: 16, height: 32 });
61//! assert_eq!(Chunk::Header { width: 16, height: 32 }.to_bytes().unwrap(), hdr);
62//! ```
63//!
64//! A leading enum-level **`magic` prefix** is verified on read and written on encode,
65//! once, before dispatch:
66//!
67//! ```
68//! # use bnb::bin;
69//! #[bin(big, magic = b"BNB")]
70//! #[derive(Debug, PartialEq)]
71//! enum Pre { #[bin(magic = 1u8)] A(u16), #[bin(magic = 2u8)] B }
72//!
73//! assert_eq!(Pre::A(0xCAFE).to_bytes().unwrap(), [b'B', b'N', b'B', 0x01, 0xCA, 0xFE]);
74//! assert!(Pre::decode_exact(&[b'X', b'N', b'B', 0x01, 0, 0]).is_err()); // bad prefix
75//! ```
76//!
77//! # Tag dispatch — a read-only selector, nothing on the wire
78//!
79//! Declare a selector with `tag = <ctx-param>` and give each variant `#[bin(tag = V)]`.
80//! The enum reads/writes **no** discriminant; the parent passes the selector down with
81//! `#[br(ctx { … })]`, and `tag()` recovers it (driving a no-drift `calc`).
82//!
83//! ```
84//! use bnb::bin;
85//!
86//! #[bin(big, ctx(kind: u16), tag = kind)]
87//! #[derive(Debug, PartialEq)]
88//! enum Body {
89//! #[bin(tag = 1)] Login(u32),
90//! #[bin(tag = 2)] Data { n: u8 },
91//! }
92//!
93//! #[bin(big)]
94//! #[derive(Debug, PartialEq)]
95//! struct Packet {
96//! #[br(temp)]
97//! #[bw(calc = self.body.tag())] // recompute the tag from the chosen variant
98//! kind: u16,
99//! #[br(ctx { kind })]
100//! body: Body,
101//! }
102//!
103//! let p = Packet { body: Body::Data { n: 7 } };
104//! assert_eq!(p.to_bytes().unwrap(), [0x00, 0x02, 0x07]); // tag 2 then the payload
105//! assert_eq!(Packet::decode_exact(&[0x00, 0x02, 0x07]).unwrap(), p);
106//! ```
107//!
108//! # Composing `tag` + `magic`
109//!
110//! The two stack: the `tag` selects the variant, then its `magic` is a **signature** —
111//! verified on read, written on encode (it *is* on the wire; the tag is not).
112//!
113//! ```
114//! # use bnb::bin;
115//! #[bin(big, ctx(kind: u8), tag = kind)]
116//! #[derive(Debug, PartialEq)]
117//! enum Msg {
118//! #[bin(tag = 1, magic = b"LI")] Login(u32), // verify "LI" after the tag picks it
119//! #[bin(tag = 2)] Ping, // no signature
120//! }
121//!
122//! let li = [b'L', b'I', 0xAA, 0xBB, 0xCC, 0xDD];
123//! assert_eq!(Msg::decode_with_exact(&li, MsgCtx { kind: 1 }).unwrap(), Msg::Login(0xAABB_CCDD));
124//! assert!(Msg::decode_with_exact(&[b'X', b'X', 0, 0, 0, 0], MsgCtx { kind: 1 }).is_err());
125//! ```
126//!
127//! # Variable-width magics and a typed fallback
128//!
129//! Byte-string magics may differ in length: dispatch then **peeks** the longest, matches
130//! a prefix, and seeks past the winner (so it needs a seekable source). A variant with
131//! **no** `tag`/`magic` is a *typed fallback*, parsed when nothing matched; use a
132//! fallback **or** a `#[catch_all]` (which here reads from the unconsumed position), not
133//! both. Where nothing matches, the unmatched bytes are still there to read.
134//!
135//! ```
136//! use bnb::bin;
137//!
138//! #[bin(big)]
139//! #[derive(Debug, PartialEq)]
140//! enum Frame {
141//! #[bin(magic = b"LOGIN")] Login { user: u32 }, // 5-byte magic
142//! #[bin(magic = b"BYE")] Bye, // 3-byte magic
143//! Raw { len: u8, #[br(count = len)] body: Vec<u8> }, // typed fallback
144//! }
145//!
146//! assert_eq!(Frame::decode_exact(b"BYE").unwrap(), Frame::Bye);
147//! assert_eq!(Frame::decode_exact(&[0x02, 0xAA, 0xBB]).unwrap(),
148//! Frame::Raw { len: 2, body: vec![0xAA, 0xBB] });
149//! ```
150//!
151//! # Hybrid: `tag` priority, then `magic`
152//!
153//! One enum can mix both — the selector picks a tag variant first, and an **unmatched**
154//! selector falls through to magic dispatch:
155//!
156//! ```
157//! use bnb::bin;
158//!
159//! #[bin(big, ctx(kind: u8), tag = kind)]
160//! #[derive(Debug, PartialEq)]
161//! enum Packet {
162//! #[bin(tag = 1)] Known(u16), // chosen by kind == 1
163//! #[bin(magic = b"EXT")] Extended { sub: u8 }, // else matched by wire magic
164//! #[catch_all] Other { magic: [u8; 3], #[br(count = 1)] rest: Vec<u8> },
165//! }
166//!
167//! assert_eq!(Packet::decode_with_exact(&[0xAB, 0xCD], PacketCtx { kind: 1 }).unwrap(),
168//! Packet::Known(0xABCD));
169//! assert_eq!(Packet::decode_with_exact(b"EXT\x05", PacketCtx { kind: 9 }).unwrap(),
170//! Packet::Extended { sub: 5 });
171//! ```
172//!
173//! # Decode helpers
174//!
175//! Beyond the usual entry points, a dispatched enum gets:
176//!
177//! - **`decode_as_<variant>(bytes)`** — parse the bytes *as one explicit variant* (its
178//! magic, if any, then its payload), bypassing dispatch. Handy when the variant is
179//! known out of band, and for tests. (A `ctx` enum takes the context too.)
180//! - **`peek_variant(bytes) -> <Name>Kind`** (magic dispatch) — identify *which* variant
181//! the bytes are, from the wire magic, **without** parsing the payload — for routing.
182//! - **`decode_tagged(selector, bytes)`** (tag dispatch) — feed the selector directly.
183//!
184//! ```
185//! # use bnb::bin;
186//! #[bin(big)]
187//! #[derive(Debug, PartialEq)]
188//! enum Op {
189//! #[bin(magic = 1u8)] Get(u16),
190//! #[bin(magic = 2u8)] Set { key: u8, val: u8 },
191//! }
192//!
193//! assert_eq!(Op::peek_variant(&[0x02, 9, 9]).unwrap(), OpKind::Set);
194//! assert_eq!(Op::decode_as_get(&[0x01, 0xAB, 0xCD]).unwrap(), Op::Get(0xABCD));
195//! ```
196//!
197//! # Notes
198//!
199//! - **`magic` values are literals**: a byte string, or a width-suffixed unsigned integer
200//! (`1u16`, `0xCAu8`). Sub-byte and non-literal magics are rejected so the wire width is
201//! always unambiguous. Variable-width / fallback dispatch needs **byte-string** magics
202//! (so an unmatched discriminant can be re-read).
203//! - A single-read `#[catch_all]` stores the discriminant in its first field (the captured
204//! magic, or the selector under tag dispatch) so it can round-trip. On the peek path
205//! (variable width / fallback) the magic stays in the catch-all's own fields instead.
206//! - Variant fields support the full directive grammar (`count`, `if`, `map`, `ctx`,
207//! `temp`/`calc`, `parse_with`, …), so a catch-all can read its own length and recompute
208//! it on encode.
209//! - `tag()` / `magic()` return the variant's discriminant — generated only when there is a
210//! single one to report (so not for variable-width magic, a typed fallback, or a hybrid);
211//! `peek_variant` likewise needs an on-wire magic (so not under tag/hybrid dispatch).
212//! - With overlapping byte-string magics, declaration order decides — a magic that is a
213//! prefix of another should come first, and a fallback must not begin like a magic.