Skip to main content

sse_parse/
parser.rs

1use alloc::collections::VecDeque;
2use alloc::string::String;
3use alloc::vec::Vec;
4use core::mem;
5use core::time::Duration;
6
7use crate::Event;
8
9const BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
10
11/// Whether the one-time leading BOM check at stream offset 0 has completed.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13enum Bom {
14    Undecided,
15    Done,
16}
17
18/// The WHATWG per-stream state: the three line buffers plus the two pieces
19/// of cross-event state, and the queue of completed events.
20#[derive(Debug, Clone, Default)]
21struct Core {
22    data: String,
23    event_type: String,
24    id_buf: String,
25    last_event_id: String,
26    retry: Option<Duration>,
27    pending_retry: Option<Duration>,
28    retry_updates: u64,
29    events: VecDeque<Event>,
30}
31
32impl Core {
33    /// Classify one completed line and apply its field, per the field
34    /// processing rules of the HTML Living Standard's "Interpreting an
35    /// event stream" section.
36    fn process_line(&mut self, line: &[u8]) {
37        if line.is_empty() {
38            self.dispatch();
39            return;
40        }
41        if line[0] == b':' {
42            return;
43        }
44        let (name, value) = match memchr::memchr(b':', line) {
45            Some(i) => {
46                let value = &line[i + 1..];
47                (&line[..i], value.strip_prefix(b" ").unwrap_or(value))
48            }
49            None => (line, &line[line.len()..]),
50        };
51        match name {
52            b"event" => {
53                self.event_type.clear();
54                push_lossy(&mut self.event_type, value);
55            }
56            b"data" => {
57                push_lossy(&mut self.data, value);
58                self.data.push('\n');
59            }
60            b"id" => {
61                if !value.contains(&0) {
62                    self.id_buf.clear();
63                    push_lossy(&mut self.id_buf, value);
64                }
65            }
66            b"retry" => {
67                if let Some(ms) = parse_retry(value) {
68                    let retry = Duration::from_millis(ms);
69                    self.retry = Some(retry);
70                    self.pending_retry = Some(retry);
71                    self.retry_updates += 1;
72                }
73            }
74            _ => {}
75        }
76    }
77
78    /// The blank-line "dispatch the event" steps of the HTML Living
79    /// Standard's "Interpreting an event stream" section, in order.
80    fn dispatch(&mut self) {
81        if self.last_event_id != self.id_buf {
82            self.last_event_id.clone_from(&self.id_buf);
83        }
84        if self.data.is_empty() {
85            self.event_type.clear();
86            return;
87        }
88        if self.data.ends_with('\n') {
89            self.data.pop();
90        }
91        let event = if self.event_type.is_empty() {
92            String::from("message")
93        } else {
94            mem::take(&mut self.event_type)
95        };
96        self.events.push_back(Event {
97            event,
98            data: mem::take(&mut self.data),
99            id: self.last_event_id.clone(),
100            retry: self.pending_retry.take(),
101        });
102    }
103
104    /// Drop the block under construction. The last event ID buffer is stream
105    /// state and survives.
106    fn discard_block(&mut self) {
107        self.data.clear();
108        self.event_type.clear();
109    }
110
111    fn block_len(&self) -> usize {
112        self.data.len() + self.event_type.len()
113    }
114}
115
116fn push_lossy(dst: &mut String, bytes: &[u8]) {
117    match core::str::from_utf8(bytes) {
118        Ok(s) => dst.push_str(s),
119        Err(_) => dst.push_str(&String::from_utf8_lossy(bytes)),
120    }
121}
122
123/// The spec's `retry` rule plus a pinned edge decision: only ASCII
124/// digits, at least one, and the value must fit in a `u64`.
125fn parse_retry(value: &[u8]) -> Option<u64> {
126    if value.is_empty() {
127        return None;
128    }
129    let mut ms: u64 = 0;
130    for &b in value {
131        if !b.is_ascii_digit() {
132            return None;
133        }
134        ms = ms.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
135    }
136    Some(ms)
137}
138
139/// Test and diagnostics hook. Not part of the stable API.
140#[doc(hidden)]
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
142pub struct Stats {
143    /// Bytes examined by terminator scanning plus bytes handed to line
144    /// processing. Bounded by twice the bytes fed.
145    pub bytes_scanned: u64,
146    /// Number of times a valid `retry` field updated the reconnection time.
147    pub retry_updates: u64,
148    /// Bytes currently held: the rolling input buffer plus the block under
149    /// construction.
150    pub buffered_bytes: usize,
151}
152
153/// Sans-IO incremental SSE parser.
154///
155/// Feed bytes with [`feed`](Self::feed), drain completed events with
156/// [`next_event`](Self::next_event), and call [`finish`](Self::finish) at end
157/// of stream. Chunk boundaries never affect the output: any chunking of a
158/// byte stream yields the same events as feeding it whole.
159///
160/// Time is linear in the bytes fed. Each byte is scanned once for a line
161/// terminator and decoded once as part of its completed line, whatever the
162/// chunk size. Memory is proportional to the longest block in flight unless
163/// [`max_buffer`](Self::max_buffer) caps it.
164#[derive(Debug, Clone)]
165pub struct SseParser {
166    /// Rolling input buffer: `buf[..consumed]` is processed and awaits
167    /// compaction, `buf[consumed..scan_pos]` is a scanned partial line, and
168    /// `buf[scan_pos..]` is not yet scanned.
169    buf: Vec<u8>,
170    consumed: usize,
171    scan_pos: usize,
172    /// The last scanned byte was a CR that ended a line. The next byte decides
173    /// whether it was the CR of a CRLF pair (then the LF is swallowed).
174    held_cr: bool,
175    bom: Bom,
176    finished: bool,
177    core: Core,
178    max_buffer: Option<usize>,
179    /// An oversized block is being thrown away until the next blank line.
180    discarding: bool,
181    /// Bytes of the current line were dropped, so its terminator must not be
182    /// mistaken for a blank line.
183    in_dropped_line: bool,
184    overflows: u64,
185    bytes_scanned: u64,
186}
187
188impl Default for SseParser {
189    fn default() -> Self {
190        Self::new()
191    }
192}
193
194impl SseParser {
195    /// A parser with an empty last event ID string and no buffer limit.
196    pub fn new() -> Self {
197        SseParser {
198            buf: Vec::new(),
199            consumed: 0,
200            scan_pos: 0,
201            held_cr: false,
202            bom: Bom::Undecided,
203            finished: false,
204            core: Core::default(),
205            max_buffer: None,
206            discarding: false,
207            in_dropped_line: false,
208            overflows: 0,
209            bytes_scanned: 0,
210        }
211    }
212
213    /// A parser seeded with a last event ID string, for reconnection.
214    ///
215    /// Events dispatched before the stream's first `id` field report this ID.
216    pub fn with_last_event_id(id: impl Into<String>) -> Self {
217        let mut parser = Self::new();
218        parser.set_last_event_id(id);
219        parser
220    }
221
222    /// Cap the bytes held for one block.
223    ///
224    /// A block overflows when the raw bytes of one of its lines, before the
225    /// terminator, plus the bytes already in the block's `data` and `event`
226    /// buffers exceed `limit`, or when those buffers exceed `limit` after a
227    /// line is decoded (invalid UTF-8 expands to the three-byte U+FFFD). The
228    /// checks run on every completed line and on the unterminated line after
229    /// every feed, so the outcome is the same for every chunking of the
230    /// stream, and after each feed the parser holds at most `limit` bytes
231    /// plus one chunk.
232    ///
233    /// On overflow the parser drops the block and ignores every following
234    /// line up to the next blank line, where it resynchronizes. Fields
235    /// processed before the overflow keep their effect: an `id` or `retry`
236    /// seen earlier in the block stands, and the blank line still commits the
237    /// last event ID. [`overflows`](Self::overflows) counts dropped blocks.
238    /// The default is unlimited.
239    pub fn max_buffer(mut self, limit: usize) -> Self {
240        self.max_buffer = Some(limit);
241        self
242    }
243
244    /// Number of blocks dropped because they exceeded the
245    /// [`max_buffer`](Self::max_buffer) cap.
246    pub fn overflows(&self) -> u64 {
247        self.overflows
248    }
249
250    pub(crate) fn has_pending_events(&self) -> bool {
251        !self.core.events.is_empty()
252    }
253
254    pub(crate) fn set_last_event_id(&mut self, id: impl Into<String>) {
255        let id = id.into();
256        self.core.id_buf.clone_from(&id);
257        self.core.last_event_id = id;
258    }
259
260    /// Feed one chunk of bytes.
261    ///
262    /// Every byte sequence is a valid event stream, so there is nothing to
263    /// reject. `tests/no_panic.rs` and the fuzz target feed arbitrary bytes
264    /// in arbitrary chunkings.
265    ///
266    /// Feeding after [`finish`](Self::finish) is a contract violation: it
267    /// trips a debug assertion and is ignored in release builds.
268    pub fn feed(&mut self, bytes: &[u8]) {
269        debug_assert!(!self.finished, "SseParser::feed called after finish()");
270        if self.finished || bytes.is_empty() {
271            return;
272        }
273        self.buf.extend_from_slice(bytes);
274        if self.bom == Bom::Undecided && !self.resolve_bom() {
275            return;
276        }
277        self.scan();
278        self.enforce_limit();
279        self.compact();
280    }
281
282    /// Next completed event, in stream order, if any.
283    pub fn next_event(&mut self) -> Option<Event> {
284        self.core.events.pop_front()
285    }
286
287    /// Signal end of stream.
288    ///
289    /// A block with no closing blank line is discarded, per the WHATWG
290    /// algorithm. Events completed before the end stay available through
291    /// [`next_event`](Self::next_event). Calling this twice is harmless.
292    pub fn finish(&mut self) {
293        if self.finished {
294            return;
295        }
296        self.finished = true;
297        self.held_cr = false;
298        self.buf = Vec::new();
299        self.consumed = 0;
300        self.scan_pos = 0;
301        self.discarding = false;
302        self.in_dropped_line = false;
303        self.core.discard_block();
304    }
305
306    /// The last event ID string: the value a client sends back as
307    /// `Last-Event-ID` on reconnection.
308    ///
309    /// Committed at every blank line, including blocks that dispatch no event.
310    pub fn last_event_id(&self) -> &str {
311        &self.core.last_event_id
312    }
313
314    /// Reconnection time from the most recent valid `retry` field, if any.
315    ///
316    /// Updated when the field is processed, whether or not its block
317    /// dispatches an event.
318    pub fn retry(&self) -> Option<Duration> {
319        self.core.retry
320    }
321
322    /// Test and diagnostics hook. Not part of the stable API.
323    #[doc(hidden)]
324    pub fn stats(&self) -> Stats {
325        Stats {
326            bytes_scanned: self.bytes_scanned,
327            retry_updates: self.core.retry_updates,
328            buffered_bytes: self.buf.len() + self.core.block_len(),
329        }
330    }
331
332    /// Strip one leading `EF BB BF` byte order mark at absolute offset 0,
333    /// split-proof, per the event stream grammar of the HTML Living
334    /// Standard's "Parsing an event stream" section.
335    ///
336    /// Returns false when more bytes are needed to decide. Only called while
337    /// nothing has been consumed, so the buffer starts at stream offset 0.
338    fn resolve_bom(&mut self) -> bool {
339        let n = self.buf.len().min(BOM.len());
340        if self.buf[..n] != BOM[..n] {
341            self.bom = Bom::Done;
342            return true;
343        }
344        if n < BOM.len() {
345            return false;
346        }
347        self.bom = Bom::Done;
348        self.consumed = BOM.len();
349        self.scan_pos = BOM.len();
350        true
351    }
352
353    /// Split completed lines at CR, LF, and CRLF, per the event stream
354    /// grammar of the HTML Living Standard's "Parsing an event stream"
355    /// section. Only `buf[scan_pos..]` is examined, so no byte is scanned
356    /// twice.
357    fn scan(&mut self) {
358        if self.held_cr {
359            if self.scan_pos >= self.buf.len() {
360                return;
361            }
362            self.held_cr = false;
363            if self.buf[self.scan_pos] == b'\n' {
364                self.bytes_scanned += 1;
365                self.scan_pos += 1;
366                self.consumed = self.scan_pos;
367            }
368        }
369        while self.scan_pos < self.buf.len() {
370            let unscanned = &self.buf[self.scan_pos..];
371            let i = match memchr::memchr2(b'\r', b'\n', unscanned) {
372                Some(i) => i,
373                None => {
374                    self.bytes_scanned += unscanned.len() as u64;
375                    self.scan_pos = self.buf.len();
376                    break;
377                }
378            };
379            let term = self.scan_pos + i;
380            let is_cr = self.buf[term] == b'\r';
381            self.bytes_scanned += i as u64 + 1;
382            self.handle_line(term);
383            self.consumed = term + 1;
384            self.scan_pos = term + 1;
385            if is_cr {
386                if self.scan_pos < self.buf.len() {
387                    if self.buf[self.scan_pos] == b'\n' {
388                        self.bytes_scanned += 1;
389                        self.scan_pos += 1;
390                        self.consumed = self.scan_pos;
391                    }
392                } else {
393                    self.held_cr = true;
394                }
395            }
396        }
397    }
398
399    /// The line `buf[consumed..end]` just got its terminator.
400    fn handle_line(&mut self, end: usize) {
401        let line = &self.buf[self.consumed..end];
402        self.bytes_scanned += line.len() as u64;
403        if self.discarding {
404            if self.in_dropped_line {
405                self.in_dropped_line = false;
406            } else if line.is_empty() {
407                self.discarding = false;
408                self.core.process_line(line);
409            }
410            return;
411        }
412        if !line.is_empty() && self.exceeds_cap(line.len()) {
413            self.overflow();
414            return;
415        }
416        self.core.process_line(line);
417        // Lossy decoding can grow a value: each invalid byte becomes the
418        // three-byte U+FFFD. Re-check the decoded block so the held bytes
419        // stay under the cap.
420        if self.exceeds_cap(0) {
421            self.overflow();
422        }
423    }
424
425    /// Whether `line_bytes` of one line on top of the block in progress
426    /// exceed the [`max_buffer`](Self::max_buffer) cap.
427    fn exceeds_cap(&self, line_bytes: usize) -> bool {
428        match self.max_buffer {
429            Some(limit) => line_bytes.saturating_add(self.core.block_len()) > limit,
430            None => false,
431        }
432    }
433
434    fn overflow(&mut self) {
435        self.overflows += 1;
436        self.discarding = true;
437        self.core.discard_block();
438    }
439
440    /// Apply the cap to the unterminated line left after a scan. While a
441    /// block is being discarded, its pending bytes are dropped as they arrive
442    /// and their terminator is remembered as the end of a dropped line.
443    fn enforce_limit(&mut self) {
444        if self.max_buffer.is_none() {
445            return;
446        }
447        let pending = self.buf.len() - self.consumed;
448        if pending == 0 {
449            return;
450        }
451        if !self.discarding && self.exceeds_cap(pending) {
452            self.overflow();
453        }
454        if self.discarding {
455            self.in_dropped_line = true;
456            self.consumed = self.buf.len();
457        }
458    }
459
460    /// Reclaim consumed bytes. A move only happens once the consumed prefix
461    /// is at least as long as the remainder, so moves cost amortized O(1)
462    /// per byte fed.
463    fn compact(&mut self) {
464        if self.consumed == 0 {
465            return;
466        }
467        if self.consumed == self.buf.len() {
468            self.buf.clear();
469        } else if self.consumed >= self.buf.len() / 2 {
470            self.buf.drain(..self.consumed);
471        } else {
472            return;
473        }
474        self.scan_pos -= self.consumed;
475        self.consumed = 0;
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    #[test]
484    fn retry_parsing() {
485        assert_eq!(parse_retry(b""), None);
486        assert_eq!(parse_retry(b"0"), Some(0));
487        assert_eq!(parse_retry(b"3000"), Some(3000));
488        assert_eq!(parse_retry(b"30x0"), None);
489        assert_eq!(parse_retry(b"12 "), None);
490        assert_eq!(parse_retry(b"-1"), None);
491        assert_eq!(parse_retry(b"18446744073709551615"), Some(u64::MAX));
492        assert_eq!(parse_retry(b"18446744073709551616"), None);
493        assert_eq!(parse_retry(b"99999999999999999999999"), None);
494    }
495
496    #[test]
497    fn compaction_keeps_offsets_consistent() {
498        let mut parser = SseParser::new();
499        parser.feed(b"data: a\n\ndata: bbbbbbbbbbbbbbbbbbbb");
500        assert_eq!(parser.next_event().unwrap().data, "a");
501        parser.feed(b"\n\n");
502        assert_eq!(parser.next_event().unwrap().data, "bbbbbbbbbbbbbbbbbbbb");
503        assert_eq!(parser.buf.len(), 0);
504        assert_eq!(parser.consumed, 0);
505        assert_eq!(parser.scan_pos, 0);
506    }
507}