From 9d986e4ea70f185324439c72acb65237a79aff25 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 10 Oct 2026 08:09:37 +0000 Subject: [PATCH] Warichu: hostile line spacing and break minimums can't break layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A document's warichu line spacing was used unclamped, so 1e308 (IDML or type.char) put an infinite coordinate into the layout and the PDF; it is clamped to ±16 em and a non-finite value counts as 0. The break minimums' sum could overflow on 32-bit targets (wasm) and now saturates. --- crates/compose/src/tests.rs | 21 +++++++++++++++++++++ crates/compose/src/warichu.rs | 19 +++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index dea9008d5..4301d9ff5 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -1095,6 +1095,27 @@ fn warichu_stacks_the_run_inside_the_line_and_closes_up() { assert!((l.end_x - (z.x + z.adv)).abs() < 1e-3, "the line-end caret follows the close-up"); } +#[test] +fn warichu_line_spacing_from_a_document_stays_finite() { + for spacing in [1e308, -1e308, f64::INFINITY, f64::NAN] { + let mut d = Document::new(&designcraft_doc::build::NewDocument::default()); + let lid = d.default_layer(); + let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 500.0, 200.0), lid, "ABCDEFGHZ", ParaFormat::default()).unwrap(); + d.story_mut(sid).unwrap().format_chars(0..8, |f| { + f.over.warichu = Some(true); + f.over.warichu_lines = Some(16); + f.over.warichu_line_spacing = Some(spacing); + }); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + let l = &cs.frames[0].lines[0]; + assert!( + l.glyphs.iter().all(|g| g.x.is_finite() && g.y.is_finite() && g.y.abs() < 1e4), + "{spacing}: {:?}", + l.glyphs.iter().map(|g| g.y).collect::>() + ); + } +} + #[test] fn warichu_break_minimum_keeps_a_short_run_on_one_line() { let mut d = Document::new(&designcraft_doc::build::NewDocument::default()); diff --git a/crates/compose/src/warichu.rs b/crates/compose/src/warichu.rs index aaabb2e5f..133e392ee 100644 --- a/crates/compose/src/warichu.rs +++ b/crates/compose/src/warichu.rs @@ -84,7 +84,8 @@ fn line_count(len: usize, want: usize, before: usize, after: usize) -> usize { let before = before.max(1); let after = after.max(1); while n >= 2 { - if before + after + n - 2 <= len { + // The minimums come from documents and commands: on 32-bit targets their sum can overflow. + if before.saturating_add(after).saturating_add(n - 2) <= len { return n; } n -= 1; @@ -220,7 +221,9 @@ fn layout(styles: &[RunStyle], line: &mut [PlacedGlyph], members: &[usize]) -> f let small = st.size * scale; // 0 is the default (one small em). Any other value, including a negative one, is added to // that em and then clamped so the rows cannot swap order. - let gap = if st.warichu_line_spacing == 0.0 { small } else { (small + st.warichu_line_spacing).max(0.0) }; + // Bounded, so a hostile value can't move a row to an infinite or huge coordinate. + let spacing = if st.warichu_line_spacing.is_finite() { st.warichu_line_spacing.clamp(-16.0 * st.size, 16.0 * st.size) } else { 0.0 }; + let gap = if spacing == 0.0 { small } else { (small + spacing).max(0.0) }; let mut off = 0_usize; for (li, &c) in counts.iter().enumerate() { let w = widths[li]; @@ -264,3 +267,15 @@ fn layout(styles: &[RunStyle], line: &mut [PlacedGlyph], members: &[usize]) -> f } delta } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn huge_break_minimums_keep_one_line() { + assert_eq!(line_count(10, 2, usize::MAX, 1), 1); + assert_eq!(line_count(10, 3, 1, usize::MAX), 1); + assert_eq!(line_count(10, 2, 3, 3), 2); + } +}