use anyhow::{ensure, Result};
pub const MAX_LINE_BYTES: usize = 64 * 1024;
pub const MAX_EVENT_BYTES: usize = 1024 * 1024;
fn single_line(field: &str, value: &str) -> Result<()> {
ensure!(
!value.contains(['\r', '\n']),
"{field} is one line: it may not contain CR or LF"
);
Ok(())
}
pub fn encode_event(
event: Option<&str>,
id: Option<&str>,
data: &str,
retry_ms: Option<u64>,
) -> Result<Vec<u8>> {
ensure!(
data.len() <= MAX_EVENT_BYTES,
"data is at most {MAX_EVENT_BYTES} bytes"
);
let mut out = String::with_capacity(data.len() + 64);
if let Some(event) = event.filter(|e| !e.is_empty()) {
single_line("event", event)?;
out.push_str("event: ");
out.push_str(event);
out.push('\n');
}
if let Some(id) = id {
single_line("id", id)?;
ensure!(
!id.contains('\0'),
"id may not contain NUL (the client would ignore it)"
);
out.push_str("id: ");
out.push_str(id);
out.push('\n');
}
if let Some(retry) = retry_ms {
out.push_str(&format!("retry: {retry}\n"));
}
for line in data.split("\r\n").flat_map(|l| l.split(['\r', '\n'])) {
out.push_str("data: ");
out.push_str(line);
out.push('\n');
}
out.push('\n');
Ok(out.into_bytes())
}
pub fn encode_comment(text: &str) -> Result<Vec<u8>> {
single_line("comment", text)?;
Ok(format!(": {text}\n\n").into_bytes())
}
pub fn encode_retry(retry_ms: u64) -> Vec<u8> {
format!("retry: {retry_ms}\n\n").into_bytes()
}
#[derive(Debug, Clone, PartialEq)]
pub struct Event {
pub event: String,
pub data: String,
pub last_event_id: String,
}
#[derive(Default)]
pub struct Parser {
pending: Vec<u8>,
started: bool,
skip_lf: bool,
event: String,
data: String,
has_data: bool,
pub last_event_id: String,
pub retry_ms: Option<u64>,
}
impl Parser {
pub fn with_last_event_id(id: &str) -> Self {
Self {
last_event_id: id.to_string(),
..Self::default()
}
}
pub fn reset_stream(&mut self) {
self.pending.clear();
self.started = false;
self.skip_lf = false;
self.event.clear();
self.data.clear();
self.has_data = false;
}
pub fn feed(&mut self, bytes: &[u8]) -> Result<Vec<Event>> {
let mut out = Vec::new();
for &b in bytes {
if self.skip_lf {
self.skip_lf = false;
if b == b'\n' {
continue;
}
}
match b {
b'\r' | b'\n' => {
self.skip_lf = b == b'\r';
let line = std::mem::take(&mut self.pending);
if let Some(e) = self.line(&line)? {
out.push(e);
}
}
_ => {
ensure!(
self.pending.len() < MAX_LINE_BYTES,
"an event-stream line longer than {MAX_LINE_BYTES} bytes"
);
self.pending.push(b);
}
}
}
Ok(out)
}
fn line(&mut self, raw: &[u8]) -> Result<Option<Event>> {
let mut line = String::from_utf8_lossy(raw).into_owned();
if !self.started {
self.started = true;
if let Some(rest) = line.strip_prefix('\u{feff}') {
line = rest.to_string();
}
}
if line.is_empty() {
return Ok(self.dispatch());
}
if line.starts_with(':') {
return Ok(None);
}
let (field, value) = match line.split_once(':') {
Some((f, v)) => (f.to_string(), v.strip_prefix(' ').unwrap_or(v).to_string()),
None => (line.clone(), String::new()),
};
match field.as_str() {
"event" => self.event = value,
"data" => {
ensure!(
self.data.len() + value.len() < MAX_EVENT_BYTES,
"an event's data over {MAX_EVENT_BYTES} bytes"
);
self.data.push_str(&value);
self.data.push('\n');
self.has_data = true;
}
"id" if !value.contains('\0') => self.last_event_id = value,
"retry" if !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()) => {
self.retry_ms = value.parse().ok();
}
_ => {}
}
Ok(None)
}
fn dispatch(&mut self) -> Option<Event> {
let event = std::mem::take(&mut self.event);
if !self.has_data {
return None;
}
self.has_data = false;
let mut data = std::mem::take(&mut self.data);
data.pop();
Some(Event {
event: if event.is_empty() {
"message".into()
} else {
event
},
data,
last_event_id: self.last_event_id.clone(),
})
}
}