8 Commits
Author SHA1 Message Date
swaitsandClaude Opus 4.6 ff883d6f8e docs: update README benchmarks with fresh 7840U numbers and emoji indicators
Replace all N100 benchmark data with accurate 7840U results. Add emoji
legend (winner/loser/tie) to every comparison row. Update crate property
table, machine description, and recommendation prose to reflect current
competitive picture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:30:05 -06:00
swaitsandClaude Opus 4.6 63ebaa0232 docs: document legacy-msrv feature in README and crate docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 00:39:48 -06:00
swaitsandClaude Opus 4.6 f4665a46cb chore: bump version to 0.3.0, add CHANGELOG
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 00:35:32 -06:00
swaitsandClaude Opus 4.6 92021a2608 feat: add legacy-msrv feature for Rust 1.83+ compat
Adds a `legacy-msrv` feature flag that replaces the const
copy_from_slice (Rust 1.93+) with a manual byte loop, allowing
the crate to compile on older toolchains like Xtensa.

All other optimizations (sub-slice scan, zero fast path, decode
batch fill) work on 1.83 without the feature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 00:34:31 -06:00
swaitsandClaude Opus 4.6 adb0db7ee5 perf: major encode/decode optimization via const copy_from_slice and batch zero fill
Encode: rewrite to two-pass (scan + copy_from_slice) leveraging Rust 1.93's
const fn copy_from_slice for memcpy at runtime while keeping const fn encode.
Sub-slice scan pattern helps LLVM eliminate bounds checks. Added zero byte
fast path for all-zeros workloads. Now leads corncobs on nonzero/mixed encode.

Decode: batch consecutive 0x01 codes (all-zeros data) and fill zeros via
memset instead of per-byte loop. Decode zeros throughput jumps from ~1.2 GB/s
to ~4.6 GB/s, now 3x faster than corncobs on that workload.

Bump MSRV 1.83 → 1.93 for const copy_from_slice.
Update dev-deps: cobs 0.3→0.5, criterion 0.5→0.8, rand 0.8→0.10.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 23:00:33 -06:00
swaitsandClaude Opus 4.6 3d674989ab feat: const fn encode, decode optimization, comprehensive docs and examples
- Make `encode` const fn for compile-time COBS encoding (MSRV bumped to 1.83)
- Optimize decode by removing unnecessary branch in inner loop
- Add comprehensive rustdoc with examples for every function
- Add crate-level doc sections: quick start, buffer sizing, framing,
  stream parsing, compile-time encoding, error handling
- Expand README with decision matrix, examples section, testing depth
- Bump version to 0.2.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:31:41 -06:00
swaitsandClaude Opus 4.6 531f5311d8 docs: update README to use just commands instead of cargo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:23:25 -06:00
swaitsandClaude Opus 4.6 a5e3e5f63b feat: add benchmarks, justfile, optimize throughput, harden quality gates
Benchmarking & comparison:
- Add criterion benchmarks (ucobs vs cobs 0.3 vs corncobs 0.1)
- Add Python cobs benchmark script
- Add performance comparison section to README with full results

Performance:
- Rewrite encode to scan-and-bulk-copy via copy_from_slice
- Rewrite decode to bulk-copy via copy_from_slice
- ~2-4x throughput improvement; now matches corncobs on most workloads

Quality hardening:
- Add #![forbid(unsafe_code)], #![warn(missing_docs)], #[must_use]
- Add [lints.rust] unsafe_code = "forbid" in Cargo.toml
- Add rust-version (MSRV) = "1.56"
- Add docs.rs metadata
- 106 tests: 80 unit/corpus + 25 property tests (25k cases each = 625k total)
- External test vectors from: Wikipedia, Cheshire & Baker paper, nanocobs,
  cobs-c, cobs2-rs, Jacques Fortier, Python cobs package
- Add fuzz_small_dest fuzz target (encode/decode with undersized buffers)
- Improve doc examples to show sentinel append/strip convention

