Skip to main content

dvb_si/tables/
mpe.rs

1//! MPE datagram_section — ETSI EN 301 192 v1.7.1 §7.1 (PDF pp. 17-19).
2//!
3//! The Multiprotocol Encapsulation (MPE) `datagram_section` carries an IP
4//! datagram (optionally LLC/SNAP-encapsulated) over a DVB transport stream,
5//! tagged with the destination MAC address. Its `table_id` is `0x3E` — the
6//! DSM-CC "sections with private data" value (ISO/IEC 13818-6). This is the
7//! *typed* view of exactly what [`crate::tables::dsmcc::DsmccSection`] carries
8//! raw: a `0x3E` section reaching `dsmcc.rs` is the same wire bytes this module
9//! decodes into MAC address + scrambling control + payload fields.
10//!
11//! Like DSM-CC, MPE has no well-known PID — the elementary PID is signalled by
12//! the PMT (via a `data_broadcast_descriptor`, EN 301 192 §7.2.1) — so [`PID`]
13//! is `0x0000`, following the `dsmcc.rs` precedent.
14//!
15//! Per the crate contract this parser does NOT verify the trailing CRC/checksum
16//! integrity; [`dvb_common`]'s section machinery owns CRC validation. Reserved
17//! bits are ignored on parse and emitted as `1`s on serialize.
18//!
19//! ## Trailer (SSI-dependent)
20//!
21//! EN 301 192 Table 3 makes the 4-byte trailer conditional on
22//! `section_syntax_indicator` (SSI):
23//! - SSI == 1 → `CRC_32` (computed over the whole section).
24//! - SSI == 0 → `checksum` per ISO/IEC 13818-6.
25//!
26//! The ISO/IEC 13818-6 private-section *checksum* algorithm is not
27//! implementable without that spec, so for `SSI == 0` we preserve the four
28//! parsed trailer bytes verbatim in [`MpeDatagramSection::checksum`] and
29//! re-emit them byte-for-byte. For `SSI == 1` the trailer is recomputed as
30//! CRC_32 on serialize and `checksum` is ignored.
31
32use crate::error::{Error, Result};
33use crate::traits::Table;
34use dvb_common::{Parse, Serialize};
35
36/// table_id for an MPE `datagram_section` — the DSM-CC private-data value
37/// (ISO/IEC 13818-6); see [`crate::tables::dsmcc`] for the raw view.
38pub const TABLE_ID: u8 = 0x3E;
39
40/// MPE has no well-known PID — the elementary PID comes from the PMT.
41pub const PID: u16 = 0x0000;
42
43/// Bytes 0-2: table_id(1) + SSI/private/reserved/section_length(2).
44const HEADER_LEN: usize = 3;
45
46/// Bytes 3-11: MAC_6(1) + MAC_5(1) + flags(1) + section_number(1)
47/// + last_section_number(1) + MAC_4(1) + MAC_3(1) + MAC_2(1) + MAC_1(1).
48const EXTENSION_LEN: usize = 9;
49
50/// Bytes occupied by the trailing CRC_32 / checksum field.
51const CRC_LEN: usize = 4;
52
53/// Minimum total encoded length: header + extension + trailer (empty payload).
54const MIN_SECTION_LEN: usize = HEADER_LEN + EXTENSION_LEN + CRC_LEN;
55
56/// MPE `datagram_section` (ETSI EN 301 192 §7.1).
57///
58/// The 48-bit destination MAC is scattered across the section by the wire
59/// format (Figure 1, PDF p. 18): `MAC_address_1` (the most-significant byte)
60/// lands last, `MAC_address_6` (the least-significant byte) lands first:
61///
62/// ```text
63/// section byte:   3        4        8        9        10       11
64/// MAC field:      MAC_6    MAC_5    MAC_4    MAC_3    MAC_2    MAC_1
65/// MAC byte:       LSB ...                                 ... MSB
66/// ```
67///
68/// We reassemble it into [`MpeDatagramSection::mac_address`] in network order
69/// (`MAC_1..MAC_6`, most-significant first), so `mac_address[0]` is `MAC_1`
70/// and `mac_address[5]` is `MAC_6`.
71#[derive(Debug, Clone, PartialEq, Eq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize))]
73pub struct MpeDatagramSection<'a> {
74    /// `section_syntax_indicator` bit. When `true` the trailer is a computed
75    /// `CRC_32`; when `false` it is an ISO/IEC 13818-6 checksum preserved
76    /// verbatim in [`Self::checksum`].
77    pub section_syntax_indicator: bool,
78
79    /// `private_indicator` bit (byte 1, bit 6).
80    pub private_indicator: bool,
81
82    /// Destination MAC address in network order, `MAC_1` (MSB) first through
83    /// `MAC_6` (LSB) last. See the struct docs for the wire scatter.
84    pub mac_address: [u8; 6],
85
86    /// 2-bit `payload_scrambling_control` (EN 301 192 Table 4). `0` =
87    /// unscrambled; `1`/`2`/`3` = service-defined.
88    pub payload_scrambling_control: u8,
89
90    /// 2-bit `address_scrambling_control` (EN 301 192 Table 5). `0` =
91    /// unscrambled; `1`/`2`/`3` = service-defined.
92    pub address_scrambling_control: u8,
93
94    /// `LLC_SNAP_flag`. When `true`, [`Self::payload`] is an LLC/SNAP-
95    /// encapsulated datagram; when `false`, a bare IP datagram. We keep the
96    /// payload raw either way (LLC/SNAP and IP framing are out of scope).
97    pub llc_snap_flag: bool,
98
99    /// `current_next_indicator` bit (the spec mandates `1`).
100    pub current_next_indicator: bool,
101
102    /// Section index within the fragmented datagram.
103    pub section_number: u8,
104
105    /// Final section index of the fragmented datagram.
106    pub last_section_number: u8,
107
108    /// Raw payload: LLC/SNAP bytes when [`Self::llc_snap_flag`] is set, else
109    /// IP datagram bytes — plus any trailing `stuffing_byte`s — kept as one
110    /// borrowed slice running from byte 12 to the 4-byte trailer. We do not
111    /// parse LLC/SNAP or IP, nor split out stuffing (EN 301 192 §7.1).
112    pub payload: &'a [u8],
113
114    /// Verbatim trailer bytes when `section_syntax_indicator == false` (an
115    /// ISO/IEC 13818-6 checksum we cannot recompute). Ignored when SSI is
116    /// `true`, where the trailer is a computed `CRC_32`.
117    pub checksum: [u8; 4],
118}
119
120impl<'a> Parse<'a> for MpeDatagramSection<'a> {
121    type Error = crate::error::Error;
122
123    fn parse(bytes: &'a [u8]) -> Result<Self> {
124        if bytes.len() < MIN_SECTION_LEN {
125            return Err(Error::BufferTooShort {
126                need: MIN_SECTION_LEN,
127                have: bytes.len(),
128                what: "MpeDatagramSection",
129            });
130        }
131
132        if bytes[0] != TABLE_ID {
133            return Err(Error::UnexpectedTableId {
134                table_id: bytes[0],
135                what: "MpeDatagramSection",
136                expected: &[TABLE_ID],
137            });
138        }
139
140        // Byte 1: SSI(1) | private(1) | reserved(2) | section_length[11:8].
141        let section_syntax_indicator = (bytes[1] & 0x80) != 0;
142        let private_indicator = (bytes[1] & 0x40) != 0;
143        let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
144        let total = HEADER_LEN + section_length;
145        if bytes.len() < total {
146            return Err(Error::SectionLengthOverflow {
147                declared: section_length,
148                available: bytes.len() - HEADER_LEN,
149            });
150        }
151        // The declared section must at least span the extension + trailer.
152        if section_length < EXTENSION_LEN + CRC_LEN {
153            return Err(Error::SectionLengthOverflow {
154                declared: section_length,
155                available: bytes.len() - HEADER_LEN,
156            });
157        }
158
159        // MAC scatter: byte 3 = MAC_6 (LSB), byte 4 = MAC_5,
160        // bytes 8-11 = MAC_4, MAC_3, MAC_2, MAC_1 (MSB). Reassemble MSB-first.
161        let mac_6 = bytes[3];
162        let mac_5 = bytes[4];
163
164        // Byte 5: reserved(2) | payload_sc(2) | address_sc(2) | LLC_SNAP(1) | cni(1).
165        let payload_scrambling_control = (bytes[5] >> 4) & 0x03;
166        let address_scrambling_control = (bytes[5] >> 2) & 0x03;
167        let llc_snap_flag = (bytes[5] & 0x02) != 0;
168        let current_next_indicator = (bytes[5] & 0x01) != 0;
169
170        let section_number = bytes[6];
171        let last_section_number = bytes[7];
172
173        let mac_4 = bytes[8];
174        let mac_3 = bytes[9];
175        let mac_2 = bytes[10];
176        let mac_1 = bytes[11];
177        let mac_address = [mac_1, mac_2, mac_3, mac_4, mac_5, mac_6];
178
179        let payload_start = HEADER_LEN + EXTENSION_LEN;
180        let trailer_start = total - CRC_LEN;
181        let payload = &bytes[payload_start..trailer_start];
182        let checksum = [
183            bytes[trailer_start],
184            bytes[trailer_start + 1],
185            bytes[trailer_start + 2],
186            bytes[trailer_start + 3],
187        ];
188
189        Ok(MpeDatagramSection {
190            section_syntax_indicator,
191            private_indicator,
192            mac_address,
193            payload_scrambling_control,
194            address_scrambling_control,
195            llc_snap_flag,
196            current_next_indicator,
197            section_number,
198            last_section_number,
199            payload,
200            checksum,
201        })
202    }
203}
204
205impl Serialize for MpeDatagramSection<'_> {
206    type Error = crate::error::Error;
207
208    fn serialized_len(&self) -> usize {
209        HEADER_LEN + EXTENSION_LEN + self.payload.len() + CRC_LEN
210    }
211
212    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
213        let len = self.serialized_len();
214        if buf.len() < len {
215            return Err(Error::OutputBufferTooSmall {
216                need: len,
217                have: buf.len(),
218            });
219        }
220
221        // 2-bit scrambling fields must fit; reject over-range values rather
222        // than silently truncating (mirrors cit/sdt guarding derived fields).
223        if self.payload_scrambling_control > 0x03 {
224            return Err(Error::ReservedBitsViolation {
225                field: "payload_scrambling_control",
226                reason: "value exceeds 2-bit field",
227            });
228        }
229        if self.address_scrambling_control > 0x03 {
230            return Err(Error::ReservedBitsViolation {
231                field: "address_scrambling_control",
232                reason: "value exceeds 2-bit field",
233            });
234        }
235
236        let section_length = (len - HEADER_LEN) as u16;
237
238        buf[0] = TABLE_ID;
239        // Byte 1: SSI(1) | private(1) | reserved(2)=11 | section_length[11:8].
240        buf[1] = (u8::from(self.section_syntax_indicator) << 7)
241            | (u8::from(self.private_indicator) << 6)
242            | 0x30 // reserved bits set to 1
243            | ((section_length >> 8) as u8 & 0x0F);
244        buf[2] = (section_length & 0xFF) as u8;
245
246        // MAC scatter: byte 3 = MAC_6 (mac_address[5]), byte 4 = MAC_5.
247        buf[3] = self.mac_address[5];
248        buf[4] = self.mac_address[4];
249
250        // Byte 5: reserved(2)=11 | payload_sc(2) | address_sc(2) | LLC_SNAP(1) | cni(1).
251        buf[5] = 0xC0
252            | ((self.payload_scrambling_control & 0x03) << 4)
253            | ((self.address_scrambling_control & 0x03) << 2)
254            | (u8::from(self.llc_snap_flag) << 1)
255            | u8::from(self.current_next_indicator);
256
257        buf[6] = self.section_number;
258        buf[7] = self.last_section_number;
259
260        // bytes 8-11 = MAC_4, MAC_3, MAC_2, MAC_1.
261        buf[8] = self.mac_address[3];
262        buf[9] = self.mac_address[2];
263        buf[10] = self.mac_address[1];
264        buf[11] = self.mac_address[0];
265
266        let payload_start = HEADER_LEN + EXTENSION_LEN;
267        let trailer_start = payload_start + self.payload.len();
268        buf[payload_start..trailer_start].copy_from_slice(self.payload);
269
270        if self.section_syntax_indicator {
271            // SSI=1 → recompute CRC_32 over the whole section up to the trailer.
272            let crc = dvb_common::crc32_mpeg2::compute(&buf[..trailer_start]);
273            buf[trailer_start..len].copy_from_slice(&crc.to_be_bytes());
274        } else {
275            // SSI=0 → ISO/IEC 13818-6 checksum we cannot recompute; re-emit
276            // the preserved trailer bytes verbatim.
277            buf[trailer_start..len].copy_from_slice(&self.checksum);
278        }
279
280        Ok(len)
281    }
282}
283
284impl<'a> Table<'a> for MpeDatagramSection<'a> {
285    const TABLE_ID: u8 = TABLE_ID;
286    const PID: u16 = PID;
287}
288
289impl<'a> crate::traits::TableDef<'a> for MpeDatagramSection<'a> {
290    /// `0x3E` is included in `DsmccSection`'s range `[(0x3A, 0x3F)]` and is
291    /// NOT auto-dispatched to this type by the default dispatcher. Use
292    /// `AnyTable::parse_as::<MpeDatagramSection>` or
293    /// `MpeDatagramSection::parse` to obtain the typed MPE view.
294    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
295    const NAME: &'static str = "MPE_DATAGRAM_SECTION";
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    /// Build a syntactically valid MPE datagram_section.
303    ///
304    /// `mac_address` is in network order (MAC_1 first). The 4-byte trailer is
305    /// written from `trailer` verbatim (callers pass a computed CRC or an
306    /// arbitrary checksum), matching what the serializer would emit for the
307    /// `ssi == false` path.
308    #[allow(clippy::too_many_arguments)]
309    fn build_mpe(
310        ssi: bool,
311        private_indicator: bool,
312        mac_address: [u8; 6],
313        payload_sc: u8,
314        address_sc: u8,
315        llc_snap: bool,
316        section_number: u8,
317        last_section_number: u8,
318        payload: &[u8],
319        trailer: [u8; 4],
320    ) -> Vec<u8> {
321        let section_length = (EXTENSION_LEN + payload.len() + CRC_LEN) as u16;
322        let flags = 0xC0
323            | ((payload_sc & 0x03) << 4)
324            | ((address_sc & 0x03) << 2)
325            | (u8::from(llc_snap) << 1)
326            | 0x01; // cni = 1
327        let mut v = vec![
328            TABLE_ID,
329            (u8::from(ssi) << 7)
330                | (u8::from(private_indicator) << 6)
331                | 0x30
332                | ((section_length >> 8) as u8 & 0x0F),
333            (section_length & 0xFF) as u8,
334            mac_address[5], // MAC_6
335            mac_address[4], // MAC_5
336            flags,
337            section_number,
338            last_section_number,
339            mac_address[3], // MAC_4
340            mac_address[2], // MAC_3
341            mac_address[1], // MAC_2
342            mac_address[0], // MAC_1
343        ];
344        v.extend_from_slice(payload);
345        v.extend_from_slice(&trailer);
346        v
347    }
348
349    #[test]
350    fn parse_happy_path() {
351        let mac = [0x01, 0x00, 0x5E, 0x12, 0x34, 0x56];
352        let payload = [0xDE, 0xAD, 0xBE, 0xEF];
353        let bytes = build_mpe(
354            false,
355            true,
356            mac,
357            0b10,
358            0b01,
359            true,
360            2,
361            3,
362            &payload,
363            [0xAA, 0xBB, 0xCC, 0xDD],
364        );
365        let sec = MpeDatagramSection::parse(&bytes).unwrap();
366        assert!(!sec.section_syntax_indicator);
367        assert!(sec.private_indicator);
368        assert_eq!(sec.mac_address, mac);
369        assert_eq!(sec.payload_scrambling_control, 0b10);
370        assert_eq!(sec.address_scrambling_control, 0b01);
371        assert!(sec.llc_snap_flag);
372        assert!(sec.current_next_indicator);
373        assert_eq!(sec.section_number, 2);
374        assert_eq!(sec.last_section_number, 3);
375        assert_eq!(sec.payload, &payload);
376        assert_eq!(sec.checksum, [0xAA, 0xBB, 0xCC, 0xDD]);
377    }
378
379    #[test]
380    fn mac_scatter_decoded_in_network_order() {
381        // Distinct bytes per MAC position so a wrong scatter is obvious.
382        let mac = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
383        let bytes = build_mpe(true, false, mac, 0, 0, false, 0, 0, &[], [0, 0, 0, 0]);
384        // Verify the on-wire scatter directly:
385        assert_eq!(bytes[3], 0x66, "byte 3 = MAC_6 (LSB)");
386        assert_eq!(bytes[4], 0x55, "byte 4 = MAC_5");
387        assert_eq!(bytes[8], 0x44, "byte 8 = MAC_4");
388        assert_eq!(bytes[9], 0x33, "byte 9 = MAC_3");
389        assert_eq!(bytes[10], 0x22, "byte 10 = MAC_2");
390        assert_eq!(bytes[11], 0x11, "byte 11 = MAC_1 (MSB)");
391        let sec = MpeDatagramSection::parse(&bytes).unwrap();
392        assert_eq!(sec.mac_address, mac);
393    }
394
395    #[test]
396    fn parse_empty_payload() {
397        let bytes = build_mpe(
398            true,
399            false,
400            [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
401            0,
402            0,
403            false,
404            0,
405            0,
406            &[],
407            [0, 0, 0, 0],
408        );
409        let sec = MpeDatagramSection::parse(&bytes).unwrap();
410        assert!(sec.payload.is_empty());
411        assert_eq!(sec.mac_address, [0xFF; 6]);
412    }
413
414    #[test]
415    fn parse_rejects_wrong_table_id() {
416        let mut bytes = build_mpe(
417            true,
418            false,
419            [0; 6],
420            0,
421            0,
422            false,
423            0,
424            0,
425            &[0x01],
426            [0, 0, 0, 0],
427        );
428        bytes[0] = 0x3F; // valid DSM-CC range value, but not the MPE table_id
429        assert!(matches!(
430            MpeDatagramSection::parse(&bytes).unwrap_err(),
431            Error::UnexpectedTableId { table_id: 0x3F, .. }
432        ));
433    }
434
435    #[test]
436    fn parse_rejects_short_buffer() {
437        let err = MpeDatagramSection::parse(&[TABLE_ID, 0x00]).unwrap_err();
438        assert!(matches!(err, Error::BufferTooShort { .. }));
439    }
440
441    #[test]
442    fn parse_rejects_section_length_overflow() {
443        let mut bytes = build_mpe(
444            true,
445            false,
446            [0; 6],
447            0,
448            0,
449            false,
450            0,
451            0,
452            &[0xAA],
453            [0, 0, 0, 0],
454        );
455        // Inflate declared section_length well past the actual buffer.
456        let fake_sl: u16 = (bytes.len() as u16) + 100 - HEADER_LEN as u16;
457        bytes[1] = (bytes[1] & 0xF0) | ((fake_sl >> 8) as u8 & 0x0F);
458        bytes[2] = (fake_sl & 0xFF) as u8;
459        assert!(matches!(
460            MpeDatagramSection::parse(&bytes).unwrap_err(),
461            Error::SectionLengthOverflow { .. }
462        ));
463    }
464
465    #[test]
466    fn round_trip_identity_ssi_set_crc() {
467        // SSI=1: serialize recomputes CRC_32. Build with a matching CRC so the
468        // parsed `checksum` field also matches (it is ignored when SSI=1, but
469        // we set it correctly to assert full struct equality).
470        let mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
471        let payload = [0x45, 0x00, 0x00, 0x1C, 0x00, 0x01];
472        let original = MpeDatagramSection {
473            section_syntax_indicator: true,
474            private_indicator: false,
475            mac_address: mac,
476            payload_scrambling_control: 0,
477            address_scrambling_control: 0,
478            llc_snap_flag: false,
479            current_next_indicator: true,
480            section_number: 0,
481            last_section_number: 0,
482            payload: &payload,
483            checksum: [0, 0, 0, 0],
484        };
485        let mut buf = vec![0u8; original.serialized_len()];
486        original.serialize_into(&mut buf).unwrap();
487        let parsed = MpeDatagramSection::parse(&buf).unwrap();
488        // Everything but the (ignored-on-SSI=1) checksum must match.
489        assert!(parsed.section_syntax_indicator);
490        assert_eq!(parsed.mac_address, mac);
491        assert_eq!(parsed.payload, &payload);
492        // Re-serialize the parsed value: bytes must be byte-identical.
493        let mut buf2 = vec![0u8; parsed.serialized_len()];
494        parsed.serialize_into(&mut buf2).unwrap();
495        assert_eq!(buf, buf2);
496    }
497
498    #[test]
499    fn round_trip_identity_ssi_clear_checksum_preserved() {
500        // SSI=0: the trailer is an opaque checksum preserved verbatim.
501        let mac = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
502        let payload = [0x11, 0x22, 0x33];
503        let trailer = [0x12, 0x34, 0x56, 0x78];
504        let bytes = build_mpe(false, true, mac, 0b11, 0b10, true, 1, 5, &payload, trailer);
505        let parsed = MpeDatagramSection::parse(&bytes).unwrap();
506        assert_eq!(parsed.checksum, trailer);
507        let mut buf = vec![0u8; parsed.serialized_len()];
508        parsed.serialize_into(&mut buf).unwrap();
509        // Full byte-for-byte identity, including the preserved checksum.
510        assert_eq!(buf, bytes);
511        assert_eq!(MpeDatagramSection::parse(&buf).unwrap(), parsed);
512    }
513
514    #[test]
515    fn serialize_rejects_output_buffer_too_small() {
516        let sec = MpeDatagramSection {
517            section_syntax_indicator: true,
518            private_indicator: false,
519            mac_address: [0; 6],
520            payload_scrambling_control: 0,
521            address_scrambling_control: 0,
522            llc_snap_flag: false,
523            current_next_indicator: true,
524            section_number: 0,
525            last_section_number: 0,
526            payload: &[],
527            checksum: [0; 4],
528        };
529        let mut buf = [0u8; 2];
530        assert!(matches!(
531            sec.serialize_into(&mut buf).unwrap_err(),
532            Error::OutputBufferTooSmall { .. }
533        ));
534    }
535
536    #[test]
537    fn serialize_rejects_over_range_scrambling_control() {
538        let sec = MpeDatagramSection {
539            section_syntax_indicator: true,
540            private_indicator: false,
541            mac_address: [0; 6],
542            payload_scrambling_control: 0x04, // > 2-bit field
543            address_scrambling_control: 0,
544            llc_snap_flag: false,
545            current_next_indicator: true,
546            section_number: 0,
547            last_section_number: 0,
548            payload: &[],
549            checksum: [0; 4],
550        };
551        let mut buf = vec![0u8; sec.serialized_len()];
552        assert!(matches!(
553            sec.serialize_into(&mut buf).unwrap_err(),
554            Error::ReservedBitsViolation {
555                field: "payload_scrambling_control",
556                ..
557            }
558        ));
559    }
560
561    #[test]
562    fn table_trait_constants() {
563        assert_eq!(<MpeDatagramSection as Table>::TABLE_ID, 0x3E);
564        assert_eq!(<MpeDatagramSection as Table>::PID, 0x0000);
565    }
566
567    #[cfg(feature = "serde")]
568    #[test]
569    fn serde_json_round_trip() {
570        let payload = [0xAB, 0xCD];
571        let bytes = build_mpe(
572            false,
573            true,
574            [0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F],
575            0b01,
576            0b11,
577            true,
578            3,
579            7,
580            &payload,
581            [0xDE, 0xAD, 0xBE, 0xEF],
582        );
583        let sec = MpeDatagramSection::parse(&bytes).unwrap();
584        let j = serde_json::to_string(&sec).unwrap();
585
586        // The borrowed `payload: &[u8]` field cannot be JSON-deserialized
587        // zero-copy (serde_json renders it as a number sequence, not a
588        // borrowed byte array — the same constraint that affects every
589        // borrowed-slice table in the crate). Unlike cat.rs, whose fields are
590        // all owned and so round-trip via `from_str::<Self>`, we exercise the
591        // serde derive through the WIRE form: a re-parse of the same bytes
592        // must serialize to byte-identical JSON. This pins the Serialize impl.
593        let reparsed = MpeDatagramSection::parse(&bytes).unwrap();
594        assert_eq!(serde_json::to_string(&reparsed).unwrap(), j);
595
596        // And confirm the JSON carries the decoded fields: network-order MAC,
597        // both 2-bit scrambling controls, and the preserved checksum trailer.
598        assert!(j.contains("\"mac_address\":[10,11,12,13,14,15]"));
599        assert!(j.contains("\"payload_scrambling_control\":1"));
600        assert!(j.contains("\"address_scrambling_control\":3"));
601        assert!(j.contains("\"checksum\":[222,173,190,239]"));
602    }
603}