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