Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee40455c5f
|
||
|
|
1eb7e09412
|
||
|
|
f805386bb8
|
||
|
|
f6778e20b8
|
||
|
|
aa09783cdd
|
||
|
|
b91fbb2ed9
|
||
|
|
777a37d4cd
|
No files matched your search
@@ -0,0 +1,18 @@
|
||||
# Use nextest for faster test execution
|
||||
test_tool = "nextest"
|
||||
|
||||
# Skip slow proptests during mutation testing — the 60+ unit/interop tests
|
||||
# are sufficient to catch mutants. Full proptests run separately via just test-unit.
|
||||
additional_cargo_test_args = ["-E", "not test(proptests)"]
|
||||
|
||||
# False positives excluded:
|
||||
#
|
||||
# - `> 0` → `>= 0` or `< 0` on usize: tautological (usize always >= 0)
|
||||
# - `< 254` → `<= 254` in max_run: both branches yield 254 at the boundary
|
||||
#
|
||||
# The legacy-msrv const_copy is excluded via #[mutants::skip] in source.
|
||||
exclude_re = [
|
||||
"replace > with < in encode",
|
||||
"replace > with >= in encode",
|
||||
"replace < with <= in encode",
|
||||
]
|
||||
@@ -0,0 +1,69 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test (${{ matrix.rust }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
rust: [stable, "1.93"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
- run: cargo test
|
||||
|
||||
test-legacy-msrv:
|
||||
name: Test (1.83, legacy-msrv)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "1.83"
|
||||
- run: cargo test --features legacy-msrv
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
fmt:
|
||||
name: Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- run: cargo fmt -- --check
|
||||
|
||||
doc:
|
||||
name: Doc
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo doc --no-deps
|
||||
|
||||
deny:
|
||||
name: Cargo Deny
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
@@ -2,6 +2,8 @@
|
||||
/fuzz/target
|
||||
/fuzz/corpus
|
||||
/fuzz/artifacts
|
||||
/mutants.out
|
||||
/mutants.out.old
|
||||
Cargo.lock
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -1,5 +1,78 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.4 — 2026-04-01
|
||||
|
||||
### Changed
|
||||
|
||||
- **Encode shrunk by 41 bytes** — simplified branch structure in encoder exit
|
||||
path. 388 B encode, 392 B decode, 780 B total (was 821 B). Zero benchmark
|
||||
regressions.
|
||||
- **Clarified decode invariant** — added inline comment explaining the subtle
|
||||
trailing-0x01 COBS rule (end-of-data marker vs zero byte).
|
||||
- **Clippy cleanup** — `saturating_sub` in proptest (cosmetic, test-only).
|
||||
|
||||
### Added
|
||||
|
||||
- **GitHub Actions CI** — tests on stable + MSRV 1.93 + 1.83/legacy-msrv,
|
||||
clippy, fmt, doc, and cargo-deny.
|
||||
- **`cargo-deny` integration** — `deny.toml` with permissive-only license
|
||||
policy, advisory checks, source auditing. Added `just test-deny` gate.
|
||||
- **Encode-only fuzz target** — `fuzz_encode` exercises buffer-too-small
|
||||
paths and validates output properties independently of decode.
|
||||
- **Feature coverage** — `just test-unit` and `just test-clippy` now test
|
||||
both default and `legacy-msrv` features. `just test-doc` builds with
|
||||
`--all-features`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **README accuracy** — overhead description changed from "exactly" to
|
||||
"at most ... plus 1", added miri to test mentions, updated code size
|
||||
numbers.
|
||||
- **CLAUDE.md rewrite** — updated stale API signatures, LOC count, roadmap,
|
||||
dev-deps list, and development commands.
|
||||
|
||||
## 0.3.3 — 2026-04-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Fix compilation for downstream users** — `#[mutants::skip]` attribute was
|
||||
not gated behind `cfg(test)`, causing unresolved module error when ucobs is
|
||||
used as a dependency.
|
||||
|
||||
## 0.3.2 — 2026-04-01 (yanked)
|
||||
|
||||
### Testing
|
||||
|
||||
- **Mutation testing: 100% kill rate** — 89 mutants tested, 59 caught, 30
|
||||
unviable (type system rejects them), 0 missed. Uses `cargo-mutants` v27
|
||||
with nextest. Added `mutants` crate for `#[mutants::skip]` annotations.
|
||||
- **111 tests** (was 106): added block-boundary interop verification against
|
||||
corncobs, exact-fit and too-small decode buffer sizing for the batch zero
|
||||
path.
|
||||
|
||||
## 0.3.1 — 2026-03-31
|
||||
|
||||
### Performance
|
||||
|
||||
- **Decode: rewritten with shrinking-slice pattern** — decode zeros is 14%
|
||||
faster (25,018 vs 29,114 instructions at 4 KB), decode nonzero flipped from
|
||||
5% behind corncobs to 2% ahead, decode mixed closed from 8% behind to tied.
|
||||
ucobs now wins or ties 16 of 18 benchmarks.
|
||||
- **Encode zeros: 6% faster** — zero fast path moved before `split_at` calls,
|
||||
eliminating per-byte overhead. Now tied with cobs (previously 6% behind).
|
||||
- **Code size reduced** — 821 B total (was 869 B). Encode 429 B, decode 392 B.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Replaced criterion with iai-callgrind** — benchmarks now measure
|
||||
deterministic instruction counts via Valgrind instead of wall-clock time.
|
||||
Zero run-to-run variance. Requires `valgrind` installed.
|
||||
- **Expanded benchmark coverage** — 9 payload sizes (0–4096 B) × 3 patterns
|
||||
× 3 crates × encode/decode = 162 benchmarks, all running in ~2 minutes.
|
||||
- **Code size measurement** added to `just bench` via `just bench-size`.
|
||||
- **Mutation testing** added via `cargo-mutants` (`just test-mutants`).
|
||||
- Added `mise.toml` for dev tool management.
|
||||
|
||||
## 0.3.0 — 2026-03-31
|
||||
|
||||
### Performance
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
1. **Correct** — 100% COBS spec-compliant (Cheshire & Baker 1999). No shortcuts, no deviations.
|
||||
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. ~120 lines of implementation. Runs on 8-bit MCUs and servers alike.
|
||||
3. **Insanely tested** — canonical vectors, property-based tests (proptest), fuzz targets (cargo-fuzz), cross-crate interop, miri, mutation testing, 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 run via `just bench`.
|
||||
5. **Tiny** — `no_std`, zero-alloc, zero runtime dependencies. ~140 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
|
||||
@@ -22,7 +22,7 @@ Single-file library: `src/lib.rs`
|
||||
### Public API (3 functions)
|
||||
|
||||
```rust
|
||||
pub fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize>
|
||||
pub const fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize>
|
||||
pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize>
|
||||
pub const fn max_encoded_len(src_len: usize) -> usize
|
||||
```
|
||||
@@ -32,26 +32,29 @@ pub const fn max_encoded_len(src_len: usize) -> usize
|
||||
## Development
|
||||
|
||||
```sh
|
||||
# Run all tests
|
||||
cargo test
|
||||
# Run all quality gates (tests, clippy, fmt, doc, fuzz, miri, mutants)
|
||||
just test
|
||||
|
||||
# Unit + property + interop tests only
|
||||
just test-unit
|
||||
|
||||
# Benchmarks (iai-callgrind, requires valgrind)
|
||||
just bench
|
||||
|
||||
# Fuzz (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
|
||||
just test-fuzz
|
||||
```
|
||||
|
||||
## Dev Dependencies (test-only)
|
||||
|
||||
- `cobs` — cross-validation against the `cobs` 0.5 crate
|
||||
- `corncobs` — cross-validation against another COBS implementation
|
||||
- `iai-callgrind` — deterministic instruction-count benchmarks
|
||||
- `mutants` — mutation testing annotations
|
||||
- `proptest` — property-based testing
|
||||
- `rand` — randomized test payloads
|
||||
|
||||
## Roadmap
|
||||
|
||||
- More interop targets: `cobs` crate, Python `cobs` package
|
||||
- Benchmarks (criterion) vs corncobs, cobs, and others
|
||||
- quickcheck in addition to proptest
|
||||
- Even more fuzz targets and edge-case generators
|
||||
- MSRV policy and CI
|
||||
- `#[deny(unsafe_code)]` badge
|
||||
+5
-4
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "ucobs"
|
||||
version = "0.3.0"
|
||||
version = "0.3.5"
|
||||
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>"]
|
||||
repository = "https://github.com/swaits/ucobs"
|
||||
homepage = "https://github.com/swaits/ucobs"
|
||||
repository = "https://git.swaits.com/swaits/ucobs"
|
||||
homepage = "https://git.swaits.com/swaits/ucobs"
|
||||
documentation = "https://docs.rs/ucobs"
|
||||
readme = "README.md"
|
||||
keywords = ["cobs", "framing", "no-std", "embedded", "codec"]
|
||||
@@ -26,7 +26,8 @@ all-features = true
|
||||
[dev-dependencies]
|
||||
cobs = "0.5"
|
||||
corncobs = "0.1"
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
iai-callgrind = "0.16"
|
||||
mutants = "0.0.4"
|
||||
proptest = "1"
|
||||
rand = "0.10"
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
[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 exactly
|
||||
1 byte per 254 input bytes (worst case).
|
||||
allowing `0x00` to serve as an unambiguous frame delimiter. Overhead is at most
|
||||
1 byte per 254 input bytes, plus 1.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -18,58 +18,62 @@ allowing `0x00` to serve as an unambiguous frame delimiter. Overhead is exactly
|
||||
- **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
|
||||
cross-crate interop, miri, mutation testing, randomized payloads
|
||||
|
||||
## 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%).
|
||||
[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,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 |
|
||||
| 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 | 🥇 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 |
|
||||
| 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):
|
||||
combined). Run `just bench-size` to reproduce.
|
||||
|
||||
| Crate | `encode` | `decode` | Total |
|
||||
|----------------|--------:|---------:|------:|
|
||||
| μCOBS | 434 B | 435 B | 869 B |
|
||||
| `cobs` 0.5 | 245 B | 292 B | 537 B |
|
||||
| μ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 is the largest due to its
|
||||
optimized encode (sub-slice scan, zero fast path, `copy_from_slice`)
|
||||
and decode (batch zero fill) paths.
|
||||
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
|
||||
|
||||
@@ -84,8 +88,9 @@ and decode (batch zero fill) paths.
|
||||
[^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/`.
|
||||
> Benchmarked with iai-callgrind (Valgrind instruction counting) for
|
||||
> deterministic, reproducible results. Run `just bench` to reproduce.
|
||||
> Requires `valgrind` installed.
|
||||
|
||||
## When to use μCOBS
|
||||
|
||||
@@ -93,26 +98,28 @@ and decode (batch zero fill) paths.
|
||||
|---|:---:|:---:|:---:|
|
||||
| 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** |
|
||||
| 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 |
|
||||
| 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
|
||||
competitive throughput across most workloads — especially on embedded targets.
|
||||
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 large mixed-pattern decoding where it holds
|
||||
a throughput edge.
|
||||
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, a streaming state
|
||||
machine, or your data is predominantly zero-heavy where it leads on encode.
|
||||
**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
|
||||
|
||||
@@ -275,7 +282,7 @@ 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:
|
||||
111 tests across 8 categories:
|
||||
|
||||
- **Canonical vectors** — every example from Cheshire & Baker 1999 (the original
|
||||
COBS paper), plus the full Wikipedia vector set
|
||||
@@ -288,6 +295,8 @@ affected — it uses a byte loop instead of `copy_from_slice`/memcpy.
|
||||
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
|
||||
@@ -297,11 +306,11 @@ affected — it uses a byte loop instead of `copy_from_slice`/memcpy.
|
||||
# Unit + property + interop tests
|
||||
just test-unit
|
||||
|
||||
# All quality gates (tests, clippy, fmt, doc, fuzz, miri)
|
||||
# All quality gates (tests, clippy, fmt, doc, fuzz, miri, mutants)
|
||||
just test
|
||||
|
||||
# Fuzz testing only (requires nightly + cargo-fuzz)
|
||||
just test-fuzz
|
||||
# Mutation testing only
|
||||
just test-mutants
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
+243
-125
@@ -1,153 +1,271 @@
|
||||
use iai_callgrind::{library_benchmark, library_benchmark_group, main};
|
||||
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 zeros(n: usize) -> Vec<u8> {
|
||||
vec![0u8; n]
|
||||
}
|
||||
|
||||
fn payload_nonzero(len: usize) -> Vec<u8> {
|
||||
(0..len).map(|i| (i % 255 + 1) as u8).collect()
|
||||
fn nonzero(n: usize) -> Vec<u8> {
|
||||
(0..n).map(|i| (i % 255 + 1) as u8).collect()
|
||||
}
|
||||
|
||||
fn payload_mixed(len: usize) -> Vec<u8> {
|
||||
(0..len).map(|i| (i % 256) as u8).collect()
|
||||
fn mixed(n: usize) -> Vec<u8> {
|
||||
(0..n).map(|i| (i % 256) as u8).collect()
|
||||
}
|
||||
|
||||
// ── ucobs ───────────────────────────────────────────────────────────
|
||||
// ── Setup helpers ───────────────────────────────────────────────────
|
||||
|
||||
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 setup_encode(data: Vec<u8>) -> (Vec<u8>, Vec<u8>) {
|
||||
let buf = vec![0u8; ucobs::max_encoded_len(data.len()) + 1];
|
||||
(data, buf)
|
||||
}
|
||||
|
||||
fn ucobs_decode(encoded: &[u8], out: &mut [u8]) -> usize {
|
||||
ucobs::decode(encoded, out).unwrap()
|
||||
fn setup_decode_ucobs(data: Vec<u8>) -> (Vec<u8>, Vec<u8>) {
|
||||
let mut enc = vec![0u8; ucobs::max_encoded_len(data.len()) + 1];
|
||||
let n = ucobs::encode(&data, &mut enc).unwrap();
|
||||
enc.truncate(n);
|
||||
let dec = vec![0u8; data.len() + 1];
|
||||
(enc, dec)
|
||||
}
|
||||
|
||||
// ── 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 setup_encode_corncobs(data: Vec<u8>) -> (Vec<u8>, Vec<u8>) {
|
||||
let buf = vec![0u8; corncobs::max_encoded_len(data.len())];
|
||||
(data, buf)
|
||||
}
|
||||
|
||||
fn corncobs_decode(encoded: &[u8], out: &mut [u8]) -> usize {
|
||||
// corncobs expects trailing sentinel
|
||||
corncobs::decode_buf(encoded, out).unwrap()
|
||||
fn setup_decode_corncobs(data: Vec<u8>) -> (Vec<u8>, Vec<u8>) {
|
||||
let mut enc = vec![0u8; corncobs::max_encoded_len(data.len())];
|
||||
let n = corncobs::encode_buf(&data, &mut enc);
|
||||
enc.truncate(n);
|
||||
let dec = vec![0u8; data.len() + 1];
|
||||
(enc, dec)
|
||||
}
|
||||
|
||||
// ── 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 setup_encode_cobs(data: Vec<u8>) -> (Vec<u8>, Vec<u8>) {
|
||||
let buf = vec![0u8; cobs::max_encoding_length(data.len())];
|
||||
(data, 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)
|
||||
fn setup_decode_cobs(data: Vec<u8>) -> (Vec<u8>, Vec<u8>) {
|
||||
let mut enc = vec![0u8; cobs::max_encoding_length(data.len())];
|
||||
let n = cobs::encode(&data, &mut enc);
|
||||
enc.truncate(n);
|
||||
let dec = vec![0u8; data.len() + 1];
|
||||
(enc, dec)
|
||||
}
|
||||
|
||||
// ── Benchmarks ──────────────────────────────────────────────────────
|
||||
// ── Encode 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();
|
||||
}
|
||||
#[library_benchmark]
|
||||
#[bench::zeros_0(setup_encode(zeros(0)))]
|
||||
#[bench::zeros_1(setup_encode(zeros(1)))]
|
||||
#[bench::zeros_10(setup_encode(zeros(10)))]
|
||||
#[bench::zeros_64(setup_encode(zeros(64)))]
|
||||
#[bench::zeros_254(setup_encode(zeros(254)))]
|
||||
#[bench::zeros_255(setup_encode(zeros(255)))]
|
||||
#[bench::zeros_256(setup_encode(zeros(256)))]
|
||||
#[bench::zeros_1024(setup_encode(zeros(1024)))]
|
||||
#[bench::zeros_4096(setup_encode(zeros(4096)))]
|
||||
#[bench::nonzero_0(setup_encode(nonzero(0)))]
|
||||
#[bench::nonzero_1(setup_encode(nonzero(1)))]
|
||||
#[bench::nonzero_10(setup_encode(nonzero(10)))]
|
||||
#[bench::nonzero_64(setup_encode(nonzero(64)))]
|
||||
#[bench::nonzero_254(setup_encode(nonzero(254)))]
|
||||
#[bench::nonzero_255(setup_encode(nonzero(255)))]
|
||||
#[bench::nonzero_256(setup_encode(nonzero(256)))]
|
||||
#[bench::nonzero_1024(setup_encode(nonzero(1024)))]
|
||||
#[bench::nonzero_4096(setup_encode(nonzero(4096)))]
|
||||
#[bench::mixed_0(setup_encode(mixed(0)))]
|
||||
#[bench::mixed_1(setup_encode(mixed(1)))]
|
||||
#[bench::mixed_10(setup_encode(mixed(10)))]
|
||||
#[bench::mixed_64(setup_encode(mixed(64)))]
|
||||
#[bench::mixed_254(setup_encode(mixed(254)))]
|
||||
#[bench::mixed_255(setup_encode(mixed(255)))]
|
||||
#[bench::mixed_256(setup_encode(mixed(256)))]
|
||||
#[bench::mixed_1024(setup_encode(mixed(1024)))]
|
||||
#[bench::mixed_4096(setup_encode(mixed(4096)))]
|
||||
fn encode_ucobs((data, mut buf): (Vec<u8>, Vec<u8>)) {
|
||||
black_box(ucobs::encode(black_box(&data), &mut buf));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
#[library_benchmark]
|
||||
#[bench::zeros_0(setup_encode_corncobs(zeros(0)))]
|
||||
#[bench::zeros_1(setup_encode_corncobs(zeros(1)))]
|
||||
#[bench::zeros_10(setup_encode_corncobs(zeros(10)))]
|
||||
#[bench::zeros_64(setup_encode_corncobs(zeros(64)))]
|
||||
#[bench::zeros_254(setup_encode_corncobs(zeros(254)))]
|
||||
#[bench::zeros_255(setup_encode_corncobs(zeros(255)))]
|
||||
#[bench::zeros_256(setup_encode_corncobs(zeros(256)))]
|
||||
#[bench::zeros_1024(setup_encode_corncobs(zeros(1024)))]
|
||||
#[bench::zeros_4096(setup_encode_corncobs(zeros(4096)))]
|
||||
#[bench::nonzero_0(setup_encode_corncobs(nonzero(0)))]
|
||||
#[bench::nonzero_1(setup_encode_corncobs(nonzero(1)))]
|
||||
#[bench::nonzero_10(setup_encode_corncobs(nonzero(10)))]
|
||||
#[bench::nonzero_64(setup_encode_corncobs(nonzero(64)))]
|
||||
#[bench::nonzero_254(setup_encode_corncobs(nonzero(254)))]
|
||||
#[bench::nonzero_255(setup_encode_corncobs(nonzero(255)))]
|
||||
#[bench::nonzero_256(setup_encode_corncobs(nonzero(256)))]
|
||||
#[bench::nonzero_1024(setup_encode_corncobs(nonzero(1024)))]
|
||||
#[bench::nonzero_4096(setup_encode_corncobs(nonzero(4096)))]
|
||||
#[bench::mixed_0(setup_encode_corncobs(mixed(0)))]
|
||||
#[bench::mixed_1(setup_encode_corncobs(mixed(1)))]
|
||||
#[bench::mixed_10(setup_encode_corncobs(mixed(10)))]
|
||||
#[bench::mixed_64(setup_encode_corncobs(mixed(64)))]
|
||||
#[bench::mixed_254(setup_encode_corncobs(mixed(254)))]
|
||||
#[bench::mixed_255(setup_encode_corncobs(mixed(255)))]
|
||||
#[bench::mixed_256(setup_encode_corncobs(mixed(256)))]
|
||||
#[bench::mixed_1024(setup_encode_corncobs(mixed(1024)))]
|
||||
#[bench::mixed_4096(setup_encode_corncobs(mixed(4096)))]
|
||||
fn encode_corncobs((data, mut buf): (Vec<u8>, Vec<u8>)) {
|
||||
black_box(corncobs::encode_buf(black_box(&data), &mut buf));
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_encode, bench_decode);
|
||||
criterion_main!(benches);
|
||||
#[library_benchmark]
|
||||
#[bench::zeros_0(setup_encode_cobs(zeros(0)))]
|
||||
#[bench::zeros_1(setup_encode_cobs(zeros(1)))]
|
||||
#[bench::zeros_10(setup_encode_cobs(zeros(10)))]
|
||||
#[bench::zeros_64(setup_encode_cobs(zeros(64)))]
|
||||
#[bench::zeros_254(setup_encode_cobs(zeros(254)))]
|
||||
#[bench::zeros_255(setup_encode_cobs(zeros(255)))]
|
||||
#[bench::zeros_256(setup_encode_cobs(zeros(256)))]
|
||||
#[bench::zeros_1024(setup_encode_cobs(zeros(1024)))]
|
||||
#[bench::zeros_4096(setup_encode_cobs(zeros(4096)))]
|
||||
#[bench::nonzero_0(setup_encode_cobs(nonzero(0)))]
|
||||
#[bench::nonzero_1(setup_encode_cobs(nonzero(1)))]
|
||||
#[bench::nonzero_10(setup_encode_cobs(nonzero(10)))]
|
||||
#[bench::nonzero_64(setup_encode_cobs(nonzero(64)))]
|
||||
#[bench::nonzero_254(setup_encode_cobs(nonzero(254)))]
|
||||
#[bench::nonzero_255(setup_encode_cobs(nonzero(255)))]
|
||||
#[bench::nonzero_256(setup_encode_cobs(nonzero(256)))]
|
||||
#[bench::nonzero_1024(setup_encode_cobs(nonzero(1024)))]
|
||||
#[bench::nonzero_4096(setup_encode_cobs(nonzero(4096)))]
|
||||
#[bench::mixed_0(setup_encode_cobs(mixed(0)))]
|
||||
#[bench::mixed_1(setup_encode_cobs(mixed(1)))]
|
||||
#[bench::mixed_10(setup_encode_cobs(mixed(10)))]
|
||||
#[bench::mixed_64(setup_encode_cobs(mixed(64)))]
|
||||
#[bench::mixed_254(setup_encode_cobs(mixed(254)))]
|
||||
#[bench::mixed_255(setup_encode_cobs(mixed(255)))]
|
||||
#[bench::mixed_256(setup_encode_cobs(mixed(256)))]
|
||||
#[bench::mixed_1024(setup_encode_cobs(mixed(1024)))]
|
||||
#[bench::mixed_4096(setup_encode_cobs(mixed(4096)))]
|
||||
fn encode_cobs((data, mut buf): (Vec<u8>, Vec<u8>)) {
|
||||
black_box(cobs::encode(black_box(&data), &mut buf));
|
||||
}
|
||||
|
||||
// ── Decode benchmarks ──────────────────────────────────────────────
|
||||
|
||||
#[library_benchmark]
|
||||
#[bench::zeros_0(setup_decode_ucobs(zeros(0)))]
|
||||
#[bench::zeros_1(setup_decode_ucobs(zeros(1)))]
|
||||
#[bench::zeros_10(setup_decode_ucobs(zeros(10)))]
|
||||
#[bench::zeros_64(setup_decode_ucobs(zeros(64)))]
|
||||
#[bench::zeros_254(setup_decode_ucobs(zeros(254)))]
|
||||
#[bench::zeros_255(setup_decode_ucobs(zeros(255)))]
|
||||
#[bench::zeros_256(setup_decode_ucobs(zeros(256)))]
|
||||
#[bench::zeros_1024(setup_decode_ucobs(zeros(1024)))]
|
||||
#[bench::zeros_4096(setup_decode_ucobs(zeros(4096)))]
|
||||
#[bench::nonzero_0(setup_decode_ucobs(nonzero(0)))]
|
||||
#[bench::nonzero_1(setup_decode_ucobs(nonzero(1)))]
|
||||
#[bench::nonzero_10(setup_decode_ucobs(nonzero(10)))]
|
||||
#[bench::nonzero_64(setup_decode_ucobs(nonzero(64)))]
|
||||
#[bench::nonzero_254(setup_decode_ucobs(nonzero(254)))]
|
||||
#[bench::nonzero_255(setup_decode_ucobs(nonzero(255)))]
|
||||
#[bench::nonzero_256(setup_decode_ucobs(nonzero(256)))]
|
||||
#[bench::nonzero_1024(setup_decode_ucobs(nonzero(1024)))]
|
||||
#[bench::nonzero_4096(setup_decode_ucobs(nonzero(4096)))]
|
||||
#[bench::mixed_0(setup_decode_ucobs(mixed(0)))]
|
||||
#[bench::mixed_1(setup_decode_ucobs(mixed(1)))]
|
||||
#[bench::mixed_10(setup_decode_ucobs(mixed(10)))]
|
||||
#[bench::mixed_64(setup_decode_ucobs(mixed(64)))]
|
||||
#[bench::mixed_254(setup_decode_ucobs(mixed(254)))]
|
||||
#[bench::mixed_255(setup_decode_ucobs(mixed(255)))]
|
||||
#[bench::mixed_256(setup_decode_ucobs(mixed(256)))]
|
||||
#[bench::mixed_1024(setup_decode_ucobs(mixed(1024)))]
|
||||
#[bench::mixed_4096(setup_decode_ucobs(mixed(4096)))]
|
||||
fn decode_ucobs((enc, mut buf): (Vec<u8>, Vec<u8>)) {
|
||||
black_box(ucobs::decode(black_box(&enc), &mut buf));
|
||||
}
|
||||
|
||||
#[library_benchmark]
|
||||
#[bench::zeros_0(setup_decode_corncobs(zeros(0)))]
|
||||
#[bench::zeros_1(setup_decode_corncobs(zeros(1)))]
|
||||
#[bench::zeros_10(setup_decode_corncobs(zeros(10)))]
|
||||
#[bench::zeros_64(setup_decode_corncobs(zeros(64)))]
|
||||
#[bench::zeros_254(setup_decode_corncobs(zeros(254)))]
|
||||
#[bench::zeros_255(setup_decode_corncobs(zeros(255)))]
|
||||
#[bench::zeros_256(setup_decode_corncobs(zeros(256)))]
|
||||
#[bench::zeros_1024(setup_decode_corncobs(zeros(1024)))]
|
||||
#[bench::zeros_4096(setup_decode_corncobs(zeros(4096)))]
|
||||
#[bench::nonzero_0(setup_decode_corncobs(nonzero(0)))]
|
||||
#[bench::nonzero_1(setup_decode_corncobs(nonzero(1)))]
|
||||
#[bench::nonzero_10(setup_decode_corncobs(nonzero(10)))]
|
||||
#[bench::nonzero_64(setup_decode_corncobs(nonzero(64)))]
|
||||
#[bench::nonzero_254(setup_decode_corncobs(nonzero(254)))]
|
||||
#[bench::nonzero_255(setup_decode_corncobs(nonzero(255)))]
|
||||
#[bench::nonzero_256(setup_decode_corncobs(nonzero(256)))]
|
||||
#[bench::nonzero_1024(setup_decode_corncobs(nonzero(1024)))]
|
||||
#[bench::nonzero_4096(setup_decode_corncobs(nonzero(4096)))]
|
||||
#[bench::mixed_0(setup_decode_corncobs(mixed(0)))]
|
||||
#[bench::mixed_1(setup_decode_corncobs(mixed(1)))]
|
||||
#[bench::mixed_10(setup_decode_corncobs(mixed(10)))]
|
||||
#[bench::mixed_64(setup_decode_corncobs(mixed(64)))]
|
||||
#[bench::mixed_254(setup_decode_corncobs(mixed(254)))]
|
||||
#[bench::mixed_255(setup_decode_corncobs(mixed(255)))]
|
||||
#[bench::mixed_256(setup_decode_corncobs(mixed(256)))]
|
||||
#[bench::mixed_1024(setup_decode_corncobs(mixed(1024)))]
|
||||
#[bench::mixed_4096(setup_decode_corncobs(mixed(4096)))]
|
||||
fn decode_corncobs((enc, mut buf): (Vec<u8>, Vec<u8>)) {
|
||||
let _ = black_box(corncobs::decode_buf(black_box(&enc), &mut buf));
|
||||
}
|
||||
|
||||
#[library_benchmark]
|
||||
#[bench::zeros_0(setup_decode_cobs(zeros(0)))]
|
||||
#[bench::zeros_1(setup_decode_cobs(zeros(1)))]
|
||||
#[bench::zeros_10(setup_decode_cobs(zeros(10)))]
|
||||
#[bench::zeros_64(setup_decode_cobs(zeros(64)))]
|
||||
#[bench::zeros_254(setup_decode_cobs(zeros(254)))]
|
||||
#[bench::zeros_255(setup_decode_cobs(zeros(255)))]
|
||||
#[bench::zeros_256(setup_decode_cobs(zeros(256)))]
|
||||
#[bench::zeros_1024(setup_decode_cobs(zeros(1024)))]
|
||||
#[bench::zeros_4096(setup_decode_cobs(zeros(4096)))]
|
||||
#[bench::nonzero_0(setup_decode_cobs(nonzero(0)))]
|
||||
#[bench::nonzero_1(setup_decode_cobs(nonzero(1)))]
|
||||
#[bench::nonzero_10(setup_decode_cobs(nonzero(10)))]
|
||||
#[bench::nonzero_64(setup_decode_cobs(nonzero(64)))]
|
||||
#[bench::nonzero_254(setup_decode_cobs(nonzero(254)))]
|
||||
#[bench::nonzero_255(setup_decode_cobs(nonzero(255)))]
|
||||
#[bench::nonzero_256(setup_decode_cobs(nonzero(256)))]
|
||||
#[bench::nonzero_1024(setup_decode_cobs(nonzero(1024)))]
|
||||
#[bench::nonzero_4096(setup_decode_cobs(nonzero(4096)))]
|
||||
#[bench::mixed_0(setup_decode_cobs(mixed(0)))]
|
||||
#[bench::mixed_1(setup_decode_cobs(mixed(1)))]
|
||||
#[bench::mixed_10(setup_decode_cobs(mixed(10)))]
|
||||
#[bench::mixed_64(setup_decode_cobs(mixed(64)))]
|
||||
#[bench::mixed_254(setup_decode_cobs(mixed(254)))]
|
||||
#[bench::mixed_255(setup_decode_cobs(mixed(255)))]
|
||||
#[bench::mixed_256(setup_decode_cobs(mixed(256)))]
|
||||
#[bench::mixed_1024(setup_decode_cobs(mixed(1024)))]
|
||||
#[bench::mixed_4096(setup_decode_cobs(mixed(4096)))]
|
||||
fn decode_cobs((enc, mut buf): (Vec<u8>, Vec<u8>)) {
|
||||
black_box(
|
||||
cobs::decode(black_box(&enc), &mut buf)
|
||||
.map(|r| r.parsed_size())
|
||||
.unwrap_or(0),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Groups ─────────────────────────────────────────────────────────
|
||||
|
||||
library_benchmark_group!(
|
||||
name = encode;
|
||||
benchmarks = encode_ucobs, encode_corncobs, encode_cobs
|
||||
);
|
||||
|
||||
library_benchmark_group!(
|
||||
name = decode;
|
||||
benchmarks = decode_ucobs, decode_corncobs, decode_cobs
|
||||
);
|
||||
|
||||
main!(library_benchmark_groups = encode, decode);
|
||||
@@ -0,0 +1,37 @@
|
||||
[graph]
|
||||
all-features = true
|
||||
|
||||
[advisories]
|
||||
yanked = "deny"
|
||||
ignore = [
|
||||
# bincode 1.3.3 — transitive dev-dep via iai-callgrind (latest 0.16.1).
|
||||
# Unmaintained but stable and complete; no security issue. Dev-only, never
|
||||
# ships in ucobs. Remove this ignore when iai-callgrind drops bincode.
|
||||
"RUSTSEC-2025-0141",
|
||||
]
|
||||
|
||||
[licenses]
|
||||
# Permissive licenses only. All copyleft (GPL, LGPL, AGPL, MPL, etc.) is denied by omission.
|
||||
allow = [
|
||||
"MIT",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"Zlib",
|
||||
"Unlicense",
|
||||
"CC0-1.0",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
]
|
||||
unused-allowed-license = "allow"
|
||||
|
||||
[bans]
|
||||
multiple-versions = "warn"
|
||||
wildcards = "deny"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = []
|
||||
@@ -30,3 +30,9 @@ name = "fuzz_small_dest"
|
||||
path = "fuzz_targets/fuzz_small_dest.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_encode"
|
||||
path = "fuzz_targets/fuzz_encode.rs"
|
||||
test = false
|
||||
doc = false
|
||||
@@ -0,0 +1,33 @@
|
||||
//! Fuzz target: encode arbitrary input into buffers of varying sizes.
|
||||
//! Exercises encode error paths (buffer too small) and validates output properties.
|
||||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if data.len() > 1024 {
|
||||
return;
|
||||
}
|
||||
|
||||
let max = ucobs::max_encoded_len(data.len());
|
||||
|
||||
// Encode into a correctly-sized buffer — must always succeed.
|
||||
let mut buf = vec![0u8; max];
|
||||
let n = ucobs::encode(data, &mut buf)
|
||||
.expect("encode failed with max_encoded_len buffer");
|
||||
let encoded = &buf[..n];
|
||||
|
||||
// Output must never contain 0x00.
|
||||
assert!(!encoded.contains(&0x00), "encoded output contains 0x00");
|
||||
|
||||
// Output length must be bounded by max_encoded_len.
|
||||
assert!(n <= max, "encoded length exceeds max_encoded_len");
|
||||
|
||||
// Output must be at least 1 byte (even empty input encodes to [0x01]).
|
||||
assert!(n >= 1, "encoded length is zero");
|
||||
|
||||
// Encode into every possible undersized buffer — must return None, never panic.
|
||||
for size in 0..n {
|
||||
let mut small = vec![0u8; size];
|
||||
assert_eq!(ucobs::encode(data, &mut small), None);
|
||||
}
|
||||
});
|
||||
@@ -4,8 +4,8 @@
|
||||
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
|
||||
# run ALL quality gates (tests, clippy, fmt check, doc, deny, fuzz, miri, mutants)
|
||||
test: test-unit test-clippy test-fmt test-doc test-deny test-fuzz test-miri test-mutants
|
||||
|
||||
# quick compile check (no tests)
|
||||
check:
|
||||
@@ -14,11 +14,13 @@ check:
|
||||
# unit + integration + proptest tests (via nextest for parallelism)
|
||||
test-unit:
|
||||
cargo nextest run
|
||||
cargo nextest run --features legacy-msrv
|
||||
cargo test --doc
|
||||
|
||||
# clippy with all warnings as errors
|
||||
test-clippy:
|
||||
cargo clippy -- -D warnings
|
||||
cargo clippy --features legacy-msrv -- -D warnings
|
||||
|
||||
# check formatting
|
||||
test-fmt:
|
||||
@@ -26,11 +28,16 @@ test-fmt:
|
||||
|
||||
# doc build (catches broken doc links)
|
||||
test-doc:
|
||||
cargo doc --no-deps
|
||||
cargo doc --no-deps --all-features
|
||||
|
||||
# license and advisory audit (requires cargo-deny)
|
||||
test-deny:
|
||||
cargo deny check
|
||||
|
||||
# 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_encode -- -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
|
||||
|
||||
@@ -39,10 +46,26 @@ test-fuzz:
|
||||
test-miri:
|
||||
cargo +nightly miri test -- --skip proptests
|
||||
|
||||
# run criterion benchmarks (ucobs vs cobs vs corncobs)
|
||||
bench:
|
||||
# mutation testing (requires cargo-mutants)
|
||||
test-mutants:
|
||||
cargo mutants --no-shuffle
|
||||
|
||||
# run iai-callgrind benchmarks (deterministic instruction counts, requires valgrind)
|
||||
bench: bench-iai bench-size
|
||||
|
||||
# run iai-callgrind benchmarks (ucobs vs cobs vs corncobs)
|
||||
bench-iai:
|
||||
cargo bench
|
||||
|
||||
# measure compiled code size (encode + decode .text bytes)
|
||||
bench-size:
|
||||
@cargo build --release 2>/dev/null
|
||||
@echo ""
|
||||
@echo "Code size (release, encode + decode .text):"
|
||||
@nm -S target/release/libucobs.rlib 2>/dev/null \
|
||||
| grep 'T _ZN5ucobs' \
|
||||
| awk '{size=strtonum("0x"$2); name=$4; sub(/.*ucobs[0-9]*/, "", name); sub(/17h.*/, "", name); printf " ucobs %-7s %4d B\n", name, size}'
|
||||
|
||||
# run python cobs benchmarks (requires: pip install cobs)
|
||||
bench-python:
|
||||
python3 bench_python.py
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[tools]
|
||||
rust = "1.93"
|
||||
"cargo:cargo-deny" = "latest"
|
||||
"cargo:cargo-fuzz" = "latest"
|
||||
"cargo:cargo-mutants" = "latest"
|
||||
"cargo:cargo-nextest" = "latest"
|
||||
"cargo:iai-callgrind-runner" = "latest"
|
||||
+114
-50
@@ -155,23 +155,25 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
/// Copy `src` into `dest[offset..]` in a const-compatible way.
|
||||
/// Default path uses `copy_from_slice` (Rust 1.93+, compiles to memcpy).
|
||||
/// Uses `copy_from_slice` (Rust 1.93+, compiles to memcpy).
|
||||
/// With `legacy-msrv` feature, falls back to a manual loop for Rust 1.83+.
|
||||
#[inline(always)]
|
||||
#[cfg(not(feature = "legacy-msrv"))]
|
||||
const fn const_copy(dest: &mut [u8], offset: usize, src: &[u8]) {
|
||||
let (_, tail) = dest.split_at_mut(offset);
|
||||
let (chunk, _) = tail.split_at_mut(src.len());
|
||||
chunk.copy_from_slice(src);
|
||||
const fn const_copy(dest: &mut [u8], di: usize, src: &[u8], n: usize) {
|
||||
let (_, dt) = dest.split_at_mut(di);
|
||||
let (dc, _) = dt.split_at_mut(n);
|
||||
let (sc, _) = src.split_at(n);
|
||||
dc.copy_from_slice(sc);
|
||||
}
|
||||
|
||||
/// Fallback const copy for toolchains older than Rust 1.93.
|
||||
#[cfg_attr(test, mutants::skip)] // dead code under default features
|
||||
#[inline(always)]
|
||||
#[cfg(feature = "legacy-msrv")]
|
||||
const fn const_copy(dest: &mut [u8], offset: usize, src: &[u8]) {
|
||||
const fn const_copy(dest: &mut [u8], di: usize, src: &[u8], n: usize) {
|
||||
let mut i = 0;
|
||||
while i < src.len() {
|
||||
dest[offset + i] = src[i];
|
||||
while i < n {
|
||||
dest[di + i] = src[i];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
@@ -215,13 +217,8 @@ pub const fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
let remaining = src.len() - si;
|
||||
let max_run = if remaining < 254 { remaining } else { 254 };
|
||||
|
||||
// Create bounded source window (split_at is const since Rust 1.71).
|
||||
let (_, src_tail) = src.split_at(si);
|
||||
let (src_window, _) = src_tail.split_at(max_run);
|
||||
|
||||
// Fast path: leading zero byte avoids scan loop overhead.
|
||||
// Always continue — the next iteration emits the trailing group code byte.
|
||||
if max_run > 0 && src_window[0] == 0x00 {
|
||||
// Fast path: leading zero byte — skip split_at overhead entirely.
|
||||
if max_run > 0 && src[si] == 0x00 {
|
||||
if di >= dest.len() {
|
||||
return None;
|
||||
}
|
||||
@@ -231,8 +228,11 @@ pub const fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Phase 1: Scan for next zero byte.
|
||||
// LLVM sees `run < src_window.len()` → eliminates bounds check.
|
||||
// Create bounded source window for scan + copy.
|
||||
let (_, src_tail) = src.split_at(si);
|
||||
let (src_window, _) = src_tail.split_at(max_run);
|
||||
|
||||
// Scan for next zero byte.
|
||||
let mut run = 0;
|
||||
while run < src_window.len() && src_window[run] != 0x00 {
|
||||
run += 1;
|
||||
@@ -246,25 +246,20 @@ pub const fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
dest[di] = if full_block { 0xFF } else { (run + 1) as u8 };
|
||||
di += 1;
|
||||
|
||||
// Phase 2: Copy via const copy_from_slice (Rust 1.93+) → memcpy.
|
||||
// Copy the run.
|
||||
if run > 0 {
|
||||
if di + run > dest.len() {
|
||||
return None;
|
||||
}
|
||||
let (src_chunk, _) = src_window.split_at(run);
|
||||
const_copy(dest, di, src_chunk);
|
||||
const_copy(dest, di, src_window, run);
|
||||
di += run;
|
||||
}
|
||||
si += run;
|
||||
|
||||
if full_block {
|
||||
if si >= src.len() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if si >= src.len() {
|
||||
break;
|
||||
}
|
||||
if si >= src.len() {
|
||||
break;
|
||||
}
|
||||
if !full_block {
|
||||
si += 1; // skip the zero byte
|
||||
}
|
||||
}
|
||||
@@ -297,44 +292,47 @@ pub const fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
let mut si = 0;
|
||||
let mut src = src;
|
||||
let mut di = 0;
|
||||
while si < src.len() {
|
||||
let code = src[si] as usize;
|
||||
si += 1;
|
||||
while let Some((&code_byte, rest)) = src.split_first() {
|
||||
src = rest;
|
||||
let code = code_byte as usize;
|
||||
if code == 0 {
|
||||
return None; // unexpected zero in COBS data
|
||||
return None;
|
||||
}
|
||||
|
||||
if code == 1 {
|
||||
// Batch path: count consecutive 0x01 codes, fill zeros in bulk.
|
||||
// Batch consecutive 0x01 codes and fill zeros in bulk.
|
||||
let mut count = 1usize;
|
||||
while si < src.len() && src[si] == 0x01 {
|
||||
si += 1;
|
||||
while let Some((&next, rest)) = src.split_first() {
|
||||
if next != 0x01 {
|
||||
break;
|
||||
}
|
||||
src = rest;
|
||||
count += 1;
|
||||
}
|
||||
// Each 0x01 inserts an implicit zero, except the last at end-of-input.
|
||||
let zeros = if si < src.len() { count } else { count - 1 };
|
||||
if zeros > 0 {
|
||||
if di + zeros > dest.len() {
|
||||
return None;
|
||||
}
|
||||
dest[di..di + zeros].fill(0);
|
||||
di += zeros;
|
||||
// If more data follows, every 0x01 represents a zero byte.
|
||||
// If the stream ends here, the final 0x01 is the end-of-data
|
||||
// marker, not a zero — so emit one fewer.
|
||||
let zeros = if !src.is_empty() { count } else { count - 1 };
|
||||
if di + zeros > dest.len() {
|
||||
return None;
|
||||
}
|
||||
dest[di..di + zeros].fill(0);
|
||||
di += zeros;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bulk-copy `code - 1` data bytes.
|
||||
let n = code - 1;
|
||||
if si + n > src.len() || di + n > dest.len() {
|
||||
if src.len() < n || di + n > dest.len() {
|
||||
return None;
|
||||
}
|
||||
dest[di..di + n].copy_from_slice(&src[si..si + n]);
|
||||
let (block, rest) = src.split_at(n);
|
||||
dest[di..di + n].copy_from_slice(block);
|
||||
di += n;
|
||||
si += n;
|
||||
// If code < 0xFF, an implicit zero follows (unless we're at the end).
|
||||
if code != 0xFF && si < src.len() {
|
||||
src = rest;
|
||||
if code != 0xFF && !src.is_empty() {
|
||||
if di >= dest.len() {
|
||||
return None;
|
||||
}
|
||||
@@ -836,6 +834,49 @@ mod tests {
|
||||
assert_eq!(enc(&input), Some(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_254_block_boundary_exact() {
|
||||
// 254 nonzero bytes MUST produce code 0xFF (full block), NOT 0xFF+1=0x00.
|
||||
// This catches max_run being 255 instead of 254.
|
||||
let input: Vec<u8> = (1..=254).map(|i| i as u8).collect();
|
||||
let encoded = enc(&input).unwrap();
|
||||
assert_eq!(encoded[0], 0xFF); // code byte for full 254-byte block
|
||||
assert_eq!(encoded.len(), 255); // 1 code byte + 254 data bytes
|
||||
// Verify it round-trips
|
||||
let mut dec = vec![0u8; 256];
|
||||
let n = decode(&encoded, &mut dec).unwrap();
|
||||
assert_eq!(&dec[..n], &input[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_255_nonzero_needs_two_blocks() {
|
||||
// 255 nonzero bytes must split into a 254-byte full block + 1-byte block.
|
||||
// If max_run were 255 (bug), this would be a single block — wrong.
|
||||
let input: Vec<u8> = (1..=255).map(|i| (i % 255 + 1) as u8).collect();
|
||||
let encoded = enc(&input).unwrap();
|
||||
assert_eq!(encoded[0], 0xFF); // first block: full 254
|
||||
assert_eq!(encoded[255], 0x02); // second block: 1 byte (code = 2)
|
||||
assert_eq!(encoded.len(), 257); // 1 + 254 + 1 + 1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_block_boundary_matches_corncobs() {
|
||||
// Verify byte-for-byte match with corncobs at the critical 254-byte
|
||||
// block boundary. Catches max_run being off by one.
|
||||
for len in [254, 255, 256, 508, 512] {
|
||||
let input: Vec<u8> = (0..len).map(|i| (i % 254 + 1) as u8).collect();
|
||||
let ours = enc(&input).unwrap();
|
||||
let mut their_buf = vec![0u8; corncobs::max_encoded_len(input.len())];
|
||||
let their_len = corncobs::encode_buf(&input, &mut their_buf);
|
||||
// corncobs appends a sentinel; strip it for comparison
|
||||
assert_eq!(
|
||||
&ours[..],
|
||||
&their_buf[..their_len - 1],
|
||||
"mismatch at len={len}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Round-trip tests ────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
@@ -1039,6 +1080,29 @@ mod tests {
|
||||
assert_eq!(out, [0x11, 0x22]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_zeros_dest_exact_size() {
|
||||
// Decode all-zeros into an exact-fit buffer.
|
||||
// Encoded: N+1 bytes of 0x01 → N bytes of 0x00.
|
||||
// This catches `> dest.len()` mutated to `>= dest.len()`.
|
||||
for n in [1, 2, 10, 64, 254, 255, 256] {
|
||||
let encoded: Vec<u8> = vec![0x01; n + 1];
|
||||
let mut out = vec![0u8; n]; // exact fit
|
||||
assert_eq!(decode(&encoded, &mut out), Some(n), "failed at n={n}");
|
||||
assert!(out.iter().all(|&b| b == 0x00), "wrong data at n={n}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_zeros_dest_one_too_small() {
|
||||
// Buffer one byte too small for all-zeros decode.
|
||||
for n in [2, 10, 64, 256] {
|
||||
let encoded: Vec<u8> = vec![0x01; n + 1];
|
||||
let mut out = vec![0u8; n - 1]; // one too small
|
||||
assert_eq!(decode(&encoded, &mut out), None, "should fail at n={n}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Multi-block boundary tests ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
@@ -1194,7 +1258,7 @@ mod proptests {
|
||||
shrink in 1usize..256,
|
||||
) {
|
||||
let max = max_encoded_len(data.len());
|
||||
let dest_size = if shrink >= max { 0 } else { max - shrink };
|
||||
let dest_size = max.saturating_sub(shrink);
|
||||
let mut buf = vec![0u8; dest_size];
|
||||
let _ = encode(&data, &mut buf); // may return None, must not panic
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user