use std::fmt;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use thiserror::Error;
mod cmap;
mod glyph;
mod read;
mod sfnt;
#[cfg(test)]
mod tests;
const TRUE_TYPE_SFNT_VERSION: u32 = 0x0001_0000;
const HEAD_MAGIC: u32 = 0x5f0f_3cf5;
const MAX_COMPOSITE_DEPTH: usize = 32;
const MAX_COMPONENTS_PER_GLYPH: usize = 4_096;
const MAX_EXPANDED_GLYPH_COMPLEXITY: usize = 1_000_000;
const MAX_VALIDATED_FONT_DATA_LEN: usize = 256 * 1024 * 1024;
const MAX_INITIAL_FILE_CAPACITY: usize = 8 * 1024 * 1024;
const CMAP: [u8; 4] = *b"cmap";
const GLYF: [u8; 4] = *b"glyf";
const HEAD: [u8; 4] = *b"head";
const HHEA: [u8; 4] = *b"hhea";
const HMTX: [u8; 4] = *b"hmtx";
const LOCA: [u8; 4] = *b"loca";
const MAXP: [u8; 4] = *b"maxp";
#[derive(Clone)]
pub struct StbTrueTypeFontData {
bytes: Arc<[u8]>,
}
impl StbTrueTypeFontData {
pub const MAX_BYTES: usize = MAX_VALIDATED_FONT_DATA_LEN;
pub fn from_bytes(
bytes: impl AsRef<[u8]> + Into<Arc<[u8]>>,
) -> Result<Self, StbTrueTypeFontError> {
validate_font_data_length(bytes.as_ref().len())?;
Self::from_length_checked_bytes(bytes.into())
}
pub fn from_slice(bytes: &[u8]) -> Result<Self, StbTrueTypeFontError> {
validate_font_data_length(bytes.len())?;
Self::from_length_checked_bytes(Arc::<[u8]>::from(bytes))
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, StbTrueTypeFontLoadError> {
let path = path.as_ref();
let file = File::open(path).map_err(|source| StbTrueTypeFontLoadError::Io {
path: path.to_owned(),
source,
})?;
let metadata = file
.metadata()
.map_err(|source| StbTrueTypeFontLoadError::Io {
path: path.to_owned(),
source,
})?;
let declared_length = usize::try_from(metadata.len()).unwrap_or(usize::MAX);
validate_font_data_length(declared_length).map_err(StbTrueTypeFontLoadError::Validation)?;
let mut bytes = Vec::with_capacity(declared_length.min(MAX_INITIAL_FILE_CAPACITY));
file.take((MAX_VALIDATED_FONT_DATA_LEN as u64) + 1)
.read_to_end(&mut bytes)
.map_err(|source| StbTrueTypeFontLoadError::Io {
path: path.to_owned(),
source,
})?;
Self::from_bytes(bytes).map_err(StbTrueTypeFontLoadError::Validation)
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
fn from_length_checked_bytes(bytes: Arc<[u8]>) -> Result<Self, StbTrueTypeFontError> {
validate_font(&bytes)?;
Ok(Self { bytes })
}
}
impl AsRef<[u8]> for StbTrueTypeFontData {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl fmt::Debug for StbTrueTypeFontData {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("StbTrueTypeFontData")
.field("len", &self.bytes.len())
.finish_non_exhaustive()
}
}
impl TryFrom<Vec<u8>> for StbTrueTypeFontData {
type Error = StbTrueTypeFontError;
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
Self::from_bytes(bytes)
}
}
impl TryFrom<Arc<[u8]>> for StbTrueTypeFontData {
type Error = StbTrueTypeFontError;
fn try_from(bytes: Arc<[u8]>) -> Result<Self, Self::Error> {
Self::from_bytes(bytes)
}
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum StbTrueTypeFontError {
#[error("font data length {length} exceeds the validated stb_truetype limit {limit}")]
DataTooLarge { length: usize, limit: usize },
#[error("unsupported font container signature {signature:#010x}; expected 0x00010000")]
UnsupportedContainer { signature: u32 },
#[error(
"truncated {context} at byte {offset}: need {needed} bytes but only {available} remain"
)]
Truncated {
context: &'static str,
offset: usize,
needed: usize,
available: usize,
},
#[error("invalid sfnt table directory at byte {offset}: {reason}")]
InvalidDirectory { offset: usize, reason: &'static str },
#[error("duplicate sfnt table tag {tag:?}")]
DuplicateTable { tag: [u8; 4] },
#[error("missing required sfnt table {tag:?}")]
MissingTable { tag: [u8; 4] },
#[error("sfnt table {tag:?} range {offset}..{end} is outside font data of length {data_len}")]
TableOutOfBounds {
tag: [u8; 4],
offset: usize,
end: usize,
data_len: usize,
},
#[error("invalid sfnt table {tag:?} at byte {offset}: {reason}")]
InvalidTable {
tag: [u8; 4],
offset: usize,
reason: &'static str,
},
#[error(
"stb_truetype selected unsupported cmap format {format} at byte {offset}; only formats 4 and 12 are accepted"
)]
UnsupportedCmapFormat { format: u16, offset: usize },
#[error("invalid stb-selected cmap at byte {offset}: {reason}")]
InvalidCmap { offset: usize, reason: &'static str },
#[error("invalid glyf record {glyph_id} at byte {offset}: {reason}")]
InvalidGlyph {
glyph_id: u16,
offset: usize,
reason: &'static str,
},
#[error(
"composite glyph {glyph_id} refers to glyph {referenced_glyph}, but the font declares only {glyph_count} glyphs"
)]
InvalidGlyphReference {
glyph_id: u16,
referenced_glyph: u16,
glyph_count: u16,
},
#[error("composite glyph cycle from glyph {glyph_id} to glyph {referenced_glyph}")]
CompositeCycle {
glyph_id: u16,
referenced_glyph: u16,
},
#[error("composite glyph {glyph_id} has depth {depth}, exceeding the validated limit {limit}")]
CompositeDepth {
glyph_id: u16,
depth: usize,
limit: usize,
},
#[error(
"composite glyph {glyph_id} expands to complexity {complexity}, exceeding the validated limit {limit}"
)]
CompositeComplexity {
glyph_id: u16,
complexity: usize,
limit: usize,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StbTrueTypeFontLoadError {
#[error("failed to read TrueType font file {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error(transparent)]
Validation(#[from] StbTrueTypeFontError),
}
#[derive(Clone, Copy, Debug)]
struct Table {
tag: [u8; 4],
offset: usize,
length: usize,
}
impl Table {
fn end(self) -> usize {
self.offset + self.length
}
fn bytes(self, data: &[u8]) -> &[u8] {
&data[self.offset..self.end()]
}
fn invalid(self, relative_offset: usize, reason: &'static str) -> StbTrueTypeFontError {
StbTrueTypeFontError::InvalidTable {
tag: self.tag,
offset: self.offset.saturating_add(relative_offset),
reason,
}
}
}
#[derive(Clone, Copy, Debug)]
struct MaxpLimits {
glyph_count: u16,
max_points: usize,
max_contours: usize,
max_composite_points: usize,
max_composite_contours: usize,
max_instruction_bytes: usize,
max_component_elements: usize,
max_component_depth: usize,
}
fn validate_font(data: &[u8]) -> Result<(), StbTrueTypeFontError> {
validate_font_data_length(data.len())?;
let signature = read::read_u32(data, 0, "sfnt header")?;
if signature != TRUE_TYPE_SFNT_VERSION {
return Err(StbTrueTypeFontError::UnsupportedContainer { signature });
}
let tables = sfnt::parse_table_directory(data)?;
let cmap = sfnt::required_table(&tables, CMAP)?;
let glyf = sfnt::required_table(&tables, GLYF)?;
let head = sfnt::required_table(&tables, HEAD)?;
let hhea = sfnt::required_table(&tables, HHEA)?;
let hmtx = sfnt::required_table(&tables, HMTX)?;
let loca = sfnt::required_table(&tables, LOCA)?;
let maxp = sfnt::required_table(&tables, MAXP)?;
let index_to_loc_format = sfnt::validate_head(data, head)?;
let maxp_limits = sfnt::validate_maxp(data, maxp)?;
sfnt::validate_horizontal_metrics(data, hhea, hmtx, maxp_limits.glyph_count)?;
cmap::validate_cmap(data, cmap, maxp_limits.glyph_count)?;
let locations = sfnt::validate_loca(
data,
loca,
glyf.length,
maxp_limits.glyph_count,
index_to_loc_format,
)?;
glyph::validate_glyphs(data, glyf, &locations, maxp_limits)?;
Ok(())
}
fn validate_font_data_length(length: usize) -> Result<(), StbTrueTypeFontError> {
if length > MAX_VALIDATED_FONT_DATA_LEN {
return Err(StbTrueTypeFontError::DataTooLarge {
length,
limit: MAX_VALIDATED_FONT_DATA_LEN,
});
}
Ok(())
}