Files
ucobs/README.md
T
swaitsandClaude Opus 4.6 f805386bb8
CI / Test (1.93) (push) Canceled after 0s
CI / Test (stable) (push) Canceled after 0s
CI / Test (1.83, legacy-msrv) (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Format (push) Canceled after 0s
CI / Doc (push) Canceled after 0s
CI / Cargo Deny (push) Canceled after 0s
chore: audit cleanup, CI, cargo-deny, encode fuzz target (0.3.4)
Simplify encoder exit branch (−41 B code size), add GitHub Actions CI
with MSRV matrix, cargo-deny with permissive-only license policy,
encode-only fuzz target, and feature coverage across test gates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:01:19 -06:00

319 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# μCOBS
[![crates.io](https://img.shields.io/crates/v/ucobs.svg)](https://crates.io/crates/ucobs)
[![docs.rs](https://docs.rs/ucobs/badge.svg)](https://docs.rs/ucobs)
[![license](https://img.shields.io/crates/l/ucobs.svg)](LICENSE)
**The** COBS implementation for Rust.
[Consistent Overhead Byte Stuffing](https://en.wikipedia.org/wiki/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 at most
1 byte per 254 input bytes, plus 1.
## 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, miri, mutation testing, randomized payloads
## Performance
Benchmarked against the two other `no_std` COBS crates using
[iai-callgrind](https://github.com/iai-callgrind/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 | 388 B | 392 B | 780 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 and `cobs` are nearly tied;
`corncobs` is the smallest. μCOBS's 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"` to shrink further.
### 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 | **111 tests, fuzz, proptest, miri, mutants** | 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
```rust
// 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:
```rust
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:
```rust
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:
```rust
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:
```rust
// 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:
```rust
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:
```rust
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:
```toml
[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 —
111 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 testing** — `cargo-mutants` with 100% kill rate (59/59 viable
mutants caught; 30 more rejected by the type system)
- **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** — `const` assertions that validate encode
correctness at compile time
```sh
# 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](LICENSE) — Stephen Waits