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").unwrap();
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, PartialEq, Eq, Hash)]
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    /// # Errors
43    ///
44    /// Returns [`Error::EmptyDelimiter`] if `delimiter` is empty. An empty
45    /// delimiter cannot uniquely separate frames.
46    #[inline]
47    pub const fn new(delimiter: &'d [u8]) -> Result<Self> {
48        if delimiter.is_empty() {
49            return Err(Error::EmptyDelimiter);
50        }
51        Ok(Self {
52            delimiter,
53            max_payload: usize::MAX,
54        })
55    }
56
57    /// Set an upper bound on payload size. Frames whose payload would exceed
58    /// this size cause [`Error::FrameTooLarge`].
59    #[inline]
60    #[must_use]
61    pub const fn with_max_payload(mut self, max: usize) -> Self {
62        self.max_payload = max;
63        self
64    }
65
66    /// Configured delimiter sequence.
67    #[inline]
68    pub const fn delimiter(&self) -> &'d [u8] {
69        self.delimiter
70    }
71
72    /// Configured maximum payload size.
73    #[inline]
74    pub const fn max_payload(&self) -> usize {
75        self.max_payload
76    }
77
78    fn find_delimiter(&self, haystack: &[u8]) -> Option<usize> {
79        let needle = self.delimiter;
80        if haystack.len() < needle.len() {
81            return None;
82        }
83        let last = haystack.len() - needle.len();
84        let mut i = 0;
85        while i <= last {
86            if &haystack[i..i + needle.len()] == needle {
87                return Some(i);
88            }
89            i += 1;
90        }
91        None
92    }
93}
94
95impl<'d> Framer for Delimited<'d> {
96    fn next_frame<'a>(&self, input: &'a [u8]) -> Result<Option<Frame<'a>>> {
97        // Cap the search window at max_payload + delimiter length. Saturating
98        // because the default max_payload is usize::MAX.
99        let window_cap = self.max_payload.saturating_add(self.delimiter.len());
100        let scan_window = if input.len() > window_cap {
101            &input[..window_cap]
102        } else {
103            input
104        };
105        match self.find_delimiter(scan_window) {
106            Some(pos) => {
107                if pos > self.max_payload {
108                    return Err(Error::FrameTooLarge {
109                        len: pos,
110                        limit: self.max_payload,
111                    });
112                }
113                let consumed = pos + self.delimiter.len();
114                Ok(Some(Frame::new(&input[..pos], consumed)))
115            }
116            None => {
117                // No delimiter inside the window, and the input has already
118                // exceeded the limit: the protocol is broken.
119                if input.len() > window_cap {
120                    return Err(Error::FrameTooLarge {
121                        len: input.len(),
122                        limit: self.max_payload,
123                    });
124                }
125                Ok(None)
126            }
127        }
128    }
129
130    fn write_frame(&self, payload: &[u8], out: &mut WriteBuf<'_>) -> Result<()> {
131        if payload.len() > self.max_payload {
132            return Err(Error::FrameTooLarge {
133                len: payload.len(),
134                limit: self.max_payload,
135            });
136        }
137        out.write_bytes(payload)?;
138        out.write_bytes(self.delimiter)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn newline_round_trip() {
148        let framer = Delimited::new(b"\n").unwrap();
149        let mut storage = [0u8; 32];
150        let mut buf = WriteBuf::new(&mut storage);
151        framer.write_frame(b"hello", &mut buf).unwrap();
152        let n = buf.position();
153        assert_eq!(&storage[..n], b"hello\n");
154
155        let frame = framer.next_frame(&storage[..n]).unwrap().unwrap();
156        assert_eq!(frame.payload(), b"hello");
157        assert_eq!(frame.consumed(), 6);
158    }
159
160    #[test]
161    fn multi_byte_delimiter() {
162        let framer = Delimited::new(b"\r\n").unwrap();
163        let frame = framer.next_frame(b"GET /\r\nrest").unwrap().unwrap();
164        assert_eq!(frame.payload(), b"GET /");
165        assert_eq!(frame.consumed(), 7);
166    }
167
168    #[test]
169    fn no_delimiter_returns_none() {
170        let framer = Delimited::new(b"\n").unwrap();
171        assert_eq!(framer.next_frame(b"no terminator here").unwrap(), None);
172    }
173
174    #[test]
175    fn exceeds_max_payload_short_circuits_search() {
176        // Input is 6 bytes; max_payload=3 limits the scan window to 4 bytes
177        // (3 + delimiter length). The delimiter is beyond the window, so the
178        // framer bails with FrameTooLarge instead of scanning to the end.
179        let framer = Delimited::new(b"\n").unwrap().with_max_payload(3);
180        let result = framer.next_frame(b"abcde\n");
181        assert!(matches!(result, Err(Error::FrameTooLarge { limit: 3, .. })));
182    }
183
184    #[test]
185    fn empty_payload_is_valid() {
186        let framer = Delimited::new(b"\n").unwrap();
187        let frame = framer.next_frame(b"\nrest").unwrap().unwrap();
188        assert_eq!(frame.payload(), b"");
189        assert_eq!(frame.consumed(), 1);
190    }
191
192    #[test]
193    fn empty_delimiter_rejected() {
194        let result = Delimited::new(b"");
195        assert!(matches!(result, Err(Error::EmptyDelimiter)));
196    }
197}