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