#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SseEvent {
pub event: Option<String>,
pub data: String,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum StreamParseError {
#[error("SSE buffer exceeded {0} bytes")]
BufferLimit(usize),
#[error("stream ended with an incomplete SSE event")]
Incomplete,
}
pub struct SseDecoder {
buffer: String,
scanned: usize,
max_buffer_bytes: usize,
}
impl Default for SseDecoder {
fn default() -> Self {
Self::new(1024 * 1024)
}
}
impl SseDecoder {
pub fn new(max_buffer_bytes: usize) -> Self {
Self {
buffer: String::new(),
scanned: 0,
max_buffer_bytes,
}
}
pub fn push(&mut self, chunk: &str) -> Result<Vec<SseEvent>, StreamParseError> {
self.buffer.push_str(chunk);
if self.buffer.len() > self.max_buffer_bytes {
return Err(StreamParseError::BufferLimit(self.max_buffer_bytes));
}
let mut events = Vec::new();
let mut search = self.scanned;
let mut consumed = 0;
while let Some(offset) = event_end(&self.buffer[search..]) {
let end = search + offset;
let block = self.buffer[consumed..end].replace('\r', "");
let delimiter_len = if self.buffer[end..].starts_with("\r\n\r\n") {
4
} else {
2
};
consumed = end + delimiter_len;
search = consumed;
if let Some(event) = parse_event(&block) {
events.push(event);
}
}
self.buffer.drain(..consumed);
let mut scanned = self.buffer.len().saturating_sub(DELIMITER_OVERLAP);
while scanned > 0 && !self.buffer.is_char_boundary(scanned) {
scanned -= 1;
}
self.scanned = scanned;
Ok(events)
}
#[cfg(fuzzing)]
pub fn fuzz_buffered(&self) -> &str {
&self.buffer
}
pub fn finish(self) -> Result<(), StreamParseError> {
if self.buffer.trim().is_empty() {
Ok(())
} else {
Err(StreamParseError::Incomplete)
}
}
}
const DELIMITER_OVERLAP: usize = 3;
fn event_end(buffer: &str) -> Option<usize> {
let bytes = buffer.as_bytes();
for index in 0..bytes.len().saturating_sub(1) {
if bytes[index] == b'\n' && bytes[index + 1] == b'\n' {
return Some(index);
}
if bytes[index] == b'\r' && bytes[index..].starts_with(b"\r\n\r\n") {
return Some(index);
}
}
None
}
fn parse_event(block: &str) -> Option<SseEvent> {
let mut event = None;
let mut data = Vec::new();
for line in block.lines() {
if line.starts_with(':') {
continue;
}
if let Some(value) = line.strip_prefix("event:") {
event = Some(value.trim_start().to_owned());
} else if let Some(value) = line.strip_prefix("data:") {
data.push(value.trim_start());
}
}
(!data.is_empty()).then(|| SseEvent {
event,
data: data.join("\n"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_fragmented_and_multiline_events() {
let mut decoder = SseDecoder::default();
assert!(
decoder
.push("event: delta\r\ndata: {\"a\":")
.unwrap()
.is_empty()
);
let events = decoder.push("1}\r\ndata: tail\r\n\r\n").unwrap();
assert_eq!(events[0].event.as_deref(), Some("delta"));
assert_eq!(events[0].data, "{\"a\":1}\ntail");
decoder.finish().unwrap();
}
#[test]
fn every_chunk_boundary_decodes_a_body_identically() {
for body in [
"data: one\n\ndata: two\n\n",
"event: delta\r\ndata: {\"a\":1}\r\n\r\n: keep-alive\r\n\r\ndata: [DONE]\r\n\r\n",
"data: first\ndata: second\n\ndata: tail",
": comment only\n\n\n\ndata: \n\n",
] {
let mut whole = SseDecoder::default();
let expected = whole.push(body).unwrap();
for cut in 1..body.len() {
if !body.is_char_boundary(cut) {
continue;
}
let mut split = SseDecoder::default();
let mut events = split.push(&body[..cut]).unwrap();
events.extend(split.push(&body[cut..]).unwrap());
assert_eq!(events, expected, "{body:?} split at {cut}");
}
}
}
#[test]
fn a_truncated_final_event_is_refused_by_finish() {
let mut decoder = SseDecoder::default();
assert!(decoder.push("data: complete\n\ndata: trunc").unwrap().len() == 1);
assert_eq!(decoder.finish(), Err(StreamParseError::Incomplete));
}
#[test]
fn a_delimiter_split_byte_by_byte_still_terminates_an_event() {
let body = "event: delta\r\ndata: one\r\n\r\ndata: two\n\n";
let mut decoder = SseDecoder::default();
let mut events = Vec::new();
for byte in 0..body.len() {
events.extend(decoder.push(&body[byte..=byte]).unwrap());
}
assert_eq!(events.len(), 2);
assert_eq!(events[0].event.as_deref(), Some("delta"));
assert_eq!(events[0].data, "one");
assert_eq!(events[1].data, "two");
decoder.finish().unwrap();
}
#[test]
fn a_chunk_of_many_tiny_events_is_parsed_in_one_pass() {
let events = 200_000;
let mut decoder = SseDecoder::new(8 * 1024 * 1024);
let decoded = decoder.push(&"data: x\n\n".repeat(events)).unwrap();
assert_eq!(decoded.len(), events);
decoder.finish().unwrap();
}
#[test]
fn an_unterminated_event_trips_the_buffer_limit() {
let mut decoder = SseDecoder::new(64);
assert_eq!(
decoder.push(&"data: ".repeat(64)),
Err(StreamParseError::BufferLimit(64))
);
}
}