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}