Skip to main content

foundation_core/io/
incremental_decoder.rs

1//! `IncrementalDecoder` — the shared resumable frame-decoding seam (Decision 12 §11).
2//!
3//! WHY: Every frame-oriented protocol we run on a non-blocking fd hits the same
4//! wall — a frame can span multiple reads, so a decoder that consumes part of a
5//! header/payload and then sees `WouldBlock` (or a short read) must **save its
6//! progress and resume**, not error. The Connect/gRPC envelope reader (5-byte
7//! prefix + body), the `http2/` 9-byte frame codec, HTTP/3 framing, and the
8//! WebSocket decoder all share this requirement, so it is a foundation primitive
9//! rather than a per-protocol concern.
10//!
11//! WHAT: A generic [`IncrementalDecoder`] trait whose [`step`](IncrementalDecoder::step)
12//! feeds currently-available bytes and returns [`DecodeStep::Pending`] on a short
13//! read (state retained) or [`DecodeStep::Frame`] when a whole frame is ready;
14//! errors are reserved for genuine protocol violations. [`AccumulatingBuffer`] is
15//! the reusable core each codec is a state machine over — it carries leftover
16//! bytes between steps and yields completed payloads as **zero-copy** [`Bytes`].
17//! [`read_frame_blocking`] adapts a blocking caller (`loop { step }`-until-frame).
18//!
19//! HOW: The buffer is a task-owned [`BytesMut`] (it must survive parks, so it is
20//! never thread-local and never pooled — Decision 06 read path). Reads land in the
21//! buffer's spare capacity; completed frames leave via `split_to().freeze()` (a
22//! refcount split, no memcpy on the identity path). Parking composes with Decision
23//! 00: a `Pending` on an empty socket lets the driving task `Depends` on the
24//! reactor / wake-queue (feature 10) — this primitive stays transport-agnostic.
25
26use std::io::{self, Read};
27
28use bytes::{Buf, Bytes, BytesMut};
29
30/// Default number of bytes each [`AccumulatingBuffer::fill_from`] tries to read.
31const DEFAULT_READ_CHUNK: usize = 8 * 1024;
32
33/// Outcome of a single [`IncrementalDecoder::step`].
34///
35/// `Pending` never means "error" — it means "need more bytes, state retained".
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum DecodeStep<F> {
38    /// Not enough bytes to complete a frame yet; the decoder kept its partial state.
39    Pending,
40    /// A complete frame was decoded.
41    Frame(F),
42}
43
44/// Error from incremental decoding — a genuine protocol violation or an I/O
45/// failure, **never** a short read (that is [`DecodeStep::Pending`]).
46///
47/// WHY: Separating "need more bytes" from "the wire is malformed" is what lets one
48/// decoder resume across non-blocking reads without spurious errors.
49///
50/// WHAT: An I/O error surfaced from the source, or a protocol-level violation the
51/// decoder detected.
52///
53/// HOW: `derive_more` supplies `Display`/`From`; `Io` converts from `io::Error`.
54#[derive(Debug, derive_more::Display, derive_more::From)]
55pub enum DecodeError {
56    /// The underlying source returned an I/O error (not `WouldBlock`).
57    #[display("i/o error while decoding a frame: {_0}")]
58    Io(io::Error),
59    /// The bytes on the wire violate the frame format.
60    #[from(ignore)]
61    #[display("protocol violation while decoding a frame: {_0}")]
62    Protocol(Box<str>),
63}
64
65impl std::error::Error for DecodeError {}
66
67impl DecodeError {
68    /// Build a [`DecodeError::Protocol`] from anything string-like.
69    pub fn protocol(message: impl Into<Box<str>>) -> Self {
70        DecodeError::Protocol(message.into())
71    }
72}
73
74/// Drives a frame/record decoder one readable chunk at a time, holding partial
75/// state across calls and never erroring on a short read (Decision 12 §11).
76pub trait IncrementalDecoder {
77    /// The decoded frame type (often [`Bytes`], but codec-specific).
78    type Frame;
79
80    /// Feed currently-available bytes (may be empty) and try to decode one frame.
81    ///
82    /// WHY: Non-blocking sources deliver a frame across several reads; `step` makes
83    /// progress with whatever is available and resumes next time.
84    ///
85    /// WHAT: Reads what it can from `src`, advances the decoder's state machine, and
86    /// returns [`DecodeStep::Frame`] on completion or [`DecodeStep::Pending`] when
87    /// more bytes are needed (state retained).
88    ///
89    /// HOW: Typically `buffer.fill_from(src)?` then a state-machine pass over the
90    /// accumulated [`AccumulatingBuffer::view`].
91    ///
92    /// # Errors
93    /// [`DecodeError::Io`] on a non-`WouldBlock` source error; [`DecodeError::Protocol`]
94    /// on a genuine frame-format violation. A short read is `Pending`, never an error.
95    fn step(&mut self, src: &mut impl Read) -> Result<DecodeStep<Self::Frame>, DecodeError>;
96
97    /// Whether the decoder is currently mid-frame (holds buffered/partial bytes).
98    ///
99    /// WHY: Lets [`read_frame_blocking`] tell a **clean** end-of-stream (`false`)
100    /// from a **truncated** frame (`true`) when the source reaches EOF.
101    ///
102    /// # Panics
103    /// Never panics.
104    fn has_partial(&self) -> bool;
105}
106
107/// Loop [`IncrementalDecoder::step`] against a **blocking** source until one frame
108/// is decoded, the source cleanly ends, or a protocol error occurs.
109///
110/// WHY: Existing blocking callers (today's `decode` / envelope reads) want a simple
111/// "give me the next frame" call; this is the backward-compatible wrapper over the
112/// resumable `step` seam.
113///
114/// WHAT: Returns `Ok(Some(frame))` for the next frame, `Ok(None)` on a clean
115/// end-of-stream at a frame boundary, or an error.
116///
117/// HOW: Wraps `src` so a real `read == 0` (EOF) is observed; on EOF with no partial
118/// state it returns `Ok(None)`, and with partial state it reports a truncated frame.
119///
120/// # Errors
121/// [`DecodeError::Io`] / [`DecodeError::Protocol`] from `step`, or a truncated-frame
122/// [`DecodeError::Protocol`] if the source ends mid-frame.
123///
124/// # Panics
125/// Never panics.
126pub fn read_frame_blocking<D, R>(
127    decoder: &mut D,
128    src: &mut R,
129) -> Result<Option<D::Frame>, DecodeError>
130where
131    D: IncrementalDecoder,
132    R: Read,
133{
134    let mut tracked = EofTrackingReader {
135        inner: src,
136        hit_eof: false,
137    };
138    loop {
139        match decoder.step(&mut tracked)? {
140            DecodeStep::Frame(frame) => return Ok(Some(frame)),
141            DecodeStep::Pending => {
142                if tracked.hit_eof {
143                    return if decoder.has_partial() {
144                        Err(DecodeError::protocol("source ended mid-frame (truncated)"))
145                    } else {
146                        Ok(None)
147                    };
148                }
149                // Blocking source: the next `step` will block on `read` for more.
150            }
151        }
152    }
153}
154
155/// A `Read` wrapper that records whether the inner source has hit end-of-stream.
156struct EofTrackingReader<'a, R: Read> {
157    inner: &'a mut R,
158    hit_eof: bool,
159}
160
161impl<R: Read> Read for EofTrackingReader<'_, R> {
162    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
163        let read = self.inner.read(buf)?;
164        // A zero-length request also returns 0 — only a non-empty request reaching
165        // 0 is a true EOF.
166        if !buf.is_empty() && read == 0 {
167            self.hit_eof = true;
168        }
169        Ok(read)
170    }
171}
172
173/// The reusable accumulating-buffer core: carries leftover bytes between decode
174/// steps, exposes a contiguous view, and yields completed payloads as zero-copy
175/// [`Bytes`] (Decision 12 §11).
176///
177/// WHY: Every incremental codec needs the same "read some, keep the remainder,
178/// hand out finished payloads without copying" mechanics.
179///
180/// WHAT: A task-owned [`BytesMut`] (survives parks — never thread-local, never
181/// pooled) plus the per-`fill` read size.
182///
183/// HOW: `fill_from` reads into the buffer's tail; `split_to` refcount-splits the
184/// front out as [`Bytes`]; `advance` discards consumed bytes.
185#[derive(Debug)]
186pub struct AccumulatingBuffer {
187    buf: BytesMut,
188    read_chunk: usize,
189}
190
191impl Default for AccumulatingBuffer {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197impl AccumulatingBuffer {
198    /// Create an empty buffer with the default per-read chunk size.
199    #[must_use]
200    pub fn new() -> Self {
201        Self {
202            buf: BytesMut::new(),
203            read_chunk: DEFAULT_READ_CHUNK,
204        }
205    }
206
207    /// Create an empty buffer that pre-reserves `capacity` bytes.
208    #[must_use]
209    pub fn with_capacity(capacity: usize) -> Self {
210        Self {
211            buf: BytesMut::with_capacity(capacity),
212            read_chunk: DEFAULT_READ_CHUNK,
213        }
214    }
215
216    /// Set how many bytes each [`fill_from`](Self::fill_from) attempts to read
217    /// (clamped to a minimum of 1). Returns `self` for chaining.
218    #[must_use]
219    pub fn with_read_chunk(mut self, read_chunk: usize) -> Self {
220        self.read_chunk = read_chunk.max(1);
221        self
222    }
223
224    /// Number of buffered (undecoded) bytes.
225    #[must_use]
226    pub fn len(&self) -> usize {
227        self.buf.len()
228    }
229
230    /// Whether no bytes are currently buffered.
231    #[must_use]
232    pub fn is_empty(&self) -> bool {
233        self.buf.is_empty()
234    }
235
236    /// Contiguous view of the accumulated bytes (for the decoder's state machine).
237    #[must_use]
238    pub fn view(&self) -> &[u8] {
239        &self.buf
240    }
241
242    /// Read one chunk of currently-available bytes from `src` into the buffer.
243    ///
244    /// WHY: The "feed currently-available bytes" step — one best-effort read.
245    ///
246    /// WHAT: Returns the number of new bytes appended. `Ok(0)` means either EOF or
247    /// (on a non-blocking source) no data is available right now — `WouldBlock` is
248    /// mapped to `0` so `step` can return `Pending` cleanly.
249    ///
250    /// HOW: Grows the buffer by `read_chunk`, reads directly into that spare region
251    /// (no temp-buffer copy), then truncates to what was actually read.
252    ///
253    /// # Errors
254    /// Any source error other than `WouldBlock`.
255    ///
256    /// # Panics
257    /// Never panics.
258    pub fn fill_from(&mut self, src: &mut impl Read) -> io::Result<usize> {
259        let start = self.buf.len();
260        // Zero-extend the tail so we have an initialized `&mut [u8]` to read into —
261        // one memset, no separate temp buffer / second data copy.
262        self.buf.resize(start + self.read_chunk, 0);
263        match src.read(&mut self.buf[start..]) {
264            Ok(read) => {
265                self.buf.truncate(start + read);
266                Ok(read)
267            }
268            Err(err) => {
269                self.buf.truncate(start);
270                if err.kind() == io::ErrorKind::WouldBlock {
271                    Ok(0)
272                } else {
273                    Err(err)
274                }
275            }
276        }
277    }
278
279    /// Split the first `n` bytes off the front as a zero-copy [`Bytes`], advancing
280    /// the buffer past them.
281    ///
282    /// # Panics
283    /// Panics if `n > len()`.
284    #[must_use = "the split-off payload is the decoded frame"]
285    pub fn split_to(&mut self, n: usize) -> Bytes {
286        assert!(
287            n <= self.buf.len(),
288            "split_to({n}) exceeds buffered length {}",
289            self.buf.len()
290        );
291        self.buf.split_to(n).freeze()
292    }
293
294    /// Discard the first `n` bytes (e.g. a consumed frame header).
295    ///
296    /// # Panics
297    /// Panics if `n > len()`.
298    pub fn advance(&mut self, n: usize) {
299        assert!(
300            n <= self.buf.len(),
301            "advance({n}) exceeds buffered length {}",
302            self.buf.len()
303        );
304        self.buf.advance(n);
305    }
306
307    /// Drop all buffered bytes.
308    pub fn clear(&mut self) {
309        self.buf.clear();
310    }
311}