From 0a93c5f02d1c846da65fa60c060f18dcc2f4e5c6 Mon Sep 17 00:00:00 2001 From: David Calvo Date: Tue, 21 Jul 2026 11:26:46 -0400 Subject: [PATCH] Sort table records when parsing so unsorted table directories remain searchable --- src/lib.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index cfa473e1..fa9568f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -280,6 +280,10 @@ fn parse(data: &[u8], index: u32) -> Result> { records.push(r.read::().ok_or(MalformedFont)?); } + // `Face::table` binary-searches by tag, but fonts in the wild violate + // the spec's requirement that the directory be sorted. + records.sort_by_key(|record| record.tag); + Ok(Face { data, records }) } @@ -693,6 +697,36 @@ mod tests { assert_eq!(result, Err(Error::CFFError)); } + #[test] + fn unsorted_table_directory_is_searchable() { + // Two table records deliberately stored in non-alphabetical order + // (`head` before `cmap`): the directory violates the spec's sort + // requirement, but both tables must still be found. + let mut font = Vec::new(); + font.extend(0x00010000u32.to_be_bytes()); + font.extend(2u16.to_be_bytes()); + font.extend(0u16.to_be_bytes()); + font.extend(0u16.to_be_bytes()); + font.extend(0u16.to_be_bytes()); + // Directory (2 records à 16 bytes; data starts at 12 + 32 = 44). + font.extend(*b"head"); + font.extend(0u32.to_be_bytes()); + font.extend(44u32.to_be_bytes()); + font.extend(4u32.to_be_bytes()); + font.extend(*b"cmap"); + font.extend(0u32.to_be_bytes()); + font.extend(48u32.to_be_bytes()); + font.extend(4u32.to_be_bytes()); + // Table payloads. + font.extend([1, 2, 3, 4]); + font.extend([5, 6, 7, 8]); + + let face = parse(&font, 0).unwrap(); + + assert_eq!(face.table(Tag::HEAD), Some([1, 2, 3, 4].as_slice())); + assert_eq!(face.table(Tag::CMAP), Some([5, 6, 7, 8].as_slice())); + } + #[test] fn table_record_with_invalid_bounds_returns_none() { let face = Face {