use super::*;
use alloc::vec::Vec;
use broadcast_common::len::{need_in, need_out};
impl<'a> ExtensionBodyDef<'a> for TtmlSubtitling<'a> {
const TAG_EXTENSION: u8 = 0x20;
const NAME: &'static str = "TTML_SUBTITLING";
}
pub const TTML_RESERVED_ZERO_FUTURE_USE_DEFAULT: u8 = 0x00;
const RESERVED_ZERO_2_MASK: u8 = 0x03;
const FONT_ID_MASK: u8 = 0x7F;
const FONT_RESERVED_MASK: u8 = 0x01;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct TtmlFont {
pub reserved_zero_future_use: u8,
pub font_id: u8,
}
impl TtmlFont {
#[must_use]
pub const fn new(font_id: u8) -> Self {
Self {
reserved_zero_future_use: TTML_RESERVED_ZERO_FUTURE_USE_DEFAULT,
font_id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
#[non_exhaustive]
pub struct TtmlSubtitling<'a> {
pub iso_639_language_code: LangCode,
pub subtitle_purpose: u8,
pub tts_suitability: u8,
pub reserved_zero_future_use: u8,
pub dvb_ttml_profiles: Vec<u8>,
pub qualifier: Option<u32>,
pub font_ids: Option<Vec<TtmlFont>>,
pub text: DvbText<'a>,
pub reserved_tail: &'a [u8],
}
impl<'a> TtmlSubtitling<'a> {
#[must_use]
pub const fn new(
iso_639_language_code: LangCode,
subtitle_purpose: u8,
tts_suitability: u8,
dvb_ttml_profiles: Vec<u8>,
qualifier: Option<u32>,
font_ids: Option<Vec<TtmlFont>>,
text: DvbText<'a>,
) -> Self {
Self {
iso_639_language_code,
subtitle_purpose,
tts_suitability,
reserved_zero_future_use: TTML_RESERVED_ZERO_FUTURE_USE_DEFAULT,
dvb_ttml_profiles,
qualifier,
font_ids,
text,
reserved_tail: &[],
}
}
}
impl<'a> Parse<'a> for TtmlSubtitling<'a> {
type Error = crate::error::Error;
fn parse(sel: &'a [u8]) -> Result<Self> {
need_in(sel, TTML_FIXED_LEN, "TTML_subtitling body")?;
let iso_639_language_code = LangCode([sel[0], sel[1], sel[2]]);
let b3 = sel[ISO_639_LEN];
let subtitle_purpose = b3 >> 2;
let tts_suitability = b3 & 0x03;
let b4 = sel[ISO_639_LEN + 1];
let essential_font_usage_flag = (b4 >> 7) & 1;
let qualifier_present_flag = (b4 >> 6) & 1;
let reserved_zero_future_use = (b4 >> 4) & RESERVED_ZERO_2_MASK;
let dvb_ttml_profile_count = (b4 & 0x0F) as usize;
let mut pos = TTML_FIXED_LEN;
need_in(sel, pos + dvb_ttml_profile_count, "TTML_subtitling body")?;
let dvb_ttml_profiles = sel[pos..pos + dvb_ttml_profile_count].to_vec();
pos += dvb_ttml_profile_count;
let qualifier = if qualifier_present_flag != 0 {
let (b, _) = sel
.get(pos..)
.and_then(|s| s.split_first_chunk::<4>())
.ok_or(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: pos + 4,
have: sel.len(),
what: "TTML_subtitling body",
},
))?;
let q = u32::from_be_bytes(*b);
pos += 4;
Some(q)
} else {
None
};
let font_ids = if essential_font_usage_flag != 0 {
if sel.len() <= pos {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: pos + 1,
have: sel.len(),
what: "TTML_subtitling body",
},
));
}
let font_count = sel[pos] as usize;
pos += 1;
need_in(sel, pos + font_count, "TTML_subtitling body")?;
let ids: Vec<TtmlFont> = sel[pos..pos + font_count]
.iter()
.map(|b| TtmlFont {
reserved_zero_future_use: (b >> 7) & FONT_RESERVED_MASK,
font_id: b & FONT_ID_MASK,
})
.collect();
pos += font_count;
Some(ids)
} else {
None
};
if sel.len() <= pos {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: pos + 1,
have: sel.len(),
what: "TTML_subtitling body",
},
));
}
let text_length = sel[pos] as usize;
pos += 1;
need_in(sel, pos + text_length, "TTML_subtitling body")?;
let text = DvbText::new(&sel[pos..pos + text_length]);
pos += text_length;
let reserved_tail = &sel[pos..];
Ok(TtmlSubtitling {
iso_639_language_code,
subtitle_purpose,
tts_suitability,
reserved_zero_future_use,
dvb_ttml_profiles,
qualifier,
font_ids,
text,
reserved_tail,
})
}
}
impl Serialize for TtmlSubtitling<'_> {
type Error = crate::error::Error;
fn serialized_len(&self) -> usize {
TTML_FIXED_LEN
+ self.dvb_ttml_profiles.len()
+ self.qualifier.map_or(0, |_| 4)
+ self.font_ids.as_ref().map_or(0, |ids| 1 + ids.len())
+ 1
+ self.text.len()
+ self.reserved_tail.len()
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let len = self.serialized_len();
need_out(buf, len)?;
buf[..ISO_639_LEN].copy_from_slice(&self.iso_639_language_code.0);
buf[ISO_639_LEN] =
(broadcast_common::len::fit_mask(self.subtitle_purpose, 0x3F, "subtitle_purpose")?
<< 2)
| broadcast_common::len::fit_mask(self.tts_suitability, 0x03, "tts_suitability")?;
let dvb_ttml_profile_count = broadcast_common::len::fit_bits(
self.dvb_ttml_profiles.len() as u64,
4,
"dvb_ttml_profile_count",
)? as u8;
buf[ISO_639_LEN + 1] = (if self.font_ids.is_some() { 0x80 } else { 0 })
| (if self.qualifier.is_some() { 0x40 } else { 0 })
| (broadcast_common::len::fit_mask(
self.reserved_zero_future_use,
RESERVED_ZERO_2_MASK,
"reserved_zero_future_use",
)? << 4)
| dvb_ttml_profile_count;
let mut pos = TTML_FIXED_LEN;
buf[pos..pos + self.dvb_ttml_profiles.len()].copy_from_slice(&self.dvb_ttml_profiles);
pos += self.dvb_ttml_profiles.len();
if let Some(q) = self.qualifier {
buf[pos..pos + 4].copy_from_slice(&q.to_be_bytes());
pos += 4;
}
if let Some(ids) = &self.font_ids {
buf[pos] = broadcast_common::len::fit_u8(ids.len(), "font_id_count")?;
pos += 1;
for id in ids {
buf[pos] = (broadcast_common::len::fit_mask(
id.reserved_zero_future_use,
FONT_RESERVED_MASK,
"reserved_zero_future_use",
)? << 7)
| broadcast_common::len::fit_mask(id.font_id, FONT_ID_MASK, "font_id")?;
pos += 1;
}
}
let raw = self.text.raw();
buf[pos] = broadcast_common::len::fit_u8(raw.len(), "text_length")?;
pos += 1;
buf[pos..pos + raw.len()].copy_from_slice(raw);
pos += raw.len();
buf[pos..pos + self.reserved_tail.len()].copy_from_slice(self.reserved_tail);
Ok(len)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptors::extension::test_support::*;
use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor};
use crate::text::LangCode;
#[test]
fn parse_full_ttml_subtitling() {
let b3 = (0x10 << 2) | 0x01; let b4 = 0x80 | 0x40 | 0x02; let sel = [
b'e', b'n', b'g', b3, b4, 0x00, 0x02, 0xDE, 0xAD, 0xBE, 0xEF, 3, 0x10, 0x20, 0x30, 5, b'h', b'e', b'l', b'l', b'o', 0xFF, 0xFE, ];
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_eq!(b.dvb_ttml_profiles, &[0x00, 0x02]);
assert_eq!(b.qualifier, Some(0xDEAD_BEEF));
assert_eq!(
b.font_ids.as_deref(),
Some(
&[
TtmlFont::new(0x10),
TtmlFont::new(0x20),
TtmlFont::new(0x30)
][..]
)
);
assert_eq!(b.text.decode(), "hello");
assert_eq!(b.text.raw(), b"hello");
assert_eq!(b.reserved_tail, &[0xFF, 0xFE]);
}
other => panic!("expected TtmlSubtitling, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn parse_minimal_ttml_subtitling() {
let sel = [
b'f', b'r', b'e', 0x00, 0x00, 3, b'f', b'o', b'o', ];
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"fre"));
assert_eq!(b.subtitle_purpose, 0);
assert_eq!(b.tts_suitability, 0);
assert!(b.dvb_ttml_profiles.is_empty());
assert_eq!(b.qualifier, None);
assert_eq!(b.font_ids, None);
assert_eq!(b.text.decode(), "foo");
assert_eq!(b.text.raw(), b"foo");
assert!(b.reserved_tail.is_empty());
}
other => panic!("expected TtmlSubtitling, got {other:?}"),
}
round_trip(&d);
}
#[test]
fn ttml_reserved_preserved() {
for r2 in 0u8..=RESERVED_ZERO_2_MASK {
for r1 in 0u8..=1 {
let sel = [
b'e',
b'n',
b'g',
0x00,
0x80 | (r2 << 4) | 1, 0x05, 2, (r1 << 7) | 0x11,
((1 - r1) << 7) | 0x22,
1,
b'x',
];
let bytes = wrap(0x20, &sel);
let d = ExtensionDescriptor::parse(&bytes).unwrap();
let mut buf = vec![0u8; d.serialized_len()];
d.serialize_into(&mut buf).unwrap();
assert_eq!(buf, bytes, "r2={r2} r1={r1}");
}
}
}
#[test]
fn ttml_default_is_spec_value() {
let d = TtmlSubtitling::new(
LangCode(*b"eng"),
0,
0,
vec![],
None,
Some(vec![TtmlFont::new(0x11)]),
DvbText::new(b""),
);
let mut buf = vec![0u8; d.serialized_len()];
d.serialize_into(&mut buf).unwrap();
assert_eq!(buf, [b'e', b'n', b'g', 0x00, 0x80, 1, 0x11, 0]);
}
#[test]
fn parse_rejects_truncated_font_loop() {
let sel = [
b'e', b'n', b'g', 0x00, 0x80, 3, 0x10, ];
let bytes = wrap(0x20, &sel);
let result = ExtensionDescriptor::parse(&bytes);
assert!(
matches!(result, Err(crate::error::Error::BufferTooShort(..))),
"expected BufferTooShort, got {result:?}"
);
}
#[test]
fn parse_rejects_truncated_before_text_length() {
let sel = [b'e', b'n', b'g', 0x00, 0x00];
let bytes = wrap(0x20, &sel);
let result = ExtensionDescriptor::parse(&bytes);
assert!(
matches!(result, Err(crate::error::Error::BufferTooShort(..))),
"expected BufferTooShort, got {result:?}"
);
}
#[test]
fn parse_rejects_header_too_short() {
let sel = *b"en"; let bytes = wrap(0x20, &sel);
let result = ExtensionDescriptor::parse(&bytes);
assert!(
matches!(result, Err(crate::error::Error::BufferTooShort(..))),
"expected BufferTooShort, got {result:?}"
);
}
}