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.
This commit is contained in:
Karim Jerbi committed 2026-09-30 19:15:49 +01:00
1 parent 86642bb639
commit eabc7e216b
1 file changed
+58
+58
View File
@@ -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}");
}
}
}