use crate::buf::WriteBuf;
use crate::error::{Error, Result};
use crate::framing::search;
use crate::framing::{Frame, Framer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Delimited<'d> {
delimiter: &'d [u8],
max_payload: usize,
}
impl<'d> Delimited<'d> {
pub const DEFAULT_MAX_PAYLOAD: usize = 64 * 1024;
#[inline]
pub const fn new(delimiter: &'d [u8]) -> Result<Self> {
if delimiter.is_empty() {
return Err(Error::EmptyDelimiter);
}
Ok(Self {
delimiter,
max_payload: usize::MAX,
})
}
#[inline]
pub const fn bounded(delimiter: &'d [u8]) -> Result<Self> {
match Self::new(delimiter) {
Ok(framer) => Ok(framer.with_max_payload(Self::DEFAULT_MAX_PAYLOAD)),
Err(e) => Err(e),
}
}
#[inline]
#[must_use]
pub const fn with_max_payload(mut self, max: usize) -> Self {
self.max_payload = max;
self
}
#[inline]
#[must_use]
pub const fn delimiter(&self) -> &'d [u8] {
self.delimiter
}
#[inline]
#[must_use]
pub const fn max_payload(&self) -> usize {
self.max_payload
}
fn would_split(&self, payload: &[u8]) -> bool {
let delim = self.delimiter;
if search::find(payload, delim, 0).is_some() {
return true;
}
let m = delim.len();
(1..m.min(payload.len() + 1))
.any(|k| payload[payload.len() - k..] == delim[..k] && delim[k..] == delim[..m - k])
}
}
impl Framer for Delimited<'_> {
#[inline]
fn next_frame<'a>(&self, input: &'a [u8]) -> Result<Option<Frame<'a>>> {
self.next_frame_from(input, &mut 0)
}
fn write_frame(&self, payload: &[u8], out: &mut WriteBuf<'_>) -> Result<()> {
if payload.len() > self.max_payload {
return Err(Error::FrameTooLarge {
len: payload.len(),
limit: self.max_payload,
});
}
if self.would_split(payload) {
return Err(Error::DelimiterInPayload);
}
let total = payload
.len()
.checked_add(self.delimiter.len())
.ok_or(Error::BufferFull)?;
if total > out.remaining() {
return Err(Error::BufferFull);
}
out.write_bytes(payload)?;
out.write_bytes(self.delimiter)
}
fn next_frame_from<'a>(
&self,
input: &'a [u8],
scanned: &mut usize,
) -> Result<Option<Frame<'a>>> {
let delim_len = self.delimiter.len();
let window_cap = self.max_payload.saturating_add(delim_len);
let window = if input.len() > window_cap {
&input[..window_cap]
} else {
input
};
let start = (*scanned).min(window.len());
if let Some(pos) = search::find(window, self.delimiter, start) {
*scanned = 0;
return Ok(Some(Frame::new(&input[..pos], pos + delim_len)));
}
if input.len() > window_cap {
return Err(Error::FrameTooLarge {
len: self.max_payload.saturating_add(1),
limit: self.max_payload,
});
}
*scanned = window.len().saturating_sub(delim_len - 1);
Ok(None)
}
#[inline]
fn max_frame_len(&self, payload_len: usize) -> Option<usize> {
payload_len.checked_add(self.delimiter.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::framing::StreamDecoder;
#[test]
fn newline_round_trip() {
let framer = Delimited::new(b"\n").unwrap();
let mut storage = [0u8; 32];
let mut buf = WriteBuf::new(&mut storage);
framer.write_frame(b"hello", &mut buf).unwrap();
let n = buf.position();
assert_eq!(&storage[..n], b"hello\n");
let frame = framer.next_frame(&storage[..n]).unwrap().unwrap();
assert_eq!(frame.payload(), b"hello");
assert_eq!(frame.consumed(), 6);
}
#[test]
fn multi_byte_delimiter() {
let framer = Delimited::new(b"\r\n").unwrap();
let frame = framer.next_frame(b"GET /\r\nrest").unwrap().unwrap();
assert_eq!(frame.payload(), b"GET /");
assert_eq!(frame.consumed(), 7);
}
#[test]
fn overlapping_delimiter_prefix() {
let framer = Delimited::new(b"\r\n").unwrap();
let frame = framer.next_frame(b"ab\r\r\ncd").unwrap().unwrap();
assert_eq!(frame.payload(), b"ab\r");
assert_eq!(frame.consumed(), 5);
}
#[test]
fn no_delimiter_returns_none() {
let framer = Delimited::new(b"\n").unwrap();
assert_eq!(framer.next_frame(b"no terminator here").unwrap(), None);
}
#[test]
fn exceeds_max_payload_short_circuits_search() {
let framer = Delimited::new(b"\n").unwrap().with_max_payload(3);
let result = framer.next_frame(b"abcde\n");
assert_eq!(result, Err(Error::FrameTooLarge { len: 4, limit: 3 }));
}
#[test]
fn read_limit_boundaries() {
let framer = Delimited::new(b"\r\n").unwrap().with_max_payload(3);
let frame = framer.next_frame(b"abc\r\n").unwrap().unwrap();
assert_eq!(frame.payload(), b"abc");
assert_eq!(framer.next_frame(b"abc\r"), Ok(None));
assert_eq!(framer.next_frame(b"abcd\r"), Ok(None));
assert_eq!(
framer.next_frame(b"abcd\r\n"),
Err(Error::FrameTooLarge { len: 4, limit: 3 })
);
}
#[test]
fn write_limit_boundaries() {
let framer = Delimited::new(b"\n").unwrap().with_max_payload(3);
let mut storage = [0u8; 16];
let mut buf = WriteBuf::new(&mut storage);
framer.write_frame(b"abc", &mut buf).unwrap();
assert_eq!(
framer.write_frame(b"abcd", &mut buf),
Err(Error::FrameTooLarge { len: 4, limit: 3 })
);
assert_eq!(buf.written(), b"abc\n");
}
#[test]
fn write_rejects_delimiter_inside_payload() {
let framer = Delimited::new(b"\r\n").unwrap();
let mut storage = [0u8; 16];
let mut buf = WriteBuf::new(&mut storage);
assert_eq!(
framer.write_frame(b"a\r\nb", &mut buf),
Err(Error::DelimiterInPayload)
);
assert_eq!(buf.position(), 0, "nothing is written on error");
framer.write_frame(b"a\rb\nc", &mut buf).unwrap();
let n = buf.position();
let frame = framer.next_frame(&storage[..n]).unwrap().unwrap();
assert_eq!(frame.payload(), b"a\rb\nc");
assert_eq!(frame.consumed(), n);
}
#[test]
fn write_rejects_payloads_that_merge_with_a_self_overlapping_delimiter() {
let mut storage = [0u8; 16];
let cases: [(&[u8], &[u8], bool); 7] = [
(b"aa", b"a", true), (b"aa", b"ba", true), (b"aa", b"ab", false), (b"aba", b"xab", true),
(b"aba", b"xa", false), (b"\r\n", b"x\r", false),
(b"abab", b"xab", true),
];
for (delim, payload, rejected) in cases {
let framer = Delimited::new(delim).unwrap();
let mut buf = WriteBuf::new(&mut storage);
let result = framer.write_frame(payload, &mut buf);
if rejected {
assert_eq!(
result,
Err(Error::DelimiterInPayload),
"{delim:?} {payload:?}"
);
} else {
result.unwrap();
let n = buf.position();
let frame = framer.next_frame(&storage[..n]).unwrap().unwrap();
assert_eq!(frame.payload(), payload, "{delim:?}");
assert_eq!(frame.consumed(), n);
}
}
}
#[test]
fn write_is_atomic_on_buffer_full() {
let framer = Delimited::new(b"\r\n").unwrap();
let mut storage = [0u8; 4];
let mut buf = WriteBuf::new(&mut storage);
assert_eq!(framer.write_frame(b"abc", &mut buf), Err(Error::BufferFull));
assert_eq!(buf.position(), 0);
framer.write_frame(b"ab", &mut buf).unwrap();
assert_eq!(buf.position(), 4);
}
#[test]
fn empty_payload_is_valid() {
let framer = Delimited::new(b"\n").unwrap();
let frame = framer.next_frame(b"\nrest").unwrap().unwrap();
assert_eq!(frame.payload(), b"");
assert_eq!(frame.consumed(), 1);
}
#[test]
fn empty_delimiter_rejected() {
assert!(matches!(Delimited::new(b""), Err(Error::EmptyDelimiter)));
assert!(matches!(
Delimited::bounded(b""),
Err(Error::EmptyDelimiter)
));
}
#[test]
fn bounded_constructor_and_max_frame_len() {
let framer = Delimited::bounded(b"\r\n").unwrap();
assert_eq!(framer.max_payload(), Delimited::DEFAULT_MAX_PAYLOAD);
assert_eq!(framer.max_frame_len(10), Some(12));
assert_eq!(framer.max_frame_len(usize::MAX), None);
}
#[test]
fn resumable_scan_handles_split_delimiter() {
let framer = Delimited::new(b"\r\n").unwrap();
let mut scanned = 0;
assert_eq!(framer.next_frame_from(b"abc\r", &mut scanned), Ok(None));
assert_eq!(scanned, 3);
let frame = framer
.next_frame_from(b"abc\r\n", &mut scanned)
.unwrap()
.unwrap();
assert_eq!(frame.payload(), b"abc");
assert_eq!(scanned, 0);
}
#[test]
fn resumable_scan_clamps_oversized_hint() {
let framer = Delimited::new(b"\n").unwrap();
let mut scanned = 1_000;
assert_eq!(framer.next_frame_from(b"ab", &mut scanned), Ok(None));
assert_eq!(scanned, 2);
}
#[test]
fn byte_at_a_time_stream_does_linear_work() {
let framer = Delimited::new(b"\r\n").unwrap();
let mut decoder = StreamDecoder::new(framer);
let mut line = vec![b'x'; 256 * 1024];
line.extend_from_slice(b"\r\n");
let mut examined = 0usize;
let mut frames = 0;
for end in 1..=line.len() {
let before = decoder.scanned;
match decoder.decode(&line[..end]).unwrap() {
Some(frame) => {
assert_eq!(frame.payload().len(), 256 * 1024);
frames += 1;
}
None => examined += end - before,
}
}
assert_eq!(frames, 1);
assert!(examined <= 2 * line.len(), "examined {examined} bytes");
}
}