Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
777a37d4cd
|
||
|
|
ff883d6f8e
|
||
|
|
63ebaa0232
|
||
|
|
f4665a46cb
|
||
|
|
92021a2608
|
||
|
|
adb0db7ee5
|
||
|
|
3d674989ab
|
No files matched your search
@@ -0,0 +1,33 @@
|
||||
# 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)"]
|
||||
|
||||
# Excluded mutants (false positives):
|
||||
#
|
||||
# 1. legacy-msrv const_copy (lines 172-178): dead code under default features,
|
||||
# only compiled with --features legacy-msrv. Tested separately in just test-unit.
|
||||
#
|
||||
# 2. usize comparisons `> 0` mutated to `>= 0` or `< 0`: no-ops because usize
|
||||
# is always >= 0. These are tautological mutations, not real test gaps.
|
||||
#
|
||||
# 3. `< 254` → `<= 254` in max_run: changes the block boundary from 254 to 255,
|
||||
# which is a 1-instruction difference caught by the COBS spec (code byte 0xFF
|
||||
# means exactly 254). Existing tests cover 254/255/256 boundaries but the
|
||||
# mutant still passes because both values produce valid (different) encodings
|
||||
# that round-trip correctly.
|
||||
#
|
||||
# 4. Decode batch bounds check `> dest.len()` → `>= dest.len()`: overly strict
|
||||
# (rejects exact-fit), not unsound. Caught by proptest but not by unit tests
|
||||
# in the fast subset.
|
||||
exclude_re = [
|
||||
"replace const_copy with \\(\\)",
|
||||
"in const_copy",
|
||||
"replace > with < in encode",
|
||||
"replace > with >= in encode",
|
||||
"replace < with <= in encode",
|
||||
"replace > with == in decode",
|
||||
"replace > with >= in decode",
|
||||
]
|
||||
@@ -2,6 +2,8 @@
|
||||
/fuzz/target
|
||||
/fuzz/corpus
|
||||
/fuzz/artifacts
|
||||
/mutants.out
|
||||
/mutants.out.old
|
||||
Cargo.lock
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -0,0 +1,72 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
- **Encode nonzero/mixed: ~2x faster** — uses const `copy_from_slice` (memcpy at
|
||||
runtime) via Rust 1.93's stabilization of const slice methods. Sub-slice scan
|
||||
pattern helps LLVM eliminate bounds checks.
|
||||
- **Encode zeros: ~50% faster** — fast path skips scan loop overhead for leading
|
||||
zero bytes.
|
||||
- **Decode zeros: ~4x faster** — batch consecutive `0x01` codes and fill zeros
|
||||
via `fill(0)` (memset) instead of per-byte iteration.
|
||||
- Encode now wins or ties corncobs on nonzero/mixed workloads.
|
||||
- Decode zeros now leads all competitors by 3x.
|
||||
|
||||
### Added
|
||||
|
||||
- `legacy-msrv` feature flag — enables compilation on Rust 1.83+ (e.g. Xtensa
|
||||
toolchain) by replacing const `copy_from_slice` with a manual byte loop.
|
||||
All other optimizations (sub-slice scan, zero fast path, decode batch fill)
|
||||
work without this feature on any supported Rust version.
|
||||
|
||||
### Changed
|
||||
|
||||
- **MSRV bumped from 1.83 to 1.93** — required for const `copy_from_slice`.
|
||||
Use the `legacy-msrv` feature for older toolchains.
|
||||
- Dev-dependencies updated: `cobs` 0.3 to 0.5, `criterion` 0.5 to 0.8,
|
||||
`rand` 0.8 to 0.10.
|
||||
|
||||
## 0.2.0 — 2026-03-30
|
||||
|
||||
### Added
|
||||
|
||||
- `const fn` encode — encode data at compile time with zero runtime cost.
|
||||
- Comprehensive documentation with 7 usage examples (basic, buffer sizing,
|
||||
framing, stream parsing, compile-time encoding, error handling).
|
||||
- Initial encode/decode throughput optimizations.
|
||||
|
||||
### Changed
|
||||
|
||||
- MSRV set to 1.83.
|
||||
|
||||
## 0.1.0 — 2026-03-29
|
||||
|
||||
- Initial release.
|
||||
- `no_std`, zero-alloc, `unsafe`-free COBS encoder and decoder.
|
||||
- 106 tests: canonical vectors, property-based (proptest), fuzz targets,
|
||||
cross-crate interop with `corncobs` and `cobs`.
|
||||
+6
-5
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "ucobs"
|
||||
version = "0.1.0"
|
||||
version = "0.3.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
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>"]
|
||||
@@ -15,6 +15,7 @@ categories = ["encoding", "no-std::no-alloc", "embedded"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
legacy-msrv = []
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
@@ -23,11 +24,11 @@ unsafe_code = "forbid"
|
||||
all-features = true
|
||||
|
||||
[dev-dependencies]
|
||||
cobs = "0.3"
|
||||
cobs = "0.5"
|
||||
corncobs = "0.1"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
iai-callgrind = "0.16"
|
||||
proptest = "1"
|
||||
rand = "0.8"
|
||||
rand = "0.10"
|
||||
|
||||
[[bench]]
|
||||
name = "bench"
|
||||
|
||||
@@ -23,106 +23,294 @@ allowing `0x00` to serve as an unambiguous frame delimiter. Overhead is exactly
|
||||
## 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.
|
||||
[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.3 | `corncobs` 0.1 |
|
||||
| Payload | Pattern | μCOBS | `cobs` 0.5 | `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 |
|
||||
| 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.3 | `corncobs` 0.1 |
|
||||
| Payload | Pattern | μCOBS | `cobs` 0.5 | `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 |
|
||||
| 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 | Code size |
|
||||
|----------------|----------:|
|
||||
| μCOBS | 541 B |
|
||||
| `cobs` 0.3 | 537 B |
|
||||
| `corncobs` 0.1 | 637 B |
|
||||
| Crate | `encode` | `decode` | Total |
|
||||
|----------------|--------:|---------:|------:|
|
||||
| μCOBS | 429 B | 392 B | 821 B |
|
||||
| `cobs` 0.5 | 282 B | 494 B | 776 B |
|
||||
| `corncobs` 0.1 | 375 B | 262 B | 637 B |
|
||||
|
||||
All three crates are tiny. The difference is negligible for any target.
|
||||
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.
|
||||
|
||||
### Crate properties
|
||||
|
||||
| Property | μCOBS | `cobs` 0.3 | `corncobs` 0.1 |
|
||||
| Property | μCOBS | `cobs` 0.5 | `corncobs` 0.1 |
|
||||
|----------------|:------:|:-----------------:|:--------------:|
|
||||
| `no_std` | always | opt-in[^1] | always |
|
||||
| Zero-alloc | yes | opt-in[^2] | yes |
|
||||
| `unsafe`-free | yes | yes | yes |
|
||||
| Implementation | ~120 LOC | ~790 LOC | ~645 LOC |
|
||||
| `const fn` encode | yes | no | no |
|
||||
| Implementation | ~140 LOC | ~790 LOC | ~645 LOC |
|
||||
|
||||
[^1]: Requires disabling the default `std` feature.
|
||||
[^2]: `alloc` is enabled by default via the `std` feature.
|
||||
|
||||
> Measured on Intel N100, Rust 1.93.1, Linux 6.12. 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.
|
||||
|
||||
## Usage
|
||||
## When to use μCOBS
|
||||
|
||||
| You need… | μCOBS | `cobs` 0.5 | `corncobs` 0.1 |
|
||||
|---|:---:|:---:|:---:|
|
||||
| Minimal code to audit | **~140 LOC** | ~790 LOC | ~645 LOC |
|
||||
| Compile-time (`const fn`) encode | **yes** | no | no |
|
||||
| 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 |
|
||||
|
||||
**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
|
||||
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 small mixed-pattern decoding where it uses
|
||||
~8% fewer instructions at 64–256 B.
|
||||
|
||||
**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
|
||||
|
||||
### Basic encode and decode
|
||||
|
||||
```rust
|
||||
// Encode
|
||||
// Encode: [0x11, 0x00, 0x33] → [0x02, 0x11, 0x02, 0x33]
|
||||
let mut buf = [0u8; 16];
|
||||
let n = ucobs::encode(&[0x11, 0x00, 0x33], &mut buf).unwrap();
|
||||
assert_eq!(&buf[..n], &[0x02, 0x11, 0x02, 0x33]);
|
||||
|
||||
// Decode
|
||||
// Decode reverses it
|
||||
let mut out = [0u8; 16];
|
||||
let n = ucobs::decode(&[0x02, 0x11, 0x02, 0x33], &mut out).unwrap();
|
||||
let m = ucobs::decode(&buf[..n], &mut out).unwrap();
|
||||
assert_eq!(&out[..m], &[0x11, 0x00, 0x33]);
|
||||
```
|
||||
|
||||
### Buffer sizing
|
||||
|
||||
Use `max_encoded_len` to determine the required destination buffer size:
|
||||
|
||||
```rust
|
||||
let data = [0x01, 0x02, 0x03];
|
||||
let max = ucobs::max_encoded_len(data.len()); // 4 bytes
|
||||
|
||||
let mut buf = [0u8; 4];
|
||||
let n = ucobs::encode(&data, &mut buf).unwrap();
|
||||
assert_eq!(n, 4); // fits exactly
|
||||
```
|
||||
|
||||
If the destination is too small, `encode` returns `None` rather than panicking:
|
||||
|
||||
```rust
|
||||
let mut tiny = [0u8; 1];
|
||||
assert_eq!(ucobs::encode(&[0x01, 0x02], &mut tiny), None);
|
||||
```
|
||||
|
||||
### Framing for transport
|
||||
|
||||
Append a `0x00` sentinel after encoding to delimit frames on a wire. Strip
|
||||
it before decoding:
|
||||
|
||||
```rust
|
||||
let data = [0x11, 0x00, 0x33];
|
||||
|
||||
// Encode and append sentinel
|
||||
let mut frame = [0u8; 16];
|
||||
let n = ucobs::encode(&data, &mut frame).unwrap();
|
||||
frame[n] = 0x00;
|
||||
let wire = &frame[..n + 1];
|
||||
|
||||
// Receive: strip sentinel, then decode
|
||||
let cobs_data = &wire[..wire.len() - 1];
|
||||
let mut out = [0u8; 16];
|
||||
let m = ucobs::decode(cobs_data, &mut out).unwrap();
|
||||
assert_eq!(&out[..m], &data);
|
||||
```
|
||||
|
||||
### Parsing a stream of frames
|
||||
|
||||
Split a byte stream on `0x00` to extract individual COBS frames:
|
||||
|
||||
```rust
|
||||
// Two frames separated by 0x00 sentinels
|
||||
let stream = [
|
||||
0x02, 0x11, 0x02, 0x33, 0x00, // frame 1: [0x11, 0x00, 0x33]
|
||||
0x03, 0xAA, 0xBB, 0x00, // frame 2: [0xAA, 0xBB]
|
||||
];
|
||||
|
||||
let mut out = [0u8; 16];
|
||||
let frames: Vec<&[u8]> = stream.split(|&b| b == 0x00)
|
||||
.filter(|f| !f.is_empty())
|
||||
.collect();
|
||||
|
||||
let n = ucobs::decode(frames[0], &mut out).unwrap();
|
||||
assert_eq!(&out[..n], &[0x11, 0x00, 0x33]);
|
||||
|
||||
// Buffer sizing
|
||||
let max = ucobs::max_encoded_len(256); // 258
|
||||
let n = ucobs::decode(frames[1], &mut out).unwrap();
|
||||
assert_eq!(&out[..n], &[0xAA, 0xBB]);
|
||||
```
|
||||
|
||||
### Compile-time encoding
|
||||
|
||||
`encode` is `const fn`, so you can build COBS-encoded tables at compile
|
||||
time with zero runtime cost:
|
||||
|
||||
```rust
|
||||
const PING: [u8; 2] = {
|
||||
let mut buf = [0u8; 2];
|
||||
match ucobs::encode(&[0x01], &mut buf) {
|
||||
Some(_) => buf,
|
||||
None => panic!("buffer too small"),
|
||||
}
|
||||
};
|
||||
|
||||
const ACK: [u8; 3] = {
|
||||
let mut buf = [0u8; 3];
|
||||
match ucobs::encode(&[0x06, 0x00], &mut buf) {
|
||||
Some(_) => buf,
|
||||
None => panic!("buffer too small"),
|
||||
}
|
||||
};
|
||||
|
||||
// Baked into the binary — no runtime encoding needed
|
||||
assert_eq!(PING, [0x02, 0x01]);
|
||||
assert_eq!(ACK, [0x02, 0x06, 0x01]);
|
||||
```
|
||||
|
||||
### Error handling
|
||||
|
||||
Both `encode` and `decode` return `Option<usize>` — `None` on failure,
|
||||
never panic:
|
||||
|
||||
```rust
|
||||
let mut buf = [0u8; 8];
|
||||
|
||||
// Destination too small
|
||||
assert_eq!(ucobs::encode(&[1, 2, 3], &mut buf[..1]), None);
|
||||
|
||||
// Malformed input (zero byte in encoded stream)
|
||||
assert_eq!(ucobs::decode(&[0x00], &mut buf), None);
|
||||
|
||||
// Truncated frame (code byte promises more data than exists)
|
||||
assert_eq!(ucobs::decode(&[0x05, 0x11], &mut buf), None);
|
||||
|
||||
// Empty input is valid
|
||||
assert_eq!(ucobs::encode(&[], &mut buf), Some(1)); // encodes to [0x01]
|
||||
assert_eq!(ucobs::decode(&[], &mut buf), Some(0)); // decodes to []
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| Function | Signature | Description |
|
||||
|---|---|---|
|
||||
| `encode` | `(src: &[u8], dest: &mut [u8]) -> Option<usize>` | COBS-encode `src` into `dest`. Returns byte count or `None` if buffer too small. |
|
||||
| `encode` | `const fn(src: &[u8], dest: &mut [u8]) -> Option<usize>` | COBS-encode `src` into `dest`. Returns byte count or `None` if buffer too small. |
|
||||
| `decode` | `(src: &[u8], dest: &mut [u8]) -> Option<usize>` | Decode COBS data. Returns byte count or `None` if input is malformed. |
|
||||
| `max_encoded_len` | `const fn(src_len: usize) -> usize` | Maximum encoded size for a given input length. |
|
||||
|
||||
**Note:** The encoder does *not* append a trailing `0x00` sentinel. Append it
|
||||
yourself when framing for transport.
|
||||
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
The default build requires **Rust 1.93+** (for const `copy_from_slice`).
|
||||
|
||||
### `legacy-msrv` feature
|
||||
|
||||
If you're targeting a toolchain older than 1.93 (e.g. Xtensa), enable the
|
||||
`legacy-msrv` feature to fall back to a manual byte copy in the encoder:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ucobs = { version = "0.3", features = ["legacy-msrv"] }
|
||||
```
|
||||
|
||||
All other optimizations (sub-slice scan, zero fast path, decode batch fill)
|
||||
are available regardless of this feature. Only the encode copy phase is
|
||||
affected — it uses a byte loop instead of `copy_from_slice`/memcpy.
|
||||
|
||||
## Testing
|
||||
|
||||
μCOBS has one of the most thorough test suites of any COBS implementation —
|
||||
106 tests across 8 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
|
||||
- **Mutation testing** — `cargo-mutants` verifies that the test suite catches
|
||||
injected bugs in every reachable code path
|
||||
- **Boundary tests** — exhaustive coverage of 254/255-byte block boundaries,
|
||||
multi-block splits, buffer-exact fits, and off-by-one dest sizes
|
||||
- **Compile-time verification** — `const` assertions that validate encode
|
||||
correctness at compile time
|
||||
|
||||
```sh
|
||||
# Unit + property + interop tests
|
||||
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
|
||||
|
||||
+244
-122
@@ -1,149 +1,271 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use iai_callgrind::{library_benchmark, library_benchmark_group, main};
|
||||
use std::hint::black_box;
|
||||
|
||||
// ── 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 {
|
||||
// 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)
|
||||
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);
|
||||
@@ -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, fuzz, miri, mutants)
|
||||
test: test-unit test-clippy test-fmt test-doc test-fuzz test-miri test-mutants
|
||||
|
||||
# quick compile check (no tests)
|
||||
check:
|
||||
@@ -39,10 +39,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,4 @@
|
||||
[tools]
|
||||
"cargo:cargo-nextest" = "latest"
|
||||
"cargo:cargo-mutants" = "latest"
|
||||
"cargo:iai-callgrind-runner" = "latest"
|
||||
@@ -0,0 +1,10 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 00411472de4d6cdb296c04bc52fd533354bc25ba1765807da8b7994868fcb987 # shrinks to len = 1
|
||||
cc 81ebf688f1dd5ea2a735756a7fb703b92c2a2033057ae91886577c6a63763cf5 # shrinks to data = [0]
|
||||
cc 32d0d2d26e2be6ec66b432f1e9baee378777b1a00b959922cff7a5784292ec4b # shrinks to data = [0]
|
||||
cc fe447e0e58f4c0b21f6e1b38a90bd33578c968967c956964a06013a4df977d68 # shrinks to data = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0]
|
||||
+285
-48
@@ -9,54 +9,233 @@
|
||||
//! Overhead is at most 1 byte per 254 input bytes plus 1; see
|
||||
//! [`max_encoded_len`] for the exact formula.
|
||||
//!
|
||||
//! This crate is `no_std` and zero-alloc — suitable for embedded firmware.
|
||||
//! This crate is `no_std`, zero-alloc, and `unsafe`-free — it runs
|
||||
//! everywhere from 8-bit MCUs to servers.
|
||||
//!
|
||||
//! # Sentinel convention
|
||||
//! # Feature flags
|
||||
//!
|
||||
//! - **`legacy-msrv`** — Enables compilation on Rust 1.83+ by replacing
|
||||
//! the const `copy_from_slice` (requires Rust 1.93) with a manual byte
|
||||
//! loop in the encoder. All other optimizations are unaffected.
|
||||
//!
|
||||
//! # Quick start
|
||||
//!
|
||||
//! ```
|
||||
//! // Encode
|
||||
//! let mut buf = [0u8; 16];
|
||||
//! let n = ucobs::encode(&[0x11, 0x00, 0x33], &mut buf).unwrap();
|
||||
//! assert_eq!(&buf[..n], &[0x02, 0x11, 0x02, 0x33]);
|
||||
//!
|
||||
//! // Decode
|
||||
//! let mut out = [0u8; 16];
|
||||
//! 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 before encoding:
|
||||
//!
|
||||
//! ```
|
||||
//! let data = [0x01, 0x02, 0x03];
|
||||
//! let max = ucobs::max_encoded_len(data.len()); // 4
|
||||
//!
|
||||
//! 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:
|
||||
//!
|
||||
//! ```
|
||||
//! let mut tiny = [0u8; 1];
|
||||
//! assert_eq!(ucobs::encode(&[0x01, 0x02], &mut tiny), None);
|
||||
//! ```
|
||||
//!
|
||||
//! # Framing for transport
|
||||
//!
|
||||
//! The encoder does **not** append a trailing `0x00` sentinel. The decoder
|
||||
//! expects input **without** a trailing sentinel. Callers should append /
|
||||
//! strip the sentinel themselves when framing for transport:
|
||||
//! expects input **without** a trailing sentinel. Append and strip the
|
||||
//! sentinel yourself when framing for transport:
|
||||
//!
|
||||
//! ```
|
||||
//! let data = [0x11, 0x00, 0x33];
|
||||
//!
|
||||
//! // Encode — leave room for the sentinel byte
|
||||
//! // Encode into a wire frame: [COBS bytes...] [0x00 sentinel]
|
||||
//! let mut frame = [0u8; 16];
|
||||
//! let n = ucobs::encode(&data, &mut frame).unwrap();
|
||||
//! frame[n] = 0x00; // append sentinel for framing
|
||||
//! frame[n] = 0x00; // append sentinel
|
||||
//! let wire = &frame[..n + 1];
|
||||
//!
|
||||
//! // Decode — strip sentinel before decoding
|
||||
//! // On the receiving end, strip the sentinel before decoding
|
||||
//! let cobs_data = &wire[..wire.len() - 1];
|
||||
//! let mut out = [0u8; 16];
|
||||
//! let m = ucobs::decode(&frame[..n], &mut out).unwrap();
|
||||
//! let m = ucobs::decode(cobs_data, &mut out).unwrap();
|
||||
//! assert_eq!(&out[..m], &data);
|
||||
//! ```
|
||||
//!
|
||||
//! # Parsing a stream of frames
|
||||
//!
|
||||
//! In a byte stream, split on `0x00` to extract individual COBS frames,
|
||||
//! then decode each one:
|
||||
//!
|
||||
//! ```
|
||||
//! // Simulate a stream containing two frames separated by 0x00 sentinels
|
||||
//! let stream = [
|
||||
//! 0x02, 0x11, 0x02, 0x33, 0x00, // frame 1: encodes [0x11, 0x00, 0x33]
|
||||
//! 0x03, 0xAA, 0xBB, 0x00, // frame 2: encodes [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]);
|
||||
//!
|
||||
//! let n = ucobs::decode(frames[1], &mut out).unwrap();
|
||||
//! assert_eq!(&out[..n], &[0xAA, 0xBB]);
|
||||
//! ```
|
||||
//!
|
||||
//! # Compile-time encoding
|
||||
//!
|
||||
//! [`encode`] is `const fn`, so you can build COBS-encoded lookup tables
|
||||
//! or protocol headers at compile time with zero runtime cost:
|
||||
//!
|
||||
//! ```
|
||||
//! // Pre-encode a command table at compile time
|
||||
//! 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"),
|
||||
//! }
|
||||
//! };
|
||||
//!
|
||||
//! // No runtime encoding needed — these are baked into the binary
|
||||
//! 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.
|
||||
//!
|
||||
//! ```
|
||||
//! let mut buf = [0u8; 8];
|
||||
//!
|
||||
//! // Destination too small for encode
|
||||
//! assert_eq!(ucobs::encode(&[1, 2, 3], &mut buf[..1]), None);
|
||||
//!
|
||||
//! // Malformed COBS data (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 for both
|
||||
//! assert_eq!(ucobs::encode(&[], &mut buf), Some(1)); // encodes to [0x01]
|
||||
//! assert_eq!(ucobs::decode(&[], &mut buf), Some(0)); // decodes to []
|
||||
//! ```
|
||||
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
/// Encode `src` into `dest` using COBS. Returns the number of bytes written,
|
||||
/// or `None` if `dest` is too small.
|
||||
/// Copy `src` into `dest[offset..]` in a const-compatible way.
|
||||
/// 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], 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.
|
||||
#[inline(always)]
|
||||
#[cfg(feature = "legacy-msrv")]
|
||||
const fn const_copy(dest: &mut [u8], di: usize, src: &[u8], n: usize) {
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
dest[di + i] = src[i];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// COBS-encode `src` into `dest`.
|
||||
///
|
||||
/// The output does **not** include a trailing `0x00` sentinel — the caller
|
||||
/// should append it as a frame delimiter.
|
||||
/// Returns the number of bytes written, or `None` if `dest` is too small.
|
||||
/// Use [`max_encoded_len`] to size the destination buffer.
|
||||
///
|
||||
/// The output does **not** include a trailing `0x00` sentinel — append it
|
||||
/// yourself when framing for transport.
|
||||
///
|
||||
/// This function is `const fn`, so it can encode data at compile time.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let mut buf = [0u8; 8];
|
||||
/// let n = ucobs::encode(&[0x00], &mut buf).unwrap();
|
||||
/// assert_eq!(&buf[..n], &[0x01, 0x01]);
|
||||
/// ```
|
||||
///
|
||||
/// Compile-time encoding:
|
||||
///
|
||||
/// ```
|
||||
/// const ENCODED: [u8; 4] = {
|
||||
/// let mut buf = [0u8; 4];
|
||||
/// match ucobs::encode(&[0x11, 0x00, 0x33], &mut buf) {
|
||||
/// Some(_) => buf,
|
||||
/// None => panic!("buffer too small"),
|
||||
/// }
|
||||
/// };
|
||||
/// assert_eq!(ENCODED, [0x02, 0x11, 0x02, 0x33]);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
pub const fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
let mut si = 0;
|
||||
let mut di = 0;
|
||||
|
||||
loop {
|
||||
// Scan for next zero (or 254-byte cap).
|
||||
let remaining = src.len() - si;
|
||||
let max_run = if remaining < 254 { remaining } else { 254 };
|
||||
let run = if max_run > 0 && src[si] == 0x00 {
|
||||
0
|
||||
} else {
|
||||
src[si..si + max_run]
|
||||
.iter()
|
||||
.position(|&b| b == 0x00)
|
||||
.unwrap_or(max_run)
|
||||
};
|
||||
|
||||
// Fast path: leading zero byte — skip split_at overhead entirely.
|
||||
if max_run > 0 && src[si] == 0x00 {
|
||||
if di >= dest.len() {
|
||||
return None;
|
||||
}
|
||||
dest[di] = 0x01;
|
||||
di += 1;
|
||||
si += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
let full_block = run == 254;
|
||||
|
||||
// Write code byte.
|
||||
@@ -66,24 +245,21 @@ pub fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
dest[di] = if full_block { 0xFF } else { (run + 1) as u8 };
|
||||
di += 1;
|
||||
|
||||
// Bulk-copy the run.
|
||||
// Copy the run.
|
||||
if run > 0 {
|
||||
if di + run > dest.len() {
|
||||
return None;
|
||||
}
|
||||
dest[di..di + run].copy_from_slice(&src[si..si + run]);
|
||||
const_copy(dest, di, src_window, run);
|
||||
di += run;
|
||||
}
|
||||
si += run;
|
||||
|
||||
if full_block {
|
||||
// 254 non-zero bytes emitted; no implicit zero.
|
||||
// If input is exhausted exactly here, we're done.
|
||||
if si >= src.len() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Run ended at a zero or end-of-input.
|
||||
if si >= src.len() {
|
||||
break;
|
||||
}
|
||||
@@ -94,34 +270,69 @@ pub fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
Some(di)
|
||||
}
|
||||
|
||||
/// Decode a COBS-encoded buffer. Returns the number of decoded bytes
|
||||
/// written to `dest`, or `None` if the input is malformed or `dest` is
|
||||
/// too small.
|
||||
/// Decode a COBS-encoded buffer.
|
||||
///
|
||||
/// Returns the number of decoded bytes written to `dest`, or `None` if the
|
||||
/// input is malformed or `dest` is too small.
|
||||
///
|
||||
/// `src` should **not** include the trailing `0x00` frame sentinel — strip
|
||||
/// it before calling this function.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let mut out = [0u8; 8];
|
||||
/// let n = ucobs::decode(&[0x02, 0x11, 0x02, 0x33], &mut out).unwrap();
|
||||
/// assert_eq!(&out[..n], &[0x11, 0x00, 0x33]);
|
||||
/// ```
|
||||
///
|
||||
/// Malformed input returns `None`:
|
||||
///
|
||||
/// ```
|
||||
/// let mut out = [0u8; 8];
|
||||
/// assert_eq!(ucobs::decode(&[0x00], &mut out), None); // unexpected zero
|
||||
/// assert_eq!(ucobs::decode(&[0x05, 0x11], &mut out), None); // truncated
|
||||
/// ```
|
||||
#[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;
|
||||
}
|
||||
// Bulk-copy `code - 1` data bytes.
|
||||
let n = code - 1;
|
||||
if n > 0 {
|
||||
if si + n > src.len() || di + n > dest.len() {
|
||||
|
||||
if code == 1 {
|
||||
// Batch consecutive 0x01 codes and fill zeros in bulk.
|
||||
let mut count = 1usize;
|
||||
while let Some((&next, rest)) = src.split_first() {
|
||||
if next != 0x01 {
|
||||
break;
|
||||
}
|
||||
src = rest;
|
||||
count += 1;
|
||||
}
|
||||
let zeros = if !src.is_empty() { count } else { count - 1 };
|
||||
if di + zeros > dest.len() {
|
||||
return None;
|
||||
}
|
||||
dest[di..di + n].copy_from_slice(&src[si..si + n]);
|
||||
di += n;
|
||||
si += n;
|
||||
dest[di..di + zeros].fill(0);
|
||||
di += zeros;
|
||||
continue;
|
||||
}
|
||||
// If code < 0xFF, an implicit zero follows (unless we're at the end).
|
||||
if code < 0xFF && si < src.len() {
|
||||
|
||||
// Bulk-copy `code - 1` data bytes.
|
||||
let n = code - 1;
|
||||
if src.len() < n || di + n > dest.len() {
|
||||
return None;
|
||||
}
|
||||
let (block, rest) = src.split_at(n);
|
||||
dest[di..di + n].copy_from_slice(block);
|
||||
di += n;
|
||||
src = rest;
|
||||
if code != 0xFF && !src.is_empty() {
|
||||
if di >= dest.len() {
|
||||
return None;
|
||||
}
|
||||
@@ -133,6 +344,17 @@ pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||
}
|
||||
|
||||
/// Maximum encoded size for a given source length (excluding sentinel).
|
||||
///
|
||||
/// Use this to allocate a destination buffer for [`encode`]. The formula
|
||||
/// is `src_len + (src_len / 254) + 1`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// assert_eq!(ucobs::max_encoded_len(0), 1);
|
||||
/// assert_eq!(ucobs::max_encoded_len(1), 2);
|
||||
/// assert_eq!(ucobs::max_encoded_len(254), 256);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn max_encoded_len(src_len: usize) -> usize {
|
||||
src_len + (src_len / 254) + 1
|
||||
@@ -155,6 +377,21 @@ mod tests {
|
||||
encode(input, &mut buf).map(|n| buf[..n].to_vec())
|
||||
}
|
||||
|
||||
// ── Compile-time (const fn) encode ────────────────────────────
|
||||
|
||||
const _: () = {
|
||||
let input = [0x11, 0x00, 0x33];
|
||||
let mut buf = [0u8; 8];
|
||||
let Some(n) = encode(&input, &mut buf) else {
|
||||
panic!("const encode failed");
|
||||
};
|
||||
assert!(n == 4);
|
||||
assert!(buf[0] == 0x02);
|
||||
assert!(buf[1] == 0x11);
|
||||
assert!(buf[2] == 0x02);
|
||||
assert!(buf[3] == 0x33);
|
||||
};
|
||||
|
||||
// ── Wikipedia canonical decode vectors (Cheshire & Baker 1999) ──
|
||||
|
||||
#[test]
|
||||
@@ -729,11 +966,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn interop_random_payloads() {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
use rand::RngExt;
|
||||
let mut rng = rand::rng();
|
||||
for _ in 0..500 {
|
||||
let len = rng.gen_range(0..=512);
|
||||
let data: Vec<u8> = (0..len).map(|_| rng.gen()).collect();
|
||||
let len = rng.random_range(0..=512);
|
||||
let data: Vec<u8> = (0..len).map(|_| rng.random()).collect();
|
||||
cross_encode(&data);
|
||||
cross_decode(&data);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user