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

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:
swaitsandClaude Opus 4.6 committed 2026-04-01 13:01:19 -06:00
1 parent f6778e20b8
commit f805386bb8
11 files changed
+226 -38

No files matched your search

+69
View File
@@ -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
+31
View File
@@ -1,5 +1,36 @@
# Changelog # 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 ## 0.3.3 — 2026-04-01
### Fixed ### Fixed
+16 -13
View File
@@ -10,9 +10,9 @@
1. **Correct** — 100% COBS spec-compliant (Cheshire & Baker 1999). No shortcuts, no deviations. 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. 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, 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 are CI-enforced. 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. ~120 lines of implementation. Runs on 8-bit MCUs and servers alike. 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. 6. **Trustworthy** — small enough to audit by hand. No unsafe. No panics in release. Fuzz-hardened.
## Architecture ## Architecture
@@ -22,7 +22,7 @@ Single-file library: `src/lib.rs`
### Public API (3 functions) ### Public API (3 functions)
```rust ```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 fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize>
pub const fn max_encoded_len(src_len: usize) -> 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 ## Development
```sh ```sh
# Run all tests # Run all quality gates (tests, clippy, fmt, doc, fuzz, miri, mutants)
cargo test just test
# Unit + property + interop tests only
just test-unit
# Benchmarks (iai-callgrind, requires valgrind)
just bench
# Fuzz (requires nightly + cargo-fuzz) # Fuzz (requires nightly + cargo-fuzz)
cd fuzz just test-fuzz
cargo +nightly fuzz run fuzz_decode -- -max_total_time=60
cargo +nightly fuzz run fuzz_roundtrip -- -max_total_time=60
``` ```
## Dev Dependencies (test-only) ## Dev Dependencies (test-only)
- `cobs` — cross-validation against the `cobs` 0.5 crate
- `corncobs` — cross-validation against another COBS implementation - `corncobs` — cross-validation against another COBS implementation
- `iai-callgrind` — deterministic instruction-count benchmarks
- `mutants` — mutation testing annotations
- `proptest` — property-based testing - `proptest` — property-based testing
- `rand` — randomized test payloads - `rand` — randomized test payloads
## Roadmap ## Roadmap
- More interop targets: `cobs` crate, Python `cobs` package
- Benchmarks (criterion) vs corncobs, cobs, and others
- quickcheck in addition to proptest - quickcheck in addition to proptest
- Even more fuzz targets and edge-case generators - Even more fuzz targets and edge-case generators
- MSRV policy and CI
- `#[deny(unsafe_code)]` badge
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ucobs" name = "ucobs"
version = "0.3.3" version = "0.3.4"
edition = "2021" edition = "2021"
rust-version = "1.93" rust-version = "1.93"
license = "MIT" license = "MIT"
+11 -11
View File
@@ -8,8 +8,8 @@
[Consistent Overhead Byte Stuffing](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) [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, (COBS) encodes arbitrary bytes so that `0x00` never appears in the output,
allowing `0x00` to serve as an unambiguous frame delimiter. Overhead is exactly allowing `0x00` to serve as an unambiguous frame delimiter. Overhead is at most
1 byte per 254 input bytes (worst case). 1 byte per 254 input bytes, plus 1.
## Features ## 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 - **100% spec-compliant** — Cheshire & Baker, IEEE/ACM Transactions on Networking, 1999
- **Proven interoperability** — cross-validated against `corncobs`, `cobs`, and Python `cobs` - **Proven interoperability** — cross-validated against `corncobs`, `cobs`, and Python `cobs`
- **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, miri, mutation testing, randomized payloads
## Performance ## Performance
@@ -64,16 +64,16 @@ combined). Run `just bench-size` to reproduce.
| Crate | `encode` | `decode` | Total | | 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 | | `cobs` 0.5 | 282 B | 494 B | 776 B |
| `corncobs` 0.1 | 375 B | 262 B | 637 B | | `corncobs` 0.1 | 375 B | 262 B | 637 B |
All three crates are under 1 KB. μCOBS is the largest because safe All three crates are under 1 KB. μCOBS and `cobs` are nearly tied;
`const fn` encoding with `copy_from_slice`/memcpy requires `split_at` `corncobs` is the smallest. μCOBS's safe `const fn` encoding with
bounds checks that generate panic paths — the cost of combining `copy_from_slice`/memcpy requires `split_at` bounds checks that
compile-time safety with runtime speed. Embedded users targeting size generate panic paths — the cost of combining compile-time safety with
over speed can build with `opt-level = "s"` which reduces μCOBS to runtime speed. Embedded users targeting size over speed can build with
~738 B. `opt-level = "s"` to shrink further.
### Crate properties ### 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 | | Dead-simple API (3 functions) | **yes** | yes | more surface area |
| In-place / streaming encode | no | no | **yes** | | In-place / streaming encode | no | no | **yes** |
| `no_std` + zero-alloc by default | **yes** | opt-in | **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 **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 with a `const fn` encoder, a dead-simple 3-function API, and leading or tied
+37
View File
@@ -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 = []
+6
View File
@@ -30,3 +30,9 @@ name = "fuzz_small_dest"
path = "fuzz_targets/fuzz_small_dest.rs" path = "fuzz_targets/fuzz_small_dest.rs"
test = false test = false
doc = false doc = false
[[bin]]
name = "fuzz_encode"
path = "fuzz_targets/fuzz_encode.rs"
test = false
doc = false
+33
View File
@@ -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);
}
});
+10 -3
View File
@@ -4,8 +4,8 @@
default: default:
@just --list @just --list
# run ALL quality gates (tests, clippy, fmt check, doc, fuzz, miri, mutants) # run ALL quality gates (tests, clippy, fmt check, doc, deny, fuzz, miri, mutants)
test: test-unit test-clippy test-fmt test-doc test-fuzz test-miri test-mutants test: test-unit test-clippy test-fmt test-doc test-deny test-fuzz test-miri test-mutants
# quick compile check (no tests) # quick compile check (no tests)
check: check:
@@ -14,11 +14,13 @@ check:
# unit + integration + proptest tests (via nextest for parallelism) # unit + integration + proptest tests (via nextest for parallelism)
test-unit: test-unit:
cargo nextest run cargo nextest run
cargo nextest run --features legacy-msrv
cargo test --doc cargo test --doc
# clippy with all warnings as errors # clippy with all warnings as errors
test-clippy: test-clippy:
cargo clippy -- -D warnings cargo clippy -- -D warnings
cargo clippy --features legacy-msrv -- -D warnings
# check formatting # check formatting
test-fmt: test-fmt:
@@ -26,11 +28,16 @@ test-fmt:
# doc build (catches broken doc links) # doc build (catches broken doc links)
test-doc: 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) # fuzz all targets (60s each, requires nightly + cargo-fuzz)
test-fuzz: test-fuzz:
cd fuzz && cargo +nightly fuzz run fuzz_decode -- -max_total_time=60 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_roundtrip -- -max_total_time=60
cd fuzz && cargo +nightly fuzz run fuzz_small_dest -- -max_total_time=60 cd fuzz && cargo +nightly fuzz run fuzz_small_dest -- -max_total_time=60
+4 -1
View File
@@ -1,4 +1,7 @@
[tools] [tools]
"cargo:cargo-nextest" = "latest" rust = "1.93"
"cargo:cargo-deny" = "latest"
"cargo:cargo-fuzz" = "latest"
"cargo:cargo-mutants" = "latest" "cargo:cargo-mutants" = "latest"
"cargo:cargo-nextest" = "latest"
"cargo:iai-callgrind-runner" = "latest" "cargo:iai-callgrind-runner" = "latest"
+8 -9
View File
@@ -256,14 +256,10 @@ pub const fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
} }
si += run; si += run;
if full_block { if si >= src.len() {
if si >= src.len() { break;
break; }
} if !full_block {
} else {
if si >= src.len() {
break;
}
si += 1; // skip the zero byte si += 1; // skip the zero byte
} }
} }
@@ -315,6 +311,9 @@ pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
src = rest; src = rest;
count += 1; 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 }; let zeros = if !src.is_empty() { count } else { count - 1 };
if di + zeros > dest.len() { if di + zeros > dest.len() {
return None; return None;
@@ -1259,7 +1258,7 @@ mod proptests {
shrink in 1usize..256, shrink in 1usize..256,
) { ) {
let max = max_encoded_len(data.len()); 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 mut buf = vec![0u8; dest_size];
let _ = encode(&data, &mut buf); // may return None, must not panic let _ = encode(&data, &mut buf); // may return None, must not panic
} }