protocol_droid/domain/transport/framing.rs
1//! Newline-delimited framing: bytes in, whole messages out.
2//!
3//! # What this is for
4//!
5//! The stdio transport carries one JSON-RPC message per line, and the line is
6//! the only framing the specification defines for it. Everything else in that
7//! transport — the pipe, the child process, `tokio::process` — is I/O, and this
8//! module is not: it is a state machine over a byte buffer, and a test can
9//! drive it with `&[u8]` and no process anywhere. That separation is the whole
10//! reason the framing is worth having as its own stage.
11//!
12//! # The limit is the point
13//!
14//! [`FrameDecoder`] takes a [`MaxFrameSize`] from its caller and refuses a frame
15//! larger than it, with [`FramingError::FrameTooLarge`]. Two properties of *how*
16//! it refuses are the security-relevant part, and both are load-bearing:
17//!
18//! 1. **The check runs while the bytes are being read, never after.** A decoder
19//! that accumulates a line and measures it once the `\n` arrives has already
20//! paid for the memory it was trying to avoid — and the peer controls how
21//! much that costs, because it controls whether a `\n` ever arrives. This one
22//! counts against the limit inside [`FrameDecoder::push`], before the
23//! offending bytes are appended, and a refusal appends nothing at all.
24//! 2. **The retained buffer does not grow with the size of a hostile frame.** It
25//! is bounded by the configured limit plus whatever the caller pushed, and
26//! not by the frame. A peer that streams gigabytes without a terminator is
27//! cut off after `limit` bytes, and the decoder is holding at most that.
28//! [`FrameDecoder::buffered`] exists so a test can assert this rather than
29//! take the claim on trust.
30//!
31//! # What is deliberately not here
32//!
33//! - **No MCP.** Framing delimits a JSON-RPC message; it does not know one from
34//! another, and nothing in this module names a method or a revision.
35//! - **No I/O.** Nothing here opens, reads or writes anything. The transport
36//! adapter supplies bytes and takes documents.
37//! - **No interpretation.** A `\r` before the `\n` is content, not a line ending
38//! to be trimmed, and an empty line is an empty frame rather than a skipped
39//! one. Trimming would be the same "helpful" rewriting the project forbids
40//! elsewhere: a decoder that edits a peer's bytes cannot forward them
41//! unchanged, and lossless passthrough is a property the bridge depends on.
42//! - **No default limit.** [`FrameDecoder::new`] takes a [`MaxFrameSize`] and
43//! there is no way to build one without it, so a transport cannot acquire a
44//! ceiling by accident. [`MaxFrameSize::DEFAULT`] exists for a caller who wants
45//! a starting point and will still name it.
46//!
47//! # Examples
48//!
49//! A frame split across two reads, which is the case hand-rolled framing gets
50//! wrong most often:
51//!
52//! ```
53//! use protocol_droid::domain::{FrameDecoder, MaxFrameSize};
54//!
55//! let limit = MaxFrameSize::new(64)?;
56//! let mut decoder = FrameDecoder::new(limit);
57//!
58//! decoder.push(b"{\"jsonrpc\":\"2.0\",")?;
59//! assert_eq!(decoder.next_frame()?, None, "no terminator yet");
60//!
61//! decoder.push(b"\"id\":1}\n")?;
62//! assert_eq!(decoder.next_frame()?, Some(r#"{"jsonrpc":"2.0","id":1}"#));
63//! assert_eq!(decoder.next_frame()?, None, "the frame was consumed");
64//! # Ok::<(), Box<dyn std::error::Error>>(())
65//! ```
66
67use core::num::NonZeroU32;
68
69use thiserror::Error;
70
71/// The byte that terminates a frame.
72const TERMINATOR: u8 = b'\n';
73
74/// Above this many consumed bytes the buffer is compacted rather than left to
75/// drift.
76///
77/// The decoder reads out of the front of a `Vec` by advancing a cursor rather
78/// than by draining, because draining the front of a buffer is linear in what
79/// remains and a long-lived connection would make that quadratic. A cursor means
80/// the consumed prefix sits in the allocation until the buffer is emptied, so it
81/// is reclaimed once reclaiming it is worth the copy.
82const COMPACT_AT: usize = 8 * 1024;
83
84/// The largest frame a decoder will accept, in bytes, excluding the terminator.
85///
86/// A caller sets this. There is no constant a decoder falls back to, because a
87/// ceiling nobody chose is a ceiling nobody has checked against the largest
88/// message the application actually sends.
89///
90/// The inner type is [`NonZeroU32`]: a limit of zero would accept no frame at
91/// all, including the empty one, which is a configuration mistake rather than a
92/// policy, and refusing it at construction is the last point at which the caller
93/// can still see what they did.
94///
95/// # Examples
96///
97/// ```
98/// use protocol_droid::domain::{MaxFrameSize, MaxFrameSizeError};
99///
100/// let limit = MaxFrameSize::new(8 * 1024 * 1024)?;
101/// assert_eq!(limit.bytes(), 8 * 1024 * 1024);
102///
103/// assert_eq!(MaxFrameSize::new(0), Err(MaxFrameSizeError::Zero));
104/// assert!(MaxFrameSize::new(1).is_ok());
105/// # Ok::<(), MaxFrameSizeError>(())
106/// ```
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
108pub struct MaxFrameSize(NonZeroU32);
109
110impl MaxFrameSize {
111 /// A starting point, for a caller who wants one and will still name it.
112 ///
113 /// Four mebibytes comfortably holds a `tools/list` of a large catalogue
114 /// while still bounding a hostile peer.
115 pub const DEFAULT: Self = Self(match NonZeroU32::new(4 * 1024 * 1024) {
116 Some(inner) => inner,
117 // Unreachable for the literal above, and `NonZeroU32::MIN` keeps this a
118 // `const` without a `panic!`, which the project's clippy profile denies
119 // across all targets.
120 None => NonZeroU32::MIN,
121 });
122
123 /// Builds a limit.
124 ///
125 /// # Errors
126 ///
127 /// [`MaxFrameSizeError::Zero`] when `bytes` is zero. See the type's docs for
128 /// why that is refused rather than accepted.
129 pub const fn new(bytes: u32) -> Result<Self, MaxFrameSizeError> {
130 match NonZeroU32::new(bytes) {
131 Some(inner) => Ok(Self(inner)),
132 None => Err(MaxFrameSizeError::Zero),
133 }
134 }
135
136 /// The limit, in bytes.
137 pub const fn bytes(self) -> u32 {
138 self.0.get()
139 }
140
141 /// The limit as a `usize`, for the comparisons a buffer length needs.
142 ///
143 /// Not `From<u32>`: `usize` may be narrower than `u32` in principle, so std
144 /// declines the infallible conversion and this is where the project chooses
145 /// to make one. `TryFrom` is not const-stable, so neither is this.
146 fn as_usize(self) -> usize {
147 usize::try_from(self.0.get()).unwrap_or(usize::MAX)
148 }
149}
150
151impl core::fmt::Display for MaxFrameSize {
152 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153 write!(f, "{} bytes", self.0)
154 }
155}
156
157/// Why a [`MaxFrameSize`] could not be built.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
159pub enum MaxFrameSizeError {
160 /// A limit of zero was requested.
161 ///
162 /// It would reject every frame including the empty one, so it encodes a
163 /// caller mistake rather than a policy.
164 #[error("a maximum frame size of zero would reject every frame, including an empty one")]
165 Zero,
166}
167
168/// A frame that could not be framed.
169///
170/// A domain error: the framing layer knows about sizes and encodings and nothing
171/// else, so nothing in it is an `io::Error` or a `serde_json::Error`, and an
172/// adapter that hits a broken pipe maps that at its own boundary instead.
173///
174/// Every field is `Copy`, so the whole error is: it is a size and an offset,
175/// and there is nothing in it worth owning twice.
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
177pub enum FramingError {
178 /// A frame exceeded the decoder's configured limit.
179 ///
180 /// Raised while the bytes are still being read, so the decoder is holding at
181 /// most `at_least` bytes of the offending frame and the peer cannot make
182 /// that number grow by declining to send a terminator.
183 #[error("frame exceeds the configured maximum of {limit}")]
184 FrameTooLarge {
185 /// The limit the caller configured.
186 limit: MaxFrameSize,
187 /// How many bytes of this frame had arrived when the refusal happened.
188 ///
189 /// A lower bound, not a length. The frame has no known length — that is
190 /// the situation — so this is what was buffered, one byte past the limit.
191 at_least: usize,
192 },
193 /// A frame's bytes were not valid UTF-8.
194 ///
195 /// Refused rather than replaced. A lossy decode would hand the next layer a
196 /// message the peer did not send, and JSON-RPC ids are compared as text, so
197 /// the corruption would surface as a routing bug rather than as an encoding
198 /// one.
199 #[error("frame is not valid UTF-8 (first invalid byte at offset {valid_up_to})")]
200 NotUtf8 {
201 /// Offset within the frame at which decoding stopped being possible.
202 valid_up_to: usize,
203 },
204}
205
206/// Reassembles newline-delimited frames from arbitrarily chopped-up bytes.
207///
208/// Feed it whatever a read returned with [`push`](Self::push), then take
209/// complete frames out with [`next_frame`](Self::next_frame) until it says
210/// `None`, which means "not yet" rather than "nothing more".
211///
212/// The cursor ([`buffered`](Self::buffered)) matters for reading these two in
213/// order. A caller that pushes without draining accumulates complete frames as
214/// well as the partial one, and the buffer is then bounded by the limit plus the
215/// chunk it was given — never by the peer's frame size. The stdio adapter reads
216/// a chunk, pushes it, and drains it before reading again.
217///
218/// # Examples
219///
220/// ```
221/// use protocol_droid::domain::{FrameDecoder, MaxFrameSize};
222///
223/// let mut decoder = FrameDecoder::new(MaxFrameSize::new(1024)?);
224/// decoder.push(b"first\nsecond\n")?;
225///
226/// assert_eq!(decoder.next_frame()?, Some("first"));
227/// assert_eq!(decoder.next_frame()?, Some("second"));
228/// assert_eq!(decoder.next_frame()?, None);
229/// assert!(decoder.is_empty());
230/// # Ok::<(), Box<dyn std::error::Error>>(())
231/// ```
232#[derive(Debug, Clone)]
233pub struct FrameDecoder {
234 limit: MaxFrameSize,
235 /// The read buffer. `0..start` is consumed; the rest is pending.
236 buffer: Vec<u8>,
237 start: usize,
238}
239
240impl FrameDecoder {
241 /// A decoder that refuses frames larger than `limit`.
242 ///
243 /// No default: see the module docs.
244 pub const fn new(limit: MaxFrameSize) -> Self {
245 Self {
246 limit,
247 buffer: Vec::new(),
248 start: 0,
249 }
250 }
251
252 /// The limit this decoder was built with.
253 pub const fn limit(&self) -> MaxFrameSize {
254 self.limit
255 }
256
257 /// Bytes read and not yet handed out, including a partially-read frame.
258 ///
259 /// Exposed so a caller — or a test — can see the decoder's own footprint. It
260 /// is bounded by the limit plus the size of the last chunk pushed, and is not
261 /// a function of how large a frame the peer chose to send.
262 pub const fn buffered(&self) -> usize {
263 self.buffer.len() - self.start
264 }
265
266 /// Whether every frame pushed so far has been taken.
267 pub const fn is_empty(&self) -> bool {
268 self.buffered() == 0
269 }
270
271 /// Adds bytes read from the peer.
272 ///
273 /// The frame in progress may be split anywhere, including inside a
274 /// multi-byte UTF-8 sequence — nothing here decodes until a frame is
275 /// complete, so a character is never truncated by a read boundary.
276 ///
277 /// # Errors
278 ///
279 /// [`FramingError::FrameTooLarge`] when the bytes after the last terminator
280 /// would push the frame in progress past the limit. The check runs before
281 /// those bytes are buffered, so the decoder's footprint is unaffected and
282 /// the decoder is left exactly as it was.
283 pub fn push(&mut self, bytes: &[u8]) -> Result<(), FramingError> {
284 // Count the frame in progress before appending, so the whole check runs
285 // against `bytes` plus what is already held and nothing is written until
286 // the scan has passed. This ordering is the type's security property: a
287 // refusal that had already appended would have allocated exactly the
288 // memory it exists to avoid.
289 let limit = self.limit.as_usize();
290 let mut frame_len = self.buffered();
291 for byte in bytes {
292 if *byte == TERMINATOR {
293 frame_len = 0;
294 } else {
295 frame_len += 1;
296 if frame_len > limit {
297 return Err(FramingError::FrameTooLarge {
298 limit: self.limit,
299 at_least: frame_len,
300 });
301 }
302 }
303 }
304
305 self.buffer.extend_from_slice(bytes);
306 Ok(())
307 }
308
309 /// Takes the next complete frame, or `None` if the terminator has not
310 /// arrived.
311 ///
312 /// The returned `&str` borrows the read buffer and is valid until the next
313 /// `push` or `next_frame`. A caller that needs to keep a frame past that
314 /// point copies it; the copy is then at the call site, where it is visible,
315 /// rather than hidden inside a type that allocates for everyone.
316 ///
317 /// # Errors
318 ///
319 /// [`FramingError::NotUtf8`] when the frame's bytes are not UTF-8. The
320 /// offending frame is dropped, so a caller that chooses to carry on reads
321 /// the *next* frame instead of being handed the same bad one for ever. A
322 /// transport will normally close on this error, because a peer that emits a
323 /// non-UTF-8 frame has said something about itself; the discard is here so
324 /// that a caller which decides to continue is not trapped in a loop.
325 pub fn next_frame(&mut self) -> Result<Option<&str>, FramingError> {
326 // Compact before borrowing: compaction can reallocate, and a borrow
327 // taken first would be invalidated by it.
328 self.compact();
329
330 let Some(pending) = self.buffer.get(self.start..) else {
331 // `start` is only ever advanced to at most `buffer.len()`, so this
332 // cannot happen. It is still handled rather than asserted, because
333 // the project denies `panic!` and `unreachable!`, and a read that
334 // cannot yield a frame is the honest answer if the invariant is
335 // ever broken.
336 return Ok(None);
337 };
338 let Some(newline) = pending.iter().position(|byte| *byte == TERMINATOR) else {
339 return Ok(None);
340 };
341
342 let end = self.start + newline;
343 let frame = self.buffer.get(self.start..end).unwrap_or_default();
344 // Past the terminator, and set before validating so that the error path
345 // below discards the offending frame as documented.
346 self.start = end + 1;
347
348 match core::str::from_utf8(frame) {
349 Ok(text) => Ok(Some(text)),
350 Err(error) => Err(FramingError::NotUtf8 {
351 valid_up_to: error.valid_up_to(),
352 }),
353 }
354 }
355
356 /// Reclaims the consumed prefix once it outweighs its own size.
357 fn compact(&mut self) {
358 if self.start == self.buffer.len() {
359 self.buffer.clear();
360 self.start = 0;
361 } else if self.start >= COMPACT_AT {
362 self.buffer.drain(..self.start);
363 self.start = 0;
364 }
365 }
366}
367
368/// Appends one frame — the document, then the terminator — to `out`.
369///
370/// The write half of the same framing, so a server and a client cannot disagree
371/// about where a frame ends. `out` is taken rather than returned so a transport
372/// writing into a reused buffer pays no allocation per frame.
373///
374/// The document is neither validated nor escaped: it is a document some codec
375/// already produced, and a frame is a container, not a parser. What *is* checked
376/// is the size, because a frame this process emits has to satisfy the same
377/// ceiling a peer's does, and learning that at the write is far better than
378/// learning it as a truncated read on the other side.
379///
380/// # Errors
381///
382/// [`FramingError::FrameTooLarge`] when `document` is longer than `limit`. The
383/// limit counts the document's bytes and not the terminator, so a document of
384/// exactly `limit` bytes is a legal frame.
385///
386/// # Examples
387///
388/// ```
389/// use protocol_droid::domain::{MaxFrameSize, encode_frame};
390///
391/// let mut wire = Vec::new();
392/// encode_frame("{\"jsonrpc\":\"2.0\"}", &mut wire, MaxFrameSize::new(64)?)?;
393///
394/// assert_eq!(wire, b"{\"jsonrpc\":\"2.0\"}\n");
395/// # Ok::<(), Box<dyn std::error::Error>>(())
396/// ```
397pub fn encode_frame(
398 document: &str,
399 out: &mut Vec<u8>,
400 limit: MaxFrameSize,
401) -> Result<(), FramingError> {
402 let len = document.len();
403 if len > limit.as_usize() {
404 return Err(FramingError::FrameTooLarge {
405 limit,
406 at_least: len,
407 });
408 }
409 out.extend_from_slice(document.as_bytes());
410 out.push(TERMINATOR);
411 Ok(())
412}
413
414#[cfg(test)]
415mod tests {
416 use assert_matches::assert_matches;
417
418 use super::{FrameDecoder, FramingError, MaxFrameSize, MaxFrameSizeError, encode_frame};
419
420 /// The `Failure` an `?` in a test can produce, so a fallible call whose error
421 /// type is not the one under test still propagates instead of unwrapping.
422 #[derive(Debug)]
423 #[expect(dead_code, reason = "the payloads are read by the derived Debug")]
424 enum Failure {
425 /// A frame the test tried to push or take was refused.
426 Framing(FramingError),
427 /// A limit the test tried to build was refused.
428 Limit(MaxFrameSizeError),
429 }
430
431 impl From<FramingError> for Failure {
432 fn from(error: FramingError) -> Self {
433 Self::Framing(error)
434 }
435 }
436
437 impl From<MaxFrameSizeError> for Failure {
438 fn from(error: MaxFrameSizeError) -> Self {
439 Self::Limit(error)
440 }
441 }
442
443 /// A decoder with a roomy limit, for the cases that are not about size.
444 fn roomy() -> Result<FrameDecoder, MaxFrameSizeError> {
445 Ok(FrameDecoder::new(MaxFrameSize::new(1024)?))
446 }
447
448 /// `u32` to `usize` for the size assertions. std has no `From<u32> for
449 /// usize` because `usize` may be narrower, so the cast is spelled once here
450 /// rather than repeated.
451 const fn limit_in_usize(limit: MaxFrameSize) -> usize {
452 limit.bytes() as usize
453 }
454
455 #[test]
456 fn one_frame_round_trips_through_encode_and_decode() -> Result<(), Failure> {
457 let document = r#"{"jsonrpc":"2.0","id":1,"result":{}}"#;
458 let mut wire = Vec::new();
459 encode_frame(document, &mut wire, MaxFrameSize::new(1024)?)?;
460
461 let mut decoder = roomy()?;
462 decoder.push(&wire)?;
463 assert_eq!(decoder.next_frame()?, Some(document));
464 assert_eq!(decoder.next_frame()?, None);
465 Ok(())
466 }
467
468 #[test]
469 fn several_frames_in_one_read_come_back_in_order() -> Result<(), Failure> {
470 let mut decoder = roomy()?;
471 decoder.push(b"one\ntwo\nthree\n")?;
472
473 for expected in ["one", "two", "three"] {
474 assert_eq!(decoder.next_frame()?, Some(expected));
475 }
476 assert_eq!(decoder.next_frame()?, None);
477 assert!(decoder.is_empty());
478 Ok(())
479 }
480
481 #[test]
482 fn several_frames_one_per_read_come_back_in_order() -> Result<(), Failure> {
483 let mut decoder = roomy()?;
484 for chunk in [b"one\n".as_slice(), b"two\n", b"three\n"] {
485 decoder.push(chunk)?;
486 }
487
488 for expected in ["one", "two", "three"] {
489 assert_eq!(decoder.next_frame()?, Some(expected));
490 }
491 assert_eq!(decoder.next_frame()?, None);
492 Ok(())
493 }
494
495 #[test]
496 fn a_frame_split_across_reads_is_reassembled() -> Result<(), Failure> {
497 let document = r#"{"jsonrpc":"2.0","method":"ping"}"#;
498 let bytes = document.as_bytes();
499 let split = document.len() / 2;
500
501 let mut decoder = roomy()?;
502 decoder.push(bytes.get(..split).unwrap_or_default())?;
503 assert_eq!(
504 decoder.next_frame()?,
505 None,
506 "a frame with no terminator is not yet a frame"
507 );
508
509 decoder.push(bytes.get(split..).unwrap_or_default())?;
510 decoder.push(b"\n")?;
511 assert_eq!(decoder.next_frame()?, Some(document));
512 Ok(())
513 }
514
515 /// Every single-byte split of a two-frame stream, which is where hand-rolled
516 /// framing usually breaks: the terminator landing alone in its own read, and
517 /// a frame boundary coinciding with one.
518 #[test]
519 fn any_single_byte_split_of_a_two_frame_stream_decodes() -> Result<(), Failure> {
520 let stream = b"{\"a\":1}\n{\"b\":2}\n";
521
522 for split in 0..=stream.len() {
523 let mut decoder = roomy()?;
524 decoder.push(stream.get(..split).unwrap_or_default())?;
525 decoder.push(stream.get(split..).unwrap_or_default())?;
526
527 assert_eq!(decoder.next_frame()?, Some(r#"{"a":1}"#), "split {split}");
528 assert_eq!(decoder.next_frame()?, Some(r#"{"b":2}"#), "split {split}");
529 assert_eq!(decoder.next_frame()?, None, "split {split}");
530 }
531 Ok(())
532 }
533
534 #[test]
535 fn a_multi_byte_character_split_across_reads_is_not_truncated() -> Result<(), Failure> {
536 // "…" is three bytes. Splitting between them is the case a decoder that
537 // validated eagerly would corrupt, and it is why validation happens on a
538 // complete frame and never on a chunk.
539 let document = r#"{"emoji":"…"}"#;
540 let bytes = document.as_bytes();
541 let split = bytes
542 .windows(3)
543 .position(|window| window == "…".as_bytes())
544 .map_or(0, |at| at + 1);
545
546 let mut decoder = roomy()?;
547 decoder.push(bytes.get(..split).unwrap_or_default())?;
548 assert_eq!(decoder.next_frame()?, None);
549
550 decoder.push(bytes.get(split..).unwrap_or_default())?;
551 decoder.push(b"\n")?;
552 assert_eq!(decoder.next_frame()?, Some(document));
553 Ok(())
554 }
555
556 #[test]
557 fn a_frame_at_exactly_the_limit_is_accepted() -> Result<(), Failure> {
558 let limit = MaxFrameSize::new(8)?;
559 let document = "12345678";
560 let mut wire = Vec::new();
561 encode_frame(document, &mut wire, limit)?;
562
563 let mut decoder = FrameDecoder::new(limit);
564 decoder.push(&wire)?;
565 assert_eq!(decoder.next_frame()?, Some(document));
566 Ok(())
567 }
568
569 #[test]
570 fn a_frame_one_byte_over_the_limit_is_refused() -> Result<(), Failure> {
571 let limit = MaxFrameSize::new(8)?;
572 let mut wire = Vec::new();
573 assert_matches!(
574 encode_frame("123456789", &mut wire, limit),
575 Err(FramingError::FrameTooLarge { limit: got, .. }) if got == limit
576 );
577 assert!(wire.is_empty(), "a refused frame must write nothing");
578 Ok(())
579 }
580
581 /// The security property, asserted rather than asserted-in-prose: a peer
582 /// that never sends a terminator is cut off at the limit, and the decoder's
583 /// footprint is a function of the limit rather than of what the peer sent.
584 #[test]
585 fn an_unterminated_frame_cannot_grow_the_buffer_past_the_limit() -> Result<(), Failure> {
586 let limit = MaxFrameSize::new(64)?;
587 let mut decoder = FrameDecoder::new(limit);
588
589 // Sixteen kilobytes of a frame that never ends: 256x the limit.
590 assert_matches!(
591 decoder.push(&vec![b'x'; 16 * 1024]),
592 Err(FramingError::FrameTooLarge { limit: got, at_least })
593 if got == limit && at_least == limit_in_usize(limit) + 1
594 );
595
596 assert!(
597 decoder.buffered() <= limit_in_usize(limit),
598 "held {} bytes for a {limit} limit",
599 decoder.buffered()
600 );
601 Ok(())
602 }
603
604 #[test]
605 fn a_refusal_leaves_the_decoder_exactly_as_it_was() -> Result<(), Failure> {
606 let limit = MaxFrameSize::new(16)?;
607 let mut decoder = FrameDecoder::new(limit);
608 decoder.push(b"short\npartial")?;
609 let before = decoder.buffered();
610
611 assert_matches!(
612 decoder.push(&[b'y'; 64]),
613 Err(FramingError::FrameTooLarge { .. })
614 );
615 assert_eq!(decoder.buffered(), before, "the refusal buffered bytes");
616 assert_eq!(decoder.next_frame()?, Some("short"));
617 Ok(())
618 }
619
620 /// The limit is per frame, not per connection: a stream of frames that are
621 /// each under the limit is a stream of legal frames however many there are.
622 #[test]
623 fn a_completed_frame_does_not_count_against_the_next_one() -> Result<(), Failure> {
624 let limit = MaxFrameSize::new(8)?;
625 let mut decoder = FrameDecoder::new(limit);
626
627 for _ in 0..100 {
628 decoder.push(b"12345678\n")?;
629 assert_eq!(decoder.next_frame()?, Some("12345678"));
630 }
631 assert!(decoder.is_empty());
632 Ok(())
633 }
634
635 #[test]
636 fn invalid_utf8_is_a_typed_error_rather_than_a_lossy_decode() -> Result<(), Failure> {
637 let mut decoder = roomy()?;
638 // 0xFF is not a legal UTF-8 byte in any position; here it is the sixth.
639 decoder.push(b"{\"a\":\"\xff\"}\n")?;
640
641 assert_matches!(
642 decoder.next_frame(),
643 Err(FramingError::NotUtf8 { valid_up_to: 6 })
644 );
645
646 // The bad frame was dropped, so a caller that continues is not trapped.
647 decoder.push(b"next\n")?;
648 assert_eq!(decoder.next_frame()?, Some("next"));
649 Ok(())
650 }
651
652 #[test]
653 fn an_empty_frame_is_a_frame_rather_than_a_skipped_line() -> Result<(), Failure> {
654 let mut decoder = roomy()?;
655 decoder.push(b"\n\n")?;
656
657 assert_eq!(decoder.next_frame()?, Some(""));
658 assert_eq!(decoder.next_frame()?, Some(""));
659 assert_eq!(decoder.next_frame()?, None);
660 Ok(())
661 }
662
663 /// A `\r` is content. Trimming it would mean the decoder edits a peer's
664 /// bytes, and the bytes the peer sent are the bytes the bridge forwards.
665 #[test]
666 fn a_carriage_return_is_kept_as_content() -> Result<(), Failure> {
667 let mut decoder = roomy()?;
668 decoder.push(b"{\"a\":1}\r\n")?;
669 assert_eq!(decoder.next_frame()?, Some("{\"a\":1}\r"));
670 Ok(())
671 }
672
673 #[test]
674 fn a_carriage_return_before_a_newline_counts_against_the_limit() -> Result<(), Failure> {
675 let limit = MaxFrameSize::new(4)?;
676 let mut decoder = FrameDecoder::new(limit);
677 assert_matches!(
678 decoder.push(b"1234\r"),
679 Err(FramingError::FrameTooLarge { .. })
680 );
681 Ok(())
682 }
683
684 #[test]
685 fn the_limit_is_configurable_and_readable_back() -> Result<(), Failure> {
686 let limit = MaxFrameSize::new(4096)?;
687 let decoder = FrameDecoder::new(limit);
688 assert_eq!(decoder.limit(), limit);
689 assert_eq!(decoder.limit().bytes(), 4096);
690 Ok(())
691 }
692
693 #[test]
694 fn a_zero_limit_is_refused_at_construction() {
695 assert_eq!(MaxFrameSize::new(0), Err(MaxFrameSizeError::Zero));
696 assert!(MaxFrameSize::new(1).is_ok());
697 }
698
699 #[test]
700 fn the_default_limit_is_an_ordinary_value_rather_than_a_fallback() {
701 // The point of the type having no `Default`: a decoder is always built
702 // from a limit somebody chose. `DEFAULT` is available, and it is nothing
703 // more than a value.
704 assert_eq!(MaxFrameSize::DEFAULT.bytes(), 4 * 1024 * 1024);
705 }
706
707 /// The buffer is compacted rather than growing to hold a whole connection's
708 /// history. `COMPACT_AT` is private, so the test drives past it by pushing a
709 /// great many frames and then checks the footprint is back to the live
710 /// bytes.
711 #[test]
712 fn a_long_connection_does_not_retain_every_frame_it_has_seen() -> Result<(), Failure> {
713 let limit = MaxFrameSize::new(64)?;
714 let mut decoder = FrameDecoder::new(limit);
715
716 for _ in 0..4096 {
717 decoder.push(b"a-frame\n")?;
718 assert_eq!(decoder.next_frame()?, Some("a-frame"));
719 }
720 assert!(
721 decoder.buffered() <= limit_in_usize(limit),
722 "retained {} bytes of history",
723 decoder.buffered()
724 );
725 assert!(decoder.is_empty());
726 Ok(())
727 }
728}