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