Skip to main content

cobs_io_async/
sync.rs

1//! Synchronous COBS encoding and decoding between in-memory slices.
2//!
3//! This module is always available, including in `no_std` builds. It needs
4//! no executor or heap allocation. [`Reliable`](crate::Reliable) uses the
5//! same framing rules internally, but accepts only delimiter-terminated
6//! frames.
7//!
8//! | Operation | API |
9//! |---|---|
10//! | Encode a complete payload as an undelimited body | [`encode_from_slice`] |
11//! | Encode a payload with surrounding zero delimiters | [`encode_from_slice_including_sentinels`] |
12//! | Decode one frame from the start of a slice | [`decode_to_slice`] |
13//!
14//! Size encoding buffers with [`max_encoding_length`](crate::max_encoding_length),
15//! adding two bytes for delimiters. Decoding requires capacity only for the
16//! decoded payload.
17//!
18//! # Example
19//!
20//! Decode consecutive frames from one buffer, using
21//! [`DecodedFrame::consumed`] to locate the next frame.
22//!
23//! ```
24//! use cobs_io_async::{max_encoding_length, sync};
25//!
26//! let mut stream = [0u8; 2 * (max_encoding_length(3) + 2)];
27//! let first = sync::encode_from_slice_including_sentinels(&[7, 0, 8], &mut stream).unwrap();
28//! let second =
29//!     sync::encode_from_slice_including_sentinels(&[9], &mut stream[first..]).unwrap();
30//! let mut input = &stream[..first + second];
31//!
32//! let mut payload = [0u8; 3];
33//! let frame = sync::decode_to_slice(input, &mut payload).unwrap();
34//! assert_eq!(&payload[..frame.len], &[7, 0, 8]);
35//! input = &input[frame.consumed..];
36//!
37//! let frame = sync::decode_to_slice(input, &mut payload).unwrap();
38//! assert_eq!(&payload[..frame.len], &[9]);
39//! assert!(input[frame.consumed..].is_empty());
40//! ```
41
42use core::convert::Infallible;
43
44use crate::codec::decode::decode_slice;
45use crate::{DecodeError, SeekableError};
46
47/// Result of successfully decoding one frame with [`decode_to_slice`].
48#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50#[cfg_attr(feature = "defmt", derive(defmt::Format))]
51pub struct DecodedFrame {
52    /// Number of decoded payload bytes written to the start of the destination.
53    pub len: usize,
54
55    /// Number of encoded bytes processed from the start of the source.
56    ///
57    /// Includes leading zero padding and the completing delimiter, if any.
58    /// Input following the frame starts at `source[consumed..]`.
59    pub consumed: usize,
60}
61
62/// Encodes all of `source` as one complete, undelimited COBS body into `dest`.
63///
64/// Returns the encoded length; the body occupies `dest[..len]`. Payload zeros
65/// are encoded as data. Empty input produces `[1]`. A terminal full block of
66/// 254 nonzero payload bytes does not receive a redundant trailing code byte.
67///
68/// A destination of [`max_encoding_length(source.len())`](crate::max_encoding_length)
69/// bytes is always sufficient.
70///
71/// # Errors
72///
73/// Returns [`SeekableError::OutOfBounds`] if `dest` is too small. The
74/// destination may then have been partially overwritten.
75///
76/// # Example
77///
78/// ```
79/// use cobs_io_async::sync::encode_from_slice;
80///
81/// let mut encoded = [0u8; 4];
82/// let len = encode_from_slice(&[7, 0, 8], &mut encoded).unwrap();
83/// assert_eq!(&encoded[..len], &[2, 7, 2, 8]);
84/// ```
85pub fn encode_from_slice(source: &[u8], dest: &mut [u8]) -> Result<usize, SeekableError> {
86    let mut output = SliceOutput { dest, len: 0 };
87    let mut code_idx = 0;
88    let mut code = 1u8;
89    let mut needs_code_placeholder = false;
90    output.append(0)?;
91    for &byte in source {
92        if needs_code_placeholder {
93            output.append(0)?;
94            needs_code_placeholder = false;
95        }
96        if byte == 0 {
97            output.set(code_idx, code)?;
98            code_idx = output.len;
99            output.append(0)?;
100            code = 1;
101            continue;
102        }
103        code += 1;
104        if code == u8::MAX {
105            output.set(code_idx, code)?;
106            output.append(byte)?;
107            code_idx = output.len;
108            code = 1;
109            needs_code_placeholder = true;
110        } else {
111            output.append(byte)?;
112        }
113    }
114    if !needs_code_placeholder {
115        output.set(code_idx, code)?;
116    }
117    Ok(output.len)
118}
119
120/// Encodes `source` with one leading and one trailing zero delimiter.
121///
122/// On success, `dest[..len]` contains `[0, encoded_body..., 0]`. Empty input
123/// produces `[0, 1, 0]`. Payload zeros remain data and do not split the frame.
124///
125/// Reserve [`max_encoding_length(source.len())`](crate::max_encoding_length)
126/// plus two bytes.
127///
128/// # Errors
129///
130/// Returns [`SeekableError::OutOfBounds`] if `dest` is too small. The
131/// destination may then have been partially overwritten.
132pub fn encode_from_slice_including_sentinels(
133    source: &[u8],
134    dest: &mut [u8],
135) -> Result<usize, SeekableError> {
136    let (first, rest) = dest.split_first_mut().ok_or(SeekableError::OutOfBounds)?;
137    *first = 0;
138    let body_len = encode_from_slice(source, rest)?;
139    *rest.get_mut(body_len).ok_or(SeekableError::OutOfBounds)? = 0;
140    Ok(body_len + 2)
141}
142
143/// Decodes the first COBS frame in `source` into the beginning of `dest`.
144///
145/// Leading zeros are ignored as padding. Decoding stops after the zero
146/// delimiter completing the frame; bytes after it are not examined, and
147/// [`DecodedFrame::consumed`] identifies where they begin.
148///
149/// If `source` ends before a delimiter, the frame is accepted when one has
150/// started and its current COBS block is structurally complete. Both `[1]`
151/// and `[1, 0]` decode to an empty payload. This end-of-input acceptance
152/// cannot detect truncation at a COBS block boundary; check that `consumed`
153/// includes a delimiter (`source[consumed - 1] == 0`) when the protocol
154/// requires delimited frames.
155///
156/// On success, bytes of `dest` after the decoded payload remain unchanged.
157///
158/// # Errors
159///
160/// - `DecodeError::Destination(SeekableError::OutOfBounds)`: `dest` cannot
161///   hold the decoded payload.
162/// - [`DecodeError::InvalidFrame`]: a delimiter arrived inside an incomplete
163///   block. The attached progress counts the invalidating delimiter, so the
164///   next frame starts at `source[progress.consumed as usize..]`.
165/// - [`DecodeError::UnexpectedSourceEof`]: `source` ended inside an
166///   incomplete block.
167/// - [`DecodeError::EmptyFrame`]: `source` is empty or contains only padding.
168///
169/// Errors may leave `dest` partially overwritten. Source errors are never
170/// returned.
171///
172/// # Example
173///
174/// ```
175/// use cobs_io_async::sync::decode_to_slice;
176///
177/// let source = [0, 2, 7, 2, 8, 0, 2, 9, 0];
178/// let mut output = [0x80u8; 5];
179/// let frame = decode_to_slice(&source, &mut output).unwrap();
180///
181/// assert_eq!(frame.len, 3);
182/// assert_eq!(output, [7, 0, 8, 0x80, 0x80]);
183/// assert_eq!(&source[frame.consumed..], &[2, 9, 0]);
184/// ```
185pub fn decode_to_slice(
186    source: &[u8],
187    dest: &mut [u8],
188) -> Result<DecodedFrame, DecodeError<Infallible, SeekableError>> {
189    decode_slice(source, dest)
190}
191
192struct SliceOutput<'a> {
193    dest: &'a mut [u8],
194    len: usize,
195}
196
197impl SliceOutput<'_> {
198    #[inline]
199    fn append(&mut self, byte: u8) -> Result<(), SeekableError> {
200        *self
201            .dest
202            .get_mut(self.len)
203            .ok_or(SeekableError::OutOfBounds)? = byte;
204        self.len += 1;
205        Ok(())
206    }
207
208    #[inline]
209    fn set(&mut self, idx: usize, byte: u8) -> Result<(), SeekableError> {
210        *self.dest.get_mut(idx).ok_or(SeekableError::OutOfBounds)? = byte;
211        Ok(())
212    }
213}