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).
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]);§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);§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);§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 captures the actual reserved bits; spec_decode reports the expected ones.
let actual = R::decode_exact(&[0x01, 0x55, 0x55, 0x02]).unwrap();
assert_eq!((actual.pad, actual.ones), (0x55, 0x55));
let spec = R::spec_decode_exact(&[0x01, 0x55, 0x55, 0x02]).unwrap();
assert_eq!((spec.pad, spec.ones), (0x00, 0xFF)); // the spec values
assert_eq!(spec.to_spec_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_from on a forward-only stream is a compile error; the
slice paths (decode/peek/decode_exact) 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_from 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 });