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