Skip to main content

sim_lib_net_core/
sse.rs

1//! Server-Sent Events record decoding.
2
3use crate::{
4    NetError,
5    line::{DEFAULT_MAX_LINE_BYTES, LineDecoder},
6};
7
8/// A decoded Server-Sent Events record.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct SseEvent {
11    /// The `event:` field value, if any was present in the record.
12    pub event: Option<String>,
13    /// The `data:` payload. Multiple `data:` lines are joined with `\n`.
14    pub data: String,
15}
16
17/// Incremental Server-Sent Events decoder.
18///
19/// Accumulates `event:` and `data:` field lines and emits an [`SseEvent`] when a
20/// blank line terminates the record (standard SSE dispatch). Multiple `data:`
21/// lines within one record are joined with `\n`. Comment lines (those starting
22/// with `:`) and unknown fields are ignored. Field values have a single leading
23/// space after the colon stripped, per the SSE specification.
24///
25/// `push` does the newline framing internally via [`LineDecoder`]; callers that
26/// already have whole lines can use
27/// [`push_line_checked`](SseDecoder::push_line_checked).
28///
29/// [`new`](SseDecoder::new) uses [`DEFAULT_MAX_LINE_BYTES`] for both a single
30/// line and the accumulated `data:` payload in one record.
31/// [`unbounded`](SseDecoder::unbounded) is the explicit opt-out for trusted
32/// in-memory inputs.
33#[derive(Debug)]
34pub struct SseDecoder {
35    lines: LineDecoder,
36    event: Option<String>,
37    data: Vec<String>,
38    data_bytes: usize,
39    max_line_bytes: Option<usize>,
40    have_record: bool,
41}
42
43impl Default for SseDecoder {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl SseDecoder {
50    /// Create an empty decoder using [`DEFAULT_MAX_LINE_BYTES`].
51    pub fn new() -> Self {
52        Self::with_max_line_bytes(DEFAULT_MAX_LINE_BYTES)
53    }
54
55    /// Create an empty decoder with no line or event-data limit.
56    ///
57    /// Use this only for trusted, already-bounded inputs.
58    pub fn unbounded() -> Self {
59        Self {
60            lines: LineDecoder::unbounded(),
61            event: None,
62            data: Vec::new(),
63            data_bytes: 0,
64            max_line_bytes: None,
65            have_record: false,
66        }
67    }
68
69    /// Create an empty decoder that rejects any line, buffered partial line, or
70    /// accumulated record data longer than `max_line_bytes`.
71    pub fn with_max_line_bytes(max_line_bytes: usize) -> Self {
72        Self {
73            lines: LineDecoder::with_max_line_bytes(max_line_bytes),
74            event: None,
75            data: Vec::new(),
76            data_bytes: 0,
77            max_line_bytes: Some(max_line_bytes),
78            have_record: false,
79        }
80    }
81
82    /// Feed raw bytes; return any records completed by blank lines.
83    pub fn push(&mut self, bytes: &[u8]) -> Result<Vec<SseEvent>, NetError> {
84        self.push_checked(bytes)
85    }
86
87    /// Feed raw bytes; return any records completed by blank lines.
88    pub fn push_checked(&mut self, bytes: &[u8]) -> Result<Vec<SseEvent>, NetError> {
89        let mut events = Vec::new();
90        for line in self.lines.push_checked(bytes)? {
91            // SSE payloads are UTF-8; lossy decoding keeps the framer robust and
92            // pushes any invalid-byte handling onto the caller's data parse.
93            let text = String::from_utf8_lossy(&line);
94            if let Some(event) = self.push_line_checked(&text)? {
95                events.push(event);
96            }
97        }
98        Ok(events)
99    }
100
101    /// Feed a single already-framed line (without its terminating newline).
102    /// Returns an [`SseEvent`] when this line is the blank line that dispatches
103    /// the accumulated record.
104    pub fn push_line(&mut self, line: &str) -> Option<SseEvent> {
105        self.push_line_checked(line)
106            .expect("sse decoder cap exceeded; use push_line_checked for fallible handling")
107    }
108
109    /// Feed a single already-framed line, checking line and accumulated data
110    /// bounds.
111    pub fn push_line_checked(&mut self, line: &str) -> Result<Option<SseEvent>, NetError> {
112        self.check_line(line.len())?;
113        if line.is_empty() {
114            return Ok(self.dispatch());
115        }
116        if line.starts_with(':') {
117            // Comment line: ignored. A following blank line dispatches only if
118            // fields were set.
119            return Ok(None);
120        }
121        let (field, value) = match line.split_once(':') {
122            Some((field, rest)) => (field, rest.strip_prefix(' ').unwrap_or(rest)),
123            None => (line, ""),
124        };
125        match field {
126            "event" => {
127                self.event = Some(value.to_owned());
128                self.have_record = true;
129            }
130            "data" => {
131                self.add_data(value)?;
132                self.data.push(value.to_owned());
133                self.have_record = true;
134            }
135            _ => {}
136        }
137        Ok(None)
138    }
139
140    fn dispatch(&mut self) -> Option<SseEvent> {
141        if !self.have_record {
142            return None;
143        }
144        let event = self.event.take();
145        let data = std::mem::take(&mut self.data).join("\n");
146        self.data_bytes = 0;
147        self.have_record = false;
148        Some(SseEvent { event, data })
149    }
150
151    /// Flush any buffered partial line and any accumulated-but-undispatched
152    /// record (a stream that ended without a final blank line).
153    pub fn flush(&mut self) -> Option<SseEvent> {
154        self.flush_checked()
155            .expect("sse decoder cap exceeded; use flush_checked for fallible handling")
156    }
157
158    /// Flush any buffered partial line and any accumulated-but-undispatched
159    /// record, checking configured bounds first.
160    pub fn flush_checked(&mut self) -> Result<Option<SseEvent>, NetError> {
161        if let Some(line) = self.lines.flush_checked()? {
162            let text = String::from_utf8_lossy(&line);
163            if let Some(event) = self.push_line_checked(&text)? {
164                return Ok(Some(event));
165            }
166        }
167        Ok(self.dispatch())
168    }
169
170    fn add_data(&mut self, value: &str) -> Result<(), NetError> {
171        let separator = usize::from(!self.data.is_empty());
172        let next_len = self
173            .data_bytes
174            .saturating_add(separator)
175            .saturating_add(value.len());
176        self.check_line(next_len)?;
177        self.data_bytes = next_len;
178        Ok(())
179    }
180
181    fn check_line(&self, len: usize) -> Result<(), NetError> {
182        if let Some(max) = self.max_line_bytes
183            && len > max
184        {
185            return Err(NetError::LineTooLong { max });
186        }
187        Ok(())
188    }
189}