Skip to main content

dvb_si/descriptors/extension/
mod.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/`. Loop-heavy descriptors have the first fixed level typed and
27//! any variable-length inner loop that has no defined syntax kept as a raw
28//! slice (e.g. service_prominence target_region loop is raw). Per-variant
29//! section comments 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//! - `0x00` image_icon (Table 145, §6.4.7) — fully typed; icon TransportMode per
34//!   Table 146, coordinate_system per Table 147.
35//! - `0x04` T2_delivery_system (Table 133, §6.4.6.3) — cell loop unfolded.
36//! - `0x05` SH_delivery_system (Table 119, §6.4.6.2) — fully typed modulation loop.
37//! - `0x06` supplementary_audio (Table 153, §6.4.11).
38//! - `0x07` network_change_notify (Table 149, §6.4.9) — cell/change loop unfolded.
39//! - `0x08` message (Table 148, §6.4.9).
40//! - `0x09` target_region (Table 156, §6.4.12) — region loop unfolded.
41//! - `0x0A` target_region_name (Table 157, §6.4.13) — region loop unfolded.
42//! - `0x0B` service_relocated (Table 152, §6.4.10).
43//! - `0x0D` C2_delivery_system (Table 115, §6.4.6.1).
44//! - `0x10` video_depth_range (Table 160, §6.4.16.1) — fully typed range loop.
45//! - `0x11` T2MI (Table 158, §6.4.14).
46//! - `0x13` URI_linkage (Table 159, §6.4.16.1) — uri/private split typed.
47//! - `0x15` AC-4 (annex D syntax table, §D.5) — first level; toc/extra raw.
48//! - `0x16` C2_bundle_delivery_system (Table 139, §6.4.6.4) — full fixed loop.
49//! - `0x17` S2X_satellite_delivery_system (Table 140, §6.4.6.5.2) — primary
50//!   channel + channel-bond entries typed; reserved_tail raw.
51//! - `0x19` audio_preselection (Table 110, §6.4.1) — preselection loop unfolded.
52//! - `0x20` TTML_subtitling (`en_303_560_ttml.md` Table 1, §5.2.1.1).
53//! - `0x22` service_prominence (Table 162c, §6.4.18) — SOGI loop typed; target_region loop raw.
54//! - `0x23` vvc_subpictures (Table 162a, §6.4.17) — fully typed.
55//!
56//! Kept [`ExtensionBody::Raw`] (tag value preserved), with reason:
57//! - `0x01` cpcm_delivery_signalling — spec not vendored (ETSI TS 102 825).
58//! - `0x02` CP / `0x03` CP_identifier — spec not vendored (ETSI TS 102 825).
59//! - `0x0C` XAIT_PID — deferred (TS 102 727 PDF vendored, no extracted syntax table yet).
60//! - `0x0E` DTS-HD / `0x0F` DTS_Neural / `0x21` DTS-UHD — spec not vendored (annex G/L).
61//! - `0x14` CI_ancillary_data — spec not vendored (ETSI TS 103 205).
62//! - `0x18` protection_message — spec not vendored (ETSI TS 102 809).
63//! - `0x24` S2Xv2 — niche; deferred.
64//! - any other value (incl. `0x80`..=`0xFF` user-defined) — unknown; preserved.
65
66use crate::error::{Error, Result};
67use crate::text::{DvbText, LangCode};
68use dvb_common::{Parse, Serialize};
69
70mod ac4;
71mod audio_preselection;
72mod c2_bundle_delivery_system;
73mod c2_delivery_system;
74mod image_icon;
75mod message;
76mod network_change_notify;
77pub mod registry;
78mod s2x_satellite_delivery_system;
79mod service_prominence;
80mod service_relocated;
81mod sh_delivery_system;
82mod supplementary_audio;
83mod t2_delivery_system;
84mod t2mi;
85mod target_region;
86mod target_region_name;
87mod ttml_subtitling;
88mod uri_linkage;
89mod video_depth_range;
90mod vvc_subpictures;
91
92#[cfg(test)]
93mod test_support;
94
95pub use ac4::*;
96pub use audio_preselection::*;
97pub use c2_bundle_delivery_system::*;
98pub use c2_delivery_system::*;
99pub use image_icon::*;
100pub use message::*;
101pub use network_change_notify::*;
102pub use s2x_satellite_delivery_system::*;
103pub use service_prominence::*;
104pub use service_relocated::*;
105pub use sh_delivery_system::*;
106pub use supplementary_audio::*;
107pub use t2_delivery_system::*;
108pub use t2mi::*;
109pub use target_region::*;
110pub use target_region_name::*;
111pub use ttml_subtitling::*;
112pub use uri_linkage::*;
113pub use video_depth_range::*;
114pub use vvc_subpictures::*;
115
116/// Descriptor tag for extension_descriptor (EN 300 468 Table 54, §6.2.18.1).
117pub const TAG: u8 = 0x7F;
118pub(crate) const HEADER_LEN: usize = 2;
119/// `descriptor_tag_extension` occupies one byte immediately after the header.
120pub(crate) const TAG_EXTENSION_LEN: usize = 1;
121/// Minimum body length: just the `descriptor_tag_extension` byte.
122pub(crate) const MIN_BODY_LEN: usize = TAG_EXTENSION_LEN;
123/// `descriptor_length` is a single byte; a serialized body may not exceed this.
124pub(crate) const MAX_DESCRIPTOR_LENGTH: usize = 0xFF;
125
126// Per-variant fixed lengths (bytes after `descriptor_tag_extension`).
127pub(crate) const ISO_639_LEN: usize = 3;
128pub(crate) const T2_FIXED_PREFIX_LEN: usize = 3; // plp_id(1) + T2_system_id(2)
129pub(crate) const T2_FLAGS_BLOCK_LEN: usize = 2; // SISO_MISO..tfs_flag, packed in 2 bytes
130pub(crate) const C2_LEN: usize = 7; // plp + data_slice + freq(4) + 1 packed byte
131pub(crate) const C2_BUNDLE_ENTRY_LEN: usize = 8; // plp + data_slice + freq(4) + 1 packed + 1 (primary(1)+reserved_zero(7))
132pub(crate) const SERVICE_RELOCATED_LEN: usize = 6; // 3 × u16
133/// S2X primary-channel block after the 2 flags bytes (excl. scrambling/ISI/timeslice):
134/// frequency(4) + orbital_position(2) + 1 packed byte + symbol_rate(4 bytes).
135pub(crate) const S2X_PRIMARY_LEN: usize = 11;
136pub(crate) const S2X_SCRAMBLING_LEN: usize = 3;
137pub(crate) const TTML_FIXED_LEN: usize = ISO_639_LEN + 2; // ISO_639(3) + 2 packed bytes
138/// Minimum T2MI selector length (Table 158 §6.4.14): 3 fixed bytes before the reserved tail.
139pub(crate) const T2MI_MIN_LEN: usize = 3;
140/// Range header bytes per depth-range entry (Table 160): `range_type` + `range_length`.
141pub(crate) const VD_RANGE_HDR_LEN: usize = 2;
142/// Production disparity hint body length (Table 162): 3 bytes — two 12-bit signed values.
143pub(crate) const VD_DISPARITY_LEN: usize = 3;
144
145/// Known `descriptor_tag_extension` values (EN 300 468 Table 109, §6.4.0).
146///
147/// This is a *naming* aid for callers and parser dispatch; the stored
148/// discriminant is the raw [`ExtensionDescriptor::tag_extension`] `u8` so that
149/// unknown / reserved / user-defined tags round-trip unchanged.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize))]
152#[non_exhaustive]
153#[repr(u8)]
154pub enum ExtensionTag {
155    /// image_icon_descriptor (Table 145, §6.4.7).
156    ImageIcon = 0x00,
157    /// T2_delivery_system_descriptor.
158    T2DeliverySystem = 0x04,
159    /// SH_delivery_system_descriptor (Table 119, §6.4.6.2).
160    ShDeliverySystem = 0x05,
161    /// supplementary_audio_descriptor.
162    SupplementaryAudio = 0x06,
163    /// network_change_notify_descriptor.
164    NetworkChangeNotify = 0x07,
165    /// message_descriptor.
166    Message = 0x08,
167    /// target_region_descriptor.
168    TargetRegion = 0x09,
169    /// target_region_name_descriptor.
170    TargetRegionName = 0x0A,
171    /// service_relocated_descriptor.
172    ServiceRelocated = 0x0B,
173    /// C2_delivery_system_descriptor.
174    C2DeliverySystem = 0x0D,
175    /// video_depth_range_descriptor (Table 160, §6.4.16.1).
176    VideoDepthRange = 0x10,
177    /// T2-MI_descriptor (Table 158, §6.4.14).
178    T2mi = 0x11,
179    /// URI_linkage_descriptor.
180    UriLinkage = 0x13,
181    /// AC-4_descriptor (annex D).
182    Ac4 = 0x15,
183    /// C2_bundle_delivery_system_descriptor.
184    C2BundleDeliverySystem = 0x16,
185    /// S2X_satellite_delivery_system_descriptor.
186    S2XSatelliteDeliverySystem = 0x17,
187    /// audio_preselection_descriptor.
188    AudioPreselection = 0x19,
189    /// TTML_subtitling_descriptor (ETSI EN 303 560).
190    TtmlSubtitling = 0x20,
191    /// service_prominence_descriptor (Table 162c, §6.4.18).
192    ServiceProminence = 0x22,
193    /// vvc_subpictures_descriptor (Table 162a, §6.4.17).
194    VvcSubpictures = 0x23,
195}
196
197/// Generates the extension-body dispatch from one list (ADR-0001): the
198/// [`ExtensionBody`] enum (+ a `Raw` fall-through), the `parse_body`
199/// dispatcher, the `selector_len`/`write_selector` serialize delegation, and
200/// a drift test pinning each `descriptor_tag_extension` literal to the body
201/// type's [`ExtensionBodyDef::TAG_EXTENSION`] and its [`ExtensionTag`] variant.
202/// One line per typed body — the single source of truth for the sub-dispatch.
203macro_rules! declare_extension_bodies {
204    (
205        $lt:lifetime;
206        $( $(#[doc = $doc:literal])* $variant:ident = $tag:literal => $($path:ident)::+ $(<$plt:lifetime>)? ),+ $(,)?
207    ) => {
208        /// Typed body of an extension descriptor, keyed on `descriptor_tag_extension`.
209        ///
210        /// Unrecognised or not-yet-typed discriminants land in [`ExtensionBody::Raw`],
211        /// which carries the selector bytes verbatim so the descriptor round-trips.
212        #[derive(Debug, Clone, PartialEq, Eq)]
213        #[cfg_attr(feature = "serde", derive(serde::Serialize))]
214        #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
215        #[non_exhaustive]
216        pub enum ExtensionBody<$lt> {
217            $(
218                $(#[doc = $doc])*
219                $variant($($path)::+ $(<$plt>)?),
220            )+
221            /// Any not-yet-typed / unknown / user-defined discriminant: selector bytes verbatim.
222            Raw(&$lt [u8]),
223        }
224
225        /// Parse the selector bytes (everything after `descriptor_tag_extension`)
226        /// into a typed body, falling through to [`ExtensionBody::Raw`].
227        fn parse_body(tag_extension: u8, sel: &[u8]) -> Result<ExtensionBody<'_>> {
228            Ok(match tag_extension {
229                $(
230                    $tag => ExtensionBody::$variant(<$($path)::+>::parse(sel)?),
231                )+
232                _ => ExtensionBody::Raw(sel),
233            })
234        }
235
236        impl ExtensionBody<'_> {
237            /// Selector-byte length (everything after `descriptor_tag_extension`).
238            fn selector_len(&self) -> usize {
239                match self {
240                    $(
241                        ExtensionBody::$variant(b) => b.serialized_len(),
242                    )+
243                    ExtensionBody::Raw(s) => s.len(),
244                }
245            }
246
247            /// Write the selector bytes into `out` (assumed `>= selector_len()`).
248            fn write_selector(&self, out: &mut [u8]) {
249                match self {
250                    $(
251                        ExtensionBody::$variant(b) => {
252                            // `ExtensionDescriptor::serialize_into` sizes `out` from
253                            // `selector_len()`, so `serialize_into` cannot fail.
254                            b.serialize_into(out)
255                                .expect("caller pre-sizes out to selector_len");
256                        }
257                    )+
258                    ExtensionBody::Raw(s) => out[..s.len()].copy_from_slice(s),
259                }
260            }
261        }
262
263        /// Map a `descriptor_tag_extension` byte to its known [`ExtensionTag`].
264        fn kind_from_tag(tag_extension: u8) -> Option<ExtensionTag> {
265            Some(match tag_extension {
266                $(
267                    $tag => ExtensionTag::$variant,
268                )+
269                _ => return None,
270            })
271        }
272
273        #[cfg(test)]
274        mod dispatch_drift {
275            use super::*;
276
277            /// The macro list is the single source of truth: each tag literal must
278            /// equal the body's `ExtensionBodyDef::TAG_EXTENSION`, its `NAME` must be
279            /// non-empty, and `kind()` must map the tag to the matching `ExtensionTag`.
280            #[test]
281            fn ext_dispatch_single_source() {
282                $(
283                    assert_eq!(
284                        $tag,
285                        <$($path)::+ as ExtensionBodyDef<'_>>::TAG_EXTENSION,
286                        concat!("TAG_EXTENSION drift for ", stringify!($variant)),
287                    );
288                    assert!(
289                        !<$($path)::+ as ExtensionBodyDef<'_>>::NAME.is_empty(),
290                        concat!("empty NAME for ", stringify!($variant)),
291                    );
292                    assert_eq!(
293                        ExtensionDescriptor {
294                            tag_extension: $tag,
295                            body: ExtensionBody::Raw(&[]),
296                        }
297                        .kind(),
298                        Some(ExtensionTag::$variant),
299                        concat!("kind() drift for ", stringify!($variant)),
300                    );
301                )+
302            }
303        }
304    };
305}
306
307declare_extension_bodies! {'a;
308    /// `0x00` — image_icon (Table 145, §6.4.7; icon_transport_mode Table 146, §6.4.8;
309    /// coordinate_system Table 147, §6.4.8).
310    ImageIcon = 0x00 => ImageIcon<'a>,
311    /// `0x04` — T2_delivery_system (Table 133, §6.4.6.3).
312    T2DeliverySystem = 0x04 => T2DeliverySystem,
313    /// `0x05` — SH_delivery_system (Table 119, §6.4.6.2).
314    ShDeliverySystem = 0x05 => ShDeliverySystem,
315    /// `0x06` — supplementary_audio (Table 153, §6.4.11).
316    SupplementaryAudio = 0x06 => SupplementaryAudio<'a>,
317    /// `0x07` — network_change_notify (Table 149, §6.4.9).
318    NetworkChangeNotify = 0x07 => NetworkChangeNotify,
319    /// `0x08` — message (Table 148, §6.4.9).
320    Message = 0x08 => Message<'a>,
321    /// `0x09` — target_region (Table 156, §6.4.12).
322    TargetRegion = 0x09 => TargetRegion,
323    /// `0x0A` — target_region_name (Table 157, §6.4.13).
324    TargetRegionName = 0x0A => TargetRegionName<'a>,
325    /// `0x0B` — service_relocated (Table 152, §6.4.10).
326    ServiceRelocated = 0x0B => ServiceRelocated,
327    /// `0x0D` — C2_delivery_system (Table 115, §6.4.6.1).
328    C2DeliverySystem = 0x0D => C2DeliverySystem,
329    /// `0x10` — video_depth_range (Table 160, §6.4.16.1).
330    VideoDepthRange = 0x10 => VideoDepthRange<'a>,
331    /// `0x11` — T2-MI (Table 158, §6.4.14).
332    T2mi = 0x11 => T2mi<'a>,
333    /// `0x13` — URI_linkage (Table 159, §6.4.16.1).
334    UriLinkage = 0x13 => UriLinkage<'a>,
335    /// `0x15` — AC-4 (annex D).
336    Ac4 = 0x15 => Ac4<'a>,
337    /// `0x16` — C2_bundle_delivery_system (Table 139, §6.4.6.4).
338    C2BundleDeliverySystem = 0x16 => C2BundleDeliverySystem,
339    /// `0x17` — S2X_satellite_delivery_system (Table 140, §6.4.6.5.2).
340    S2XSatelliteDeliverySystem = 0x17 => S2XSatelliteDeliverySystem<'a>,
341    /// `0x19` — audio_preselection (Table 110, §6.4.1).
342    AudioPreselection = 0x19 => AudioPreselection<'a>,
343    /// `0x20` — TTML_subtitling (EN 303 560 Table 1, §5.2.1.1).
344    TtmlSubtitling = 0x20 => TtmlSubtitling<'a>,
345    /// `0x22` — service_prominence (Table 162c, §6.4.18).
346    ServiceProminence = 0x22 => ServiceProminence<'a>,
347    /// `0x23` — vvc_subpictures (Table 162a, §6.4.17).
348    VvcSubpictures = 0x23 => VvcSubpictures<'a>,
349}
350
351/// Per-body metadata for the extension-descriptor sub-dispatch — the
352/// `descriptor_tag_extension` value and a diagnostic name. Mirrors
353/// [`crate::traits::DescriptorDef`] for the second dispatch level (ADR-0001).
354pub trait ExtensionBodyDef<'a>: dvb_common::Parse<'a, Error = crate::error::Error> {
355    /// The `descriptor_tag_extension` value this body is selected by.
356    const TAG_EXTENSION: u8;
357    /// SCREAMING_SNAKE diagnostic name, suffix-free.
358    const NAME: &'static str;
359}
360
361/// Extension descriptor (EN 300 468 Table 54, §6.2.18.1).
362#[derive(Debug, Clone, PartialEq, Eq)]
363#[cfg_attr(feature = "serde", derive(serde::Serialize))]
364#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
365pub struct ExtensionDescriptor<'a> {
366    /// `descriptor_tag_extension` (raw `u8`; see [`ExtensionTag`] for names).
367    pub tag_extension: u8,
368    /// Typed body, or [`ExtensionBody::Raw`] for not-yet-typed discriminants.
369    pub body: ExtensionBody<'a>,
370}
371
372impl ExtensionDescriptor<'_> {
373    /// Typed view of [`Self::tag_extension`], or `None` if not a known tag.
374    #[must_use]
375    pub fn kind(&self) -> Option<ExtensionTag> {
376        kind_from_tag(self.tag_extension)
377    }
378}
379
380// ---------------------------------------------------------------------------
381//  Body parsers (each consumes the selector bytes after descriptor_tag_extension)
382// ---------------------------------------------------------------------------
383
384pub(crate) fn invalid(reason: &'static str) -> Error {
385    Error::InvalidDescriptor { tag: TAG, reason }
386}
387
388/// Validate an extension_descriptor header and split into (tag_extension, selector).
389/// Shared by [`ExtensionDescriptor::parse`] and [`ExtensionRegistry::parse`].
390pub(crate) fn validate_and_split(bytes: &[u8]) -> Result<(u8, &[u8])> {
391    if bytes.len() < HEADER_LEN {
392        return Err(Error::BufferTooShort {
393            need: HEADER_LEN,
394            have: bytes.len(),
395            what: "ExtensionDescriptor header",
396        });
397    }
398    if bytes[0] != TAG {
399        return Err(Error::InvalidDescriptor {
400            tag: bytes[0],
401            reason: "unexpected tag for extension_descriptor",
402        });
403    }
404    let length = bytes[1] as usize;
405    let end = HEADER_LEN + length;
406    if bytes.len() < end {
407        return Err(Error::BufferTooShort {
408            need: end,
409            have: bytes.len(),
410            what: "ExtensionDescriptor body",
411        });
412    }
413    if length < MIN_BODY_LEN {
414        return Err(Error::InvalidDescriptor {
415            tag: TAG,
416            reason: "body must contain at least the descriptor_tag_extension byte",
417        });
418    }
419    let tag_extension = bytes[HEADER_LEN];
420    let sel = &bytes[HEADER_LEN + TAG_EXTENSION_LEN..end];
421    Ok((tag_extension, sel))
422}
423
424impl<'a> Parse<'a> for ExtensionDescriptor<'a> {
425    type Error = crate::error::Error;
426    fn parse(bytes: &'a [u8]) -> Result<Self> {
427        let (tag_extension, sel) = validate_and_split(bytes)?;
428        let body = parse_body(tag_extension, sel)?;
429        Ok(Self {
430            tag_extension,
431            body,
432        })
433    }
434}
435
436impl Serialize for ExtensionDescriptor<'_> {
437    type Error = crate::error::Error;
438    fn serialized_len(&self) -> usize {
439        HEADER_LEN + TAG_EXTENSION_LEN + self.body.selector_len()
440    }
441
442    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
443        let len = self.serialized_len();
444        if buf.len() < len {
445            return Err(Error::OutputBufferTooSmall {
446                need: len,
447                have: buf.len(),
448            });
449        }
450        let body_len = len - HEADER_LEN;
451        if body_len > MAX_DESCRIPTOR_LENGTH {
452            return Err(Error::InvalidDescriptor {
453                tag: TAG,
454                reason: "descriptor_length exceeds 255 bytes",
455            });
456        }
457        buf[0] = TAG;
458        buf[1] = body_len as u8;
459        buf[HEADER_LEN] = self.tag_extension;
460        self.body
461            .write_selector(&mut buf[HEADER_LEN + TAG_EXTENSION_LEN..len]);
462        Ok(len)
463    }
464}
465impl<'a> crate::traits::DescriptorDef<'a> for ExtensionDescriptor<'a> {
466    const TAG: u8 = TAG;
467    const NAME: &'static str = "EXTENSION";
468}
469
470#[cfg(test)]
471mod tests {
472    use super::test_support::*;
473    use super::*;
474
475    #[test]
476    fn parse_rejects_wrong_tag() {
477        let raw = [0x43, 1, 0x04];
478        assert!(matches!(
479            ExtensionDescriptor::parse(&raw).unwrap_err(),
480            Error::InvalidDescriptor { tag: 0x43, .. }
481        ));
482    }
483
484    #[test]
485    fn parse_rejects_empty_body() {
486        let raw = [TAG, 0];
487        assert!(matches!(
488            ExtensionDescriptor::parse(&raw).unwrap_err(),
489            Error::InvalidDescriptor { tag: TAG, .. }
490        ));
491    }
492
493    #[test]
494    fn parse_rejects_truncated_body() {
495        // declares length 3 but only 1 body byte present
496        let raw = [TAG, 3, 0x08];
497        assert!(matches!(
498            ExtensionDescriptor::parse(&raw).unwrap_err(),
499            Error::BufferTooShort { .. }
500        ));
501    }
502
503    #[test]
504    fn unknown_tag_round_trips_as_raw() {
505        // 0x42 is reserved/unknown — must survive as Raw with bytes preserved.
506        let sel = [0xDE, 0xAD, 0xBE, 0xEF];
507        let bytes = wrap(0x42, &sel);
508        let d = ExtensionDescriptor::parse(&bytes).unwrap();
509        assert_eq!(d.tag_extension, 0x42);
510        assert_eq!(d.kind(), None);
511        assert!(matches!(d.body, ExtensionBody::Raw(b) if b == sel));
512        round_trip(&d);
513    }
514
515    #[test]
516    fn user_defined_tag_preserved() {
517        let bytes = wrap(0x90, &[0x01, 0x02]);
518        let d = ExtensionDescriptor::parse(&bytes).unwrap();
519        assert_eq!(d.tag_extension, 0x90);
520        assert!(matches!(d.body, ExtensionBody::Raw(_)));
521        round_trip(&d);
522    }
523
524    #[test]
525    fn serialize_rejects_too_small_buffer() {
526        let d = ExtensionDescriptor {
527            tag_extension: 0x0B,
528            body: ExtensionBody::ServiceRelocated(ServiceRelocated {
529                old_original_network_id: 1,
530                old_transport_stream_id: 2,
531                old_service_id: 3,
532            }),
533        };
534        let mut tiny = [0u8; 2];
535        assert!(matches!(
536            d.serialize_into(&mut tiny).unwrap_err(),
537            Error::OutputBufferTooSmall { .. }
538        ));
539    }
540
541    #[test]
542    fn descriptor_length_matches_body() {
543        let d = ExtensionDescriptor {
544            tag_extension: 0x08,
545            body: ExtensionBody::Message(Message {
546                message_id: 1,
547                iso_639_language_code: LangCode(*b"eng"),
548                text: DvbText::new(b"hello"),
549            }),
550        };
551        // tag_ext(1) + message_id(1) + iso(3) + text(5) = 10
552        assert_eq!(d.serialized_len() - 2, 10);
553    }
554
555    /// Serialization is deterministic for an all-owned typed body (no borrowed
556    /// slices). `ExtensionDescriptor` is serialize-only because
557    /// `ExtensionBody::Message` contains `DvbText` which is serialize-only; we
558    /// therefore assert serialize stability rather than a round-trip.
559    #[cfg(feature = "serde")]
560    #[test]
561    fn serde_serialize_is_stable_owned_body() {
562        let typed = ExtensionDescriptor {
563            tag_extension: 0x0D,
564            body: ExtensionBody::C2DeliverySystem(C2DeliverySystem {
565                plp_id: 1,
566                data_slice_id: 2,
567                c2_system_tuning_frequency: 0xDEAD_BEEF,
568                c2_system_tuning_frequency_type: C2TuningFrequencyType::C2SystemCentreFrequency,
569                active_ofdm_symbol_duration: ActiveOfdmSymbolDuration::Reserved(2),
570                guard_interval: C2GuardInterval::Reserved(3),
571            }),
572        };
573        let json = serde_json::to_string(&typed).unwrap();
574        assert_eq!(json, serde_json::to_string(&typed.clone()).unwrap());
575        assert!(json.contains("\"tag_extension\":13"));
576        assert!(json.contains("\"c2DeliverySystem\""));
577    }
578
579    /// Borrowed bodies (Raw, Message, …) serialize cleanly; the discriminant +
580    /// tag survive the JSON encoding.
581    #[cfg(feature = "serde")]
582    #[test]
583    fn serde_serializes_borrowed_body() {
584        let raw = ExtensionDescriptor {
585            tag_extension: 0x42,
586            body: ExtensionBody::Raw(&[0x01, 0x02, 0x03]),
587        };
588        let json = serde_json::to_string(&raw).unwrap();
589        assert!(json.contains("\"tag_extension\":66"));
590        assert!(json.contains("\"raw\""));
591
592        let msg = ExtensionDescriptor {
593            tag_extension: 0x08,
594            body: ExtensionBody::Message(Message {
595                message_id: 7,
596                iso_639_language_code: LangCode(*b"eng"),
597                text: DvbText::new(b"hi"),
598            }),
599        };
600        let json = serde_json::to_string(&msg).unwrap();
601        assert!(json.contains("\"message_id\":7"));
602    }
603
604    /// Cross-implementation conformance: exact extension-descriptor bytes
605    /// compiled by TSDuck (github.com/tsduck/tsduck-test, reference tests 015
606    /// and 115). Parsing then re-serializing must reproduce each descriptor
607    /// verbatim — this validates our wire layout (including
608    /// `reserved_future_use` bits = 1, per the DVB convention) against an
609    /// independent encoder, which a self-round-trip cannot.
610    #[test]
611    fn tsduck_reference_round_trip_byte_exact() {
612        let vectors = [
613            // service_prominence (test-115)
614            (
615                "7f20221a300c04d2f000092911fc465241fd47425211fa0102fb0b0c000ddeadbeef",
616                ExtensionTag::ServiceProminence,
617            ),
618            ("7f0a22000011223344556677", ExtensionTag::ServiceProminence),
619            (
620                "7f0d220b700e092906fe4742520102",
621                ExtensionTag::ServiceProminence,
622            ),
623            // image_icon (test-015)
624            (
625                "7f1a000cfc2b3e71c809696d6167652f706e67080123456789abcdef",
626                ExtensionTag::ImageIcon,
627            ),
628            (
629                "7f220007fe5f0a696d6167652f6a70656712687474703a2f2f666f6f2f6261722e6a7067",
630                ExtensionTag::ImageIcon,
631            ),
632            ("7f090033fe050123456789", ExtensionTag::ImageIcon),
633            // SH_delivery_system (test-015)
634            ("7f02055f", ExtensionTag::ShDeliverySystem),
635            (
636                "7f0d05afff94ac175f68831d8d99ad",
637                ExtensionTag::ShDeliverySystem,
638            ),
639        ];
640        for (hex, ext) in vectors {
641            let bytes = from_hex(hex);
642            let d =
643                ExtensionDescriptor::parse(&bytes).unwrap_or_else(|e| panic!("parse {hex}: {e:?}"));
644            assert_eq!(d.kind(), Some(ext), "kind for {hex}");
645            let mut out = vec![0u8; d.serialized_len()];
646            let n = d.serialize_into(&mut out).unwrap();
647            assert_eq!(out[..n], bytes[..], "byte-exact re-serialize for {hex}");
648        }
649
650        // Decoded-field spot checks (prove we interpret TSDuck's known values,
651        // not merely round-trip our own): the rich image_icon and the rich
652        // service_prominence from the XML sources of tests 015 / 115.
653        let icon = from_hex("7f1a000cfc2b3e71c809696d6167652f706e67080123456789abcdef");
654        match &ExtensionDescriptor::parse(&icon).unwrap().body {
655            ExtensionBody::ImageIcon(b) => {
656                assert_eq!(b.descriptor_number, 0);
657                assert_eq!(b.last_descriptor_number, 12);
658                assert_eq!(b.icon_id, 4);
659                match &b.body {
660                    ImageIconBody::First(f) => {
661                        assert_eq!(f.icon_transport_mode, 0);
662                        let p = f.position.as_ref().unwrap();
663                        assert_eq!(p.coordinate_system, 2);
664                        assert_eq!(p.icon_horizontal_origin, 999);
665                        assert_eq!(p.icon_vertical_origin, 456);
666                        assert_eq!(f.icon_type.decode(), "image/png");
667                        match &f.payload {
668                            IconLocation::Data(d) => {
669                                assert_eq!(*d, &[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF])
670                            }
671                            other => panic!("expected Data, got {other:?}"),
672                        }
673                    }
674                    other => panic!("expected First, got {other:?}"),
675                }
676            }
677            other => panic!("expected ImageIcon, got {other:?}"),
678        }
679
680        let sp = from_hex("7f20221a300c04d2f000092911fc465241fd47425211fa0102fb0b0c000ddeadbeef");
681        match &ExtensionDescriptor::parse(&sp).unwrap().body {
682            ExtensionBody::ServiceProminence(b) => {
683                assert_eq!(b.sogi_list.len(), 2);
684                assert_eq!(b.sogi_list[0].sogi_priority, 12);
685                assert_eq!(b.sogi_list[0].service_id, Some(1234));
686                assert!(b.sogi_list[1].sogi_flag);
687                assert_eq!(b.sogi_list[1].service_id, Some(2345));
688                assert!(b.sogi_list[1].target_region_loop.is_some());
689                assert_eq!(b.private_data, &[0xDE, 0xAD, 0xBE, 0xEF]);
690            }
691            other => panic!("expected ServiceProminence, got {other:?}"),
692        }
693    }
694}