Skip to main content

protocol_droid/domain/stream/
event_name.rs

1//! The `event:` field of a `text/event-stream` frame.
2//!
3//! # Why this is a newtype and not a `String`
4//!
5//! Because a name is a wire token, and a wire token that is spliced into a
6//! frame without being checked is an injection primitive. The encoder writes
7//!
8//! ```text
9//! event: <name>
10//! ```
11//!
12//! and a `name` containing a newline therefore does not become a name that
13//! renders oddly — it becomes a *second field*, and everything after the break
14//! is re-read by the client as a field of its own. A name of
15//! `message\ndata: {"jsonrpc":"2.0"}` is a name that carries a payload, and a
16//! payload that the peer never sent.
17//!
18//! That is not a hypothetical: the legacy transport's names come from a
19//! [`MessageEndpoint`](crate::domain::stream::event_name::MessageEndpoint) the
20//! server built and a tool name a caller chose, and a tool name is a string
21//! somebody typed. The parse is where the check belongs — the crate's rule is
22//! to parse rather than to validate, and this is a parse.
23//!
24//! # What the standard actually requires
25//!
26//! The WHATWG event-stream grammar makes `event` a field whose value is the
27//! rest of the line. It does not forbid a colon in the value, because a colon
28//! cannot terminate a line — so `a:b` is a legal name. What the grammar cannot
29//! express, and what this type rejects, is a value containing a line
30//! terminator, because the grammar has no escaping for one and the only
31//! faithful encoding of an embedded break is to emit the break.
32
33use core::fmt;
34use core::str::FromStr;
35use std::borrow::Cow;
36
37use thiserror::Error;
38
39/// The `event` name a legacy-transport frame carries, or the unnamed default.
40///
41/// The two names the `2024-11-05` HTTP+SSE transport defines, kept as
42/// constants so no call site spells them as a literal. The specification
43/// names them in prose — "the server MUST send an `endpoint` event" and
44/// "server messages are sent as SSE `message` events" — and a literal at a
45/// call site is how one of them ends up spelled `Endpoint` and silently
46/// matches nothing.
47///
48/// Cited: <https://modelcontextprotocol.io/specification/2024-11-05/basic/transports>
49pub mod names {
50    /// The first event on a legacy stream: where to POST.
51    pub const ENDPOINT: super::EventName = super::EventName::const_new("endpoint");
52
53    /// Every JSON-RPC message the server sends back.
54    pub const MESSAGE: super::EventName = super::EventName::const_new("message");
55}
56
57/// A validated `event:` value.
58///
59/// A [`Cow`] rather than an `Arc<str>` for one reason that turned out to
60/// decide the design: `Arc::from` is not `const`, so an `Arc`-backed name
61/// cannot be a `const`, and the two names the legacy transport is defined by
62/// are exactly the kind of thing that should be a constant rather than a value
63/// built at first use. `Cow::Borrowed` *is* const-constructible, so the two
64/// names the specification names cost nothing and compare without a pointer
65/// chase, while a name that arrived from a peer is owned and costs one
66/// allocation — which is right, because a peer's name is not known ahead of
67/// time and there is nothing to borrow it from.
68///
69/// `Borrowed` is safe here for the same reason it is safe anywhere: a `&'static
70/// str` has no lifetime to shorten, and `from_str` is the only constructor
71/// that produces a `Borrowed` variant.
72#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
73pub struct EventName(Cow<'static, str>);
74
75impl EventName {
76    /// A compile-time name, for the constants the legacy transport uses.
77    ///
78    /// `const` because those are the only names this crate writes itself, and a
79    /// name that needed a parse to be written would be a name the crate could
80    /// get wrong. Only a `&'static str` can be passed, so a name with a
81    /// lifetime — a tool name held across an await, say — cannot be quietly
82    /// promoted into a constant that outlives it.
83    ///
84    /// `pub(crate)` and not `pub` for the reason the whole type exists: a
85    /// `const fn` cannot scan for a line terminator, so this constructor is
86    /// the one door that bypasses the check. Making it crate-private leaves
87    /// [`from_str`](Self::from_str) as the only way a name can enter from
88    /// outside this crate, and leaves exactly two callers inside it — the two
89    /// constants below, which are literals and are asserted by a test. A
90    /// `pub const_new` would hand every caller the injection this newtype
91    /// closes, in a form that looked like the safe constructor.
92    pub(crate) const fn const_new(value: &'static str) -> Self {
93        Self(Cow::Borrowed(value))
94    }
95
96    /// The name as a string.
97    #[must_use]
98    pub fn as_str(&self) -> &str {
99        &self.0
100    }
101
102    /// Whether this is `name`.
103    ///
104    /// Compares contents, not representations: a `Borrowed("message")` and an
105    /// owned `"message"` are the same name, and a client that dispatched on
106    /// which one it was would be matching on an implementation detail.
107    #[must_use]
108    pub fn is(&self, name: &Self) -> bool {
109        self.0 == name.0
110    }
111}
112
113/// Why a string is not usable as an `event:` name.
114#[derive(Debug, Clone, PartialEq, Eq, Error)]
115pub enum EventNameError {
116    /// The value was empty.
117    ///
118    /// `event:` with nothing after it is a field with an empty value, which the
119    /// grammar allows and which means the *default* event name rather than the
120    /// empty one. Accepting it would make `Some("")` and `None` two spellings
121    /// of one thing, and a client dispatching on the name would have to treat
122    /// them differently for no reason.
123    #[error("an event name may not be empty")]
124    Empty,
125
126    /// The value contained a line terminator.
127    ///
128    /// The load-bearing refusal. See the module docs: this is what would turn
129    /// a name into a second field.
130    #[error("an event name may not contain a line terminator")]
131    LineTerminator,
132}
133
134impl FromStr for EventName {
135    type Err = EventNameError;
136
137    /// Parses a name, refusing the one thing that would inject a field.
138    ///
139    /// CR and LF are both rejected, and both must be: the encoder's line
140    /// terminator is LF, but a conformant *reader* also treats a lone CR as a
141    /// break, so a name holding a CR reads as two fields on a reader that the
142    /// writer did not have in mind. Rejecting only LF would leave that reader
143    /// injectable.
144    ///
145    /// A colon is **not** rejected. The grammar makes the value the rest of the
146    /// line, so `a:b` is a legal name and a client comparing it to `b` has a
147    /// bug, not a server that corrupted it.
148    ///
149    /// # Errors
150    ///
151    /// [`EventNameError::Empty`] for an empty value, and
152    /// [`EventNameError::LineTerminator`] for one containing CR or LF.
153    fn from_str(value: &str) -> Result<Self, Self::Err> {
154        if value.is_empty() {
155            return Err(EventNameError::Empty);
156        }
157        if value.contains(['\n', '\r']) {
158            return Err(EventNameError::LineTerminator);
159        }
160        Ok(Self(Cow::Owned(value.to_string())))
161    }
162}
163
164impl fmt::Display for EventName {
165    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
166        formatter.write_str(&self.0)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::{EventName, EventNameError, names};
173    use core::str::FromStr;
174
175    /// What a test here can fail with.
176    ///
177    /// A test's own complaint, or the parse's own error. Two unrelated error
178    /// types in one test module is the case the crate's own testing rules call
179    /// out — a single `enum` with a `From` each, rather than a `Box<dyn
180    /// Error>` that accepts anything and so converts nothing usefully.
181    #[derive(Debug)]
182    enum Failure {
183        /// The test's own complaint.
184        Complaint(String),
185        /// A parse the test wanted to succeed refused.
186        Refused(EventNameError),
187    }
188
189    impl From<EventNameError> for Failure {
190        fn from(error: EventNameError) -> Self {
191            Self::Refused(error)
192        }
193    }
194
195    impl core::fmt::Display for Failure {
196        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
197            match self {
198                Self::Complaint(text) => f.write_str(text),
199                Self::Refused(error) => write!(f, "refused when a name was wanted: {error}"),
200            }
201        }
202    }
203
204    type TestError = Failure;
205
206    /// The test's own complaint.
207    fn fail(message: impl Into<String>) -> TestError {
208        Failure::Complaint(message.into())
209    }
210
211    /// Asserts that `value` parses to a name reading back as `expected`.
212    fn parses_to(value: &str, expected: &str) -> Result<(), TestError> {
213        let name = EventName::from_str(value)?;
214        if name.as_str() != expected {
215            return Err(fail(format!(
216                "{value:?} parsed as {:?}, not {expected:?}",
217                name.as_str()
218            )));
219        }
220        Ok(())
221    }
222
223    /// Asserts that `value` is refused, and with `expected`.
224    fn refused_with(value: &str, expected: &EventNameError) -> Result<(), TestError> {
225        match EventName::from_str(value) {
226            Err(error) if &error == expected => Ok(()),
227            Err(error) => Err(fail(format!(
228                "{value:?} was refused as {error:?}, not {expected:?}"
229            ))),
230            Ok(name) => Err(fail(format!(
231                "{value:?} parsed as {:?}, which cannot be encoded safely",
232                name.as_str()
233            ))),
234        }
235    }
236
237    #[test]
238    fn a_plain_name_parses() -> Result<(), TestError> {
239        parses_to("endpoint", "endpoint")
240    }
241
242    /// A colon is the rest of the line, and the grammar says so before it says
243    /// anything about which characters a name may contain. A client that
244    /// compared `a:b` to `b` has a bug; a server that refused to send it would
245    /// be refusing a legal frame.
246    #[test]
247    fn a_colon_is_legal_because_the_value_is_the_rest_of_the_line() -> Result<(), TestError> {
248        parses_to("a:b", "a:b")
249    }
250
251    /// The refusal that is the whole reason this type exists.
252    ///
253    /// Without it, `event: message\ndata: {...}` reaches a client as a name
254    /// and a payload, and the client dispatches a message the peer never sent.
255    #[test]
256    fn a_newline_in_a_name_is_refused() -> Result<(), TestError> {
257        refused_with(
258            "message\ndata: {\"jsonrpc\":\"2.0\"}",
259            &EventNameError::LineTerminator,
260        )
261    }
262
263    /// A lone CR is a terminator to a conformant *reader* even though the
264    /// encoder writes LF, so the parse has to reject it too. A reader that
265    /// only splits on LF would otherwise see the name and the payload as one
266    /// line, and the writer would have disagreed with a reader it cannot see.
267    #[test]
268    fn a_carriage_return_in_a_name_is_refused() -> Result<(), TestError> {
269        refused_with("message\rdata: x", &EventNameError::LineTerminator)
270    }
271
272    /// Empty is refused so that `Some("")` and `None` cannot both mean the
273    /// default name. The grammar allows an empty value; two spellings of one
274    /// meaning is where a dispatch comparison goes wrong.
275    #[test]
276    fn an_empty_name_is_refused() -> Result<(), TestError> {
277        refused_with("", &EventNameError::Empty)
278    }
279
280    /// The constants have to be the names on the wire, spelled exactly, and a
281    /// constant that did not compare equal to a parsed name would silently
282    /// match nothing in every dispatch that used it.
283    #[test]
284    fn the_legacy_names_are_the_specified_spellings() -> Result<(), TestError> {
285        if names::ENDPOINT.as_str() != "endpoint" {
286            return Err(fail("the endpoint constant is not spelled `endpoint`"));
287        }
288        if names::MESSAGE.as_str() != "message" {
289            return Err(fail("the message constant is not spelled `message`"));
290        }
291        let parsed = EventName::from_str("message")?;
292        if !parsed.is(&names::MESSAGE) {
293            return Err(fail("a parsed name does not match the message constant"));
294        }
295        if names::MESSAGE.is(&names::ENDPOINT) {
296            return Err(fail("the two legacy names are equal"));
297        }
298        Ok(())
299    }
300}