Skip to main content

dvb_si/descriptors/
extension.rs

1//! Extension descriptor — ETSI EN 300 468 §6.2.18.1 (tag `0x7F`).
2//!
3//! The extension descriptor is a container whose first payload byte,
4//! `descriptor_tag_extension`, selects one of a large family of sub-descriptors
5//! (EN 300 468 §6.4.0, Table 109 "Possible locations of extended descriptors").
6//! The framing is Table 54:
7//!
8//! ```text
9//!  byte 0      descriptor_tag           (0x7F)
10//!  byte 1      descriptor_length
11//!  byte 2      descriptor_tag_extension (the discriminant)
12//!  byte 3..    selector_byte[]          (the sub-descriptor body)
13//! ```
14//!
15//! This type mirrors the SAT precedent (`tables/sat.rs`): a typed discriminant
16//! ([`ExtensionDescriptor::tag_extension`], a plain `u8` so unknown values
17//! round-trip) plus a typed body enum ([`ExtensionBody`]) with a
18//! [`ExtensionBody::Raw`] fall-through. [`ExtensionTag`] names the known
19//! discriminant constants but is deliberately NOT used as the stored field type
20//! — unknown tags must survive a parse→serialize round-trip, which a
21//! `TryFromPrimitive` enum could not guarantee.
22//!
23//! # Typed vs. raw bodies
24//!
25//! A body variant is typed only when its syntax table is vendored under
26//! `dvb-si/docs/`. For loop-heavy descriptors the first fixed level is typed and
27//! the variable-length inner loop is kept as a raw slice (SAT precedent —
28//! `tables/sat.rs` keeps bit-packed loops raw). Per-variant section comments
29//! cite the governing table + clause.
30//!
31//! Typed (syntax table vendored in `en_300_468.md`, except TTML in
32//! `en_303_560_ttml.md`):
33//! - `0x04` T2_delivery_system (Table 133, §6.4.6.3) — first level; cell loop raw.
34//! - `0x06` supplementary_audio (Table 153, §6.4.11).
35//! - `0x07` network_change_notify (Table 149, §6.4.9) — cell loop raw.
36//! - `0x08` message (Table 148, §6.4.9).
37//! - `0x09` target_region (Table 156, §6.4.12) — region loop raw.
38//! - `0x0A` target_region_name (Table 157, §6.4.13) — region loop raw.
39//! - `0x0B` service_relocated (Table 152, §6.4.10).
40//! - `0x0D` C2_delivery_system (Table 115, §6.4.6.1).
41//! - `0x13` URI_linkage (Table 159, §6.4.16.1) — uri/private split typed.
42//! - `0x15` AC-4 (annex D syntax table, §D.5) — first level; toc/extra raw.
43//! - `0x16` C2_bundle_delivery_system (Table 139, §6.4.6.4) — full fixed loop.
44//! - `0x17` S2X_satellite_delivery_system (Table 140, §6.4.6.5.2) — primary
45//!   channel typed; channel-bonding / reserved tail kept raw.
46//! - `0x19` audio_preselection (Table 110, §6.4.1) — preselection loop raw.
47//! - `0x20` TTML_subtitling (`en_303_560_ttml.md` Table 1, §5.2.1.1).
48//!
49//! Kept [`ExtensionBody::Raw`] (tag value preserved), with reason:
50//! - `0x00` image_icon — syntax vendored (Table 145) but niche (carousel icons); deferred.
51//! - `0x01` cpcm_delivery_signalling — spec not vendored (ETSI TS 102 825).
52//! - `0x02` CP / `0x03` CP_identifier — spec not vendored (ETSI TS 102 825).
53//! - `0x05` SH_delivery_system — niche (satellite-to-handheld); deferred.
54//! - `0x0C` XAIT_PID — deferred (TS 102 727 PDF vendored, no extracted syntax table yet).
55//! - `0x0E` DTS-HD / `0x0F` DTS_Neural / `0x21` DTS-UHD — spec not vendored (annex G/L).
56//! - `0x10` video_depth_range — niche (3D disparity); deferred.
57//! - `0x11` T2MI — niche (T2-MI encapsulation); deferred.
58//! - `0x14` CI_ancillary_data — spec not vendored (ETSI TS 103 205).
59//! - `0x18` protection_message — spec not vendored (ETSI TS 102 809).
60//! - `0x22` service_prominence / `0x23` vvc_subpictures / `0x24` S2Xv2 — niche; deferred.
61//! - any other value (incl. `0x80`..=`0xFF` user-defined) — unknown; preserved.
62
63use crate::error::{Error, Result};
64use crate::text::{DvbText, LangCode};
65use crate::traits::Descriptor;
66use dvb_common::{Parse, Serialize};
67
68/// Descriptor tag for extension_descriptor (EN 300 468 Table 54, §6.2.18.1).
69pub const TAG: u8 = 0x7F;
70const HEADER_LEN: usize = 2;
71/// `descriptor_tag_extension` occupies one byte immediately after the header.
72const TAG_EXTENSION_LEN: usize = 1;
73/// Minimum body length: just the `descriptor_tag_extension` byte.
74const MIN_BODY_LEN: usize = TAG_EXTENSION_LEN;
75/// `descriptor_length` is a single byte; a serialized body may not exceed this.
76const MAX_DESCRIPTOR_LENGTH: usize = 0xFF;
77
78// Per-variant fixed lengths (bytes after `descriptor_tag_extension`).
79const ISO_639_LEN: usize = 3;
80const T2_FIXED_PREFIX_LEN: usize = 3; // plp_id(1) + T2_system_id(2)
81const T2_FLAGS_BLOCK_LEN: usize = 2; // SISO_MISO..tfs_flag, packed in 2 bytes
82const C2_LEN: usize = 7; // plp + data_slice + freq(4) + 1 packed byte
83const C2_BUNDLE_ENTRY_LEN: usize = 8; // plp + data_slice + freq(4) + 1 packed + 1 (primary(1)+reserved_zero(7))
84const SERVICE_RELOCATED_LEN: usize = 6; // 3 × u16
85/// S2X primary-channel block after the 2 flags bytes (excl. scrambling/ISI/timeslice):
86/// frequency(4) + orbital_position(2) + 1 packed byte + symbol_rate(4 bytes).
87const S2X_PRIMARY_LEN: usize = 11;
88const S2X_SCRAMBLING_LEN: usize = 3;
89const TTML_FIXED_LEN: usize = ISO_639_LEN + 2; // ISO_639(3) + 2 packed bytes
90
91/// Known `descriptor_tag_extension` values (EN 300 468 Table 109, §6.4.0).
92///
93/// This is a *naming* aid for callers and parser dispatch; the stored
94/// discriminant is the raw [`ExtensionDescriptor::tag_extension`] `u8` so that
95/// unknown / reserved / user-defined tags round-trip unchanged.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98#[non_exhaustive]
99#[repr(u8)]
100pub enum ExtensionTag {
101    /// image_icon_descriptor (kept raw — see module docs).
102    ImageIcon = 0x00,
103    /// T2_delivery_system_descriptor.
104    T2DeliverySystem = 0x04,
105    /// supplementary_audio_descriptor.
106    SupplementaryAudio = 0x06,
107    /// network_change_notify_descriptor.
108    NetworkChangeNotify = 0x07,
109    /// message_descriptor.
110    Message = 0x08,
111    /// target_region_descriptor.
112    TargetRegion = 0x09,
113    /// target_region_name_descriptor.
114    TargetRegionName = 0x0A,
115    /// service_relocated_descriptor.
116    ServiceRelocated = 0x0B,
117    /// C2_delivery_system_descriptor.
118    C2DeliverySystem = 0x0D,
119    /// URI_linkage_descriptor.
120    UriLinkage = 0x13,
121    /// AC-4_descriptor (annex D).
122    Ac4 = 0x15,
123    /// C2_bundle_delivery_system_descriptor.
124    C2BundleDeliverySystem = 0x16,
125    /// S2X_satellite_delivery_system_descriptor.
126    S2XSatelliteDeliverySystem = 0x17,
127    /// audio_preselection_descriptor.
128    AudioPreselection = 0x19,
129    /// TTML_subtitling_descriptor (ETSI EN 303 560).
130    TtmlSubtitling = 0x20,
131}
132
133/// Typed body of an extension descriptor, keyed on `descriptor_tag_extension`.
134///
135/// Unrecognised or not-yet-typed discriminants land in [`ExtensionBody::Raw`],
136/// which carries the selector bytes verbatim so the descriptor round-trips.
137#[derive(Debug, Clone, PartialEq, Eq)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize))] // Deserialize dropped: Message contains DvbText
139pub enum ExtensionBody<'a> {
140    /// `0x04` — T2_delivery_system (Table 133, §6.4.6.3).
141    T2DeliverySystem(#[cfg_attr(feature = "serde", serde(borrow))] T2DeliverySystem<'a>),
142    /// `0x06` — supplementary_audio (Table 153, §6.4.11).
143    SupplementaryAudio(#[cfg_attr(feature = "serde", serde(borrow))] SupplementaryAudio<'a>),
144    /// `0x07` — network_change_notify (Table 149, §6.4.9).
145    NetworkChangeNotify(#[cfg_attr(feature = "serde", serde(borrow))] NetworkChangeNotify<'a>),
146    /// `0x08` — message (Table 148, §6.4.9).
147    Message(#[cfg_attr(feature = "serde", serde(borrow))] Message<'a>),
148    /// `0x09` — target_region (Table 156, §6.4.12).
149    TargetRegion(#[cfg_attr(feature = "serde", serde(borrow))] TargetRegion<'a>),
150    /// `0x0A` — target_region_name (Table 157, §6.4.13).
151    TargetRegionName(#[cfg_attr(feature = "serde", serde(borrow))] TargetRegionName<'a>),
152    /// `0x0B` — service_relocated (Table 152, §6.4.10).
153    ServiceRelocated(ServiceRelocated),
154    /// `0x0D` — C2_delivery_system (Table 115, §6.4.6.1).
155    C2DeliverySystem(C2DeliverySystem),
156    /// `0x13` — URI_linkage (Table 159, §6.4.16.1).
157    UriLinkage(#[cfg_attr(feature = "serde", serde(borrow))] UriLinkage<'a>),
158    /// `0x15` — AC-4 (annex D).
159    Ac4(#[cfg_attr(feature = "serde", serde(borrow))] Ac4<'a>),
160    /// `0x16` — C2_bundle_delivery_system (Table 139, §6.4.6.4).
161    C2BundleDeliverySystem(C2BundleDeliverySystem),
162    /// `0x17` — S2X_satellite_delivery_system (Table 140, §6.4.6.5.2).
163    S2XSatelliteDeliverySystem(
164        #[cfg_attr(feature = "serde", serde(borrow))] S2XSatelliteDeliverySystem<'a>,
165    ),
166    /// `0x19` — audio_preselection (Table 110, §6.4.1).
167    AudioPreselection(#[cfg_attr(feature = "serde", serde(borrow))] AudioPreselection<'a>),
168    /// `0x20` — TTML_subtitling (EN 303 560 Table 1, §5.2.1.1).
169    TtmlSubtitling(#[cfg_attr(feature = "serde", serde(borrow))] TtmlSubtitling<'a>),
170    /// Any not-yet-typed / unknown / user-defined discriminant: selector bytes verbatim.
171    Raw(#[cfg_attr(feature = "serde", serde(borrow))] &'a [u8]),
172}
173
174// ===========================================================================
175//  Section 0x04 — T2_delivery_system_descriptor (Table 133, §6.4.6.3)
176// ---------------------------------------------------------------------------
177//  plp_id(8) T2_system_id(16) then, if descriptor_length > 4, a packed flags
178//  block (SISO_MISO 2 | bandwidth 4 | reserved 2 ; guard 3 | tx_mode 3 | off 1 |
179//  tfs 1) followed by a variable cell loop (cells carry tfs-conditional
180//  frequency lists + subcell loops). The cell loop is length-irregular and is
181//  kept raw per the SAT precedent; the always-present prefix is typed.
182// ===========================================================================
183/// T2_delivery_system body (Table 133). `cell_loop` is the raw remainder.
184#[derive(Debug, Clone, PartialEq, Eq)]
185#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
186#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
187pub struct T2DeliverySystem<'a> {
188    /// PLP identifier.
189    pub plp_id: u8,
190    /// T2 system identifier.
191    pub t2_system_id: u16,
192    /// SISO_MISO(2), present iff `descriptor_length > 4` (flags block present).
193    pub siso_miso: Option<u8>,
194    /// bandwidth(4), present with `siso_miso`.
195    pub bandwidth: Option<u8>,
196    /// guard_interval(3), present with `siso_miso`.
197    pub guard_interval: Option<u8>,
198    /// transmission_mode(3), present with `siso_miso`.
199    pub transmission_mode: Option<u8>,
200    /// other_frequency_flag(1), present with `siso_miso`.
201    pub other_frequency_flag: Option<bool>,
202    /// tfs_flag(1), present with `siso_miso`.
203    pub tfs_flag: Option<bool>,
204    /// Raw cell loop (Table 133 inner `for`), kept raw (SAT precedent).
205    #[cfg_attr(feature = "serde", serde(borrow))]
206    pub cell_loop: &'a [u8],
207}
208
209// ===========================================================================
210//  Section 0x06 — supplementary_audio_descriptor (Table 153, §6.4.11)
211// ===========================================================================
212/// supplementary_audio body (Table 153).
213#[derive(Debug, Clone, PartialEq, Eq)]
214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
215#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
216pub struct SupplementaryAudio<'a> {
217    /// mix_type(1) — Table 154.
218    pub mix_type: bool,
219    /// editorial_classification(5) — Table 155.
220    pub editorial_classification: u8,
221    /// language_code_present(1).
222    pub language_code_present: bool,
223    /// ISO_639_language_code(24), present iff `language_code_present`.
224    pub iso_639_language_code: Option<LangCode>,
225    /// Trailing private_data_byte run.
226    #[cfg_attr(feature = "serde", serde(borrow))]
227    pub private_data: &'a [u8],
228}
229
230// ===========================================================================
231//  Section 0x07 — network_change_notify_descriptor (Table 149, §6.4.9)
232// ---------------------------------------------------------------------------
233//  Two-level loop: per cell_id a length-delimited inner change loop whose
234//  entries carry conditional invariant-TS fields. Kept raw (SAT precedent).
235// ===========================================================================
236/// network_change_notify body (Table 149); `cell_loop` is the raw outer loop.
237#[derive(Debug, Clone, PartialEq, Eq)]
238#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
239#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
240pub struct NetworkChangeNotify<'a> {
241    /// Raw `for(cell)` loop body.
242    #[cfg_attr(feature = "serde", serde(borrow))]
243    pub cell_loop: &'a [u8],
244}
245
246// ===========================================================================
247//  Section 0x08 — message_descriptor (Table 148, §6.4.9)
248// ===========================================================================
249/// message body (Table 148).
250#[derive(Debug, Clone, PartialEq, Eq)]
251#[cfg_attr(feature = "serde", derive(serde::Serialize))] // Deserialize dropped: DvbText is serialize-only
252pub struct Message<'a> {
253    /// message_id(8).
254    pub message_id: u8,
255    /// ISO_639_language_code(24).
256    pub iso_639_language_code: LangCode,
257    /// DVB Annex-A encoded text_char run (remainder of body).
258    pub text: DvbText<'a>,
259}
260
261// ===========================================================================
262//  Section 0x09 — target_region_descriptor (Table 156, §6.4.12)
263// ---------------------------------------------------------------------------
264//  Leading country_code(24) then a region loop whose entries are
265//  region_depth-conditional; the loop is kept raw (SAT precedent).
266// ===========================================================================
267/// target_region body (Table 156); `region_loop` is the raw remainder.
268#[derive(Debug, Clone, PartialEq, Eq)]
269#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
270#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
271pub struct TargetRegion<'a> {
272    /// Leading country_code(24).
273    pub country_code: LangCode,
274    /// Raw region loop.
275    #[cfg_attr(feature = "serde", serde(borrow))]
276    pub region_loop: &'a [u8],
277}
278
279// ===========================================================================
280//  Section 0x0A — target_region_name_descriptor (Table 157, §6.4.13)
281// ===========================================================================
282/// target_region_name body (Table 157); `region_loop` is the raw remainder.
283#[derive(Debug, Clone, PartialEq, Eq)]
284#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
285#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
286pub struct TargetRegionName<'a> {
287    /// country_code(24).
288    pub country_code: LangCode,
289    /// ISO_639_language_code(24).
290    pub iso_639_language_code: LangCode,
291    /// Raw region loop (length-delimited name entries).
292    #[cfg_attr(feature = "serde", serde(borrow))]
293    pub region_loop: &'a [u8],
294}
295
296// ===========================================================================
297//  Section 0x0B — service_relocated_descriptor (Table 152, §6.4.10)
298// ===========================================================================
299/// service_relocated body (Table 152) — fully typed, fixed 6 bytes.
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
302pub struct ServiceRelocated {
303    /// old_original_network_id(16).
304    pub old_original_network_id: u16,
305    /// old_transport_stream_id(16).
306    pub old_transport_stream_id: u16,
307    /// old_service_id(16).
308    pub old_service_id: u16,
309}
310
311// ===========================================================================
312//  Section 0x0D — C2_delivery_system_descriptor (Table 115, §6.4.6.1)
313// ===========================================================================
314/// C2_delivery_system body (Table 115) — fully typed, fixed 7 bytes.
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
317pub struct C2DeliverySystem {
318    /// plp_id(8).
319    pub plp_id: u8,
320    /// data_slice_id(8).
321    pub data_slice_id: u8,
322    /// C2_System_tuning_frequency(32).
323    pub c2_system_tuning_frequency: u32,
324    /// C2_System_tuning_frequency_type(2).
325    pub c2_system_tuning_frequency_type: u8,
326    /// active_OFDM_symbol_duration(3).
327    pub active_ofdm_symbol_duration: u8,
328    /// guard_interval(3).
329    pub guard_interval: u8,
330}
331
332// ===========================================================================
333//  Section 0x13 — URI_linkage_descriptor (Table 159, §6.4.16.1)
334// ---------------------------------------------------------------------------
335//  uri_linkage_type, length-delimited URI, an optional min_polling_interval
336//  (only for types 0x00/0x01), then trailing private_data. All typed.
337// ===========================================================================
338/// URI_linkage body (Table 159).
339#[derive(Debug, Clone, PartialEq, Eq)]
340#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
341#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
342pub struct UriLinkage<'a> {
343    /// uri_linkage_type(8).
344    pub uri_linkage_type: u8,
345    /// Length-delimited URI bytes.
346    #[cfg_attr(feature = "serde", serde(borrow))]
347    pub uri: &'a [u8],
348    /// min_polling_interval(16), present iff `uri_linkage_type` is 0x00 or 0x01.
349    pub min_polling_interval: Option<u16>,
350    /// Trailing private_data_byte run.
351    #[cfg_attr(feature = "serde", serde(borrow))]
352    pub private_data: &'a [u8],
353}
354
355// ===========================================================================
356//  Section 0x15 — AC-4_descriptor (annex D, §D.5)
357// ---------------------------------------------------------------------------
358//  Two flags + a packed config byte (when ac4_config_flag set), a
359//  length-delimited TOC, then additional_info bytes. The TOC + extra are kept
360//  raw; flags + config are typed.
361// ===========================================================================
362/// AC-4 body (annex D). `toc` + `additional_info` are raw.
363#[derive(Debug, Clone, PartialEq, Eq)]
364#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
365#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
366pub struct Ac4<'a> {
367    /// ac4_config_flag(1).
368    pub ac4_config_flag: bool,
369    /// ac4_toc_flag(1).
370    pub ac4_toc_flag: bool,
371    /// ac4_dialog_enhancement_enabled(1), present iff `ac4_config_flag`.
372    pub ac4_dialog_enhancement_enabled: Option<bool>,
373    /// ac4_channel_mode(2), present iff `ac4_config_flag`.
374    pub ac4_channel_mode: Option<u8>,
375    /// Length-delimited ac4_dsi bytes, present iff `ac4_toc_flag`.
376    #[cfg_attr(feature = "serde", serde(borrow))]
377    pub toc: Option<&'a [u8]>,
378    /// Trailing additional_info_byte run.
379    #[cfg_attr(feature = "serde", serde(borrow))]
380    pub additional_info: &'a [u8],
381}
382
383// ===========================================================================
384//  Section 0x16 — C2_bundle_delivery_system_descriptor (Table 139, §6.4.6.4)
385// ---------------------------------------------------------------------------
386//  A flat array of fixed 9-byte entries; fully typed.
387// ===========================================================================
388/// One C2 bundle entry (Table 139 inner loop).
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
391pub struct C2BundleEntry {
392    /// plp_id(8).
393    pub plp_id: u8,
394    /// data_slice_id(8).
395    pub data_slice_id: u8,
396    /// C2_System_tuning_frequency(32).
397    pub c2_system_tuning_frequency: u32,
398    /// C2_System_tuning_frequency_type(2).
399    pub c2_system_tuning_frequency_type: u8,
400    /// active_OFDM_symbol_duration(3).
401    pub active_ofdm_symbol_duration: u8,
402    /// guard_interval(3).
403    pub guard_interval: u8,
404    /// primary_channel(1).
405    pub primary_channel: bool,
406}
407
408/// C2_bundle_delivery_system body (Table 139) — fully typed.
409#[derive(Debug, Clone, PartialEq, Eq)]
410#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
411pub struct C2BundleDeliverySystem {
412    /// Bundle entries in wire order.
413    pub entries: Vec<C2BundleEntry>,
414}
415
416// ===========================================================================
417//  Section 0x17 — S2X_satellite_delivery_system_descriptor (Table 140, §6.4.6.5.2)
418// ---------------------------------------------------------------------------
419//  Primary-channel fields are typed. The S2X_mode==3 channel-bonding loop and
420//  the trailing reserved_future_use bytes are irregular and kept raw (SAT
421//  precedent); `tail` holds everything after the primary input_stream_identifier
422//  / timeslice_number.
423// ===========================================================================
424/// S2X_satellite_delivery_system body (Table 140); `tail` is the raw remainder.
425#[derive(Debug, Clone, PartialEq, Eq)]
426#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
427#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
428pub struct S2XSatelliteDeliverySystem<'a> {
429    /// receiver_profiles(5) — Table 141.
430    pub receiver_profiles: u8,
431    /// S2X_mode(2) — Table 142.
432    pub s2x_mode: u8,
433    /// scrambling_sequence_selector(1).
434    pub scrambling_sequence_selector: bool,
435    /// TS_GS_S2X_mode(2) — Table 143.
436    pub ts_gs_s2x_mode: u8,
437    /// scrambling_sequence_index(18), present iff `scrambling_sequence_selector`.
438    pub scrambling_sequence_index: Option<u32>,
439    /// frequency(32) — primary channel.
440    pub frequency: u32,
441    /// orbital_position(16).
442    pub orbital_position: u16,
443    /// west_east_flag(1).
444    pub west_east_flag: bool,
445    /// polarization(2).
446    pub polarization: u8,
447    /// multiple_input_stream_flag(1).
448    pub multiple_input_stream_flag: bool,
449    /// roll_off(3) — Table 144.
450    pub roll_off: u8,
451    /// symbol_rate(28).
452    pub symbol_rate: u32,
453    /// input_stream_identifier(8), present iff `multiple_input_stream_flag`.
454    pub input_stream_identifier: Option<u8>,
455    /// timeslice_number(8), present iff `s2x_mode == 2`.
456    pub timeslice_number: Option<u8>,
457    /// Raw remainder: S2X_mode==3 channel-bond loop + reserved tail.
458    #[cfg_attr(feature = "serde", serde(borrow))]
459    pub tail: &'a [u8],
460}
461
462// ===========================================================================
463//  Section 0x19 — audio_preselection_descriptor (Table 110, §6.4.1)
464// ---------------------------------------------------------------------------
465//  num_preselections then a variable preselection loop whose entries carry
466//  conditional language / message / aux-component / future-extension fields.
467//  The loop is kept raw (SAT precedent); the count byte is typed.
468// ===========================================================================
469/// audio_preselection body (Table 110); `preselection_loop` is the raw remainder.
470#[derive(Debug, Clone, PartialEq, Eq)]
471#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
472#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
473pub struct AudioPreselection<'a> {
474    /// num_preselections(5).
475    pub num_preselections: u8,
476    /// Raw preselection loop.
477    #[cfg_attr(feature = "serde", serde(borrow))]
478    pub preselection_loop: &'a [u8],
479}
480
481// ===========================================================================
482//  Section 0x20 — TTML_subtitling_descriptor (EN 303 560 Table 1, §5.2.1.1)
483// ---------------------------------------------------------------------------
484//  Fixed lead-in, a profile array, optional qualifier(32), optional font list,
485//  a length-delimited text field, then trailing reserved bytes. The profile
486//  list, optional qualifier, font list, text and trailing reserved bytes are
487//  kept raw (`tail`); the fixed lead-in is typed.
488// ===========================================================================
489/// TTML_subtitling body (EN 303 560 Table 1); `tail` is the raw remainder.
490#[derive(Debug, Clone, PartialEq, Eq)]
491#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
492#[cfg_attr(feature = "serde", serde(bound(deserialize = "'de: 'a")))]
493pub struct TtmlSubtitling<'a> {
494    /// ISO_639_language_code(24).
495    pub iso_639_language_code: LangCode,
496    /// subtitle_purpose(6) — Table 2.
497    pub subtitle_purpose: u8,
498    /// TTS_suitability(2) — Table 3.
499    pub tts_suitability: u8,
500    /// essential_font_usage_flag(1).
501    pub essential_font_usage_flag: bool,
502    /// qualifier_present_flag(1).
503    pub qualifier_present_flag: bool,
504    /// dvb_ttml_profile_count(4).
505    pub dvb_ttml_profile_count: u8,
506    /// Raw remainder: profile list + optional qualifier + font list + text + reserved.
507    #[cfg_attr(feature = "serde", serde(borrow))]
508    pub tail: &'a [u8],
509}
510
511/// Extension descriptor (EN 300 468 Table 54, §6.2.18.1).
512#[derive(Debug, Clone, PartialEq, Eq)]
513#[cfg_attr(feature = "serde", derive(serde::Serialize))] // Deserialize dropped: ExtensionBody contains DvbText
514pub struct ExtensionDescriptor<'a> {
515    /// `descriptor_tag_extension` (raw `u8`; see [`ExtensionTag`] for names).
516    pub tag_extension: u8,
517    /// Typed body, or [`ExtensionBody::Raw`] for not-yet-typed discriminants.
518    pub body: ExtensionBody<'a>,
519}
520
521impl ExtensionDescriptor<'_> {
522    /// Typed view of [`Self::tag_extension`], or `None` if not a known tag.
523    #[must_use]
524    pub fn kind(&self) -> Option<ExtensionTag> {
525        Some(match self.tag_extension {
526            0x00 => ExtensionTag::ImageIcon,
527            0x04 => ExtensionTag::T2DeliverySystem,
528            0x06 => ExtensionTag::SupplementaryAudio,
529            0x07 => ExtensionTag::NetworkChangeNotify,
530            0x08 => ExtensionTag::Message,
531            0x09 => ExtensionTag::TargetRegion,
532            0x0A => ExtensionTag::TargetRegionName,
533            0x0B => ExtensionTag::ServiceRelocated,
534            0x0D => ExtensionTag::C2DeliverySystem,
535            0x13 => ExtensionTag::UriLinkage,
536            0x15 => ExtensionTag::Ac4,
537            0x16 => ExtensionTag::C2BundleDeliverySystem,
538            0x17 => ExtensionTag::S2XSatelliteDeliverySystem,
539            0x19 => ExtensionTag::AudioPreselection,
540            0x20 => ExtensionTag::TtmlSubtitling,
541            _ => return None,
542        })
543    }
544}
545
546// ---------------------------------------------------------------------------
547//  Body parsers (each consumes the selector bytes after descriptor_tag_extension)
548// ---------------------------------------------------------------------------
549
550fn invalid(reason: &'static str) -> Error {
551    Error::InvalidDescriptor { tag: TAG, reason }
552}
553
554fn parse_t2(sel: &[u8]) -> Result<T2DeliverySystem<'_>> {
555    if sel.len() < T2_FIXED_PREFIX_LEN {
556        return Err(invalid("T2_delivery_system: prefix truncated"));
557    }
558    let plp_id = sel[0];
559    let t2_system_id = u16::from_be_bytes([sel[1], sel[2]]);
560    let mut pos = T2_FIXED_PREFIX_LEN;
561    // descriptor_length > 4 ⇔ the optional packed flags block is present
562    // (the body is plp + system_id = 3 bytes when absent; >3 ⇒ block present).
563    let (siso_miso, bandwidth, guard_interval, transmission_mode, other_frequency_flag, tfs_flag) =
564        if sel.len() > T2_FIXED_PREFIX_LEN {
565            if sel.len() < T2_FIXED_PREFIX_LEN + T2_FLAGS_BLOCK_LEN {
566                return Err(invalid("T2_delivery_system: flags block truncated"));
567            }
568            let b0 = sel[pos];
569            let b1 = sel[pos + 1];
570            pos += T2_FLAGS_BLOCK_LEN;
571            (
572                Some(b0 >> 6),
573                Some((b0 >> 2) & 0x0F),
574                Some(b1 >> 5),
575                Some((b1 >> 2) & 0x07),
576                Some((b1 & 0x02) != 0),
577                Some((b1 & 0x01) != 0),
578            )
579        } else {
580            (None, None, None, None, None, None)
581        };
582    Ok(T2DeliverySystem {
583        plp_id,
584        t2_system_id,
585        siso_miso,
586        bandwidth,
587        guard_interval,
588        transmission_mode,
589        other_frequency_flag,
590        tfs_flag,
591        cell_loop: &sel[pos..],
592    })
593}
594
595fn parse_supplementary_audio(sel: &[u8]) -> Result<SupplementaryAudio<'_>> {
596    if sel.is_empty() {
597        return Err(invalid("supplementary_audio: flags byte missing"));
598    }
599    let flags = sel[0];
600    let mix_type = (flags & 0x80) != 0;
601    let editorial_classification = (flags >> 2) & 0x1F;
602    let language_code_present = (flags & 0x01) != 0;
603    let mut pos = 1;
604    let iso_639_language_code = if language_code_present {
605        if sel.len() < pos + ISO_639_LEN {
606            return Err(invalid("supplementary_audio: language code truncated"));
607        }
608        let lc = &sel[pos..pos + ISO_639_LEN];
609        pos += ISO_639_LEN;
610        Some(LangCode([lc[0], lc[1], lc[2]]))
611    } else {
612        None
613    };
614    Ok(SupplementaryAudio {
615        mix_type,
616        editorial_classification,
617        language_code_present,
618        iso_639_language_code,
619        private_data: &sel[pos..],
620    })
621}
622
623fn parse_message(sel: &[u8]) -> Result<Message<'_>> {
624    if sel.len() < 1 + ISO_639_LEN {
625        return Err(invalid("message: header truncated"));
626    }
627    Ok(Message {
628        message_id: sel[0],
629        iso_639_language_code: LangCode([sel[1], sel[2], sel[3]]),
630        text: DvbText::new(&sel[1 + ISO_639_LEN..]),
631    })
632}
633
634fn parse_target_region(sel: &[u8]) -> Result<TargetRegion<'_>> {
635    if sel.len() < ISO_639_LEN {
636        return Err(invalid("target_region: country_code truncated"));
637    }
638    Ok(TargetRegion {
639        country_code: LangCode([sel[0], sel[1], sel[2]]),
640        region_loop: &sel[ISO_639_LEN..],
641    })
642}
643
644fn parse_target_region_name(sel: &[u8]) -> Result<TargetRegionName<'_>> {
645    if sel.len() < 2 * ISO_639_LEN {
646        return Err(invalid("target_region_name: header truncated"));
647    }
648    Ok(TargetRegionName {
649        country_code: LangCode([sel[0], sel[1], sel[2]]),
650        iso_639_language_code: LangCode([sel[3], sel[4], sel[5]]),
651        region_loop: &sel[2 * ISO_639_LEN..],
652    })
653}
654
655fn parse_service_relocated(sel: &[u8]) -> Result<ServiceRelocated> {
656    if sel.len() < SERVICE_RELOCATED_LEN {
657        return Err(invalid("service_relocated: truncated"));
658    }
659    Ok(ServiceRelocated {
660        old_original_network_id: u16::from_be_bytes([sel[0], sel[1]]),
661        old_transport_stream_id: u16::from_be_bytes([sel[2], sel[3]]),
662        old_service_id: u16::from_be_bytes([sel[4], sel[5]]),
663    })
664}
665
666fn parse_c2(sel: &[u8]) -> Result<C2DeliverySystem> {
667    if sel.len() < C2_LEN {
668        return Err(invalid("C2_delivery_system: truncated"));
669    }
670    let packed = sel[6];
671    Ok(C2DeliverySystem {
672        plp_id: sel[0],
673        data_slice_id: sel[1],
674        c2_system_tuning_frequency: u32::from_be_bytes([sel[2], sel[3], sel[4], sel[5]]),
675        c2_system_tuning_frequency_type: packed >> 6,
676        active_ofdm_symbol_duration: (packed >> 3) & 0x07,
677        guard_interval: packed & 0x07,
678    })
679}
680
681fn parse_uri_linkage(sel: &[u8]) -> Result<UriLinkage<'_>> {
682    if sel.len() < 2 {
683        return Err(invalid("URI_linkage: header truncated"));
684    }
685    let uri_linkage_type = sel[0];
686    let uri_length = sel[1] as usize;
687    let mut pos = 2;
688    if sel.len() < pos + uri_length {
689        return Err(invalid("URI_linkage: uri overruns body"));
690    }
691    let uri = &sel[pos..pos + uri_length];
692    pos += uri_length;
693    let min_polling_interval = if uri_linkage_type == 0x00 || uri_linkage_type == 0x01 {
694        if sel.len() < pos + 2 {
695            return Err(invalid("URI_linkage: min_polling_interval truncated"));
696        }
697        let v = u16::from_be_bytes([sel[pos], sel[pos + 1]]);
698        pos += 2;
699        Some(v)
700    } else {
701        None
702    };
703    Ok(UriLinkage {
704        uri_linkage_type,
705        uri,
706        min_polling_interval,
707        private_data: &sel[pos..],
708    })
709}
710
711fn parse_ac4(sel: &[u8]) -> Result<Ac4<'_>> {
712    if sel.is_empty() {
713        return Err(invalid("AC-4: flags byte missing"));
714    }
715    let flags = sel[0];
716    let ac4_config_flag = (flags & 0x80) != 0;
717    let ac4_toc_flag = (flags & 0x40) != 0;
718    let mut pos = 1;
719    let (ac4_dialog_enhancement_enabled, ac4_channel_mode) = if ac4_config_flag {
720        if sel.len() < pos + 1 {
721            return Err(invalid("AC-4: config byte truncated"));
722        }
723        let c = sel[pos];
724        pos += 1;
725        (Some((c & 0x80) != 0), Some((c >> 5) & 0x03))
726    } else {
727        (None, None)
728    };
729    let toc = if ac4_toc_flag {
730        if sel.len() < pos + 1 {
731            return Err(invalid("AC-4: toc length truncated"));
732        }
733        let toc_len = sel[pos] as usize;
734        pos += 1;
735        if sel.len() < pos + toc_len {
736            return Err(invalid("AC-4: toc overruns body"));
737        }
738        let t = &sel[pos..pos + toc_len];
739        pos += toc_len;
740        Some(t)
741    } else {
742        None
743    };
744    Ok(Ac4 {
745        ac4_config_flag,
746        ac4_toc_flag,
747        ac4_dialog_enhancement_enabled,
748        ac4_channel_mode,
749        toc,
750        additional_info: &sel[pos..],
751    })
752}
753
754fn parse_c2_bundle(sel: &[u8]) -> Result<C2BundleDeliverySystem> {
755    if sel.len() % C2_BUNDLE_ENTRY_LEN != 0 {
756        return Err(invalid(
757            "C2_bundle_delivery_system: not a whole number of entries",
758        ));
759    }
760    let mut entries = Vec::with_capacity(sel.len() / C2_BUNDLE_ENTRY_LEN);
761    for chunk in sel.chunks_exact(C2_BUNDLE_ENTRY_LEN) {
762        let packed = chunk[6];
763        entries.push(C2BundleEntry {
764            plp_id: chunk[0],
765            data_slice_id: chunk[1],
766            c2_system_tuning_frequency: u32::from_be_bytes([
767                chunk[2], chunk[3], chunk[4], chunk[5],
768            ]),
769            c2_system_tuning_frequency_type: packed >> 6,
770            active_ofdm_symbol_duration: (packed >> 3) & 0x07,
771            guard_interval: packed & 0x07,
772            primary_channel: (chunk[7] & 0x80) != 0,
773        });
774    }
775    Ok(C2BundleDeliverySystem { entries })
776}
777
778fn parse_s2x(sel: &[u8]) -> Result<S2XSatelliteDeliverySystem<'_>> {
779    // receiver_profiles byte + S2X mode/flags byte = 2 fixed bytes.
780    if sel.len() < 2 {
781        return Err(invalid("S2X: flags truncated"));
782    }
783    let receiver_profiles = sel[0] >> 3;
784    let b1 = sel[1];
785    // Table 140 byte 1, MSB-first: S2X_mode(2) scrambling_sequence_selector(1)
786    // reserved_zero_future_use(3) TS_GS_S2X_mode(2).
787    let s2x_mode = (b1 >> 6) & 0x03;
788    let scrambling_sequence_selector = (b1 & 0x20) != 0;
789    let ts_gs_s2x_mode = b1 & 0x03;
790    let mut pos = 2;
791    let scrambling_sequence_index = if scrambling_sequence_selector {
792        if sel.len() < pos + S2X_SCRAMBLING_LEN {
793            return Err(invalid("S2X: scrambling_sequence_index truncated"));
794        }
795        let idx = (u32::from(sel[pos] & 0x03) << 16)
796            | (u32::from(sel[pos + 1]) << 8)
797            | u32::from(sel[pos + 2]);
798        pos += S2X_SCRAMBLING_LEN;
799        Some(idx)
800    } else {
801        None
802    };
803    // Primary channel (Table 140): frequency(32) orbital_position(16)
804    //   packed byte = west_east(1) polarization(2) mis(1) reserved(1) roll_off(3)
805    //   then reserved(4) | symbol_rate[27:24], and 3 bytes symbol_rate[23:0].
806    if sel.len() < pos + S2X_PRIMARY_LEN {
807        return Err(invalid("S2X: primary channel truncated"));
808    }
809    let frequency = u32::from_be_bytes([sel[pos], sel[pos + 1], sel[pos + 2], sel[pos + 3]]);
810    let orbital_position = u16::from_be_bytes([sel[pos + 4], sel[pos + 5]]);
811    let pb = sel[pos + 6];
812    let west_east_flag = (pb & 0x80) != 0;
813    let polarization = (pb >> 5) & 0x03;
814    let multiple_input_stream_flag = (pb & 0x10) != 0;
815    let roll_off = pb & 0x07;
816    let symbol_rate = (u32::from(sel[pos + 7] & 0x0F) << 24)
817        | (u32::from(sel[pos + 8]) << 16)
818        | (u32::from(sel[pos + 9]) << 8)
819        | u32::from(sel[pos + 10]);
820    pos += S2X_PRIMARY_LEN;
821    let input_stream_identifier = if multiple_input_stream_flag {
822        if sel.len() < pos + 1 {
823            return Err(invalid("S2X: input_stream_identifier truncated"));
824        }
825        let isi = sel[pos];
826        pos += 1;
827        Some(isi)
828    } else {
829        None
830    };
831    let timeslice_number = if s2x_mode == 2 {
832        if sel.len() < pos + 1 {
833            return Err(invalid("S2X: timeslice_number truncated"));
834        }
835        let ts = sel[pos];
836        pos += 1;
837        Some(ts)
838    } else {
839        None
840    };
841    Ok(S2XSatelliteDeliverySystem {
842        receiver_profiles,
843        s2x_mode,
844        scrambling_sequence_selector,
845        ts_gs_s2x_mode,
846        scrambling_sequence_index,
847        frequency,
848        orbital_position,
849        west_east_flag,
850        polarization,
851        multiple_input_stream_flag,
852        roll_off,
853        symbol_rate,
854        input_stream_identifier,
855        timeslice_number,
856        tail: &sel[pos..],
857    })
858}
859
860fn parse_audio_preselection(sel: &[u8]) -> Result<AudioPreselection<'_>> {
861    if sel.is_empty() {
862        return Err(invalid("audio_preselection: count byte missing"));
863    }
864    Ok(AudioPreselection {
865        num_preselections: sel[0] >> 3,
866        preselection_loop: &sel[1..],
867    })
868}
869
870fn parse_ttml(sel: &[u8]) -> Result<TtmlSubtitling<'_>> {
871    if sel.len() < TTML_FIXED_LEN {
872        return Err(invalid("TTML_subtitling: header truncated"));
873    }
874    let b3 = sel[ISO_639_LEN];
875    let b4 = sel[ISO_639_LEN + 1];
876    Ok(TtmlSubtitling {
877        iso_639_language_code: LangCode([sel[0], sel[1], sel[2]]),
878        subtitle_purpose: b3 >> 2,
879        tts_suitability: b3 & 0x03,
880        essential_font_usage_flag: (b4 & 0x80) != 0,
881        qualifier_present_flag: (b4 & 0x40) != 0,
882        dvb_ttml_profile_count: b4 & 0x0F,
883        tail: &sel[TTML_FIXED_LEN..],
884    })
885}
886
887fn parse_body(tag_extension: u8, sel: &[u8]) -> Result<ExtensionBody<'_>> {
888    Ok(match tag_extension {
889        0x04 => ExtensionBody::T2DeliverySystem(parse_t2(sel)?),
890        0x06 => ExtensionBody::SupplementaryAudio(parse_supplementary_audio(sel)?),
891        0x07 => ExtensionBody::NetworkChangeNotify(NetworkChangeNotify { cell_loop: sel }),
892        0x08 => ExtensionBody::Message(parse_message(sel)?),
893        0x09 => ExtensionBody::TargetRegion(parse_target_region(sel)?),
894        0x0A => ExtensionBody::TargetRegionName(parse_target_region_name(sel)?),
895        0x0B => ExtensionBody::ServiceRelocated(parse_service_relocated(sel)?),
896        0x0D => ExtensionBody::C2DeliverySystem(parse_c2(sel)?),
897        0x13 => ExtensionBody::UriLinkage(parse_uri_linkage(sel)?),
898        0x15 => ExtensionBody::Ac4(parse_ac4(sel)?),
899        0x16 => ExtensionBody::C2BundleDeliverySystem(parse_c2_bundle(sel)?),
900        0x17 => ExtensionBody::S2XSatelliteDeliverySystem(parse_s2x(sel)?),
901        0x19 => ExtensionBody::AudioPreselection(parse_audio_preselection(sel)?),
902        0x20 => ExtensionBody::TtmlSubtitling(parse_ttml(sel)?),
903        _ => ExtensionBody::Raw(sel),
904    })
905}
906
907impl<'a> Parse<'a> for ExtensionDescriptor<'a> {
908    type Error = crate::error::Error;
909    fn parse(bytes: &'a [u8]) -> Result<Self> {
910        if bytes.len() < HEADER_LEN {
911            return Err(Error::BufferTooShort {
912                need: HEADER_LEN,
913                have: bytes.len(),
914                what: "ExtensionDescriptor header",
915            });
916        }
917        if bytes[0] != TAG {
918            return Err(Error::InvalidDescriptor {
919                tag: bytes[0],
920                reason: "unexpected tag for extension_descriptor",
921            });
922        }
923        let length = bytes[1] as usize;
924        let end = HEADER_LEN + length;
925        if bytes.len() < end {
926            return Err(Error::BufferTooShort {
927                need: end,
928                have: bytes.len(),
929                what: "ExtensionDescriptor body",
930            });
931        }
932        if length < MIN_BODY_LEN {
933            return Err(Error::InvalidDescriptor {
934                tag: TAG,
935                reason: "body must contain at least the descriptor_tag_extension byte",
936            });
937        }
938        let tag_extension = bytes[HEADER_LEN];
939        let sel = &bytes[HEADER_LEN + TAG_EXTENSION_LEN..end];
940        let body = parse_body(tag_extension, sel)?;
941        Ok(Self {
942            tag_extension,
943            body,
944        })
945    }
946}
947
948// ---------------------------------------------------------------------------
949//  Body serializers — report selector length + write the selector bytes
950// ---------------------------------------------------------------------------
951
952impl ExtensionBody<'_> {
953    /// Selector-byte length (everything after `descriptor_tag_extension`).
954    fn selector_len(&self) -> usize {
955        match self {
956            ExtensionBody::T2DeliverySystem(b) => {
957                T2_FIXED_PREFIX_LEN
958                    + if b.siso_miso.is_some() {
959                        T2_FLAGS_BLOCK_LEN
960                    } else {
961                        0
962                    }
963                    + b.cell_loop.len()
964            }
965            ExtensionBody::SupplementaryAudio(b) => {
966                1 + b.iso_639_language_code.map_or(0, |_| ISO_639_LEN) + b.private_data.len()
967            }
968            ExtensionBody::NetworkChangeNotify(b) => b.cell_loop.len(),
969            ExtensionBody::Message(b) => 1 + ISO_639_LEN + b.text.len(),
970            ExtensionBody::TargetRegion(b) => ISO_639_LEN + b.region_loop.len(),
971            ExtensionBody::TargetRegionName(b) => 2 * ISO_639_LEN + b.region_loop.len(),
972            ExtensionBody::ServiceRelocated(_) => SERVICE_RELOCATED_LEN,
973            ExtensionBody::C2DeliverySystem(_) => C2_LEN,
974            ExtensionBody::UriLinkage(b) => {
975                2 + b.uri.len()
976                    + if b.min_polling_interval.is_some() {
977                        2
978                    } else {
979                        0
980                    }
981                    + b.private_data.len()
982            }
983            ExtensionBody::Ac4(b) => {
984                1 + usize::from(b.ac4_config_flag)
985                    + b.toc.map_or(0, |t| 1 + t.len())
986                    + b.additional_info.len()
987            }
988            ExtensionBody::C2BundleDeliverySystem(b) => b.entries.len() * C2_BUNDLE_ENTRY_LEN,
989            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
990                2 + if b.scrambling_sequence_selector {
991                    S2X_SCRAMBLING_LEN
992                } else {
993                    0
994                } + S2X_PRIMARY_LEN
995                    + usize::from(b.input_stream_identifier.is_some())
996                    + usize::from(b.timeslice_number.is_some())
997                    + b.tail.len()
998            }
999            ExtensionBody::AudioPreselection(b) => 1 + b.preselection_loop.len(),
1000            ExtensionBody::TtmlSubtitling(b) => TTML_FIXED_LEN + b.tail.len(),
1001            ExtensionBody::Raw(s) => s.len(),
1002        }
1003    }
1004
1005    /// Write the selector bytes into `out` (assumed `>= selector_len()`).
1006    fn write_selector(&self, out: &mut [u8]) {
1007        match self {
1008            ExtensionBody::T2DeliverySystem(b) => {
1009                out[0] = b.plp_id;
1010                out[1..3].copy_from_slice(&b.t2_system_id.to_be_bytes());
1011                let mut p = T2_FIXED_PREFIX_LEN;
1012                if let (Some(sm), Some(bw), Some(gi), Some(tm), Some(off), Some(tfs)) = (
1013                    b.siso_miso,
1014                    b.bandwidth,
1015                    b.guard_interval,
1016                    b.transmission_mode,
1017                    b.other_frequency_flag,
1018                    b.tfs_flag,
1019                ) {
1020                    out[p] = (sm << 6) | ((bw & 0x0F) << 2);
1021                    out[p + 1] =
1022                        (gi << 5) | ((tm & 0x07) << 2) | (u8::from(off) << 1) | u8::from(tfs);
1023                    p += T2_FLAGS_BLOCK_LEN;
1024                }
1025                out[p..p + b.cell_loop.len()].copy_from_slice(b.cell_loop);
1026            }
1027            ExtensionBody::SupplementaryAudio(b) => {
1028                // Table 153 bit 1 is plain reserved_future_use → emitted as 1.
1029                out[0] = (u8::from(b.mix_type) << 7)
1030                    | ((b.editorial_classification & 0x1F) << 2)
1031                    | 0x02
1032                    | u8::from(b.language_code_present);
1033                let mut p = 1;
1034                if let Some(lc) = b.iso_639_language_code {
1035                    out[p..p + ISO_639_LEN].copy_from_slice(&lc.0);
1036                    p += ISO_639_LEN;
1037                }
1038                out[p..p + b.private_data.len()].copy_from_slice(b.private_data);
1039            }
1040            ExtensionBody::NetworkChangeNotify(b) => {
1041                out[..b.cell_loop.len()].copy_from_slice(b.cell_loop);
1042            }
1043            ExtensionBody::Message(b) => {
1044                out[0] = b.message_id;
1045                out[1..1 + ISO_639_LEN].copy_from_slice(&b.iso_639_language_code.0);
1046                out[1 + ISO_639_LEN..1 + ISO_639_LEN + b.text.len()].copy_from_slice(b.text.raw());
1047            }
1048            ExtensionBody::TargetRegion(b) => {
1049                out[..ISO_639_LEN].copy_from_slice(&b.country_code.0);
1050                out[ISO_639_LEN..ISO_639_LEN + b.region_loop.len()].copy_from_slice(b.region_loop);
1051            }
1052            ExtensionBody::TargetRegionName(b) => {
1053                out[..ISO_639_LEN].copy_from_slice(&b.country_code.0);
1054                out[ISO_639_LEN..2 * ISO_639_LEN].copy_from_slice(&b.iso_639_language_code.0);
1055                out[2 * ISO_639_LEN..2 * ISO_639_LEN + b.region_loop.len()]
1056                    .copy_from_slice(b.region_loop);
1057            }
1058            ExtensionBody::ServiceRelocated(b) => {
1059                out[0..2].copy_from_slice(&b.old_original_network_id.to_be_bytes());
1060                out[2..4].copy_from_slice(&b.old_transport_stream_id.to_be_bytes());
1061                out[4..6].copy_from_slice(&b.old_service_id.to_be_bytes());
1062            }
1063            ExtensionBody::C2DeliverySystem(b) => {
1064                out[0] = b.plp_id;
1065                out[1] = b.data_slice_id;
1066                out[2..6].copy_from_slice(&b.c2_system_tuning_frequency.to_be_bytes());
1067                out[6] = (b.c2_system_tuning_frequency_type << 6)
1068                    | ((b.active_ofdm_symbol_duration & 0x07) << 3)
1069                    | (b.guard_interval & 0x07);
1070            }
1071            ExtensionBody::UriLinkage(b) => {
1072                out[0] = b.uri_linkage_type;
1073                out[1] = b.uri.len() as u8;
1074                let mut p = 2;
1075                out[p..p + b.uri.len()].copy_from_slice(b.uri);
1076                p += b.uri.len();
1077                if let Some(mpi) = b.min_polling_interval {
1078                    out[p..p + 2].copy_from_slice(&mpi.to_be_bytes());
1079                    p += 2;
1080                }
1081                out[p..p + b.private_data.len()].copy_from_slice(b.private_data);
1082            }
1083            ExtensionBody::Ac4(b) => {
1084                out[0] = (u8::from(b.ac4_config_flag) << 7) | (u8::from(b.ac4_toc_flag) << 6);
1085                let mut p = 1;
1086                if b.ac4_config_flag {
1087                    let de = b.ac4_dialog_enhancement_enabled.unwrap_or(false);
1088                    let cm = b.ac4_channel_mode.unwrap_or(0) & 0x03;
1089                    out[p] = (u8::from(de) << 7) | (cm << 5);
1090                    p += 1;
1091                }
1092                if let Some(t) = b.toc {
1093                    out[p] = t.len() as u8;
1094                    p += 1;
1095                    out[p..p + t.len()].copy_from_slice(t);
1096                    p += t.len();
1097                }
1098                out[p..p + b.additional_info.len()].copy_from_slice(b.additional_info);
1099            }
1100            ExtensionBody::C2BundleDeliverySystem(b) => {
1101                let mut p = 0;
1102                for e in &b.entries {
1103                    out[p] = e.plp_id;
1104                    out[p + 1] = e.data_slice_id;
1105                    out[p + 2..p + 6].copy_from_slice(&e.c2_system_tuning_frequency.to_be_bytes());
1106                    out[p + 6] = (e.c2_system_tuning_frequency_type << 6)
1107                        | ((e.active_ofdm_symbol_duration & 0x07) << 3)
1108                        | (e.guard_interval & 0x07);
1109                    out[p + 7] = u8::from(e.primary_channel) << 7;
1110                    p += C2_BUNDLE_ENTRY_LEN;
1111                }
1112            }
1113            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
1114                out[0] = b.receiver_profiles << 3;
1115                out[1] = ((b.s2x_mode & 0x03) << 6)
1116                    | (u8::from(b.scrambling_sequence_selector) << 5)
1117                    | (b.ts_gs_s2x_mode & 0x03);
1118                let mut p = 2;
1119                if b.scrambling_sequence_selector {
1120                    let idx = b.scrambling_sequence_index.unwrap_or(0) & 0x3FFFF;
1121                    out[p] = (idx >> 16) as u8 & 0x03;
1122                    out[p + 1] = (idx >> 8) as u8;
1123                    out[p + 2] = idx as u8;
1124                    p += S2X_SCRAMBLING_LEN;
1125                }
1126                out[p..p + 4].copy_from_slice(&b.frequency.to_be_bytes());
1127                out[p + 4..p + 6].copy_from_slice(&b.orbital_position.to_be_bytes());
1128                out[p + 6] = (u8::from(b.west_east_flag) << 7)
1129                    | ((b.polarization & 0x03) << 5)
1130                    | (u8::from(b.multiple_input_stream_flag) << 4)
1131                    | (b.roll_off & 0x07);
1132                let sr = b.symbol_rate & 0x0FFF_FFFF;
1133                out[p + 7] = (sr >> 24) as u8 & 0x0F;
1134                out[p + 8] = (sr >> 16) as u8;
1135                out[p + 9] = (sr >> 8) as u8;
1136                out[p + 10] = sr as u8;
1137                p += S2X_PRIMARY_LEN;
1138                if let Some(isi) = b.input_stream_identifier {
1139                    out[p] = isi;
1140                    p += 1;
1141                }
1142                if let Some(ts) = b.timeslice_number {
1143                    out[p] = ts;
1144                    p += 1;
1145                }
1146                out[p..p + b.tail.len()].copy_from_slice(b.tail);
1147            }
1148            ExtensionBody::AudioPreselection(b) => {
1149                out[0] = b.num_preselections << 3;
1150                out[1..1 + b.preselection_loop.len()].copy_from_slice(b.preselection_loop);
1151            }
1152            ExtensionBody::TtmlSubtitling(b) => {
1153                out[..ISO_639_LEN].copy_from_slice(&b.iso_639_language_code.0);
1154                out[ISO_639_LEN] = (b.subtitle_purpose << 2) | (b.tts_suitability & 0x03);
1155                out[ISO_639_LEN + 1] = (u8::from(b.essential_font_usage_flag) << 7)
1156                    | (u8::from(b.qualifier_present_flag) << 6)
1157                    | (b.dvb_ttml_profile_count & 0x0F);
1158                out[TTML_FIXED_LEN..TTML_FIXED_LEN + b.tail.len()].copy_from_slice(b.tail);
1159            }
1160            ExtensionBody::Raw(s) => out[..s.len()].copy_from_slice(s),
1161        }
1162    }
1163}
1164
1165impl Serialize for ExtensionDescriptor<'_> {
1166    type Error = crate::error::Error;
1167    fn serialized_len(&self) -> usize {
1168        HEADER_LEN + TAG_EXTENSION_LEN + self.body.selector_len()
1169    }
1170
1171    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1172        let len = self.serialized_len();
1173        if buf.len() < len {
1174            return Err(Error::OutputBufferTooSmall {
1175                need: len,
1176                have: buf.len(),
1177            });
1178        }
1179        let body_len = len - HEADER_LEN;
1180        if body_len > MAX_DESCRIPTOR_LENGTH {
1181            return Err(Error::InvalidDescriptor {
1182                tag: TAG,
1183                reason: "descriptor_length exceeds 255 bytes",
1184            });
1185        }
1186        buf[0] = TAG;
1187        buf[1] = body_len as u8;
1188        buf[HEADER_LEN] = self.tag_extension;
1189        self.body
1190            .write_selector(&mut buf[HEADER_LEN + TAG_EXTENSION_LEN..len]);
1191        Ok(len)
1192    }
1193}
1194
1195impl<'a> Descriptor<'a> for ExtensionDescriptor<'a> {
1196    const TAG: u8 = TAG;
1197    fn descriptor_length(&self) -> u8 {
1198        (self.serialized_len() - HEADER_LEN) as u8
1199    }
1200}
1201
1202impl<'a> crate::traits::DescriptorDef<'a> for ExtensionDescriptor<'a> {
1203    const TAG: u8 = TAG;
1204    const NAME: &'static str = "EXTENSION";
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209    use super::*;
1210
1211    /// Wrap selector bytes in the extension descriptor framing (Table 54).
1212    fn wrap(tag_ext: u8, sel: &[u8]) -> Vec<u8> {
1213        let mut v = vec![TAG, (sel.len() + 1) as u8, tag_ext];
1214        v.extend_from_slice(sel);
1215        v
1216    }
1217
1218    fn round_trip(d: &ExtensionDescriptor) {
1219        let mut buf = vec![0u8; d.serialized_len()];
1220        d.serialize_into(&mut buf).unwrap();
1221        let re = ExtensionDescriptor::parse(&buf).unwrap();
1222        assert_eq!(*d, re);
1223    }
1224
1225    #[test]
1226    fn parse_rejects_wrong_tag() {
1227        let raw = [0x43, 1, 0x04];
1228        assert!(matches!(
1229            ExtensionDescriptor::parse(&raw).unwrap_err(),
1230            Error::InvalidDescriptor { tag: 0x43, .. }
1231        ));
1232    }
1233
1234    #[test]
1235    fn parse_rejects_empty_body() {
1236        let raw = [TAG, 0];
1237        assert!(matches!(
1238            ExtensionDescriptor::parse(&raw).unwrap_err(),
1239            Error::InvalidDescriptor { tag: TAG, .. }
1240        ));
1241    }
1242
1243    #[test]
1244    fn parse_rejects_truncated_body() {
1245        // declares length 3 but only 1 body byte present
1246        let raw = [TAG, 3, 0x08];
1247        assert!(matches!(
1248            ExtensionDescriptor::parse(&raw).unwrap_err(),
1249            Error::BufferTooShort { .. }
1250        ));
1251    }
1252
1253    #[test]
1254    fn unknown_tag_round_trips_as_raw() {
1255        // 0x42 is reserved/unknown — must survive as Raw with bytes preserved.
1256        let sel = [0xDE, 0xAD, 0xBE, 0xEF];
1257        let bytes = wrap(0x42, &sel);
1258        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1259        assert_eq!(d.tag_extension, 0x42);
1260        assert_eq!(d.kind(), None);
1261        assert!(matches!(d.body, ExtensionBody::Raw(b) if b == sel));
1262        round_trip(&d);
1263    }
1264
1265    #[test]
1266    fn user_defined_tag_preserved() {
1267        let bytes = wrap(0x90, &[0x01, 0x02]);
1268        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1269        assert_eq!(d.tag_extension, 0x90);
1270        assert!(matches!(d.body, ExtensionBody::Raw(_)));
1271        round_trip(&d);
1272    }
1273
1274    #[test]
1275    fn parse_service_relocated() {
1276        let sel = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC];
1277        let bytes = wrap(0x0B, &sel);
1278        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1279        assert_eq!(d.kind(), Some(ExtensionTag::ServiceRelocated));
1280        match &d.body {
1281            ExtensionBody::ServiceRelocated(b) => {
1282                assert_eq!(b.old_original_network_id, 0x1234);
1283                assert_eq!(b.old_transport_stream_id, 0x5678);
1284                assert_eq!(b.old_service_id, 0x9ABC);
1285            }
1286            other => panic!("expected ServiceRelocated, got {other:?}"),
1287        }
1288        round_trip(&d);
1289    }
1290
1291    #[test]
1292    fn parse_message() {
1293        let sel = [0x07, b'e', b'n', b'g', b'H', b'i'];
1294        let bytes = wrap(0x08, &sel);
1295        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1296        match &d.body {
1297            ExtensionBody::Message(b) => {
1298                assert_eq!(b.message_id, 0x07);
1299                assert_eq!(b.iso_639_language_code, LangCode(*b"eng"));
1300                assert_eq!(b.text.raw(), b"Hi");
1301            }
1302            other => panic!("expected Message, got {other:?}"),
1303        }
1304        round_trip(&d);
1305    }
1306
1307    #[test]
1308    fn parse_supplementary_audio_with_language() {
1309        // mix_type=1, editorial=0x17, reserved=1, language_code_present=1,
1310        // then "fre", private 0xAA
1311        let flags = 0x80 | (0x17 << 2) | 0x02 | 0x01;
1312        let sel = [flags, b'f', b'r', b'e', 0xAA];
1313        let bytes = wrap(0x06, &sel);
1314        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1315        match &d.body {
1316            ExtensionBody::SupplementaryAudio(b) => {
1317                assert!(b.mix_type);
1318                assert_eq!(b.editorial_classification, 0x17);
1319                assert!(b.language_code_present);
1320                assert_eq!(b.iso_639_language_code, Some(LangCode(*b"fre")));
1321                assert_eq!(b.private_data, &[0xAA]);
1322            }
1323            other => panic!("expected SupplementaryAudio, got {other:?}"),
1324        }
1325        round_trip(&d);
1326    }
1327
1328    #[test]
1329    fn parse_supplementary_audio_no_language() {
1330        let flags = ((0x01 << 2) & 0x7C) | 0x02; // mix=0, editorial=1, reserved=1, lang=0
1331        let sel = [flags];
1332        let bytes = wrap(0x06, &sel);
1333        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1334        match &d.body {
1335            ExtensionBody::SupplementaryAudio(b) => {
1336                assert!(!b.language_code_present);
1337                assert_eq!(b.iso_639_language_code, None);
1338                assert!(b.private_data.is_empty());
1339            }
1340            other => panic!("expected SupplementaryAudio, got {other:?}"),
1341        }
1342        round_trip(&d);
1343    }
1344
1345    #[test]
1346    fn parse_c2_delivery_system() {
1347        let packed = (0x02 << 6) | (0x01 << 3) | 0x01;
1348        let sel = [0x05, 0x09, 0x12, 0x34, 0x56, 0x78, packed];
1349        let bytes = wrap(0x0D, &sel);
1350        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1351        match &d.body {
1352            ExtensionBody::C2DeliverySystem(b) => {
1353                assert_eq!(b.plp_id, 0x05);
1354                assert_eq!(b.data_slice_id, 0x09);
1355                assert_eq!(b.c2_system_tuning_frequency, 0x1234_5678);
1356                assert_eq!(b.c2_system_tuning_frequency_type, 0x02);
1357                assert_eq!(b.active_ofdm_symbol_duration, 0x01);
1358                assert_eq!(b.guard_interval, 0x01);
1359            }
1360            other => panic!("expected C2DeliverySystem, got {other:?}"),
1361        }
1362        round_trip(&d);
1363    }
1364
1365    #[test]
1366    fn parse_c2_bundle_two_entries() {
1367        let entry = |off: u8| {
1368            let packed = (0x01u8 << 6) | 0x01; // freq_type=1, ofdm=0, guard=1
1369                                               // 8 bytes per Table 139: ... + primary_channel(1)+reserved_zero(7)
1370            [off, off + 1, 0x00, 0x00, 0x10, 0x00, packed, 0x80]
1371        };
1372        let mut sel = Vec::new();
1373        sel.extend_from_slice(&entry(0x01));
1374        sel.extend_from_slice(&entry(0x05));
1375        let bytes = wrap(0x16, &sel);
1376        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1377        match &d.body {
1378            ExtensionBody::C2BundleDeliverySystem(b) => {
1379                assert_eq!(b.entries.len(), 2);
1380                assert_eq!(b.entries[0].plp_id, 0x01);
1381                assert!(b.entries[0].primary_channel);
1382                assert_eq!(b.entries[1].plp_id, 0x05);
1383                assert_eq!(b.entries[1].guard_interval, 0x01);
1384            }
1385            other => panic!("expected C2BundleDeliverySystem, got {other:?}"),
1386        }
1387        round_trip(&d);
1388    }
1389
1390    #[test]
1391    fn parse_c2_bundle_rejects_partial_entry() {
1392        let sel = [0x01, 0x02, 0x03]; // 3 bytes, not a multiple of 8
1393        let bytes = wrap(0x16, &sel);
1394        assert!(matches!(
1395            ExtensionDescriptor::parse(&bytes).unwrap_err(),
1396            Error::InvalidDescriptor { tag: TAG, .. }
1397        ));
1398    }
1399
1400    #[test]
1401    fn parse_uri_linkage_with_polling() {
1402        let uri = b"http://x";
1403        let mut sel = vec![0x00, uri.len() as u8];
1404        sel.extend_from_slice(uri);
1405        sel.extend_from_slice(&0x1234u16.to_be_bytes());
1406        sel.push(0xFE); // private
1407        let bytes = wrap(0x13, &sel);
1408        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1409        match &d.body {
1410            ExtensionBody::UriLinkage(b) => {
1411                assert_eq!(b.uri_linkage_type, 0x00);
1412                assert_eq!(b.uri, uri);
1413                assert_eq!(b.min_polling_interval, Some(0x1234));
1414                assert_eq!(b.private_data, &[0xFE]);
1415            }
1416            other => panic!("expected UriLinkage, got {other:?}"),
1417        }
1418        round_trip(&d);
1419    }
1420
1421    #[test]
1422    fn parse_uri_linkage_no_polling() {
1423        // type 0x02 ⇒ no min_polling_interval
1424        let uri = b"dvb:";
1425        let mut sel = vec![0x02, uri.len() as u8];
1426        sel.extend_from_slice(uri);
1427        let bytes = wrap(0x13, &sel);
1428        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1429        match &d.body {
1430            ExtensionBody::UriLinkage(b) => {
1431                assert_eq!(b.min_polling_interval, None);
1432                assert!(b.private_data.is_empty());
1433            }
1434            other => panic!("expected UriLinkage, got {other:?}"),
1435        }
1436        round_trip(&d);
1437    }
1438
1439    #[test]
1440    fn parse_uri_linkage_rejects_overrun() {
1441        let sel = [0x02, 0x10, 0xAA]; // uri_length 16 but 1 byte present
1442        let bytes = wrap(0x13, &sel);
1443        assert!(matches!(
1444            ExtensionDescriptor::parse(&bytes).unwrap_err(),
1445            Error::InvalidDescriptor { tag: TAG, .. }
1446        ));
1447    }
1448
1449    #[test]
1450    fn parse_ac4_full() {
1451        // config_flag=1, toc_flag=1; config byte de=1 cm=2; toc len 2 = [0x11,0x22]; extra 0x33
1452        let sel = [0xC0, 0x80 | (0x02 << 5), 0x02, 0x11, 0x22, 0x33];
1453        let bytes = wrap(0x15, &sel);
1454        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1455        match &d.body {
1456            ExtensionBody::Ac4(b) => {
1457                assert!(b.ac4_config_flag);
1458                assert!(b.ac4_toc_flag);
1459                assert_eq!(b.ac4_dialog_enhancement_enabled, Some(true));
1460                assert_eq!(b.ac4_channel_mode, Some(0x02));
1461                assert_eq!(b.toc, Some([0x11u8, 0x22].as_slice()));
1462                assert_eq!(b.additional_info, &[0x33]);
1463            }
1464            other => panic!("expected Ac4, got {other:?}"),
1465        }
1466        round_trip(&d);
1467    }
1468
1469    #[test]
1470    fn parse_ac4_minimal() {
1471        let sel = [0x00]; // no config, no toc, no extra
1472        let bytes = wrap(0x15, &sel);
1473        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1474        match &d.body {
1475            ExtensionBody::Ac4(b) => {
1476                assert!(!b.ac4_config_flag);
1477                assert!(!b.ac4_toc_flag);
1478                assert_eq!(b.toc, None);
1479                assert!(b.additional_info.is_empty());
1480            }
1481            other => panic!("expected Ac4, got {other:?}"),
1482        }
1483        round_trip(&d);
1484    }
1485
1486    #[test]
1487    fn parse_t2_minimal() {
1488        // body = plp + system_id = 3 bytes ⇒ no flags block
1489        let sel = [0x07, 0x12, 0x34];
1490        let bytes = wrap(0x04, &sel);
1491        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1492        match &d.body {
1493            ExtensionBody::T2DeliverySystem(b) => {
1494                assert_eq!(b.plp_id, 0x07);
1495                assert_eq!(b.t2_system_id, 0x1234);
1496                assert_eq!(b.siso_miso, None);
1497                assert!(b.cell_loop.is_empty());
1498            }
1499            other => panic!("expected T2DeliverySystem, got {other:?}"),
1500        }
1501        round_trip(&d);
1502    }
1503
1504    #[test]
1505    fn parse_t2_with_flags_and_cells() {
1506        // prefix + flags block (siso=1, bw=2, gi=3, tm=4, off=1, tfs=0) + cell loop
1507        let b0 = (0x01 << 6) | (0x02 << 2);
1508        let b1 = (0x03 << 5) | (0x04 << 2) | 0x02; // off=1, tfs=0
1509        let sel = [0x07, 0x12, 0x34, b0, b1, 0xCA, 0xFE];
1510        let bytes = wrap(0x04, &sel);
1511        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1512        match &d.body {
1513            ExtensionBody::T2DeliverySystem(b) => {
1514                assert_eq!(b.siso_miso, Some(0x01));
1515                assert_eq!(b.bandwidth, Some(0x02));
1516                assert_eq!(b.guard_interval, Some(0x03));
1517                assert_eq!(b.transmission_mode, Some(0x04));
1518                assert_eq!(b.other_frequency_flag, Some(true));
1519                assert_eq!(b.tfs_flag, Some(false));
1520                assert_eq!(b.cell_loop, &[0xCA, 0xFE]);
1521            }
1522            other => panic!("expected T2DeliverySystem, got {other:?}"),
1523        }
1524        round_trip(&d);
1525    }
1526
1527    #[test]
1528    fn parse_s2x_primary_with_isi_and_timeslice() {
1529        // receiver_profiles=0x05; s2x_mode=2, scram_sel=0, ts_gs=1; ISI + timeslice
1530        let b0 = 0x05 << 3;
1531        let b1 = (0x02 << 6) | 0x01; // mode 2 [7:6], no scrambling, ts_gs 1 [1:0]
1532        let mut sel = vec![b0, b1];
1533        sel.extend_from_slice(&0x0102_0304u32.to_be_bytes()); // frequency
1534        sel.extend_from_slice(&0x00C8u16.to_be_bytes()); // orbital_position
1535        sel.push((1 << 7) | (0x02 << 5) | (1 << 4) | 0x03); // we=1 pol=2 mis=1 roll=3
1536        let sr: u32 = 0x0AB_CDEF; // symbol_rate (28-bit)
1537        sel.push((sr >> 24) as u8 & 0x0F);
1538        sel.push((sr >> 16) as u8);
1539        sel.push((sr >> 8) as u8);
1540        sel.push(sr as u8);
1541        sel.push(0x42); // input_stream_identifier (mis=1)
1542        sel.push(0x09); // timeslice_number (mode==2)
1543        let bytes = wrap(0x17, &sel);
1544        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1545        match &d.body {
1546            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
1547                assert_eq!(b.receiver_profiles, 0x05);
1548                assert_eq!(b.s2x_mode, 2);
1549                assert!(!b.scrambling_sequence_selector);
1550                assert_eq!(b.ts_gs_s2x_mode, 1);
1551                assert_eq!(b.frequency, 0x0102_0304);
1552                assert_eq!(b.orbital_position, 0x00C8);
1553                assert!(b.west_east_flag);
1554                assert_eq!(b.polarization, 2);
1555                assert!(b.multiple_input_stream_flag);
1556                assert_eq!(b.roll_off, 3);
1557                assert_eq!(b.symbol_rate, 0x0AB_CDEF);
1558                assert_eq!(b.input_stream_identifier, Some(0x42));
1559                assert_eq!(b.timeslice_number, Some(0x09));
1560                assert!(b.tail.is_empty());
1561            }
1562            other => panic!("expected S2X, got {other:?}"),
1563        }
1564        round_trip(&d);
1565    }
1566
1567    #[test]
1568    fn parse_s2x_with_scrambling_index() {
1569        let b0 = 0x01 << 3;
1570        let b1 = (0x01 << 6) | 0x20; // mode 1 [7:6], scrambling selector set [5]
1571        let mut sel = vec![b0, b1];
1572        // scrambling index 0x2ABCD (18-bit)
1573        sel.push(0x02);
1574        sel.push(0xAB);
1575        sel.push(0xCD);
1576        sel.extend_from_slice(&0u32.to_be_bytes()); // frequency
1577        sel.extend_from_slice(&0u16.to_be_bytes()); // orbital
1578        sel.push(0x00); // packed (mis=0)
1579        sel.extend_from_slice(&[0, 0, 0, 0]); // symbol_rate
1580        let bytes = wrap(0x17, &sel);
1581        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1582        match &d.body {
1583            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
1584                assert!(b.scrambling_sequence_selector);
1585                assert_eq!(b.scrambling_sequence_index, Some(0x2ABCD));
1586                assert_eq!(b.input_stream_identifier, None);
1587                assert_eq!(b.timeslice_number, None);
1588            }
1589            other => panic!("expected S2X, got {other:?}"),
1590        }
1591        round_trip(&d);
1592    }
1593
1594    #[test]
1595    fn parse_s2x_mode3_tail_preserved() {
1596        // mode 3 (channel bonding) — the bond loop lands in `tail` (raw).
1597        let b0 = 0x01 << 3;
1598        let b1 = 0x03 << 6; // mode 3 [7:6], no scrambling, ts_gs 0
1599        let mut sel = vec![b0, b1];
1600        sel.extend_from_slice(&0u32.to_be_bytes());
1601        sel.extend_from_slice(&0u16.to_be_bytes());
1602        sel.push(0x00); // mis=0
1603        sel.extend_from_slice(&[0, 0, 0, 0]); // symbol_rate
1604        sel.extend_from_slice(&[0xAA, 0xBB, 0xCC]); // raw channel-bond tail
1605        let bytes = wrap(0x17, &sel);
1606        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1607        match &d.body {
1608            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
1609                assert_eq!(b.s2x_mode, 3);
1610                assert_eq!(b.timeslice_number, None);
1611                assert_eq!(b.tail, &[0xAA, 0xBB, 0xCC]);
1612            }
1613            other => panic!("expected S2X, got {other:?}"),
1614        }
1615        round_trip(&d);
1616    }
1617
1618    #[test]
1619    fn parse_audio_preselection_keeps_loop_raw() {
1620        // num_preselections=3 then raw loop
1621        let sel = [0x03 << 3, 0xAA, 0xBB, 0xCC];
1622        let bytes = wrap(0x19, &sel);
1623        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1624        match &d.body {
1625            ExtensionBody::AudioPreselection(b) => {
1626                assert_eq!(b.num_preselections, 3);
1627                assert_eq!(b.preselection_loop, &[0xAA, 0xBB, 0xCC]);
1628            }
1629            other => panic!("expected AudioPreselection, got {other:?}"),
1630        }
1631        round_trip(&d);
1632    }
1633
1634    #[test]
1635    fn parse_ttml_subtitling() {
1636        // ISO "eng", subtitle_purpose=0x10, tts=0x1, font=0, qualifier=0, count=1, then tail
1637        let b3 = (0x10 << 2) | 0x01;
1638        let b4 = 0x01; // font=0 qual=0 reserved=0 count=1
1639        let sel = [b'e', b'n', b'g', b3, b4, 0x00, 0x02, b'h', b'i'];
1640        let bytes = wrap(0x20, &sel);
1641        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1642        match &d.body {
1643            ExtensionBody::TtmlSubtitling(b) => {
1644                assert_eq!(b.iso_639_language_code, LangCode(*b"eng"));
1645                assert_eq!(b.subtitle_purpose, 0x10);
1646                assert_eq!(b.tts_suitability, 0x01);
1647                assert!(!b.essential_font_usage_flag);
1648                assert!(!b.qualifier_present_flag);
1649                assert_eq!(b.dvb_ttml_profile_count, 1);
1650                assert_eq!(b.tail, &[0x00, 0x02, b'h', b'i']);
1651            }
1652            other => panic!("expected TtmlSubtitling, got {other:?}"),
1653        }
1654        round_trip(&d);
1655    }
1656
1657    #[test]
1658    fn parse_target_region_loop_raw() {
1659        let sel = [b'g', b'b', b'r', 0x01, 0x02, 0x03];
1660        let bytes = wrap(0x09, &sel);
1661        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1662        match &d.body {
1663            ExtensionBody::TargetRegion(b) => {
1664                assert_eq!(b.country_code, LangCode(*b"gbr"));
1665                assert_eq!(b.region_loop, &[0x01, 0x02, 0x03]);
1666            }
1667            other => panic!("expected TargetRegion, got {other:?}"),
1668        }
1669        round_trip(&d);
1670    }
1671
1672    #[test]
1673    fn parse_target_region_name_loop_raw() {
1674        let sel = [b'g', b'b', b'r', b'e', b'n', b'g', 0x44, 0x55];
1675        let bytes = wrap(0x0A, &sel);
1676        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1677        match &d.body {
1678            ExtensionBody::TargetRegionName(b) => {
1679                assert_eq!(b.country_code, LangCode(*b"gbr"));
1680                assert_eq!(b.iso_639_language_code, LangCode(*b"eng"));
1681                assert_eq!(b.region_loop, &[0x44, 0x55]);
1682            }
1683            other => panic!("expected TargetRegionName, got {other:?}"),
1684        }
1685        round_trip(&d);
1686    }
1687
1688    #[test]
1689    fn parse_network_change_notify_loop_raw() {
1690        let sel = [0x00, 0x01, 0x05, 0xAA, 0xBB];
1691        let bytes = wrap(0x07, &sel);
1692        let d = ExtensionDescriptor::parse(&bytes).unwrap();
1693        match &d.body {
1694            ExtensionBody::NetworkChangeNotify(b) => {
1695                assert_eq!(b.cell_loop, &sel);
1696            }
1697            other => panic!("expected NetworkChangeNotify, got {other:?}"),
1698        }
1699        round_trip(&d);
1700    }
1701
1702    #[test]
1703    fn serialize_rejects_too_small_buffer() {
1704        let d = ExtensionDescriptor {
1705            tag_extension: 0x0B,
1706            body: ExtensionBody::ServiceRelocated(ServiceRelocated {
1707                old_original_network_id: 1,
1708                old_transport_stream_id: 2,
1709                old_service_id: 3,
1710            }),
1711        };
1712        let mut tiny = [0u8; 2];
1713        assert!(matches!(
1714            d.serialize_into(&mut tiny).unwrap_err(),
1715            Error::OutputBufferTooSmall { .. }
1716        ));
1717    }
1718
1719    #[test]
1720    fn descriptor_length_matches_body() {
1721        let d = ExtensionDescriptor {
1722            tag_extension: 0x08,
1723            body: ExtensionBody::Message(Message {
1724                message_id: 1,
1725                iso_639_language_code: LangCode(*b"eng"),
1726                text: DvbText::new(b"hello"),
1727            }),
1728        };
1729        // tag_ext(1) + message_id(1) + iso(3) + text(5) = 10
1730        assert_eq!(d.descriptor_length(), 10);
1731    }
1732
1733    /// Serialization is deterministic for an all-owned typed body (no borrowed
1734    /// slices). `ExtensionDescriptor` no longer derives `Deserialize` because
1735    /// `ExtensionBody::Message` contains `DvbText` which is serialize-only; we
1736    /// therefore assert serialize stability rather than a round-trip.
1737    #[cfg(feature = "serde")]
1738    #[test]
1739    fn serde_serialize_is_stable_owned_body() {
1740        let typed = ExtensionDescriptor {
1741            tag_extension: 0x0D,
1742            body: ExtensionBody::C2DeliverySystem(C2DeliverySystem {
1743                plp_id: 1,
1744                data_slice_id: 2,
1745                c2_system_tuning_frequency: 0xDEAD_BEEF,
1746                c2_system_tuning_frequency_type: 1,
1747                active_ofdm_symbol_duration: 2,
1748                guard_interval: 3,
1749            }),
1750        };
1751        let json = serde_json::to_string(&typed).unwrap();
1752        assert_eq!(json, serde_json::to_string(&typed.clone()).unwrap());
1753        assert!(json.contains("\"tag_extension\":13"));
1754        assert!(json.contains("\"C2DeliverySystem\""));
1755    }
1756
1757    /// Borrowed bodies (Raw, Message, …) serialize cleanly; the discriminant +
1758    /// tag survive the JSON encoding.
1759    #[cfg(feature = "serde")]
1760    #[test]
1761    fn serde_serializes_borrowed_body() {
1762        let raw = ExtensionDescriptor {
1763            tag_extension: 0x42,
1764            body: ExtensionBody::Raw(&[0x01, 0x02, 0x03]),
1765        };
1766        let json = serde_json::to_string(&raw).unwrap();
1767        assert!(json.contains("\"tag_extension\":66"));
1768        assert!(json.contains("\"Raw\""));
1769
1770        let msg = ExtensionDescriptor {
1771            tag_extension: 0x08,
1772            body: ExtensionBody::Message(Message {
1773                message_id: 7,
1774                iso_639_language_code: LangCode(*b"eng"),
1775                text: DvbText::new(b"hi"),
1776            }),
1777        };
1778        let json = serde_json::to_string(&msg).unwrap();
1779        assert!(json.contains("\"message_id\":7"));
1780    }
1781}