chore: audit cleanup, CI, cargo-deny, encode fuzz target (0.3.4)
CI / Test (1.93) (push) Canceled after 0s
CI / Test (stable) (push) Canceled after 0s
CI / Test (1.83, legacy-msrv) (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Format (push) Canceled after 0s
CI / Doc (push) Canceled after 0s
CI / Cargo Deny (push) Canceled after 0s
CI / Test (1.93) (push) Canceled after 0s
CI / Test (stable) (push) Canceled after 0s
CI / Test (1.83, legacy-msrv) (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Format (push) Canceled after 0s
CI / Doc (push) Canceled after 0s
CI / Cargo Deny (push) Canceled after 0s
Simplify encoder exit branch (−41 B code size), add GitHub Actions CI with MSRV matrix, cargo-deny with permissive-only license policy, encode-only fuzz target, and feature coverage across test gates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
11 files changed
+223
-35
No files matched your search
@@ -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
|
||||
@@ -1,5 +1,36 @@
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ucobs"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
edition = "2021"
|
||||
rust-version = "1.93"
|
||||
license = "MIT"
|
||||
|
||||
@@ -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,7 +18,7 @@ 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
|
||||
|
||||
@@ -64,16 +64,16 @@ combined). Run `just bench-size` to reproduce.
|
||||
|
||||
| Crate | `encode` | `decode` | Total |
|
||||
|----------------|--------:|---------:|------:|
|
||||
| μCOBS | 429 B | 392 B | 821 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 because safe
|
||||
`const fn` encoding with `copy_from_slice`/memcpy requires `split_at`
|
||||
bounds checks that generate panic paths — the cost of combining
|
||||
compile-time safety with runtime speed. Embedded users targeting size
|
||||
over speed can build with `opt-level = "s"` which reduces μCOBS to
|
||||
~738 B.
|
||||
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
|
||||
|
||||
@@ -105,7 +105,7 @@ over speed can build with `opt-level = "s"` which reduces μCOBS to
|
||||
| Dead-simple API (3 functions) | **yes** | yes | more surface area |
|
||||
| In-place / streaming encode | no | no | **yes** |
|
||||
| `no_std` + zero-alloc by default | **yes** | opt-in | **yes** |
|
||||
| Thorough test suite | **111 tests, fuzz, proptest, mutants** | 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 tied
|
||||
|
||||
@@ -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, mutants)
|
||||
test: test-unit test-clippy test-fmt test-doc test-fuzz test-miri test-mutants
|
||||
# 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
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
[tools]
|
||||
"cargo:cargo-nextest" = "latest"
|
||||
rust = "1.93"
|
||||
"cargo:cargo-deny" = "latest"
|
||||
"cargo:cargo-fuzz" = "latest"
|
||||
"cargo:cargo-mutants" = "latest"
|
||||
"cargo:cargo-nextest" = "latest"
|
||||
"cargo:iai-callgrind-runner" = "latest"
|
||||
+5
-6
@@ -256,14 +256,10 @@ pub const fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
}
|
||||
si += run;
|
||||
|
||||
if full_block {
|
||||
if si >= src.len() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if si >= src.len() {
|
||||
break;
|
||||
}
|
||||
if !full_block {
|
||||
si += 1; // skip the zero byte
|
||||
}
|
||||
}
|
||||
@@ -315,6 +311,9 @@ pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
src = rest;
|
||||
count += 1;
|
||||
}
|
||||
// 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;
|
||||
@@ -1259,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