Skip to main content

mctp_rs/medium/
serial.rs

1//! DSP0253 byte-stuffed serial medium for MCTP.
2//!
3//! Two-layer split:
4//!   - [`SerialEncoding`]: stateless byte-stuffing (0x7E, 0x7D escape pair).
5//!   - [`MctpSerialMedium`]: framing (revision byte, byte_count, body, FCS-16, end-flag).
6//!
7//! Both layers are gated behind the `serial` cargo feature.
8
9use crate::{
10    MctpPacketError,
11    buffer_encoding::{BufferEncoding, DecodeError, EncodeError, EncodingDecoder, EncodingEncoder},
12    error::MctpPacketResult,
13    medium::{MctpMedium, MctpMediumFrame},
14};
15
16/// DSP0253 byte-stuffing transform. Stateless ZST.
17///
18/// Encode: `0x7E -> [0x7D, 0x5E]`, `0x7D -> [0x7D, 0x5D]`, any other
19/// byte -> `[b]`.
20/// Decode: `0x7D 0x5E -> 0x7E`, `0x7D 0x5D -> 0x7D`, `0x7D <other>` ->
21/// `InvalidEscape`.
22///
23/// Raw `0x7E` in the wire stream is NOT rejected here — that's a
24/// framing concern owned by `MctpSerialMedium::deserialize`, which
25/// checks the body region for stray flags.
26#[derive(Debug, Copy, Clone, PartialEq, Eq)]
27#[cfg_attr(feature = "defmt", derive(defmt::Format))]
28pub struct SerialEncoding;
29
30impl BufferEncoding for SerialEncoding {
31    fn write_byte(wire_buf: &mut [u8], byte: u8) -> Result<usize, EncodeError> {
32        match byte {
33            0x7E => {
34                if wire_buf.len() < 2 {
35                    return Err(EncodeError::BufferFull);
36                }
37                wire_buf[0] = 0x7D;
38                wire_buf[1] = 0x5E;
39                Ok(2)
40            }
41            0x7D => {
42                if wire_buf.len() < 2 {
43                    return Err(EncodeError::BufferFull);
44                }
45                wire_buf[0] = 0x7D;
46                wire_buf[1] = 0x5D;
47                Ok(2)
48            }
49            b => match wire_buf.first_mut() {
50                Some(slot) => {
51                    *slot = b;
52                    Ok(1)
53                }
54                None => Err(EncodeError::BufferFull),
55            },
56        }
57    }
58
59    fn read_byte(wire_buf: &[u8]) -> Result<(u8, usize), DecodeError> {
60        match wire_buf.first().copied() {
61            None => Err(DecodeError::PrematureEnd),
62            Some(0x7D) => match wire_buf.get(1).copied() {
63                None => Err(DecodeError::PrematureEnd),
64                Some(0x5E) => Ok((0x7E, 2)),
65                Some(0x5D) => Ok((0x7D, 2)),
66                Some(_) => Err(DecodeError::InvalidEscape),
67            },
68            // Raw 0x7E falls through here as a 1-byte read; the framing
69            // layer (`MctpSerialMedium::deserialize`) rejects bare
70            // 0x7E inside the body region.
71            Some(b) => Ok((b, 1)),
72        }
73    }
74
75    fn wire_size_of(decoded: &[u8]) -> usize {
76        decoded
77            .iter()
78            .map(|&b| if b == 0x7E || b == 0x7D { 2 } else { 1 })
79            .sum()
80    }
81}
82
83/// SP MCTP endpoint id per CONTEXT D-D-06.
84pub const SP_EID: crate::endpoint_id::EndpointId = crate::endpoint_id::EndpointId::Id(0x08);
85/// EC MCTP endpoint id per CONTEXT D-D-06.
86pub const EC_EID: crate::endpoint_id::EndpointId = crate::endpoint_id::EndpointId::Id(0x0A);
87/// Maximum DSP0253 packet body size (DECODED bytes, before stuffing).
88pub const CONST_MTU: usize = 251;
89
90const SERIAL_REVISION: u8 = 0x01;
91const END_FLAG: u8 = 0x7E;
92/// Header bytes: revision + byte_count (decoded body byte count).
93const HEADER_LEN: usize = 2;
94/// Worst-case trailer wire bytes: 2 stuffed FCS bytes (each may
95/// expand 1 -> 2) + 1 end-flag.
96const MAX_TRAILER_WIRE: usize = 5;
97
98// CRC-16/X-25 per DSP0253 §8 (poly 0x1021, init 0xFFFF, refin/refout,
99// xorout 0xFFFF). Algorithm catalog entry locked in CONTEXT D-D-02.
100// FCS bytes on the wire are MSB-first per DSP0253 §5.2 (overrides
101// RFC1662's LSB-first PPP convention).
102const FCS_ALGO: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
103
104#[derive(Debug, Copy, Clone, PartialEq, Eq)]
105#[cfg_attr(feature = "defmt", derive(defmt::Format))]
106pub struct MctpSerialMediumFrame {
107    pub revision: u8,
108    /// DECODED body byte count per DSP0253 §6.2 (NOT the wire byte
109    /// count). Cap = `CONST_MTU` = 251; max u8 = 255, fits comfortably.
110    pub byte_count: u8,
111    pub fcs: u16,
112}
113
114impl MctpMediumFrame<MctpSerialMedium> for MctpSerialMediumFrame {
115    fn packet_size(&self) -> usize {
116        // packet_size is the DECODED body byte count — the contract
117        // used by `MctpPacketContext::deserialize_packet`, which then
118        // subtracts 4 for the transport header.
119        self.byte_count as usize
120    }
121
122    fn reply_context(&self) {}
123}
124
125#[derive(Debug, Copy, Clone, PartialEq, Eq)]
126#[cfg_attr(feature = "defmt", derive(defmt::Format))]
127pub struct MctpSerialMedium;
128
129impl MctpMedium for MctpSerialMedium {
130    type Frame = MctpSerialMediumFrame;
131    type Error = &'static str;
132    type ReplyContext = ();
133    type Encoding = SerialEncoding;
134
135    fn max_message_body_size(&self) -> usize {
136        CONST_MTU
137    }
138
139    fn deserialize<'buf>(
140        &self,
141        packet: &'buf [u8],
142    ) -> MctpPacketResult<(Self::Frame, EncodingDecoder<'buf, Self::Encoding>), Self> {
143        // Minimum frame: 2 header + 0 body + 2 FCS (unstuffed) + 1 end-flag = 5 bytes.
144        if packet.len() < HEADER_LEN + 3 {
145            return Err(MctpPacketError::MediumError(
146                "packet too short for serial frame",
147            ));
148        }
149        let revision = packet[0];
150        if revision != SERIAL_REVISION {
151            return Err(MctpPacketError::MediumError("unsupported serial revision"));
152        }
153        let byte_count = packet[1];
154        if (byte_count as usize) > CONST_MTU {
155            return Err(MctpPacketError::MediumError("byte_count exceeds MTU"));
156        }
157
158        // Single forward walk: un-stuff body bytes (count must equal
159        // `byte_count`), un-stuff 2 FCS bytes, expect end-flag, compare
160        // CRC.
161        let body_wire_start = HEADER_LEN;
162        let mut decoded = [0u8; CONST_MTU];
163        let mut decoded_len = 0usize;
164        let mut wire_pos = 0usize; // offset from body_wire_start
165
166        while decoded_len < byte_count as usize {
167            let (b, n) =
168                SerialEncoding::read_byte(&packet[body_wire_start + wire_pos..]).map_err(|e| {
169                    match e {
170                        DecodeError::PrematureEnd => {
171                            MctpPacketError::MediumError("premature end in body")
172                        }
173                        DecodeError::InvalidEscape => {
174                            MctpPacketError::MediumError("invalid escape in body")
175                        }
176                    }
177                })?;
178            if b == END_FLAG && n == 1 {
179                // Bare (unstuffed) 0x7E inside the body region is a
180                // protocol error (MEDIUM-05). A decoded 0x7E whose wire
181                // representation was the stuffed pair `0x7D 0x5E`
182                // (n==2) is a legitimate payload byte and is kept.
183                return Err(MctpPacketError::MediumError("unexpected 0x7E in body"));
184            }
185            decoded[decoded_len] = b;
186            decoded_len += 1;
187            wire_pos += n;
188        }
189        let body_wire_end = body_wire_start + wire_pos;
190
191        // Un-stuff 2 FCS bytes (DSP0253 §7.1 stuffing applies to FCS).
192        let (fcs_msb, n_msb) = SerialEncoding::read_byte(&packet[body_wire_end..])
193            .map_err(|_| MctpPacketError::MediumError("invalid escape in fcs"))?;
194        let (fcs_lsb, n_lsb) = SerialEncoding::read_byte(&packet[body_wire_end + n_msb..])
195            .map_err(|_| MctpPacketError::MediumError("invalid escape in fcs"))?;
196        let trailer_pos = body_wire_end + n_msb + n_lsb;
197
198        if trailer_pos >= packet.len() || packet[trailer_pos] != END_FLAG {
199            return Err(MctpPacketError::MediumError("missing end flag"));
200        }
201        if trailer_pos + 1 != packet.len() {
202            return Err(MctpPacketError::MediumError(
203                "trailing bytes after end flag",
204            ));
205        }
206
207        // FCS-16/X-25 over un-stuffed (revision || byte_count || decoded body).
208        let mut digest = FCS_ALGO.digest();
209        digest.update(&[revision, byte_count]);
210        digest.update(&decoded[..decoded_len]);
211        let computed_fcs = digest.finalize();
212        // DSP0253 §5.2: MSB first on wire.
213        let wire_fcs = u16::from_be_bytes([fcs_msb, fcs_lsb]);
214        if wire_fcs != computed_fcs {
215            return Err(MctpPacketError::MediumError("fcs mismatch"));
216        }
217
218        Ok((
219            MctpSerialMediumFrame {
220                revision,
221                byte_count,
222                fcs: wire_fcs,
223            },
224            EncodingDecoder::<Self::Encoding>::new(&packet[body_wire_start..body_wire_end]),
225        ))
226    }
227
228    fn serialize<'buf, F>(
229        &self,
230        _reply_context: Self::ReplyContext,
231        buffer: &'buf mut [u8],
232        message_writer: F,
233    ) -> MctpPacketResult<&'buf [u8], Self>
234    where
235        F: for<'a> FnOnce(&mut EncodingEncoder<'a, Self::Encoding>) -> MctpPacketResult<(), Self>,
236    {
237        if buffer.len() < HEADER_LEN + MAX_TRAILER_WIRE {
238            return Err(MctpPacketError::MediumError(
239                "buffer too small for serial frame",
240            ));
241        }
242        let buffer_len = buffer.len();
243
244        // Run closure over body region (reserve worst-case 5-byte
245        // trailer). The encoder stuffs body bytes via
246        // `SerialEncoding::write_byte` automatically.
247        let body_wire_len = {
248            let body_buf = &mut buffer[HEADER_LEN..buffer_len - MAX_TRAILER_WIRE];
249            let mut encoder = EncodingEncoder::<Self::Encoding>::new(body_buf);
250            message_writer(&mut encoder)?;
251            encoder.wire_position()
252        };
253
254        // Re-decode body to recover DECODED bytes + decoded count for
255        // `byte_count` and FCS. CONTEXT D-B-02 acknowledges the
256        // double-walk; ~250 bytes max, no_std, cheap.
257        let mut decoded = [0u8; CONST_MTU];
258        let mut decoded_len = 0usize;
259        let mut wire_pos = 0usize;
260        while wire_pos < body_wire_len {
261            let (b, n) = SerialEncoding::read_byte(
262                &buffer[HEADER_LEN + wire_pos..HEADER_LEN + body_wire_len],
263            )
264            .map_err(|_| MctpPacketError::MediumError("internal: failed to re-decode body"))?;
265            if decoded_len >= CONST_MTU {
266                return Err(MctpPacketError::MediumError("body exceeds MTU"));
267            }
268            decoded[decoded_len] = b;
269            decoded_len += 1;
270            wire_pos += n;
271        }
272        // Should not fire — `EncodingEncoder::write` returns
273        // `BufferFull` long before decoded_len could exceed 251.
274        if decoded_len > u8::MAX as usize {
275            return Err(MctpPacketError::MediumError(
276                "body exceeds byte_count u8 cap",
277            ));
278        }
279        let byte_count = decoded_len as u8;
280
281        // FCS-16/X-25 over un-stuffed (revision || byte_count || decoded body).
282        let mut digest = FCS_ALGO.digest();
283        digest.update(&[SERIAL_REVISION, byte_count]);
284        digest.update(&decoded[..decoded_len]);
285        let fcs = digest.finalize();
286        // DSP0253 §5.2: MSB first on wire.
287        let [fcs_msb, fcs_lsb] = fcs.to_be_bytes();
288
289        // Header: revision + byte_count emitted directly (NOT stuffed),
290        // matching `SmbusEspiMedium`'s header pattern. See PLAN
291        // <behavior> note for the conformance caveat when byte_count
292        // happens to equal 0x7E or 0x7D — round-trips cleanly through
293        // this implementation's deserialize.
294        buffer[0] = SERIAL_REVISION;
295        buffer[1] = byte_count;
296
297        // Stuff and write FCS bytes via SerialEncoding (DSP0253 §7.1 +
298        // CONTEXT D-B-02 — deserialize un-stuffs FCS, so serialize
299        // must stuff).
300        let fcs_start = HEADER_LEN + body_wire_len;
301        let n_msb = SerialEncoding::write_byte(&mut buffer[fcs_start..], fcs_msb)
302            .map_err(|_| MctpPacketError::MediumError("internal: failed to encode fcs"))?;
303        let n_lsb = SerialEncoding::write_byte(&mut buffer[fcs_start + n_msb..], fcs_lsb)
304            .map_err(|_| MctpPacketError::MediumError("internal: failed to encode fcs"))?;
305        let end_pos = fcs_start + n_msb + n_lsb;
306
307        // End-flag is written directly (flags are NOT stuffed by
308        // definition).
309        buffer[end_pos] = END_FLAG;
310
311        Ok(&buffer[..end_pos + 1])
312    }
313}
314
315#[cfg(test)]
316mod encoding_tests {
317    use super::*;
318    use crate::buffer_encoding::EncodingDecoder;
319
320    #[test]
321    fn write_byte_stuffs_7e() {
322        let mut buf = [0u8; 4];
323        let n = SerialEncoding::write_byte(&mut buf, 0x7E).unwrap();
324        assert_eq!(n, 2);
325        assert_eq!(&buf[..2], &[0x7D, 0x5E]);
326    }
327
328    #[test]
329    fn write_byte_stuffs_7d() {
330        let mut buf = [0u8; 4];
331        let n = SerialEncoding::write_byte(&mut buf, 0x7D).unwrap();
332        assert_eq!(n, 2);
333        assert_eq!(&buf[..2], &[0x7D, 0x5D]);
334    }
335
336    #[test]
337    fn write_byte_passthrough_plain() {
338        let mut buf = [0u8; 1];
339        let n = SerialEncoding::write_byte(&mut buf, 0x41).unwrap();
340        assert_eq!(n, 1);
341        assert_eq!(buf, [0x41]);
342    }
343
344    #[test]
345    fn write_byte_full_buffer_plain() {
346        let mut buf = [];
347        assert_eq!(
348            SerialEncoding::write_byte(&mut buf, 0x41).unwrap_err(),
349            EncodeError::BufferFull
350        );
351    }
352
353    #[test]
354    fn write_byte_full_buffer_escape() {
355        let mut buf = [0u8; 1];
356        assert_eq!(
357            SerialEncoding::write_byte(&mut buf, 0x7E).unwrap_err(),
358            EncodeError::BufferFull
359        );
360    }
361
362    #[test]
363    fn read_byte_unstuffs_7e() {
364        assert_eq!(SerialEncoding::read_byte(&[0x7D, 0x5E]).unwrap(), (0x7E, 2));
365    }
366
367    #[test]
368    fn read_byte_unstuffs_7d() {
369        assert_eq!(SerialEncoding::read_byte(&[0x7D, 0x5D]).unwrap(), (0x7D, 2));
370    }
371
372    #[test]
373    fn read_byte_passthrough_plain() {
374        assert_eq!(SerialEncoding::read_byte(&[0x41]).unwrap(), (0x41, 1));
375    }
376
377    #[test]
378    fn read_byte_raw_7e_passes_through() {
379        // Raw 0x7E is NOT rejected at the encoding layer — framing is
380        // the framing layer's concern.
381        assert_eq!(SerialEncoding::read_byte(&[0x7E]).unwrap(), (0x7E, 1));
382    }
383
384    #[test]
385    fn read_byte_premature_end_empty() {
386        assert_eq!(
387            SerialEncoding::read_byte(&[]).unwrap_err(),
388            DecodeError::PrematureEnd
389        );
390    }
391
392    #[test]
393    fn read_byte_premature_end_after_escape() {
394        assert_eq!(
395            SerialEncoding::read_byte(&[0x7D]).unwrap_err(),
396            DecodeError::PrematureEnd
397        );
398    }
399
400    #[test]
401    fn read_byte_invalid_escape() {
402        assert_eq!(
403            SerialEncoding::read_byte(&[0x7D, 0xAA]).unwrap_err(),
404            DecodeError::InvalidEscape
405        );
406    }
407
408    #[test]
409    fn wire_size_of_mixed() {
410        assert_eq!(
411            SerialEncoding::wire_size_of(&[0x41, 0x7E, 0x42, 0x7D, 0x43]),
412            7
413        );
414    }
415
416    #[test]
417    fn wire_size_of_empty() {
418        assert_eq!(SerialEncoding::wire_size_of(&[]), 0);
419    }
420
421    #[test]
422    fn roundtrip_all_byte_values() {
423        // 256-byte payload of every byte value, encoded into a 512-byte
424        // wire buffer (worst case is 2x expansion if every byte stuffs;
425        // actual expansion here is 256 + 2 = 258 wire bytes).
426        let mut decoded = [0u8; 256];
427        for (i, slot) in decoded.iter_mut().enumerate() {
428            *slot = i as u8;
429        }
430        let mut wire = [0u8; 512];
431        let mut wpos = 0usize;
432        for &b in &decoded {
433            wpos += SerialEncoding::write_byte(&mut wire[wpos..], b).unwrap();
434        }
435        assert_eq!(wpos, SerialEncoding::wire_size_of(&decoded));
436        let mut dec = EncodingDecoder::<SerialEncoding>::new(&wire[..wpos]);
437        for &expected in &decoded {
438            assert_eq!(dec.read().unwrap(), expected);
439        }
440        assert_eq!(dec.read().unwrap_err(), DecodeError::PrematureEnd);
441    }
442}
443
444#[cfg(test)]
445mod fixtures {
446    //! Hand-authored DSP0253 serial frame fixtures (golden vectors).
447    //!
448    //! Layout per fixture (no leading flag — this implementation omits
449    //! the open `0x7E` per CONTEXT D-D-01; upstream UART layer supplies
450    //! it in Phase 27):
451    //!
452    //!   `[REVISION=0x01, byte_count, ...stuffed body..., ...stuffed FCS-MSB..., ...stuffed
453    //! FCS-LSB..., 0x7E]`
454    //!
455    //! - Header bytes (REVISION, byte_count) are NOT stuffed (matches production serialize).
456    //! - Body bytes are stuffed per `SerialEncoding`.
457    //! - FCS-16/X-25 computed over un-stuffed `[REVISION, byte_count, ...decoded body...]`, emitted
458    //!   MSB-first on wire (DSP0253 §5.2), each FCS byte then stuffed if equal to 0x7E or 0x7D.
459    //! - Trailing `0x7E` is the end-flag (not stuffed by definition).
460
461    pub(crate) const FIXTURE_BASIC_RX: &[u8] =
462        &[0x01, 0x04, 0xAA, 0xBB, 0xCC, 0xDD, 0x6D, 0xA1, 0x7E];
463
464    pub(crate) const FIXTURE_PAYLOAD_CONTAINS_7E: &[u8] =
465        &[0x01, 0x03, 0xAA, 0x7D, 0x5E, 0xCC, 0xFB, 0xE7, 0x7E];
466
467    pub(crate) const FIXTURE_PAYLOAD_CONTAINS_7D: &[u8] =
468        &[0x01, 0x03, 0xAA, 0x7D, 0x5D, 0xCC, 0xD1, 0x8F, 0x7E];
469
470    pub(crate) const FIXTURE_PAYLOAD_CONTAINS_BOTH: &[u8] =
471        &[0x01, 0x03, 0x7D, 0x5E, 0x7D, 0x5D, 0x42, 0x50, 0x97, 0x7E];
472
473    /// 251-byte body `(0..251)` decoded; wire = 258 bytes after stuffing
474    /// the lone 0x7D (idx 125) and 0x7E (idx 126) inside the body.
475    pub(crate) const FIXTURE_MAX_MTU_FRAME: &[u8] = &[
476        0x01, 0xFB, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
477        0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B,
478        0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A,
479        0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
480        0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
481        0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
482        0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
483        0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75,
484        0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x5D, 0x7D, 0x5E, 0x7F, 0x80, 0x81, 0x82,
485        0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91,
486        0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0,
487        0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
488        0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE,
489        0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD,
490        0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC,
491        0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB,
492        0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA,
493        0xF6, 0x07, 0x7E,
494    ];
495
496    pub(crate) const FIXTURE_EMPTY_PAYLOAD: &[u8] = &[0x01, 0x00, 0x16, 0x9F, 0x7E];
497
498    pub(crate) const FIXTURE_FCS_VALID: &[u8] = &[0x01, 0x03, 0x10, 0x20, 0x30, 0x76, 0xDB, 0x7E];
499
500    /// Same body as FCS_VALID but FCS-MSB byte XOR 0xFF (0x76 -> 0x89).
501    pub(crate) const FIXTURE_FCS_INVALID: &[u8] = &[0x01, 0x03, 0x10, 0x20, 0x30, 0x89, 0xDB, 0x7E];
502
503    /// byte_count=2 claims 2 decoded bytes; first body wire byte is
504    /// `0x7D 0xAA` (escape followed by non-`{0x5E,0x5D}`) -> rejected
505    /// as "invalid escape in body" before reaching FCS.
506    pub(crate) const FIXTURE_INVALID_ESCAPE: &[u8] = &[0x01, 0x02, 0x7D, 0xAA, 0x00, 0x00, 0x7E];
507
508    /// byte_count=3, body wire region is `[0xAA, 0x7E, 0xCC]` — the
509    /// raw 0x7E inside the body region is rejected before FCS.
510    pub(crate) const FIXTURE_PREMATURE_END_FLAG: &[u8] =
511        &[0x01, 0x03, 0xAA, 0x7E, 0xCC, 0x00, 0x00, 0x7E];
512}
513
514#[cfg(test)]
515mod medium_tests {
516    use super::{fixtures::*, *};
517
518    fn drain_decoder(mut dec: EncodingDecoder<'_, SerialEncoding>) -> ([u8; CONST_MTU], usize) {
519        let mut out = [0u8; CONST_MTU];
520        let mut n = 0;
521        while let Ok(b) = dec.read() {
522            out[n] = b;
523            n += 1;
524        }
525        (out, n)
526    }
527
528    #[test]
529    fn decode_basic_rx_succeeds() {
530        let (frame, dec) = MctpSerialMedium.deserialize(FIXTURE_BASIC_RX).unwrap();
531        assert_eq!(frame.revision, 0x01);
532        assert_eq!(frame.byte_count, 4);
533        assert_eq!(frame.fcs, 0x6DA1);
534        let (decoded, n) = drain_decoder(dec);
535        assert_eq!(&decoded[..n], &[0xAA, 0xBB, 0xCC, 0xDD]);
536    }
537
538    #[test]
539    fn decode_payload_contains_7e() {
540        let (frame, dec) = MctpSerialMedium
541            .deserialize(FIXTURE_PAYLOAD_CONTAINS_7E)
542            .unwrap();
543        assert_eq!(frame.byte_count, 3);
544        let (decoded, n) = drain_decoder(dec);
545        assert_eq!(&decoded[..n], &[0xAA, 0x7E, 0xCC]);
546    }
547
548    #[test]
549    fn decode_payload_contains_7d() {
550        let (frame, dec) = MctpSerialMedium
551            .deserialize(FIXTURE_PAYLOAD_CONTAINS_7D)
552            .unwrap();
553        assert_eq!(frame.byte_count, 3);
554        let (decoded, n) = drain_decoder(dec);
555        assert_eq!(&decoded[..n], &[0xAA, 0x7D, 0xCC]);
556    }
557
558    #[test]
559    fn decode_payload_contains_both() {
560        let (frame, dec) = MctpSerialMedium
561            .deserialize(FIXTURE_PAYLOAD_CONTAINS_BOTH)
562            .unwrap();
563        assert_eq!(frame.byte_count, 3);
564        let (decoded, n) = drain_decoder(dec);
565        assert_eq!(&decoded[..n], &[0x7E, 0x7D, 0x42]);
566    }
567
568    #[test]
569    fn decode_max_mtu_frame() {
570        let (frame, dec) = MctpSerialMedium.deserialize(FIXTURE_MAX_MTU_FRAME).unwrap();
571        assert_eq!(frame.byte_count as usize, CONST_MTU);
572        let (decoded, n) = drain_decoder(dec);
573        assert_eq!(n, CONST_MTU);
574        for (i, &b) in decoded[..n].iter().enumerate() {
575            assert_eq!(b, i as u8, "mismatch at idx {i}");
576        }
577    }
578
579    #[test]
580    fn decode_empty_payload() {
581        let (frame, dec) = MctpSerialMedium.deserialize(FIXTURE_EMPTY_PAYLOAD).unwrap();
582        assert_eq!(frame.byte_count, 0);
583        let (_, n) = drain_decoder(dec);
584        assert_eq!(n, 0);
585    }
586
587    #[test]
588    fn decode_fcs_valid() {
589        assert!(MctpSerialMedium.deserialize(FIXTURE_FCS_VALID).is_ok());
590    }
591
592    #[test]
593    fn decode_fcs_invalid_rejects() {
594        match MctpSerialMedium.deserialize(FIXTURE_FCS_INVALID) {
595            Err(crate::MctpPacketError::MediumError("fcs mismatch")) => {}
596            other => panic!(
597                "expected MediumError(\"fcs mismatch\"), got {:?}",
598                other.err()
599            ),
600        }
601    }
602
603    #[test]
604    fn decode_invalid_escape_rejects() {
605        match MctpSerialMedium.deserialize(FIXTURE_INVALID_ESCAPE) {
606            Err(crate::MctpPacketError::MediumError("invalid escape in body")) => {}
607            other => panic!(
608                "expected MediumError(\"invalid escape in body\"), got {:?}",
609                other.err()
610            ),
611        }
612    }
613
614    #[test]
615    fn decode_premature_end_flag_rejects() {
616        match MctpSerialMedium.deserialize(FIXTURE_PREMATURE_END_FLAG) {
617            Err(crate::MctpPacketError::MediumError("unexpected 0x7E in body")) => {}
618            other => panic!(
619                "expected MediumError(\"unexpected 0x7E in body\"), got {:?}",
620                other.err()
621            ),
622        }
623    }
624
625    fn fixture_roundtrip(wire: &[u8]) {
626        let m = MctpSerialMedium;
627        let (_frame, dec) = m.deserialize(wire).unwrap();
628        let (decoded, n) = drain_decoder(dec);
629        let mut out = [0u8; 1024];
630        let serialized = m
631            .serialize((), &mut out, |e| {
632                e.write_all(&decoded[..n])
633                    .map_err(|_| MctpPacketError::MediumError("write failed"))
634            })
635            .unwrap();
636        assert_eq!(serialized, wire);
637    }
638
639    #[test]
640    fn fixture_roundtrip_basic_rx() {
641        fixture_roundtrip(FIXTURE_BASIC_RX);
642    }
643
644    #[test]
645    fn fixture_roundtrip_payload_contains_7e() {
646        fixture_roundtrip(FIXTURE_PAYLOAD_CONTAINS_7E);
647    }
648
649    #[test]
650    fn fixture_roundtrip_payload_contains_7d() {
651        fixture_roundtrip(FIXTURE_PAYLOAD_CONTAINS_7D);
652    }
653
654    #[test]
655    fn fixture_roundtrip_payload_contains_both() {
656        fixture_roundtrip(FIXTURE_PAYLOAD_CONTAINS_BOTH);
657    }
658
659    #[test]
660    fn fixture_roundtrip_max_mtu_frame() {
661        fixture_roundtrip(FIXTURE_MAX_MTU_FRAME);
662    }
663
664    #[test]
665    fn fixture_roundtrip_empty_payload() {
666        fixture_roundtrip(FIXTURE_EMPTY_PAYLOAD);
667    }
668
669    #[test]
670    fn fixture_roundtrip_fcs_valid() {
671        fixture_roundtrip(FIXTURE_FCS_VALID);
672    }
673
674    #[test]
675    fn public_api_smoke() {
676        let _: crate::MctpSerialMedium = crate::MctpSerialMedium;
677        let _: crate::SerialEncoding = crate::SerialEncoding;
678        assert_eq!(crate::CONST_MTU, 251);
679        assert_eq!(crate::SP_EID, crate::EndpointId::Id(0x08));
680        assert_eq!(crate::EC_EID, crate::EndpointId::Id(0x0A));
681    }
682
683    #[test]
684    fn packetize_with_stuffing_respects_mtu() {
685        // 251-byte payload of all 0x7E. Each byte stuffs to 2 wire
686        // bytes (0x7D 0x5E), so encoded body footprint per packet is
687        // 2x decoded length. The packet body MTU is 251 wire bytes;
688        // each MCTP packet also carries a 4-byte transport header
689        // which itself is `wire_size_of`-measured. Expect the message
690        // to split across multiple packets and no body region to
691        // exceed CONST_MTU wire bytes.
692        use crate::{
693            endpoint_id::EndpointId, mctp_message_tag::MctpMessageTag,
694            mctp_packet_context::MctpReplyContext, mctp_sequence_number::MctpSequenceNumber,
695            serialize::SerializePacketState,
696        };
697
698        let payload = [0x7E_u8; 251];
699        let mut assembly = [0u8; 1024];
700        let medium = MctpSerialMedium;
701        let reply_context = MctpReplyContext::<MctpSerialMedium> {
702            destination_endpoint_id: EndpointId::Id(0x0A),
703            source_endpoint_id: EndpointId::Id(0x08),
704            packet_sequence_number: MctpSequenceNumber::new(0),
705            message_tag: MctpMessageTag::default(),
706            medium_context: (),
707        };
708        let mut state = SerializePacketState {
709            medium: &medium,
710            reply_context,
711            current_packet_num: 0,
712            serialized_message_header: false,
713            message_buffer: &payload[..],
714            assembly_buffer: &mut assembly[..],
715        };
716
717        let mut total_decoded_body = 0usize;
718        let mut packet_count = 0usize;
719        loop {
720            // We cannot iterate `state.next()` more than once because
721            // `next` mutably borrows the assembly buffer for each
722            // returned slice. Take one packet, process it, then break.
723            let pkt = match state.next() {
724                Some(Ok(pkt)) => {
725                    let mut tmp = [0u8; 1024];
726                    tmp[..pkt.len()].copy_from_slice(pkt);
727                    (tmp, pkt.len())
728                }
729                Some(Err(e)) => panic!("serialize error: {e:?}"),
730                None => break,
731            };
732            packet_count += 1;
733            // Deserialize the packet to recover the wire body length
734            // and the decoded body byte count.
735            let (frame, dec) = medium.deserialize(&pkt.0[..pkt.1]).unwrap();
736            // Decoded body byte count INCLUDES the 4 transport-header
737            // bytes — subtract to get the actual payload bytes.
738            assert!(frame.byte_count as usize >= 4);
739            let payload_decoded = frame.byte_count as usize - 4;
740            total_decoded_body += payload_decoded;
741            // Wire body region (between header and FCS) MUST be <=
742            // CONST_MTU under MEDIUM-08 chunk-sizing.
743            let _ = dec; // decoder discard
744            let wire_body_len = pkt.1 - 2 /* hdr */ - 1 /* end-flag */;
745            // Subtract the (possibly stuffed) FCS bytes — they are 2
746            // FCS bytes but each may stuff to 2 wire bytes. Worst case
747            // 4 bytes; lower bound on body wire = wire_body_len - 4.
748            assert!(
749                wire_body_len <= CONST_MTU + 4,
750                "packet {packet_count} body exceeds MTU + worst-case FCS: {wire_body_len}"
751            );
752        }
753        assert!(
754            packet_count >= 2,
755            "expected multi-packet split, got {packet_count}"
756        );
757        assert_eq!(total_decoded_body, payload.len());
758    }
759}