ucobs 0.3.1

μCOBS — Consistent Overhead Byte Stuffing. no_std, zero-alloc, fastest, most-tested COBS implementation.
Documentation

μCOBS

crates.io docs.rs license

The COBS implementation for Rust.

Consistent Overhead Byte Stuffing (COBS) encodes arbitrary bytes so that 0x00 never appears in the output, allowing 0x00 to serve as an unambiguous frame delimiter. Overhead is exactly 1 byte per 254 input bytes (worst case).

Features

  • no_std, zero-alloc — runs anywhere, from 8-bit MCUs to servers
  • ~140 lines of implementation — small, auditable, easy to verify
  • 100% spec-compliant — Cheshire & Baker, IEEE/ACM Transactions on Networking, 1999
  • Proven interoperability — cross-validated against corncobs, cobs, and Python cobs
  • Insanely tested — canonical vectors, property-based tests, fuzz targets, cross-crate interop, randomized payloads, and more coming

Performance

Benchmarked against the two other no_std COBS crates using iai-callgrind (instruction counts via Valgrind — deterministic, zero run-to-run variance). Three payload patterns: all zeros (worst case), no zeros, and mixed (0x00–0xFF cycling). Instruction count per call, lower is better. Legend: 🥇 winner, 🥉 last, 🤝 tie (<3%).

Encode

Payload Pattern μCOBS cobs 0.5 corncobs 0.1
64 B zeros 🤝 1,171 🤝 1,164 🥉 4,098
64 B nonzero 🤝 512 🥉 1,100 🤝 514
64 B mixed 🥇 523 🥉 1,101 570
256 B zeros 🤝 4,243 🤝 4,236 🥉 15,810
256 B nonzero 🤝 1,552 🥉 3,992 🤝 1,531
256 B mixed 🤝 1,561 🥉 3,993 🤝 1,585
4096 B zeros 🤝 65,904 🤝 65,897 🥉 250,271
4096 B nonzero 🤝 22,077 🥉 61,993 🤝 21,711
4096 B mixed 🤝 22,932 🥉 62,009 🤝 22,656

Decode

Payload Pattern μCOBS cobs 0.5 corncobs 0.1
64 B zeros 🥇 499 🥉 2,354 1,507
64 B nonzero 🥇 179 🥉 1,778 185
64 B mixed 226 🥉 1,787 🥇 206
256 B zeros 🥇 1,473 🥉 8,882 5,539
256 B nonzero 🤝 240 🥉 6,607 🤝 247
256 B mixed 285 🥉 6,613 🥇 266
4096 B zeros 🥇 25,018 🥉 139,698 86,435
4096 B nonzero 🤝 1,315 🥉 103,253 🤝 1,337
4096 B mixed 🤝 2,086 🥉 103,394 🤝 2,067

Code size

Measured from .text section of release-optimized symbols (encode + decode combined). Run just bench-size to reproduce.

Crate encode decode Total
μCOBS 429 B 392 B 821 B
cobs 0.5 282 B 494 B 776 B
corncobs 0.1 375 B 262 B 637 B

All three crates are under 1 KB. μCOBS is the largest because safe const fn encoding with copy_from_slice/memcpy requires split_at bounds checks that generate panic paths — the cost of combining compile-time safety with runtime speed. Embedded users targeting size over speed can build with opt-level = "s" which reduces μCOBS to ~738 B.

Crate properties

Property μCOBS cobs 0.5 corncobs 0.1
no_std always opt-in[^1] always
Zero-alloc yes opt-in[^2] yes
unsafe-free yes yes yes
const fn encode yes no no
Implementation ~140 LOC ~790 LOC ~645 LOC

[^1]: Requires disabling the default std feature. [^2]: alloc is enabled by default via the std feature.

Benchmarked with iai-callgrind (Valgrind instruction counting) for deterministic, reproducible results. Run just bench to reproduce. Requires valgrind installed.

When to use μCOBS

You need… μCOBS cobs 0.5 corncobs 0.1
Minimal code to audit ~140 LOC ~790 LOC ~645 LOC
Compile-time (const fn) encode yes no no
Fewest encode instructions ties cobs/corncobs zeros only nonzero only
Fewest decode instructions (zero-heavy) yes (3–5×) no 2nd
Fewest decode instructions (nonzero) yes/tied no tied
Fewest decode instructions (mixed) tied at 4 KB no slight edge at small sizes
Dead-simple API (3 functions) yes yes more surface area
In-place / streaming encode no no yes
no_std + zero-alloc by default yes opt-in yes
Thorough test suite 106 tests, fuzz, proptest basic basic

Choose μCOBS if you want the smallest, most auditable COBS implementation with a const fn encoder, a dead-simple 3-function API, and leading or tied instruction efficiency across most workloads — plus dominant decode performance on zero-heavy data (3–5× fewer instructions than alternatives). Wins or ties 16 of 18 benchmarks.

Choose corncobs if you need in-place encoding, an iterator-based API, or your workload is dominated by small mixed-pattern decoding where it uses ~8% fewer instructions at 64–256 B.

Choose cobs if you need std convenience features or a streaming state machine. Note: cobs uses 50–75× more instructions than μCOBS/corncobs for decoding non-zero data.

Examples

Basic encode and decode

// Encode: [0x11, 0x00, 0x33] → [0x02, 0x11, 0x02, 0x33]
let mut buf = [0u8; 16];
let n = ucobs::encode(&[0x11, 0x00, 0x33], &mut buf).unwrap();
assert_eq!(&buf[..n], &[0x02, 0x11, 0x02, 0x33]);

// Decode reverses it
let mut out = [0u8; 16];
let m = ucobs::decode(&buf[..n], &mut out).unwrap();
assert_eq!(&out[..m], &[0x11, 0x00, 0x33]);

