Skip to main content

dvb_vbi/
payload.rs

1//! Typed data-unit payloads — ETSI EN 301 775 §4.5–§4.9 (Tables 4, 6, 8, 10,
2//! 12).
3//!
4//! Each [`DataUnitPayload`] variant mirrors one data-field syntax table:
5//!
6//! - [`TeletextDataField`] — §4.5 (Table 4): EBU / Inverted Teletext, the shared
7//!   [`LineHeader`] + an 8-bit `framing_code` + a 42-byte opaque `txt_data_block`
8//!   (`0x02` / `0x03` / `0xC0`). EN 300 706 Teletext coding is out of scope, so
9//!   the block bytes are opaque.
10//! - [`VpsDataField`] — §4.6 (Table 6): VPS, the shared [`LineHeader`] + a
11//!   13-byte `vps_data_block` (`0xC3`).
12//! - [`WssDataField`] — §4.7 (Table 8): WSS, the shared [`LineHeader`] + a 14-bit
13//!   `wss_data_block` + a trailing 2-bit `reserved_future_use` `11` (`0xC4`).
14//! - [`ClosedCaptioningDataField`] — §4.8 (Table 10): Closed Captioning, the
15//!   shared [`LineHeader`] + a 16-bit `closed_captioning_data_block` (`0xC5`).
16//! - [`MonochromeDataField`] — §4.9 (Table 12): monochrome 4:2:2 luminance
17//!   samples (`0xC6`) — its own first-byte packing (two segment flags +
18//!   field_parity + line_offset), `first_pixel_position`, `n_pixels`, then the
19//!   luminance `Y_value` samples.
20//! - [`DataUnitPayload::Stuffing`] — §4.4.1: `0xFF`, no data field.
21//! - [`DataUnitPayload::Opaque`] — reserved / user-defined data_unit_ids whose
22//!   body this crate does not interpret (Table 3: discard); the raw bytes are
23//!   retained for round-trip fidelity.
24
25use alloc::vec::Vec;
26
27use crate::data_unit_id::DataUnitId;
28use crate::error::{Error, Result};
29use crate::line_header::{LINE_HEADER_LEN, LineHeader};
30
31/// Size in bytes of the EBU/Inverted Teletext `txt_data_block` (336 bits, §4.5).
32pub const TXT_DATA_BLOCK_LEN: usize = 42;
33/// Size in bytes of a Teletext data field (header + framing_code + block).
34pub const TELETEXT_FIELD_LEN: usize = LINE_HEADER_LEN + 1 + TXT_DATA_BLOCK_LEN;
35/// The fixed `data_unit_length` for `data_identifier` `0x10`–`0x1F` (`0x2C` =
36/// 44, the Teletext data-field body length — §4.4.2).
37pub const TELETEXT_DATA_UNIT_LENGTH: u8 = 0x2C;
38
39/// EBU Teletext framing_code (`11100100`, §4.5.2).
40pub const FRAMING_CODE_EBU: u8 = 0b1110_0100;
41/// Inverted Teletext framing_code (`00011011`, §4.5.2).
42pub const FRAMING_CODE_INVERTED: u8 = 0b0001_1011;
43
44/// Size in bytes of the VPS `vps_data_block` (104 bits, §4.6).
45pub const VPS_DATA_BLOCK_LEN: usize = 13;
46/// Size in bytes of a VPS data field (header + block).
47pub const VPS_FIELD_LEN: usize = LINE_HEADER_LEN + VPS_DATA_BLOCK_LEN;
48
49/// Size in bytes of a WSS data field (header + 14 wss bits + 2-bit RFU = 3
50/// bytes, §4.7).
51pub const WSS_FIELD_LEN: usize = LINE_HEADER_LEN + 2;
52/// Mask for the 14-bit `wss_data_block` (§4.7).
53pub const WSS_DATA_BLOCK_MASK: u16 = 0x3FFF;
54/// Mask for the lower 6 bits of `wss_data_block` packed into byte 2 of the WSS
55/// field (bits `[5:0]` after the 2-bit RFU tail, §4.7.1).
56const WSS_BYTE2_DATA_MASK: u8 = 0x3F;
57/// The trailing 2-bit `reserved_future_use` (`11`) of a WSS data field (§4.7.1).
58pub const WSS_RESERVED_TAIL: u8 = 0b11;
59
60/// Size in bytes of a Closed Captioning data field (header + 16 CC bits = 3
61/// bytes, §4.8).
62pub const CC_FIELD_LEN: usize = LINE_HEADER_LEN + 2;
63
64/// Size in bytes of the monochrome fixed header preceding the `Y_value` samples:
65/// first byte (flags + parity + line_offset) + 16-bit first_pixel_position +
66/// 8-bit n_pixels (§4.9.1).
67pub const MONO_HEADER_LEN: usize = 4;
68
69/// `first_segment_flag` bit (`[7]`) of the monochrome first byte (§4.9.1).
70const MONO_FIRST_SEGMENT: u8 = 0b1000_0000;
71/// `last_segment_flag` bit (`[6]`) of the monochrome first byte (§4.9.1).
72const MONO_LAST_SEGMENT: u8 = 0b0100_0000;
73/// `field_parity` bit (`[5]`) of the monochrome first byte (§4.9.1).
74const MONO_FIELD_PARITY: u8 = 0b0010_0000;
75/// `line_offset` mask (`[4:0]`) of the monochrome first byte (§4.9.1).
76const MONO_LINE_OFFSET: u8 = 0b0001_1111;
77
78/// EBU / Inverted Teletext data field — ETSI EN 301 775 §4.5.1, Table 4
79/// (`data_unit_id` `0x02`, `0x03`, `0xC0`).
80///
81/// The `txt_data_block` (42 bytes) is the EN 300 706 magazine_and_packet_address
82/// and data_block following the clock-run-in/framing-code; EN 300 706 decoding
83/// is out of scope, so it is held opaquely.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize))]
86pub struct TeletextDataField {
87    /// Shared first-byte header: reserved_future_use `11` | field_parity |
88    /// line_offset.
89    pub header: LineHeader,
90    /// 8-bit `framing_code` (`0b11100100` EBU / `0b00011011` Inverted, §4.5.2).
91    pub framing_code: u8,
92    /// 42-byte `txt_data_block` (336 bits, §4.5; EN 300 706 — opaque here).
93    #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_txt_block"))]
94    pub txt_data_block: [u8; TXT_DATA_BLOCK_LEN],
95}
96
97/// Serialize the 42-byte Teletext block as a byte slice (serde's `Serialize` for
98/// arrays stops at 32 elements).
99#[cfg(feature = "serde")]
100fn serialize_txt_block<S>(
101    block: &[u8; TXT_DATA_BLOCK_LEN],
102    s: S,
103) -> core::result::Result<S::Ok, S::Error>
104where
105    S: serde::Serializer,
106{
107    s.serialize_bytes(block)
108}
109
110impl TeletextDataField {
111    /// Bytes this data field occupies (the `data_unit_length`).
112    pub fn serialized_len(&self) -> usize {
113        TELETEXT_FIELD_LEN
114    }
115
116    /// Parse exactly one Teletext data field from `data` (`data` must be the
117    /// data-unit body of `data_unit_length` bytes).
118    pub fn parse(data: &[u8]) -> Result<Self> {
119        if data.len() < TELETEXT_FIELD_LEN {
120            return Err(Error::BufferTooShort {
121                need: TELETEXT_FIELD_LEN,
122                have: data.len(),
123                what: "txt_data_field",
124            });
125        }
126        let header = LineHeader::from_byte(data[0]);
127        let framing_code = data[1];
128        let mut txt_data_block = [0u8; TXT_DATA_BLOCK_LEN];
129        txt_data_block.copy_from_slice(&data[2..2 + TXT_DATA_BLOCK_LEN]);
130        Ok(TeletextDataField {
131            header,
132            framing_code,
133            txt_data_block,
134        })
135    }
136
137    /// Serialize into `out`, returning the number of bytes written.
138    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
139        if out.len() < TELETEXT_FIELD_LEN {
140            return Err(Error::OutputBufferTooSmall {
141                need: TELETEXT_FIELD_LEN,
142                have: out.len(),
143            });
144        }
145        out[0] = self.header.to_byte()?;
146        out[1] = self.framing_code;
147        out[2..2 + TXT_DATA_BLOCK_LEN].copy_from_slice(&self.txt_data_block);
148        Ok(TELETEXT_FIELD_LEN)
149    }
150}
151
152/// VPS data field — ETSI EN 301 775 §4.6.1, Table 6 (`data_unit_id` `0xC3`).
153///
154/// The `vps_data_block` (13 bytes) is bytes 3..=15 of an EN 300 231 VPS line,
155/// excluding the run-in and start-code byte (§4.6.2).
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157#[cfg_attr(feature = "serde", derive(serde::Serialize))]
158pub struct VpsDataField {
159    /// Shared first-byte header (§4.6.1: field_parity `1`, line_offset `16`).
160    pub header: LineHeader,
161    /// 13-byte `vps_data_block` (104 bits, §4.6; EN 300 231 — opaque here).
162    pub vps_data_block: [u8; VPS_DATA_BLOCK_LEN],
163}
164
165impl VpsDataField {
166    /// Bytes this data field occupies (the `data_unit_length`).
167    pub fn serialized_len(&self) -> usize {
168        VPS_FIELD_LEN
169    }
170
171    /// Parse exactly one VPS data field from `data`.
172    pub fn parse(data: &[u8]) -> Result<Self> {
173        if data.len() < VPS_FIELD_LEN {
174            return Err(Error::BufferTooShort {
175                need: VPS_FIELD_LEN,
176                have: data.len(),
177                what: "vps_data_field",
178            });
179        }
180        let header = LineHeader::from_byte(data[0]);
181        let mut vps_data_block = [0u8; VPS_DATA_BLOCK_LEN];
182        vps_data_block.copy_from_slice(&data[1..1 + VPS_DATA_BLOCK_LEN]);
183        Ok(VpsDataField {
184            header,
185            vps_data_block,
186        })
187    }
188
189    /// Serialize into `out`, returning the number of bytes written.
190    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
191        if out.len() < VPS_FIELD_LEN {
192            return Err(Error::OutputBufferTooSmall {
193                need: VPS_FIELD_LEN,
194                have: out.len(),
195            });
196        }
197        out[0] = self.header.to_byte()?;
198        out[1..1 + VPS_DATA_BLOCK_LEN].copy_from_slice(&self.vps_data_block);
199        Ok(VPS_FIELD_LEN)
200    }
201}
202
203/// WSS data field — ETSI EN 301 775 §4.7.1, Table 8 (`data_unit_id` `0xC4`).
204///
205/// Byte layout (24 bits): byte0 = shared header, then 14 `wss_data_block` bits
206/// followed by a 2-bit `reserved_future_use` `11` tail. So byte1 holds wss bits
207/// `[13:6]` and byte2 holds wss bits `[5:0]` then the 2-bit RFU tail.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209#[cfg_attr(feature = "serde", derive(serde::Serialize))]
210pub struct WssDataField {
211    /// Shared first-byte header (§4.7.1: field_parity `1`, line_offset `23`).
212    pub header: LineHeader,
213    /// 14-bit `wss_data_block` (§4.7; EN 300 294 — value in the low 14 bits).
214    pub wss_data_block: u16,
215}
216
217impl WssDataField {
218    /// Bytes this data field occupies (the `data_unit_length`).
219    pub fn serialized_len(&self) -> usize {
220        WSS_FIELD_LEN
221    }
222
223    /// Parse exactly one WSS data field from `data`. The trailing 2-bit RFU is
224    /// not validated (decoders ignore RFU).
225    pub fn parse(data: &[u8]) -> Result<Self> {
226        if data.len() < WSS_FIELD_LEN {
227            return Err(Error::BufferTooShort {
228                need: WSS_FIELD_LEN,
229                have: data.len(),
230                what: "wss_data_field",
231            });
232        }
233        let header = LineHeader::from_byte(data[0]);
234        // wss bits [13:6] in byte1, [5:0] in the high 6 bits of byte2.
235        let wss_data_block =
236            (((data[1] as u16) << 6) | ((data[2] as u16) >> 2)) & WSS_DATA_BLOCK_MASK;
237        Ok(WssDataField {
238            header,
239            wss_data_block,
240        })
241    }
242
243    /// Serialize into `out`, returning the number of bytes written. The 14-bit
244    /// `wss_data_block` is masked and a `11` RFU tail is emitted.
245    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
246        if out.len() < WSS_FIELD_LEN {
247            return Err(Error::OutputBufferTooSmall {
248                need: WSS_FIELD_LEN,
249                have: out.len(),
250            });
251        }
252        if self.wss_data_block > WSS_DATA_BLOCK_MASK {
253            return Err(Error::FieldTooWide {
254                what: "wss_data_block",
255                value: self.wss_data_block as u32,
256                bits: 14,
257            });
258        }
259        out[0] = self.header.to_byte()?;
260        out[1] = (self.wss_data_block >> 6) as u8;
261        out[2] = (((self.wss_data_block as u8) & WSS_BYTE2_DATA_MASK) << 2) | WSS_RESERVED_TAIL;
262        Ok(WSS_FIELD_LEN)
263    }
264}
265
266/// Closed Captioning data field — ETSI EN 301 775 §4.8.1, Table 10
267/// (`data_unit_id` `0xC5`).
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269#[cfg_attr(feature = "serde", derive(serde::Serialize))]
270pub struct ClosedCaptioningDataField {
271    /// Shared first-byte header (§4.8.1: line_offset `21`).
272    pub header: LineHeader,
273    /// 16-bit `closed_captioning_data_block` (EIA-608 Rev A, §4.8; the two CC
274    /// bytes, MSB = byte one).
275    pub closed_captioning_data_block: u16,
276}
277
278impl ClosedCaptioningDataField {
279    /// Bytes this data field occupies (the `data_unit_length`).
280    pub fn serialized_len(&self) -> usize {
281        CC_FIELD_LEN
282    }
283
284    /// Parse exactly one Closed Captioning data field from `data`.
285    pub fn parse(data: &[u8]) -> Result<Self> {
286        if data.len() < CC_FIELD_LEN {
287            return Err(Error::BufferTooShort {
288                need: CC_FIELD_LEN,
289                have: data.len(),
290                what: "closed_captioning_data_field",
291            });
292        }
293        let header = LineHeader::from_byte(data[0]);
294        let closed_captioning_data_block = u16::from_be_bytes([data[1], data[2]]);
295        Ok(ClosedCaptioningDataField {
296            header,
297            closed_captioning_data_block,
298        })
299    }
300
301    /// Serialize into `out`, returning the number of bytes written.
302    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
303        if out.len() < CC_FIELD_LEN {
304            return Err(Error::OutputBufferTooSmall {
305                need: CC_FIELD_LEN,
306                have: out.len(),
307            });
308        }
309        out[0] = self.header.to_byte()?;
310        out[1..3].copy_from_slice(&self.closed_captioning_data_block.to_be_bytes());
311        Ok(CC_FIELD_LEN)
312    }
313}
314
315/// Monochrome 4:2:2 luminance-sample data field — ETSI EN 301 775 §4.9.1,
316/// Table 12 (`data_unit_id` `0xC6`).
317///
318/// Unlike the other units this packs its first byte as `[7]` first_segment_flag,
319/// `[6]` last_segment_flag, `[5]` field_parity, `[4:0]` line_offset (no 2-bit
320/// reserved prefix), then a 16-bit `first_pixel_position`, an 8-bit `n_pixels`,
321/// and `n_pixels` luminance `Y_value` bytes.
322#[derive(Debug, Clone, PartialEq, Eq)]
323#[cfg_attr(feature = "serde", derive(serde::Serialize))]
324pub struct MonochromeDataField<'a> {
325    /// `first_segment_flag` (`1` for the first segment of a line).
326    pub first_segment: bool,
327    /// `last_segment_flag` (`1` for the last segment of a line).
328    pub last_segment: bool,
329    /// `field_parity` (`1` first field, `0` second field).
330    pub field_parity: bool,
331    /// `line_offset` (5 bits, `7..=23` valid; coded per Table 13).
332    pub line_offset: u8,
333    /// `first_pixel_position` (16 bits, `0..=719`): position of the first coded
334    /// luminance sample of this segment.
335    pub first_pixel_position: u16,
336    /// `Y_value` luminance samples; `n_pixels` is `samples.len()` (`1..=251`).
337    #[cfg_attr(feature = "serde", serde(borrow))]
338    pub samples: &'a [u8],
339}
340
341impl<'a> MonochromeDataField<'a> {
342    /// Bytes this data field occupies (the `data_unit_length`).
343    pub fn serialized_len(&self) -> usize {
344        MONO_HEADER_LEN + self.samples.len()
345    }
346
347    /// Parse exactly one monochrome data field from `data` (`data` is the
348    /// data-unit body of `data_unit_length` bytes). `n_pixels` is taken from the
349    /// wire and the sample slice is borrowed from `data`.
350    pub fn parse(data: &'a [u8]) -> Result<Self> {
351        if data.len() < MONO_HEADER_LEN {
352            return Err(Error::BufferTooShort {
353                need: MONO_HEADER_LEN,
354                have: data.len(),
355                what: "monochrome_data_field header",
356            });
357        }
358        let b0 = data[0];
359        let first_segment = (b0 & MONO_FIRST_SEGMENT) != 0;
360        let last_segment = (b0 & MONO_LAST_SEGMENT) != 0;
361        let field_parity = (b0 & MONO_FIELD_PARITY) != 0;
362        let line_offset = b0 & MONO_LINE_OFFSET;
363        let first_pixel_position = u16::from_be_bytes([data[1], data[2]]);
364        let n_pixels = data[3] as usize;
365        // §4.9.2 mandates n_pixels > 0; a zero-n_pixels unit is non-conformant.
366        if n_pixels == 0 {
367            return Err(Error::InvalidField {
368                what: "n_pixels",
369                reason: "n_pixels shall be > 0 (ETSI EN 301 775 §4.9.2)",
370            });
371        }
372        if data.len() < MONO_HEADER_LEN + n_pixels {
373            return Err(Error::BufferTooShort {
374                need: MONO_HEADER_LEN + n_pixels,
375                have: data.len(),
376                what: "monochrome Y_value samples",
377            });
378        }
379        let samples = &data[MONO_HEADER_LEN..MONO_HEADER_LEN + n_pixels];
380        Ok(MonochromeDataField {
381            first_segment,
382            last_segment,
383            field_parity,
384            line_offset,
385            first_pixel_position,
386            samples,
387        })
388    }
389
390    /// Serialize into `out`, returning the number of bytes written. `n_pixels`
391    /// is derived from `samples.len()`.
392    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
393        let total = self.serialized_len();
394        if out.len() < total {
395            return Err(Error::OutputBufferTooSmall {
396                need: total,
397                have: out.len(),
398            });
399        }
400        if self.line_offset > MONO_LINE_OFFSET {
401            return Err(Error::FieldTooWide {
402                what: "line_offset",
403                value: self.line_offset as u32,
404                bits: 5,
405            });
406        }
407        if self.samples.len() > u8::MAX as usize {
408            return Err(Error::FieldTooWide {
409                what: "n_pixels",
410                value: self.samples.len() as u32,
411                bits: 8,
412            });
413        }
414        let mut b0 = self.line_offset;
415        if self.first_segment {
416            b0 |= MONO_FIRST_SEGMENT;
417        }
418        if self.last_segment {
419            b0 |= MONO_LAST_SEGMENT;
420        }
421        if self.field_parity {
422            b0 |= MONO_FIELD_PARITY;
423        }
424        out[0] = b0;
425        out[1..3].copy_from_slice(&self.first_pixel_position.to_be_bytes());
426        out[3] = self.samples.len() as u8;
427        out[MONO_HEADER_LEN..total].copy_from_slice(self.samples);
428        Ok(total)
429    }
430}
431
432/// The typed body of one data unit (ETSI EN 301 775 §4.4, dispatched on
433/// `data_unit_id`).
434#[derive(Debug, Clone, PartialEq, Eq)]
435#[cfg_attr(feature = "serde", derive(serde::Serialize))]
436#[non_exhaustive]
437pub enum DataUnitPayload<'a> {
438    /// EBU / Inverted Teletext (`0x02`, `0x03`, `0xC0`) — §4.5.
439    Teletext(TeletextDataField),
440    /// VPS (`0xC3`) — §4.6.
441    Vps(VpsDataField),
442    /// WSS (`0xC4`) — §4.7.
443    Wss(WssDataField),
444    /// Closed Captioning (`0xC5`) — §4.8.
445    ClosedCaptioning(ClosedCaptioningDataField),
446    /// Monochrome 4:2:2 luminance samples (`0xC6`) — §4.9.
447    Monochrome(#[cfg_attr(feature = "serde", serde(borrow))] MonochromeDataField<'a>),
448    /// Stuffing (`0xFF`) — §4.4.1: no data field. `data_unit_length` stuffing
449    /// bytes follow and are discarded; their count is retained for round-trip.
450    Stuffing {
451        /// The `data_unit_length` (number of `0xFF` stuffing bytes that follow).
452        length: u8,
453    },
454    /// Reserved / user-defined data_unit_id whose body this crate does not
455    /// interpret (Table 3: discard). The raw body bytes are retained.
456    Opaque(#[cfg_attr(feature = "serde", serde(borrow))] &'a [u8]),
457}
458
459impl<'a> DataUnitPayload<'a> {
460    /// The number of body bytes (`data_unit_length`) this payload serializes to.
461    pub fn serialized_len(&self) -> usize {
462        match self {
463            DataUnitPayload::Teletext(f) => f.serialized_len(),
464            DataUnitPayload::Vps(f) => f.serialized_len(),
465            DataUnitPayload::Wss(f) => f.serialized_len(),
466            DataUnitPayload::ClosedCaptioning(f) => f.serialized_len(),
467            DataUnitPayload::Monochrome(f) => f.serialized_len(),
468            DataUnitPayload::Stuffing { length } => *length as usize,
469            DataUnitPayload::Opaque(b) => b.len(),
470        }
471    }
472
473    /// Parse a data-unit body of `length` bytes against its `id` (§4.4, Table 1
474    /// dispatch, resolved per Table 3). `body` must be exactly the
475    /// `data_unit_length` bytes following the length field.
476    pub fn parse(id: DataUnitId, body: &'a [u8]) -> Result<Self> {
477        match id {
478            DataUnitId::EbuTeletextNonSubtitle
479            | DataUnitId::EbuTeletextSubtitle
480            | DataUnitId::InvertedTeletext => {
481                Ok(DataUnitPayload::Teletext(TeletextDataField::parse(body)?))
482            }
483            DataUnitId::Vps => Ok(DataUnitPayload::Vps(VpsDataField::parse(body)?)),
484            DataUnitId::Wss => Ok(DataUnitPayload::Wss(WssDataField::parse(body)?)),
485            DataUnitId::ClosedCaptioning => Ok(DataUnitPayload::ClosedCaptioning(
486                ClosedCaptioningDataField::parse(body)?,
487            )),
488            DataUnitId::Monochrome422Samples => Ok(DataUnitPayload::Monochrome(
489                MonochromeDataField::parse(body)?,
490            )),
491            DataUnitId::Stuffing => {
492                if body.len() > u8::MAX as usize {
493                    return Err(Error::InvalidDataUnitLength {
494                        length: 0,
495                        id: id.to_u8(),
496                        reason: "stuffing length exceeds 8 bits",
497                    });
498                }
499                Ok(DataUnitPayload::Stuffing {
500                    length: body.len() as u8,
501                })
502            }
503            DataUnitId::Reserved(_) | DataUnitId::UserDefined(_) => {
504                Ok(DataUnitPayload::Opaque(body))
505            }
506        }
507    }
508
509    /// Serialize the body into `out`, returning the number of bytes written.
510    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
511        match self {
512            DataUnitPayload::Teletext(f) => f.serialize_into(out),
513            DataUnitPayload::Vps(f) => f.serialize_into(out),
514            DataUnitPayload::Wss(f) => f.serialize_into(out),
515            DataUnitPayload::ClosedCaptioning(f) => f.serialize_into(out),
516            DataUnitPayload::Monochrome(f) => f.serialize_into(out),
517            DataUnitPayload::Stuffing { length } => {
518                let n = *length as usize;
519                if out.len() < n {
520                    return Err(Error::OutputBufferTooSmall {
521                        need: n,
522                        have: out.len(),
523                    });
524                }
525                for b in out.iter_mut().take(n) {
526                    *b = crate::data_unit_id::ID_STUFFING; // 0xFF
527                }
528                Ok(n)
529            }
530            DataUnitPayload::Opaque(b) => {
531                if out.len() < b.len() {
532                    return Err(Error::OutputBufferTooSmall {
533                        need: b.len(),
534                        have: out.len(),
535                    });
536                }
537                out[..b.len()].copy_from_slice(b);
538                Ok(b.len())
539            }
540        }
541    }
542}
543
544/// One data unit: a `data_unit_id`, its `data_unit_length`, and the typed body
545/// (ETSI EN 301 775 §4.4.1, Table 1 loop body).
546#[derive(Debug, Clone, PartialEq, Eq)]
547#[cfg_attr(feature = "serde", derive(serde::Serialize))]
548pub struct DataUnit<'a> {
549    /// `data_unit_id` (Table 3).
550    pub id: DataUnitId,
551    /// The typed body.
552    #[cfg_attr(feature = "serde", serde(borrow))]
553    pub payload: DataUnitPayload<'a>,
554}
555
556impl<'a> DataUnit<'a> {
557    /// `data_unit_length`: the number of body bytes after the length field.
558    pub fn data_unit_length(&self) -> usize {
559        self.payload.serialized_len()
560    }
561
562    /// Total wire size: `data_unit_id` (1) + `data_unit_length` (1) + body.
563    pub fn serialized_len(&self) -> usize {
564        2 + self.data_unit_length()
565    }
566
567    /// Build a Teletext data unit (EBU non-subtitle by default tag — supply the
568    /// id explicitly via [`DataUnit`] if a subtitle/inverted id is wanted).
569    pub fn teletext(id: DataUnitId, field: TeletextDataField) -> Self {
570        DataUnit {
571            id,
572            payload: DataUnitPayload::Teletext(field),
573        }
574    }
575
576    /// Build a VPS data unit.
577    pub fn vps(field: VpsDataField) -> Self {
578        DataUnit {
579            id: DataUnitId::Vps,
580            payload: DataUnitPayload::Vps(field),
581        }
582    }
583
584    /// Build a WSS data unit.
585    pub fn wss(field: WssDataField) -> Self {
586        DataUnit {
587            id: DataUnitId::Wss,
588            payload: DataUnitPayload::Wss(field),
589        }
590    }
591
592    /// Build a Closed Captioning data unit.
593    pub fn closed_captioning(field: ClosedCaptioningDataField) -> Self {
594        DataUnit {
595            id: DataUnitId::ClosedCaptioning,
596            payload: DataUnitPayload::ClosedCaptioning(field),
597        }
598    }
599
600    /// Build a monochrome 4:2:2 sample data unit.
601    pub fn monochrome(field: MonochromeDataField<'a>) -> Self {
602        DataUnit {
603            id: DataUnitId::Monochrome422Samples,
604            payload: DataUnitPayload::Monochrome(field),
605        }
606    }
607
608    /// Build a stuffing data unit of `length` `0xFF` bytes.
609    pub fn stuffing(length: u8) -> Self {
610        DataUnit {
611            id: DataUnitId::Stuffing,
612            payload: DataUnitPayload::Stuffing { length },
613        }
614    }
615
616    /// Parse a single data unit from the start of `data`, returning it and the
617    /// number of bytes consumed.
618    pub fn parse(data: &'a [u8]) -> Result<(Self, usize)> {
619        if data.len() < 2 {
620            return Err(Error::BufferTooShort {
621                need: 2,
622                have: data.len(),
623                what: "data_unit header (id + length)",
624            });
625        }
626        let id = DataUnitId::from_u8(data[0]);
627        let length = data[1] as usize;
628        let body_end = 2 + length;
629        if data.len() < body_end {
630            return Err(Error::BufferTooShort {
631                need: body_end,
632                have: data.len(),
633                what: "data_unit body",
634            });
635        }
636        let body = &data[2..body_end];
637        let payload = DataUnitPayload::parse(id, body)?;
638        // Spec fidelity: the typed payload must occupy exactly data_unit_length
639        // bytes (no truncation / no over-read).
640        if payload.serialized_len() != length {
641            return Err(Error::InvalidDataUnitLength {
642                length: data[1],
643                id: data[0],
644                reason: "typed payload size does not match data_unit_length",
645            });
646        }
647        Ok((DataUnit { id, payload }, body_end))
648    }
649
650    /// Serialize the data unit into `out`, returning bytes written.
651    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
652        let total = self.serialized_len();
653        if out.len() < total {
654            return Err(Error::OutputBufferTooSmall {
655                need: total,
656                have: out.len(),
657            });
658        }
659        let length = self.data_unit_length();
660        if length > u8::MAX as usize {
661            return Err(Error::FieldTooWide {
662                what: "data_unit_length",
663                value: length as u32,
664                bits: 8,
665            });
666        }
667        out[0] = self.id.to_u8();
668        out[1] = length as u8;
669        let written = self.payload.serialize_into(&mut out[2..total])?;
670        debug_assert_eq!(written, length);
671        Ok(2 + written)
672    }
673}
674
675/// The PES data field — ETSI EN 301 775 §4.4.1, Table 1.
676///
677/// A `data_identifier` (Table 2) followed by a sequence of [`DataUnit`]s that
678/// fill the PES packet payload. `parse` walks the units until the buffer is
679/// exhausted; `serialize_into` emits them back-to-back.
680#[derive(Debug, Clone, PartialEq, Eq)]
681#[cfg_attr(feature = "serde", derive(serde::Serialize))]
682pub struct DataField<'a> {
683    /// `data_identifier` (8 bits, Table 2): `0x10`–`0x1F` or `0x99`–`0x9B` for
684    /// VBI; other values are reserved/user-defined (retained verbatim).
685    pub data_identifier: u8,
686    /// The data units carried in this PES data field.
687    #[cfg_attr(feature = "serde", serde(borrow))]
688    pub data_units: Vec<DataUnit<'a>>,
689}
690
691impl<'a> DataField<'a> {
692    /// Construct a data field from a `data_identifier` and its data units.
693    pub fn new(data_identifier: u8, data_units: Vec<DataUnit<'a>>) -> Self {
694        DataField {
695            data_identifier,
696            data_units,
697        }
698    }
699
700    /// Total wire size: 1 (`data_identifier`) + every data unit.
701    pub fn serialized_len(&self) -> usize {
702        1 + self
703            .data_units
704            .iter()
705            .map(DataUnit::serialized_len)
706            .sum::<usize>()
707    }
708
709    /// Parse a PES data field from `data`: the `data_identifier` byte then a
710    /// run of data units until the buffer is exhausted.
711    pub fn parse(data: &'a [u8]) -> Result<Self> {
712        if data.is_empty() {
713            return Err(Error::BufferTooShort {
714                need: 1,
715                have: 0,
716                what: "data_identifier",
717            });
718        }
719        let data_identifier = data[0];
720        let mut data_units = Vec::new();
721        let mut off = 1;
722        while off < data.len() {
723            let (unit, consumed) = DataUnit::parse(&data[off..])?;
724            data_units.push(unit);
725            off += consumed;
726        }
727        Ok(DataField {
728            data_identifier,
729            data_units,
730        })
731    }
732
733    /// Serialize the data field into `out`, returning bytes written.
734    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
735        let total = self.serialized_len();
736        if out.len() < total {
737            return Err(Error::OutputBufferTooSmall {
738                need: total,
739                have: out.len(),
740            });
741        }
742        out[0] = self.data_identifier;
743        let mut off = 1;
744        for unit in &self.data_units {
745            off += unit.serialize_into(&mut out[off..])?;
746        }
747        Ok(off)
748    }
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754    use crate::line_header::LineHeader;
755    use alloc::vec;
756
757    // Round-trip one data unit through exact wire bytes and reparse.
758    fn unit_round_trip(unit: &DataUnit, expected_wire: &[u8]) {
759        let mut out = vec![0u8; unit.serialized_len()];
760        let n = unit.serialize_into(&mut out).unwrap();
761        assert_eq!(n, unit.serialized_len());
762        assert_eq!(out, expected_wire, "exact wire bytes");
763        let (re, consumed) = DataUnit::parse(&out).unwrap();
764        assert_eq!(consumed, out.len());
765        assert_eq!(&re, unit, "reparse must equal the original");
766    }
767
768    #[test]
769    fn teletext_exact_wire_bytes() {
770        let block = [0xAAu8; TXT_DATA_BLOCK_LEN];
771        let field = TeletextDataField {
772            header: LineHeader::new(true, 7), // parity=1, line_offset=7
773            framing_code: FRAMING_CODE_EBU,
774            txt_data_block: block,
775        };
776        let unit = DataUnit::teletext(DataUnitId::EbuTeletextSubtitle, field);
777
778        // id 0x03, length 0x2C (44), header byte 11 1 00111 = 0xE7, framing 0xE4.
779        let mut expected = vec![0x03, 0x2C, 0xE7, FRAMING_CODE_EBU];
780        expected.extend_from_slice(&block);
781        assert_eq!(unit.data_unit_length(), TELETEXT_DATA_UNIT_LENGTH as usize);
782        unit_round_trip(&unit, &expected);
783    }
784
785    #[test]
786    fn vps_exact_wire_bytes() {
787        let block = [
788            0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
789        ];
790        let field = VpsDataField {
791            header: LineHeader::new(true, 16), // parity=1, line_offset=16
792            vps_data_block: block,
793        };
794        let unit = DataUnit::vps(field);
795
796        // id 0xC3, length 14 (0x0E), header 11 1 10000 = 0xF0.
797        let mut expected = vec![0xC3, 0x0E, 0xF0];
798        expected.extend_from_slice(&block);
799        unit_round_trip(&unit, &expected);
800    }
801
802    #[test]
803    fn wss_exact_wire_bytes_and_bit_packing() {
804        // wss_data_block = 0x3A5C (14 bits: 11 1010 0101 1100).
805        let field = WssDataField {
806            header: LineHeader::new(true, 23), // parity=1, line_offset=23
807            wss_data_block: 0x3A5C,
808        };
809        let unit = DataUnit::wss(field);
810
811        // header 11 1 10111 = 0xF7.
812        // byte1 = wss[13:6] = 0x3A5C >> 6 = 0xE9.
813        // byte2 = (wss[5:0] << 2) | 11 = ((0x3A5C & 0x3F) << 2) | 3
814        //       = (0x1C << 2) | 3 = 0x70 | 3 = 0x73.
815        let expected = vec![0xC4, 0x03, 0xF7, 0xE9, 0x73];
816        unit_round_trip(&unit, &expected);
817    }
818
819    #[test]
820    fn cc_exact_wire_bytes() {
821        let field = ClosedCaptioningDataField {
822            header: LineHeader::new(false, 21), // parity=0, line_offset=21
823            closed_captioning_data_block: 0x9425,
824        };
825        let unit = DataUnit::closed_captioning(field);
826
827        // header 11 0 10101 = 0xD5, then 0x94 0x25.
828        let expected = vec![0xC5, 0x03, 0xD5, 0x94, 0x25];
829        unit_round_trip(&unit, &expected);
830    }
831
832    #[test]
833    fn monochrome_exact_wire_bytes() {
834        let samples = [0x10u8, 0x40, 0x80, 0xEB];
835        let field = MonochromeDataField {
836            first_segment: true,
837            last_segment: false,
838            field_parity: true,
839            line_offset: 10,
840            first_pixel_position: 0x0123,
841            samples: &samples,
842        };
843        let unit = DataUnit::monochrome(field);
844
845        // b0 = 1 0 1 01010 = 0xAA. fpp = 0x0123. n_pixels = 4.
846        let mut expected = vec![0xC6, 0x08, 0xAA, 0x01, 0x23, 0x04];
847        expected.extend_from_slice(&samples);
848        unit_round_trip(&unit, &expected);
849    }
850
851    #[test]
852    fn stuffing_exact_wire_bytes() {
853        let unit = DataUnit::stuffing(3);
854        // id 0xFF, length 3, then three 0xFF stuffing bytes.
855        let expected = vec![0xFF, 0x03, 0xFF, 0xFF, 0xFF];
856        unit_round_trip(&unit, &expected);
857    }
858
859    #[test]
860    fn opaque_reserved_round_trips() {
861        let body = [0xDEu8, 0xAD, 0xBE];
862        let unit = DataUnit {
863            id: DataUnitId::Reserved(0x55),
864            payload: DataUnitPayload::Opaque(&body),
865        };
866        let expected = vec![0x55, 0x03, 0xDE, 0xAD, 0xBE];
867        unit_round_trip(&unit, &expected);
868    }
869
870    // Mutation bite: changing a typed field changes the serialized wire bytes.
871    #[test]
872    fn mutating_a_field_changes_wire_bytes() {
873        let block = [0u8; VPS_DATA_BLOCK_LEN];
874        let a = DataUnit::vps(VpsDataField {
875            header: LineHeader::new(true, 16),
876            vps_data_block: block,
877        });
878        let mut block_b = block;
879        block_b[0] = 0xFF;
880        let b = DataUnit::vps(VpsDataField {
881            header: LineHeader::new(true, 16),
882            vps_data_block: block_b,
883        });
884
885        let mut out_a = vec![0u8; a.serialized_len()];
886        a.serialize_into(&mut out_a).unwrap();
887        let mut out_b = vec![0u8; b.serialized_len()];
888        b.serialize_into(&mut out_b).unwrap();
889        assert_ne!(
890            out_a, out_b,
891            "different vps_data_block must change wire bytes"
892        );
893
894        // Mutating the line header (parity) also bites.
895        let c = DataUnit::vps(VpsDataField {
896            header: LineHeader::new(false, 16),
897            vps_data_block: block,
898        });
899        let mut out_c = vec![0u8; c.serialized_len()];
900        c.serialize_into(&mut out_c).unwrap();
901        assert_ne!(out_a, out_c, "field_parity must change the header byte");
902        assert_eq!(out_a[2] & 0b0010_0000, 0b0010_0000);
903        assert_eq!(out_c[2] & 0b0010_0000, 0);
904    }
905
906    // ≥2-data_unit boundary test: VPS + WSS + a teletext unit in one data field.
907    #[test]
908    fn multi_unit_data_field_round_trip() {
909        let vps = DataUnit::vps(VpsDataField {
910            header: LineHeader::new(true, 16),
911            vps_data_block: [0x11; VPS_DATA_BLOCK_LEN],
912        });
913        let wss = DataUnit::wss(WssDataField {
914            header: LineHeader::new(true, 23),
915            wss_data_block: 0x1234,
916        });
917        let block = [0x42u8; TXT_DATA_BLOCK_LEN];
918        let txt = DataUnit::teletext(
919            DataUnitId::EbuTeletextNonSubtitle,
920            TeletextDataField {
921                header: LineHeader::new(false, 9),
922                framing_code: FRAMING_CODE_EBU,
923                txt_data_block: block,
924            },
925        );
926
927        let field = DataField::new(0x10, vec![vps, wss, txt]);
928        let mut out = vec![0u8; field.serialized_len()];
929        let n = field.serialize_into(&mut out).unwrap();
930        assert_eq!(n, field.serialized_len());
931
932        // First byte = data_identifier.
933        assert_eq!(out[0], 0x10);
934
935        let parsed = DataField::parse(&out).unwrap();
936        assert_eq!(parsed, field, "multi-unit data field must round-trip");
937        assert_eq!(parsed.data_units.len(), 3);
938        assert_eq!(parsed.data_units[0].id, DataUnitId::Vps);
939        assert_eq!(parsed.data_units[1].id, DataUnitId::Wss);
940        assert_eq!(parsed.data_units[2].id, DataUnitId::EbuTeletextNonSubtitle);
941
942        // Byte-exact re-serialize.
943        let mut out2 = vec![0u8; parsed.serialized_len()];
944        parsed.serialize_into(&mut out2).unwrap();
945        assert_eq!(out, out2);
946    }
947
948    #[test]
949    fn rejects_truncated_data_unit() {
950        // id 0xC3 (VPS) claims length 14 but body is short.
951        let data = [0xC3u8, 0x0E, 0x00];
952        assert!(DataUnit::parse(&data).is_err());
953    }
954
955    #[test]
956    fn rejects_length_mismatch() {
957        // VPS body that's longer than the fixed 14 -> typed size != length.
958        let data = [
959            0xC3u8, 0x0F, 0xF0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
960        ];
961        assert!(matches!(
962            DataUnit::parse(&data),
963            Err(Error::InvalidDataUnitLength { .. })
964        ));
965    }
966
967    #[test]
968    fn wss_bit_packing_recovers_value() {
969        for v in [0u16, 1, 0x3FFF, 0x2A55, 0x1FFF] {
970            let f = WssDataField {
971                header: LineHeader::new(true, 23),
972                wss_data_block: v,
973            };
974            let mut out = [0u8; WSS_FIELD_LEN];
975            f.serialize_into(&mut out).unwrap();
976            let re = WssDataField::parse(&out).unwrap();
977            assert_eq!(re.wss_data_block, v, "wss value {v:#06X}");
978            // RFU tail must be 11.
979            assert_eq!(out[2] & 0b11, WSS_RESERVED_TAIL);
980        }
981    }
982
983    // Finding 1: n_pixels=0 must be rejected (ETSI EN 301 775 §4.9.2).
984    #[test]
985    fn monochrome_rejects_zero_n_pixels() {
986        // b0 = 1 1 1 00111 (first+last segment, field_parity=1, line_offset=7).
987        // first_pixel_position = 0x0000. n_pixels = 0x00 (invalid).
988        let data = [0b1110_0111u8, 0x00, 0x00, 0x00];
989        let result = MonochromeDataField::parse(&data);
990        assert!(
991            matches!(
992                result,
993                Err(crate::error::Error::InvalidField {
994                    what: "n_pixels",
995                    ..
996                })
997            ),
998            "n_pixels=0 must be rejected with InvalidField, got: {result:?}"
999        );
1000    }
1001
1002    // Finding 3: mutation-bite tests for each remaining typed payload.
1003
1004    #[test]
1005    fn mutating_teletext_framing_code_changes_wire_bytes() {
1006        let block = [0xBBu8; TXT_DATA_BLOCK_LEN];
1007        let a = DataUnit::teletext(
1008            DataUnitId::EbuTeletextNonSubtitle,
1009            TeletextDataField {
1010                header: LineHeader::new(true, 7),
1011                framing_code: FRAMING_CODE_EBU,
1012                txt_data_block: block,
1013            },
1014        );
1015        let b = DataUnit::teletext(
1016            DataUnitId::EbuTeletextNonSubtitle,
1017            TeletextDataField {
1018                header: LineHeader::new(true, 7),
1019                framing_code: FRAMING_CODE_INVERTED,
1020                txt_data_block: block,
1021            },
1022        );
1023        let mut out_a = vec![0u8; a.serialized_len()];
1024        a.serialize_into(&mut out_a).unwrap();
1025        let mut out_b = vec![0u8; b.serialized_len()];
1026        b.serialize_into(&mut out_b).unwrap();
1027        assert_ne!(
1028            out_a, out_b,
1029            "different framing_code must change wire bytes"
1030        );
1031    }
1032
1033    #[test]
1034    fn mutating_teletext_txt_data_block_changes_wire_bytes() {
1035        let block_a = [0x00u8; TXT_DATA_BLOCK_LEN];
1036        let mut block_b = block_a;
1037        block_b[0] = 0xFF;
1038        let a = DataUnit::teletext(
1039            DataUnitId::EbuTeletextNonSubtitle,
1040            TeletextDataField {
1041                header: LineHeader::new(true, 7),
1042                framing_code: FRAMING_CODE_EBU,
1043                txt_data_block: block_a,
1044            },
1045        );
1046        let b = DataUnit::teletext(
1047            DataUnitId::EbuTeletextNonSubtitle,
1048            TeletextDataField {
1049                header: LineHeader::new(true, 7),
1050                framing_code: FRAMING_CODE_EBU,
1051                txt_data_block: block_b,
1052            },
1053        );
1054        let mut out_a = vec![0u8; a.serialized_len()];
1055        a.serialize_into(&mut out_a).unwrap();
1056        let mut out_b = vec![0u8; b.serialized_len()];
1057        b.serialize_into(&mut out_b).unwrap();
1058        assert_ne!(
1059            out_a, out_b,
1060            "different txt_data_block must change wire bytes"
1061        );
1062    }
1063
1064    #[test]
1065    fn mutating_wss_data_block_changes_wire_bytes() {
1066        let a = DataUnit::wss(WssDataField {
1067            header: LineHeader::new(true, 23),
1068            wss_data_block: 0x0000,
1069        });
1070        let b = DataUnit::wss(WssDataField {
1071            header: LineHeader::new(true, 23),
1072            wss_data_block: 0x3FFF,
1073        });
1074        let mut out_a = vec![0u8; a.serialized_len()];
1075        a.serialize_into(&mut out_a).unwrap();
1076        let mut out_b = vec![0u8; b.serialized_len()];
1077        b.serialize_into(&mut out_b).unwrap();
1078        assert_ne!(
1079            out_a, out_b,
1080            "different wss_data_block must change wire bytes"
1081        );
1082    }
1083
1084    #[test]
1085    fn mutating_cc_data_block_changes_wire_bytes() {
1086        let a = DataUnit::closed_captioning(ClosedCaptioningDataField {
1087            header: LineHeader::new(false, 21),
1088            closed_captioning_data_block: 0x0000,
1089        });
1090        let b = DataUnit::closed_captioning(ClosedCaptioningDataField {
1091            header: LineHeader::new(false, 21),
1092            closed_captioning_data_block: 0xFFFF,
1093        });
1094        let mut out_a = vec![0u8; a.serialized_len()];
1095        a.serialize_into(&mut out_a).unwrap();
1096        let mut out_b = vec![0u8; b.serialized_len()];
1097        b.serialize_into(&mut out_b).unwrap();
1098        assert_ne!(
1099            out_a, out_b,
1100            "different closed_captioning_data_block must change wire bytes"
1101        );
1102    }
1103
1104    #[test]
1105    fn mutating_monochrome_first_segment_flag_changes_wire_bytes() {
1106        let samples = [0x10u8, 0x80];
1107        let a = DataUnit::monochrome(MonochromeDataField {
1108            first_segment: true,
1109            last_segment: false,
1110            field_parity: true,
1111            line_offset: 10,
1112            first_pixel_position: 0,
1113            samples: &samples,
1114        });
1115        let b = DataUnit::monochrome(MonochromeDataField {
1116            first_segment: false,
1117            last_segment: false,
1118            field_parity: true,
1119            line_offset: 10,
1120            first_pixel_position: 0,
1121            samples: &samples,
1122        });
1123        let mut out_a = vec![0u8; a.serialized_len()];
1124        a.serialize_into(&mut out_a).unwrap();
1125        let mut out_b = vec![0u8; b.serialized_len()];
1126        b.serialize_into(&mut out_b).unwrap();
1127        assert_ne!(
1128            out_a, out_b,
1129            "different first_segment flag must change the first wire byte"
1130        );
1131    }
1132
1133    #[test]
1134    fn mutating_monochrome_y_sample_changes_wire_bytes() {
1135        let samples_a = [0x10u8, 0x80];
1136        let mut samples_b = samples_a;
1137        samples_b[0] = 0xFF;
1138        let a = DataUnit::monochrome(MonochromeDataField {
1139            first_segment: true,
1140            last_segment: true,
1141            field_parity: true,
1142            line_offset: 10,
1143            first_pixel_position: 0,
1144            samples: &samples_a,
1145        });
1146        let b = DataUnit::monochrome(MonochromeDataField {
1147            first_segment: true,
1148            last_segment: true,
1149            field_parity: true,
1150            line_offset: 10,
1151            first_pixel_position: 0,
1152            samples: &samples_b,
1153        });
1154        let mut out_a = vec![0u8; a.serialized_len()];
1155        a.serialize_into(&mut out_a).unwrap();
1156        let mut out_b = vec![0u8; b.serialized_len()];
1157        b.serialize_into(&mut out_b).unwrap();
1158        assert_ne!(out_a, out_b, "different Y sample must change wire bytes");
1159    }
1160
1161    // Finding 5: exhaustiveness cross-check that every non-opaque DataUnitId
1162    // variant maps to a typed DataUnitPayload arm. This is intentionally NOT a
1163    // declarative macro dispatch (the dispatch is a hand-written match in
1164    // DataUnitPayload::parse); this test ensures a future variant added to
1165    // DataUnitId without a matching payload branch fails CI rather than silently
1166    // falling to Opaque.
1167    #[test]
1168    fn every_non_opaque_data_unit_id_has_a_typed_payload() {
1169        use crate::data_unit_id::{
1170            ID_CLOSED_CAPTIONING, ID_EBU_TELETEXT_NON_SUBTITLE, ID_EBU_TELETEXT_SUBTITLE,
1171            ID_INVERTED_TELETEXT, ID_MONOCHROME_422_SAMPLES, ID_STUFFING, ID_VPS, ID_WSS,
1172        };
1173        // Every ID that must produce a typed (non-Opaque) payload.
1174        let typed_ids: &[u8] = &[
1175            ID_EBU_TELETEXT_NON_SUBTITLE,
1176            ID_EBU_TELETEXT_SUBTITLE,
1177            ID_INVERTED_TELETEXT,
1178            ID_VPS,
1179            ID_WSS,
1180            ID_CLOSED_CAPTIONING,
1181            // Monochrome needs n_pixels > 0; supply a minimal 1-pixel body.
1182            ID_MONOCHROME_422_SAMPLES,
1183            ID_STUFFING,
1184        ];
1185
1186        // Minimal valid bodies for each (body = data_unit_length bytes).
1187        let teletext_body = {
1188            let mut b = vec![0u8; TELETEXT_FIELD_LEN];
1189            // header = canonical RFU=11, parity=1, line_offset=7.
1190            b[0] = 0xE7;
1191            b[1] = FRAMING_CODE_EBU;
1192            b
1193        };
1194        let vps_body = {
1195            let mut b = vec![0u8; VPS_FIELD_LEN];
1196            b[0] = 0xF0; // header
1197            b
1198        };
1199        let wss_body = {
1200            let mut b = vec![0u8; WSS_FIELD_LEN];
1201            b[0] = 0xF7; // header
1202            b
1203        };
1204        let cc_body = {
1205            let mut b = vec![0u8; CC_FIELD_LEN];
1206            b[0] = 0xD5; // header
1207            b
1208        };
1209        // Monochrome: 4-byte header + 1 Y sample (n_pixels=1).
1210        let mono_body = vec![0b1110_0111u8, 0x00, 0x00, 0x01, 0x80];
1211        let stuffing_body = vec![0xFFu8; 3];
1212
1213        let bodies: &[&[u8]] = &[
1214            &teletext_body,
1215            &teletext_body,
1216            &teletext_body,
1217            &vps_body,
1218            &wss_body,
1219            &cc_body,
1220            &mono_body,
1221            &stuffing_body,
1222        ];
1223
1224        for (&id, &body) in typed_ids.iter().zip(bodies.iter()) {
1225            let du_id = DataUnitId::from_u8(id);
1226            let payload = DataUnitPayload::parse(du_id, body)
1227                .unwrap_or_else(|e| panic!("id={id:#04X} parse failed: {e}"));
1228            assert!(
1229                !matches!(payload, DataUnitPayload::Opaque(_)),
1230                "id={id:#04X} ({}) fell to Opaque — add a typed dispatch arm",
1231                DataUnitId::from_u8(id).name()
1232            );
1233        }
1234    }
1235}