bitframe 0.1.1

Macro-driven bit-level packet formats with zero-copy parsing and serialization
Documentation
  • Coverage
  • 100%
    445 out of 445 items documented249 out of 256 items with examples
  • Size
  • Source code size: 38.66 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 45.4 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 26s Average build duration of successful builds.
  • all releases: 26s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • stateruntime/bitframe
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • babilonczyk

bitframe

crates.io docs.rs CI License MSRV

Describe your packet's bit fields in a struct. Get zero-copy parsing for free.

use bitframe::prelude::*;

#[bitframe]
pub struct CcsdsPrimaryHeader {
    pub version:        u3,
    pub is_telecommand: bool,
    pub has_secondary:  bool,
    pub apid:           u11,
    pub seq_flags:      u2,
    pub seq_count:      u14,
    pub pkt_len:        u16,
}

let (header, payload) = CcsdsPrimaryHeaderRef::parse(bytes)?;
assert_eq!(header.apid(), 31_u16);

That's it. You describe the layout. bitframe does the bit math.

Status: v0.1.0 — parsing is implemented. Writers and mutable views are planned for future releases.


The Problem

Bit-packed protocols are everywhere: satellite telemetry, CAN bus, ADS-B, sensor data. Parsing them by hand means shift/mask code that's hard to review and produces silent corruption — not crashes, just quietly wrong values.

// Can you spot the bug? Neither can your reviewer.
let apid = ((bytes[0] as u16 & 0x07) << 8) | bytes[1] as u16;
// Is the mask 0x07 or 0x0F? Is this bits 5-15 or 4-15?
// You won't know until the wrong satellite gets a command.

The Solution

The struct is the spec. If you can read the struct, you understand the protocol.

#[bitframe]
pub struct CcsdsPrimaryHeader {
    pub version:        u3,     // 3 bits   — bits 0..3
    pub is_telecommand: bool,   // 1 bit    — bit 3
    pub has_secondary:  bool,   // 1 bit    — bit 4
    pub apid:           u11,    // 11 bits  — bits 5..16
    pub seq_flags:      u2,     // 2 bits   — bits 16..18
    pub seq_count:      u14,    // 14 bits  — bits 18..32
    pub pkt_len:        u16,    // 16 bits  — bits 32..48
}                               // Total: 48 bits = 6 bytes, verified at compile time

Parsing gives you a zero-copy view — a thin wrapper around &[u8] that reads fields on demand:

// Parse: zero allocation, zero copying
let (header, payload) = CcsdsPrimaryHeaderRef::parse(bytes)?;

// Read fields — they read like English
if header.is_telecommand() { /* ... */ }    // "is this header telecommand?"
if header.has_secondary()  { /* ... */ }    // "does this header have a secondary?"

// Compare directly with numbers — no .value() needed
assert_eq!(header.apid(), 31_u16);
assert_eq!(header.version(), 5_u8);

// If the buffer is too short, you get a clear error — not garbage
// Err(Error::TooShort { needed_bytes: 6, have_bytes: 2 })

Start Fast

cargo add bitframe

Create src/main.rs:

use bitframe::prelude::*;

#[bitframe]
pub struct MyHeader {
    pub tag:    u4,
    pub flags:  u4,
    pub length: u16,
}

fn main() {
    let bytes = [0xA5, 0x00, 0x0A];
    match MyHeaderRef::parse(&bytes) {
        Ok((header, _rest)) => {
            println!("tag={}, flags={}, length={}", header.tag(), header.flags(), header.length());
        }
        Err(e) => eprintln!("parse error: {e}"),
    }
}
cargo run
# tag=10, flags=5, length=10

How It Works

Raw bytes:  [0xA0, 0x1F, 0xC0, 0x42, 0x00, 0x0A]

Byte 0          Byte 1        Byte 2        Byte 3        Byte 4-5
+-----------+---+-----------+----+----------+-------------------+
| ver 3b    |T|S|  APID 11b |SF 2|seq_ct 14b|    pkt_len 16b   |
+-----------+---+-----------+----+----------+-------------------+

CcsdsPrimaryHeaderRef::parse(&bytes) gives you:
  .version()          -> 5         reads bits 0..3
  .is_telecommand()   -> false     reads bit 3
  .has_secondary()    -> false     reads bit 4
  .apid()             -> 31       reads bits 5..16
  .seq_flags()        -> 3        reads bits 16..18
  .seq_count()        -> 66       reads bits 18..32
  .pkt_len()          -> 10       reads bits 32..48

No heap allocation. No struct copying. The view borrows your &[u8] — like &str borrows a String.


Features

Flag Enables Requires
std (default) std::error::Error on errors std
(none) Everything else — views, parsing, errors core only

When to Use bitframe

If you need... Use
Parse variable-length formats (JSON, protobuf, custom TLVs) serde, binrw, nom
In-memory bitfields (register getters/setters on an integer) bilge, modular-bitfield, bitfield-struct
Byte-aligned zero-copy views zerocopy, binary-layout
Bit-level fixed-size headers parsed from &[u8] as a borrowed view bitframe

Real-World Use Cases

Space telemetry (CCSDS) — Every satellite uses 6-byte headers with fields at 3, 1, 1, 11, 2, 14, and 16 bits.

Automotive (CAN bus) — CAN signals live at arbitrary bit positions within 8-byte frames. Manual parsing is a constant source of bugs.

Aviation (ADS-B/ARINC 429) — Aircraft transponder messages are 56 or 112 bits with fields at 5-bit, 3-bit, and 24-bit boundaries. ARINC 429 words pack reversed-bit labels, SDI, data, and parity into 32 bits.

Embedded sensors — ADC readings packed as 12-bit values, status codes as 4-bit nibbles. Self-documenting with bitframe.

Industrial (EtherCAT) — Process Data Images map device I/O to arbitrary bit offsets within shared memory buffers.


What You Can and Cannot Do

Can:

  • Declare fixed-size bit-packed layouts as plain Rust structs
  • Parse zero-copy views from &[u8] with on-demand field access
  • Use on no_std / no_alloc targets (embedded, WASM)
  • Compare bit-sized types directly with integers (u11 == u16)

Roadmap:

  • Encode fields into &mut [u8] with range validation (v0.2)
  • Mutate individual fields in-place via FooRefMut (v0.3)
  • Nest one #[bitframe] struct inside another (v0.3)

Cannot:

  • Parse variable-length or self-describing formats (use deku, binrw, or nom)
  • Replace in-memory register bitfields (use bilge or bitfield-struct)
  • Handle runtime-defined layouts or reflection (fixed at compile time)
  • Parse from streams — bitframe operates on &[u8] slices

Why Rely On It

  • #![forbid(unsafe_code)] on all crates — no unsafe anywhere
  • Zero runtime dependencies — only proc-macro compile-time deps
  • Clippy pedantic + nursery enabled
  • cargo deny enforced — no unmaintained deps, no license issues
  • Tests use behave BDD framework — specs read like protocol documentation
  • Reference implementation test vectors from spacepackets (CCSDS), The 1090MHz Riddle (ADS-B), and SocketCAN (CAN/J1939)
  • MSRV 1.75 tested in CI
  • Dual feature-set CI: --all-features and --no-default-features
  • Roadmap includes Kani formal verification and property-based testing

Documentation

Security

See SECURITY.md for vulnerability reporting.

License

Licensed under the Apache License, Version 2.0. See LICENSE.