use crate::error::{Error, Result};
use crate::text::{DvbText, LangCode};
use crate::traits::Descriptor;
use dvb_common::{Parse, Serialize};
pub const TAG: u8 = 0x7F;
const HEADER_LEN: usize = 2;
const TAG_EXTENSION_LEN: usize = 1;
const MIN_BODY_LEN: usize = TAG_EXTENSION_LEN;
const MAX_DESCRIPTOR_LENGTH: usize = 0xFF;
const ISO_639_LEN: usize = 3;
const T2_FIXED_PREFIX_LEN: usize = 3; const T2_FLAGS_BLOCK_LEN: usize = 2; const C2_LEN: usize = 7; const C2_BUNDLE_ENTRY_LEN: usize = 8; const SERVICE_RELOCATED_LEN: usize = 6; const S2X_PRIMARY_LEN: usize = 11;
const S2X_SCRAMBLING_LEN: usize = 3;
const TTML_FIXED_LEN: usize = ISO_639_LEN + 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum ExtensionTag {
ImageIcon = 0x00,
T2DeliverySystem = 0x04,
SupplementaryAudio = 0x06,
NetworkChangeNotify = 0x07,
Message = 0x08,
TargetRegion = 0x09,
TargetRegionName = 0x0A,
ServiceRelocated = 0x0B,
C2DeliverySystem = 0x0D,
UriLinkage = 0x13,
Ac4 = 0x15,
C2BundleDeliverySystem = 0x16,
S2XSatelliteDeliverySystem = 0x17,
AudioPreselection = 0x19,
TtmlSubtitling = 0x20,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum ExtensionBody<'a> {
T2DeliverySystem(T2DeliverySystem<'a>),
SupplementaryAudio(SupplementaryAudio<'a>),
NetworkChangeNotify(NetworkChangeNotify<'a>),
Message(Message<'a>),
TargetRegion(TargetRegion<'a>),
TargetRegionName(TargetRegionName<'a>),
ServiceRelocated(ServiceRelocated),
C2DeliverySystem(C2DeliverySystem),
UriLinkage(UriLinkage<'a>),
Ac4(Ac4<'a>),
C2BundleDeliverySystem(C2BundleDeliverySystem),
S2XSatelliteDeliverySystem(S2XSatelliteDeliverySystem<'a>),
AudioPreselection(AudioPreselection<'a>),
TtmlSubtitling(TtmlSubtitling<'a>),
Raw(&'a [u8]),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct T2DeliverySystem<'a> {
pub plp_id: u8,
pub t2_system_id: u16,
pub siso_miso: Option<u8>,
pub bandwidth: Option<u8>,
pub guard_interval: Option<u8>,
pub transmission_mode: Option<u8>,
pub other_frequency_flag: Option<bool>,
pub tfs_flag: Option<bool>,
pub cell_loop: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct SupplementaryAudio<'a> {
pub mix_type: bool,
pub editorial_classification: u8,
pub language_code_present: bool,
pub iso_639_language_code: Option<LangCode>,
pub private_data: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct NetworkChangeNotify<'a> {
pub cell_loop: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct Message<'a> {
pub message_id: u8,
pub iso_639_language_code: LangCode,
pub text: DvbText<'a>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct TargetRegion<'a> {
pub country_code: LangCode,
pub region_loop: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct TargetRegionName<'a> {
pub country_code: LangCode,
pub iso_639_language_code: LangCode,
pub region_loop: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ServiceRelocated {
pub old_original_network_id: u16,
pub old_transport_stream_id: u16,
pub old_service_id: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct C2DeliverySystem {
pub plp_id: u8,
pub data_slice_id: u8,
pub c2_system_tuning_frequency: u32,
pub c2_system_tuning_frequency_type: u8,
pub active_ofdm_symbol_duration: u8,
pub guard_interval: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct UriLinkage<'a> {
pub uri_linkage_type: u8,
pub uri: &'a [u8],
pub min_polling_interval: Option<u16>,
pub private_data: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct Ac4<'a> {
pub ac4_config_flag: bool,
pub ac4_toc_flag: bool,
pub ac4_dialog_enhancement_enabled: Option<bool>,
pub ac4_channel_mode: Option<u8>,
pub toc: Option<&'a [u8]>,
pub additional_info: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct C2BundleEntry {
pub plp_id: u8,
pub data_slice_id: u8,
pub c2_system_tuning_frequency: u32,
pub c2_system_tuning_frequency_type: u8,
pub active_ofdm_symbol_duration: u8,
pub guard_interval: u8,
pub primary_channel: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct C2BundleDeliverySystem {
pub entries: Vec<C2BundleEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct S2XSatelliteDeliverySystem<'a> {
pub receiver_profiles: u8,
pub s2x_mode: u8,
pub scrambling_sequence_selector: bool,
pub ts_gs_s2x_mode: u8,
pub scrambling_sequence_index: Option<u32>,
pub frequency: u32,
pub orbital_position: u16,
pub west_east_flag: bool,
pub polarization: u8,
pub multiple_input_stream_flag: bool,
pub roll_off: u8,
pub symbol_rate: u32,
pub input_stream_identifier: Option<u8>,
pub timeslice_number: Option<u8>,
pub tail: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct AudioPreselection<'a> {
pub num_preselections: u8,
pub preselection_loop: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct TtmlSubtitling<'a> {
pub iso_639_language_code: LangCode,
pub subtitle_purpose: u8,
pub tts_suitability: u8,
pub essential_font_usage_flag: bool,
pub qualifier_present_flag: bool,
pub dvb_ttml_profile_count: u8,
pub tail: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct ExtensionDescriptor<'a> {
pub tag_extension: u8,
pub body: ExtensionBody<'a>,
}
impl ExtensionDescriptor<'_> {
#[must_use]
pub fn kind(&self) -> Option<ExtensionTag> {
Some(match self.tag_extension {
0x00 => ExtensionTag::ImageIcon,
0x04 => ExtensionTag::T2DeliverySystem,
0x06 => ExtensionTag::SupplementaryAudio,
0x07 => ExtensionTag::NetworkChangeNotify,
0x08 => ExtensionTag::Message,
0x09 => ExtensionTag::TargetRegion,
0x0A => ExtensionTag::TargetRegionName,
0x0B => ExtensionTag::ServiceRelocated,
0x0D => ExtensionTag::C2DeliverySystem,
0x13 => ExtensionTag::UriLinkage,
0x15 => ExtensionTag::Ac4,
0x16 => ExtensionTag::C2BundleDeliverySystem,
0x17 => ExtensionTag::S2XSatelliteDeliverySystem,
0x19 => ExtensionTag::AudioPreselection,
0x20 => ExtensionTag::TtmlSubtitling,
_ => return None,
})
}
}
fn invalid(reason: &'static str) -> Error {
Error::InvalidDescriptor { tag: TAG, reason }
}
fn parse_t2(sel: &[u8]) -> Result<T2DeliverySystem<'_>> {
if sel.len() < T2_FIXED_PREFIX_LEN {
return Err(invalid("T2_delivery_system: prefix truncated"));
}
let plp_id = sel[0];
let t2_system_id = u16::from_be_bytes([sel[1], sel[2]]);
let mut pos = T2_FIXED_PREFIX_LEN;
let (siso_miso, bandwidth, guard_interval, transmission_mode, other_frequency_flag, tfs_flag) =
if sel.len() > T2_FIXED_PREFIX_LEN {
if sel.len() < T2_FIXED_PREFIX_LEN + T2_FLAGS_BLOCK_LEN {
return Err(invalid("T2_delivery_system: flags block truncated"));
}
let b0 = sel[pos];
let b1 = sel[pos + 1];
pos += T2_FLAGS_BLOCK_LEN;
(
Some(b0 >> 6),
Some((b0 >> 2) & 0x0F),
Some(b1 >> 5),
Some((b1 >> 2) & 0x07),
Some((b1 & 0x02) != 0),
Some((b1 & 0x01) != 0),
)
} else {
(None, None, None, None, None, None)
};
Ok(T2DeliverySystem {
plp_id,
t2_system_id,
siso_miso,
bandwidth,
guard_interval,
transmission_mode,
other_frequency_flag,
tfs_flag,
cell_loop: &sel[pos..],
})
}
fn parse_supplementary_audio(sel: &[u8]) -> Result<SupplementaryAudio<'_>> {
if sel.is_empty() {
return Err(invalid("supplementary_audio: flags byte missing"));
}
let flags = sel[0];
let mix_type = (flags & 0x80) != 0;
let editorial_classification = (flags >> 2) & 0x1F;
let language_code_present = (flags & 0x01) != 0;
let mut pos = 1;
let iso_639_language_code = if language_code_present {
if sel.len() < pos + ISO_639_LEN {
return Err(invalid("supplementary_audio: language code truncated"));
}
let lc = &sel[pos..pos + ISO_639_LEN];
pos += ISO_639_LEN;
Some(LangCode([lc[0], lc[1], lc[2]]))
} else {
None
};
Ok(SupplementaryAudio {
mix_type,
editorial_classification,
language_code_present,
iso_639_language_code,
private_data: &sel[pos..],
})
}
fn parse_message(sel: &[u8]) -> Result<Message<'_>> {
if sel.len() < 1 + ISO_639_LEN {
return Err(invalid("message: header truncated"));
}
Ok(Message {
message_id: sel[0],
iso_639_language_code: LangCode([sel[1], sel[2], sel[3]]),
text: DvbText::new(&sel[1 + ISO_639_LEN..]),
})
}
fn parse_target_region(sel: &[u8]) -> Result<TargetRegion<'_>> {
if sel.len() < ISO_639_LEN {
return Err(invalid("target_region: country_code truncated"));
}
Ok(TargetRegion {
country_code: LangCode([sel[0], sel[1], sel[2]]),
region_loop: &sel[ISO_639_LEN..],
})
}
fn parse_target_region_name(sel: &[u8]) -> Result<TargetRegionName<'_>> {
if sel.len() < 2 * ISO_639_LEN {
return Err(invalid("target_region_name: header truncated"));
}
Ok(TargetRegionName {
country_code: LangCode([sel[0], sel[1], sel[2]]),
iso_639_language_code: LangCode([sel[3], sel[4], sel[5]]),
region_loop: &sel[2 * ISO_639_LEN..],
})
}
fn parse_service_relocated(sel: &[u8]) -> Result<ServiceRelocated> {
if sel.len() < SERVICE_RELOCATED_LEN {
return Err(invalid("service_relocated: truncated"));
}
Ok(ServiceRelocated {
old_original_network_id: u16::from_be_bytes([sel[0], sel[1]]),
old_transport_stream_id: u16::from_be_bytes([sel[2], sel[3]]),
old_service_id: u16::from_be_bytes([sel[4], sel[5]]),
})
}
fn parse_c2(sel: &[u8]) -> Result<C2DeliverySystem> {
if sel.len() < C2_LEN {
return Err(invalid("C2_delivery_system: truncated"));
}
let packed = sel[6];
Ok(C2DeliverySystem {
plp_id: sel[0],
data_slice_id: sel[1],
c2_system_tuning_frequency: u32::from_be_bytes([sel[2], sel[3], sel[4], sel[5]]),
c2_system_tuning_frequency_type: packed >> 6,
active_ofdm_symbol_duration: (packed >> 3) & 0x07,
guard_interval: packed & 0x07,
})
}
fn parse_uri_linkage(sel: &[u8]) -> Result<UriLinkage<'_>> {
if sel.len() < 2 {
return Err(invalid("URI_linkage: header truncated"));
}
let uri_linkage_type = sel[0];
let uri_length = sel[1] as usize;
let mut pos = 2;
if sel.len() < pos + uri_length {
return Err(invalid("URI_linkage: uri overruns body"));
}
let uri = &sel[pos..pos + uri_length];
pos += uri_length;
let min_polling_interval = if uri_linkage_type == 0x00 || uri_linkage_type == 0x01 {
if sel.len() < pos + 2 {
return Err(invalid("URI_linkage: min_polling_interval truncated"));
}
let v = u16::from_be_bytes([sel[pos], sel[pos + 1]]);
pos += 2;
Some(v)
} else {
None
};
Ok(UriLinkage {
uri_linkage_type,
uri,
min_polling_interval,
private_data: &sel[pos..],
})
}
fn parse_ac4(sel: &[u8]) -> Result<Ac4<'_>> {
if sel.is_empty() {
return Err(invalid("AC-4: flags byte missing"));
}
let flags = sel[0];
let ac4_config_flag = (flags & 0x80) != 0;
let ac4_toc_flag = (flags & 0x40) != 0;
let mut pos = 1;
let (ac4_dialog_enhancement_enabled, ac4_channel_mode) = if ac4_config_flag {
if sel.len() < pos + 1 {
return Err(invalid("AC-4: config byte truncated"));
}
let c = sel[pos];
pos += 1;
(Some((c & 0x80) != 0), Some((c >> 5) & 0x03))
} else {
(None, None)
};
let toc = if ac4_toc_flag {
if sel.len() < pos + 1 {
return Err(invalid("AC-4: toc length truncated"));
}
let toc_len = sel[pos] as usize;
pos += 1;
if sel.len() < pos + toc_len {
return Err(invalid("AC-4: toc overruns body"));
}
let t = &sel[pos..pos + toc_len];
pos += toc_len;
Some(t)
} else {
None
};
Ok(Ac4 {
ac4_config_flag,
ac4_toc_flag,
ac4_dialog_enhancement_enabled,
ac4_channel_mode,
toc,
additional_info: &sel[pos..],
})
}
fn parse_c2_bundle(sel: &[u8]) -> Result<C2BundleDeliverySystem> {
if sel.len() % C2_BUNDLE_ENTRY_LEN != 0 {
return Err(invalid(
"C2_bundle_delivery_system: not a whole number of entries",
));
}
let mut entries = Vec::with_capacity(sel.len() / C2_BUNDLE_ENTRY_LEN);
for chunk in sel.chunks_exact(C2_BUNDLE_ENTRY_LEN) {
let packed = chunk[6];
entries.push(C2BundleEntry {
plp_id: chunk[0],
data_slice_id: chunk[1],
c2_system_tuning_frequency: u32::from_be_bytes([
chunk[2], chunk[3], chunk[4], chunk[5],
]),
c2_system_tuning_frequency_type: packed >> 6,
active_ofdm_symbol_duration: (packed >> 3) & 0x07,
guard_interval: packed & 0x07,
primary_channel: (chunk[7] & 0x80) != 0,
});
}
Ok(C2BundleDeliverySystem { entries })
}
fn parse_s2x(sel: &[u8]) -> Result<S2XSatelliteDeliverySystem<'_>> {
if sel.len() < 2 {
return Err(invalid("S2X: flags truncated"));
}
let receiver_profiles = sel[0] >> 3;
let b1 = sel[1];
let s2x_mode = (b1 >> 6) & 0x03;
let scrambling_sequence_selector = (b1 & 0x20) != 0;
let ts_gs_s2x_mode = b1 & 0x03;
let mut pos = 2;
let scrambling_sequence_index = if scrambling_sequence_selector {
if sel.len() < pos + S2X_SCRAMBLING_LEN {
return Err(invalid("S2X: scrambling_sequence_index truncated"));
}
let idx = (u32::from(sel[pos] & 0x03) << 16)
| (u32::from(sel[pos + 1]) << 8)
| u32::from(sel[pos + 2]);
pos += S2X_SCRAMBLING_LEN;
Some(idx)
} else {
None
};
if sel.len() < pos + S2X_PRIMARY_LEN {
return Err(invalid("S2X: primary channel truncated"));
}
let frequency = u32::from_be_bytes([sel[pos], sel[pos + 1], sel[pos + 2], sel[pos + 3]]);
let orbital_position = u16::from_be_bytes([sel[pos + 4], sel[pos + 5]]);
let pb = sel[pos + 6];
let west_east_flag = (pb & 0x80) != 0;
let polarization = (pb >> 5) & 0x03;
let multiple_input_stream_flag = (pb & 0x10) != 0;
let roll_off = pb & 0x07;
let symbol_rate = (u32::from(sel[pos + 7] & 0x0F) << 24)
| (u32::from(sel[pos + 8]) << 16)
| (u32::from(sel[pos + 9]) << 8)
| u32::from(sel[pos + 10]);
pos += S2X_PRIMARY_LEN;
let input_stream_identifier = if multiple_input_stream_flag {
if sel.len() < pos + 1 {
return Err(invalid("S2X: input_stream_identifier truncated"));
}
let isi = sel[pos];
pos += 1;
Some(isi)
} else {
None
};
let timeslice_number = if s2x_mode == 2 {
if sel.len() < pos + 1 {
return Err(invalid("S2X: timeslice_number truncated"));
}
let ts = sel[pos];
pos += 1;
Some(ts)
} else {
None
};
Ok(S2XSatelliteDeliverySystem {
receiver_profiles,
s2x_mode,
scrambling_sequence_selector,
ts_gs_s2x_mode,
scrambling_sequence_index,
frequency,
orbital_position,
west_east_flag,
polarization,
multiple_input_stream_flag,
roll_off,
symbol_rate,
input_stream_identifier,
timeslice_number,
tail: &sel[pos..],
})
}
fn parse_audio_preselection(sel: &[u8]) -> Result<AudioPreselection<'_>> {
if sel.is_empty() {
return Err(invalid("audio_preselection: count byte missing"));
}
Ok(AudioPreselection {
num_preselections: sel[0] >> 3,
preselection_loop: &sel[1..],
})
}
fn parse_ttml(sel: &[u8]) -> Result<TtmlSubtitling<'_>> {
if sel.len() < TTML_FIXED_LEN {
return Err(invalid("TTML_subtitling: header truncated"));
}
let b3 = sel[ISO_639_LEN];
let b4 = sel[ISO_639_LEN + 1];
Ok(TtmlSubtitling {
iso_639_language_code: LangCode([sel[0], sel[1], sel[2]]),
subtitle_purpose: b3 >> 2,
tts_suitability: b3 & 0x03,
essential_font_usage_flag: (b4 & 0x80) != 0,
qualifier_present_flag: (b4 & 0x40) != 0,
dvb_ttml_profile_count: b4 & 0x0F,
tail: &sel[TTML_FIXED_LEN..],
})
}
fn parse_body(tag_extension: u8, sel: &[u8]) -> Result<ExtensionBody<'_>> {
Ok(match tag_extension {
0x04 => ExtensionBody::T2DeliverySystem(parse_t2(sel)?),
0x06 => ExtensionBody::SupplementaryAudio(parse_supplementary_audio(sel)?),
0x07 => ExtensionBody::NetworkChangeNotify(NetworkChangeNotify { cell_loop: sel }),
0x08 => ExtensionBody::Message(parse_message(sel)?),
0x09 => ExtensionBody::TargetRegion(parse_target_region(sel)?),
0x0A => ExtensionBody::TargetRegionName(parse_target_region_name(sel)?),
0x0B => ExtensionBody::ServiceRelocated(parse_service_relocated(sel)?),
0x0D => ExtensionBody::C2DeliverySystem(parse_c2(sel)?),
0x13 => ExtensionBody::UriLinkage(parse_uri_linkage(sel)?),
0x15 => ExtensionBody::Ac4(parse_ac4(sel)?),
0x16 => ExtensionBody::C2BundleDeliverySystem(parse_c2_bundle(sel)?),
0x17 => ExtensionBody::S2XSatelliteDeliverySystem(parse_s2x(sel)?),
0x19 => ExtensionBody::AudioPreselection(parse_audio_preselection(sel)?),
0x20 => ExtensionBody::TtmlSubtitling(parse_ttml(sel)?),
_ => ExtensionBody::Raw(sel),
})
}
impl<'a> Parse<'a> for ExtensionDescriptor<'a> {
type Error = crate::error::Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
if bytes.len() < HEADER_LEN {
return Err(Error::BufferTooShort {
need: HEADER_LEN,
have: bytes.len(),
what: "ExtensionDescriptor header",
});
}
if bytes[0] != TAG {
return Err(Error::InvalidDescriptor {
tag: bytes[0],
reason: "unexpected tag for extension_descriptor",
});
}
let length = bytes[1] as usize;
let end = HEADER_LEN + length;
if bytes.len() < end {
return Err(Error::BufferTooShort {
need: end,
have: bytes.len(),
what: "ExtensionDescriptor body",
});
}
if length < MIN_BODY_LEN {
return Err(Error::InvalidDescriptor {
tag: TAG,
reason: "body must contain at least the descriptor_tag_extension byte",
});
}
let tag_extension = bytes[HEADER_LEN];
let sel = &bytes[HEADER_LEN + TAG_EXTENSION_LEN..end];
let body = parse_body(tag_extension, sel)?;
Ok(Self {
tag_extension,
body,
})
}
}
impl ExtensionBody<'_> {
fn selector_len(&self) -> usize {
match self {
ExtensionBody::T2DeliverySystem(b) => {
T2_FIXED_PREFIX_LEN
+ if b.siso_miso.is_some() {
T2_FLAGS_BLOCK_LEN
} else {
0
}
+ b.cell_loop.len()
}
ExtensionBody::SupplementaryAudio(b) => {
1 + b.iso_639_language_code.map_or(0, |_| ISO_639_LEN) + b.private_data.len()
}
ExtensionBody::NetworkChangeNotify(b) => b.cell_loop.len(),
ExtensionBody::Message(b) => 1 + ISO_639_LEN + b.text.len(),
ExtensionBody::TargetRegion(b) => ISO_639_LEN + b.region_loop.len(),
ExtensionBody::TargetRegionName(b) => 2 * ISO_639_LEN + b.region_loop.len(),
ExtensionBody::ServiceRelocated(_) => SERVICE_RELOCATED_LEN,
ExtensionBody::C2DeliverySystem(_) => C2_LEN,
ExtensionBody::UriLinkage(b) => {
2 + b.uri.len()
+ if b.min_polling_interval.is_some() {
2
} else {
0
}
+ b.private_data.len()
}
ExtensionBody::Ac4(b) => {
1 + usize::from(b.ac4_config_flag)
+ b.toc.map_or(0, |t| 1 + t.len())
+ b.additional_info.len()
}
ExtensionBody::C2BundleDeliverySystem(b) => b.entries.len() * C2_BUNDLE_ENTRY_LEN,
ExtensionBody::S2XSatelliteDeliverySystem(b) => {
2 + if b.scrambling_sequence_selector {
S2X_SCRAMBLING_LEN
} else {
0
} + S2X_PRIMARY_LEN
+ usize::from(b.input_stream_identifier.is_some())
+ usize::from(b.timeslice_number.is_some())
+ b.tail.len()
}
ExtensionBody::AudioPreselection(b) => 1 + b.preselection_loop.len(),
ExtensionBody::TtmlSubtitling(b) => TTML_FIXED_LEN + b.tail.len(),
ExtensionBody::Raw(s) => s.len(),
}
}
fn write_selector(&self, out: &mut [u8]) {
match self {
ExtensionBody::T2DeliverySystem(b) => {
out[0] = b.plp_id;
out[1..3].copy_from_slice(&b.t2_system_id.to_be_bytes());
let mut p = T2_FIXED_PREFIX_LEN;
if let (Some(sm), Some(bw), Some(gi), Some(tm), Some(off), Some(tfs)) = (
b.siso_miso,
b.bandwidth,
b.guard_interval,
b.transmission_mode,
b.other_frequency_flag,
b.tfs_flag,
) {
out[p] = (sm << 6) | ((bw & 0x0F) << 2);
out[p + 1] =
(gi << 5) | ((tm & 0x07) << 2) | (u8::from(off) << 1) | u8::from(tfs);
p += T2_FLAGS_BLOCK_LEN;
}
out[p..p + b.cell_loop.len()].copy_from_slice(b.cell_loop);
}
ExtensionBody::SupplementaryAudio(b) => {
out[0] = (u8::from(b.mix_type) << 7)
| ((b.editorial_classification & 0x1F) << 2)
| 0x02
| u8::from(b.language_code_present);
let mut p = 1;
if let Some(lc) = b.iso_639_language_code {
out[p..p + ISO_639_LEN].copy_from_slice(&lc.0);
p += ISO_639_LEN;
}
out[p..p + b.private_data.len()].copy_from_slice(b.private_data);
}
ExtensionBody::NetworkChangeNotify(b) => {
out[..b.cell_loop.len()].copy_from_slice(b.cell_loop);
}
ExtensionBody::Message(b) => {
out[0] = b.message_id;
out[1..1 + ISO_639_LEN].copy_from_slice(&b.iso_639_language_code.0);
out[1 + ISO_639_LEN..1 + ISO_639_LEN + b.text.len()].copy_from_slice(b.text.raw());
}
ExtensionBody::TargetRegion(b) => {
out[..ISO_639_LEN].copy_from_slice(&b.country_code.0);
out[ISO_639_LEN..ISO_639_LEN + b.region_loop.len()].copy_from_slice(b.region_loop);
}
ExtensionBody::TargetRegionName(b) => {
out[..ISO_639_LEN].copy_from_slice(&b.country_code.0);
out[ISO_639_LEN..2 * ISO_639_LEN].copy_from_slice(&b.iso_639_language_code.0);
out[2 * ISO_639_LEN..2 * ISO_639_LEN + b.region_loop.len()]
.copy_from_slice(b.region_loop);
}
ExtensionBody::ServiceRelocated(b) => {
out[0..2].copy_from_slice(&b.old_original_network_id.to_be_bytes());
out[2..4].copy_from_slice(&b.old_transport_stream_id.to_be_bytes());
out[4..6].copy_from_slice(&b.old_service_id.to_be_bytes());
}
ExtensionBody::C2DeliverySystem(b) => {
out[0] = b.plp_id;
out[1] = b.data_slice_id;
out[2..6].copy_from_slice(&b.c2_system_tuning_frequency.to_be_bytes());
out[6] = (b.c2_system_tuning_frequency_type << 6)
| ((b.active_ofdm_symbol_duration & 0x07) << 3)
| (b.guard_interval & 0x07);
}
ExtensionBody::UriLinkage(b) => {
out[0] = b.uri_linkage_type;
out[1] = b.uri.len() as u8;
let mut p = 2;
out[p..p + b.uri.len()].copy_from_slice(b.uri);
p += b.uri.len();
if let Some(mpi) = b.min_polling_interval {
out[p..p + 2].copy_from_slice(&mpi.to_be_bytes());
p += 2;
}
out[p..p + b.private_data.len()].copy_from_slice(b.private_data);
}
ExtensionBody::Ac4(b) => {
out[0] = (u8::from(b.ac4_config_flag) << 7) | (u8::from(b.ac4_toc_flag) << 6);
let mut p = 1;
if b.ac4_config_flag {
let de = b.ac4_dialog_enhancement_enabled.unwrap_or(false);
let cm = b.ac4_channel_mode.unwrap_or(0) & 0x03;
out[p] = (u8::from(de) << 7) | (cm << 5);
p += 1;
}
if let Some(t) = b.toc {
out[p] = t.len() as u8;
p += 1;
out[p..p + t.len()].copy_from_slice(t);
p += t.len();
}
out[p..p + b.additional_info.len()].copy_from_slice(b.additional_info);
}
ExtensionBody::C2BundleDeliverySystem(b) => {
let mut p = 0;
for e in &b.entries {
out[p] = e.plp_id;
out[p + 1] = e.data_slice_id;
out[p + 2..p + 6].copy_from_slice(&e.c2_system_tuning_frequency.to_be_bytes());
out[p + 6] = (e.c2_system_tuning_frequency_type << 6)
| ((e.active_ofdm_symbol_duration & 0x07) << 3)
| (e.guard_interval & 0x07);
out[p + 7] = u8::from(e.primary_channel) << 7;
p += C2_BUNDLE_ENTRY_LEN;
}
}
ExtensionBody::S2XSatelliteDeliverySystem(b) => {
out[0] = b.receiver_profiles << 3;
out[1] = ((b.s2x_mode & 0x03) << 6)
| (u8::from(b.scrambling_sequence_selector) << 5)
| (b.ts_gs_s2x_mode & 0x03);
let mut p = 2;
if b.scrambling_sequence_selector {
let idx = b.scrambling_sequence_index.unwrap_or(0) & 0x3FFFF;
out[p] = (idx >> 16) as u8 & 0x03;
out[p + 1] = (idx >> 8) as u8;
out[p + 2] = idx as u8;
p += S2X_SCRAMBLING_LEN;
}
out[p..p + 4].copy_from_slice(&b.frequency.to_be_bytes());
out[p + 4..p + 6].copy_from_slice(&b.orbital_position.to_be_bytes());
out[p + 6] = (u8::from(b.west_east_flag) << 7)
| ((b.polarization & 0x03) << 5)
| (u8::from(b.multiple_input_stream_flag) << 4)
| (b.roll_off & 0x07);
let sr = b.symbol_rate & 0x0FFF_FFFF;
out[p + 7] = (sr >> 24) as u8 & 0x0F;
out[p + 8] = (sr >> 16) as u8;
out[p + 9] = (sr >> 8) as u8;
out[p + 10] = sr as u8;
p += S2X_PRIMARY_LEN;
if let Some(isi) = b.input_stream_identifier {
out[p] = isi;
p += 1;
}
if let Some(ts) = b.timeslice_number {
out[p] = ts;
p += 1;
}
out[p..p + b.tail.len()].copy_from_slice(b.tail);
}
ExtensionBody::AudioPreselection(b) => {
out[0] = b.num_preselections << 3;
out[1..1 + b.preselection_loop.len()].copy_from_slice(b.preselection_loop);
}
ExtensionBody::TtmlSubtitling(b) => {
out[..ISO_639_LEN].copy_from_slice(&b.iso_639_language_code.0);
out[ISO_639_LEN] = (b.subtitle_purpose << 2) | (b.tts_suitability & 0x03);
out[ISO_639_LEN + 1] = (u8::from(b.essential_font_usage_flag) << 7)
| (u8::from(b.qualifier_present_flag) << 6)
| (b.dvb_ttml_profile_count & 0x0F);
out[TTML_FIXED_LEN..TTML_FIXED_LEN + b.tail.len()].copy_from_slice(b.tail);
}
ExtensionBody::Raw(s) => out[..s.len()].copy_from_slice(s),
}
}
}
impl Serialize for ExtensionDescriptor<'_> {
type Error = crate::error::Error;
fn serialized_len(&self) -> usize {
HEADER_LEN + TAG_EXTENSION_LEN + self.body.selector_len()
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let len = self.serialized_len();
if buf.len() < len {
return Err(Error::OutputBufferTooSmall {
need: len,
have: buf.len(),
});
}
let body_len = len - HEADER_LEN;
if body_len > MAX_DESCRIPTOR_LENGTH {
return Err(Error::InvalidDescriptor {
tag: TAG,
reason: "descriptor_length exceeds 255 bytes",
});
}
buf[0] = TAG;
buf[1] = body_len as u8;
buf[HEADER_LEN] = self.tag_extension;
self.body
.write_selector(&mut buf[HEADER_LEN + TAG_EXTENSION_LEN..len]);
Ok(len)
}
}
impl<'a> Descriptor<'a> for ExtensionDescriptor<'a> {
const TAG: u8 = TAG;
fn descriptor_length(&self) -> u8 {
(self.serialized_len() - HEADER_LEN) as u8
}
}
impl<'a> crate::traits::DescriptorDef<'a> for ExtensionDescriptor<'a> {
const TAG: u8 = TAG;
const NAME: &'static str = "EXTENSION";
}
#[cfg(test)]
mod tests {
use super::*;
fn wrap(tag_ext: u8, sel: &[u8]) -> Vec<u8> {
let mut v = vec![TAG, (sel.len() + 1) as u8, tag_ext];
v.extend_from_slice(sel);
v
}
fn round_trip(d: &ExtensionDescriptor) {
let mut buf = vec![0u8; d.serialized_len()];
d.serialize_into(&mut buf).unwrap();
let re = ExtensionDescriptor::parse(&buf).unwrap();
assert_eq!(*d, re);
}
#[test]
fn parse_rejects_wrong_tag() {
let raw = [0x43, 1, 0x04];
assert!(matches!(
ExtensionDescriptor::parse(&raw).unwrap_err(),
Error::InvalidDescriptor { tag: 0x43, .. }
));
}
#[test]
fn parse_rejects_empty_body() {
let raw = [TAG, 0];
assert!(matches!(
ExtensionDescriptor::parse(&raw).unwrap_err(),
Error::InvalidDescriptor { tag: TAG, .. }
));
}
#[test]
fn parse_rejects_truncated_body() {
let raw = [TAG, 3, 0x08];
assert!(matches!(
ExtensionDescriptor::parse(&raw).unwrap_err(),
Error::BufferTooShort { .. }
));
}
#[test]
fn unknown_tag_round_trips_as_raw() {
let sel = [0xDE, 0xAD, 0xBE, 0xEF];
let bytes = wrap(0x42, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
assert_eq!(d.tag_extension, 0x42);
assert_eq!(d.kind(), None);
assert!(matches!(d.body, ExtensionBody::Raw(b) if b == sel));
round_trip(&d);
}
#[test]
fn user_defined_tag_preserved() {
let bytes = wrap(0x90, &[0x01, 0x02]);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
assert_eq!(d.tag_extension, 0x90);
assert!(matches!(d.body, ExtensionBody::Raw(_)));
round_trip(&d);
}
#[test]
fn parse_service_relocated() {
let sel = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC];
let bytes = wrap(0x0B, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
assert_eq!(d.kind(), Some(ExtensionTag::ServiceRelocated));
match &d.body {
ExtensionBody::ServiceRelocated(b) => {
assert_eq!(b.old_original_network_id, 0x1234);
assert_eq!(b.old_transport_stream_id, 0x5678);
assert_eq!(b.old_service_id, 0x9ABC);
}
other => panic!("expected ServiceRelocated, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_message() {
let sel = [0x07, b'e', b'n', b'g', b'H', b'i'];
let bytes = wrap(0x08, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::Message(b) => {
assert_eq!(b.message_id, 0x07);
assert_eq!(b.iso_639_language_code, LangCode(*b"eng"));
assert_eq!(b.text.raw(), b"Hi");
}
other => panic!("expected Message, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_supplementary_audio_with_language() {
let flags = 0x80 | (0x17 << 2) | 0x02 | 0x01;
let sel = [flags, b'f', b'r', b'e', 0xAA];
let bytes = wrap(0x06, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::SupplementaryAudio(b) => {
assert!(b.mix_type);
assert_eq!(b.editorial_classification, 0x17);
assert!(b.language_code_present);
assert_eq!(b.iso_639_language_code, Some(LangCode(*b"fre")));
assert_eq!(b.private_data, &[0xAA]);
}
other => panic!("expected SupplementaryAudio, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_supplementary_audio_no_language() {
let flags = ((0x01 << 2) & 0x7C) | 0x02; let sel = [flags];
let bytes = wrap(0x06, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::SupplementaryAudio(b) => {
assert!(!b.language_code_present);
assert_eq!(b.iso_639_language_code, None);
assert!(b.private_data.is_empty());
}
other => panic!("expected SupplementaryAudio, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_c2_delivery_system() {
let packed = (0x02 << 6) | (0x01 << 3) | 0x01;
let sel = [0x05, 0x09, 0x12, 0x34, 0x56, 0x78, packed];
let bytes = wrap(0x0D, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::C2DeliverySystem(b) => {
assert_eq!(b.plp_id, 0x05);
assert_eq!(b.data_slice_id, 0x09);
assert_eq!(b.c2_system_tuning_frequency, 0x1234_5678);
assert_eq!(b.c2_system_tuning_frequency_type, 0x02);
assert_eq!(b.active_ofdm_symbol_duration, 0x01);
assert_eq!(b.guard_interval, 0x01);
}
other => panic!("expected C2DeliverySystem, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_c2_bundle_two_entries() {
let entry = |off: u8| {
let packed = (0x01u8 << 6) | 0x01; [off, off + 1, 0x00, 0x00, 0x10, 0x00, packed, 0x80]
};
let mut sel = Vec::new();
sel.extend_from_slice(&entry(0x01));
sel.extend_from_slice(&entry(0x05));
let bytes = wrap(0x16, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::C2BundleDeliverySystem(b) => {
assert_eq!(b.entries.len(), 2);
assert_eq!(b.entries[0].plp_id, 0x01);
assert!(b.entries[0].primary_channel);
assert_eq!(b.entries[1].plp_id, 0x05);
assert_eq!(b.entries[1].guard_interval, 0x01);
}
other => panic!("expected C2BundleDeliverySystem, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_c2_bundle_rejects_partial_entry() {
let sel = [0x01, 0x02, 0x03]; let bytes = wrap(0x16, &sel);
assert!(matches!(
ExtensionDescriptor::parse(&bytes).unwrap_err(),
Error::InvalidDescriptor { tag: TAG, .. }
));
}
#[test]
fn parse_uri_linkage_with_polling() {
let uri = b"http://x";
let mut sel = vec![0x00, uri.len() as u8];
sel.extend_from_slice(uri);
sel.extend_from_slice(&0x1234u16.to_be_bytes());
sel.push(0xFE); let bytes = wrap(0x13, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::UriLinkage(b) => {
assert_eq!(b.uri_linkage_type, 0x00);
assert_eq!(b.uri, uri);
assert_eq!(b.min_polling_interval, Some(0x1234));
assert_eq!(b.private_data, &[0xFE]);
}
other => panic!("expected UriLinkage, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_uri_linkage_no_polling() {
let uri = b"dvb:";
let mut sel = vec![0x02, uri.len() as u8];
sel.extend_from_slice(uri);
let bytes = wrap(0x13, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::UriLinkage(b) => {
assert_eq!(b.min_polling_interval, None);
assert!(b.private_data.is_empty());
}
other => panic!("expected UriLinkage, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_uri_linkage_rejects_overrun() {
let sel = [0x02, 0x10, 0xAA]; let bytes = wrap(0x13, &sel);
assert!(matches!(
ExtensionDescriptor::parse(&bytes).unwrap_err(),
Error::InvalidDescriptor { tag: TAG, .. }
));
}
#[test]
fn parse_ac4_full() {
let sel = [0xC0, 0x80 | (0x02 << 5), 0x02, 0x11, 0x22, 0x33];
let bytes = wrap(0x15, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::Ac4(b) => {
assert!(b.ac4_config_flag);
assert!(b.ac4_toc_flag);
assert_eq!(b.ac4_dialog_enhancement_enabled, Some(true));
assert_eq!(b.ac4_channel_mode, Some(0x02));
assert_eq!(b.toc, Some([0x11u8, 0x22].as_slice()));
assert_eq!(b.additional_info, &[0x33]);
}
other => panic!("expected Ac4, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_ac4_minimal() {
let sel = [0x00]; let bytes = wrap(0x15, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::Ac4(b) => {
assert!(!b.ac4_config_flag);
assert!(!b.ac4_toc_flag);
assert_eq!(b.toc, None);
assert!(b.additional_info.is_empty());
}
other => panic!("expected Ac4, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_t2_minimal() {
let sel = [0x07, 0x12, 0x34];
let bytes = wrap(0x04, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::T2DeliverySystem(b) => {
assert_eq!(b.plp_id, 0x07);
assert_eq!(b.t2_system_id, 0x1234);
assert_eq!(b.siso_miso, None);
assert!(b.cell_loop.is_empty());
}
other => panic!("expected T2DeliverySystem, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_t2_with_flags_and_cells() {
let b0 = (0x01 << 6) | (0x02 << 2);
let b1 = (0x03 << 5) | (0x04 << 2) | 0x02; let sel = [0x07, 0x12, 0x34, b0, b1, 0xCA, 0xFE];
let bytes = wrap(0x04, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::T2DeliverySystem(b) => {
assert_eq!(b.siso_miso, Some(0x01));
assert_eq!(b.bandwidth, Some(0x02));
assert_eq!(b.guard_interval, Some(0x03));
assert_eq!(b.transmission_mode, Some(0x04));
assert_eq!(b.other_frequency_flag, Some(true));
assert_eq!(b.tfs_flag, Some(false));
assert_eq!(b.cell_loop, &[0xCA, 0xFE]);
}
other => panic!("expected T2DeliverySystem, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_s2x_primary_with_isi_and_timeslice() {
let b0 = 0x05 << 3;
let b1 = (0x02 << 6) | 0x01; let mut sel = vec![b0, b1];
sel.extend_from_slice(&0x0102_0304u32.to_be_bytes()); sel.extend_from_slice(&0x00C8u16.to_be_bytes()); sel.push((1 << 7) | (0x02 << 5) | (1 << 4) | 0x03); let sr: u32 = 0x0AB_CDEF; sel.push((sr >> 24) as u8 & 0x0F);
sel.push((sr >> 16) as u8);
sel.push((sr >> 8) as u8);
sel.push(sr as u8);
sel.push(0x42); sel.push(0x09); let bytes = wrap(0x17, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::S2XSatelliteDeliverySystem(b) => {
assert_eq!(b.receiver_profiles, 0x05);
assert_eq!(b.s2x_mode, 2);
assert!(!b.scrambling_sequence_selector);
assert_eq!(b.ts_gs_s2x_mode, 1);
assert_eq!(b.frequency, 0x0102_0304);
assert_eq!(b.orbital_position, 0x00C8);
assert!(b.west_east_flag);
assert_eq!(b.polarization, 2);
assert!(b.multiple_input_stream_flag);
assert_eq!(b.roll_off, 3);
assert_eq!(b.symbol_rate, 0x0AB_CDEF);
assert_eq!(b.input_stream_identifier, Some(0x42));
assert_eq!(b.timeslice_number, Some(0x09));
assert!(b.tail.is_empty());
}
other => panic!("expected S2X, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_s2x_with_scrambling_index() {
let b0 = 0x01 << 3;
let b1 = (0x01 << 6) | 0x20; let mut sel = vec![b0, b1];
sel.push(0x02);
sel.push(0xAB);
sel.push(0xCD);
sel.extend_from_slice(&0u32.to_be_bytes()); sel.extend_from_slice(&0u16.to_be_bytes()); sel.push(0x00); sel.extend_from_slice(&[0, 0, 0, 0]); let bytes = wrap(0x17, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::S2XSatelliteDeliverySystem(b) => {
assert!(b.scrambling_sequence_selector);
assert_eq!(b.scrambling_sequence_index, Some(0x2ABCD));
assert_eq!(b.input_stream_identifier, None);
assert_eq!(b.timeslice_number, None);
}
other => panic!("expected S2X, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_s2x_mode3_tail_preserved() {
let b0 = 0x01 << 3;
let b1 = 0x03 << 6; let mut sel = vec![b0, b1];
sel.extend_from_slice(&0u32.to_be_bytes());
sel.extend_from_slice(&0u16.to_be_bytes());
sel.push(0x00); sel.extend_from_slice(&[0, 0, 0, 0]); sel.extend_from_slice(&[0xAA, 0xBB, 0xCC]); let bytes = wrap(0x17, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::S2XSatelliteDeliverySystem(b) => {
assert_eq!(b.s2x_mode, 3);
assert_eq!(b.timeslice_number, None);
assert_eq!(b.tail, &[0xAA, 0xBB, 0xCC]);
}
other => panic!("expected S2X, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_audio_preselection_keeps_loop_raw() {
let sel = [0x03 << 3, 0xAA, 0xBB, 0xCC];
let bytes = wrap(0x19, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::AudioPreselection(b) => {
assert_eq!(b.num_preselections, 3);
assert_eq!(b.preselection_loop, &[0xAA, 0xBB, 0xCC]);
}
other => panic!("expected AudioPreselection, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_ttml_subtitling() {
let b3 = (0x10 << 2) | 0x01;
let b4 = 0x01; let sel = [b'e', b'n', b'g', b3, b4, 0x00, 0x02, b'h', b'i'];
let bytes = wrap(0x20, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::TtmlSubtitling(b) => {
assert_eq!(b.iso_639_language_code, LangCode(*b"eng"));
assert_eq!(b.subtitle_purpose, 0x10);
assert_eq!(b.tts_suitability, 0x01);
assert!(!b.essential_font_usage_flag);
assert!(!b.qualifier_present_flag);
assert_eq!(b.dvb_ttml_profile_count, 1);
assert_eq!(b.tail, &[0x00, 0x02, b'h', b'i']);
}
other => panic!("expected TtmlSubtitling, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_target_region_loop_raw() {
let sel = [b'g', b'b', b'r', 0x01, 0x02, 0x03];
let bytes = wrap(0x09, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::TargetRegion(b) => {
assert_eq!(b.country_code, LangCode(*b"gbr"));
assert_eq!(b.region_loop, &[0x01, 0x02, 0x03]);
}
other => panic!("expected TargetRegion, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_target_region_name_loop_raw() {
let sel = [b'g', b'b', b'r', b'e', b'n', b'g', 0x44, 0x55];
let bytes = wrap(0x0A, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::TargetRegionName(b) => {
assert_eq!(b.country_code, LangCode(*b"gbr"));
assert_eq!(b.iso_639_language_code, LangCode(*b"eng"));
assert_eq!(b.region_loop, &[0x44, 0x55]);
}
other => panic!("expected TargetRegionName, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_network_change_notify_loop_raw() {
let sel = [0x00, 0x01, 0x05, 0xAA, 0xBB];
let bytes = wrap(0x07, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
match &d.body {
ExtensionBody::NetworkChangeNotify(b) => {
assert_eq!(b.cell_loop, &sel);
}
other => panic!("expected NetworkChangeNotify, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn serialize_rejects_too_small_buffer() {
let d = ExtensionDescriptor {
tag_extension: 0x0B,
body: ExtensionBody::ServiceRelocated(ServiceRelocated {
old_original_network_id: 1,
old_transport_stream_id: 2,
old_service_id: 3,
}),
};
let mut tiny = [0u8; 2];
assert!(matches!(
d.serialize_into(&mut tiny).unwrap_err(),
Error::OutputBufferTooSmall { .. }
));
}
#[test]
fn descriptor_length_matches_body() {
let d = ExtensionDescriptor {
tag_extension: 0x08,
body: ExtensionBody::Message(Message {
message_id: 1,
iso_639_language_code: LangCode(*b"eng"),
text: DvbText::new(b"hello"),
}),
};
assert_eq!(d.descriptor_length(), 10);
}
#[cfg(feature = "serde")]
#[test]
fn serde_serialize_is_stable_owned_body() {
let typed = ExtensionDescriptor {
tag_extension: 0x0D,
body: ExtensionBody::C2DeliverySystem(C2DeliverySystem {
plp_id: 1,
data_slice_id: 2,
c2_system_tuning_frequency: 0xDEAD_BEEF,
c2_system_tuning_frequency_type: 1,
active_ofdm_symbol_duration: 2,
guard_interval: 3,
}),
};
let json = serde_json::to_string(&typed).unwrap();
assert_eq!(json, serde_json::to_string(&typed.clone()).unwrap());
assert!(json.contains("\"tag_extension\":13"));
assert!(json.contains("\"C2DeliverySystem\""));
}
#[cfg(feature = "serde")]
#[test]
fn serde_serializes_borrowed_body() {
let raw = ExtensionDescriptor {
tag_extension: 0x42,
body: ExtensionBody::Raw(&[0x01, 0x02, 0x03]),
};
let json = serde_json::to_string(&raw).unwrap();
assert!(json.contains("\"tag_extension\":66"));
assert!(json.contains("\"Raw\""));
let msg = ExtensionDescriptor {
tag_extension: 0x08,
body: ExtensionBody::Message(Message {
message_id: 7,
iso_639_language_code: LangCode(*b"eng"),
text: DvbText::new(b"hi"),
}),
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"message_id\":7"));
}
}