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