Skip to main content

cobs_io_async/
lib.rs

1//! # Reliable byte streams over COBS framing
2//!
3//! A runtime-independent, `no_std` transport for an unreliable point-to-point
4//! link, such as a serial line. [`Reliable`] implements [`Transport`], a
5//! poll-based byte-stream interface. It splits written bytes into packets,
6//! frames them with Consistent Overhead Byte Stuffing (COBS), verifies them
7//! with a checksum, acknowledges them, and retransmits lost ones. Packet
8//! boundaries are internal: applications write and read bytes.
9//!
10//! The Cargo package is `cobs-io-async`; the Rust import name is
11//! `cobs_io_async`.
12//!
13//! # Overview
14//!
15//! | Item | Role |
16//! |---|---|
17//! | [`Transport`] | Poll-based byte-stream trait, both for the underlying link you supply and for [`Reliable`] itself. |
18//! | [`Timer`] | Monotonic clock and wakeup source you supply for retransmission deadlines. |
19//! | [`Reliable`] | The connection. Wraps a [`Transport`] and a [`Timer`]. |
20//! | [`Config`] | Session identifier, retransmission timeout and retry limit. |
21//! | [`sync`] | In-memory COBS encoding and decoding between slices. |
22//!
23//! This crate provides no adapters for any runtime and spawns no tasks. You
24//! implement [`Transport`] for your serial port, socket or driver, and
25//! [`Timer`] for your clock, and wrap them in whatever your runtime needs.
26//! Neither trait requires `Send`, `'static` or pinning.
27//!
28//! **Nothing happens unless the connection is polled.** Retransmissions,
29//! acknowledgments and incoming data are processed only inside
30//! [`Transport::poll_read`], [`Transport::poll_write`],
31//! [`Transport::poll_flush`] and [`Reliable::poll_progress`]. While none of
32//! these is pending, call [`Reliable::poll_progress`] from a task of your own
33//! to keep the connection alive.
34//!
35//! # Behavior
36//!
37//! | Operation | Behavior |
38//! |---|---|
39//! | `poll_write(buf)` | Copies a nonempty prefix of `buf` into the outgoing packet and returns the number of bytes accepted, at most [`Reliable::MAX_PAYLOAD`]. Returns `Pending` while the previous packet is unacknowledged. |
40//! | `poll_read(buf)` | Copies verified payload bytes into `buf`. Packet boundaries are invisible. Returns `Pending` while no payload is available. |
41//! | `poll_flush()` | Completes when all accepted data is acknowledged by the peer and the underlying transport has been flushed. |
42//! | Empty `buf` | Returns `Ready(Ok(0))` and creates no packet. |
43//! | Corrupted or malformed frame | Discarded silently. Retransmission provides recovery. |
44//! | Incoming payload slot full | New DATA is discarded without acknowledgment, so the peer retransmits it. Acknowledgments and duplicates are still processed. |
45//! | Transport error, EOF, retries exhausted, sequence numbers exhausted | The connection fails permanently. See [`ConnectionError`]. |
46//! | Dropping a future awaiting a poll method | Protocol state and accepted data are unaffected. Multi-task adapters must call [`Reliable::cancel_pending`] to preserve the other operations' wakeups. |
47//!
48//! Every poll method drives both directions, performing a bounded amount of
49//! work per call. If work remains when the budget is spent, the task is woken
50//! before `Pending` is returned, so an always-ready transport cannot
51//! monopolize the executor.
52//!
53//! # Cancellation
54//!
55//! The connection owns all protocol state, so dropping a future never rolls
56//! it back or corrupts accepted data. It does not detect the drop, however.
57//! The underlying [`Transport`] and [`Timer`] wake only their latest poller,
58//! so if the task that polled them last is canceled, another pending task may
59//! never be woken.
60//!
61//! An adapter that polls operations from separate tasks must therefore:
62//!
63//! 1. Record which operation, as a [`PendingOperation`], returned `Pending`.
64//! 2. If its future is dropped, obtain exclusive access to the
65//!    [`Reliable`], serialized with polling.
66//! 3. Call [`Reliable::cancel_pending`] for that operation before abandoning
67//!    it.
68//!
69//! The call wakes each other stored waiter once, and those tasks poll again
70//! and re-register. Only one waiter is stored per operation, so concurrent
71//! futures for the same operation are not tracked separately, and a stale
72//! future must not cancel a newer one. Without the call, the stall remains
73//! possible.
74//!
75//! # Underlying transport requirements
76//!
77//! The link underneath [`Reliable`] must behave as a byte stream that may
78//! lose, corrupt, insert or delete bytes, but must not reorder or delay them
79//! beyond the retransmission timeout:
80//!
81//! - `poll_read` returning `Ok(0)` for a nonempty buffer means end of
82//!   stream. Temporary lack of input must return `Pending`.
83//! - `poll_write` returning `Ok(0)` for a nonempty buffer is an error.
84//! - A returned `Pending` must have registered a wakeup.
85//!
86//! # Protocol
87//!
88//! Reliability is stop-and-wait in each direction independently: one
89//! unacknowledged outgoing packet and one retained incoming payload. This
90//! favors small, fixed memory over throughput.
91//!
92//! Each packet is
93//!
94//! ```text
95//! 0 | COBS(kind | session | sequence | payload | checksum) | 0
96//! ```
97//!
98//! with big-endian integers:
99//!
100//! | Field | Size | Meaning |
101//! |---|---|---|
102//! | `kind` | 1 | `1` for DATA, `2` for ACK. |
103//! | `session` | 8 | Session identifier from [`Config::session`]. |
104//! | `sequence` | 4 | DATA sequence number, starting at 0. An ACK echoes it. |
105//! | `payload` | 1 or more for DATA, absent for ACK | Application bytes. The length is implied by the frame boundary. |
106//! | `checksum` | 4 | CRC-32/ISO-HDLC over all preceding decoded fields. |
107//!
108//! A receiver accepts DATA only if the session matches, the checksum is
109//! valid and the sequence is the next expected one. It acknowledges data once
110//! it has *retained* it, not once the application has read it. A repeat of
111//! the previous DATA is acknowledged again but never delivered twice. Any
112//! other DATA is dropped. A sender releases its packet only for an ACK
113//! with a matching session and sequence.
114//!
115//! The receive path collects bytes until a zero delimiter and decodes only
116//! delimiter-terminated frames. Repeated zeros are padding. A frame longer
117//! than the connection's buffer is discarded through its delimiter, and the
118//! following frame is processed normally.
119//!
120//! # Limitations
121//!
122//! - Both peers must be constructed with the same session identifier, chosen
123//!   by the caller and fresh for each session. It distinguishes sessions; it
124//!   is not authentication.
125//! - There is no handshake or reconnection. Peer restarts, and packets
126//!   delayed beyond a session, are outside the failure model. Sequence
127//!   numbers alone do not handle them.
128//! - Sequence numbers do not wrap. After 2<sup>32</sup> packets in one
129//!   direction the connection fails with
130//!   [`ConnectionError::SequenceExhausted`]; start a new session.
131//! - A peer that stops reading for longer than the retransmission budget
132//!   (`retransmit_timeout * (max_retries + 1)`) causes the sender to fail
133//!   with [`ConnectionError::Timeout`].
134//! - Each accepted write becomes its own packet. Small writes are not
135//!   coalesced.
136//! - There is no sliding window, adaptive timeout or fragmentation metadata.
137//!
138//! # Example
139//!
140//! The stubs below stand in for a real link and clock. A real
141//! [`Timer::poll_deadline`] must register `cx.waker()` when the deadline is
142//! in the future.
143//!
144//! ```
145//! # use core::convert::Infallible;
146//! # use core::task::{Context, Poll, Waker};
147//! use cobs_io_async::{Config, Reliable, Timer, Transport};
148//!
149//! # struct Link;
150//! # impl Transport for Link {
151//! #     type Error = Infallible;
152//! #     fn poll_read(&mut self, _: &mut Context<'_>, _: &mut [u8]) -> Poll<Result<usize, Infallible>> {
153//! #         Poll::Pending
154//! #     }
155//! #     fn poll_write(&mut self, _: &mut Context<'_>, buf: &[u8]) -> Poll<Result<usize, Infallible>> {
156//! #         Poll::Ready(Ok(buf.len()))
157//! #     }
158//! #     fn poll_flush(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
159//! #         Poll::Ready(Ok(()))
160//! #     }
161//! # }
162//! # struct Clock(u64);
163//! # impl Timer for Clock {
164//! #     fn now(&self) -> u64 {
165//! #         self.0
166//! #     }
167//! #     fn poll_deadline(&mut self, _: &mut Context<'_>, deadline: u64) -> Poll<()> {
168//! #         if self.0 >= deadline { Poll::Ready(()) } else { Poll::Pending }
169//! #     }
170//! # }
171//! let mut connection: Reliable<Link, Clock> =
172//!     Reliable::new(Link, Clock(0), Config::new(0xC0B5)).unwrap();
173//! let mut cx = Context::from_waker(Waker::noop());
174//!
175//! assert!(matches!(connection.poll_write(&mut cx, b"hello"), Poll::Ready(Ok(5))));
176//! // The peer has not acknowledged the packet, so the flush stays pending.
177//! assert!(connection.poll_flush(&mut cx).is_pending());
178//! ```
179//!
180//! # In-memory encoding and decoding
181//!
182//! The [`sync`] module exposes the underlying COBS codec between slices. It
183//! needs no allocation and is independent of [`Reliable`].
184//!
185//! COBS transforms a payload into an encoded body containing no zero bytes,
186//! so zero can delimit frames. Payload zeros are encoded as data. An empty
187//! payload encodes as `[1]`, or `[0, 1, 0]` with surrounding delimiters. A
188//! terminal full block of 254 nonzero payload bytes does not receive a
189//! redundant trailing code byte.
190//!
191//! Use [`max_encoding_length`] to size a buffer for an undelimited body, and
192//! reserve two more bytes for delimiters, using checked arithmetic when sizes
193//! come from untrusted or potentially large lengths.
194//!
195//! COBS alone provides framing, not integrity or authenticity.
196//!
197//! # Features
198//!
199//! No features are enabled by default.
200//!
201//! | Feature | Effect |
202//! |---|---|
203//! | `serde` | Serialization and deserialization of [`Config`], [`ConfigError`], [`ConnectionError`], [`DecodeError`], [`SeekableError`] and [`DecodeProgress`], subject to generic parameter bounds. Live connection state is not serializable. |
204//! | `defmt` | Compact diagnostic formatting for the same types. |
205//!
206//! The crate is `no_std` and never allocates.
207
208#![no_std]
209#![warn(missing_docs)]
210
211#[cfg(test)]
212extern crate std;
213
214pub use codec::decode::DecodeProgress;
215
216mod codec;
217pub(crate) mod connection;
218mod error;
219mod protocol;
220mod timer;
221mod transport;
222
223pub mod sync;
224
225pub use connection::{Config, PendingOperation, Reliable};
226pub use error::{ConfigError, ConnectionError, DecodeError, SeekableError};
227pub use timer::Timer;
228pub use transport::Transport;
229
230#[cfg(test)]
231mod tests;
232
233/// Returns the maximum additional bytes needed for an undelimited encoding.
234///
235/// Returns `1` for empty input; otherwise returns `source_len.div_ceil(254)`.
236/// The result excludes leading and trailing frame delimiters.
237///
238/// This is an upper bound across payloads of the given length, not necessarily
239/// the actual overhead for a particular payload.
240///
241/// See [`max_encoding_length`] for the corresponding total buffer size.
242#[inline]
243pub const fn max_encoding_overhead(source_len: usize) -> usize {
244    if source_len == 0 {
245        return 1;
246    }
247    source_len.div_ceil(254)
248}
249
250/// Returns the maximum encoded body length for `source_len` payload bytes.
251///
252/// The result is `source_len + max_encoding_overhead(source_len)`.
253/// It excludes frame delimiters. To surround the body with leading and
254/// trailing zero delimiters, reserve two additional bytes, checking that
255/// the addition fits.
256///
257/// Empty input requires one byte. For example, 254 payload bytes require at
258/// most 255 encoded bytes.
259///
260/// # Overflow
261///
262/// The addition is not checked explicitly. Overflow panics when overflow
263/// checks are enabled and otherwise wraps; constant evaluation rejects it.
264///
265/// For potentially unrepresentable sizes, use
266/// `source_len.checked_add(max_encoding_overhead(source_len))` instead.
267#[inline]
268pub const fn max_encoding_length(source_len: usize) -> usize {
269    source_len + max_encoding_overhead(source_len)
270}