use crate::buf::WriteBuf;
use crate::checksum::Checksum;
use crate::error::{Error, Result};
use crate::framing::{Endian, Frame, Framer};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum ChecksumScope {
#[default]
Frame,
Payload,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Checksummed<F, C> {
inner: F,
checksum: C,
endian: Endian,
scope: ChecksumScope,
}
impl<F: Framer, C: Checksum> Checksummed<F, C> {
#[inline]
#[must_use]
pub const fn new(inner: F, checksum: C, endian: Endian) -> Self {
const {
assert!(C::LEN >= 1 && C::LEN <= 8, "Checksum::LEN must be in 1..=8");
}
Self {
inner,
checksum,
endian,
scope: ChecksumScope::Frame,
}
}
#[inline]
#[must_use]
pub const fn with_scope(mut self, scope: ChecksumScope) -> Self {
self.scope = scope;
self
}
#[inline]
#[must_use]
pub const fn inner(&self) -> &F {
&self.inner
}
#[inline]
#[must_use]
pub const fn checksum(&self) -> &C {
&self.checksum
}
#[inline]
#[must_use]
pub const fn endian(&self) -> Endian {
self.endian
}
#[inline]
#[must_use]
pub const fn scope(&self) -> ChecksumScope {
self.scope
}
fn trailer(&self, value: u64) -> [u8; 8] {
let mut out = [0u8; 8];
match self.endian {
Endian::Big => out[..C::LEN].copy_from_slice(&value.to_be_bytes()[8 - C::LEN..]),
Endian::Little => out[..C::LEN].copy_from_slice(&value.to_le_bytes()[..C::LEN]),
}
out
}
fn verify<'a>(&self, input: &'a [u8], frame: Frame<'a>) -> Result<Option<Frame<'a>>> {
let start = frame.consumed();
let end = start.saturating_add(C::LEN);
let Some(received) = input.get(start..end) else {
return Ok(None);
};
let covered = match self.scope {
ChecksumScope::Frame => &input[..start],
ChecksumScope::Payload => frame.payload(),
};
let expected = self.trailer(self.checksum.compute(covered));
if expected[..C::LEN] != *received {
return Err(Error::ChecksumMismatch);
}
Ok(Some(Frame::new(frame.payload(), end)))
}
}
impl<F: Framer, C: Checksum> Framer for Checksummed<F, C> {
#[inline]
fn next_frame<'a>(&self, input: &'a [u8]) -> Result<Option<Frame<'a>>> {
match self.inner.next_frame(input)? {
Some(frame) => self.verify(input, frame),
None => Ok(None),
}
}
fn write_frame(&self, payload: &[u8], out: &mut WriteBuf<'_>) -> Result<()> {
if let Some(inner_max) = self.inner.max_frame_len(payload.len()) {
if inner_max.saturating_add(C::LEN) > out.remaining() {
return Err(Error::BufferFull);
}
}
let start = out.position();
self.inner.write_frame(payload, out)?;
let covered = match self.scope {
ChecksumScope::Frame => &out.written()[start..],
ChecksumScope::Payload => payload,
};
let trailer = self.trailer(self.checksum.compute(covered));
if let Err(e) = out.write_bytes(&trailer[..C::LEN]) {
out.rewind(start);
return Err(e);
}
Ok(())
}
#[inline]
fn next_frame_from<'a>(
&self,
input: &'a [u8],
scanned: &mut usize,
) -> Result<Option<Frame<'a>>> {
match self.inner.next_frame_from(input, scanned)? {
Some(frame) => self.verify(input, frame),
None => Ok(None),
}
}
#[inline]
fn max_frame_len(&self, payload_len: usize) -> Option<usize> {
self.inner.max_frame_len(payload_len)?.checked_add(C::LEN)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::checksum::{Crc16CcittFalse, Crc32, Crc32c, crc16_ccitt_false};
use crate::framing::{Delimited, LengthPrefixed, LengthWidth, StreamDecoder, VarintPrefixed};
#[test]
fn trailer_byte_order_and_coverage() {
let inner = LengthPrefixed::new(LengthWidth::U8, Endian::Big);
let mut out = [0u8; 16];
let framer = Checksummed::new(inner, Crc16CcittFalse, Endian::Big);
let mut buf = WriteBuf::new(&mut out);
framer.write_frame(b"ab", &mut buf).unwrap();
let crc = crc16_ccitt_false(&[0x02, b'a', b'b']).to_be_bytes();
assert_eq!(buf.written(), &[0x02, b'a', b'b', crc[0], crc[1]]);
let framer = framer.with_scope(ChecksumScope::Payload);
let mut buf = WriteBuf::new(&mut out);
framer.write_frame(b"ab", &mut buf).unwrap();
let crc = crc16_ccitt_false(b"ab").to_be_bytes();
assert_eq!(buf.written(), &[0x02, b'a', b'b', crc[0], crc[1]]);
let frame = framer.next_frame(&out[..5]).unwrap().unwrap();
assert_eq!(frame.payload(), b"ab");
let framer = Checksummed::new(inner, Crc16CcittFalse, Endian::Little);
let mut buf = WriteBuf::new(&mut out);
framer.write_frame(b"ab", &mut buf).unwrap();
let crc = crc16_ccitt_false(&[0x02, b'a', b'b']).to_le_bytes();
assert_eq!(&buf.written()[3..], &crc);
}
#[test]
fn corrupted_header_is_detected_with_frame_scope() {
let inner = LengthPrefixed::new(LengthWidth::U8, Endian::Big);
let framer = Checksummed::new(inner, Crc32, Endian::Big);
let mut out = [0u8; 16];
let mut buf = WriteBuf::new(&mut out);
framer.write_frame(b"abc", &mut buf).unwrap();
let n = buf.position();
out[0] = 2; assert_eq!(framer.next_frame(&out[..n]), Err(Error::ChecksumMismatch));
}
#[test]
fn incomplete_trailer_needs_more_input() {
let framer = Checksummed::new(VarintPrefixed::new(), Crc32c, Endian::Big);
let mut out = [0u8; 16];
let mut buf = WriteBuf::new(&mut out);
framer.write_frame(b"xyz", &mut buf).unwrap();
let n = buf.position();
assert_eq!(Some(n), framer.max_frame_len(3));
for cut in 0..n {
assert_eq!(framer.next_frame(&out[..cut]), Ok(None), "cut={cut}");
}
assert_eq!(framer.next_frame(&out[..n]).unwrap().unwrap().consumed(), n);
}
#[test]
fn write_is_atomic_when_trailer_does_not_fit() {
let inner = LengthPrefixed::new(LengthWidth::U8, Endian::Big);
let framer = Checksummed::new(inner, Crc32, Endian::Big);
let mut out = [0xEEu8; 6];
let mut buf = WriteBuf::new(&mut out);
assert_eq!(framer.write_frame(b"abc", &mut buf), Err(Error::BufferFull));
assert_eq!(buf.position(), 0);
assert_eq!(out, [0xEE; 6], "nothing is written on error");
let mut big = [0u8; 16];
let mut buf = WriteBuf::new(&mut big);
let framer = Checksummed::new(inner.with_max_payload(1), Crc32, Endian::Big);
assert!(matches!(
framer.write_frame(b"abc", &mut buf),
Err(Error::FrameTooLarge { .. })
));
}
#[test]
fn delimited_inner_streams_linearly() {
let inner = Delimited::bounded(b"\n").unwrap();
let framer = Checksummed::new(inner, Crc16CcittFalse, Endian::Big);
let mut wire = [0u8; 64];
let mut buf = WriteBuf::new(&mut wire);
framer.write_frame(b"hello", &mut buf).unwrap();
framer.write_frame(b"world", &mut buf).unwrap();
let n = buf.position();
let mut decoder = StreamDecoder::new(framer);
let mut start = 0;
let mut frames = Vec::new();
for end in 1..=n {
while let Some(frame) = decoder.decode(&wire[start..end]).unwrap() {
frames.push(frame.payload().to_vec());
start += frame.consumed();
}
}
assert_eq!(frames, [b"hello".to_vec(), b"world".to_vec()]);
assert_eq!(start, n);
assert_eq!(decoder.framer().inner().delimiter(), b"\n");
assert_eq!(decoder.framer().checksum(), &Crc16CcittFalse);
assert_eq!(decoder.framer().endian(), Endian::Big);
assert_eq!(decoder.framer().scope(), ChecksumScope::Frame);
}
#[test]
fn default_scope_is_frame() {
assert_eq!(ChecksumScope::default(), ChecksumScope::Frame);
}
}