use std::io::Read;
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::time::{Duration, Instant};
pub const MAX_EVENT_BYTES: usize = 8 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SseFrame {
pub event: String,
pub data: String,
pub id: Option<String>,
pub retry: Option<u64>,
}
#[derive(Debug, Default)]
pub struct SseParser {
line: Vec<u8>,
after_cr: bool,
started: bool,
event: Option<String>,
data: String,
has_data: bool,
id: Option<String>,
retry: Option<u64>,
pending: usize,
comments: usize,
}
impl SseParser {
pub fn new() -> Self {
SseParser::default()
}
pub fn comments(&self) -> usize {
self.comments
}
pub fn feed(&mut self, mut bytes: &[u8]) -> Result<Vec<SseFrame>, String> {
let mut events = Vec::new();
if !self.started && !bytes.is_empty() {
if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
bytes = &bytes[3..];
}
self.started = true;
}
for &byte in bytes {
match byte {
b'\n' if self.after_cr => self.after_cr = false,
b'\n' | b'\r' => {
self.after_cr = byte == b'\r';
let line = std::mem::take(&mut self.line);
if let Some(event) = self.line_done(&line) {
events.push(event);
}
self.line = line;
self.line.clear();
}
_ => {
self.after_cr = false;
if self.line.len() + self.pending >= MAX_EVENT_BYTES {
*self = SseParser::default();
return Err(format!("an SSE event over {MAX_EVENT_BYTES} bytes"));
}
self.line.push(byte);
}
}
}
Ok(events)
}
fn line_done(&mut self, line: &[u8]) -> Option<SseFrame> {
if line.is_empty() {
let event = self.event.take();
let id = self.id.take();
let retry = self.retry.take();
let data = std::mem::take(&mut self.data);
let has_data = std::mem::replace(&mut self.has_data, false);
self.pending = 0;
if !has_data {
return None;
}
return Some(SseFrame { event: event.unwrap_or_else(|| "message".to_string()), data, id, retry });
}
if line[0] == b':' {
self.comments += 1;
return None;
}
let (name, value) = match line.iter().position(|&b| b == b':') {
Some(colon) => {
let value = &line[colon + 1..];
(&line[..colon], value.strip_prefix(b" ").unwrap_or(value))
}
None => (line, &b""[..]),
};
self.pending += line.len() + 1;
let value = String::from_utf8_lossy(value).into_owned();
match name {
b"event" => self.event = Some(value),
b"data" => {
if self.has_data {
self.data.push('\n');
}
self.data.push_str(&value);
self.has_data = true;
}
b"id" if !value.contains('\0') => self.id = Some(value),
b"retry" if !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()) => {
self.retry = value.parse().ok();
}
_ => {}
}
None
}
}
enum Chunk {
Bytes(Vec<u8>),
End,
Failed(String),
}
pub struct TestSse {
parser: SseParser,
ready: std::collections::VecDeque<SseFrame>,
chunks: Option<Receiver<Chunk>>,
ended: bool,
error: Option<String>,
}
impl TestSse {
pub fn from_bytes(body: &[u8]) -> TestSse {
let mut sse = TestSse::empty();
sse.take_in(body);
sse.ended = true;
sse
}
pub fn from_reader(mut reader: Box<dyn Read + Send>) -> TestSse {
let (tx, rx) = mpsc::channel();
let spawned = std::thread::Builder::new().name("rws-test-sse".to_string()).spawn(move || {
let mut buf = vec![0u8; 8192];
loop {
let chunk = match reader.read(&mut buf) {
Ok(0) => Chunk::End,
Ok(n) => Chunk::Bytes(buf[..n].to_vec()),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => Chunk::Failed(e.to_string()),
};
let last = !matches!(chunk, Chunk::Bytes(_));
if tx.send(chunk).is_err() || last {
return;
}
}
});
let mut sse = TestSse::empty();
match spawned {
Ok(_) => sse.chunks = Some(rx),
Err(e) => {
sse.ended = true;
sse.error = Some(format!("couldn't start the SSE reader thread: {e}"));
}
}
sse
}
fn empty() -> TestSse {
TestSse { parser: SseParser::new(), ready: Default::default(), chunks: None, ended: false, error: None }
}
fn take_in(&mut self, bytes: &[u8]) {
match self.parser.feed(bytes) {
Ok(events) => self.ready.extend(events),
Err(e) => {
self.error = Some(e);
self.ended = true;
self.chunks = None;
}
}
}
pub fn next_event(&mut self, timeout: Duration) -> Option<SseFrame> {
let deadline = Instant::now() + timeout;
loop {
if let Some(event) = self.ready.pop_front() {
return Some(event);
}
if self.ended {
return None;
}
let rx = self.chunks.as_ref()?;
let left = deadline.saturating_duration_since(Instant::now());
match rx.recv_timeout(left) {
Ok(Chunk::Bytes(bytes)) => self.take_in(&bytes),
Ok(Chunk::End) | Err(RecvTimeoutError::Disconnected) => self.finish(None),
Ok(Chunk::Failed(e)) => self.finish(Some(e)),
Err(RecvTimeoutError::Timeout) => return None,
}
}
}
fn finish(&mut self, error: Option<String>) {
self.ended = true;
self.chunks = None;
if error.is_some() {
self.error = error;
}
}
pub fn events(&mut self, n: usize, timeout: Duration) -> Vec<SseFrame> {
let deadline = Instant::now() + timeout;
let mut events = Vec::new();
while events.len() < n {
match self.next_event(deadline.saturating_duration_since(Instant::now())) {
Some(event) => events.push(event),
None => break,
}
}
events
}
pub fn collect_until_end(&mut self, timeout: Duration) -> Vec<SseFrame> {
self.events(usize::MAX, timeout)
}
pub fn is_ended(&self) -> bool {
self.ended && self.ready.is_empty()
}
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
pub fn comments(&self) -> usize {
self.parser.comments()
}
}