Skip to content

Commit 2ab9b24

Browse files
committed
feat: add configurable sentinel, in-place decode, no-alloc StreamDecoder
Adds sentinel variants, in-place decode, and a no-alloc StreamDecoder (v1.1.0).
1 parent 0c1f783 commit 2ab9b24

9 files changed

Lines changed: 609 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
All notable changes to this crate are documented here. This project adheres to
44
[Semantic Versioning](https://semver.org).
55

6+
## 1.1.0
7+
8+
### Added
9+
10+
- **Configurable sentinel**: `encode_with_sentinel` / `decode_with_sentinel`
11+
(and `*_to_vec_with_sentinel`) on both `cobs` and `cobsr`, for framing with a
12+
delimiter other than `0x00`.
13+
- **In-place decoding**: `cobs::decode_in_place` and
14+
`cobs::decode_in_place_with_sentinel`, which decode without a second buffer.
15+
- **`framing::StreamDecoder`**: an allocation-free streaming frame decoder that
16+
reassembles delimited packets into a caller-provided buffer — the pure
17+
`no_std` counterpart of `FrameDecoder`, with `reduced` and `sentinel` options.
18+
19+
All additions are backward compatible.
20+
621
## 1.0.0
722

823
Initial release.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cobs_codec_rs"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
edition = "2021"
55
rust-version = "1.81"
66
description = "Consistent Overhead Byte Stuffing (COBS) and COBS/R codec: no_std, zero-dependency, for zero-free framing of serial and packet byte streams."

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,26 @@ USB, TCP and other byte streams — ideal for embedded and robotics protocols.
2222
- **Basic COBS** and **COBS/R (Reduced)** encode/decode.
2323
- **`no_std` and zero dependencies** — the core `encode`/`decode` work on
2424
caller-provided slices, allocating nothing.
25-
- **`alloc` conveniences** (`*_to_vec`) and a streaming
25+
- **Configurable sentinel**`*_with_sentinel` variants frame with any
26+
delimiter byte, not just `0x00`.
27+
- **In-place decoding**`cobs::decode_in_place` decodes without a second
28+
buffer.
29+
- **Allocation-free streaming**`framing::StreamDecoder` reassembles
30+
delimited frames into a fixed buffer in pure `no_std`; the `alloc` feature adds
31+
the owned-`Vec`
2632
[`FrameDecoder`](https://docs.rs/cobs_codec_rs/latest/cobs_codec_rs/framing/struct.FrameDecoder.html)
27-
for `0x00`-delimited serial links, behind the default `std`/`alloc` features.
33+
and `*_to_vec` conveniences.
2834
- **`const fn`** size helpers (`max_encoded_len`, `encoding_overhead`) for
2935
compile-time buffer sizing.
3036

3137
## Install
3238

3339
```toml
3440
[dependencies]
35-
cobs_codec_rs = "1.0"
41+
cobs_codec_rs = "1.1"
3642

3743
# no_std, no allocator:
38-
# cobs_codec_rs = { version = "1.0", default-features = false }
44+
# cobs_codec_rs = { version = "1.1", default-features = false }
3945
```
4046

4147
## Usage

src/cobs.rs

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@ pub fn encode(src: &[u8], dst: &mut [u8]) -> usize {
6363
write_index
6464
}
6565

66+
/// Encodes `src` with basic COBS into `dst` using an arbitrary `sentinel` byte
67+
/// instead of `0x00`, returning the number of bytes written.
68+
///
69+
/// The output never contains the `sentinel` byte, so `sentinel` (rather than
70+
/// `0x00`) may be used to delimit packets. Choosing a sentinel that rarely
71+
/// occurs in your payload minimises overhead. `sentinel == 0` is identical to
72+
/// [`encode`].
73+
///
74+
/// # Panics
75+
///
76+
/// Panics if `dst` is shorter than [`max_encoded_len`]`(src.len())`.
77+
#[must_use]
78+
pub fn encode_with_sentinel(src: &[u8], dst: &mut [u8], sentinel: u8) -> usize {
79+
let n = encode(src, dst);
80+
if sentinel != 0 {
81+
for byte in &mut dst[..n] {
82+
*byte ^= sentinel;
83+
}
84+
}
85+
n
86+
}
87+
6688
/// Decodes basic-COBS `src` into `dst`, returning the number of bytes written.
6789
///
6890
/// The empty input decodes to nothing (returns `0`). `src` must be a single
@@ -74,6 +96,23 @@ pub fn encode(src: &[u8], dst: &mut [u8]) -> usize {
7496
/// not valid COBS, or [`DecodeError::OutputTooSmall`] if `dst` is shorter than
7597
/// the decoded output (which never exceeds `src.len()`).
7698
pub fn decode(src: &[u8], dst: &mut [u8]) -> Result<usize, DecodeError> {
99+
decode_with_sentinel(src, dst, 0)
100+
}
101+
102+
/// Decodes basic-COBS `src` that was encoded with an arbitrary `sentinel` byte
103+
/// (see [`encode_with_sentinel`]) into `dst`, returning the number of bytes
104+
/// written. `sentinel == 0` is identical to [`decode`].
105+
///
106+
/// # Errors
107+
///
108+
/// Returns [`DecodeError::ZeroByte`] (the encoded input contained the
109+
/// `sentinel`) or [`DecodeError::Truncated`] if `src` is not valid, or
110+
/// [`DecodeError::OutputTooSmall`] if `dst` is shorter than the decoded output.
111+
pub fn decode_with_sentinel(
112+
src: &[u8],
113+
dst: &mut [u8],
114+
sentinel: u8,
115+
) -> Result<usize, DecodeError> {
77116
let src_len = src.len();
78117
if src_len == 0 {
79118
return Ok(0);
@@ -83,15 +122,15 @@ pub fn decode(src: &[u8], dst: &mut [u8]) -> Result<usize, DecodeError> {
83122
let mut index = 0;
84123

85124
loop {
86-
let code = src[index];
125+
let code = src[index] ^ sentinel;
87126
if code == 0 {
88127
return Err(DecodeError::ZeroByte { index });
89128
}
90129
index += 1;
91130
let block_end = index + usize::from(code) - 1;
92131
let copy_end = block_end.min(src_len);
93132
while index < copy_end {
94-
let byte = src[index];
133+
let byte = src[index] ^ sentinel;
95134
if byte == 0 {
96135
return Err(DecodeError::ZeroByte { index });
97136
}
@@ -119,6 +158,74 @@ pub fn decode(src: &[u8], dst: &mut [u8]) -> Result<usize, DecodeError> {
119158
Ok(write_index)
120159
}
121160

161+
/// Decodes basic-COBS data in place, overwriting `buf` with the decoded output
162+
/// and returning its length. The decoded bytes occupy `buf[..len]`.
163+
///
164+
/// This needs no output buffer: COBS decoding never expands, so the write
165+
/// position always trails the read position.
166+
///
167+
/// # Errors
168+
///
169+
/// Returns [`DecodeError::ZeroByte`] or [`DecodeError::Truncated`] if `buf` is
170+
/// not valid COBS. [`DecodeError::OutputTooSmall`] is never returned.
171+
pub fn decode_in_place(buf: &mut [u8]) -> Result<usize, DecodeError> {
172+
decode_in_place_with_sentinel(buf, 0)
173+
}
174+
175+
/// Decodes basic-COBS data that was encoded with an arbitrary `sentinel` byte in
176+
/// place, overwriting `buf` with the decoded output and returning its length.
177+
/// `sentinel == 0` is identical to [`decode_in_place`].
178+
///
179+
/// # Errors
180+
///
181+
/// Returns [`DecodeError::ZeroByte`] or [`DecodeError::Truncated`] if `buf` is
182+
/// not valid. [`DecodeError::OutputTooSmall`] is never returned.
183+
pub fn decode_in_place_with_sentinel(buf: &mut [u8], sentinel: u8) -> Result<usize, DecodeError> {
184+
let src_len = buf.len();
185+
if src_len == 0 {
186+
return Ok(0);
187+
}
188+
189+
let mut write_index = 0;
190+
let mut index = 0;
191+
192+
loop {
193+
let code = buf[index] ^ sentinel;
194+
if code == 0 {
195+
return Err(DecodeError::ZeroByte { index });
196+
}
197+
index += 1;
198+
let block_end = index + usize::from(code) - 1;
199+
let copy_end = block_end.min(src_len);
200+
while index < copy_end {
201+
let byte = buf[index] ^ sentinel;
202+
if byte == 0 {
203+
return Err(DecodeError::ZeroByte { index });
204+
}
205+
// write_index < index throughout, so this never clobbers unread
206+
// input.
207+
buf[write_index] = byte;
208+
write_index += 1;
209+
index += 1;
210+
}
211+
if block_end > src_len {
212+
return Err(DecodeError::Truncated {
213+
index: block_end - usize::from(code),
214+
});
215+
}
216+
if block_end < src_len {
217+
if code < 0xFF {
218+
buf[write_index] = 0;
219+
write_index += 1;
220+
}
221+
} else {
222+
break;
223+
}
224+
}
225+
226+
Ok(write_index)
227+
}
228+
122229
/// Encodes `src` with basic COBS, returning a newly allocated [`Vec`].
123230
#[cfg(feature = "alloc")]
124231
#[must_use]
@@ -129,6 +236,17 @@ pub fn encode_to_vec(src: &[u8]) -> Vec<u8> {
129236
dst
130237
}
131238

239+
/// Encodes `src` with basic COBS and an arbitrary `sentinel` byte, returning a
240+
/// newly allocated [`Vec`].
241+
#[cfg(feature = "alloc")]
242+
#[must_use]
243+
pub fn encode_to_vec_with_sentinel(src: &[u8], sentinel: u8) -> Vec<u8> {
244+
let mut dst = alloc::vec![0u8; max_encoded_len(src.len())];
245+
let n = encode_with_sentinel(src, &mut dst, sentinel);
246+
dst.truncate(n);
247+
dst
248+
}
249+
132250
/// Decodes basic-COBS `src`, returning a newly allocated [`Vec`].
133251
///
134252
/// # Errors
@@ -141,3 +259,17 @@ pub fn decode_to_vec(src: &[u8]) -> Result<Vec<u8>, DecodeError> {
141259
dst.truncate(n);
142260
Ok(dst)
143261
}
262+
263+
/// Decodes basic-COBS `src` that was encoded with an arbitrary `sentinel` byte,
264+
/// returning a newly allocated [`Vec`].
265+
///
266+
/// # Errors
267+
///
268+
/// Returns a [`DecodeError`] if `src` is not valid.
269+
#[cfg(feature = "alloc")]
270+
pub fn decode_to_vec_with_sentinel(src: &[u8], sentinel: u8) -> Result<Vec<u8>, DecodeError> {
271+
let mut dst = alloc::vec![0u8; src.len()];
272+
let n = decode_with_sentinel(src, &mut dst, sentinel)?;
273+
dst.truncate(n);
274+
Ok(dst)
275+
}

src/cobsr.rs

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,26 @@ pub fn encode(src: &[u8], dst: &mut [u8]) -> usize {
7373
write_index
7474
}
7575

76+
/// Encodes `src` with COBS/R into `dst` using an arbitrary `sentinel` byte
77+
/// instead of `0x00`, returning the number of bytes written.
78+
///
79+
/// The output never contains the `sentinel` byte. `sentinel == 0` is identical
80+
/// to [`encode`].
81+
///
82+
/// # Panics
83+
///
84+
/// Panics if `dst` is shorter than [`max_encoded_len`]`(src.len())`.
85+
#[must_use]
86+
pub fn encode_with_sentinel(src: &[u8], dst: &mut [u8], sentinel: u8) -> usize {
87+
let n = encode(src, dst);
88+
if sentinel != 0 {
89+
for byte in &mut dst[..n] {
90+
*byte ^= sentinel;
91+
}
92+
}
93+
n
94+
}
95+
7696
/// Decodes COBS/R `src` into `dst`, returning the number of bytes written.
7797
///
7898
/// The empty input decodes to nothing (returns `0`).
@@ -85,6 +105,22 @@ pub fn encode(src: &[u8], dst: &mut [u8]) -> usize {
85105
/// points past the end of the input is not an error: it signals the reduced
86106
/// final block.
87107
pub fn decode(src: &[u8], dst: &mut [u8]) -> Result<usize, DecodeError> {
108+
decode_with_sentinel(src, dst, 0)
109+
}
110+
111+
/// Decodes COBS/R `src` that was encoded with an arbitrary `sentinel` byte (see
112+
/// [`encode_with_sentinel`]) into `dst`, returning the number of bytes written.
113+
/// `sentinel == 0` is identical to [`decode`].
114+
///
115+
/// # Errors
116+
///
117+
/// Returns [`DecodeError::ZeroByte`] if `src` contains the `sentinel` byte, or
118+
/// [`DecodeError::OutputTooSmall`] if `dst` is shorter than the decoded output.
119+
pub fn decode_with_sentinel(
120+
src: &[u8],
121+
dst: &mut [u8],
122+
sentinel: u8,
123+
) -> Result<usize, DecodeError> {
88124
let src_len = src.len();
89125
if src_len == 0 {
90126
return Ok(0);
@@ -94,15 +130,15 @@ pub fn decode(src: &[u8], dst: &mut [u8]) -> Result<usize, DecodeError> {
94130
let mut index = 0;
95131

96132
loop {
97-
let code = src[index];
133+
let code = src[index] ^ sentinel;
98134
if code == 0 {
99135
return Err(DecodeError::ZeroByte { index });
100136
}
101137
index += 1;
102138
let block_end = index + usize::from(code) - 1;
103139
let copy_end = block_end.min(src_len);
104140
while index < copy_end {
105-
let byte = src[index];
141+
let byte = src[index] ^ sentinel;
106142
if byte == 0 {
107143
return Err(DecodeError::ZeroByte { index });
108144
}
@@ -143,6 +179,17 @@ pub fn encode_to_vec(src: &[u8]) -> Vec<u8> {
143179
dst
144180
}
145181

182+
/// Encodes `src` with COBS/R and an arbitrary `sentinel` byte, returning a newly
183+
/// allocated [`Vec`].
184+
#[cfg(feature = "alloc")]
185+
#[must_use]
186+
pub fn encode_to_vec_with_sentinel(src: &[u8], sentinel: u8) -> Vec<u8> {
187+
let mut dst = alloc::vec![0u8; max_encoded_len(src.len())];
188+
let n = encode_with_sentinel(src, &mut dst, sentinel);
189+
dst.truncate(n);
190+
dst
191+
}
192+
146193
/// Decodes COBS/R `src`, returning a newly allocated [`Vec`].
147194
///
148195
/// # Errors
@@ -155,3 +202,17 @@ pub fn decode_to_vec(src: &[u8]) -> Result<Vec<u8>, DecodeError> {
155202
dst.truncate(n);
156203
Ok(dst)
157204
}
205+
206+
/// Decodes COBS/R `src` that was encoded with an arbitrary `sentinel` byte,
207+
/// returning a newly allocated [`Vec`].
208+
///
209+
/// # Errors
210+
///
211+
/// Returns a [`DecodeError`] if `src` is not valid.
212+
#[cfg(feature = "alloc")]
213+
pub fn decode_to_vec_with_sentinel(src: &[u8], sentinel: u8) -> Result<Vec<u8>, DecodeError> {
214+
let mut dst = alloc::vec![0u8; src.len()];
215+
let n = decode_with_sentinel(src, &mut dst, sentinel)?;
216+
dst.truncate(n);
217+
Ok(dst)
218+
}

0 commit comments

Comments
 (0)