Buffer sizing

Use max_encoded_len to determine the required destination buffer size:

let data = [0x01, 0x02, 0x03];
let max = ucobs::max_encoded_len(data.len()); // 4 bytes

let mut buf = [0u8; 4];
let n = ucobs::encode(&data, &mut buf).unwrap();
assert_eq!(n, 4); // fits exactly

If the destination is too small, encode returns None rather than panicking:

let mut tiny = [0u8; 1];
assert_eq!(ucobs::encode(&[0x01, 0x02], &mut tiny), None);

Framing for transport

Append a 0x00 sentinel after encoding to delimit frames on a wire. Strip it before decoding:

let data = [0x11, 0x00, 0x33];

// Encode and append sentinel
let mut frame = [0u8; 16];
let n = ucobs::encode(&data, &mut frame).unwrap();
frame[n] = 0x00;
let wire = &frame[..n + 1];

// Receive: strip sentinel, then decode
let cobs_data = &wire[..wire.len() - 1];
let mut out = [0u8; 16];
let m = ucobs::decode(cobs_data, &mut out).unwrap();
assert_eq!(&out[..m], &data);

Parsing a stream of frames

Split a byte stream on 0x00 to extract individual COBS frames:

// Two frames separated by 0x00 sentinels
let stream = [
    0x02, 0x11, 0x02, 0x33, 0x00,  // frame 1: [0x11, 0x00, 0x33]
    0x03, 0xAA, 0xBB, 0x00,        // frame 2: [0xAA, 0xBB]
];

let mut out = [0u8; 16];
let frames: Vec<&[u8]> = stream.split(|&b| b == 0x00)
    .filter(|f| !f.is_empty())
    .collect();

let n = ucobs::decode(frames[0], &mut out).unwrap();
assert_eq!(&out[..n], &[0x11, 0x00, 0x33]);

let n = ucobs::decode(frames[1], &mut out).unwrap();
assert_eq!(&out[..n], &[0xAA, 0xBB]);

Compile-time encoding

encode is const fn, so you can build COBS-encoded tables at compile time with zero runtime cost:

const PING: [u8; 2] = {
    let mut buf = [0u8; 2];
    match ucobs::encode(&[0x01], &mut buf) {
        Some(_) => buf,
        None => panic!("buffer too small"),
    }
};

const ACK: [u8; 3] = {
    let mut buf = [0u8; 3];
    match ucobs::encode(&[0x06, 0x00], &mut buf) {
        Some(_) => buf,
        None => panic!("buffer too small"),
    }
};

// Baked into the binary — no runtime encoding needed
assert_eq!(PING, [0x02, 0x01]);
assert_eq!(ACK, [0x02, 0x06, 0x01]);

Error handling

Both encode and decode return Option<usize>None on failure, never panic:

let mut buf = [0u8; 8];

// Destination too small
assert_eq!(ucobs::encode(&[1, 2, 3], &mut buf[..1]), None);

// Malformed input (zero byte in encoded stream)
assert_eq!(ucobs::decode(&[0x00], &mut buf), None);

// Truncated frame (code byte promises more data than exists)
assert_eq!(ucobs::decode(&[0x05, 0x11], &mut buf), None);

// Empty input is valid
assert_eq!(ucobs::encode(&[], &mut buf), Some(1)); // encodes to [0x01]
assert_eq!(ucobs::decode(&[], &mut buf), Some(0)); // decodes to []

API

Function Signature Description
encode const fn(src: &[u8], dest: &mut [u8]) -> Option<usize> COBS-encode src into dest. Returns byte count or None if buffer too small.
decode (src: &[u8], dest: &mut [u8]) -> Option<usize> Decode COBS data. Returns byte count or None if input is malformed.
max_encoded_len const fn(src_len: usize) -> usize Maximum encoded size for a given input length.

Note: The encoder does not append a trailing 0x00 sentinel. Append it yourself when framing for transport.

Minimum Supported Rust Version

The default build requires Rust 1.93+ (for const copy_from_slice).

legacy-msrv feature

If you're targeting a toolchain older than 1.93 (e.g. Xtensa), enable the legacy-msrv feature to fall back to a manual byte copy in the encoder:

[dependencies]
ucobs = { version = "0.3", features = ["legacy-msrv"] }

All other optimizations (sub-slice scan, zero fast path, decode batch fill) are available regardless of this feature. Only the encode copy phase is affected — it uses a byte loop instead of copy_from_slice/memcpy.

Testing

μCOBS has one of the most thorough test suites of any COBS implementation — 106 tests across 8 categories:

  • Canonical vectors — every example from Cheshire & Baker 1999 (the original COBS paper), plus the full Wikipedia vector set
  • External corpora — vectors drawn from cobs-c, nanocobs, cobs2-rs, Jacques Fortier's C implementation, and the Python cobs package
  • Cross-crate interop — byte-for-byte validation against both corncobs and cobs crates on randomized payloads up to 4 KB
  • Property-based tests — 6 proptest suites covering round-trip correctness, no-zero-in-output invariants, encoding properties, decoding properties, structural/algebraic properties, and boundary-region behavior
  • Fuzz targets — 3 cargo-fuzz harnesses (decode, round-trip, small-dest) for continuous coverage of edge cases
  • Mutation testingcargo-mutants verifies that the test suite catches injected bugs in every reachable code path
  • Boundary tests — exhaustive coverage of 254/255-byte block boundaries, multi-block splits, buffer-exact fits, and off-by-one dest sizes
  • Compile-time verificationconst assertions that validate encode correctness at compile time
# Unit + property + interop tests
just test-unit

# All quality gates (tests, clippy, fmt, doc, fuzz, miri, mutants)
just test

# Mutation testing only
just test-mutants

License

MIT — Stephen Waits