Skip to main content

wire_codec/framing/
delimiter.rs

1//! Byte-delimited framing.
2//!
3//! Frames are separated by a configured delimiter byte sequence (typically
4//! `\n` for newline-terminated text, or `\r\n` for HTTP-style protocols). The
5//! delimiter is stripped from the emitted payload but counted toward
6//! [`Frame::consumed`].
7
8use crate::buf::WriteBuf;
9use crate::error::{Error, Result};
10use crate::framing::{Frame, Framer};
11
12/// Byte-delimited framer.
13///
14/// # Example
15///
16/// ```
17/// use wire_codec::WriteBuf;
18/// use wire_codec::framing::{Delimited, Framer};
19///
20/// let framer = Delimited::new(b"\n");
21///
22/// let mut out = [0u8; 32];
23/// let mut buf = WriteBuf::new(&mut out);
24/// framer.write_frame(b"line one", &mut buf).unwrap();
25/// framer.write_frame(b"line two", &mut buf).unwrap();
26/// let n = buf.position();
27/// assert_eq!(&out[..n], b"line one\nline two\n");
28///
29/// let first = framer.next_frame(&out[..n]).unwrap().unwrap();
30/// assert_eq!(first.payload(), b"line one");
31/// assert_eq!(first.consumed(), 9);
32/// ```
33#[derive(Debug, Clone, Copy)]
34pub struct Delimited<'d> {
35    delimiter: &'d [u8],
36    max_payload: usize,
37}
38
39impl<'d> Delimited<'d> {
40    /// Build a framer using `delimiter` to separate frames.
41    ///
42    /// # Panics
43    ///
44    /// Panics if `delimiter` is empty. An empty delimiter cannot uniquely
45    /// separate frames and is a programmer error.
46    #[inline]
47    pub const fn new(delimiter: &'d [u8]) -> Self {
48        // Compile-time guard against the empty-delimiter footgun.
49        assert!(!delimiter.is_empty(), "delimiter must be non-empty");
50        Self {
51            delimiter,
52            max_payload: usize::MAX,
53        }
54    }
55
56    /// Set an upper bound on payload size. Frames whose payload would exceed
57    /// this size cause [`Error::FrameTooLarge`].
58    #[inline]
59    #[must_use]
60    pub const fn with_max_payload(mut self, max: usize) -> Self {
61        self.max_payload = max;
62        self
63    }
64
65    /// Configured delimiter sequence.
66    #[inline]
67    pub const fn delimiter(&self) -> &'d [u8] {
68        self.delimiter
69    }
70
71    /// Configured maximum payload size.
72    #[inline]
73    pub const fn max_payload(&self) -> usize {
74        self.max_payload
75    }
76
77    fn find_delimiter(&self, haystack: &[u8]) -> Option<usize> {
78        let needle = self.delimiter;
79        if haystack.len() < needle.len() {
80            return None;
81        }
82        let last = haystack.len() - needle.len();
83        let mut i = 0;
84        while i <= last {
85            if &haystack[i..i + needle.len()] == needle {
86                return Some(i);
87            }
88            i += 1;
89        }
90        None
91    }
92}
93
94impl<'d> Framer for Delimited<'d> {
95    fn next_frame<'a>(&self, input: &'a [u8]) -> Result<Option<Frame<'a>>> {
96        // Cap the search window at max_payload + delimiter length. Saturating
97        // because the default max_payload is usize::MAX.
98        let window_cap = self.max_payload.saturating_add(self.delimiter.len());
99        let scan_window = if input.len() > window_cap {
100            &input[..window_cap]
101        } else {
102            input
103        };
104        match self.find_delimiter(scan_window) {
105            Some(pos) => {
106                if pos > self.max_payload {
107                    return Err(Error::FrameTooLarge {
108                        len: pos,
109                        limit: self.max_payload,
110                    });
111                }
112                let consumed = pos + self.delimiter.len();
113                Ok(Some(Frame::new(&input[..pos], consumed)))
114            }
115            None => {
116                // No delimiter inside the window, and the input has already
117                // exceeded the limit: the protocol is broken.
118                if input.len() > window_cap {
119                    return Err(Error::FrameTooLarge {
120                        len: input.len(),
121                        limit: self.max_payload,
122                    });
123                }
124                Ok(None)
125            }
126        }
127    }
128
129    fn write_frame(&self, payload: &[u8], out: &mut WriteBuf<'_>) -> Result<()> {
130        if payload.len() > self.max_payload {
131            return Err(Error::FrameTooLarge {
132                len: payload.len(),
133                limit: self.max_payload,
134            });
135        }
136        out.write_bytes(payload)?;
137        out.write_bytes(self.delimiter)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn newline_round_trip() {
147        let framer = Delimited::new(b"\n");
148        let mut storage = [0u8; 32];
149        let mut buf = WriteBuf::new(&mut storage);
150        framer.write_frame(b"hello", &mut buf).unwrap();
151        let n = buf.position();
152        assert_eq!(&storage[..n], b"hello\n");
153
154        let frame = framer.next_frame(&storage[..n]).unwrap().unwrap();
155        assert_eq!(frame.payload(), b"hello");
156        assert_eq!(frame.consumed(), 6);
157    }
158
159    #[test]
160    fn multi_byte_delimiter() {
161        let framer = Delimited::new(b"\r\n");
162        let frame = framer.next_frame(b"GET /\r\nrest").unwrap().unwrap();
163        assert_eq!(frame.payload(), b"GET /");
164        assert_eq!(frame.consumed(), 7);
165    }
166
167    #[test]
168    fn no_delimiter_returns_none() {
169        let framer = Delimited::new(b"\n");
170        assert_eq!(framer.next_frame(b"no terminator here").unwrap(), None);
171    }
172
173    #[test]
174    fn exceeds_max_payload_short_circuits_search() {
175        // Input is 6 bytes; max_payload=3 limits the scan window to 4 bytes
176        // (3 + delimiter length). The delimiter is beyond the window, so the
177        // framer bails with FrameTooLarge instead of scanning to the end.
178        let framer = Delimited::new(b"\n").with_max_payload(3);
179        let result = framer.next_frame(b"abcde\n");
180        assert!(matches!(result, Err(Error::FrameTooLarge { limit: 3, .. })));
181    }
182
183    #[test]
184    fn empty_payload_is_valid() {
185        let framer = Delimited::new(b"\n");
186        let frame = framer.next_frame(b"\nrest").unwrap().unwrap();
187        assert_eq!(frame.payload(), b"");
188        assert_eq!(frame.consumed(), 1);
189    }
190}