M2: a loca table with padding after short offsets stays short (hayro-interpret patch 79)

Room for numGlyphs + 1 long offsets means long, room only for short ones
means short (padding after them allowed), and a table too short for
either keeps the field as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-11-05 23:25:23 +08:00
1 parent 02f0eaa8f0
commit abc348894c
2 files changed
+22 -11

No files matched your search

@@ -18,8 +18,9 @@ fn be32(out: &mut Vec<u8>, n: u32) {
out.extend_from_slice(&n.to_be_bytes());
}
/// The font, its `loca` offsets long when `long`, its `head.indexToLocFormat` set to `format`.
fn square_font(long: bool, format: u16) -> Vec<u8> {
/// The font, its `loca` offsets long when `long` and followed by `padding` zero bytes, its
/// `head.indexToLocFormat` set to `format`.
fn square_font(long: bool, padding: usize, format: u16) -> Vec<u8> {
// Glyph 1: one contour, the square 0..1000 on both axes (glyph 0 is empty).
let mut glyf = Vec::new();
for n in [1u16, 0, 0, 1000, 1000] {
@@ -38,6 +39,7 @@ fn square_font(long: bool, format: u16) -> Vec<u8> {
for offset in [0u32, 0, glyf.len() as u32] {
if long { be32(&mut loca, offset) } else { be16(&mut loca, (offset / 2) as u16) }
}
loca.resize(loca.len() + padding, 0);
let mut head = vec![0; 54];
head[0..4].copy_from_slice(&[0, 1, 0, 0]);
@@ -130,14 +132,16 @@ fn page_with_font(font: &[u8]) -> Vec<u8> {
#[test]
fn a_truetype_font_with_an_out_of_range_loca_format_draws_its_glyphs() {
// The square covers (5, 5)–(35, 35) in page space: row 20 from the top is inside it.
let square_drawn = |long: bool, format: u16| {
let page = render_first_page(page_with_font(&square_font(long, format)));
let square_drawn = |long: bool, padding: usize, format: u16| {
let page = render_first_page(page_with_font(&square_font(long, padding, format)));
let at = |x: u32, y: u32| page.rgba.get(((y * page.width + x) * 4) as usize).copied().unwrap_or(255);
assert_eq!(at(2, 2), 255, "nothing is drawn outside the square");
at(20, 20) < 128
};
assert!(square_drawn(true, 1), "long offsets, marked long");
assert!(square_drawn(false, 0), "short offsets, marked short");
assert!(square_drawn(true, 0x0100), "long offsets marked 256: the table's length says long");
assert!(square_drawn(false, 0x0100), "short offsets marked 256: the table's length says short");
assert!(square_drawn(true, 0, 1), "long offsets, marked long");
assert!(square_drawn(false, 0, 0), "short offsets, marked short");
assert!(square_drawn(true, 0, 0x0100), "long offsets marked 256: the table's length says long");
assert!(square_drawn(false, 0, 0x0100), "short offsets marked 256: the table's length says short");
// Three short offsets are 6 bytes and three long ones 12: 8 bytes is short ones and padding.
assert!(square_drawn(false, 2, 0x0100), "short offsets and padding marked 256: still short");
}
+10 -3
View File
@@ -200,8 +200,9 @@ impl Debug for OpenTypeFontBlob {
/// it needs no change. The glyph outline reader takes only 1 as long, so a font whose field reads
/// 256 (the value's bytes swapped) had its long `loca` read as short offsets: every glyph came
/// out wrong, and a line of text set in it drew boxes (pdf.js `issue2537r.pdf`). The `loca`
/// table's length decides (`numGlyphs + 1` offsets of 4 bytes or of 2); a length that fits
/// neither keeps the rule that any value other than 0 means long offsets.
/// table's length decides: room for `numGlyphs + 1` offsets of 4 bytes is long, room for only
/// that many of 2 bytes short (a table may carry padding after its offsets), and a table too
/// short for either is left as it is.
fn with_loca_format_fixed(data: &[u8], index: u32) -> Option<Vec<u8>> {
use skrifa::raw::types::Tag;
const HEAD: Tag = Tag::new(b"head");
@@ -215,7 +216,13 @@ fn with_loca_format_fixed(data: &[u8], index: u32) -> Option<Vec<u8>> {
}
let offsets = usize::from(f.maxp().ok()?.num_glyphs()).checked_add(1)?;
let loca_len = f.table_data(LOCA)?.len();
let long = loca_len != offsets.checked_mul(2)? || loca_len == offsets.checked_mul(4)?;
let long = if loca_len >= offsets.checked_mul(4)? {
true
} else if loca_len >= offsets.checked_mul(2)? {
false
} else {
return None;
};
let head_at = f.table_directory.table_records().iter().find(|r| r.tag() == HEAD)?.offset() as usize;
let at = head_at.checked_add(FORMAT_AT)?;
let mut fixed = data.to_vec();