Infrastructure:
- Add justfile with test (nextest), check, bench, fuzz, miri, fmt targets
- cargo-nextest for parallel test execution
- Update .gitignore for Python cache files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:19:12 -06:00
12 changed files with 1686 additions and 115 deletions

No files matched your search

+2
View File
@@ -3,3 +3,5 @@
/fuzz/corpus
/fuzz/artifacts
Cargo.lock
__pycache__/
*.pyc
+49
View File
@@ -0,0 +1,49 @@
# Changelog
## 0.3.0 — 2026-03-31
### Performance
- **Encode nonzero/mixed: ~2x faster** — uses const `copy_from_slice` (memcpy at
runtime) via Rust 1.93's stabilization of const slice methods. Sub-slice scan
pattern helps LLVM eliminate bounds checks.
- **Encode zeros: ~50% faster** — fast path skips scan loop overhead for leading
zero bytes.
- **Decode zeros: ~4x faster** — batch consecutive `0x01` codes and fill zeros
via `fill(0)` (memset) instead of per-byte iteration.
- Encode now wins or ties corncobs on nonzero/mixed workloads.
- Decode zeros now leads all competitors by 3x.
### Added
- `legacy-msrv` feature flag — enables compilation on Rust 1.83+ (e.g. Xtensa
toolchain) by replacing const `copy_from_slice` with a manual byte loop.
All other optimizations (sub-slice scan, zero fast path, decode batch fill)
work without this feature on any supported Rust version.
### Changed
- **MSRV bumped from 1.83 to 1.93** — required for const `copy_from_slice`.
Use the `legacy-msrv` feature for older toolchains.
- Dev-dependencies updated: `cobs` 0.3 to 0.5, `criterion` 0.5 to 0.8,
`rand` 0.8 to 0.10.
## 0.2.0 — 2026-03-30
### Added
- `const fn` encode — encode data at compile time with zero runtime cost.
- Comprehensive documentation with 7 usage examples (basic, buffer sizing,
framing, stream parsing, compile-time encoding, error handling).
- Initial encode/decode throughput optimizations.
### Changed
- MSRV set to 1.83.
## 0.1.0 — 2026-03-29
- Initial release.
- `no_std`, zero-alloc, `unsafe`-free COBS encoder and decoder.
- 106 tests: canonical vectors, property-based (proptest), fuzz targets,
cross-crate interop with `corncobs` and `cobs`.
+1 -1
View File
@@ -12,7 +12,7 @@
2. **Interoperable** — proven byte-for-byte compatibility with `corncobs`, `cobs` crates and Python `cobs` packages.
3. **Insanely tested** — canonical vectors, property-based tests (proptest), fuzz targets (cargo-fuzz), cross-crate interop, randomized payloads. If you think testing is done, you're 10% done.
4. **Fast** — benchmark against every competing implementation. Must be at least as fast, ideally fastest. Benchmarks are CI-enforced.
5. **Tiny** — `no_std`, zero-alloc, zero runtime dependencies. ~140 lines of implementation. Runs on 8-bit MCUs and servers alike.
5. **Tiny** — `no_std`, zero-alloc, zero runtime dependencies. ~120 lines of implementation. Runs on 8-bit MCUs and servers alike.
6. **Trustworthy** — small enough to audit by hand. No unsafe. No panics in release. Fuzz-hardened.
## Architecture
+16 -2
View File
@@ -1,7 +1,8 @@
[package]
name = "ucobs"
version = "0.1.0"
version = "0.3.0"
edition = "2021"
rust-version = "1.93"
license = "MIT"
description = "μCOBS — Consistent Overhead Byte Stuffing. no_std, zero-alloc, fastest, most-tested COBS implementation."
authors = ["Stephen Waits <steve@waits.net>"]
@@ -14,8 +15,21 @@ categories = ["encoding", "no-std::no-alloc", "embedded"]
[features]
default = []
legacy-msrv = []
[lints.rust]
unsafe_code = "forbid"
[package.metadata.docs.rs]
all-features = true
[dev-dependencies]
cobs = "0.5"
corncobs = "0.1"
criterion = { version = "0.8", features = ["html_reports"] }
proptest = "1"
rand = "0.8"
rand = "0.10"
[[bench]]
name = "bench"
harness = false
+256 -12
View File
@@ -20,44 +20,288 @@ allowing `0x00` to serve as an unambiguous frame delimiter. Overhead is exactly
- **Insanely tested** — canonical vectors, property-based tests, fuzz targets,
cross-crate interop, randomized payloads, and more coming
## Usage
## Performance
Benchmarked against the two other `no_std` COBS crates using
[Criterion](https://github.com/bheisler/criterion.rs) across three payload
patterns: **all zeros** (worst case — every byte triggers a code emit),
**no zeros**, and **mixed** (0x00–0xFF cycling). Throughput in MB/s, higher
is better. Legend: 🥇 winner, 🥉 last, 🤝 statistical tie (<5%).
### Encode
| Payload | Pattern | μCOBS | `cobs` 0.5 | `corncobs` 0.1 |
|---------|---------|------:|-----------:|---------------:|
| 64 B | zeros | 1,485 | 🥇 1,572 | 🥉 274 |
| 64 B | nonzero | 🤝 3,721 | 🥉 2,177 | 🤝 3,616 |
| 64 B | mixed | 🥇 3,596 | 🥉 2,019 | 3,137 |
| 256 B | zeros | 1,494 | 🥇 1,962 | 🥉 341 |
| 256 B | nonzero | 🥇 4,267 | 🥉 2,218 | 3,951 |
| 256 B | mixed | 🤝 3,891 | 🥉 2,114 | 🤝 3,867 |
| 4096 B | zeros | 1,376 | 🥇 2,052 | 🥉 334 |
| 4096 B | nonzero | 🤝 4,538 | 🥉 2,227 | 🤝 4,501 |
| 4096 B | mixed | 🤝 3,993 | 🥉 1,549 | 🤝 3,888 |
### Decode
| Payload | Pattern | μCOBS | `cobs` 0.5 | `corncobs` 0.1 |
|---------|---------|------:|-----------:|---------------:|
| 64 B | zeros | 🥇 2,896 | 🥉 924 | 1,520 |
| 64 B | nonzero | 🤝 16,000 | 🥉 1,168 | 🤝 15,238 |
| 64 B | mixed | 9,275 | 🥉 1,133 | 🥇 15,610 |
| 256 B | zeros | 🥇 3,088 | 🥉 914 | 1,496 |
| 256 B | nonzero | 40,000 | 🥉 1,179 | 🥇 42,667 |
| 256 B | mixed | 28,444 | 🥉 1,140 | 🥇 37,647 |
| 4096 B | zeros | 🥇 4,620 | 🥉 942 | 1,549 |
| 4096 B | nonzero | 🥇 68,040 | 🥉 1,203 | 58,851 |
| 4096 B | mixed | 40,236 | 🥉 1,154 | 🥇 49,769 |
### Code size
Measured from `.text` section of release-optimized symbols (`encode` + `decode`
combined):
| Crate | `encode` | `decode` | Total |
|----------------|--------:|---------:|------:|
| μCOBS | 434 B | 435 B | 869 B |
| `cobs` 0.5 | 245 B | 292 B | 537 B |
| `corncobs` 0.1 | 375 B | 262 B | 637 B |
All three crates are under 1 KB. μCOBS is the largest due to its
optimized encode (sub-slice scan, zero fast path, `copy_from_slice`)
and decode (batch zero fill) paths.
### 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.
> Measured on AMD Ryzen 7 7840U, Rust 1.93.1, Linux 6.18. Run `just bench`
> to reproduce. Full Criterion reports are generated in `target/criterion/report/`.
## 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 |
| Fastest encode (nonzero/mixed) | **yes** | no | ties μCOBS |
| Fastest encode (zero-heavy) | 2nd | **yes** | no |
| Fastest decode (zero-heavy) | **yes (3×)** | no | 2nd |
| Fastest decode (nonzero) | **ties corncobs** | no | **ties μCOBS** |
| Fastest decode (mixed) | 2nd | no | **yes** |
| 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
competitive throughput across most workloads — especially on embedded targets.
**Choose `corncobs`** if you need in-place encoding, an iterator-based API,
or your workload is dominated by large mixed-pattern decoding where it holds
a throughput edge.
**Choose `cobs`** if you need `std` convenience features, a streaming state
machine, or your data is predominantly zero-heavy where it leads on encode.
## Examples
### Basic encode and decode
```rust
// Encode
// 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
// Decode reverses it
let mut out = [0u8; 16];
let n = ucobs::decode(&[0x02, 0x11, 0x02, 0x33], &mut out).unwrap();
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]);
// Buffer sizing
let max = ucobs::max_encoded_len(256); // 258
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` | `(src: &[u8], dest: &mut [u8]) -> Option<usize>` | COBS-encode `src` into `dest`. Returns byte count or `None` if buffer too small. |
| `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 —
106 tests across 7 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
- **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
cargo test
just test-unit
# Fuzz testing (requires nightly + cargo-fuzz)
cd fuzz
cargo +nightly fuzz run fuzz_decode -- -max_total_time=60
cargo +nightly fuzz run fuzz_roundtrip -- -max_total_time=60
# All quality gates (tests, clippy, fmt, doc, fuzz, miri)
just test
# Fuzz testing only (requires nightly + cargo-fuzz)
just test-fuzz
```
## License
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Benchmark Python cobs encode/decode vs ucobs (reference timing only).
Requires: pip install cobs
"""
import time
def payload_zeros(n: int) -> bytes:
return b"\x00" * n
def payload_nonzero(n: int) -> bytes:
return bytes((i % 255 + 1) for i in range(n))
def payload_mixed(n: int) -> bytes:
return bytes(i % 256 for i in range(n))
def bench(func, data, iterations=10_000):
start = time.perf_counter()
for _ in range(iterations):
func(data)
elapsed = time.perf_counter() - start
return elapsed
def main():
try:
from cobs import cobs as cobs_mod
except ImportError:
print("ERROR: python cobs package not installed. Run: pip install cobs")
return
sizes = [0, 1, 10, 64, 254, 255, 256, 1024, 4096]
payloads = [
("zeros", payload_zeros),
("nonzero", payload_nonzero),
("mixed", payload_mixed),
]
iterations = 10_000
print(f"{'operation':<30} {'size':>6} {'iters':>7} {'total_ms':>10} {'per_op_us':>10} {'MB/s':>10}")
print("-" * 80)
for name, make in payloads:
for size in sizes:
data = make(size)
# Encode
elapsed = bench(cobs_mod.encode, data, iterations)
per_op_us = (elapsed / iterations) * 1_000_000
mbps = (size * iterations / elapsed / 1_000_000) if elapsed > 0 and size > 0 else 0
print(f"{'python-cobs encode/' + name:<30} {size:>6} {iterations:>7} {elapsed*1000:>10.2f} {per_op_us:>10.3f} {mbps:>10.1f}")
# Decode
encoded = cobs_mod.encode(data)
elapsed = bench(cobs_mod.decode, encoded, iterations)
per_op_us = (elapsed / iterations) * 1_000_000
mbps = (size * iterations / elapsed / 1_000_000) if elapsed > 0 and size > 0 else 0
print(f"{'python-cobs decode/' + name:<30} {size:>6} {iterations:>7} {elapsed*1000:>10.2f} {per_op_us:>10.3f} {mbps:>10.1f}")
if __name__ == "__main__":
main()
+153
View File
@@ -0,0 +1,153 @@
use std::hint::black_box;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
// ── Test payloads ───────────────────────────────────────────────────
fn payload_zeros(len: usize) -> Vec<u8> {
vec![0u8; len]
}
fn payload_nonzero(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 255 + 1) as u8).collect()
}
fn payload_mixed(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 256) as u8).collect()
}
// ── ucobs ───────────────────────────────────────────────────────────
fn ucobs_encode(data: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; ucobs::max_encoded_len(data.len()) + 1];
let n = ucobs::encode(data, &mut buf).unwrap();
buf.truncate(n);
buf
}
fn ucobs_decode(encoded: &[u8], out: &mut [u8]) -> usize {
ucobs::decode(encoded, out).unwrap()
}
// ── corncobs ────────────────────────────────────────────────────────
fn corncobs_encode(data: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; corncobs::max_encoded_len(data.len())];
let n = corncobs::encode_buf(data, &mut buf);
buf.truncate(n);
buf
}
fn corncobs_decode(encoded: &[u8], out: &mut [u8]) -> usize {
// corncobs expects trailing sentinel
corncobs::decode_buf(encoded, out).unwrap()
}
// ── cobs crate ──────────────────────────────────────────────────────
fn cobs_encode(data: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; cobs::max_encoding_length(data.len())];
let n = cobs::encode(data, &mut buf);
buf.truncate(n);
buf
}
fn cobs_decode(encoded: &[u8], out: &mut [u8]) -> usize {
// The cobs crate returns DecodeReport on success or Err(EmptyFrame)
// for empty input. We extract parsed_size, treating errors as 0.
cobs::decode(encoded, out)
.map(|report| report.parsed_size())
.unwrap_or(0)
}
// ── Benchmarks ──────────────────────────────────────────────────────
const SIZES: &[usize] = &[0, 1, 10, 64, 254, 255, 256, 1024, 4096];
fn bench_encode(c: &mut Criterion) {
let payloads: Vec<(&str, Box<dyn Fn(usize) -> Vec<u8>>)> = vec![
("zeros", Box::new(payload_zeros)),
("nonzero", Box::new(payload_nonzero)),
("mixed", Box::new(payload_mixed)),
];
for (name, make) in &payloads {
let mut group = c.benchmark_group(format!("encode/{name}"));
for &size in SIZES {
let data = make(size);
group.throughput(Throughput::Bytes(size as u64));
group.bench_with_input(BenchmarkId::new("ucobs", size), &data, |b, d| {
let mut buf = vec![0u8; ucobs::max_encoded_len(d.len()) + 1];
b.iter(|| {
black_box(ucobs::encode(black_box(d), &mut buf).unwrap());
});
});
group.bench_with_input(BenchmarkId::new("corncobs", size), &data, |b, d| {
let mut buf = vec![0u8; corncobs::max_encoded_len(d.len())];
b.iter(|| {
black_box(corncobs::encode_buf(black_box(d), &mut buf));
});
});
group.bench_with_input(BenchmarkId::new("cobs", size), &data, |b, d| {
let mut buf = vec![0u8; cobs::max_encoding_length(d.len())];
b.iter(|| {
black_box(cobs::encode(black_box(d), &mut buf));
});
});
}
group.finish();
}
}
fn bench_decode(c: &mut Criterion) {
let payloads: Vec<(&str, Box<dyn Fn(usize) -> Vec<u8>>)> = vec![
("zeros", Box::new(payload_zeros)),
("nonzero", Box::new(payload_nonzero)),
("mixed", Box::new(payload_mixed)),
];
for (name, make) in &payloads {
let mut group = c.benchmark_group(format!("decode/{name}"));
for &size in SIZES {
let data = make(size);
let ucobs_enc = ucobs_encode(&data);
// corncobs expects sentinel in encoded form
let corncobs_enc = corncobs_encode(&data);
let cobs_enc = cobs_encode(&data);
group.throughput(Throughput::Bytes(size as u64));
group.bench_with_input(BenchmarkId::new("ucobs", size), &ucobs_enc, |b, enc| {
let mut buf = vec![0u8; size + 1];
b.iter(|| {
black_box(ucobs_decode(black_box(enc), &mut buf));
});
});
group.bench_with_input(
BenchmarkId::new("corncobs", size),
&corncobs_enc,
|b, enc| {
let mut buf = vec![0u8; size + 1];
b.iter(|| {
black_box(corncobs_decode(black_box(enc), &mut buf));
});
},
);
group.bench_with_input(BenchmarkId::new("cobs", size), &cobs_enc, |b, enc| {
let mut buf = vec![0u8; size + 1];
b.iter(|| {
black_box(cobs_decode(black_box(enc), &mut buf));
});
});
}
group.finish();
}
}
criterion_group!(benches, bench_encode, bench_decode);
criterion_main!(benches);
+6
View File
@@ -24,3 +24,9 @@ name = "fuzz_roundtrip"
path = "fuzz_targets/fuzz_roundtrip.rs"
test = false
doc = false
[[bin]]
name = "fuzz_small_dest"
path = "fuzz_targets/fuzz_small_dest.rs"
test = false
doc = false
+36
View File
@@ -0,0 +1,36 @@
//! Fuzz target: encode and decode with undersized destination buffers.
//! Must never panic — should return None when dest is too small.
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if data.is_empty() {
return;
}
// Split fuzz input: first byte selects dest size, rest is payload.
let dest_size = data[0] as usize;
let payload = &data[1..];
// Encode with a potentially too-small buffer.
let mut enc_buf = vec![0u8; dest_size];
let enc_result = ucobs::encode(payload, &mut enc_buf);
if let Some(enc_len) = enc_result {
let encoded = &enc_buf[..enc_len];
// Encoded output must never contain 0x00.
assert!(!encoded.contains(&0x00), "encoded contains sentinel");
// Decode with a potentially too-small buffer.
let mut dec_buf = vec![0u8; dest_size];
let dec_result = ucobs::decode(encoded, &mut dec_buf);
if let Some(dec_len) = dec_result {
assert_eq!(
&dec_buf[..dec_len], payload,
"round-trip mismatch with small buffers"
);
}
}
});
+64
View File
@@ -0,0 +1,64 @@
# μCOBS development tasks
# default: list all recipes
default:
@just --list
# run ALL quality gates (tests, clippy, fmt check, doc, fuzz, miri)
test: test-unit test-clippy test-fmt test-doc test-fuzz test-miri
# quick compile check (no tests)
check:
cargo check
# unit + integration + proptest tests (via nextest for parallelism)
test-unit:
cargo nextest run
cargo test --doc
# clippy with all warnings as errors
test-clippy:
cargo clippy -- -D warnings
# check formatting
test-fmt:
cargo fmt -- --check
# doc build (catches broken doc links)
test-doc:
cargo doc --no-deps
# fuzz all targets (60s each, requires nightly + cargo-fuzz)
test-fuzz:
cd fuzz && cargo +nightly fuzz run fuzz_decode -- -max_total_time=60
cd fuzz && cargo +nightly fuzz run fuzz_roundtrip -- -max_total_time=60
cd fuzz && cargo +nightly fuzz run fuzz_small_dest -- -max_total_time=60
# run tests under miri (catches UB, requires nightly + miri component)
# proptest is excluded — its file-persistence layer calls getcwd which miri does not support
test-miri:
cargo +nightly miri test -- --skip proptests
# run criterion benchmarks (ucobs vs cobs vs corncobs)
bench:
cargo bench
# run python cobs benchmarks (requires: pip install cobs)
bench-python:
python3 bench_python.py
# run all benchmarks (rust + python)
bench-all: bench bench-python
# format code
fmt:
cargo fmt
# build in release mode
build:
cargo build --release
# clean all build artifacts
clean:
cargo clean
rm -rf fuzz/target fuzz/corpus fuzz/artifacts
+10
View File
@@ -0,0 +1,10 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 00411472de4d6cdb296c04bc52fd533354bc25ba1765807da8b7994868fcb987 # shrinks to len = 1
cc 81ebf688f1dd5ea2a735756a7fb703b92c2a2033057ae91886577c6a63763cf5 # shrinks to data = [0]
cc 32d0d2d26e2be6ec66b432f1e9baee378777b1a00b959922cff7a5784292ec4b # shrinks to data = [0]
cc fe447e0e58f4c0b21f6e1b38a90bd33578c968967c956964a06013a4df977d68 # shrinks to data = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0]
+1026 -100
View File
File diff suppressed because it is too large. Load diff