use alloc::string::String;
use alloc::vec::Vec;
use alloc_helpers::take_string;
mod alloc_helpers {
use alloc::string::String;
#[inline]
pub fn take_string(s: &mut String) -> String {
core::mem::take(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SseEvent {
pub event: Option<String>,
pub data: String,
pub id: Option<String>,
pub retry: Option<u64>,
}
#[derive(Debug, Default)]
pub struct SseParser {
line_buf: Vec<u8>,
pending_cr: bool,
data: String,
event_type: Option<String>,
last_id: Option<String>,
retry: Option<u64>,
reconnection_time: Option<u64>,
bom: BomState,
}
#[derive(Debug, Default)]
enum BomState {
#[default]
Start,
Partial {
buf: [u8; 3],
len: usize,
},
Done,
}
const BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
impl SseParser {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn last_id(&self) -> Option<&str> {
self.last_id.as_deref()
}
#[must_use]
pub fn reconnection_time(&self) -> Option<u64> {
self.reconnection_time
}
pub fn feed(&mut self, chunk: &[u8], out: &mut Vec<SseEvent>) {
let mut bytes = chunk;
if self.pending_cr {
self.pending_cr = false;
if let [b'\n', rest @ ..] = bytes {
bytes = rest;
}
}
if !matches!(self.bom, BomState::Done) {
let mut recovered: [u8; 3] = [0; 3];
let recovered = self.strip_bom(&mut bytes, &mut recovered);
self.scan(recovered, out);
}
self.scan(bytes, out);
}
fn strip_bom<'a>(&mut self, bytes: &mut &[u8], scratch: &'a mut [u8; 3]) -> &'a [u8] {
let (mut matched, mut matched_len) = match self.bom {
BomState::Partial { buf, len } => (buf, len),
_ => ([0u8; 3], 0),
};
while matched_len < 3 {
let Some((&next, rest)) = bytes.split_first() else {
break;
};
if next != BOM[matched_len] {
break;
}
matched[matched_len] = next;
matched_len += 1;
*bytes = rest;
}
if matched_len == 3 {
self.bom = BomState::Done;
return &scratch[..0];
}
if !bytes.is_empty() {
self.bom = BomState::Done;
scratch[..matched_len].copy_from_slice(&matched[..matched_len]);
return &scratch[..matched_len];
}
if matched_len == 0 {
self.bom = BomState::Start;
} else {
self.bom = BomState::Partial {
buf: matched,
len: matched_len,
};
}
&scratch[..0]
}
fn scan(&mut self, bytes: &[u8], out: &mut Vec<SseEvent>) {
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\n' => {
self.end_line(out);
i += 1;
}
b'\r' => {
self.end_line(out);
if i + 1 < bytes.len() {
if bytes[i + 1] == b'\n' {
i += 2;
} else {
i += 1;
}
} else {
self.pending_cr = true;
i += 1;
}
}
b => {
self.line_buf.push(b);
i += 1;
}
}
}
}
pub fn finish(&mut self, out: &mut Vec<SseEvent>) {
if !self.line_buf.is_empty() {
let line = take_line(&mut self.line_buf);
if line[0] != b':' {
let (name, value) = split_field(&line);
self.process_field(name, value);
}
}
let _ = out;
self.line_buf.clear();
self.pending_cr = false;
self.data.clear();
self.event_type = None;
self.retry = None;
self.last_id = None;
self.reconnection_time = None;
self.bom = BomState::Start;
}
fn end_line(&mut self, out: &mut Vec<SseEvent>) {
let line = take_line(&mut self.line_buf);
if line.is_empty() {
self.dispatch(out);
return;
}
if line[0] == b':' {
return;
}
let (name, value) = split_field(&line);
self.process_field(name, value);
}
fn process_field(&mut self, name: &[u8], value: &[u8]) {
match name {
b"event" => {
self.event_type = Some(decode_utf8_lossy(value));
}
b"data" => {
self.data.push_str(&decode_utf8_lossy(value));
self.data.push('\n');
}
b"id" => {
if !value.contains(&0) {
self.last_id = Some(decode_utf8_lossy(value));
}
}
b"retry" => {
if !value.is_empty() && value.iter().all(u8::is_ascii_digit) {
if let Ok(s) = core::str::from_utf8(value) {
if let Ok(ms) = s.parse::<u64>() {
self.retry = Some(ms);
self.reconnection_time = Some(ms);
}
}
}
}
_ => { }
}
}
fn dispatch(&mut self, out: &mut Vec<SseEvent>) {
if self.data.is_empty() {
self.event_type = None;
self.retry = None;
return;
}
let mut data = take_string(&mut self.data);
if data.ends_with('\n') {
data.pop();
}
let event = SseEvent {
event: self.event_type.take(),
data,
id: self.last_id.clone(),
retry: self.retry.take(),
};
out.push(event);
}
}
#[inline]
fn take_line(buf: &mut Vec<u8>) -> Vec<u8> {
core::mem::take(buf)
}
#[inline]
fn split_field(line: &[u8]) -> (&[u8], &[u8]) {
match line.iter().position(|&b| b == b':') {
Some(colon) => {
let name = &line[..colon];
let mut value = &line[colon + 1..];
if let [b' ', rest @ ..] = value {
value = rest;
}
(name, value)
}
None => (line, &[][..]),
}
}
#[inline]
fn decode_utf8_lossy(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into_owned()
}