vectorcraft/crates/text/src/suitcase.rs

152 lines
5.9 KiB
Rust

//! Suitcase fonts: classic Mac OS font files whose data fork is empty and whose fonts are `sfnt`
//! resources in the resource fork (Microsoft Office installs its fonts this way in
//! `/Library/Fonts/Microsoft`). macOS lists them as installed; a reader of the file's data finds
//! nothing, so the scan reads the resource fork. The fork's layout is Apple's published resource
//! file format (Inside Macintosh: More Macintosh Toolbox, "Resource Manager").
use std::path::Path;
/// Caps on what a (possibly damaged) fork can make the scan read.
const MAX_FORK: u64 = 256 << 20;
const MAX_TYPES: usize = 1 << 12;
const MAX_FONTS: usize = 256;
fn be16(b: &[u8], at: usize) -> Option<usize> {
b.get(at..at.checked_add(2)?).and_then(|s| s.try_into().ok()).map(|s| u16::from_be_bytes(s) as usize)
}
fn be32(b: &[u8], at: usize) -> Option<usize> {
b.get(at..at.checked_add(4)?).and_then(|s| s.try_into().ok()).map(|s| u32::from_be_bytes(s) as usize)
}
/// The font files (`sfnt` resources) of a resource fork, in the order its map lists them. A
/// fork that isn't well formed gives the fonts read before the damage, or none.
pub(crate) fn sfnt_resources(fork: &[u8]) -> Vec<&[u8]> {
let mut fonts = Vec::new();
let _ = collect(fork, &mut fonts);
fonts
}
fn collect<'a>(fork: &'a [u8], fonts: &mut Vec<&'a [u8]>) -> Option<()> {
// The header: where the data and the map start.
let data = be32(fork, 0)?;
let map = fork.get(be32(fork, 4)?..)?;
// The map: its type list starts at the offset kept 24 bytes in, and counts from the map.
let types = be16(map, 24)?;
// Counts are stored less one, so a list of none is 0xFFFF.
let n_types = (be16(map, types)? + 1) & 0xFFFF;
let mut total = 0usize;
for t in 0..n_types.min(MAX_TYPES) {
let at = types + 2 + t * 8;
if map.get(at..at + 4)? != b"sfnt" {
continue;
}
let count = (be16(map, at + 4)? + 1) & 0xFFFF;
// Each reference: its data's offset is the low three bytes of the word 4 bytes in.
let refs = types + be16(map, at + 6)?;
for r in 0..count {
let start = data.checked_add(be32(map, refs + r * 12 + 4)? & 0x00FF_FFFF)?;
let len = be32(fork, start)?;
// References can share data: cap the total too, as every font is copied out.
total = total.checked_add(len)?;
if fonts.len() >= MAX_FONTS || total as u64 > MAX_FORK {
return Some(());
}
fonts.push(fork.get(start + 4..start.checked_add(4)?.checked_add(len)?)?);
}
}
Some(())
}
/// The fonts of the suitcase font at `path`, read from its resource fork; none for a file
/// without one, and off macOS, the only place such a fork is a part of a file.
pub(crate) fn read_fonts(path: &Path) -> Vec<Vec<u8>> {
let Some(fork) = read_fork(path) else { return vec![] };
sfnt_resources(&fork).into_iter().map(<[u8]>::to_vec).collect()
}
/// Whether the file at `path` is a suitcase font: nothing in its data fork and something in its
/// resource fork.
pub(crate) fn is_suitcase(path: &Path) -> bool {
// Off macOS no file has a fork: don't look at every font file twice.
cfg!(target_os = "macos") && std::fs::metadata(path).is_ok_and(|m| m.is_file() && m.len() == 0) && fork_len(path) > 0
}
#[cfg(target_os = "macos")]
fn fork_path(path: &Path) -> std::path::PathBuf {
path.join("..namedfork/rsrc")
}
#[cfg(target_os = "macos")]
pub(crate) fn fork_len(path: &Path) -> u64 {
std::fs::metadata(fork_path(path)).map_or(0, |m| m.len())
}
#[cfg(target_os = "macos")]
fn read_fork(path: &Path) -> Option<Vec<u8>> {
use std::io::Read;
let mut fork = Vec::new();
std::fs::File::open(fork_path(path)).ok()?.take(MAX_FORK).read_to_end(&mut fork).ok()?;
Some(fork)
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn fork_len(_: &Path) -> u64 {
0
}
#[cfg(not(target_os = "macos"))]
fn read_fork(_: &Path) -> Option<Vec<u8>> {
let _ = MAX_FORK;
None
}
/// A resource fork holding `fonts` as `sfnt` resources and one other resource (a `vers`), as
/// a font utility writes them.
#[cfg(test)]
pub(crate) fn fork_of(fonts: &[Vec<u8>]) -> Vec<u8> {
let mut data = Vec::new();
let mut offsets = Vec::new();
for body in fonts.iter().map(Vec::as_slice).chain([&b"1.0"[..]]) {
offsets.push(data.len() as u32);
data.extend_from_slice(&(body.len() as u32).to_be_bytes());
data.extend_from_slice(body);
}
let n = fonts.len();
// The type list: `vers` (1 resource) then `sfnt`, each entry pointing at its references.
let list = 2 + 2 * 8;
let mut types = Vec::new();
types.extend_from_slice(&1u16.to_be_bytes());
types.extend_from_slice(b"vers");
types.extend_from_slice(&0u16.to_be_bytes());
types.extend_from_slice(&(list as u16).to_be_bytes());
types.extend_from_slice(b"sfnt");
types.extend_from_slice(&((n - 1) as u16).to_be_bytes());
types.extend_from_slice(&((list + 12) as u16).to_be_bytes());
let reference = |id: i16, offset: u32| {
let mut r = Vec::new();
r.extend_from_slice(&id.to_be_bytes());
r.extend_from_slice(&0xFFFFu16.to_be_bytes());
r.extend_from_slice(&offset.to_be_bytes());
r.extend_from_slice(&[0; 4]);
r
};
types.extend(reference(1, offsets[n]));
for (i, o) in offsets[..n].iter().enumerate() {
types.extend(reference(5000 + i as i16, *o));
}
// The map: a copy of the header, a handle, attributes, then the offsets of the lists.
let mut map = vec![0; 24];
map.extend_from_slice(&28u16.to_be_bytes());
map.extend_from_slice(&((28 + types.len()) as u16).to_be_bytes());
map.extend(types);
let mut fork = Vec::new();
for v in [256u32, 256 + data.len() as u32, data.len() as u32, map.len() as u32] {
fork.extend_from_slice(&v.to_be_bytes());
}
fork.resize(256, 0);
fork.extend(data);
fork.extend(map);
fork
}