μCOBS
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 Pythoncobs - 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 benchto reproduce. Requiresvalgrindinstalled.
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 = ;
let n = encode.unwrap;
assert_eq!;
// Decode reverses it
let mut out = ;
let m = decode.unwrap;
assert_eq!;
Buffer sizing
Use max_encoded_len to determine the required destination buffer size:
let data = ;
let max = max_encoded_len; // 4 bytes
let mut buf = ;
let n = encode.unwrap;
assert_eq!; // fits exactly
If the destination is too small, encode returns None rather than panicking:
let mut tiny = ;
assert_eq!;
Framing for transport
Append a 0x00 sentinel after encoding to delimit frames on a wire. Strip
it before decoding:
let data = ;
// Encode and append sentinel
let mut frame = ;
let n = encode.unwrap;
frame = 0x00;
let wire = &frame;
// Receive: strip sentinel, then decode
let cobs_data = &wire;
let mut out = ;
let m = decode.unwrap;
assert_eq!;
Parsing a stream of frames
Split a byte stream on 0x00 to extract individual COBS frames:
// Two frames separated by 0x00 sentinels
let stream = ;
let mut out = ;
let frames: = stream.split
.filter
.collect;
let n = decode.unwrap;
assert_eq!;
let n = decode.unwrap;
assert_eq!;
Compile-time encoding
encode is const fn, so you can build COBS-encoded tables at compile
time with zero runtime cost:
const PING: = ;
const ACK: = ;
// Baked into the binary — no runtime encoding needed
assert_eq!;
assert_eq!;
Error handling
Both encode and decode return Option<usize> — None on failure,
never panic:
let mut buf = ;
// Destination too small
assert_eq!;
// Malformed input (zero byte in encoded stream)
assert_eq!;
// Truncated frame (code byte promises more data than exists)
assert_eq!;
// Empty input is valid
assert_eq!; // encodes to [0x01]
assert_eq!; // 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:
[]
= { = "0.3", = ["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 Pythoncobspackage - Cross-crate interop — byte-for-byte validation against both
corncobsandcobscrates 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-fuzzharnesses (decode, round-trip, small-dest) for continuous coverage of edge cases - Mutation testing —
cargo-mutantsverifies 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 verification —
constassertions that validate encode correctness at compile time
# Unit + property + interop tests
# All quality gates (tests, clippy, fmt, doc, fuzz, miri, mutants)
# Mutation testing only
License
MIT — Stephen Waits