sse.rs
Language Rust5.9 KB189 lines
//! A byte-level Server-Sent Events parser.//!//! Network chunks can split anywhere: in the middle of a line, a `\r\n` pair,//! or a multi-byte UTF-8 character. The parser buffers raw bytes and only//! decodes complete lines, so every way of chunking the same stream yields the//! same events (the property test below checks this)./// One dispatched event: its `event:` name, if any, and its joined `data:` lines.#[derive(Debug, Clone, PartialEq, Eq)]pub struct SseEvent {pub event: Option<String>,pub data: String,}#[derive(Debug, Default)]pub struct SseParser {buf: Vec<u8>,data: Vec<String>,event: Option<String>,/// A `\r` ended the last chunk; a `\n` at the start of the next one belongs to it.pending_cr: bool,}impl SseParser {pub fn new() -> Self {Self::default()}/// Feed bytes; returns every event completed by them.pub fn push(&mut self, chunk: &[u8]) -> Vec<SseEvent> {let mut out = Vec::new();let mut bytes = chunk;if self.pending_cr {self.pending_cr = false;if bytes.first() == Some(&b'\n') {bytes = &bytes[1..];}}self.buf.extend_from_slice(bytes);let mut start = 0;let mut i = 0;while i < self.buf.len() {match self.buf[i] {b'\n' => {let line = self.buf[start..i].to_vec();self.line(&line, &mut out);i += 1;start = i;}b'\r' => {let line = self.buf[start..i].to_vec();self.line(&line, &mut out);i += 1;if i < self.buf.len() {if self.buf[i] == b'\n' {i += 1;}} else {self.pending_cr = true;}start = i;}_ => i += 1,}}self.buf.drain(..start);out}/// End of stream: an event left without its blank line is still delivered.pub fn finish(&mut self) -> Vec<SseEvent> {let mut out = Vec::new();if !self.buf.is_empty() {let line = std::mem::take(&mut self.buf);self.line(&line, &mut out);}self.dispatch(&mut out);out}fn line(&mut self, raw: &[u8], out: &mut Vec<SseEvent>) {if raw.is_empty() {self.dispatch(out);return;}let line = String::from_utf8_lossy(raw);if line.starts_with(':') {return; // comment / keepalive}let (field, value) = match line.find(':') {Some(at) => {let v = &line[at + 1..];(&line[..at], v.strip_prefix(' ').unwrap_or(v))}None => (&line[..], ""),};match field {"data" => self.data.push(value.to_string()),"event" => self.event = Some(value.to_string()),_ => {} // id, retry and unknown fields are not used}}fn dispatch(&mut self, out: &mut Vec<SseEvent>) {if self.data.is_empty() {self.event = None;return;}out.push(SseEvent { event: self.event.take(), data: std::mem::take(&mut self.data).join("\n") });}}#[cfg(test)]mod tests {use super::*;use proptest::prelude::*;fn all(input: &[u8]) -> Vec<SseEvent> {let mut p = SseParser::new();let mut out = p.push(input);out.extend(p.finish());out}fn data(events: &[SseEvent]) -> Vec<&str> {events.iter().map(|e| e.data.as_str()).collect()}#[test]fn basic_lf_and_crlf() {assert_eq!(data(&all(b"data: a\n\ndata: b\n\n")), ["a", "b"]);assert_eq!(data(&all(b"data: a\r\n\r\ndata: b\r\n\r\n")), ["a", "b"]);assert_eq!(data(&all(b"data: a\r\rdata: b\r\r")), ["a", "b"]);}#[test]fn comments_multiline_and_event_names() {let ev = all(b": keepalive\n\nevent: error\ndata: one\ndata: two\n\ndata:x\n\n");assert_eq!(ev.len(), 2);assert_eq!(ev[0].event.as_deref(), Some("error"));assert_eq!(ev[0].data, "one\ntwo");assert_eq!(ev[1], SseEvent { event: None, data: "x".into() });}#[test]fn unterminated_final_event_is_delivered() {assert_eq!(data(&all(b"data: [DONE]")), ["[DONE]"]);}#[test]fn utf8_split_across_chunks() {let text = "data: caf\u{e9} \u{1F600}\n\n".as_bytes();let mut p = SseParser::new();let mut out = Vec::new();for b in text {out.extend(p.push(std::slice::from_ref(b)));}assert_eq!(out[0].data, "caf\u{e9} \u{1F600}");}#[test]fn crlf_split_between_chunks() {let mut p = SseParser::new();let mut out = p.push(b"data: a\r");out.extend(p.push(b"\n\r"));out.extend(p.push(b"\ndata: b\r\n\r\n"));assert_eq!(data(&out), ["a", "b"]);}proptest! {#[test]fn any_chunking_gives_the_same_events(cuts in proptest::collection::vec(0usize..400, 0..30)) {let stream = "data: {\"a\":1}\r\n\r\n: ping\n\nevent: x\ndata: l1\ndata: l2 \u{e9}\u{4e2d}\n\ndata: [DONE]\n\n".as_bytes();let expected = all(stream);let mut points: Vec<usize> = cuts.into_iter().map(|c| c % (stream.len() + 1)).collect();points.sort();let mut p = SseParser::new();let mut got = Vec::new();let mut last = 0;for at in points {got.extend(p.push(&stream[last..at]));last = at;}got.extend(p.push(&stream[last..]));got.extend(p.finish());prop_assert_eq!(got, expected);}}}