From eabc7e216b49b07f0175f85827aed2ba826dd65e Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Tue, 29 Sep 2026 12:09:48 +0100 Subject: [PATCH] fix(underlay): clamp hostile arc sweep before sizing pieces Bug: arc_cubics turned a raw sweep angle from the DGN file straight into a piece count - ceil(sweep ...) as usize fed with_capacity, and an extreme sweep (huge, or +/-inf) produced a count that overflows usize allocation, causing a capacity overflow panic while attaching the file. Legal DGN arcs top out near 104 radians. Reproduce: 1. Craft a DGN with an arc element whose sweep angle is enormous (1e18+, +/-inf). 2. ATTACH it. 3. Before: "capacity overflow" at raw_vec during model build. 4. After: the arc clamps to +/-4096 quarter-turns (sign preserved, NaN passes through, +/-inf handled) and renders as many cubics as fit. Fix: The clamp happens in the one funnel (arc_cubics) before ceil: sweep is limited to +/-4096 x 90 degrees, far above the legal 104 rad but orders below what overflows the count. The sign of the sweep is preserved so arcs keep their direction, and NaN passes through to the existing empty-arc handling. Tests cover three sweep magnitudes (including +/-inf) building without overflow. --- src/scene/model/underlay_vector/model.rs | 58 ++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/scene/model/underlay_vector/model.rs b/src/scene/model/underlay_vector/model.rs index a4bcdae1a..804f101b9 100644 --- a/src/scene/model/underlay_vector/model.rs +++ b/src/scene/model/underlay_vector/model.rs @@ -152,6 +152,18 @@ pub fn arc_cubics( start: f64, sweep: f64, ) -> Vec<[[f64; 2]; 4]> { + // The sweep can be a raw f64 out of file bytes (DGN `f64_at(e, 112)`): + // 1e300 or inf saturates the `as usize` cast to `usize::MAX` and + // `Vec::with_capacity` panics `capacity overflow`; ~1e17 would ask + // for 2 EB and abort. Legitimate callers top out near 104 rad (DGN + // V7, ±5965°), so the sweep is clamped at 4096 pieces' worth; NaN + // keeps its old single-degenerate-piece behaviour. + const MAX_SWEEP_MAGNITUDE: f64 = 4096.0 * std::f64::consts::FRAC_PI_2; + let sweep = if sweep.is_nan() || sweep.abs() <= MAX_SWEEP_MAGNITUDE { + sweep + } else { + sweep.signum() * MAX_SWEEP_MAGNITUDE + }; let n = ((sweep.abs() / (std::f64::consts::FRAC_PI_2)).ceil() as usize).max(1); let step = sweep / n as f64; let k = 4.0 / 3.0 * (step / 4.0).tan(); @@ -331,3 +343,49 @@ pub fn bspline_points(order: usize, poles: &[[f64; 2]], knots: &[f64], samples: } out } + +#[cfg(test)] +mod arc_cubics_sweep_tests { + use super::arc_cubics; + + /// The sweep is a raw f64 out of DGN element bytes (`f64_at(e, 112)`): + /// 1e300 / inf saturate the `as usize` cast to `usize::MAX` and + /// `Vec::with_capacity` panics `capacity overflow`; ~1e17 would ask + /// for 2 EB and abort. Legitimate callers top out near 104 rad. + #[test] + fn an_enormous_sweep_is_clamped_instead_of_panicking() { + for sweep in [1e300, f64::INFINITY, -1e300, f64::NEG_INFINITY] { + let pieces = arc_cubics([0.0; 2], 1.0, 1.0, 0.0, 0.0, sweep); + assert_eq!(pieces.len(), 4096, "sweep {sweep}"); + assert!(pieces + .iter() + .flatten() + .all(|p| p[0].is_finite() && p[1].is_finite())); + } + } + + /// NaN behaves as before: one degenerate piece, no panic. + #[test] + fn a_nan_sweep_still_yields_a_single_piece() { + assert_eq!(arc_cubics([0.0; 2], 1.0, 1.0, 0.0, 0.0, f64::NAN).len(), 1); + } + + /// Ordinary arcs keep their exact piece count, ceil(|sweep| / 90°). + #[test] + fn ordinary_sweeps_keep_their_piece_counts() { + let expect = |sweep: f64| { + ((sweep.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1) + }; + for sweep in [ + 0.0, + 1.0, + std::f64::consts::FRAC_PI_2, + std::f64::consts::TAU, + 104.1, + -2.5, + ] { + let pieces = arc_cubics([0.0; 2], 3.0, 2.0, 0.3, 0.5, sweep); + assert_eq!(pieces.len(), expect(sweep), "sweep {sweep}"); + } + } +}