bnb
An owned, bit-aware binary codec: ergonomic, fast bit/byte field types plus a
unified #[bin] whole-message macro for binary protocols. No external codec
dependency — bnb is self-contained.
Published on crates.io as bitsandbytes; import it as bnb:
bnb = { package = "bitsandbytes", version = "0.1" }. Docs: https://docs.rs/bitsandbytes.
bnb collapses a stack of overlapping helpers — modular-bitfield(-msb),
bitfield-struct, bitbybit, arbitrary-int, num_enum, and a binrw-style
codec — into one crate that is:
- Integer-backed and fast — bitfields are plain shift/mask on a single backing
integer (no
bitvec); the stream codec reads/writes at arbitrary bit offsets. - Explicit about ordering — independent control of bit order (MSB/LSB-first) and byte order (big/little), exactly what protocol layouts need.
- Dual-use by default — the guided path is RFC-correct, but parsers stay permissive (unknown enum values are preserved as a catch-all, never rejected), so fuzzing / red-teaming / interop testing can emit deliberately non-conformant data.
What's in it
| Item | Replaces | Purpose |
|---|---|---|
u1..u127 (UInt<T, N>) |
arbitrary-int |
sub-byte unsigned integers |
#[bitfield] |
modular-bitfield(-msb), bitbybit, bitfield-struct |
pack typed fields into one integer |
#[derive(BitEnum)] |
num_enum, bitbybit::bitenum |
enum ⇄ integer with an optional catch-all |
#[bitflags] |
bitflags |
named single-bit flag sets with set algebra |
#[derive(BitsBuilder)] |
derive_builder (for bit/byte structs) |
required-by-default builder |
#[bin] |
a hand-written codec | whole-message codec: magic/count/ctx/map/if/calc/reserved/positioning/validate |
Bitfields, enums, flags
use ;
// MSB-first packing (network order), big-endian on the wire.
let s = new
.with_opcode
.with_rcode;
assert_eq!;
assert_eq!;
A byte-aligned #[derive(BitEnum)] also gets num_enum-parity conversions
(From<Enum> for uN always; From<uN>/TryFrom<uN> depending on the catch-all),
so a magic-byte enum needs no hand-written From impl or round-trip test.
#[bitflags] packs named single-bit flags into one integer with full set algebra;
a flag set implements Bits, so it nests in a #[bitfield] and in a #[bin]
message:
use bitflags;
let f = SYN | ACK;
assert!;
assert!; // per-flag accessor
The #[bin] whole-message codec
#[bin] folds the read/write codec and a required-by-default builder over one
struct. It reads/writes fields at arbitrary bit offsets, so the same attribute
handles byte-aligned headers and sub-byte frames. A field that is another
#[bitfield]/BitEnum/#[bitflags] nests with no glue.
use ;
// Derived & framed: `len` is never stored — it is read into a temp that drives the
// Vec, and recomputed from the data on write, so it can't drift.
let header = builder.id.flags
.qdcount.ancount.nscount.arcount.build?; // Err names any unset field
let bytes = header.to_bytes?; // -> 12 bytes
let parsed = decode_exact?; // exact inverse
The generated API per #[bin] type: decode (over a Source cursor) / decode_all /
decode_iter / peek / decode_exact to read; to_bytes / encode (to any io::Write) /
BitEncode::bit_encode (to a Sink) to write; and Type::builder() / Type::new(…). See
examples/bin_message.rs for a complete, runnable DNS-header +
framed-payload round-trip.
Directives & I/O
- Struct-level:
big/little,bit_order = msb|lsb,read_only/write_only,no_builder,forward_only,magic = <expr>,ctx(name: Ty, …),validate = <path>. - Field-level (
#[br]/#[bw]):count,ctx { … },temp+calc,if(…),map/try_map(+ inversebw(map)),parse_with/write_with,ignore,pad_before/after/align_before/after/restore_position,#[reserved]/#[reserved_with(…)], and#[try_str](aDebug-rendering hint — a byte buffer prints as a string when valid UTF-8, else hex). - I/O ladder: decode from a
&[u8]slice (BitReader), a forwardRead(StreamBitReader), a bounded retain-and-seek socket adapter (BufSource), a pushable in-memory buffer (BitBuf), or aRead + Seekfile (SeekReader); with the opt-inbytesfeature, the zero-copyBytesReader/BytesWriter.
validate gates build() only — the parser stays permissive (it never rejects
representable input, per the dual-use rule), so deliberately malformed messages are
still decodable.
Bit order vs. byte order
bits = msb | lsb(defaultmsb): does the first declared field land in the high or low bits of the backing integer.bytes = be | le(defaultbe): byte order of the backing integer on the wire.
These are independent knobs — the whole point. MSB-first big-endian matches the ASCII-art layouts in RFCs.
Field widths
In order of precedence: an explicit #[bits(N)]; an explicit #[bits(A..=B)]
range (which fixes the absolute offset — the manual layout escape hatch); or, by
default, <FieldType as bnb::Bits>::BITS. Use inference/widths for automatic
layout, or ranges on every field for fully manual layout — the two styles cannot be
mixed in one struct.
Crate layout
bnb (this crate, the runtime types) re-exports the macros from bnb-macros (the
proc-macro crate). Depend only on bnb. The optional bytes feature adds the
bytes-crate I/O adapters; the core is otherwise dependency-light.
See DESIGN.md for the design rationale and ROADMAP.md
for the capability/status summary.
Inspiration
bnb collapses the capabilities of several excellent crates into one. The
arbitrary-width integers echo arbitrary-int; the bitfield packing echoes
modular-bitfield, bitfield-struct, and bitbybit; the enum ⇄ integer
mapping echoes num_enum; and — most of all — the declarative, bidirectional
codec and its #[br]/#[bw] attribute vocabulary are modeled on
binrw, so the two feel like one toolkit.
bnb shares no code with any of them — it is a from-scratch implementation,
extended to do the one thing a byte-oriented Read + Seek codec cannot: read and
write fields at arbitrary bit offsets. See ACKNOWLEDGMENTS.md
for the full credit.