bnb/guide/dual_use.rs
1//! Dual-use: compliant by default, deliberately violatable.
2//!
3//! These types are meant for both sides of the wire: the guided path emits and accepts
4//! RFC-correct traffic, but a caller who *wants* to produce or read non-conformant
5//! bytes — for fuzzing, red-teaming, or interop testing — can. The rules that make
6//! that work:
7//!
8//! 1. **Builder defaults are compliant**, but the fields stay settable.
9//! 2. **Parsers accept representable-but-non-compliant values** — unknowns are modelled
10//! as data (`#[catch_all]`, retained flag bits), never hard errors.
11//! 3. **Policy lives on the construction path, never in a parser.** `validate` gates
12//! `build()`; decoding stays permissive.
13//! 4. **Raw constructors never validate** — they are the open escape hatch.
14//!
15//! # Unknowns are preserved, not rejected
16//!
17//! A `#[catch_all]` enum and a flag set both round-trip values they don't have a name
18//! for, so a parser never throws away or rejects representable input:
19//!
20//! ```
21//! use bnb::{BitEnum, Bits, bitflags, u4};
22//!
23//! #[derive(BitEnum, Clone, Copy, Debug, PartialEq, Eq)]
24//! #[bit_enum(u4)]
25//! enum Op { Read, Write, #[catch_all] Other(u4) }
26//!
27//! assert_eq!(Op::from_bits(0xE), Op::Other(u4::new(0xE))); // preserved...
28//! assert_eq!(Op::Other(u4::new(0xE)).into_bits(), 0xE); // ...and round-trips
29//!
30//! #[bitflags(u8)]
31//! #[derive(Clone, Copy)]
32//! struct F { a: bool, b: bool }
33//! assert_eq!(F::from_bits(0b1011).bits(), 0b1011); // undefined bits retained
34//! ```
35//!
36//! # The parser stays permissive; `validate` is construction-only
37//!
38//! `#[bin(validate = …)]` runs only in `build()`. Decoding the very same malformed
39//! bytes still succeeds — so you can parse hostile input for analysis, but can't
40//! *accidentally build* a malformed message. The same check is exposed as re-runnable
41//! `validate()` / `is_valid()` methods (computed on demand, never a stale flag), so you can
42//! re-confirm soundness before sending a value that may have been mutated since `build()`:
43//!
44//! ```
45//! use bnb::bin;
46//!
47//! #[bin(big, validate = check)]
48//! #[derive(Debug, PartialEq)]
49//! struct Cell { tag: u8 }
50//!
51//! fn check(c: &Cell) -> Result<(), String> {
52//! if c.tag > 3 { return Err("tag must be 0..=3".into()); }
53//! Ok(())
54//! }
55//!
56//! assert!(Cell::builder().tag(9).build().is_err()); // construction: rejected
57//! assert_eq!(Cell::decode_exact(&[9]).unwrap(), Cell { tag: 9 }); // parser: accepts
58//! ```
59//!
60//! # Raw constructors are the escape hatch
61//!
62//! `from_raw` (on a `#[bitfield]`) and `from_bits` never validate — they let you build
63//! any representable bit pattern on purpose:
64//!
65//! ```
66//! use bnb::{bitfield, u4};
67//! #[bitfield(u8, bits = msb)]
68//! #[derive(Clone, Copy)]
69//! struct B { hi: u4, lo: u4 }
70//!
71//! let weird = B::from_raw(0xFF); // no checks — exactly the bits you asked for
72//! assert_eq!(weird.hi().value(), 0xF);
73//! ```
74//!
75//! # Encoding reproduces; normalizing is opt-in
76//!
77//! The same principle governs *output*. The default
78//! [`to_bytes`](super::bin_codec#two-encode-forms-verbatim-vs-canonical) is **verbatim** — it
79//! re-emits exactly what you hold (retained `reserved` bits, a stored `calc` value), so a
80//! message you parsed off the wire round-trips byte-for-byte, malformed or not. Normalizing to
81//! the spec is the *explicit* `to_canonical_bytes` (or `to_canonical()` in memory, then
82//! `encode`), never something the codec does behind your back.
83//!
84//! # What is still rejected
85//!
86//! Only the *physically unencodable* is refused — never the merely non-conformant. A
87//! value that doesn't fit its field's bits can't be represented, so checked
88//! construction errors (and `new` panics):
89//!
90//! ```
91//! use bnb::u4;
92//! assert!(u4::try_new(0x10).is_err()); // 16 doesn't fit in 4 bits — unencodable
93//! ```
94//!
95//! The one place a *decode* can panic is a [`closed`](super::enums) enum fed an
96//! out-of-set discriminant — which is exactly why `closed` is an explicit opt-in and
97//! the default for untrusted input is `#[catch_all]`.