Skip to main content

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}