fix(io): purge degenerate arcs/splines that OOM on load

Large survey drawings (105k entities, multi-million unit extents) could
exhaust all RAM + swap on one core during load and get OOM-killed before
display. Two parser-garbage entities slipped past the corrupt-entity
purge and blew up truck's parameter_division tessellation:

- Near-zero-sweep arcs: the zero-sweep guard only caught sweep < 1e-9,
  but a tiny non-zero sweep (e.g. 1.6e-6 rad) still places start/mid/end
  within truck's coincidence tolerance and recurses unboundedly. Gate on
  arc length (radius x sweep) instead so large-radius small-sweep arcs
  (still visible curves) survive while sub-precision arcs drop.
- Desync splines with 100k control points and an invalid knot vector had
  no guard at all. Add a Spline arm rejecting the 100k desync signature,
  non-finite control points, degree < 1, and bad knot vectors.

Also cap complex-linetype dash expansion (complex_lt::apply_along) at
1e6 pattern repeats, falling back to a solid stroke — a latent OOM of
the same class on map-scale entities carrying a finely-scaled pattern.

Adds regression tests for the arc-length and spline guards.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-10 08:39:50 +03:00
1 parent e6ab0e70ac
commit 9cdb5ea5fd
2 files changed
+106

No files matched your search

+20
View File
@@ -47,6 +47,26 @@ pub fn apply_along(
return vec![];
}
// Guard against pattern blow-up. The per-segment walk below emits at least
// one stroke vertex per pattern element, so the total vertex count scales
// with `path_len / pattern_len`. On a large-extent drawing (e.g. a city/
// street map) carrying a finely-scaled complex linetype, that ratio can
// reach billions — the strokes Vec grows until the process is OOM-killed,
// single-threaded, before the drawing ever finishes loading. Past a sane
// repeat count the dashes are sub-pixel anyway, so render the base wire
// solid: returning empty here makes the caller fall back to the solid base.
const MAX_PATTERN_REPEATS: f32 = 1_000_000.0;
let path_len: f32 = path_pts
.windows(2)
.map(|w| {
let d = [w[1][0] - w[0][0], w[1][1] - w[0][1], w[1][2] - w[0][2]];
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
})
.sum();
if path_len / pattern_len > MAX_PATTERN_REPEATS {
return vec![];
}
let mut strokes: Vec<Vec<[f32; 3]>> = Vec::new();
let mut cur_stroke: Vec<[f32; 3]> = Vec::new();