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}