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, serde::Deserialize))]
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    #[cfg_attr(feature = "serde", serde(borrow))]
113    pub payload: &'a [u8],
114
115    /// Verbatim trailer bytes when `section_syntax_indicator == false` (an
116    /// ISO/IEC 13818-6 checksum we cannot recompute). Ignored when SSI is
117    /// `true`, where the trailer is a computed `CRC_32`.
118    pub checksum: [u8; 4],
119}
120
121impl<'a> Parse<'a> for MpeDatagramSection<'a> {
122    type Error = crate::error::Error;
123
124    fn parse(bytes: &'a [u8]) -> Result<Self> {
125        if bytes.len() < MIN_SECTION_LEN {
126            return Err(Error::BufferTooShort {
127                need: MIN_SECTION_LEN,
128                have: bytes.len(),
129                what: "MpeDatagramSection",
130            });
131        }
132
133        if bytes[0] != TABLE_ID {
134            return Err(Error::UnexpectedTableId {
135                table_id: bytes[0],
136                what: "MpeDatagramSection",
137                expected: &[TABLE_ID],
138            });
139        }
140
141        // Byte 1: SSI(1) | private(1) | reserved(2) | section_length[11:8].
142        let section_syntax_indicator = (bytes[1] & 0x80) != 0;
143        let private_indicator = (bytes[1] & 0x40) != 0;
144        let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
145        let total = HEADER_LEN + section_length;
146        if bytes.len() < total {
147            return Err(Error::SectionLengthOverflow {
148                declared: section_length,
149                available: bytes.len() - HEADER_LEN,
150            });
151        }
152        // The declared section must at least span the extension + trailer.
153        if section_length < EXTENSION_LEN + CRC_LEN {
154            return Err(Error::SectionLengthOverflow {
155                declared: section_length,
156                available: bytes.len() - HEADER_LEN,
157            });
158        }
159
160        // MAC scatter: byte 3 = MAC_6 (LSB), byte 4 = MAC_5,
161        // bytes 8-11 = MAC_4, MAC_3, MAC_2, MAC_1 (MSB). Reassemble MSB-first.
162        let mac_6 = bytes[3];
163        let mac_5 = bytes[4];
164
165        // Byte 5: reserved(2) | payload_sc(2) | address_sc(2) | LLC_SNAP(1) | cni(1).
166        let payload_scrambling_control = (bytes[5] >> 4) & 0x03;
167        let address_scrambling_control = (bytes[5] >> 2) & 0x03;
168        let llc_snap_flag = (bytes[5] & 0x02) != 0;
169        let current_next_indicator = (bytes[5] & 0x01) != 0;
170
171        let section_number = bytes[6];
172        let last_section_number = bytes[7];
173
174        let mac_4 = bytes[8];
175        let mac_3 = bytes[9];
176        let mac_2 = bytes[10];
177        let mac_1 = bytes[11];
178        let mac_address = [mac_1, mac_2, mac_3, mac_4, mac_5, mac_6];
179
180        let payload_start = HEADER_LEN + EXTENSION_LEN;
181        let trailer_start = total - CRC_LEN;
182        let payload = &bytes[payload_start..trailer_start];
183        let checksum = [
184            bytes[trailer_start],
185            bytes[trailer_start + 1],
186            bytes[trailer_start + 2],
187            bytes[trailer_start + 3],
188        ];
189
190        Ok(MpeDatagramSection {
191            section_syntax_indicator,
192            private_indicator,
193            mac_address,
194            payload_scrambling_control,
195            address_scrambling_control,
196            llc_snap_flag,
197            current_next_indicator,
198            section_number,
199            last_section_number,
200            payload,
201            checksum,
202        })
203    }
204}
205
206impl Serialize for MpeDatagramSection<'_> {
207    type Error = crate::error::Error;
208
209    fn serialized_len(&self) -> usize {
210        HEADER_LEN + EXTENSION_LEN + self.payload.len() + CRC_LEN
211    }
212
213    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
214        let len = self.serialized_len();
215        if buf.len() < len {
216            return Err(Error::OutputBufferTooSmall {
217                need: len,
218                have: buf.len(),
219            });
220        }
221
222        // 2-bit scrambling fields must fit; reject over-range values rather
223        // than silently truncating (mirrors cit/sdt guarding derived fields).
224        if self.payload_scrambling_control > 0x03 {
225            return Err(Error::ReservedBitsViolation {
226                field: "payload_scrambling_control",
227                reason: "value exceeds 2-bit field",
228            });
229        }
230        if self.address_scrambling_control > 0x03 {
231            return Err(Error::ReservedBitsViolation {
232                field: "address_scrambling_control",
233                reason: "value exceeds 2-bit field",
234            });
235        }
236
237        let section_length = (len - HEADER_LEN) as u16;
238
239        buf[0] = TABLE_ID;
240        // Byte 1: SSI(1) | private(1) | reserved(2)=11 | section_length[11:8].
241        buf[1] = (u8::from(self.section_syntax_indicator) << 7)
242            | (u8::from(self.private_indicator) << 6)
243            | 0x30 // reserved bits set to 1
244            | ((section_length >> 8) as u8 & 0x0F);
245        buf[2] = (section_length & 0xFF) as u8;
246
247        // MAC scatter: byte 3 = MAC_6 (mac_address[5]), byte 4 = MAC_5.
248        buf[3] = self.mac_address[5];
249        buf[4] = self.mac_address[4];
250
251        // Byte 5: reserved(2)=11 | payload_sc(2) | address_sc(2) | LLC_SNAP(1) | cni(1).
252        buf[5] = 0xC0
253            | ((self.payload_scrambling_control & 0x03) << 4)
254            | ((self.address_scrambling_control & 0x03) << 2)
255            | (u8::from(self.llc_snap_flag) << 1)
256            | u8::from(self.current_next_indicator);
257
258        buf[6] = self.section_number;
259        buf[7] = self.last_section_number;
260
261        // bytes 8-11 = MAC_4, MAC_3, MAC_2, MAC_1.
262        buf[8] = self.mac_address[3];
263        buf[9] = self.mac_address[2];
264        buf[10] = self.mac_address[1];
265        buf[11] = self.mac_address[0];
266
267        let payload_start = HEADER_LEN + EXTENSION_LEN;
268        let trailer_start = payload_start + self.payload.len();
269        buf[payload_start..trailer_start].copy_from_slice(self.payload);
270
271        if self.section_syntax_indicator {
272            // SSI=1 → recompute CRC_32 over the whole section up to the trailer.
273            let crc = dvb_common::crc32_mpeg2::compute(&buf[..trailer_start]);
274            buf[trailer_start..len].copy_from_slice(&crc.to_be_bytes());
275        } else {
276            // SSI=0 → ISO/IEC 13818-6 checksum we cannot recompute; re-emit
277            // the preserved trailer bytes verbatim.
278            buf[trailer_start..len].copy_from_slice(&self.checksum);
279        }
280
281        Ok(len)
282    }
283}
284
285impl<'a> Table<'a> for MpeDatagramSection<'a> {
286    const TABLE_ID: u8 = TABLE_ID;
287    const PID: u16 = PID;
288}
289
290impl<'a> crate::traits::TableDef<'a> for MpeDatagramSection<'a> {
291    /// `0x3E` is included in `DsmccSection`'s range `[(0x3A, 0x3F)]` and is
292    /// NOT auto-dispatched to this type by the default dispatcher. Use
293    /// `AnyTable::parse_as::<MpeDatagramSection>` or
294    /// `MpeDatagramSection::parse` to obtain the typed MPE view.
295    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
296    const NAME: &'static str = "MPE_DATAGRAM_SECTION";
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    /// Build a syntactically valid MPE datagram_section.
304    ///
305    /// `mac_address` is in network order (MAC_1 first). The 4-byte trailer is
306    /// written from `trailer` verbatim (callers pass a computed CRC or an
307    /// arbitrary checksum), matching what the serializer would emit for the
308    /// `ssi == false` path.
309    #[allow(clippy::too_many_arguments)]
310    fn build_mpe(
311        ssi: bool,
312        private_indicator: bool,
313        mac_address: [u8; 6],
314        payload_sc: u8,
315        address_sc: u8,
316        llc_snap: bool,
317        section_number: u8,
318        last_section_number: u8,
319        payload: &[u8],
320        trailer: [u8; 4],
321    ) -> Vec<u8> {
322        let section_length = (EXTENSION_LEN + payload.len() + CRC_LEN) as u16;
323        let flags = 0xC0
324            | ((payload_sc & 0x03) << 4)
325            | ((address_sc & 0x03) << 2)
326            | (u8::from(llc_snap) << 1)
327            | 0x01; // cni = 1
328        let mut v = vec![
329            TABLE_ID,
330            (u8::from(ssi) << 7)
331                | (u8::from(private_indicator) << 6)
332                | 0x30
333                | ((section_length >> 8) as u8 & 0x0F),
334            (section_length & 0xFF) as u8,
335            mac_address[5], // MAC_6
336            mac_address[4], // MAC_5
337            flags,
338            section_number,
339            last_section_number,
340            mac_address[3], // MAC_4
341            mac_address[2], // MAC_3
342            mac_address[1], // MAC_2
343            mac_address[0], // MAC_1
344        ];
345        v.extend_from_slice(payload);
346        v.extend_from_slice(&trailer);
347        v
348    }
349
350    #[test]
351    fn parse_happy_path() {
352        let mac = [0x01, 0x00, 0x5E, 0x12, 0x34, 0x56];
353        let payload = [0xDE, 0xAD, 0xBE, 0xEF];
354        let bytes = build_mpe(
355            false,
356            true,
357            mac,
358            0b10,
359            0b01,
360            true,
361            2,
362            3,
363            &payload,
364            [0xAA, 0xBB, 0xCC, 0xDD],
365        );
366        let sec = MpeDatagramSection::parse(&bytes).unwrap();
367        assert!(!sec.section_syntax_indicator);
368        assert!(sec.private_indicator);
369        assert_eq!(sec.mac_address, mac);
370        assert_eq!(sec.payload_scrambling_control, 0b10);
371        assert_eq!(sec.address_scrambling_control, 0b01);
372        assert!(sec.llc_snap_flag);
373        assert!(sec.current_next_indicator);
374        assert_eq!(sec.section_number, 2);
375        assert_eq!(sec.last_section_number, 3);
376        assert_eq!(sec.payload, &payload);
377        assert_eq!(sec.checksum, [0xAA, 0xBB, 0xCC, 0xDD]);
378    }
379
380    #[test]
381    fn mac_scatter_decoded_in_network_order() {
382        // Distinct bytes per MAC position so a wrong scatter is obvious.
383        let mac = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
384        let bytes = build_mpe(true, false, mac, 0, 0, false, 0, 0, &[], [0, 0, 0, 0]);
385        // Verify the on-wire scatter directly:
386        assert_eq!(bytes[3], 0x66, "byte 3 = MAC_6 (LSB)");
387        assert_eq!(bytes[4], 0x55, "byte 4 = MAC_5");
388        assert_eq!(bytes[8], 0x44, "byte 8 = MAC_4");
389        assert_eq!(bytes[9], 0x33, "byte 9 = MAC_3");
390        assert_eq!(bytes[10], 0x22, "byte 10 = MAC_2");
391        assert_eq!(bytes[11], 0x11, "byte 11 = MAC_1 (MSB)");
392        let sec = MpeDatagramSection::parse(&bytes).unwrap();
393        assert_eq!(sec.mac_address, mac);
394    }
395
396    #[test]
397    fn parse_empty_payload() {
398        let bytes = build_mpe(
399            true,
400            false,
401            [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
402            0,
403            0,
404            false,
405            0,
406            0,
407            &[],
408            [0, 0, 0, 0],
409        );
410        let sec = MpeDatagramSection::parse(&bytes).unwrap();
411        assert!(sec.payload.is_empty());
412        assert_eq!(sec.mac_address, [0xFF; 6]);
413    }
414
415    #[test]
416    fn parse_rejects_wrong_table_id() {
417        let mut bytes = build_mpe(
418            true,
419            false,
420            [0; 6],
421            0,
422            0,
423            false,
424            0,
425            0,
426            &[0x01],
427            [0, 0, 0, 0],
428        );
429        bytes[0] = 0x3F; // valid DSM-CC range value, but not the MPE table_id
430        assert!(matches!(
431            MpeDatagramSection::parse(&bytes).unwrap_err(),
432            Error::UnexpectedTableId { table_id: 0x3F, .. }
433        ));
434    }
435
436    #[test]
437    fn parse_rejects_short_buffer() {
438        let err = MpeDatagramSection::parse(&[TABLE_ID, 0x00]).unwrap_err();
439        assert!(matches!(err, Error::BufferTooShort { .. }));
440    }
441
442    #[test]
443    fn parse_rejects_section_length_overflow() {
444        let mut bytes = build_mpe(
445            true,
446            false,
447            [0; 6],
448            0,
449            0,
450            false,
451            0,
452            0,
453            &[0xAA],
454            [0, 0, 0, 0],
455        );
456        // Inflate declared section_length well past the actual buffer.
457        let fake_sl: u16 = (bytes.len() as u16) + 100 - HEADER_LEN as u16;
458        bytes[1] = (bytes[1] & 0xF0) | ((fake_sl >> 8) as u8 & 0x0F);
459        bytes[2] = (fake_sl & 0xFF) as u8;
460        assert!(matches!(
461            MpeDatagramSection::parse(&bytes).unwrap_err(),
462            Error::SectionLengthOverflow { .. }
463        ));
464    }
465
466    #[test]
467    fn round_trip_identity_ssi_set_crc() {
468        // SSI=1: serialize recomputes CRC_32. Build with a matching CRC so the
469        // parsed `checksum` field also matches (it is ignored when SSI=1, but
470        // we set it correctly to assert full struct equality).
471        let mac = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
472        let payload = [0x45, 0x00, 0x00, 0x1C, 0x00, 0x01];
473        let original = MpeDatagramSection {
474            section_syntax_indicator: true,
475            private_indicator: false,
476            mac_address: mac,
477            payload_scrambling_control: 0,
478            address_scrambling_control: 0,
479            llc_snap_flag: false,
480            current_next_indicator: true,
481            section_number: 0,
482            last_section_number: 0,
483            payload: &payload,
484            checksum: [0, 0, 0, 0],
485        };
486        let mut buf = vec![0u8; original.serialized_len()];
487        original.serialize_into(&mut buf).unwrap();
488        let parsed = MpeDatagramSection::parse(&buf).unwrap();
489        // Everything but the (ignored-on-SSI=1) checksum must match.
490        assert!(parsed.section_syntax_indicator);
491        assert_eq!(parsed.mac_address, mac);
492        assert_eq!(parsed.payload, &payload);
493        // Re-serialize the parsed value: bytes must be byte-identical.
494        let mut buf2 = vec![0u8; parsed.serialized_len()];
495        parsed.serialize_into(&mut buf2).unwrap();
496        assert_eq!(buf, buf2);
497    }
498
499    #[test]
500    fn round_trip_identity_ssi_clear_checksum_preserved() {
501        // SSI=0: the trailer is an opaque checksum preserved verbatim.
502        let mac = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
503        let payload = [0x11, 0x22, 0x33];
504        let trailer = [0x12, 0x34, 0x56, 0x78];
505        let bytes = build_mpe(false, true, mac, 0b11, 0b10, true, 1, 5, &payload, trailer);
506        let parsed = MpeDatagramSection::parse(&bytes).unwrap();
507        assert_eq!(parsed.checksum, trailer);
508        let mut buf = vec![0u8; parsed.serialized_len()];
509        parsed.serialize_into(&mut buf).unwrap();
510        // Full byte-for-byte identity, including the preserved checksum.
511        assert_eq!(buf, bytes);
512        assert_eq!(MpeDatagramSection::parse(&buf).unwrap(), parsed);
513    }
514
515    #[test]
516    fn serialize_rejects_output_buffer_too_small() {
517        let sec = MpeDatagramSection {
518            section_syntax_indicator: true,
519            private_indicator: false,
520            mac_address: [0; 6],
521            payload_scrambling_control: 0,
522            address_scrambling_control: 0,
523            llc_snap_flag: false,
524            current_next_indicator: true,
525            section_number: 0,
526            last_section_number: 0,
527            payload: &[],
528            checksum: [0; 4],
529        };
530        let mut buf = [0u8; 2];
531        assert!(matches!(
532            sec.serialize_into(&mut buf).unwrap_err(),
533            Error::OutputBufferTooSmall { .. }
534        ));
535    }
536
537    #[test]
538    fn serialize_rejects_over_range_scrambling_control() {
539        let sec = MpeDatagramSection {
540            section_syntax_indicator: true,
541            private_indicator: false,
542            mac_address: [0; 6],
543            payload_scrambling_control: 0x04, // > 2-bit field
544            address_scrambling_control: 0,
545            llc_snap_flag: false,
546            current_next_indicator: true,
547            section_number: 0,
548            last_section_number: 0,
549            payload: &[],
550            checksum: [0; 4],
551        };
552        let mut buf = vec![0u8; sec.serialized_len()];
553        assert!(matches!(
554            sec.serialize_into(&mut buf).unwrap_err(),
555            Error::ReservedBitsViolation {
556                field: "payload_scrambling_control",
557                ..
558            }
559        ));
560    }
561
562    #[test]
563    fn table_trait_constants() {
564        assert_eq!(<MpeDatagramSection as Table>::TABLE_ID, 0x3E);
565        assert_eq!(<MpeDatagramSection as Table>::PID, 0x0000);
566    }
567
568    #[cfg(feature = "serde")]
569    #[test]
570    fn serde_json_round_trip() {
571        let payload = [0xAB, 0xCD];
572        let bytes = build_mpe(
573            false,
574            true,
575            [0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F],
576            0b01,
577            0b11,
578            true,
579            3,
580            7,
581            &payload,
582            [0xDE, 0xAD, 0xBE, 0xEF],
583        );
584        let sec = MpeDatagramSection::parse(&bytes).unwrap();
585        let j = serde_json::to_string(&sec).unwrap();
586
587        // The borrowed `payload: &[u8]` field cannot be JSON-deserialized
588        // zero-copy (serde_json renders it as a number sequence, not a
589        // borrowed byte array — the same constraint that affects every
590        // borrowed-slice table in the crate). Unlike cat.rs, whose fields are
591        // all owned and so round-trip via `from_str::<Self>`, we exercise the
592        // serde derive through the WIRE form: a re-parse of the same bytes
593        // must serialize to byte-identical JSON. This pins the Serialize impl.
594        let reparsed = MpeDatagramSection::parse(&bytes).unwrap();
595        assert_eq!(serde_json::to_string(&reparsed).unwrap(), j);
596
597        // And confirm the JSON carries the decoded fields: network-order MAC,
598        // both 2-bit scrambling controls, and the preserved checksum trailer.
599        assert!(j.contains("\"mac_address\":[10,11,12,13,14,15]"));
600        assert!(j.contains("\"payload_scrambling_control\":1"));
601        assert!(j.contains("\"address_scrambling_control\":3"));
602        assert!(j.contains("\"checksum\":[222,173,190,239]"));
603    }
604}