use super::*;
impl<'a> ExtensionBodyDef<'a> for TtmlSubtitling<'a> {
const TAG_EXTENSION: u8 = 0x20;
const NAME: &'static str = "TTML_SUBTITLING";
}
#[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 dvb_ttml_profiles: Vec<u8>,
pub qualifier: Option<u32>,
pub font_ids: Option<Vec<u8>>,
pub text: DvbText<'a>,
pub reserved_tail: &'a [u8],
}
impl<'a> Parse<'a> for TtmlSubtitling<'a> {
type Error = crate::error::Error;
fn parse(sel: &'a [u8]) -> Result<Self> {
if sel.len() < TTML_FIXED_LEN {
return Err(Error::BufferTooShort {
need: TTML_FIXED_LEN,
have: sel.len(),
what: "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 dvb_ttml_profile_count = (b4 & 0x0F) as usize;
let mut pos = TTML_FIXED_LEN;
if sel.len() < pos + dvb_ttml_profile_count {
return Err(Error::BufferTooShort {
need: pos + dvb_ttml_profile_count,
have: sel.len(),
what: "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 {
if sel.len() < pos + 4 {
return Err(Error::BufferTooShort {
need: pos + 4,
have: sel.len(),
what: "TTML_subtitling body",
});
}
let q = u32::from_be_bytes([sel[pos], sel[pos + 1], sel[pos + 2], sel[pos + 3]]);
pos += 4;
Some(q)
} else {
None
};
let font_ids = if essential_font_usage_flag != 0 {
if sel.len() <= pos {
return Err(Error::BufferTooShort {
need: pos + 1,
have: sel.len(),
what: "TTML_subtitling body",
});
}
let font_count = sel[pos] as usize;
pos += 1;
if sel.len() < pos + font_count {
return Err(Error::BufferTooShort {
need: pos + font_count,
have: sel.len(),
what: "TTML_subtitling body",
});
}
let ids: Vec<u8> = sel[pos..pos + font_count]
.iter()
.map(|b| b & 0x7F)
.collect();
pos += font_count;
Some(ids)
} else {
None
};
if sel.len() <= pos {
return Err(Error::BufferTooShort {
need: pos + 1,
have: sel.len(),
what: "TTML_subtitling body",
});
}
let text_length = sel[pos] as usize;
pos += 1;
if sel.len() < pos + text_length {
return Err(Error::BufferTooShort {
need: pos + text_length,
have: sel.len(),
what: "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,
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> {
if self.dvb_ttml_profiles.len() > 0x0F {
return Err(Error::ValueOutOfRange {
field: "dvb_ttml_profiles",
reason: "more than 15 profiles (dvb_ttml_profile_count is 4 bits)",
});
}
if self
.font_ids
.as_ref()
.is_some_and(|ids| ids.len() > u8::MAX as usize)
{
return Err(Error::ValueOutOfRange {
field: "font_ids",
reason: "more than 255 font_ids (font_id_count is 8 bits)",
});
}
let len = self.serialized_len();
if buf.len() < len {
return Err(Error::OutputBufferTooSmall {
need: len,
have: buf.len(),
});
}
buf[..ISO_639_LEN].copy_from_slice(&self.iso_639_language_code.0);
buf[ISO_639_LEN] = ((self.subtitle_purpose & 0x3F) << 2) | (self.tts_suitability & 0x03);
buf[ISO_639_LEN + 1] = (if self.font_ids.is_some() { 0x80 } else { 0 })
| (if self.qualifier.is_some() { 0x40 } else { 0 })
| (self.dvb_ttml_profiles.len() as u8 & 0x0F);
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] = ids.len() as u8;
pos += 1;
for &id in ids {
buf[pos] = id & 0x7F;
pos += 1;
}
}
let raw = self.text.raw();
buf[pos] = raw.len() as u8;
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(&[0x10, 0x20, 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 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'e', b'n']; let bytes = wrap(0x20, &sel);
let result = ExtensionDescriptor::parse(&bytes);
assert!(
matches!(result, Err(crate::error::Error::BufferTooShort { .. })),
"expected BufferTooShort, got {result:?}"
);
}
}