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)]
282#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
283#[cfg_attr(feature = "schemars", schemars(inline))]
284#[serde(deny_unknown_fields)]
285pub struct NotificationSettings {
286 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub direct_messages: Option<bool>,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub broadcasts: Option<bool>,
293}
294
295impl NotificationSettings {
296 pub fn is_empty(&self) -> bool {
298 self.direct_messages.is_none() && self.broadcasts.is_none()
299 }
300}
301
302pub const MAX_LINE_BYTES: usize = 64 * 1024;
306
307pub const MAX_FRAME_BYTES: usize = 256 * 1024;
310
311#[derive(Debug, Clone, Default, PartialEq, Eq)]
313pub struct SseFrame {
314 pub id: Option<String>,
317 pub event: Option<String>,
320 pub data: String,
322 pub retry: Option<u64>,
324}
325
326type Feed = futures::channel::mpsc::UnboundedReceiver<
327 Result<String, std::convert::Infallible>,
328>;
329
330pub struct SseParser {
344 framer: Framer,
345 feed: futures::channel::mpsc::UnboundedSender<
346 Result<String, std::convert::Infallible>,
347 >,
348 events: eventsource_stream::EventStream<Feed>,
349 #[cfg(test)]
351 longest_fed: usize,
352}
353
354impl std::fmt::Debug for SseParser {
355 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356 f.debug_struct("SseParser")
357 .field("framer", &self.framer)
358 .finish_non_exhaustive()
359 }
360}
361
362impl Default for SseParser {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368impl SseParser {
369 pub fn new() -> Self {
370 let (feed, rx) = futures::channel::mpsc::unbounded();
371 let mut parser = Self {
372 framer: Framer::default(),
373 feed,
374 events: eventsource_stream::EventStream::new(rx),
375 #[cfg(test)]
376 longest_fed: 0,
377 };
378 parser.send(":\n".to_owned());
383 parser.drain(&mut Vec::new());
384 parser
385 }
386
387 fn send(&mut self, line: String) {
388 #[cfg(test)]
389 {
390 self.longest_fed = self.longest_fed.max(line.len());
391 }
392 if self.feed.unbounded_send(Ok(line)).is_err() {
393 unreachable!("the parser's own feed closed");
395 }
396 }
397
398 pub fn push(&mut self, bytes: &[u8]) -> Vec<SseFrame> {
400 let mut frames = Vec::new();
401 for text in self.framer.push(bytes) {
402 let retry = retry_of(&text);
403 let before = frames.len();
404 for line in text.split_inclusive('\n') {
408 self.send(line.to_owned());
409 self.drain(&mut frames);
410 }
411 if frames.len() == before
412 && let Some(ms) = retry
413 {
414 frames.push(SseFrame {
415 retry: Some(ms),
416 ..Default::default()
417 });
418 }
419 }
420 frames
421 }
422
423 fn drain(&mut self, frames: &mut Vec<SseFrame>) {
426 use futures::Stream;
427 use std::task::{Context, Poll};
428 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
429 loop {
430 match std::pin::Pin::new(&mut self.events).poll_next(&mut cx) {
431 Poll::Ready(Some(Ok(event))) => frames.push(SseFrame {
432 id: (!event.id.is_empty()).then_some(event.id),
433 event: Some(event.event),
434 data: event.data,
435 retry: event.retry.map(|d| d.as_millis() as u64),
436 }),
437 Poll::Ready(Some(Err(_))) => {
441 let framer = std::mem::take(&mut self.framer);
442 *self = Self::new();
443 self.framer = framer;
444 return;
445 }
446 Poll::Ready(None) | Poll::Pending => return,
447 }
448 }
449 }
450
451 pub fn take_dropped(&mut self) -> Vec<DroppedFrame> {
453 std::mem::take(&mut self.framer.dropped)
454 }
455
456 #[cfg(test)]
458 fn buffered(&self) -> usize {
459 self.framer.line.capacity() + self.framer.frame.capacity()
460 }
461}
462
463fn retry_of(text: &str) -> Option<u64> {
465 text.lines().rev().find_map(|line| {
466 let value = line.strip_prefix("retry:")?;
467 value.strip_prefix(' ').unwrap_or(value).parse().ok()
468 })
469}
470
471const BOM: &[u8] = "\u{feff}".as_bytes();
472
473#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub enum SseCap {
476 Line,
478 Frame,
480}
481
482#[derive(Debug, Clone, Copy, PartialEq, Eq)]
484pub struct DroppedFrame {
485 pub cap: SseCap,
486 pub bytes: usize,
489}
490
491#[derive(Debug, Default)]
494struct Framer {
495 line: Vec<u8>,
496 line_seen: bool,
498 frame: Vec<u8>,
499 frame_bytes: usize,
501 spoiled: Option<SseCap>,
504 dropped: Vec<DroppedFrame>,
505 after_cr: bool,
507 bom: usize,
510 started: bool,
511}
512
513impl Framer {
514 fn push(&mut self, bytes: &[u8]) -> Vec<String> {
515 let mut out = Vec::new();
516 for &b in bytes {
517 if !self.started {
518 if b == BOM[self.bom] {
519 self.bom += 1;
520 self.started = self.bom == BOM.len();
521 continue;
522 }
523 self.started = true;
524 for &p in &BOM[..self.bom] {
525 self.byte(p, &mut out);
526 }
527 }
528 self.byte(b, &mut out);
529 }
530 out
531 }
532
533 fn byte(&mut self, b: u8, out: &mut Vec<String>) {
534 if std::mem::take(&mut self.after_cr) && b == b'\n' {
535 return;
536 }
537 match b {
538 b'\n' | b'\r' => {
539 self.after_cr = b == b'\r';
540 self.end_line(out);
541 }
542 _ => {
543 self.line_seen = true;
544 self.frame_bytes = self.frame_bytes.saturating_add(1);
545 if self.spoiled.is_some() {
546 } else if self.line.len() >= MAX_LINE_BYTES {
547 self.spoil(SseCap::Line);
548 } else {
549 self.line.push(b);
550 }
551 }
552 }
553 }
554
555 fn spoil(&mut self, cap: SseCap) {
557 self.spoiled = Some(cap);
558 self.line = Vec::new();
559 self.frame = Vec::new();
560 }
561
562 fn end_line(&mut self, out: &mut Vec<String>) {
563 if !std::mem::take(&mut self.line_seen) {
564 let mut frame = std::mem::take(&mut self.frame);
565 let bytes = std::mem::take(&mut self.frame_bytes);
566 match self.spoiled.take() {
567 Some(cap) if self.dropped.len() < 64 => {
569 self.dropped.push(DroppedFrame { cap, bytes })
570 }
571 Some(_) => {}
572 None if !frame.is_empty() => {
573 frame.push(b'\n');
574 out.push(String::from_utf8_lossy(&frame).into_owned());
575 }
576 None => {}
577 }
578 return;
579 }
580 self.frame_bytes = self.frame_bytes.saturating_add(1);
581 let line = std::mem::take(&mut self.line);
582 if self.spoiled.is_some() {
583 return;
584 }
585 if self.frame.len() + line.len() + 1 > MAX_FRAME_BYTES {
586 self.spoil(SseCap::Frame);
587 return;
588 }
589 self.frame.extend_from_slice(&line);
590 self.frame.push(b'\n');
591 self.line = line;
592 self.line.clear();
593 }
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599
600 use crate::ids::EventSeq;
601
602 fn notice(seq: i64) -> MessageNotice {
603 MessageNotice {
604 recipient_id: AgentId::from(uuid::Uuid::from_u128(9)),
605 message_id: MessageId::from(uuid::Uuid::from_u128(seq as u128)),
606 sender_id: AgentId::from(uuid::Uuid::from_u128(7)),
607 sender_name: Some("alice".parse().unwrap()),
608 sent_at: DateTime::from_timestamp(1_760_000_000, 0).unwrap(),
609 }
610 }
611
612 #[test]
613 fn an_event_says_who_and_which_message_and_nothing_else() {
614 let json =
615 serde_json::to_value(Event::DirectMessage(notice(42))).unwrap();
616 assert_eq!(
617 json,
618 serde_json::json!({
619 "kind": "direct_message",
620 "message_id": "00000000-0000-0000-0000-00000000002a",
621 "recipient_id": "00000000-0000-0000-0000-000000000009",
622 "sender_id": "00000000-0000-0000-0000-000000000007",
623 "sender_name": "alice",
624 "sent_at": "2025-10-09T08:53:20Z",
625 })
626 );
627 let back: Event = serde_json::from_value(json).unwrap();
628 assert_eq!(back, Event::DirectMessage(notice(42)));
629 assert_eq!(back.name(), "direct_message");
630 assert_eq!(
631 back.recipient_id(),
632 AgentId::from(uuid::Uuid::from_u128(9))
633 );
634 assert_eq!(Event::Broadcast(notice(1)).name(), "broadcast");
635 let gap = Event::Gap(GapNotice {
636 recipient_id: AgentId::from(uuid::Uuid::from_u128(9)),
637 skipped: 12,
638 at_least: false,
639 });
640 assert_eq!(
641 serde_json::to_value(&gap).unwrap(),
642 serde_json::json!({
643 "kind": "gap",
644 "recipient_id": "00000000-0000-0000-0000-000000000009",
645 "skipped": 12
646 })
647 );
648 assert!(gap.message().is_none());
649 assert!(
650 NotificationPreferences {
651 direct_messages: false,
652 broadcasts: false
653 }
654 .wants(&gap)
655 );
656 }
657
658 #[test]
659 fn a_signed_event_verifies_and_a_changed_one_does_not() {
660 let (key, public) = crypto::generate_keypair();
661 let signed = SignedEvent::sign(
662 &key,
663 Event::DirectMessage(notice(3)),
664 1_760_000_000,
665 );
666 assert!(signed.verify(&public));
667
668 let wire = serde_json::to_string(&signed).unwrap();
669 let read: SignedEvent = serde_json::from_str(&wire).unwrap();
670 assert!(read.verify(&public));
671
672 let mut moved = read.clone();
673 moved.event = Event::DirectMessage(notice(4));
674 assert!(!moved.verify(&public));
675 let mut forwarded = read.clone();
676 if let Event::DirectMessage(m) = &mut forwarded.event {
677 m.recipient_id = AgentId::from(uuid::Uuid::from_u128(10));
678 }
679 assert!(!forwarded.verify(&public), "an event for another agent");
680 let mut kind = read.clone();
681 kind.event = Event::Broadcast(notice(3));
682 assert!(!kind.verify(&public));
683 let mut late = read;
684 late.timestamp += 1;
685 assert!(!late.verify(&public));
686
687 let (_, other) = crypto::generate_keypair();
688 assert!(!signed.verify(&other));
689 }
690
691 #[test]
694 fn signing_bytes_are_the_context_then_sorted_keys() {
695 let bytes = SignedEvent::signing_bytes(&Event::Broadcast(notice(5)));
696 let text = String::from_utf8(bytes).unwrap();
697 assert_eq!(
698 text,
699 "agora/event/v1\n{\"kind\":\"broadcast\",\
700 \"message_id\":\"00000000-0000-0000-0000-000000000005\",\
701 \"recipient_id\":\"00000000-0000-0000-0000-000000000009\",\
702 \"sender_id\":\"00000000-0000-0000-0000-000000000007\",\
703 \"sender_name\":\"alice\",\"sent_at\":\"2025-10-09T08:53:20Z\"}"
704 );
705 }
706
707 #[test]
708 fn preferences_filter_and_change_field_by_field() {
709 let all = NotificationPreferences::default();
710 assert!(all.wants(&Event::DirectMessage(notice(1))));
711 assert!(all.wants(&Event::Broadcast(notice(1))));
712
713 let quiet = all.with(&NotificationSettings {
714 direct_messages: Some(false),
715 broadcasts: None,
716 });
717 assert!(!quiet.wants(&Event::DirectMessage(notice(1))));
718 assert!(quiet.wants(&Event::Broadcast(notice(1))));
719 assert_eq!(quiet.with(&NotificationSettings::default()), quiet);
720 assert!(NotificationSettings::default().is_empty());
721 }
722
723 #[test]
724 fn event_seq_parses_what_it_prints_and_refuses_the_rest() {
725 assert_eq!("17".parse::<EventSeq>().unwrap(), EventSeq::from(17));
726 assert_eq!(" 0 ".parse::<EventSeq>().unwrap(), EventSeq::ZERO);
727 assert!("-1".parse::<EventSeq>().is_err());
728 assert!("abc".parse::<EventSeq>().is_err());
729 assert_eq!(EventSeq::from(9).to_string(), "9");
730 assert_eq!(serde_json::to_string(&EventSeq::from(9)).unwrap(), "9");
731 }
732
733 #[cfg(feature = "schemars")]
734 #[test]
735 fn schemas_carry_no_ref() {
736 for (name, schema) in [
737 (
738 "SignedEvent",
739 serde_json::to_value(schemars::schema_for!(SignedEvent))
740 .unwrap(),
741 ),
742 (
743 "NotificationSettings",
744 serde_json::to_value(schemars::schema_for!(
745 NotificationSettings
746 ))
747 .unwrap(),
748 ),
749 ] {
750 let text = schema.to_string();
751 assert!(!text.contains("$ref"), "{name}: {text}");
752 assert!(!text.contains("$defs"), "{name}: {text}");
753 }
754 }
755
756 #[test]
757 fn the_parser_reads_frames_across_chunks_and_skips_keep_alives() {
758 let mut p = SseParser::new();
759 assert!(p.push(b": keep-alive\n\nid: 1\nevent: direct").is_empty());
760 let frames = p.push(b"_message\ndata: {\"a\":\ndata: 1}\n\n");
761 assert_eq!(
762 frames,
763 vec![SseFrame {
764 id: Some("1".into()),
765 event: Some("direct_message".into()),
766 data: "{\"a\":\n1}".into(),
767 retry: None,
768 }]
769 );
770 let frames = p.push(b"retry: 3000\r");
773 assert!(frames.is_empty());
774 let frames = p.push(b"\n\r\ndata:x\r\n\r\n");
775 assert_eq!(
776 frames,
777 vec![
778 SseFrame {
779 retry: Some(3000),
780 ..Default::default()
781 },
782 SseFrame {
783 id: Some("1".into()),
784 event: Some("message".into()),
785 data: "x".into(),
786 retry: None,
787 },
788 ]
789 );
790 assert!(p.push(b"foo: bar\n\n").is_empty());
792 assert!(p.take_dropped().is_empty());
793 let line = format!("data: {}\n", "x".repeat(MAX_LINE_BYTES - 16));
796 let mut big = line.repeat(MAX_FRAME_BYTES / line.len() + 2);
797 big.push_str("\ndata: after\n\n");
798 let frames = p.push(big.as_bytes());
799 assert_eq!(frames.len(), 1);
800 assert_eq!(frames[0].data, "after");
801 let dropped = p.take_dropped();
802 assert_eq!(dropped.len(), 1);
803 assert_eq!(dropped[0].cap, SseCap::Frame);
804 assert_eq!(dropped[0].bytes, big.len() - "\ndata: after\n\n".len());
805 }
806
807 #[test]
810 fn an_overlong_line_drops_its_frame_whole_across_chunks() {
811 let mut p = SseParser::new();
812 assert_eq!(p.push(b"id: before\ndata: a\n\n").len(), 1);
813 let long = "y".repeat(MAX_LINE_BYTES + 1);
814 let wire =
815 format!("id: cut\ndata: {long}\ndata: tail\n\ndata: next\n\n");
816 let mut frames = Vec::new();
817 for piece in wire.as_bytes().chunks(4093) {
818 frames.extend(p.push(piece));
819 assert!(p.buffered() <= 2 * (MAX_LINE_BYTES + MAX_FRAME_BYTES));
820 }
821 assert_eq!(
822 frames,
823 vec![SseFrame {
824 id: Some("before".into()),
825 event: Some("message".into()),
826 data: "next".into(),
827 retry: None,
828 }],
829 "the cut frame's id never becomes the stream's"
830 );
831 assert_eq!(
832 p.take_dropped(),
833 vec![DroppedFrame {
834 cap: SseCap::Line,
835 bytes: "id: cut\n".len()
836 + "data: ".len()
837 + long.len()
838 + 1
839 + "data: tail\n".len(),
840 }]
841 );
842 let at = "z".repeat(MAX_LINE_BYTES - "data: ".len());
844 let frames = p.push(format!("data: {at}\n\n").as_bytes());
845 assert_eq!(frames.len(), 1);
846 assert_eq!(frames[0].data, at);
847 }
848
849 #[test]
850 fn a_bom_comments_and_an_empty_id_are_read_as_the_format_says() {
851 let mut p = SseParser::new();
852 let mut frames = Vec::new();
853 for piece in [&b"\xEF"[..], b"\xBB", b"\xBFid: 1\ndata: a\n\n"] {
855 frames.extend(p.push(piece));
856 }
857 frames.extend(p.push(b":c\n: keep-alive\n\nid\ndata: b\n\n"));
858 assert_eq!(frames.len(), 2);
859 assert_eq!(frames[0].id.as_deref(), Some("1"));
860 assert_eq!(frames[1].id, None, "an empty id resets it");
861 assert!(p.push("\u{feff}data: c\n\n".as_bytes()).is_empty());
863 let frames = p.push(b"data: \xFF\xFE ok\n\n");
865 assert_eq!(frames[0].data, "\u{fffd}\u{fffd} ok");
866 }
867
868 #[test]
871 fn a_late_bom_and_odd_ids_read_as_the_format_says() {
872 let mut p = SseParser::new();
873 assert!(p.push("\n\n\u{feff}data: x\n\n".as_bytes()).is_empty());
874 let frames = p.push(b"id: 1\ndata: a\n\nid: n\0ul\ndata: b\n\n");
875 assert_eq!(frames[1].id.as_deref(), Some("1"), "a NUL id is ignored");
876 let frames = p.push(b"id:\ndata: c\n\n");
877 assert_eq!(frames[0].id, None);
878 }
879
880 #[test]
884 fn a_frame_of_many_short_lines_is_cheap() {
885 for line in [":\n", "data\n", "x\n"] {
886 let mut wire = line.repeat((MAX_FRAME_BYTES - 64) / line.len());
887 wire.push_str("data: end\n\n");
888 let mut p = SseParser::new();
889 let started = std::time::Instant::now();
890 let frames = p.push(wire.as_bytes());
891 let took = started.elapsed();
892 assert_eq!(frames.len(), 1, "{line:?}");
893 assert!(
894 p.longest_fed <= MAX_LINE_BYTES + 1,
895 "{line:?}: fed {} bytes at once",
896 p.longest_fed
897 );
898 assert!(
900 took < std::time::Duration::from_secs(5),
901 "{line:?}: {took:?}"
902 );
903 }
904 }
905
906 mod props {
907 use super::*;
908 use proptest::prelude::*;
909
910 #[derive(Debug, Clone)]
912 struct Spec {
913 comment_before: Option<String>,
914 id: Option<String>,
915 event: Option<String>,
916 data: Vec<String>,
917 retry: Option<u64>,
918 oversize: bool,
920 }
921
922 fn spec() -> impl Strategy<Value = Spec> {
923 (
924 proptest::option::of("[^\r\n]{0,20}"),
925 proptest::option::of("[A-Za-z0-9_-]{1,12}"),
926 proptest::option::of("[a-z_]{1,12}"),
927 proptest::collection::vec("[^\r\n]{0,40}", 0..4),
928 proptest::option::of(any::<u64>()),
929 proptest::bool::weighted(0.08),
930 )
931 .prop_map(
932 |(comment_before, id, event, data, retry, oversize)| Spec {
933 comment_before,
934 id,
935 event,
936 data,
937 retry,
938 oversize,
939 },
940 )
941 }
942
943 fn lines(spec: &Spec) -> Vec<String> {
944 let mut lines = Vec::new();
945 if let Some(c) = &spec.comment_before {
946 lines.push(format!(":{c}"));
947 }
948 if let Some(id) = &spec.id {
949 lines.push(format!("id: {id}"));
950 }
951 if let Some(e) = &spec.event {
952 lines.push(format!("event: {e}"));
953 }
954 for d in &spec.data {
955 lines.push(format!("data: {d}"));
956 }
957 if spec.oversize {
958 lines.push(format!("data: {}", "o".repeat(MAX_LINE_BYTES)));
959 }
960 if let Some(r) = spec.retry {
961 lines.push(format!("retry: {r}"));
962 }
963 lines
964 }
965
966 fn model(specs: &[Spec]) -> Vec<SseFrame> {
968 let mut id = None;
969 let mut out = Vec::new();
970 for spec in specs.iter().filter(|s| !s.oversize) {
971 if spec.id.is_some() {
972 id = spec.id.clone();
973 }
974 if !spec.data.is_empty() {
975 out.push(SseFrame {
976 id: id.clone(),
977 event: Some(
978 spec.event.clone().unwrap_or("message".into()),
979 ),
980 data: spec.data.join("\n"),
981 retry: spec.retry,
982 });
983 } else if spec.retry.is_some() {
984 out.push(SseFrame {
985 retry: spec.retry,
986 ..Default::default()
987 });
988 }
989 }
990 out
991 }
992
993 fn wire(specs: &[Spec], endings: &[u8], bom: bool) -> Vec<u8> {
997 let mut out = Vec::new();
998 if bom {
999 out.extend_from_slice(BOM);
1000 }
1001 let mut endings = endings.iter().cycle();
1002 let mut after_cr = false;
1003 let mut end = |out: &mut Vec<u8>, blank: bool| {
1004 let mut e = *endings.next().unwrap_or(&0);
1005 if blank && after_cr && e == 0 {
1006 e = 1;
1007 }
1008 after_cr = e == 1;
1009 out.extend_from_slice(match e {
1010 0 => b"\n",
1011 1 => b"\r",
1012 _ => b"\r\n",
1013 });
1014 };
1015 for spec in specs {
1016 for line in lines(spec) {
1017 out.extend_from_slice(line.as_bytes());
1018 end(&mut out, false);
1019 }
1020 end(&mut out, true);
1021 }
1022 out
1023 }
1024
1025 fn read(wire: &[u8], cuts: &[usize]) -> (Vec<SseFrame>, usize) {
1026 let mut cuts: Vec<usize> =
1027 cuts.iter().map(|c| c % (wire.len() + 1)).collect();
1028 cuts.sort();
1029 let mut p = SseParser::new();
1030 let mut frames = Vec::new();
1031 let mut at = 0;
1032 let mut held = 0;
1033 for cut in cuts.into_iter().chain([wire.len()]) {
1034 frames.extend(p.push(&wire[at..cut]));
1035 held = held.max(p.buffered());
1036 at = cut;
1037 }
1038 (frames, held)
1039 }
1040
1041 proptest! {
1042 #![proptest_config(ProptestConfig::with_cases(512))]
1043
1044 #[test]
1047 fn chunk_boundaries_never_change_the_frames(
1048 specs in proptest::collection::vec(spec(), 0..12),
1049 endings in proptest::collection::vec(0u8..3, 1..16),
1050 bom in any::<bool>(),
1051 cuts in proptest::collection::vec(any::<usize>(), 0..24),
1052 ) {
1053 let wire = wire(&specs, &endings, bom);
1054 let expected = model(&specs);
1055 prop_assert_eq!(&read(&wire, &[]).0, &expected);
1056 prop_assert_eq!(&read(&wire, &cuts).0, &expected);
1057 let every: Vec<usize> = (0..wire.len()).collect();
1059 prop_assert_eq!(&read(&wire, &every).0, &expected);
1060 }
1061
1062 #[test]
1065 fn garbage_never_panics_or_grows_past_the_caps(
1066 runs in proptest::collection::vec(
1067 (
1068 prop_oneof![
1069 any::<u8>(),
1070 Just(b'\n'),
1071 Just(b'\r'),
1072 Just(b':'),
1073 Just(b'x'),
1074 ],
1075 prop_oneof![1usize..8, 1usize..400_000],
1076 ),
1077 0..10,
1078 ),
1079 noise in proptest::collection::vec(any::<u8>(), 0..512),
1080 prefixes in proptest::collection::vec(
1081 prop_oneof![
1082 Just(&b"data: "[..]),
1083 Just(&b"id: "[..]),
1084 Just(&b"retry: 9"[..]),
1085 Just(&b"\r\n\r\n"[..]),
1086 Just(&b"\xEF\xBB\xBF"[..]),
1087 Just(&b"\xFF"[..]),
1088 ],
1089 0..10,
1090 ),
1091 cuts in proptest::collection::vec(any::<usize>(), 0..32),
1092 ) {
1093 let mut wire = Vec::new();
1094 for (i, (byte, n)) in runs.iter().enumerate() {
1095 if let Some(p) = prefixes.get(i) {
1096 wire.extend_from_slice(p);
1097 }
1098 wire.extend(std::iter::repeat_n(*byte, *n));
1099 wire.extend_from_slice(&noise);
1100 }
1101 let (frames, held) = read(&wire, &cuts);
1102 prop_assert!(held <= 2 * (MAX_LINE_BYTES + MAX_FRAME_BYTES));
1103 for f in frames {
1104 prop_assert!(f.data.len() <= 3 * MAX_FRAME_BYTES);
1105 prop_assert!(
1106 f.id.as_ref().is_none_or(|id| id.len() <= MAX_LINE_BYTES)
1107 );
1108 }
1109 }
1110 }
1111 }
1112}