use crate::{Error, Result};
pub const MAX_EVENT_BYTES: usize = 8 << 20;
#[derive(Debug, Default)]
pub struct SseDecoder {
buf: Vec<u8>,
start: usize,
scanned: usize,
data: String,
}
impl SseDecoder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, chunk: &[u8]) -> Result<()> {
if self.start > 0 && self.start * 2 >= self.buf.len() {
self.buf.drain(..self.start);
self.scanned -= self.start;
self.start = 0;
}
if self.buf.len() - self.start + chunk.len() > MAX_EVENT_BYTES {
return Err(Error::TooLarge { limit: MAX_EVENT_BYTES as u64 });
}
self.buf.extend_from_slice(chunk);
Ok(())
}
pub fn next_data(&mut self) -> Result<Option<&str>> {
loop {
let Some((end, next)) = self.find_boundary() else { return Ok(None) };
let event = &self.buf[self.start..end];
self.start = next;
self.scanned = next;
self.data.clear();
let mut has_data = false;
for line in event.split(|&b| b == b'\n') {
let line = line.strip_suffix(b"\r").unwrap_or(line);
let Some(rest) = line.strip_prefix(b"data:") else { continue };
let rest = rest.strip_prefix(b" ").unwrap_or(rest);
if has_data {
self.data.push('\n');
}
has_data = true;
let text = std::str::from_utf8(rest)
.map_err(|_| Error::Protocol { message: "sse data is not utf-8".into() })?;
self.data.push_str(text);
}
if has_data {
return Ok(Some(&self.data));
}
}
}
#[must_use]
pub fn has_partial(&self) -> bool {
self.buf[self.start..].iter().any(|b| !b.is_ascii_whitespace())
}
fn find_boundary(&mut self) -> Option<(usize, usize)> {
let b = &self.buf;
let mut i = self.scanned.max(self.start);
while i < b.len() {
if b[i] == b'\n' {
if b.get(i + 1) == Some(&b'\n') {
return Some((i, i + 2));
}
if b.get(i + 1) == Some(&b'\r') && b.get(i + 2) == Some(&b'\n') {
return Some((i, i + 3));
}
if i + 2 >= b.len() {
self.scanned = i;
return None;
}
}
i += 1;
}
self.scanned = b.len();
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn collect(chunks: &[&[u8]]) -> Vec<String> {
let mut d = SseDecoder::new();
let mut out = Vec::new();
for c in chunks {
d.push(c).unwrap();
while let Some(data) = d.next_data().unwrap() {
out.push(data.to_owned());
}
}
out
}
#[test]
fn whole_events() {
assert_eq!(collect(&[b"data: a\n\ndata: b\n\n"]), ["a", "b"]);
}
#[test]
fn events_split_at_every_byte() {
let src =
b"data: {\"x\":\"\xc3\xa9\"}\r\n\r\n: keep-alive\n\nevent: e\ndata: l1\ndata: l2\n\n";
let chunks: Vec<&[u8]> = src.chunks(1).collect();
assert_eq!(collect(&chunks), ["{\"x\":\"\u{e9}\"}", "l1\nl2"]);
}
#[test]
fn no_data_events_are_skipped() {
assert_eq!(collect(&[b": comment\n\nid: 1\n\n"]), Vec::<String>::new());
}
#[test]
fn partial_tail_is_reported() {
let mut d = SseDecoder::new();
d.push(b"data: a\n\ndata: trunc").unwrap();
assert_eq!(d.next_data().unwrap(), Some("a"));
assert_eq!(d.next_data().unwrap(), None);
assert!(d.has_partial());
}
#[test]
fn oversize_events_are_refused() {
let mut d = SseDecoder::new();
let big = vec![b'x'; MAX_EVENT_BYTES];
d.push(&big).unwrap();
assert!(d.push(b"y").is_err());
}
#[test]
fn buffer_compacts_under_steady_streaming() {
let mut d = SseDecoder::new();
for _ in 0..10_000 {
d.push(b"data: 0123456789\n\n").unwrap();
assert_eq!(d.next_data().unwrap(), Some("0123456789"));
}
assert!(d.buf.capacity() < 4096, "consumed events do not accumulate");
}
}