Compare commits
3
Commits
v0.1.0
...
3d674989ab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d674989ab
|
||
|
|
531f5311d8
|
||
|
|
a5e3e5f63b
|
No files matched your search
@@ -3,3 +3,5 @@
|
|||||||
/fuzz/corpus
|
/fuzz/corpus
|
||||||
/fuzz/artifacts
|
/fuzz/artifacts
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
2. **Interoperable** — proven byte-for-byte compatibility with `corncobs`, `cobs` crates and Python `cobs` packages.
|
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.
|
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.
|
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.
|
6. **Trustworthy** — small enough to audit by hand. No unsafe. No panics in release. Fuzz-hardened.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|||||||
+14
-1
@@ -1,7 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ucobs"
|
name = "ucobs"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
rust-version = "1.83"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "μCOBS — Consistent Overhead Byte Stuffing. no_std, zero-alloc, fastest, most-tested COBS implementation."
|
description = "μCOBS — Consistent Overhead Byte Stuffing. no_std, zero-alloc, fastest, most-tested COBS implementation."
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
@@ -15,7 +16,19 @@ categories = ["encoding", "no-std::no-alloc", "embedded"]
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
cobs = "0.3"
|
||||||
corncobs = "0.1"
|
corncobs = "0.1"
|
||||||
|
criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "bench"
|
||||||
|
harness = false
|
||||||
@@ -20,28 +20,229 @@ allowing `0x00` to serve as an unambiguous frame delimiter. Overhead is exactly
|
|||||||
- **Insanely tested** — canonical vectors, property-based tests, fuzz targets,
|
- **Insanely tested** — canonical vectors, property-based tests, fuzz targets,
|
||||||
cross-crate interop, randomized payloads, and more coming
|
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.
|
||||||
|
|
||||||
|
### Encode
|
||||||
|
|
||||||
|
| Payload | Pattern | μCOBS | `cobs` 0.3 | `corncobs` 0.1 |
|
||||||
|
|---------|---------|------:|-----------:|---------------:|
|
||||||
|
| 64 B | zeros | 595 | 748 | 180 |
|
||||||
|
| 64 B | mixed | 1,853 | 1,011 | 1,734 |
|
||||||
|
| 64 B | nonzero | 1,884 | 1,012 | 2,110 |
|
||||||
|
| 256 B | zeros | 654 | 817 | 184 |
|
||||||
|
| 256 B | mixed | 1,888 | 1,002 | 1,887 |
|
||||||
|
| 256 B | nonzero | 1,964 | 1,004 | 1,968 |
|
||||||
|
| 4096 B | zeros | 675 | 845 | 188 |
|
||||||
|
| 4096 B | mixed | 1,734 | 1,016 | 1,746 |
|
||||||
|
| 4096 B | nonzero | 1,775 | 1,020 | 2,245 |
|
||||||
|
|
||||||
|
### Decode
|
||||||
|
|
||||||
|
| Payload | Pattern | μCOBS | `cobs` 0.3 | `corncobs` 0.1 |
|
||||||
|
|---------|---------|------:|-----------:|---------------:|
|
||||||
|
| 64 B | zeros | 595 | 440 | 732 |
|
||||||
|
| 64 B | mixed | 7,041 | 504 | 7,991 |
|
||||||
|
| 64 B | nonzero | 9,900 | 502 | 9,807 |
|
||||||
|
| 256 B | zeros | 656 | 472 | 742 |
|
||||||
|
| 256 B | mixed |17,024 | 645 | 16,207 |
|
||||||
|
| 256 B | nonzero |18,456 | 647 | 19,549 |
|
||||||
|
| 4096 B | zeros | 675 | 483 | 768 |
|
||||||
|
| 4096 B | mixed |17,522 | 656 | 22,959 |
|
||||||
|
| 4096 B | nonzero |37,987 | 623 | 40,005 |
|
||||||
|
|
||||||
|
### Code size
|
||||||
|
|
||||||
|
Measured from `.text` section of release-optimized symbols (`encode` + `decode`
|
||||||
|
combined):
|
||||||
|
|
||||||
|
| Crate | Code size |
|
||||||
|
|----------------|----------:|
|
||||||
|
| μCOBS | 541 B |
|
||||||
|
| `cobs` 0.3 | 537 B |
|
||||||
|
| `corncobs` 0.1 | 637 B |
|
||||||
|
|
||||||
|
All three crates are tiny. The difference is negligible for any target.
|
||||||
|
|
||||||
|
### Crate properties
|
||||||
|
|
||||||
|
| Property | μCOBS | `cobs` 0.3 | `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 | ~120 LOC | ~790 LOC | ~645 LOC |
|
||||||
|
|
||||||
|
[^1]: Requires disabling the default `std` feature.
|
||||||
|
[^2]: `alloc` is enabled by default via the `std` feature.
|
||||||
|
|
||||||
|
> Measured on Intel N100, Rust 1.93.1, Linux 6.12. Run `just bench` to
|
||||||
|
> reproduce. Full Criterion reports are generated in `target/criterion/report/`.
|
||||||
|
|
||||||
|
## When to use μCOBS
|
||||||
|
|
||||||
|
| You need… | μCOBS | `cobs` 0.3 | `corncobs` 0.1 |
|
||||||
|
|---|:---:|:---:|:---:|
|
||||||
|
| Minimal code to audit | **~120 LOC** | ~790 LOC | ~645 LOC |
|
||||||
|
| Compile-time (`const fn`) encode | **yes** | no | no |
|
||||||
|
| Fastest encode (zero-heavy data) | **3–4×** faster | baseline | slowest |
|
||||||
|
| Fastest decode (large payloads) | competitive | slow | **slight edge** |
|
||||||
|
| 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 minimal API and strong performance — especially on embedded targets or
|
||||||
|
payloads containing many zero bytes.
|
||||||
|
|
||||||
|
**Choose `corncobs`** if you need in-place encoding, an iterator-based API, or
|
||||||
|
your workload is dominated by large non-zero payloads where it holds a slight
|
||||||
|
throughput edge.
|
||||||
|
|
||||||
|
**Choose `cobs`** if you need `std` convenience features and don't care about
|
||||||
|
`no_std` or encode performance.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic encode and decode
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Encode
|
// Encode: [0x11, 0x00, 0x33] → [0x02, 0x11, 0x02, 0x33]
|
||||||
let mut buf = [0u8; 16];
|
let mut buf = [0u8; 16];
|
||||||
let n = ucobs::encode(&[0x11, 0x00, 0x33], &mut buf).unwrap();
|
let n = ucobs::encode(&[0x11, 0x00, 0x33], &mut buf).unwrap();
|
||||||
assert_eq!(&buf[..n], &[0x02, 0x11, 0x02, 0x33]);
|
assert_eq!(&buf[..n], &[0x02, 0x11, 0x02, 0x33]);
|
||||||
|
|
||||||
// Decode
|
// Decode reverses it
|
||||||
let mut out = [0u8; 16];
|
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]);
|
assert_eq!(&out[..n], &[0x11, 0x00, 0x33]);
|
||||||
|
|
||||||
// Buffer sizing
|
let n = ucobs::decode(frames[1], &mut out).unwrap();
|
||||||
let max = ucobs::max_encoded_len(256); // 258
|
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
|
## API
|
||||||
|
|
||||||
| Function | Signature | Description |
|
| 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. |
|
| `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. |
|
| `max_encoded_len` | `const fn(src_len: usize) -> usize` | Maximum encoded size for a given input length. |
|
||||||
|
|
||||||
@@ -50,14 +251,34 @@ yourself when framing for transport.
|
|||||||
|
|
||||||
## Testing
|
## 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
|
```sh
|
||||||
# Unit + property + interop tests
|
# Unit + property + interop tests
|
||||||
cargo test
|
just test-unit
|
||||||
|
|
||||||
# Fuzz testing (requires nightly + cargo-fuzz)
|
# All quality gates (tests, clippy, fmt, doc, fuzz, miri)
|
||||||
cd fuzz
|
just test
|
||||||
cargo +nightly fuzz run fuzz_decode -- -max_total_time=60
|
|
||||||
cargo +nightly fuzz run fuzz_roundtrip -- -max_total_time=60
|
# Fuzz testing only (requires nightly + cargo-fuzz)
|
||||||
|
just test-fuzz
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
use criterion::{black_box, 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 {
|
||||||
|
// unwrap_or(0): the cobs crate returns Err(EmptyFrame) for empty input,
|
||||||
|
// which is valid COBS. We treat it as 0 decoded bytes for benchmarking.
|
||||||
|
cobs::decode(encoded, out).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);
|
||||||
@@ -24,3 +24,9 @@ name = "fuzz_roundtrip"
|
|||||||
path = "fuzz_targets/fuzz_roundtrip.rs"
|
path = "fuzz_targets/fuzz_roundtrip.rs"
|
||||||
test = false
|
test = false
|
||||||
doc = false
|
doc = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_small_dest"
|
||||||
|
path = "fuzz_targets/fuzz_small_dest.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
@@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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
|
||||||
+969
-101
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user