1use chrono::{DateTime, Utc};
39use serde::{Deserialize, Serialize};
40
41use crate::crypto::{self, SigningKey, VerifyingKey};
42use crate::govlog::{SignatureHex, canonical_json};
43use crate::ids::{AgentId, MessageId};
44use crate::names::AgentName;
45
46pub const EVENT_CONTEXT: &[u8] = b"agora/event/v1\n";
48
49pub const EVENTS_PATH: &str = "events";
51
52pub const EVENTS_KEY_PATH: &str = "events/key";
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
58#[cfg_attr(feature = "schemars", schemars(inline))]
59#[serde(tag = "kind", rename_all = "snake_case")]
60pub enum Event {
61 DirectMessage(MessageNotice),
63 Broadcast(MessageNotice),
66 Gap(GapNotice),
70}
71
72impl Event {
73 pub fn name(&self) -> &'static str {
75 match self {
76 Event::DirectMessage(_) => "direct_message",
77 Event::Broadcast(_) => "broadcast",
78 Event::Gap(_) => "gap",
79 }
80 }
81
82 pub fn recipient_id(&self) -> AgentId {
84 match self {
85 Event::DirectMessage(m) | Event::Broadcast(m) => m.recipient_id,
86 Event::Gap(g) => g.recipient_id,
87 }
88 }
89
90 pub fn message(&self) -> Option<&MessageNotice> {
92 match self {
93 Event::DirectMessage(m) | Event::Broadcast(m) => Some(m),
94 Event::Gap(_) => None,
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
103#[cfg_attr(feature = "schemars", schemars(inline))]
104pub struct GapNotice {
105 pub recipient_id: AgentId,
107 pub skipped: u64,
110 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
112 pub at_least: bool,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
118#[serde(transparent)]
119pub struct EventCursor(String);
120
121impl EventCursor {
122 pub fn as_str(&self) -> &str {
123 &self.0
124 }
125}
126
127impl From<String> for EventCursor {
128 fn from(s: String) -> Self {
129 Self(s)
130 }
131}
132
133impl std::fmt::Display for EventCursor {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 f.write_str(&self.0)
136 }
137}
138
139pub const REPLAY_CEILING: usize = 500;
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
146#[cfg_attr(feature = "schemars", schemars(inline))]
147pub struct MessageNotice {
148 pub message_id: MessageId,
149 pub recipient_id: AgentId,
153 pub sender_id: AgentId,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub sender_name: Option<AgentName>,
157 pub sent_at: DateTime<Utc>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
164pub struct SignedEvent {
165 pub event: Event,
166 pub timestamp: i64,
169 pub signature: SignatureHex,
172}
173
174impl SignedEvent {
175 pub fn signing_bytes(event: &Event) -> Vec<u8> {
178 let value =
179 serde_json::to_value(event).expect("an Event always serializes");
180 let mut bytes = EVENT_CONTEXT.to_vec();
181 bytes.extend_from_slice(&canonical_json(&value));
182 bytes
183 }
184
185 pub fn sign(key: &SigningKey, event: Event, timestamp: i64) -> Self {
187 let signature =
188 crypto::sign(key, &Self::signing_bytes(&event), timestamp);
189 Self {
190 event,
191 timestamp,
192 signature: signature.into(),
193 }
194 }
195
196 pub fn verify(&self, key: &VerifyingKey) -> bool {
198 crypto::verify(
199 key,
200 &Self::signing_bytes(&self.event),
201 self.timestamp,
202 &(&self.signature).into(),
203 )
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
214pub struct EventSigningKey {
215 pub public_key: crate::govlog::PublicKeyHex,
217 pub context: String,
219}
220
221impl EventSigningKey {
222 pub fn new(key: &VerifyingKey) -> Self {
223 Self {
224 public_key: key.into(),
225 context: String::from_utf8_lossy(EVENT_CONTEXT).into_owned(),
226 }
227 }
228
229 pub fn verifying_key(&self) -> Option<VerifyingKey> {
231 VerifyingKey::from_bytes(self.public_key.as_bytes()).ok()
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
238#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
239#[cfg_attr(feature = "schemars", schemars(inline))]
240pub struct NotificationPreferences {
241 pub direct_messages: bool,
243 pub broadcasts: bool,
245}
246
247impl Default for NotificationPreferences {
248 fn default() -> Self {
249 Self {
250 direct_messages: true,
251 broadcasts: true,
252 }
253 }
254}
255
256impl NotificationPreferences {
257 pub fn wants(&self, event: &Event) -> bool {
259 match event {
260 Event::DirectMessage(_) => self.direct_messages,
261 Event::Broadcast(_) => self.broadcasts,
262 Event::Gap(_) => true,
263 }
264 }
265
266 pub fn with(mut self, change: &NotificationSettings) -> Self {
269 if let Some(on) = change.direct_messages {
270 self.direct_messages = on;
271 }
272 if let Some(on) = change.broadcasts {
273 self.broadcasts = on;
274 }
275 self
276 }
277}
278
279#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
281#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
282#[cfg_attr(feature = "schemars", schemars(inline))]
283#[serde(deny_unknown_fields)]
284pub struct NotificationSettings {
285 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub direct_messages: Option<bool>,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub broadcasts: Option<bool>,
291}
292
293impl NotificationSettings {
294 pub fn is_empty(&self) -> bool {
296 self.direct_messages.is_none() && self.broadcasts.is_none()
297 }
298}
299
300pub const MAX_LINE_BYTES: usize = 64 * 1024;
304
305pub const MAX_FRAME_BYTES: usize = 256 * 1024;
308
309#[derive(Debug, Clone, Default, PartialEq, Eq)]
311pub struct SseFrame {
312 pub id: Option<String>,
315 pub event: Option<String>,
318 pub data: String,
320 pub retry: Option<u64>,
322}
323
324type Feed = futures::channel::mpsc::UnboundedReceiver<
325 Result<String, std::convert::Infallible>,
326>;
327
328pub struct SseParser {
342 framer: Framer,
343 feed: futures::channel::mpsc::UnboundedSender<
344 Result<String, std::convert::Infallible>,
345 >,
346 events: eventsource_stream::EventStream<Feed>,
347 #[cfg(test)]
349 longest_fed: usize,
350}
351
352impl std::fmt::Debug for SseParser {
353 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354 f.debug_struct("SseParser")
355 .field("framer", &self.framer)
356 .finish_non_exhaustive()
357 }
358}
359
360impl Default for SseParser {
361 fn default() -> Self {
362 Self::new()
363 }
364}
365
366impl SseParser {
367 pub fn new() -> Self {
368 let (feed, rx) = futures::channel::mpsc::unbounded();
369 let mut parser = Self {
370 framer: Framer::default(),
371 feed,
372 events: eventsource_stream::EventStream::new(rx),
373 #[cfg(test)]
374 longest_fed: 0,
375 };
376 parser.send(":\n".to_owned());
381 parser.drain(&mut Vec::new());
382 parser
383 }
384
385 fn send(&mut self, line: String) {
386 #[cfg(test)]
387 {
388 self.longest_fed = self.longest_fed.max(line.len());
389 }
390 if self.feed.unbounded_send(Ok(line)).is_err() {
391 unreachable!("the parser's own feed closed");
393 }
394 }
395
396 pub fn push(&mut self, bytes: &[u8]) -> Vec<SseFrame> {
398 let mut frames = Vec::new();
399 for text in self.framer.push(bytes) {
400 let retry = retry_of(&text);
401 let before = frames.len();
402 for line in text.split_inclusive('\n') {
406 self.send(line.to_owned());
407 self.drain(&mut frames);
408 }
409 if frames.len() == before
410 && let Some(ms) = retry
411 {
412 frames.push(SseFrame {
413 retry: Some(ms),
414 ..Default::default()
415 });
416 }
417 }
418 frames
419 }
420
421 fn drain(&mut self, frames: &mut Vec<SseFrame>) {
424 use futures::Stream;
425 use std::task::{Context, Poll};
426 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
427 loop {
428 match std::pin::Pin::new(&mut self.events).poll_next(&mut cx) {
429 Poll::Ready(Some(Ok(event))) => frames.push(SseFrame {
430 id: (!event.id.is_empty()).then_some(event.id),
431 event: Some(event.event),
432 data: event.data,
433 retry: event.retry.map(|d| d.as_millis() as u64),
434 }),
435 Poll::Ready(Some(Err(_))) => {
439 let framer = std::mem::take(&mut self.framer);
440 *self = Self::new();
441 self.framer = framer;
442 return;
443 }
444 Poll::Ready(None) | Poll::Pending => return,
445 }
446 }
447 }
448
449 pub fn take_dropped(&mut self) -> Vec<DroppedFrame> {
451 std::mem::take(&mut self.framer.dropped)
452 }
453
454 #[cfg(test)]
456 fn buffered(&self) -> usize {
457 self.framer.line.capacity() + self.framer.frame.capacity()
458 }
459}
460
461fn retry_of(text: &str) -> Option<u64> {
463 text.lines().rev().find_map(|line| {
464 let value = line.strip_prefix("retry:")?;
465 value.strip_prefix(' ').unwrap_or(value).parse().ok()
466 })
467}
468
469const BOM: &[u8] = "\u{feff}".as_bytes();
470
471#[derive(Debug, Clone, Copy, PartialEq, Eq)]
473pub enum SseCap {
474 Line,
476 Frame,
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub struct DroppedFrame {
483 pub cap: SseCap,
484 pub bytes: usize,
487}
488
489#[derive(Debug, Default)]
492struct Framer {
493 line: Vec<u8>,
494 line_seen: bool,
496 frame: Vec<u8>,
497 frame_bytes: usize,
499 spoiled: Option<SseCap>,
502 dropped: Vec<DroppedFrame>,
503 after_cr: bool,
505 bom: usize,
508 started: bool,
509}
510
511impl Framer {
512 fn push(&mut self, bytes: &[u8]) -> Vec<String> {
513 let mut out = Vec::new();
514 for &b in bytes {
515 if !self.started {
516 if b == BOM[self.bom] {
517 self.bom += 1;
518 self.started = self.bom == BOM.len();
519 continue;
520 }
521 self.started = true;
522 for &p in &BOM[..self.bom] {
523 self.byte(p, &mut out);
524 }
525 }
526 self.byte(b, &mut out);
527 }
528 out
529 }
530
531 fn byte(&mut self, b: u8, out: &mut Vec<String>) {
532 if std::mem::take(&mut self.after_cr) && b == b'\n' {
533 return;
534 }
535 match b {
536 b'\n' | b'\r' => {
537 self.after_cr = b == b'\r';
538 self.end_line(out);
539 }
540 _ => {
541 self.line_seen = true;
542 self.frame_bytes = self.frame_bytes.saturating_add(1);
543 if self.spoiled.is_some() {
544 } else if self.line.len() >= MAX_LINE_BYTES {
545 self.spoil(SseCap::Line);
546 } else {
547 self.line.push(b);
548 }
549 }
550 }
551 }
552
553 fn spoil(&mut self, cap: SseCap) {
555 self.spoiled = Some(cap);
556 self.line = Vec::new();
557 self.frame = Vec::new();
558 }
559
560 fn end_line(&mut self, out: &mut Vec<String>) {
561 if !std::mem::take(&mut self.line_seen) {
562 let mut frame = std::mem::take(&mut self.frame);
563 let bytes = std::mem::take(&mut self.frame_bytes);
564 match self.spoiled.take() {
565 Some(cap) if self.dropped.len() < 64 => {
567 self.dropped.push(DroppedFrame { cap, bytes })
568 }
569 Some(_) => {}
570 None if !frame.is_empty() => {
571 frame.push(b'\n');
572 out.push(String::from_utf8_lossy(&frame).into_owned());
573 }
574 None => {}
575 }
576 return;
577 }
578 self.frame_bytes = self.frame_bytes.saturating_add(1);
579 let line = std::mem::take(&mut self.line);
580 if self.spoiled.is_some() {
581 return;
582 }
583 if self.frame.len() + line.len() + 1 > MAX_FRAME_BYTES {
584 self.spoil(SseCap::Frame);
585 return;
586 }
587 self.frame.extend_from_slice(&line);
588 self.frame.push(b'\n');
589 self.line = line;
590 self.line.clear();
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 use crate::ids::EventSeq;
599
600 fn notice(seq: i64) -> MessageNotice {
601 MessageNotice {
602 recipient_id: AgentId::from(uuid::Uuid::from_u128(9)),
603 message_id: MessageId::from(uuid::Uuid::from_u128(seq as u128)),
604 sender_id: AgentId::from(uuid::Uuid::from_u128(7)),
605 sender_name: Some("alice".parse().unwrap()),
606 sent_at: DateTime::from_timestamp(1_760_000_000, 0).unwrap(),
607 }
608 }
609
610 #[test]
611 fn an_event_says_who_and_which_message_and_nothing_else() {
612 let json =
613 serde_json::to_value(Event::DirectMessage(notice(42))).unwrap();
614 assert_eq!(
615 json,
616 serde_json::json!({
617 "kind": "direct_message",
618 "message_id": "00000000-0000-0000-0000-00000000002a",
619 "recipient_id": "00000000-0000-0000-0000-000000000009",
620 "sender_id": "00000000-0000-0000-0000-000000000007",
621 "sender_name": "alice",
622 "sent_at": "2025-10-09T08:53:20Z",
623 })
624 );
625 let back: Event = serde_json::from_value(json).unwrap();
626 assert_eq!(back, Event::DirectMessage(notice(42)));
627 assert_eq!(back.name(), "direct_message");
628 assert_eq!(
629 back.recipient_id(),
630 AgentId::from(uuid::Uuid::from_u128(9))
631 );
632 assert_eq!(Event::Broadcast(notice(1)).name(), "broadcast");
633 let gap = Event::Gap(GapNotice {
634 recipient_id: AgentId::from(uuid::Uuid::from_u128(9)),
635 skipped: 12,
636 at_least: false,
637 });
638 assert_eq!(
639 serde_json::to_value(&gap).unwrap(),
640 serde_json::json!({
641 "kind": "gap",
642 "recipient_id": "00000000-0000-0000-0000-000000000009",
643 "skipped": 12
644 })
645 );
646 assert!(gap.message().is_none());
647 assert!(
648 NotificationPreferences {
649 direct_messages: false,
650 broadcasts: false
651 }
652 .wants(&gap)
653 );
654 }
655
656 #[test]
657 fn a_signed_event_verifies_and_a_changed_one_does_not() {
658 let (key, public) = crypto::generate_keypair();
659 let signed = SignedEvent::sign(
660 &key,
661 Event::DirectMessage(notice(3)),
662 1_760_000_000,
663 );
664 assert!(signed.verify(&public));
665
666 let wire = serde_json::to_string(&signed).unwrap();
667 let read: SignedEvent = serde_json::from_str(&wire).unwrap();
668 assert!(read.verify(&public));
669
670 let mut moved = read.clone();
671 moved.event = Event::DirectMessage(notice(4));
672 assert!(!moved.verify(&public));
673 let mut forwarded = read.clone();
674 if let Event::DirectMessage(m) = &mut forwarded.event {
675 m.recipient_id = AgentId::from(uuid::Uuid::from_u128(10));
676 }
677 assert!(!forwarded.verify(&public), "an event for another agent");
678 let mut kind = read.clone();
679 kind.event = Event::Broadcast(notice(3));
680 assert!(!kind.verify(&public));
681 let mut late = read;
682 late.timestamp += 1;
683 assert!(!late.verify(&public));
684
685 let (_, other) = crypto::generate_keypair();
686 assert!(!signed.verify(&other));
687 }
688
689 #[test]
692 fn signing_bytes_are_the_context_then_sorted_keys() {
693 let bytes = SignedEvent::signing_bytes(&Event::Broadcast(notice(5)));
694 let text = String::from_utf8(bytes).unwrap();
695 assert_eq!(
696 text,
697 "agora/event/v1\n{\"kind\":\"broadcast\",\
698 \"message_id\":\"00000000-0000-0000-0000-000000000005\",\
699 \"recipient_id\":\"00000000-0000-0000-0000-000000000009\",\
700 \"sender_id\":\"00000000-0000-0000-0000-000000000007\",\
701 \"sender_name\":\"alice\",\"sent_at\":\"2025-10-09T08:53:20Z\"}"
702 );
703 }
704
705 #[test]
706 fn preferences_filter_and_change_field_by_field() {
707 let all = NotificationPreferences::default();
708 assert!(all.wants(&Event::DirectMessage(notice(1))));
709 assert!(all.wants(&Event::Broadcast(notice(1))));
710
711 let quiet = all.with(&NotificationSettings {
712 direct_messages: Some(false),
713 broadcasts: None,
714 });
715 assert!(!quiet.wants(&Event::DirectMessage(notice(1))));
716 assert!(quiet.wants(&Event::Broadcast(notice(1))));
717 assert_eq!(quiet.with(&NotificationSettings::default()), quiet);
718 assert!(NotificationSettings::default().is_empty());
719 }
720
721 #[test]
722 fn event_seq_parses_what_it_prints_and_refuses_the_rest() {
723 assert_eq!("17".parse::<EventSeq>().unwrap(), EventSeq::from(17));
724 assert_eq!(" 0 ".parse::<EventSeq>().unwrap(), EventSeq::ZERO);
725 assert!("-1".parse::<EventSeq>().is_err());
726 assert!("abc".parse::<EventSeq>().is_err());
727 assert_eq!(EventSeq::from(9).to_string(), "9");
728 assert_eq!(serde_json::to_string(&EventSeq::from(9)).unwrap(), "9");
729 }
730
731 #[cfg(feature = "schemars")]
732 #[test]
733 fn schemas_carry_no_ref() {
734 for (name, schema) in [
735 (
736 "SignedEvent",
737 serde_json::to_value(schemars::schema_for!(SignedEvent))
738 .unwrap(),
739 ),
740 (
741 "NotificationSettings",
742 serde_json::to_value(schemars::schema_for!(
743 NotificationSettings
744 ))
745 .unwrap(),
746 ),
747 ] {
748 let text = schema.to_string();
749 assert!(!text.contains("$ref"), "{name}: {text}");
750 assert!(!text.contains("$defs"), "{name}: {text}");
751 }
752 }
753
754 #[test]
755 fn the_parser_reads_frames_across_chunks_and_skips_keep_alives() {
756 let mut p = SseParser::new();
757 assert!(p.push(b": keep-alive\n\nid: 1\nevent: direct").is_empty());
758 let frames = p.push(b"_message\ndata: {\"a\":\ndata: 1}\n\n");
759 assert_eq!(
760 frames,
761 vec![SseFrame {
762 id: Some("1".into()),
763 event: Some("direct_message".into()),
764 data: "{\"a\":\n1}".into(),
765 retry: None,
766 }]
767 );
768 let frames = p.push(b"retry: 3000\r");
771 assert!(frames.is_empty());
772 let frames = p.push(b"\n\r\ndata:x\r\n\r\n");
773 assert_eq!(
774 frames,
775 vec![
776 SseFrame {
777 retry: Some(3000),
778 ..Default::default()
779 },
780 SseFrame {
781 id: Some("1".into()),
782 event: Some("message".into()),
783 data: "x".into(),
784 retry: None,
785 },
786 ]
787 );
788 assert!(p.push(b"foo: bar\n\n").is_empty());
790 assert!(p.take_dropped().is_empty());
791 let line = format!("data: {}\n", "x".repeat(MAX_LINE_BYTES - 16));
794 let mut big = line.repeat(MAX_FRAME_BYTES / line.len() + 2);
795 big.push_str("\ndata: after\n\n");
796 let frames = p.push(big.as_bytes());
797 assert_eq!(frames.len(), 1);
798 assert_eq!(frames[0].data, "after");
799 let dropped = p.take_dropped();
800 assert_eq!(dropped.len(), 1);
801 assert_eq!(dropped[0].cap, SseCap::Frame);
802 assert_eq!(dropped[0].bytes, big.len() - "\ndata: after\n\n".len());
803 }
804
805 #[test]
808 fn an_overlong_line_drops_its_frame_whole_across_chunks() {
809 let mut p = SseParser::new();
810 assert_eq!(p.push(b"id: before\ndata: a\n\n").len(), 1);
811 let long = "y".repeat(MAX_LINE_BYTES + 1);
812 let wire =
813 format!("id: cut\ndata: {long}\ndata: tail\n\ndata: next\n\n");
814 let mut frames = Vec::new();
815 for piece in wire.as_bytes().chunks(4093) {
816 frames.extend(p.push(piece));
817 assert!(p.buffered() <= 2 * (MAX_LINE_BYTES + MAX_FRAME_BYTES));
818 }
819 assert_eq!(
820 frames,
821 vec![SseFrame {
822 id: Some("before".into()),
823 event: Some("message".into()),
824 data: "next".into(),
825 retry: None,
826 }],
827 "the cut frame's id never becomes the stream's"
828 );
829 assert_eq!(
830 p.take_dropped(),
831 vec![DroppedFrame {
832 cap: SseCap::Line,
833 bytes: "id: cut\n".len()
834 + "data: ".len()
835 + long.len()
836 + 1
837 + "data: tail\n".len(),
838 }]
839 );
840 let at = "z".repeat(MAX_LINE_BYTES - "data: ".len());
842 let frames = p.push(format!("data: {at}\n\n").as_bytes());
843 assert_eq!(frames.len(), 1);
844 assert_eq!(frames[0].data, at);
845 }
846
847 #[test]
848 fn a_bom_comments_and_an_empty_id_are_read_as_the_format_says() {
849 let mut p = SseParser::new();
850 let mut frames = Vec::new();
851 for piece in [&b"\xEF"[..], b"\xBB", b"\xBFid: 1\ndata: a\n\n"] {
853 frames.extend(p.push(piece));
854 }
855 frames.extend(p.push(b":c\n: keep-alive\n\nid\ndata: b\n\n"));
856 assert_eq!(frames.len(), 2);
857 assert_eq!(frames[0].id.as_deref(), Some("1"));
858 assert_eq!(frames[1].id, None, "an empty id resets it");
859 assert!(p.push("\u{feff}data: c\n\n".as_bytes()).is_empty());
861 let frames = p.push(b"data: \xFF\xFE ok\n\n");
863 assert_eq!(frames[0].data, "\u{fffd}\u{fffd} ok");
864 }
865
866 #[test]
869 fn a_late_bom_and_odd_ids_read_as_the_format_says() {
870 let mut p = SseParser::new();
871 assert!(p.push("\n\n\u{feff}data: x\n\n".as_bytes()).is_empty());
872 let frames = p.push(b"id: 1\ndata: a\n\nid: n\0ul\ndata: b\n\n");
873 assert_eq!(frames[1].id.as_deref(), Some("1"), "a NUL id is ignored");
874 let frames = p.push(b"id:\ndata: c\n\n");
875 assert_eq!(frames[0].id, None);
876 }
877
878 #[test]
882 fn a_frame_of_many_short_lines_is_cheap() {
883 for line in [":\n", "data\n", "x\n"] {
884 let mut wire = line.repeat((MAX_FRAME_BYTES - 64) / line.len());
885 wire.push_str("data: end\n\n");
886 let mut p = SseParser::new();
887 let started = std::time::Instant::now();
888 let frames = p.push(wire.as_bytes());
889 let took = started.elapsed();
890 assert_eq!(frames.len(), 1, "{line:?}");
891 assert!(
892 p.longest_fed <= MAX_LINE_BYTES + 1,
893 "{line:?}: fed {} bytes at once",
894 p.longest_fed
895 );
896 assert!(
898 took < std::time::Duration::from_secs(5),
899 "{line:?}: {took:?}"
900 );
901 }
902 }
903
904 mod props {
905 use super::*;
906 use proptest::prelude::*;
907
908 #[derive(Debug, Clone)]
910 struct Spec {
911 comment_before: Option<String>,
912 id: Option<String>,
913 event: Option<String>,
914 data: Vec<String>,
915 retry: Option<u64>,
916 oversize: bool,
918 }
919
920 fn spec() -> impl Strategy<Value = Spec> {
921 (
922 proptest::option::of("[^\r\n]{0,20}"),
923 proptest::option::of("[A-Za-z0-9_-]{1,12}"),
924 proptest::option::of("[a-z_]{1,12}"),
925 proptest::collection::vec("[^\r\n]{0,40}", 0..4),
926 proptest::option::of(any::<u64>()),
927 proptest::bool::weighted(0.08),
928 )
929 .prop_map(
930 |(comment_before, id, event, data, retry, oversize)| Spec {
931 comment_before,
932 id,
933 event,
934 data,
935 retry,
936 oversize,
937 },
938 )
939 }
940
941 fn lines(spec: &Spec) -> Vec<String> {
942 let mut lines = Vec::new();
943 if let Some(c) = &spec.comment_before {
944 lines.push(format!(":{c}"));
945 }
946 if let Some(id) = &spec.id {
947 lines.push(format!("id: {id}"));
948 }
949 if let Some(e) = &spec.event {
950 lines.push(format!("event: {e}"));
951 }
952 for d in &spec.data {
953 lines.push(format!("data: {d}"));
954 }
955 if spec.oversize {
956 lines.push(format!("data: {}", "o".repeat(MAX_LINE_BYTES)));
957 }
958 if let Some(r) = spec.retry {
959 lines.push(format!("retry: {r}"));
960 }
961 lines
962 }
963
964 fn model(specs: &[Spec]) -> Vec<SseFrame> {
966 let mut id = None;
967 let mut out = Vec::new();
968 for spec in specs.iter().filter(|s| !s.oversize) {
969 if spec.id.is_some() {
970 id = spec.id.clone();
971 }
972 if !spec.data.is_empty() {
973 out.push(SseFrame {
974 id: id.clone(),
975 event: Some(
976 spec.event.clone().unwrap_or("message".into()),
977 ),
978 data: spec.data.join("\n"),
979 retry: spec.retry,
980 });
981 } else if spec.retry.is_some() {
982 out.push(SseFrame {
983 retry: spec.retry,
984 ..Default::default()
985 });
986 }
987 }
988 out
989 }
990
991 fn wire(specs: &[Spec], endings: &[u8], bom: bool) -> Vec<u8> {
995 let mut out = Vec::new();
996 if bom {
997 out.extend_from_slice(BOM);
998 }
999 let mut endings = endings.iter().cycle();
1000 let mut after_cr = false;
1001 let mut end = |out: &mut Vec<u8>, blank: bool| {
1002 let mut e = *endings.next().unwrap_or(&0);
1003 if blank && after_cr && e == 0 {
1004 e = 1;
1005 }
1006 after_cr = e == 1;
1007 out.extend_from_slice(match e {
1008 0 => b"\n",
1009 1 => b"\r",
1010 _ => b"\r\n",
1011 });
1012 };
1013 for spec in specs {
1014 for line in lines(spec) {
1015 out.extend_from_slice(line.as_bytes());
1016 end(&mut out, false);
1017 }
1018 end(&mut out, true);
1019 }
1020 out
1021 }
1022
1023 fn read(wire: &[u8], cuts: &[usize]) -> (Vec<SseFrame>, usize) {
1024 let mut cuts: Vec<usize> =
1025 cuts.iter().map(|c| c % (wire.len() + 1)).collect();
1026 cuts.sort();
1027 let mut p = SseParser::new();
1028 let mut frames = Vec::new();
1029 let mut at = 0;
1030 let mut held = 0;
1031 for cut in cuts.into_iter().chain([wire.len()]) {
1032 frames.extend(p.push(&wire[at..cut]));
1033 held = held.max(p.buffered());
1034 at = cut;
1035 }
1036 (frames, held)
1037 }
1038
1039 proptest! {
1040 #![proptest_config(ProptestConfig::with_cases(512))]
1041
1042 #[test]
1045 fn chunk_boundaries_never_change_the_frames(
1046 specs in proptest::collection::vec(spec(), 0..12),
1047 endings in proptest::collection::vec(0u8..3, 1..16),
1048 bom in any::<bool>(),
1049 cuts in proptest::collection::vec(any::<usize>(), 0..24),
1050 ) {
1051 let wire = wire(&specs, &endings, bom);
1052 let expected = model(&specs);
1053 prop_assert_eq!(&read(&wire, &[]).0, &expected);
1054 prop_assert_eq!(&read(&wire, &cuts).0, &expected);
1055 let every: Vec<usize> = (0..wire.len()).collect();
1057 prop_assert_eq!(&read(&wire, &every).0, &expected);
1058 }
1059
1060 #[test]
1063 fn garbage_never_panics_or_grows_past_the_caps(
1064 runs in proptest::collection::vec(
1065 (
1066 prop_oneof![
1067 any::<u8>(),
1068 Just(b'\n'),
1069 Just(b'\r'),
1070 Just(b':'),
1071 Just(b'x'),
1072 ],
1073 prop_oneof![1usize..8, 1usize..400_000],
1074 ),
1075 0..10,
1076 ),
1077 noise in proptest::collection::vec(any::<u8>(), 0..512),
1078 prefixes in proptest::collection::vec(
1079 prop_oneof![
1080 Just(&b"data: "[..]),
1081 Just(&b"id: "[..]),
1082 Just(&b"retry: 9"[..]),
1083 Just(&b"\r\n\r\n"[..]),
1084 Just(&b"\xEF\xBB\xBF"[..]),
1085 Just(&b"\xFF"[..]),
1086 ],
1087 0..10,
1088 ),
1089 cuts in proptest::collection::vec(any::<usize>(), 0..32),
1090 ) {
1091 let mut wire = Vec::new();
1092 for (i, (byte, n)) in runs.iter().enumerate() {
1093 if let Some(p) = prefixes.get(i) {
1094 wire.extend_from_slice(p);
1095 }
1096 wire.extend(std::iter::repeat_n(*byte, *n));
1097 wire.extend_from_slice(&noise);
1098 }
1099 let (frames, held) = read(&wire, &cuts);
1100 prop_assert!(held <= 2 * (MAX_LINE_BYTES + MAX_FRAME_BYTES));
1101 for f in frames {
1102 prop_assert!(f.data.len() <= 3 * MAX_FRAME_BYTES);
1103 prop_assert!(
1104 f.id.as_ref().is_none_or(|id| id.len() <= MAX_LINE_BYTES)
1105 );
1106 }
1107 }
1108 }
1109 }
1110}