Skip to main content

protocol_droid/adapters/secondary/transport/http/
frames.rs

1//! Turning a byte stream into SSE frames, without deciding what they mean.
2//!
3//! [`SseEvent`] already knows how to parse one block of text into an event and
4//! what its fields mean. It does not know that a block arrives in pieces, nor
5//! where one block ends and the next begins on the wire. This module owns
6//! exactly that gap, and nothing else — the frame grammar stays with the
7//! server's codec, because a client with its own grammar is a client that
8//! disagrees with the server on resume, and the disagreement shows up as a
9//! duplicated event rather than as an error.
10//!
11//! The split is not cosmetic. A reader that re-joined frames on `\n` alone
12//! splits a CRLF-terminated block into one block plus an empty one; a reader
13//! that treated an empty block as an event delivers a keep-alive to the
14//! protocol layer as if the server had said something. Both are silent: the
15//! stream stays well-formed and the client just sees more events than there
16//! were.
17
18use crate::adapters::primary::transport::http::{SseError, SseEvent};
19
20/// The largest single SSE block this reader will assemble.
21///
22/// A block is one event. The server writes each in one go and the grammar puts
23/// no bound on one, but an unbounded buffer over a peer-supplied stream is a
24/// memory cost with no ceiling. Exceeding this is an error rather than a
25/// truncation: a truncated event handed upward is a lie about what the peer
26/// said, and a lie with an event id attached is a lie a resume will repeat.
27const MAX_BLOCK: usize = 8 * 1024;
28
29/// Why a byte stream did not yield frames this client can hand on.
30///
31/// Separate from [`SseError`] on purpose. [`SseError`] is the wire grammar's
32/// vocabulary and lives with the codec that both sides share; the length bound
33/// is this reader's memory policy, not a rule a peer could observe if we chose
34/// a different number.
35#[derive(Debug, thiserror::Error)]
36pub enum FrameError {
37    /// The peer sent more than `MAX_BLOCK` bytes without a block terminator.
38    #[error("the peer sent {limit} bytes with no block terminator")]
39    BlockTooLong {
40        /// The bound that was exceeded.
41        limit: usize,
42    },
43
44    /// The block was terminated but is not a frame.
45    #[error("the block is not a frame this client can read: {source}")]
46    Grammar {
47        /// The shared codec's account of what was wrong.
48        source: SseError,
49    },
50}
51
52impl From<SseError> for FrameError {
53    fn from(source: SseError) -> Self {
54        Self::Grammar { source }
55    }
56}
57
58/// Assembles SSE blocks out of arbitrarily chunked bytes.
59///
60/// Fed whatever the transport delivers; yields complete frames. The buffer is
61/// retained between calls because a frame routinely spans two reads, and
62/// dropping it per read is what turns a normal stream into a stream of one-byte
63/// events.
64#[derive(Debug, Default)]
65pub struct FrameReader {
66    buffer: Vec<u8>,
67}
68
69impl FrameReader {
70    /// A reader with nothing buffered.
71    #[must_use]
72    pub const fn new() -> Self {
73        Self { buffer: Vec::new() }
74    }
75
76    /// How many bytes are held waiting for their terminator.
77    #[must_use]
78    pub const fn buffered(&self) -> usize {
79        self.buffer.len()
80    }
81
82    /// Feeds bytes and yields every complete frame they finished.
83    ///
84    /// Yields in wire order and stops at the first partial block, because
85    /// anything after an unterminated block is inside that block — the grammar
86    /// has no way to say "and here is the next event".
87    ///
88    /// # Errors
89    ///
90    /// [`FrameError::BlockTooLong`] if the buffered block grows past
91    /// `MAX_BLOCK` before a terminator arrives, or [`FrameError::Grammar`] if
92    /// a completed block does not parse.
93    pub fn push(&mut self, chunk: &[u8]) -> Result<Vec<SseEvent>, FrameError> {
94        self.buffer.extend_from_slice(chunk);
95        if self.buffer.len() > MAX_BLOCK {
96            // Cleared before returning, so a peer that keeps sending does not
97            // make the next call fail on a block this one already rejected.
98            self.buffer.clear();
99            return Err(FrameError::BlockTooLong { limit: MAX_BLOCK });
100        }
101
102        // An index rather than a repeated re-slice-and-reassign: the block
103        // borrows the buffer it was found in, so consuming it as it goes means
104        // one `drain` at the end instead of a borrow that ends and restarts
105        // once per frame. On a stream that reads forever, that is the
106        // difference between one copy per read and one copy per frame.
107        let mut frames = Vec::new();
108        let mut consumed = 0_usize;
109        while let Some(tail) = self.buffer.get(consumed..) {
110            let Some((block, used)) = split_block(tail) else {
111                break;
112            };
113            if let Some(event) = SseEvent::dispatch(block)? {
114                frames.push(event);
115            }
116            consumed = consumed.saturating_add(used);
117        }
118        if consumed > 0 {
119            let _ = self.buffer.drain(..consumed);
120        }
121        Ok(frames)
122    }
123
124    /// Takes the bytes of a block that never got its terminator.
125    ///
126    /// `None` for a stream that ended cleanly — one whose trailing block is
127    /// absent, or complete. `Some` is the difference between a connection that
128    /// dropped mid-event and one that ended on a boundary, and it is what
129    /// decides whether a resume has to guess.
130    #[must_use]
131    pub fn into_partial(self) -> Option<Vec<u8>> {
132        if self.buffer.is_empty() {
133            None
134        } else {
135            Some(self.buffer)
136        }
137    }
138}
139
140/// Finds the first complete block in `bytes`, and how long it is.
141///
142/// A block ends at a **blank line**, not at the first newline. That is the
143/// whole framing: `id: 1\ndata: x\n\n` is one event, and a reader that stops
144/// at the first newline hands the grammar a block with no blank line in it and
145/// never dispatches anything at all. The block is returned **including** its
146/// last line terminator, because that is the shape the encoder wrote and the
147/// shape the decoder is written against — a payload's final line is empty, and
148/// the trailing newline is what makes it there to be read.
149///
150/// The length is returned rather than a remainder because the block borrows
151/// `bytes`: a caller that reassigns the buffer to make progress cannot do so
152/// while that borrow lives.
153///
154/// # Panics
155///
156/// Never. The returned block is a subslice of `bytes` and `used` is its length.
157#[must_use]
158pub fn split_block(bytes: &[u8]) -> Option<(&str, usize)> {
159    let mut start = 0_usize;
160    loop {
161        let end = line_end(bytes, start)?;
162        // The line just read is the block's last if the line after it is empty.
163        // An empty line is a terminator with nothing between it and this one,
164        // and it is the only thing that dispatches an event.
165        let blank = match bytes.get(end) {
166            Some(b'\n') => Some(end.saturating_add(1)),
167            Some(b'\r') => Some(if bytes.get(end + 1) == Some(&b'\n') {
168                end.saturating_add(2)
169            } else {
170                end.saturating_add(1)
171            }),
172            // The stream ended on a line boundary with no blank line after it,
173            // so the event was never dispatched. Reporting "no block" is what
174            // lets the caller tell a cut-off frame from a finished one.
175            _ => None,
176        };
177        if let Some(consumed) = blank {
178            let block = bytes.get(..end)?;
179            return Some((decode_lossy(block), consumed));
180        }
181        start = end;
182    }
183}
184
185/// The index just past the terminator of the line starting at `from`.
186///
187/// All three terminators are accepted, because a peer that writes CRLF and a
188/// peer that writes LF are both conforming and a reader that understands only
189/// one of them fails on half its peers. A lone `CR` at the end returns `None`
190/// rather than guessing: the `LF` may be in the next chunk, and resolving it
191/// here is how a CRLF stream gets a phantom event whenever a chunk boundary
192/// lands between the pair.
193fn line_end(bytes: &[u8], from: usize) -> Option<usize> {
194    let mut index = from;
195    while index < bytes.len() {
196        match bytes.get(index) {
197            Some(b'\n') => return Some(index.saturating_add(1)),
198            Some(b'\r') => {
199                return Some(if bytes.get(index + 1) == Some(&b'\n') {
200                    index.saturating_add(2)
201                } else {
202                    index.saturating_add(1)
203                });
204            }
205            _ => index = index.saturating_add(1),
206        }
207    }
208    None
209}
210
211/// Decodes a block that may have arrived split across a multi-byte character.
212///
213/// Lossy on purpose. A block cut short by a dropped connection can end inside a
214/// multi-byte sequence, and the caller is about to discard those bytes anyway;
215/// what reaches the grammar decoder is the valid prefix.
216fn decode_lossy(bytes: &[u8]) -> &str {
217    core::str::from_utf8(bytes).unwrap_or_default()
218}