Skip to main content

wire_codec/framing/
checksummed.rs

1//! Checksum-trailer framing.
2//!
3//! [`Checksummed`] wraps any [`Framer`] and appends a fixed-size checksum
4//! after each inner frame:
5//!
6//! ```text
7//! +----------------------------+----------------+
8//! | inner frame (header,       | checksum       |
9//! | payload, delimiter, ...)   | (C::LEN bytes) |
10//! +----------------------------+----------------+
11//! ```
12//!
13//! The trailer sits outside the inner frame, so the inner framer is used
14//! unchanged and nothing has to be copied or buffered on either path. For a
15//! length-prefixed inner framer the prefix counts the payload only, which is
16//! the layout of many serial and embedded protocols.
17
18use crate::buf::WriteBuf;
19use crate::checksum::Checksum;
20use crate::error::{Error, Result};
21use crate::framing::{Endian, Frame, Framer};
22
23/// Which bytes the checksum covers.
24///
25/// # Example
26///
27/// ```
28/// use wire_codec::checksum::Crc16CcittFalse;
29/// use wire_codec::framing::{Checksummed, ChecksumScope, Endian, LengthPrefixed, LengthWidth};
30///
31/// let inner = LengthPrefixed::new(LengthWidth::U8, Endian::Big);
32/// let framer = Checksummed::new(inner, Crc16CcittFalse, Endian::Big)
33///     .with_scope(ChecksumScope::Payload);
34/// assert_eq!(framer.scope(), ChecksumScope::Payload);
35/// ```
36#[non_exhaustive]
37#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
38pub enum ChecksumScope {
39    /// Every byte of the inner frame: header, payload, and delimiter. A
40    /// corrupted length prefix is caught as a checksum mismatch.
41    #[default]
42    Frame,
43    /// The payload bytes only.
44    Payload,
45}
46
47/// A framer decorator that appends and verifies a checksum trailer.
48///
49/// # Example
50///
51/// ```
52/// use wire_codec::checksum::Crc32;
53/// use wire_codec::framing::{Checksummed, Endian, Framer, LengthPrefixed, LengthWidth};
54/// use wire_codec::{Error, WriteBuf};
55///
56/// let inner = LengthPrefixed::bounded(LengthWidth::U16, Endian::Big);
57/// let framer = Checksummed::new(inner, Crc32, Endian::Little);
58///
59/// let mut out = [0u8; 32];
60/// let mut buf = WriteBuf::new(&mut out);
61/// framer.write_frame(b"ping", &mut buf).unwrap();
62/// let n = buf.position();
63/// assert_eq!(n, 2 + 4 + 4); // prefix + payload + CRC-32
64///
65/// let frame = framer.next_frame(&out[..n]).unwrap().unwrap();
66/// assert_eq!(frame.payload(), b"ping");
67/// assert_eq!(frame.consumed(), n);
68///
69/// out[3] ^= 0x01; // flip one payload bit in transit
70/// assert_eq!(framer.next_frame(&out[..n]), Err(Error::ChecksumMismatch));
71/// ```
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub struct Checksummed<F, C> {
74    inner: F,
75    checksum: C,
76    endian: Endian,
77    scope: ChecksumScope,
78}
79
80impl<F: Framer, C: Checksum> Checksummed<F, C> {
81    /// Wrap `inner`, appending `checksum` in `endian` byte order. The
82    /// checksum covers the whole inner frame ([`ChecksumScope::Frame`]).
83    #[inline]
84    #[must_use]
85    pub const fn new(inner: F, checksum: C, endian: Endian) -> Self {
86        const {
87            assert!(C::LEN >= 1 && C::LEN <= 8, "Checksum::LEN must be in 1..=8");
88        }
89        Self {
90            inner,
91            checksum,
92            endian,
93            scope: ChecksumScope::Frame,
94        }
95    }
96
97    /// Choose which bytes the checksum covers.
98    #[inline]
99    #[must_use]
100    pub const fn with_scope(mut self, scope: ChecksumScope) -> Self {
101        self.scope = scope;
102        self
103    }
104
105    /// The wrapped framer.
106    #[inline]
107    #[must_use]
108    pub const fn inner(&self) -> &F {
109        &self.inner
110    }
111
112    /// The configured checksum.
113    #[inline]
114    #[must_use]
115    pub const fn checksum(&self) -> &C {
116        &self.checksum
117    }
118
119    /// Byte order of the trailer.
120    #[inline]
121    #[must_use]
122    pub const fn endian(&self) -> Endian {
123        self.endian
124    }
125
126    /// Which bytes the checksum covers.
127    #[inline]
128    #[must_use]
129    pub const fn scope(&self) -> ChecksumScope {
130        self.scope
131    }
132
133    fn trailer(&self, value: u64) -> [u8; 8] {
134        let mut out = [0u8; 8];
135        match self.endian {
136            Endian::Big => out[..C::LEN].copy_from_slice(&value.to_be_bytes()[8 - C::LEN..]),
137            Endian::Little => out[..C::LEN].copy_from_slice(&value.to_le_bytes()[..C::LEN]),
138        }
139        out
140    }
141
142    fn verify<'a>(&self, input: &'a [u8], frame: Frame<'a>) -> Result<Option<Frame<'a>>> {
143        let start = frame.consumed();
144        // Saturating: a misbehaving custom inner framer could report a
145        // `consumed` past the input; `get` then simply finds no trailer.
146        let end = start.saturating_add(C::LEN);
147        let Some(received) = input.get(start..end) else {
148            return Ok(None);
149        };
150        let covered = match self.scope {
151            ChecksumScope::Frame => &input[..start],
152            ChecksumScope::Payload => frame.payload(),
153        };
154        let expected = self.trailer(self.checksum.compute(covered));
155        if expected[..C::LEN] != *received {
156            return Err(Error::ChecksumMismatch);
157        }
158        Ok(Some(Frame::new(frame.payload(), end)))
159    }
160}
161
162impl<F: Framer, C: Checksum> Framer for Checksummed<F, C> {
163    /// Extract an inner frame and verify the trailer that follows it.
164    ///
165    /// # Errors
166    ///
167    /// - [`Error::ChecksumMismatch`] if the trailer does not match.
168    /// - Any error from the inner framer.
169    #[inline]
170    fn next_frame<'a>(&self, input: &'a [u8]) -> Result<Option<Frame<'a>>> {
171        match self.inner.next_frame(input)? {
172            Some(frame) => self.verify(input, frame),
173            None => Ok(None),
174        }
175    }
176
177    /// Write the inner frame followed by the checksum trailer.
178    ///
179    /// # Errors
180    ///
181    /// - [`Error::BufferFull`] if `out` cannot hold the frame and trailer.
182    ///   When the inner framer reports [`Framer::max_frame_len`] (every
183    ///   built-in framer does, exactly), that bound plus the trailer is
184    ///   checked before anything is written.
185    /// - Any error from the inner framer.
186    ///
187    /// Because the room is checked first, a short buffer is reported as
188    /// [`Error::BufferFull`] even when the inner framer would also reject
189    /// the payload (with [`Error::FrameTooLarge`] or
190    /// [`Error::DelimiterInPayload`], say); given enough room, the inner
191    /// framer's error is returned instead.
192    ///
193    /// The cursor is unchanged on error. With a built-in inner framer
194    /// nothing is written either. With a custom inner framer that reports no
195    /// size bound, a trailer that does not fit can leave the inner frame's
196    /// bytes behind the (restored) cursor.
197    fn write_frame(&self, payload: &[u8], out: &mut WriteBuf<'_>) -> Result<()> {
198        if let Some(inner_max) = self.inner.max_frame_len(payload.len()) {
199            if inner_max.saturating_add(C::LEN) > out.remaining() {
200                return Err(Error::BufferFull);
201            }
202        }
203        let start = out.position();
204        self.inner.write_frame(payload, out)?;
205        let covered = match self.scope {
206            ChecksumScope::Frame => &out.written()[start..],
207            ChecksumScope::Payload => payload,
208        };
209        let trailer = self.trailer(self.checksum.compute(covered));
210        if let Err(e) = out.write_bytes(&trailer[..C::LEN]) {
211            out.rewind(start);
212            return Err(e);
213        }
214        Ok(())
215    }
216
217    /// Resumable variant: forwards the progress hint to the inner framer.
218    ///
219    /// # Errors
220    ///
221    /// Same contract as [`Framer::next_frame`] for this type.
222    #[inline]
223    fn next_frame_from<'a>(
224        &self,
225        input: &'a [u8],
226        scanned: &mut usize,
227    ) -> Result<Option<Frame<'a>>> {
228        match self.inner.next_frame_from(input, scanned)? {
229            Some(frame) => self.verify(input, frame),
230            None => Ok(None),
231        }
232    }
233
234    #[inline]
235    fn max_frame_len(&self, payload_len: usize) -> Option<usize> {
236        self.inner.max_frame_len(payload_len)?.checked_add(C::LEN)
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use crate::checksum::{Crc16CcittFalse, Crc32, Crc32c, crc16_ccitt_false};
244    use crate::framing::{Delimited, LengthPrefixed, LengthWidth, StreamDecoder, VarintPrefixed};
245
246    #[test]
247    fn trailer_byte_order_and_coverage() {
248        let inner = LengthPrefixed::new(LengthWidth::U8, Endian::Big);
249        let mut out = [0u8; 16];
250
251        let framer = Checksummed::new(inner, Crc16CcittFalse, Endian::Big);
252        let mut buf = WriteBuf::new(&mut out);
253        framer.write_frame(b"ab", &mut buf).unwrap();
254        let crc = crc16_ccitt_false(&[0x02, b'a', b'b']).to_be_bytes();
255        assert_eq!(buf.written(), &[0x02, b'a', b'b', crc[0], crc[1]]);
256
257        let framer = framer.with_scope(ChecksumScope::Payload);
258        let mut buf = WriteBuf::new(&mut out);
259        framer.write_frame(b"ab", &mut buf).unwrap();
260        let crc = crc16_ccitt_false(b"ab").to_be_bytes();
261        assert_eq!(buf.written(), &[0x02, b'a', b'b', crc[0], crc[1]]);
262        let frame = framer.next_frame(&out[..5]).unwrap().unwrap();
263        assert_eq!(frame.payload(), b"ab");
264
265        let framer = Checksummed::new(inner, Crc16CcittFalse, Endian::Little);
266        let mut buf = WriteBuf::new(&mut out);
267        framer.write_frame(b"ab", &mut buf).unwrap();
268        let crc = crc16_ccitt_false(&[0x02, b'a', b'b']).to_le_bytes();
269        assert_eq!(&buf.written()[3..], &crc);
270    }
271
272    #[test]
273    fn corrupted_header_is_detected_with_frame_scope() {
274        let inner = LengthPrefixed::new(LengthWidth::U8, Endian::Big);
275        let framer = Checksummed::new(inner, Crc32, Endian::Big);
276        let mut out = [0u8; 16];
277        let mut buf = WriteBuf::new(&mut out);
278        framer.write_frame(b"abc", &mut buf).unwrap();
279        let n = buf.position();
280        out[0] = 2; // length 3 -> 2: the inner frame still parses
281        assert_eq!(framer.next_frame(&out[..n]), Err(Error::ChecksumMismatch));
282    }
283
284    #[test]
285    fn incomplete_trailer_needs_more_input() {
286        let framer = Checksummed::new(VarintPrefixed::new(), Crc32c, Endian::Big);
287        let mut out = [0u8; 16];
288        let mut buf = WriteBuf::new(&mut out);
289        framer.write_frame(b"xyz", &mut buf).unwrap();
290        let n = buf.position();
291        assert_eq!(Some(n), framer.max_frame_len(3));
292        for cut in 0..n {
293            assert_eq!(framer.next_frame(&out[..cut]), Ok(None), "cut={cut}");
294        }
295        assert_eq!(framer.next_frame(&out[..n]).unwrap().unwrap().consumed(), n);
296    }
297
298    #[test]
299    fn write_is_atomic_when_trailer_does_not_fit() {
300        let inner = LengthPrefixed::new(LengthWidth::U8, Endian::Big);
301        let framer = Checksummed::new(inner, Crc32, Endian::Big);
302        // Room for the inner frame (4 bytes) but not the 4-byte trailer.
303        let mut out = [0xEEu8; 6];
304        let mut buf = WriteBuf::new(&mut out);
305        assert_eq!(framer.write_frame(b"abc", &mut buf), Err(Error::BufferFull));
306        assert_eq!(buf.position(), 0);
307        assert_eq!(out, [0xEE; 6], "nothing is written on error");
308        let mut big = [0u8; 16];
309        let mut buf = WriteBuf::new(&mut big);
310        // Inner framer errors propagate unchanged.
311        let framer = Checksummed::new(inner.with_max_payload(1), Crc32, Endian::Big);
312        assert!(matches!(
313            framer.write_frame(b"abc", &mut buf),
314            Err(Error::FrameTooLarge { .. })
315        ));
316    }
317
318    #[test]
319    fn delimited_inner_streams_linearly() {
320        let inner = Delimited::bounded(b"\n").unwrap();
321        let framer = Checksummed::new(inner, Crc16CcittFalse, Endian::Big);
322        let mut wire = [0u8; 64];
323        let mut buf = WriteBuf::new(&mut wire);
324        framer.write_frame(b"hello", &mut buf).unwrap();
325        framer.write_frame(b"world", &mut buf).unwrap();
326        let n = buf.position();
327
328        let mut decoder = StreamDecoder::new(framer);
329        let mut start = 0;
330        let mut frames = Vec::new();
331        for end in 1..=n {
332            while let Some(frame) = decoder.decode(&wire[start..end]).unwrap() {
333                frames.push(frame.payload().to_vec());
334                start += frame.consumed();
335            }
336        }
337        assert_eq!(frames, [b"hello".to_vec(), b"world".to_vec()]);
338        assert_eq!(start, n);
339        assert_eq!(decoder.framer().inner().delimiter(), b"\n");
340        assert_eq!(decoder.framer().checksum(), &Crc16CcittFalse);
341        assert_eq!(decoder.framer().endian(), Endian::Big);
342        assert_eq!(decoder.framer().scope(), ChecksumScope::Frame);
343    }
344
345    #[test]
346    fn default_scope_is_frame() {
347        assert_eq!(ChecksumScope::default(), ChecksumScope::Frame);
348    }
349}