feat: const fn encode, decode optimization, comprehensive docs and examples
- Make `encode` const fn for compile-time COBS encoding (MSRV bumped to 1.83) - Optimize decode by removing unnecessary branch in inner loop - Add comprehensive rustdoc with examples for every function - Add crate-level doc sections: quick start, buffer sizing, framing, stream parsing, compile-time encoding, error handling - Expand README with decision matrix, examples section, testing depth - Bump version to 0.2.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
3 files changed
+379
-42
No files matched your search
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ucobs"
|
name = "ucobs"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.56"
|
rust-version = "1.83"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "μCOBS — Consistent Overhead Byte Stuffing. no_std, zero-alloc, fastest, most-tested COBS implementation."
|
description = "μCOBS — Consistent Overhead Byte Stuffing. no_std, zero-alloc, fastest, most-tested COBS implementation."
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ All three crates are tiny. The difference is negligible for any target.
|
|||||||
| `no_std` | always | opt-in[^1] | always |
|
| `no_std` | always | opt-in[^1] | always |
|
||||||
| Zero-alloc | yes | opt-in[^2] | yes |
|
| Zero-alloc | yes | opt-in[^2] | yes |
|
||||||
| `unsafe`-free | yes | yes | yes |
|
| `unsafe`-free | yes | yes | yes |
|
||||||
|
| `const fn` encode | yes | no | no |
|
||||||
| Implementation | ~120 LOC | ~790 LOC | ~645 LOC |
|
| Implementation | ~120 LOC | ~790 LOC | ~645 LOC |
|
||||||
|
|
||||||
[^1]: Requires disabling the default `std` feature.
|
[^1]: Requires disabling the default `std` feature.
|
||||||
@@ -84,28 +85,164 @@ All three crates are tiny. The difference is negligible for any target.
|
|||||||
> Measured on Intel N100, Rust 1.93.1, Linux 6.12. Run `just bench` to
|
> 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/`.
|
> reproduce. Full Criterion reports are generated in `target/criterion/report/`.
|
||||||
|
|
||||||
## Usage
|
## When to use μCOBS
|
||||||
|
|
||||||
|
| You need… | μCOBS | `cobs` 0.3 | `corncobs` 0.1 |
|
||||||
|
|---|:---:|:---:|:---:|
|
||||||
|
| Minimal code to audit | **~120 LOC** | ~790 LOC | ~645 LOC |
|
||||||
|
| Compile-time (`const fn`) encode | **yes** | no | no |
|
||||||
|
| Fastest encode (zero-heavy data) | **3–4×** faster | baseline | slowest |
|
||||||
|
| Fastest decode (large payloads) | competitive | slow | **slight edge** |
|
||||||
|
| Dead-simple API (3 functions) | **yes** | yes | more surface area |
|
||||||
|
| In-place / streaming encode | no | no | **yes** |
|
||||||
|
| `no_std` + zero-alloc by default | **yes** | opt-in | **yes** |
|
||||||
|
| Thorough test suite | **106 tests, fuzz, proptest** | basic | basic |
|
||||||
|
|
||||||
|
**Choose μCOBS** if you want the smallest, most auditable COBS implementation
|
||||||
|
with a minimal API and strong performance — especially on embedded targets or
|
||||||
|
payloads containing many zero bytes.
|
||||||
|
|
||||||
|
**Choose `corncobs`** if you need in-place encoding, an iterator-based API, or
|
||||||
|
your workload is dominated by large non-zero payloads where it holds a slight
|
||||||
|
throughput edge.
|
||||||
|
|
||||||
|
**Choose `cobs`** if you need `std` convenience features and don't care about
|
||||||
|
`no_std` or encode performance.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic encode and decode
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Encode
|
// Encode: [0x11, 0x00, 0x33] → [0x02, 0x11, 0x02, 0x33]
|
||||||
let mut buf = [0u8; 16];
|
let mut buf = [0u8; 16];
|
||||||
let n = ucobs::encode(&[0x11, 0x00, 0x33], &mut buf).unwrap();
|
let n = ucobs::encode(&[0x11, 0x00, 0x33], &mut buf).unwrap();
|
||||||
assert_eq!(&buf[..n], &[0x02, 0x11, 0x02, 0x33]);
|
assert_eq!(&buf[..n], &[0x02, 0x11, 0x02, 0x33]);
|
||||||
|
|
||||||
// Decode
|
// Decode reverses it
|
||||||
let mut out = [0u8; 16];
|
let mut out = [0u8; 16];
|
||||||
let n = ucobs::decode(&[0x02, 0x11, 0x02, 0x33], &mut out).unwrap();
|
let m = ucobs::decode(&buf[..n], &mut out).unwrap();
|
||||||
|
assert_eq!(&out[..m], &[0x11, 0x00, 0x33]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Buffer sizing
|
||||||
|
|
||||||
|
Use `max_encoded_len` to determine the required destination buffer size:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let data = [0x01, 0x02, 0x03];
|
||||||
|
let max = ucobs::max_encoded_len(data.len()); // 4 bytes
|
||||||
|
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
let n = ucobs::encode(&data, &mut buf).unwrap();
|
||||||
|
assert_eq!(n, 4); // fits exactly
|
||||||
|
```
|
||||||
|
|
||||||
|
If the destination is too small, `encode` returns `None` rather than panicking:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut tiny = [0u8; 1];
|
||||||
|
assert_eq!(ucobs::encode(&[0x01, 0x02], &mut tiny), None);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Framing for transport
|
||||||
|
|
||||||
|
Append a `0x00` sentinel after encoding to delimit frames on a wire. Strip
|
||||||
|
it before decoding:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let data = [0x11, 0x00, 0x33];
|
||||||
|
|
||||||
|
// Encode and append sentinel
|
||||||
|
let mut frame = [0u8; 16];
|
||||||
|
let n = ucobs::encode(&data, &mut frame).unwrap();
|
||||||
|
frame[n] = 0x00;
|
||||||
|
let wire = &frame[..n + 1];
|
||||||
|
|
||||||
|
// Receive: strip sentinel, then decode
|
||||||
|
let cobs_data = &wire[..wire.len() - 1];
|
||||||
|
let mut out = [0u8; 16];
|
||||||
|
let m = ucobs::decode(cobs_data, &mut out).unwrap();
|
||||||
|
assert_eq!(&out[..m], &data);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parsing a stream of frames
|
||||||
|
|
||||||
|
Split a byte stream on `0x00` to extract individual COBS frames:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Two frames separated by 0x00 sentinels
|
||||||
|
let stream = [
|
||||||
|
0x02, 0x11, 0x02, 0x33, 0x00, // frame 1: [0x11, 0x00, 0x33]
|
||||||
|
0x03, 0xAA, 0xBB, 0x00, // frame 2: [0xAA, 0xBB]
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut out = [0u8; 16];
|
||||||
|
let frames: Vec<&[u8]> = stream.split(|&b| b == 0x00)
|
||||||
|
.filter(|f| !f.is_empty())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let n = ucobs::decode(frames[0], &mut out).unwrap();
|
||||||
assert_eq!(&out[..n], &[0x11, 0x00, 0x33]);
|
assert_eq!(&out[..n], &[0x11, 0x00, 0x33]);
|
||||||
|
|
||||||
// Buffer sizing
|
let n = ucobs::decode(frames[1], &mut out).unwrap();
|
||||||
let max = ucobs::max_encoded_len(256); // 258
|
assert_eq!(&out[..n], &[0xAA, 0xBB]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile-time encoding
|
||||||
|
|
||||||
|
`encode` is `const fn`, so you can build COBS-encoded tables at compile
|
||||||
|
time with zero runtime cost:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
const PING: [u8; 2] = {
|
||||||
|
let mut buf = [0u8; 2];
|
||||||
|
match ucobs::encode(&[0x01], &mut buf) {
|
||||||
|
Some(_) => buf,
|
||||||
|
None => panic!("buffer too small"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACK: [u8; 3] = {
|
||||||
|
let mut buf = [0u8; 3];
|
||||||
|
match ucobs::encode(&[0x06, 0x00], &mut buf) {
|
||||||
|
Some(_) => buf,
|
||||||
|
None => panic!("buffer too small"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Baked into the binary — no runtime encoding needed
|
||||||
|
assert_eq!(PING, [0x02, 0x01]);
|
||||||
|
assert_eq!(ACK, [0x02, 0x06, 0x01]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error handling
|
||||||
|
|
||||||
|
Both `encode` and `decode` return `Option<usize>` — `None` on failure,
|
||||||
|
never panic:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut buf = [0u8; 8];
|
||||||
|
|
||||||
|
// Destination too small
|
||||||
|
assert_eq!(ucobs::encode(&[1, 2, 3], &mut buf[..1]), None);
|
||||||
|
|
||||||
|
// Malformed input (zero byte in encoded stream)
|
||||||
|
assert_eq!(ucobs::decode(&[0x00], &mut buf), None);
|
||||||
|
|
||||||
|
// Truncated frame (code byte promises more data than exists)
|
||||||
|
assert_eq!(ucobs::decode(&[0x05, 0x11], &mut buf), None);
|
||||||
|
|
||||||
|
// Empty input is valid
|
||||||
|
assert_eq!(ucobs::encode(&[], &mut buf), Some(1)); // encodes to [0x01]
|
||||||
|
assert_eq!(ucobs::decode(&[], &mut buf), Some(0)); // decodes to []
|
||||||
```
|
```
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
| Function | Signature | Description |
|
| Function | Signature | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `encode` | `(src: &[u8], dest: &mut [u8]) -> Option<usize>` | COBS-encode `src` into `dest`. Returns byte count or `None` if buffer too small. |
|
| `encode` | `const fn(src: &[u8], dest: &mut [u8]) -> Option<usize>` | COBS-encode `src` into `dest`. Returns byte count or `None` if buffer too small. |
|
||||||
| `decode` | `(src: &[u8], dest: &mut [u8]) -> Option<usize>` | Decode COBS data. Returns byte count or `None` if input is malformed. |
|
| `decode` | `(src: &[u8], dest: &mut [u8]) -> Option<usize>` | Decode COBS data. Returns byte count or `None` if input is malformed. |
|
||||||
| `max_encoded_len` | `const fn(src_len: usize) -> usize` | Maximum encoded size for a given input length. |
|
| `max_encoded_len` | `const fn(src_len: usize) -> usize` | Maximum encoded size for a given input length. |
|
||||||
|
|
||||||
@@ -114,6 +251,25 @@ yourself when framing for transport.
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
μCOBS has one of the most thorough test suites of any COBS implementation —
|
||||||
|
106 tests across 7 categories:
|
||||||
|
|
||||||
|
- **Canonical vectors** — every example from Cheshire & Baker 1999 (the original
|
||||||
|
COBS paper), plus the full Wikipedia vector set
|
||||||
|
- **External corpora** — vectors drawn from `cobs-c`, `nanocobs`, `cobs2-rs`,
|
||||||
|
Jacques Fortier's C implementation, and the Python `cobs` package
|
||||||
|
- **Cross-crate interop** — byte-for-byte validation against both `corncobs` and
|
||||||
|
`cobs` crates on randomized payloads up to 4 KB
|
||||||
|
- **Property-based tests** — 6 proptest suites covering round-trip correctness,
|
||||||
|
no-zero-in-output invariants, encoding properties, decoding properties,
|
||||||
|
structural/algebraic properties, and boundary-region behavior
|
||||||
|
- **Fuzz targets** — 3 `cargo-fuzz` harnesses (decode, round-trip, small-dest)
|
||||||
|
for continuous coverage of edge cases
|
||||||
|
- **Boundary tests** — exhaustive coverage of 254/255-byte block boundaries,
|
||||||
|
multi-block splits, buffer-exact fits, and off-by-one dest sizes
|
||||||
|
- **Compile-time verification** — `const` assertions that validate encode
|
||||||
|
correctness at compile time
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Unit + property + interop tests
|
# Unit + property + interop tests
|
||||||
just test-unit
|
just test-unit
|
||||||
|
|||||||
+214
-33
@@ -9,39 +9,177 @@
|
|||||||
//! Overhead is at most 1 byte per 254 input bytes plus 1; see
|
//! Overhead is at most 1 byte per 254 input bytes plus 1; see
|
||||||
//! [`max_encoded_len`] for the exact formula.
|
//! [`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
|
//! # 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
|
//! The encoder does **not** append a trailing `0x00` sentinel. The decoder
|
||||||
//! expects input **without** a trailing sentinel. Callers should append /
|
//! expects input **without** a trailing sentinel. Append and strip the
|
||||||
//! strip the sentinel themselves when framing for transport:
|
//! sentinel yourself when framing for transport:
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! let data = [0x11, 0x00, 0x33];
|
//! 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 mut frame = [0u8; 16];
|
||||||
//! let n = ucobs::encode(&data, &mut frame).unwrap();
|
//! 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 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);
|
//! 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]
|
#![no_std]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
/// Encode `src` into `dest` using COBS. Returns the number of bytes written,
|
/// COBS-encode `src` into `dest`.
|
||||||
/// or `None` if `dest` is too small.
|
|
||||||
///
|
///
|
||||||
/// The output does **not** include a trailing `0x00` sentinel — the caller
|
/// Returns the number of bytes written, or `None` if `dest` is too small.
|
||||||
/// should append it as a frame delimiter.
|
/// 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]
|
#[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 si = 0;
|
||||||
let mut di = 0;
|
let mut di = 0;
|
||||||
|
|
||||||
@@ -49,14 +187,12 @@ pub fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
|||||||
// Scan for next zero (or 254-byte cap).
|
// Scan for next zero (or 254-byte cap).
|
||||||
let remaining = src.len() - si;
|
let remaining = src.len() - si;
|
||||||
let max_run = if remaining < 254 { remaining } else { 254 };
|
let max_run = if remaining < 254 { remaining } else { 254 };
|
||||||
let run = if max_run > 0 && src[si] == 0x00 {
|
let mut run = 0;
|
||||||
0
|
if max_run > 0 && src[si] != 0x00 {
|
||||||
} else {
|
while run < max_run && src[si + run] != 0x00 {
|
||||||
src[si..si + max_run]
|
run += 1;
|
||||||
.iter()
|
}
|
||||||
.position(|&b| b == 0x00)
|
}
|
||||||
.unwrap_or(max_run)
|
|
||||||
};
|
|
||||||
let full_block = run == 254;
|
let full_block = run == 254;
|
||||||
|
|
||||||
// Write code byte.
|
// Write code byte.
|
||||||
@@ -66,12 +202,16 @@ pub fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
|||||||
dest[di] = if full_block { 0xFF } else { (run + 1) as u8 };
|
dest[di] = if full_block { 0xFF } else { (run + 1) as u8 };
|
||||||
di += 1;
|
di += 1;
|
||||||
|
|
||||||
// Bulk-copy the run.
|
// Copy the run.
|
||||||
if run > 0 {
|
if run > 0 {
|
||||||
if di + run > dest.len() {
|
if di + run > dest.len() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
dest[di..di + run].copy_from_slice(&src[si..si + run]);
|
let mut i = 0;
|
||||||
|
while i < run {
|
||||||
|
dest[di + i] = src[si + i];
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
di += run;
|
di += run;
|
||||||
}
|
}
|
||||||
si += run;
|
si += run;
|
||||||
@@ -94,12 +234,29 @@ pub fn encode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
|||||||
Some(di)
|
Some(di)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode a COBS-encoded buffer. Returns the number of decoded bytes
|
/// Decode a COBS-encoded buffer.
|
||||||
/// written to `dest`, or `None` if the input is malformed or `dest` is
|
///
|
||||||
/// too small.
|
/// 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
|
/// `src` should **not** include the trailing `0x00` frame sentinel — strip
|
||||||
/// it before calling this function.
|
/// 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]
|
#[must_use]
|
||||||
pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
||||||
let mut si = 0;
|
let mut si = 0;
|
||||||
@@ -112,14 +269,12 @@ pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
|||||||
}
|
}
|
||||||
// Bulk-copy `code - 1` data bytes.
|
// Bulk-copy `code - 1` data bytes.
|
||||||
let n = code - 1;
|
let n = code - 1;
|
||||||
if n > 0 {
|
if si + n > src.len() || di + n > dest.len() {
|
||||||
if si + n > src.len() || di + n > dest.len() {
|
return None;
|
||||||
return None;
|
|
||||||
}
|
|
||||||
dest[di..di + n].copy_from_slice(&src[si..si + n]);
|
|
||||||
di += n;
|
|
||||||
si += n;
|
|
||||||
}
|
}
|
||||||
|
dest[di..di + n].copy_from_slice(&src[si..si + n]);
|
||||||
|
di += n;
|
||||||
|
si += n;
|
||||||
// If code < 0xFF, an implicit zero follows (unless we're at the end).
|
// If code < 0xFF, an implicit zero follows (unless we're at the end).
|
||||||
if code < 0xFF && si < src.len() {
|
if code < 0xFF && si < src.len() {
|
||||||
if di >= dest.len() {
|
if di >= dest.len() {
|
||||||
@@ -133,6 +288,17 @@ pub fn decode(src: &[u8], dest: &mut [u8]) -> Option<usize> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Maximum encoded size for a given source length (excluding sentinel).
|
/// 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]
|
#[must_use]
|
||||||
pub const fn max_encoded_len(src_len: usize) -> usize {
|
pub const fn max_encoded_len(src_len: usize) -> usize {
|
||||||
src_len + (src_len / 254) + 1
|
src_len + (src_len / 254) + 1
|
||||||
@@ -155,6 +321,21 @@ mod tests {
|
|||||||
encode(input, &mut buf).map(|n| buf[..n].to_vec())
|
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) ──
|
// ── Wikipedia canonical decode vectors (Cheshire & Baker 1999) ──
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user