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