use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
const SFNT_HEADER_LEN: usize = 12;
const TABLE_RECORD_LEN: usize = 16;
const TTC_HEADER_LEN: usize = 12;
const WANTED: [Tag; 2] = [Tag(*b"name"), Tag(*b"OS/2")];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct Tag([u8; 4]);
#[derive(Debug)]
pub(super) enum ProbeError {
NotSfnt,
Malformed,
Io,
}
impl From<std::io::Error> for ProbeError {
fn from(_: std::io::Error) -> Self {
Self::Io
}
}
#[derive(Debug)]
struct TableDirectory {
sfnt_version: [u8; 4],
entries: Vec<TableRecord>,
}
#[derive(Debug, Clone, Copy)]
struct TableRecord {
tag: Tag,
checksum: u32,
offset: u32,
length: u32,
}
#[derive(Debug)]
pub(super) struct FaceProbe {
sfnt: Vec<u8>,
}
impl FaceProbe {
pub(super) fn read(path: &Path, index: u32) -> Result<Self, ProbeError> {
let mut file = File::open(path)?;
let directory = TableDirectory::read(&mut file, index)?;
let mut kept = Vec::new();
for tag in WANTED {
let Some(record) = directory.find(tag) else {
continue;
};
let body = read_at(&mut file, record.offset, record.length)?;
kept.push((*record, body));
}
Ok(Self {
sfnt: assemble(directory.sfnt_version, &kept),
})
}
pub(super) fn as_font_bytes(&self) -> &[u8] {
&self.sfnt
}
}
impl TableDirectory {
fn read(file: &mut File, index: u32) -> Result<Self, ProbeError> {
let mut header = [0u8; SFNT_HEADER_LEN];
file.rewind()?;
read_exact_or_malformed(file, &mut header)?;
let tag = subarray(&header, 0)?;
let directory_start = if &tag == b"ttcf" {
let count = be_u32(&header, 8)?;
if index >= count {
return Err(ProbeError::Malformed);
}
let mut offsets = vec![0u8; count as usize * 4];
file.seek(SeekFrom::Start(TTC_HEADER_LEN as u64))?;
read_exact_or_malformed(file, &mut offsets)?;
let at = index
.checked_mul(4)
.ok_or(ProbeError::Malformed)?
.try_into()
.map_err(|_| ProbeError::Malformed)?;
be_u32(&offsets, at)?
} else if is_sfnt_version(tag) {
if index != 0 {
return Err(ProbeError::Malformed);
}
0
} else {
return Err(ProbeError::NotSfnt);
};
let offset_table = read_at(file, directory_start, SFNT_HEADER_LEN as u32)?;
let sfnt_version: [u8; 4] = subarray(&offset_table, 0)?;
if !is_sfnt_version(sfnt_version) {
return Err(ProbeError::Malformed);
}
let num_tables = be_u16(&offset_table, 4)?;
let records_at = directory_start
.checked_add(SFNT_HEADER_LEN as u32)
.ok_or(ProbeError::Malformed)?;
let records_len = u32::from(num_tables)
.checked_mul(TABLE_RECORD_LEN as u32)
.ok_or(ProbeError::Malformed)?;
let records = read_at(file, records_at, records_len)?;
let mut entries = Vec::with_capacity(num_tables as usize);
for i in 0..num_tables as usize {
let at = i
.checked_mul(TABLE_RECORD_LEN)
.ok_or(ProbeError::Malformed)?;
entries.push(TableRecord {
tag: Tag(subarray(&records, at)?),
checksum: be_u32(&records, at + 4)?,
offset: be_u32(&records, at + 8)?,
length: be_u32(&records, at + 12)?,
});
}
Ok(Self {
sfnt_version,
entries,
})
}
fn find(&self, tag: Tag) -> Option<&TableRecord> {
self.entries.iter().find(|record| record.tag == tag)
}
}
fn assemble(sfnt_version: [u8; 4], tables: &[(TableRecord, Vec<u8>)]) -> Vec<u8> {
let count = u16::try_from(tables.len()).unwrap_or(u16::MAX);
let entry_selector = if count == 0 { 0 } else { count.ilog2() };
let search_range = if count == 0 {
0
} else {
(1u32 << entry_selector) * 16
};
let range_shift = u32::from(count) * 16 - search_range;
let mut out = Vec::new();
out.extend_from_slice(&sfnt_version);
out.extend_from_slice(&count.to_be_bytes());
out.extend_from_slice(&(search_range as u16).to_be_bytes());
out.extend_from_slice(&(entry_selector as u16).to_be_bytes());
out.extend_from_slice(&(range_shift as u16).to_be_bytes());
let mut body_at = SFNT_HEADER_LEN + tables.len() * TABLE_RECORD_LEN;
for (record, body) in tables {
out.extend_from_slice(&record.tag.0);
out.extend_from_slice(&record.checksum.to_be_bytes());
out.extend_from_slice(&(body_at as u32).to_be_bytes());
out.extend_from_slice(&record.length.to_be_bytes());
body_at += (body.len() + 3) & !3;
}
for (_, body) in tables {
out.extend_from_slice(body);
while out.len() % 4 != 0 {
out.push(0);
}
}
out
}
fn is_sfnt_version(tag: [u8; 4]) -> bool {
matches!(&tag, [0x00, 0x01, 0x00, 0x00] | b"true" | b"OTTO")
}
fn read_at(file: &mut File, offset: u32, length: u32) -> Result<Vec<u8>, ProbeError> {
const MAX_TABLE_LEN: u32 = 64 * 1024 * 1024;
if length > MAX_TABLE_LEN {
return Err(ProbeError::Malformed);
}
let mut buf = vec![0u8; length as usize];
file.seek(SeekFrom::Start(u64::from(offset)))?;
read_exact_or_malformed(file, &mut buf)?;
Ok(buf)
}
fn read_exact_or_malformed(file: &mut File, buf: &mut [u8]) -> Result<(), ProbeError> {
file.read_exact(buf).map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
ProbeError::Malformed
} else {
ProbeError::Io
}
})
}
fn subarray(bytes: &[u8], at: usize) -> Result<[u8; 4], ProbeError> {
bytes
.get(at..at + 4)
.and_then(|s| <[u8; 4]>::try_from(s).ok())
.ok_or(ProbeError::Malformed)
}
fn be_u32(bytes: &[u8], at: usize) -> Result<u32, ProbeError> {
subarray(bytes, at).map(u32::from_be_bytes)
}
fn be_u16(bytes: &[u8], at: usize) -> Result<u16, ProbeError> {
bytes
.get(at..at + 2)
.and_then(|s| <[u8; 2]>::try_from(s).ok())
.map(u16::from_be_bytes)
.ok_or(ProbeError::Malformed)
}