Expand description
Field-directive reference — #[br] (read), #[bw] (write), and the standalone
field attributes, one runnable example each.
These appear on the fields of a #[bin] struct (or a bare
#[derive(BitDecode/BitEncode)]). Struct-level options (magic, ctx, validate,
big/little, …) are covered in bin_codec.
§count — a length-driven Vec
#[br(count = <expr>)] reads that many elements into a Vec<T>; the expression may
name an earlier field. On write, every element is emitted (the length is the
caller’s to track — usually with temp+calc, below, or the count_prefix sugar).
The count obligation. The expression drives decode sizing only — encode trusts
the Vec and writes everything it holds. If a stored count (or a ctx param, which
doesn’t even exist at encode time) disagrees with len(), the emitted bytes won’t
round-trip. Keeping them consistent is the constructor’s job: derive the count
(count_prefix / temp+calc) where the layout allows, or enforce it with
construction-side validate (see the ctx_length example). The flip side is
deliberate: a stored count field can disagree on purpose — that’s how dual-use
code forges adversarial frames.
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct List { len: u8, #[br(count = len)] items: Vec<u16> }
let v = List { len: 2, items: vec![0xAABB, 0xCCDD] };
assert_eq!(v.to_bytes().unwrap(), [0x02, 0xAA, 0xBB, 0xCC, 0xDD]);
assert_eq!(List::decode_exact(&[0x02, 0xAA, 0xBB, 0xCC, 0xDD]).unwrap(), v);§temp + calc — derived, never stored
#[br(temp)] reads a value into a local that later directives can use, but does not
store it; its #[bw(calc = <expr>)] recomputes it on write. Together they keep a
length/count from ever drifting from the data it describes.
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Msg {
#[br(temp)]
#[bw(calc = self.items.len() as u8)]
n: u8,
#[br(count = n)]
items: Vec<u8>,
}
let m = Msg { items: vec![10, 20, 30] }; // no `n` field to set
assert_eq!(m.to_bytes().unwrap(), [0x03, 10, 20, 30]);
assert_eq!(Msg::decode_exact(&[0x02, 5, 6]).unwrap().items, vec![5, 6]);§br(calc) — a decode-computed field (the read-side dual of bw(calc))
#[br(calc = <expr>)] binds a stored field from earlier fields on decode (no
wire read) and writes nothing on encode — its bytes live in the raw fields it
derives from. It resolves a field whose typed meaning depends on a field later on
the wire without a look-ahead: read the raw bits into temps in wire order, then
calc the typed field from the full set. (The shape of NXDN’s LICH and DMR’s
directional tables, where a direction bit read near the end selects how earlier
bits are interpreted.)
use bnb::{bin, u2, u3};
#[bin]
#[derive(Debug, PartialEq)]
struct Frame {
header: u3,
#[br(temp)]
#[bw(calc = u2::new(self.level & 0b11))] // raw bits, recomputed from `level`
raw: u2,
outbound: bool, // the context — read *after* `raw`
trailing: u2,
// `level` is the raw value when outbound, or that value plus four when inbound;
// it needs `outbound`, which is read after `raw`. Resolved after the whole
// record is read; writes nothing (its bytes are `raw`'s).
#[br(calc = if outbound { raw.value() } else { raw.value() + 4 })]
level: u8,
}
// `raw` is a temp, so the literal omits it and sets only the semantic `level`.
let inbound = Frame { header: u3::new(0b101), outbound: false, trailing: u2::new(0), level: 6 };
let bytes = inbound.to_bytes().unwrap();
assert_eq!(bytes.len(), 1);
assert_eq!(Frame::decode_exact(&bytes).unwrap(), inbound); // round-trips
// The later `outbound` bit changes what the same raw bits mean: level 2 and level 6
// both store raw `0b10`, distinguished only by `outbound`.
let outbound = Frame { header: u3::new(0b101), outbound: true, trailing: u2::new(0), level: 2 };
assert_eq!(Frame::decode_exact(&outbound.to_bytes().unwrap()).unwrap().level, 2);§count_prefix — the length-prefixed count, in one line
A count immediately followed by the elements it counts is the most common shape of
the temp+calc+count triad above — so #[brw(count_prefix = <Ty>)] on the Vec
generates the whole triad. The prefix is read into a hidden local, sizes the Vec,
and is recomputed from len() on write: derived, never stored, can never drift.
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Msg {
#[brw(count_prefix = u8)]
items: Vec<u8>,
}
let m = Msg { items: vec![10, 20, 30] };
assert_eq!(m.to_bytes().unwrap(), [0x03, 10, 20, 30]); // same wire as the triad
assert_eq!(Msg::decode_exact(&[0x02, 5, 6]).unwrap().items, vec![5, 6]);Any Bits type works as the prefix — a uN occupies its declared
width, so a count_prefix = u12 puts a true 12-bit count on the wire. Encode is
checked: a collection too long for the prefix is a BitError
(Convert), never a silently wrapped count (which is what a hand-written
as u8 calc would do).
let too_long = Msg { items: vec![0; 300] }; // 300 > u8::MAX
assert!(too_long.to_bytes().is_err()); // checked, not truncatedcount_prefix is adjacent (immediately before its Vec) and derive-only (never
stored, so never overridable). When the count sits away from its data (DNS’s
qdcount…arcount header block) or measures bytes rather than elements (DNS’s
rdlength), or when you want to forge a disagreeing length — reach for [WireLen],
below.
§WireLen — an auto-deriving, overridable length/count
WireLen<T> is a length field that is either auto() (derive at
encode — the default) or set(n) (an explicit value). Decode always yields Set, so a
plain to_bytes() is correct by default and decode → encode is byte-identical (a
forged length survives a round-trip). It’s the dual-use, non-adjacent, byte-or-element
counterpart to count_prefix:
- Same-struct:
#[bw(auto_len = count(<field>))](element count) or#[bw(auto_len = bytes(<field>))](encoded byte length) on theWireLenfield. - Cross-struct:
#[bin(auto_len(<field>.<nested> = count(<source>), …))]on the enclosing struct — a count nested in a sub-struct that sizes a sibling collection. The targeted sub-struct must beClone(it is filled through a clone at encode), and anauto_lentarget may carry only positioning directives, no other codec directive.
use bnb::{bin, WireLen};
#[bin(big)]
struct Framed {
#[bw(auto_len = count(items))] // Auto → items.len(); Set(n) → n (a forgery)
len: WireLen<u16>,
#[br(count = len.to_count())]
items: Vec<u8>,
}
let m = Framed { len: WireLen::auto(), items: vec![1, 2, 3] };
assert_eq!(m.to_bytes().unwrap(), [0, 3, 1, 2, 3]); // auto-derived
let forged = Framed { len: WireLen::set(9), items: vec![1, 2, 3] };
assert_eq!(forged.to_bytes().unwrap(), [0, 9, 1, 2, 3]); // the lie is writtenDerivation is checked (a too-long collection is a BitError, never
a truncation), and a WireLen-auto field is optional in the builder (defaults to
auto()), so you never mention it unless forging.
§if — a conditional Option
#[br(if(<cond>))] on an Option<T> reads Some when the condition (over earlier
fields) holds, else None. On write, the Option’s presence drives whether it is
emitted.
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Opt { has_ext: u8, #[br(if(has_ext != 0))] ext: Option<u16> }
let with = Opt { has_ext: 1, ext: Some(0xBEEF) };
let without = Opt { has_ext: 0, ext: None };
assert_eq!(with.to_bytes().unwrap(), [0x01, 0xBE, 0xEF]);
assert_eq!(without.to_bytes().unwrap(), [0x00]);
assert_eq!(Opt::decode_exact(&[0x01, 0xBE, 0xEF]).unwrap(), with);
assert_eq!(Opt::decode_exact(&[0x00]).unwrap(), without);§assert — a decode-time guard
#[br(assert(<expr>))] runs after the field is read (and mapped): if the expression —
over this and any earlier field — is false, decode fails with a position-aware
Convert error naming the field. An optional
message takes format args: assert(<expr>, "fmt {}", args…). Multiple asserts run in
order.
Doctrine note. The parser is permissive by default — it never rejects representable
input. assert is the explicit opt-in for values that are unrepresentable in your
domain (an impossible version, a violated framing invariant) — the same rejection family
as magic, closed enums, and try_map. It is read-only: no bw inverse is needed,
and encode still writes whatever is stored — you can forge what you would not accept
(dual-use). For semantic validity, use struct-level validate (construction-side,
never the parser); for a genuine type conversion, use try_map.
use bnb::bin;
use bnb::bitstream::ErrorKind;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Event {
#[br(assert((1..=2).contains(&version), "unsupported version {}", version))]
version: u8,
id: u16,
}
assert!(Event::decode_exact(&[0x02, 0x00, 0x07]).is_ok());
let err = Event::decode_exact(&[0x09, 0x00, 0x07]).unwrap_err();
assert_eq!(err.field, Some("version"));
assert!(matches!(err.kind, ErrorKind::Convert { .. }));
// Encode is untouched by the guard — forging stays possible:
assert_eq!(Event { version: 9, id: 7 }.to_bytes().unwrap(), [0x09, 0x00, 0x07]);§map / try_map — transform the wire value
#[br(map = <f>)] reads the wire value (its type inferred from f’s argument) and
maps it to the field type; #[bw(map = <f>)] is the inverse on write. Use them to
store a friendly type while the wire keeps a raw encoding.
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Reading {
// wire: u8 biased by 40; stored: signed °C.
#[br(map = |raw: u8| raw as i16 - 40)]
#[bw(map = |c: &i16| (*c + 40) as u8)]
celsius: i16,
}
let r = Reading { celsius: 10 };
assert_eq!(r.to_bytes().unwrap(), [0x32]); // 10 + 40 = 50
assert_eq!(Reading::decode_exact(&[0x32]).unwrap(), r);try_map is the fallible form — the converter returns a Result, and an error
becomes a decode error (no panic):
use bnb::bin;
#[bin(big, read_only)]
#[derive(Debug, PartialEq)]
struct Checked {
#[br(try_map = |raw: u8| if raw < 100 { Ok(raw) } else { Err("out of range") })]
pct: u8,
}
assert_eq!(Checked::decode_exact(&[42]).unwrap().pct, 42);
assert!(Checked::decode_exact(&[200]).is_err()); // converter rejected it§parse_with / write_with — a custom codec escape hatch
When a field’s shape needs arbitrary logic, supply your own functions. parse_with
takes fn(&mut impl Source) -> Result<T, BitError>; write_with takes
fn(&T, &mut impl Sink) -> Result<(), BitError>.
use bnb::{bin, BitError, Sink, Source};
fn read_pascal<S: Source>(r: &mut S) -> Result<String, BitError> {
let len: u8 = r.read()?;
let mut s = String::new();
for _ in 0..len { s.push(r.read::<u8>()? as char); }
Ok(s)
}
fn write_pascal<K: Sink>(s: &String, w: &mut K) -> Result<(), BitError> {
w.write(s.len() as u8)?;
for &b in s.as_bytes() { w.write(b)?; }
Ok(())
}
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Named {
#[br(parse_with = read_pascal)]
#[bw(write_with = write_pascal)]
name: String,
}
let n = Named { name: "Hi".into() };
assert_eq!(n.to_bytes().unwrap(), [0x02, b'H', b'i']);
assert_eq!(Named::decode_exact(&[0x02, b'H', b'i']).unwrap(), n);§Ready-made codecs — bnb::codecs
Before rolling your own, check the shipped library: LEB128 varints, NUL-terminated
C strings, and length-prefixed strings, all referenced by path. (The read_pascal
above is exactly what codecs::prefixed ships — checked
and UTF-8-validated.)
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Packet {
#[br(parse_with = bnb::codecs::leb128::parse)]
#[bw(write_with = bnb::codecs::leb128::write)]
length: u64,
#[br(parse_with = bnb::codecs::prefixed::parse_string::<_, u8>)]
#[bw(write_with = bnb::codecs::prefixed::write_string::<_, u8>)]
name: String,
}
let p = Packet { length: 300, name: "Hi".into() };
assert_eq!(p.to_bytes().unwrap(), [0xAC, 0x02, 0x02, b'H', b'i']);
assert_eq!(Packet::decode_exact(&p.to_bytes().unwrap()).unwrap(), p);§Shared encode state — Sink::scratch
A write_with sees only its own field and the Sink. When a codec needs mutable state
shared across every field of a message — a back-reference / compression dictionary
(e.g. DNS name compression: a repeated name becomes a pointer to the first occurrence) —
attach it to the sink with BitWriter::with_scratch and
reach it via w.scratch() + downcast_mut. The sink is the
one &mut threaded through all fields, so the value is visible to them all; it is dropped on
into_bytes and never written. (Scope the borrow — copy what you need out before calling
w.write.)
use bnb::{bin, BitEncode, BitWriter, Sink, BitError, Source};
// First write of a byte records its offset; a repeat emits a 0xFF marker + that offset.
fn backref<K: Sink>(v: &u8, w: &mut K) -> Result<(), BitError> {
let at = (w.bit_pos() / 8) as u8;
let prior = w.scratch()
.and_then(|s| s.downcast_mut::<std::collections::HashMap<u8, u8>>())
.and_then(|d| match d.get(v).copied() { Some(o) => Some(o), None => { d.insert(*v, at); None } });
match prior { Some(o) => { w.write(0xFFu8)?; w.write(o) } None => w.write(*v) }
}
fn read<S: Source>(r: &mut S) -> Result<u8, BitError> { r.read() }
#[bin(big)]
struct M { #[bw(write_with = backref)] #[br(parse_with = read)] a: u8,
#[bw(write_with = backref)] #[br(parse_with = read)] b: u8 }
let mut w = BitWriter::new().with_scratch(Box::new(std::collections::HashMap::<u8, u8>::new()));
M { a: 7, b: 7 }.bit_encode(&mut w).unwrap();
assert_eq!(w.into_bytes(), [7, 0xFF, 0x00]); // second 7 → back-reference to offset 0§Per-type codecs — #[bin(codec = …)] newtypes
When the same codec applies to many fields, hoist it onto a newtype: a
single-field tuple struct whose wire form is owned by the fn pair. The type then
carries its codec everywhere — fields need no attributes at all (just
#[brw(variable)], below).
use bnb::bin;
/// A LEB128-encoded u64 — annotate once, use as a plain field forever.
#[bin(codec = bnb::codecs::leb128)] // the module's `parse`/`write` pair…
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Varint(pub u64);
// …or any fns: #[bin(codec(parse = <f>, write = <f>))] — turbofish welcome,
// e.g. parse = bnb::codecs::prefixed::parse_string::<_, u16>.
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Frame {
kind: u8,
#[brw(variable)] // a variable-length type in an otherwise-fixed parent
length: Varint, // ← the codec travels with the type
crc: u16,
}
let f = Frame { kind: 1, length: Varint(300), crc: 0xBEEF };
assert_eq!(f.to_bytes().unwrap(), [0x01, 0xAC, 0x02, 0xBE, 0xEF]);
assert_eq!(Frame::decode_exact(&f.to_bytes().unwrap()).unwrap(), f);
assert_eq!(u64::from(Varint(300)), 300); // `From` both ways comes generatedThe newtype gets BitDecode/BitEncode, the slice entry points
(decode_exact/decode_all/to_bytes, at its own declared big/little/
bits — a field of this type decodes through the parent’s cursor), and
From conversions both ways. It emits no FixedBitLen — a codec’s wire form
is assumed variable; a genuinely fixed-width codec adds the one-line manual impl
(see mapping). read_only/write_only narrow the direction
(the paren form may then omit the unneeded fn). For a one-off field, plain
parse_with/write_with stays the right tool.
§#[brw(variable)] — a variable-length field in a fixed parent
A struct with no Vec or codec directives normally derives FixedBitLen by
summing its fields — which fails to compile against a variable-length custom type
(the error names the missing FixedBitLen). #[brw(variable)] declares the truth:
this field’s width isn’t fixed, so the parent never claims to be. It’s harmlessly
redundant on a field that is already variable (a Vec, a directive-bearing field).
§brw(ignore) — a field neither read nor written
#[brw(ignore)] consumes no wire bits: the field is Default::default() on read and
skipped on write. Use it for derived/scratch state you want on the struct but not on
the wire. It is spelled with brw (not br) because it applies to both
directions.
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Parsed { raw: u8, #[brw(ignore)] note: u32 }
let p = Parsed { raw: 7, note: 999 };
assert_eq!(p.to_bytes().unwrap(), [0x07]); // note not written
assert_eq!(Parsed::decode_exact(&[0x07]).unwrap(), Parsed { raw: 7, note: 0 });§reserved / reserved_with — fixed wire bits with a spec value
A reserved field is a normal stored field with a known spec value: the type’s
zero for #[reserved], the given expression for #[reserved_with(<expr>)] (e.g. a
must-be-one pattern). On the default path it reads and writes its actual value —
so you can observe a peer’s reserved bits and override them — while the builder
defaults it to the spec value (so it isn’t required) and the spec_* codecs use the
spec value instead.
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct R {
a: u8,
#[reserved] pad: u8, // spec value 0x00
#[reserved_with(0xFFu8)] ones: u8, // spec value 0xFF
b: u8,
}
// The builder makes the reserved fields optional, defaulting to their spec values.
let r = R::builder().a(1).b(2).build().unwrap();
assert_eq!(r.to_bytes().unwrap(), [0x01, 0x00, 0xFF, 0x02]);
// Decode is verbatim — it captures the actual reserved bits off the wire...
let actual = R::decode_exact(&[0x01, 0x55, 0x55, 0x02]).unwrap();
assert_eq!((actual.pad, actual.ones), (0x55, 0x55));
assert_eq!(actual.to_bytes().unwrap(), [0x01, 0x55, 0x55, 0x02]); // re-emitted as-is
// ...while `to_canonical_bytes` writes the reserved fields' spec values instead.
assert_eq!(actual.to_canonical_bytes().unwrap(), [0x01, 0x00, 0xFF, 0x02]);§pad_* / align_* — forward positioning
#[br(pad_before = <bits>)] / pad_after skip a bit count around a field;
align_before / align_after skip to the next byte boundary. Bit/byte amounts come
from the prelude (1.bytes(), 4.bits()).
use bnb::{bin, prelude::*};
#[bin(big)]
#[derive(Debug, PartialEq)]
struct P { a: u8, #[br(pad_before = 1u32.bytes())] b: u8 }
let p = P { a: 1, b: 2 };
assert_eq!(p.to_bytes().unwrap(), [0x01, 0x00, 0x02]); // one zero pad byte
assert_eq!(P::decode_exact(&[0x01, 0x99, 0x02]).unwrap(), p); // pad skipped on readuse bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct A { flag: bool, #[br(align_before)] val: u8 } // val starts on a byte boundary
let a = A { flag: true, val: 0x2A };
assert_eq!(a.to_bytes().unwrap(), [0x80, 0x2A]); // flag in the high bit, then val
assert_eq!(A::decode_exact(&[0x80, 0x2A]).unwrap(), a);§restore_position — peek without consuming
#[br(restore_position)] reads the field, then rewinds the cursor so later fields
re-read the same bytes (e.g. peek a discriminant, then read the full record). The
field is not re-emitted on write — the overlapping field owns those bytes. It needs
a seekable source, so decode on a forward-only stream is a compile error; the
slice paths (decode_exact/decode_all/peek) always qualify.
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Peeked {
#[br(restore_position)] tag: u8, // peek the first byte...
full: u16, // ...then read it as the high byte of a u16
}
let p = Peeked::decode_exact(&[0xAB, 0xCD]).unwrap();
assert_eq!(p.tag, 0xAB);
assert_eq!(p.full, 0xABCD);
assert_eq!(p.to_bytes().unwrap(), [0xAB, 0xCD]); // `full` emits the bytes; `tag` does not§seek — read at an absolute offset (pointer-following)
#[br(seek = <bits>)] jumps the cursor to an absolute bit offset before reading
the field — the building block for offset tables and pointer chains. Bit/byte amounts
come from the prelude (ptr.bytes(), n.bits()). It is read-side
(the writer is append-only); pair it with restore_position to read at the offset and
return so later fields continue in order. Like restore_position it seeks, so
decode on a forward-only stream is a compile error; the slice paths qualify.
use bnb::{bin, prelude::*};
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Ptr {
ptr: u8, // byte offset of `target`
#[br(seek = ptr.bytes(), restore_position)]
target: u8, // read at `ptr`, then rewind
next: u8, // continues right after `ptr`
}
// `peek` doesn't require full consumption (seek/restore leave the tail untouched).
let p = Ptr::peek(&[0x03, 0x11, 0x22, 0xAB]).unwrap();
assert_eq!((p.ptr, p.target, p.next), (3, 0xAB, 0x11));On encode the seek is a no-op (the writer appends), so a relocated layout won’t
round-trip through the default encoder — emit such formats with write_with /
write_only, where you control placement.
§dbg — trace a field as it decodes
#[br(dbg)] emits a tracing event as the field is read,
carrying its start bit offset and decoded value (the field type must be Debug). It
is a read-side diagnostic — no extra bits are consumed and encode is unaffected. The
event is at TRACE level under the bnb::dbg target, so you can surface just these
with RUST_LOG=bnb::dbg=trace (the application installs the subscriber; libraries
only emit).
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq)]
struct Framed { tag: u8, #[br(dbg)] len: u16 }
// Decoding is identical with or without `dbg`; it just also traces `len`.
let f = Framed::decode_exact(&[0x01, 0x00, 0x2A]).unwrap();
assert_eq!(f, Framed { tag: 1, len: 42 });§try_str — render a byte buffer as a string in Debug
#[try_str] is a rendering hint, not a codec directive: a byte-buffer field (Vec<u8>
/ [u8; N]) prints in Debug as a quoted, escaped string when its bytes are valid
UTF-8, and falls back to hex bytes otherwise — all-or-nothing, never lossy (no �). It
changes nothing on the wire: the field still stores raw bytes (sized by count, etc.), so
the parser stays permissive — a non-UTF-8 value decodes fine, it just renders as bytes.
Debug is what tracing’s ? and {:#?} use, so this is what tidies up log output.
use bnb::bin;
#[bin(big)]
#[derive(Debug, PartialEq, Eq)]
struct Record {
id: u8,
#[br(temp)] #[bw(calc = self.name.len() as u8)] len: u8,
#[br(count = len)] #[try_str] name: Vec<u8>,
}
let text = Record { id: 1, name: b"hi".to_vec() };
assert!(format!("{text:?}").contains(r#"name: "hi""#)); // valid UTF-8 -> "hi"
let bin = Record { id: 2, name: vec![0xC0, 0xDE] };
assert!(format!("{bin:?}").contains("name: [c0, de]")); // not UTF-8 -> hex bytes