use crate::parser::{read_u16, read_u32};
use crate::Error;
pub const TTC_MAGIC: u32 = 0x7474_6366;
const MAX_SUBFONTS: u32 = 1024;
#[derive(Debug, Clone)]
pub struct CollectionHeader {
pub version: (u16, u16),
pub offsets: Vec<u32>,
}
impl CollectionHeader {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < 12 {
return Err(Error::UnexpectedEof);
}
let tag = read_u32(bytes, 0)?;
if tag != TTC_MAGIC {
return Err(Error::BadMagic);
}
let major = read_u16(bytes, 4)?;
let minor = read_u16(bytes, 6)?;
if major != 1 && major != 2 {
return Err(Error::BadHeader);
}
let num_fonts = read_u32(bytes, 8)?;
if num_fonts == 0 || num_fonts > MAX_SUBFONTS {
return Err(Error::BadHeader);
}
let table_end = 12usize
.checked_add(num_fonts as usize * 4)
.ok_or(Error::BadHeader)?;
if bytes.len() < table_end {
return Err(Error::UnexpectedEof);
}
let mut offsets = Vec::with_capacity(num_fonts as usize);
for i in 0..num_fonts as usize {
let off = read_u32(bytes, 12 + i * 4)?;
if (off as usize)
.checked_add(12)
.map(|end| end > bytes.len())
.unwrap_or(true)
{
return Err(Error::BadOffset);
}
offsets.push(off);
}
Ok(Self {
version: (major, minor),
offsets,
})
}
pub fn num_fonts(&self) -> u32 {
self.offsets.len() as u32
}
pub fn font_offset(&self, index: u32) -> Option<u32> {
self.offsets.get(index as usize).copied()
}
}
pub fn is_collection(bytes: &[u8]) -> bool {
if bytes.len() < 4 {
return false;
}
read_u32(bytes, 0).map(|t| t == TTC_MAGIC).unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_ttc_header() -> Vec<u8> {
let mut bytes = vec![0u8; 256];
bytes[0..4].copy_from_slice(&TTC_MAGIC.to_be_bytes());
bytes[4..6].copy_from_slice(&1u16.to_be_bytes());
bytes[6..8].copy_from_slice(&0u16.to_be_bytes());
bytes[8..12].copy_from_slice(&2u32.to_be_bytes());
bytes[12..16].copy_from_slice(&100u32.to_be_bytes());
bytes[16..20].copy_from_slice(&200u32.to_be_bytes());
bytes
}
#[test]
fn parses_minimal_v1_collection() {
let bytes = synth_ttc_header();
let hdr = CollectionHeader::parse(&bytes).expect("parse");
assert_eq!(hdr.version, (1, 0));
assert_eq!(hdr.num_fonts(), 2);
assert_eq!(hdr.font_offset(0), Some(100));
assert_eq!(hdr.font_offset(1), Some(200));
assert_eq!(hdr.font_offset(2), None);
}
#[test]
fn rejects_non_ttc_magic() {
let mut bytes = synth_ttc_header();
bytes[0..4].copy_from_slice(&0x00010000u32.to_be_bytes());
assert!(matches!(
CollectionHeader::parse(&bytes),
Err(Error::BadMagic)
));
}
#[test]
fn rejects_offset_past_eof() {
let mut bytes = synth_ttc_header();
bytes.truncate(150);
assert!(matches!(
CollectionHeader::parse(&bytes),
Err(Error::BadOffset)
));
}
#[test]
fn rejects_zero_subfonts() {
let mut bytes = synth_ttc_header();
bytes[8..12].copy_from_slice(&0u32.to_be_bytes());
assert!(matches!(
CollectionHeader::parse(&bytes),
Err(Error::BadHeader)
));
}
#[test]
fn is_collection_distinguishes() {
assert!(is_collection(&TTC_MAGIC.to_be_bytes()));
assert!(!is_collection(&0x00010000u32.to_be_bytes()));
assert!(!is_collection(&0x4F54544Fu32.to_be_bytes())); assert!(!is_collection(&[]));
}
}