use crate::error::{Error, Result};
use alloc::vec::Vec;
use broadcast_common::len::{need_in, need_out};
use broadcast_common::{Parse, Serialize};
pub const TABLE_ID: u8 = 0x7C;
pub const FONT_INFO_TYPE_STYLE_WEIGHT: u8 = 0x00;
pub const FONT_INFO_TYPE_FILE_URI: u8 = 0x01;
pub const FONT_INFO_TYPE_FONT_SIZE: u8 = 0x02;
const HEADER_LEN: usize = 8;
const SECTION_LENGTH_PREFIX: usize = 3;
use super::CRC_LEN;
const STYLE_MASK: u8 = 0x07;
const WEIGHT_MASK: u8 = 0x0F;
const FORMAT_MASK: u8 = 0x0F;
const RESERVED_ZERO_1: u8 = 0x01;
const RESERVED_ZERO_4_SHIFT: u32 = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum FontInfo<'a> {
StyleWeight {
style: u8,
weight: u8,
#[cfg_attr(feature = "serde", serde(skip))]
reserved_zero_future_use: u8,
},
FileUri {
format: u8,
uri: &'a [u8],
#[cfg_attr(feature = "serde", serde(skip))]
reserved_zero_future_use: u8,
},
FontSize {
size: u16,
info: &'a [u8],
},
LengthDelimited {
font_info_type: u8,
info: &'a [u8],
},
}
impl<'a> FontInfo<'a> {
#[must_use]
pub fn style_weight(style: u8, weight: u8) -> Self {
Self::StyleWeight {
style,
weight,
reserved_zero_future_use: 0,
}
}
#[must_use]
pub fn file_uri(format: u8, uri: &'a [u8]) -> Self {
Self::FileUri {
format,
uri,
reserved_zero_future_use: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
#[non_exhaustive]
pub struct DownloadableFontInfoSection<'a> {
pub font_id_extension: u16,
pub font_id: u8,
pub version_number: u8,
pub current_next_indicator: bool,
pub section_number: u8,
pub last_section_number: u8,
pub font_info: Vec<FontInfo<'a>>,
#[cfg_attr(feature = "serde", serde(skip))]
pub bits: super::SectionBits,
}
impl<'a> DownloadableFontInfoSection<'a> {
#[must_use]
pub fn new(
font_id_extension: u16,
font_id: u8,
version_number: u8,
current_next_indicator: bool,
section_number: u8,
last_section_number: u8,
font_info: Vec<FontInfo<'a>>,
) -> Self {
Self {
font_id_extension,
font_id,
version_number,
current_next_indicator,
section_number,
last_section_number,
font_info,
bits: super::SectionBits::dvb(),
}
}
}
impl<'a> Parse<'a> for DownloadableFontInfoSection<'a> {
type Error = crate::error::Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let min_len = HEADER_LEN + CRC_LEN;
need_in(bytes, min_len, "DownloadableFontInfoSection")?;
if bytes[0] != TABLE_ID {
return Err(Error::UnexpectedTableId {
table_id: bytes[0],
what: "DownloadableFontInfoSection",
expected: &[TABLE_ID],
});
}
let section_length = super::section_length_of(bytes);
let total = super::check_section_length(
bytes.len(),
SECTION_LENGTH_PREFIX,
section_length,
HEADER_LEN + CRC_LEN,
)?;
let id_word = crate::wire::be_u16(bytes, 3, "downloadable_font_info")?;
let font_id_extension = id_word >> 7;
let font_id = (id_word & 0x7F) as u8;
let version_number = super::version_number_of(bytes[5]);
let current_next_indicator = super::current_next_of(bytes[5]);
let section_number = bytes[6];
let last_section_number = bytes[7];
let loop_end = total - CRC_LEN;
let mut font_info = Vec::new();
let mut pos = HEADER_LEN;
while pos < loop_end {
let font_info_type = bytes[pos];
pos += 1;
match font_info_type {
FONT_INFO_TYPE_STYLE_WEIGHT => {
if pos + 1 > loop_end {
return Err(Error::SectionLengthOverflow {
declared: 1,
available: loop_end - pos,
});
}
let b = bytes[pos];
pos += 1;
font_info.push(FontInfo::StyleWeight {
style: b >> 5,
weight: (b >> 1) & WEIGHT_MASK,
reserved_zero_future_use: b & RESERVED_ZERO_1,
});
}
FONT_INFO_TYPE_FILE_URI => {
if pos + 2 > loop_end {
return Err(Error::SectionLengthOverflow {
declared: 2,
available: loop_end - pos,
});
}
let format = bytes[pos] & FORMAT_MASK;
let reserved_zero_future_use = bytes[pos] >> RESERVED_ZERO_4_SHIFT;
let uri_length = bytes[pos + 1] as usize;
let uri_start = pos + 2;
let uri_end = uri_start + uri_length;
if uri_end > loop_end {
return Err(Error::SectionLengthOverflow {
declared: uri_length,
available: loop_end - uri_start,
});
}
font_info.push(FontInfo::FileUri {
format,
uri: &bytes[uri_start..uri_end],
reserved_zero_future_use,
});
pos = uri_end;
}
FONT_INFO_TYPE_FONT_SIZE => {
if pos + 3 > loop_end {
return Err(Error::SectionLengthOverflow {
declared: 3,
available: loop_end - pos,
});
}
let (b2, _) = bytes
.get(pos..)
.and_then(|s| s.split_first_chunk::<2>())
.ok_or(Error::SectionLengthOverflow {
declared: 3,
available: loop_end - pos,
})?;
let size = u16::from_be_bytes(*b2);
let info_length = bytes[pos + 2] as usize;
let info_start = pos + 3;
let info_end = info_start + info_length;
if info_end > loop_end {
return Err(Error::SectionLengthOverflow {
declared: info_length,
available: loop_end - info_start,
});
}
font_info.push(FontInfo::FontSize {
size,
info: &bytes[info_start..info_end],
});
pos = info_end;
}
_ => {
if pos + 1 > loop_end {
return Err(Error::SectionLengthOverflow {
declared: 1,
available: loop_end - pos,
});
}
let info_length = bytes[pos] as usize;
let info_start = pos + 1;
let info_end = info_start + info_length;
if info_end > loop_end {
return Err(Error::SectionLengthOverflow {
declared: info_length,
available: loop_end - info_start,
});
}
font_info.push(FontInfo::LengthDelimited {
font_info_type,
info: &bytes[info_start..info_end],
});
pos = info_end;
}
}
}
Ok(DownloadableFontInfoSection {
font_id_extension,
font_id,
version_number,
current_next_indicator,
section_number,
last_section_number,
font_info,
bits: super::SectionBits::parse(bytes[1], Some(bytes[5])),
})
}
}
impl Serialize for DownloadableFontInfoSection<'_> {
type Error = crate::error::Error;
fn serialized_len(&self) -> usize {
let loop_bytes: usize = self
.font_info
.iter()
.map(|f| match f {
FontInfo::StyleWeight { .. } => 2, FontInfo::FileUri { uri, .. } => 1 + 2 + uri.len(), FontInfo::FontSize { info, .. } => 1 + 2 + 1 + info.len(), FontInfo::LengthDelimited { info, .. } => 1 + 1 + info.len(), })
.sum();
HEADER_LEN + loop_bytes + CRC_LEN
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let len = self.serialized_len();
need_out(buf, len)?;
buf[0] = TABLE_ID;
buf[1] = self.bits.b1()?;
super::write_section_length(buf, len - SECTION_LENGTH_PREFIX)?;
let id_word =
(broadcast_common::len::fit_mask(self.font_id_extension, 0x01FF, "font_id_extension")?
<< 7)
| broadcast_common::len::fit_mask(self.font_id as u16, 0x7F, "font_id")?;
buf[3..5].copy_from_slice(&id_word.to_be_bytes());
buf[5] = self
.bits
.b5(self.version_number, self.current_next_indicator)?;
buf[6] = self.section_number;
buf[7] = self.last_section_number;
let guard_u8 = |len: usize| -> Result<()> {
if len > u8::MAX as usize {
return Err(Error::SectionLengthOverflow {
declared: len,
available: u8::MAX as usize,
});
}
Ok(())
};
let mut pos = HEADER_LEN;
for f in &self.font_info {
match f {
FontInfo::StyleWeight {
style,
weight,
reserved_zero_future_use,
} => {
buf[pos] = FONT_INFO_TYPE_STYLE_WEIGHT;
buf[pos + 1] = (broadcast_common::len::fit_mask(*style, STYLE_MASK, "style")?
<< 5)
| (broadcast_common::len::fit_mask(*weight, WEIGHT_MASK, "weight")? << 1)
| broadcast_common::len::fit_mask(
*reserved_zero_future_use,
RESERVED_ZERO_1,
"reserved_zero_future_use",
)?;
pos += 2;
}
FontInfo::FileUri {
format,
uri,
reserved_zero_future_use,
} => {
guard_u8(uri.len())?;
buf[pos] = FONT_INFO_TYPE_FILE_URI;
buf[pos + 1] = (broadcast_common::len::fit_mask(
*reserved_zero_future_use,
FORMAT_MASK,
"reserved_zero_future_use",
)? << RESERVED_ZERO_4_SHIFT)
| broadcast_common::len::fit_mask(*format, FORMAT_MASK, "format")?;
buf[pos + 2] = broadcast_common::len::fit_u8(uri.len(), "uri")?;
let s = pos + 3;
buf[s..s + uri.len()].copy_from_slice(uri);
pos = s + uri.len();
}
FontInfo::FontSize { size, info } => {
guard_u8(info.len())?;
buf[pos] = FONT_INFO_TYPE_FONT_SIZE;
buf[pos + 1..pos + 3].copy_from_slice(&size.to_be_bytes());
buf[pos + 3] = broadcast_common::len::fit_u8(info.len(), "info")?;
let s = pos + 4;
buf[s..s + info.len()].copy_from_slice(info);
pos = s + info.len();
}
FontInfo::LengthDelimited {
font_info_type,
info,
} => {
if matches!(
*font_info_type,
FONT_INFO_TYPE_STYLE_WEIGHT
| FONT_INFO_TYPE_FILE_URI
| FONT_INFO_TYPE_FONT_SIZE
) {
return Err(Error::ValueOutOfRange {
field: "font_info_type",
reason: "0x00..=0x02 are fixed-layout types, not LengthDelimited",
});
}
guard_u8(info.len())?;
buf[pos] = *font_info_type;
buf[pos + 1] = broadcast_common::len::fit_u8(info.len(), "info")?;
let s = pos + 2;
buf[s..s + info.len()].copy_from_slice(info);
pos = s + info.len();
}
}
}
super::write_crc(buf, pos);
Ok(len)
}
}
impl<'a> crate::traits::TableDef<'a> for DownloadableFontInfoSection<'a> {
const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
const NAME: &'static str = "DOWNLOADABLE_FONT_INFO";
}
#[cfg(test)]
mod tests {
use super::*;
fn build_section(font_id: u8, version: u8, loop_body: &[u8]) -> Vec<u8> {
let section_length =
(HEADER_LEN - SECTION_LENGTH_PREFIX + loop_body.len() + CRC_LEN) as u16;
let id_word = (font_id as u16) & 0x7F;
let mut v = vec![
TABLE_ID,
super::super::SECTION_B1_FLAGS_DVB | ((section_length >> 8) as u8 & 0x0F),
(section_length & 0xFF) as u8,
(id_word >> 8) as u8,
(id_word & 0xFF) as u8,
0xC0 | (version << 1) | 0x01,
0x00,
0x00,
];
v.extend_from_slice(loop_body);
v.extend_from_slice(&[0, 0, 0, 0]);
v
}
fn mixed_loop() -> Vec<u8> {
let uri = b"https://f.example/Droid.otf";
let family = b"Droid Sans";
let mut b = vec![
FONT_INFO_TYPE_STYLE_WEIGHT, (2u8 << 5) | (2u8 << 1), FONT_INFO_TYPE_FILE_URI, 0x01, uri.len() as u8, ];
b.extend_from_slice(uri);
b.push(FONT_INFO_TYPE_FONT_SIZE);
b.extend_from_slice(&24u16.to_be_bytes());
b.push(2);
b.extend_from_slice(b"px");
b.push(0x03);
b.push(family.len() as u8);
b.extend_from_slice(family);
b
}
#[test]
fn parse_header_fields() {
let bytes = build_section(0x42, 9, &[]);
let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
assert_eq!(sec.font_id, 0x42);
assert_eq!(sec.font_id_extension, 0);
assert_eq!(sec.version_number, 9);
assert!(sec.current_next_indicator);
assert!(sec.font_info.is_empty());
}
#[test]
fn parse_all_variants() {
let bytes = build_section(1, 0, &mixed_loop());
let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
assert_eq!(sec.font_info.len(), 4);
assert_eq!(sec.font_info[0], FontInfo::style_weight(2, 2));
match &sec.font_info[1] {
FontInfo::FileUri { format, uri, .. } => {
assert_eq!(*format, 1);
assert_eq!(*uri, b"https://f.example/Droid.otf");
}
other => panic!("expected FileUri, got {other:?}"),
}
match &sec.font_info[2] {
FontInfo::FontSize { size, info } => {
assert_eq!(*size, 24);
assert_eq!(*info, b"px");
}
other => panic!("expected FontSize, got {other:?}"),
}
match &sec.font_info[3] {
FontInfo::LengthDelimited {
font_info_type,
info,
} => {
assert_eq!(*font_info_type, 0x03);
assert_eq!(*info, b"Droid Sans");
}
other => panic!("expected LengthDelimited, got {other:?}"),
}
}
#[test]
fn reserved_type_round_trips_as_length_delimited() {
let mut body = vec![0x77u8, 0x03];
body.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
let bytes = build_section(1, 0, &body);
let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
assert_eq!(
sec.font_info[0],
FontInfo::LengthDelimited {
font_info_type: 0x77,
info: &[0xAA, 0xBB, 0xCC]
}
);
}
#[test]
fn parse_rejects_wrong_tag() {
let mut bytes = build_section(1, 0, &mixed_loop());
bytes[0] = 0x4C; assert!(matches!(
DownloadableFontInfoSection::parse(&bytes).unwrap_err(),
Error::UnexpectedTableId { table_id: 0x4C, .. }
));
}
#[test]
fn rejects_short_buffer() {
assert!(matches!(
DownloadableFontInfoSection::parse(&[0x7C, 0xB0]).unwrap_err(),
Error::BufferTooShort(broadcast_common::error::BufferTooShort {
what: "DownloadableFontInfoSection",
..
})
));
}
#[test]
fn uri_length_overflow_rejected() {
let body = vec![FONT_INFO_TYPE_FILE_URI, 0x01, 0x20];
let bytes = build_section(1, 0, &body);
assert!(matches!(
DownloadableFontInfoSection::parse(&bytes).unwrap_err(),
Error::SectionLengthOverflow { .. }
));
}
#[test]
fn round_trip_all_variants() {
let bytes = build_section(0x33, 4, &mixed_loop());
let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
let mut buf = vec![0u8; sec.serialized_len()];
sec.serialize_into(&mut buf).unwrap();
let re = DownloadableFontInfoSection::parse(&buf).unwrap();
assert_eq!(sec, re);
}
#[test]
fn table_trait_constants() {
assert_eq!(TABLE_ID, 0x7C);
}
#[test]
#[cfg(feature = "serde")]
fn serde_json_round_trip() {
let bytes = build_section(1, 0, &mixed_loop());
let sec = DownloadableFontInfoSection::parse(&bytes).unwrap();
let j = serde_json::to_string(&sec).unwrap();
let reparsed = DownloadableFontInfoSection::parse(&bytes).unwrap();
assert_eq!(serde_json::to_string(&reparsed).unwrap(), j);
assert!(j.contains("\"font_id\":1"));
}
#[test]
fn parse_rejects_zero_section_length() {
let mut buf = vec![0u8; 64];
buf[0] = TABLE_ID;
buf[1] = 0xF0;
buf[2] = 0x00;
for b in &mut buf[3..] {
*b = 0xFF;
}
assert!(matches!(
DownloadableFontInfoSection::parse(&buf).unwrap_err(),
Error::SectionLengthOverflow { .. }
));
}
#[test]
fn serialize_rejects_length_delimited_with_fixed_layout_type_code() {
for bad_type in [
FONT_INFO_TYPE_STYLE_WEIGHT,
FONT_INFO_TYPE_FILE_URI,
FONT_INFO_TYPE_FONT_SIZE,
] {
let section = DownloadableFontInfoSection::new(
0,
1,
0,
true,
0,
0,
vec![FontInfo::LengthDelimited {
font_info_type: bad_type,
info: b"x",
}],
);
let mut buf = vec![0u8; section.serialized_len()];
assert!(
matches!(
section.serialize_into(&mut buf).unwrap_err(),
Error::ValueOutOfRange { .. }
),
"font_info_type {bad_type:#04x} must be rejected"
);
}
}
#[test]
fn serialize_rejects_font_info_loop_exceeding_section_length() {
let section = DownloadableFontInfoSection::new(
0,
1,
0,
true,
0,
0,
(0..2044).map(|_| FontInfo::style_weight(1, 1)).collect(),
);
let mut buf = vec![0u8; section.serialized_len()];
assert!(matches!(
section.serialize_into(&mut buf).unwrap_err(),
Error::FieldOverflow(_)
));
}
#[test]
fn serialize_accepts_font_info_loop_at_section_length_boundary() {
let section = DownloadableFontInfoSection::new(
0,
1,
0,
true,
0,
0,
(0..2043).map(|_| FontInfo::style_weight(1, 1)).collect(),
);
let mut buf = vec![0u8; section.serialized_len()];
section.serialize_into(&mut buf).expect("boundary fits");
let reparsed = DownloadableFontInfoSection::parse(&buf).expect("reparse");
assert_eq!(section, reparsed);
}
}