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