From a492033aa18a554d5069efd212990e5297c1bdab Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sun, 5 Jul 2026 06:45:39 +0200 Subject: [PATCH] Guard float overlay entry points against non-finite coordinates --- iOverlay/src/float/overlay.rs | 45 ++++++++-- iOverlay/src/float/relate.rs | 16 +++- iOverlay/src/float/scale.rs | 26 ++++-- iOverlay/src/float/string_overlay.rs | 19 ++++- iOverlay/src/geom/v_segment.rs | 2 +- iOverlay/src/mesh/outline/offset.rs | 7 +- iOverlay/src/mesh/stroke/offset.rs | 10 ++- iOverlay/tests/crash_tests.rs | 1 + iOverlay/tests/float_point_adapter.rs | 117 +++++++++++++++++++++++++- iOverlay/tests/overlay_tests.rs | 2 +- 10 files changed, 225 insertions(+), 20 deletions(-) diff --git a/iOverlay/src/float/overlay.rs b/iOverlay/src/float/overlay.rs index 52b48d21..d1a01ea1 100644 --- a/iOverlay/src/float/overlay.rs +++ b/iOverlay/src/float/overlay.rs @@ -25,6 +25,24 @@ use i_shape::float::despike::DeSpikeContour; use i_shape::float::simple::SimplifyContour; use i_tree::{Expiration, LayoutNumber}; +/// Returns true when both coordinates of the point are finite (not NaN or infinite). +#[inline(always)] +pub(crate) fn is_finite_point(point: &P) -> bool { + point.x().to_f64().is_finite() && point.y().to_f64().is_finite() +} + +/// Returns true if the contour contains any non-finite point. +#[inline(always)] +pub(crate) fn contour_has_non_finite(contour: &[P]) -> bool { + contour.iter().any(|p| !is_finite_point(p)) +} + +/// Checks finiteness for `&&P` (used with iterator adapters that double-reference). +#[inline(always)] +pub(crate) fn is_finite_point_ref(point: &&P) -> bool { + is_finite_point(*point) +} + /// Options for float overlay extraction. /// /// `F` is the floating-point scalar type (`f32` or `f64`). `I` is the integer engine @@ -149,7 +167,11 @@ where R0: ShapeResource

+ ?Sized, R1: ShapeResource

+ ?Sized, { - let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let iter = subj + .iter_paths() + .chain(clip.iter_paths()) + .flatten() + .filter(is_finite_point_ref); let adapter = FloatPointAdapter::with_iter(iter); let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); let clip_capacity = clip.iter_paths().fold(0, |s, c| s + c.len()); @@ -178,7 +200,11 @@ where R0: ShapeResource

+ ?Sized, R1: ShapeResource

+ ?Sized, { - let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let iter = subj + .iter_paths() + .chain(clip.iter_paths()) + .flatten() + .filter(is_finite_point_ref); let adapter = FloatPointAdapter::with_iter(iter); let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); let clip_capacity = clip.iter_paths().fold(0, |s, c| s + c.len()); @@ -198,7 +224,7 @@ where where R: ShapeResource

+ ?Sized, { - let iter = subj.iter_paths().flatten(); + let iter = subj.iter_paths().flatten().filter(is_finite_point_ref); let adapter = FloatPointAdapter::with_iter(iter); let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); @@ -217,7 +243,7 @@ where where R: ShapeResource

+ ?Sized, { - let iter = subj.iter_paths().flatten(); + let iter = subj.iter_paths().flatten().filter(is_finite_point_ref); let adapter = FloatPointAdapter::with_iter(iter); let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); @@ -260,6 +286,9 @@ where #[inline] fn add_source + ?Sized>(&mut self, resource: &R, shape_type: ShapeType) { for contour in resource.iter_paths() { + if contour_has_non_finite(contour) { + continue; + } self.overlay .add_path_iter(contour.iter().map(|p| self.adapter.float_to_int(p)), shape_type); } @@ -279,7 +308,11 @@ where { self.clear(); - let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let iter = subj + .iter_paths() + .chain(clip.iter_paths()) + .flatten() + .filter(is_finite_point_ref); self.adapter = FloatPointAdapter::with_iter(iter); self.add_source(subj, ShapeType::Subject); self.add_source(clip, ShapeType::Clip); @@ -297,7 +330,7 @@ where { self.clear(); - let iter = subj.iter_paths().flatten(); + let iter = subj.iter_paths().flatten().filter(is_finite_point_ref); self.adapter = FloatPointAdapter::with_iter(iter); self.add_source(subj, ShapeType::Subject); } diff --git a/iOverlay/src/float/relate.rs b/iOverlay/src/float/relate.rs index 7ac97a9f..4e6e64d9 100644 --- a/iOverlay/src/float/relate.rs +++ b/iOverlay/src/float/relate.rs @@ -2,6 +2,7 @@ use crate::core::fill_rule::FillRule; use crate::core::overlay::ShapeType; use crate::core::relate::PredicateOverlay; use crate::core::solver::Solver; +use crate::float::overlay::{contour_has_non_finite, is_finite_point_ref}; use i_float::adapter::FloatPointAdapter; use i_float::float::compatible::FloatPointCompatible; use i_float::int::number::int::IntNumber; @@ -82,7 +83,11 @@ where R0: ShapeResource

+ ?Sized, R1: ShapeResource

+ ?Sized, { - let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let iter = subj + .iter_paths() + .chain(clip.iter_paths()) + .flatten() + .filter(is_finite_point_ref); let adapter = FloatPointAdapter::<_, I>::with_iter(iter); let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); let clip_capacity = clip.iter_paths().fold(0, |s, c| s + c.len()); @@ -107,7 +112,11 @@ where R0: ShapeResource

+ ?Sized, R1: ShapeResource

+ ?Sized, { - let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let iter = subj + .iter_paths() + .chain(clip.iter_paths()) + .flatten() + .filter(is_finite_point_ref); let adapter = FloatPointAdapter::<_, I>::with_iter(iter); let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); let clip_capacity = clip.iter_paths().fold(0, |s, c| s + c.len()); @@ -129,6 +138,9 @@ where /// * `shape_type` - Whether to add as `Subject` or `Clip`. pub fn add_source + ?Sized>(&mut self, resource: &R, shape_type: ShapeType) { for contour in resource.iter_paths() { + if contour_has_non_finite(contour) { + continue; + } self.overlay .add_path_iter(contour.iter().map(|p| self.adapter.float_to_int(p)), shape_type); } diff --git a/iOverlay/src/float/scale.rs b/iOverlay/src/float/scale.rs index 6e567e80..117aef68 100644 --- a/iOverlay/src/float/scale.rs +++ b/iOverlay/src/float/scale.rs @@ -2,7 +2,7 @@ use crate::core::fill_rule::FillRule; use crate::core::overlay::ShapeType; use crate::core::overlay_rule::OverlayRule; use crate::core::solver::Solver; -use crate::float::overlay::{FloatOverlay, OverlayOptions}; +use crate::float::overlay::{FloatOverlay, OverlayOptions, is_finite_point_ref}; use crate::float::relate::FloatPredicateOverlay; use i_float::adapter::{FloatPointAdapter, FloatPointAdapterScaleError}; use i_float::float::compatible::FloatPointCompatible; @@ -178,7 +178,11 @@ where R0: ShapeResource

+ ?Sized, R1: ShapeResource

+ ?Sized, { - let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let iter = subj + .iter_paths() + .chain(clip.iter_paths()) + .flatten() + .filter(is_finite_point_ref); let adapter = FloatPointAdapter::with_iter_and_scale_checked(iter, scale)?; let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); @@ -214,7 +218,11 @@ where R0: ShapeResource

+ ?Sized, R1: ShapeResource

+ ?Sized, { - let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let iter = subj + .iter_paths() + .chain(clip.iter_paths()) + .flatten() + .filter(is_finite_point_ref); let adapter = FloatPointAdapter::with_iter_and_scale_checked(iter, scale)?; let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); @@ -287,7 +295,11 @@ where R0: ShapeResource

+ ?Sized, R1: ShapeResource

+ ?Sized, { - let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let iter = subj + .iter_paths() + .chain(clip.iter_paths()) + .flatten() + .filter(is_finite_point_ref); let adapter = FloatPointAdapter::with_iter_and_scale_checked(iter, scale)?; let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); @@ -319,7 +331,11 @@ where R0: ShapeResource

+ ?Sized, R1: ShapeResource

+ ?Sized, { - let iter = subj.iter_paths().chain(clip.iter_paths()).flatten(); + let iter = subj + .iter_paths() + .chain(clip.iter_paths()) + .flatten() + .filter(is_finite_point_ref); let adapter = FloatPointAdapter::with_iter_and_scale_checked(iter, scale)?; let subj_capacity = subj.iter_paths().fold(0, |s, c| s + c.len()); diff --git a/iOverlay/src/float/string_overlay.rs b/iOverlay/src/float/string_overlay.rs index f6217514..51db5497 100644 --- a/iOverlay/src/float/string_overlay.rs +++ b/iOverlay/src/float/string_overlay.rs @@ -1,5 +1,6 @@ use crate::core::fill_rule::FillRule; use crate::core::solver::Solver; +use crate::float::overlay::{contour_has_non_finite, is_finite_point_ref}; use crate::float::scale::FixedScaleOverlayError; use crate::float::string_graph::FloatStringGraph; use crate::string::clip::ClipRule; @@ -64,7 +65,11 @@ where R0: ShapeResource

, R1: ShapeResource

, { - let iter = shape.iter_paths().chain(string.iter_paths()).flatten(); + let iter = shape + .iter_paths() + .chain(string.iter_paths()) + .flatten() + .filter(is_finite_point_ref); let adapter = FloatPointAdapter::with_iter(iter); let shape_capacity = shape.iter_paths().fold(0, |s, c| s + c.len()); let string_capacity = string.iter_paths().fold(0, |s, c| s + c.len()); @@ -87,7 +92,11 @@ where R0: ShapeResource

, R1: ShapeResource

, { - let iter = shape.iter_paths().chain(string.iter_paths()).flatten(); + let iter = shape + .iter_paths() + .chain(string.iter_paths()) + .flatten() + .filter(is_finite_point_ref); let adapter = FloatPointAdapter::with_iter_and_scale_checked(iter, scale)?; let shape_capacity = shape.iter_paths().fold(0, |s, c| s + c.len()); @@ -108,6 +117,9 @@ where #[inline] pub fn unsafe_add_shapes>(mut self, source: &S) -> Self { for contour in source.iter_paths() { + if contour_has_non_finite(contour) { + continue; + } self = self.unsafe_add_shape_contour(contour); } self @@ -122,6 +134,9 @@ where #[inline] pub fn unsafe_add_string_lines>(mut self, resource: &S) -> Self { for path in resource.iter_paths() { + if contour_has_non_finite(path) { + continue; + } self = self.unsafe_add_string_line(path); } self diff --git a/iOverlay/src/geom/v_segment.rs b/iOverlay/src/geom/v_segment.rs index f8449d9a..2ffc6c8a 100644 --- a/iOverlay/src/geom/v_segment.rs +++ b/iOverlay/src/geom/v_segment.rs @@ -58,7 +58,7 @@ impl BottomSegment for Option> { #[inline(always)] fn update_if_under(&mut self, segment: VSegment) { if let Some(best) = self { - if segment.is_under_segment(&best) { + if segment.is_under_segment(best) { *best = segment } } else { diff --git a/iOverlay/src/mesh/outline/offset.rs b/iOverlay/src/mesh/outline/offset.rs index 41c27cf5..5574b702 100644 --- a/iOverlay/src/mesh/outline/offset.rs +++ b/iOverlay/src/mesh/outline/offset.rs @@ -4,6 +4,7 @@ use crate::core::overlay::ShapeType::Subject; use crate::core::overlay::{ContourDirection, Overlay}; use crate::core::overlay_rule::OverlayRule; use crate::float::overlay::OverlayOptions; +use crate::float::overlay::{contour_has_non_finite, is_finite_point_ref}; use crate::float::scale::FixedScaleOverlayError; use crate::mesh::outline::builder::OutlineBuilder; use crate::mesh::style::OutlineStyle; @@ -446,7 +447,8 @@ where let additional_offset = outer_additional_offset.abs() + inner_additional_offset.abs(); - let mut rect = FloatRect::with_iter(source.iter_paths().flatten()).unwrap_or(FloatRect::zero()); + let mut rect = FloatRect::with_iter(source.iter_paths().flatten().filter(is_finite_point_ref)) + .unwrap_or(FloatRect::zero()); rect.add_offset(additional_offset); let adapter = FloatPointAdapter::::new(rect); @@ -485,6 +487,9 @@ where let mut flat_buffer = FlatContoursBuffer::::with_capacity(0); for path in source.iter_paths() { + if contour_has_non_finite(path) { + continue; + } let area = path.unsafe_int_area(&self.adapter); if area.unsigned_abs() <= ::from_u64(1) { // ignore degenerate paths diff --git a/iOverlay/src/mesh/stroke/offset.rs b/iOverlay/src/mesh/stroke/offset.rs index 150e5253..b95354ea 100644 --- a/iOverlay/src/mesh/stroke/offset.rs +++ b/iOverlay/src/mesh/stroke/offset.rs @@ -2,6 +2,7 @@ use crate::core::fill_rule::FillRule; use crate::core::overlay::Overlay; use crate::core::overlay_rule::OverlayRule; use crate::float::overlay::OverlayOptions; +use crate::float::overlay::{contour_has_non_finite, is_finite_point_ref}; use crate::float::scale::FixedScaleOverlayError; use crate::i_shape::source::resource::ShapeResource; use crate::mesh::stroke::builder::StrokeBuilder; @@ -483,7 +484,8 @@ where let builder = StrokeBuilder::::new(style); let a = builder.additional_offset(r); - let mut rect = FloatRect::with_iter(source.iter_paths().flatten()).unwrap_or(FloatRect::zero()); + let mut rect = FloatRect::with_iter(source.iter_paths().flatten().filter(is_finite_point_ref)) + .unwrap_or(FloatRect::zero()); rect.add_offset(a); let adapter = FloatPointAdapter::::new(rect); @@ -519,6 +521,9 @@ where let mut segments = Vec::with_capacity(capacity); for path in source.iter_paths() { + if contour_has_non_finite(path) { + continue; + } self.builder .build(path, is_closed_path, &self.adapter, &mut segments); } @@ -561,6 +566,9 @@ where let mut segments = Vec::with_capacity(capacity); for path in source.iter_paths() { + if contour_has_non_finite(path) { + continue; + } self.builder .build(path, is_closed_path, &self.adapter, &mut segments); } diff --git a/iOverlay/tests/crash_tests.rs b/iOverlay/tests/crash_tests.rs index ae04ba35..2e86d1b9 100644 --- a/iOverlay/tests/crash_tests.rs +++ b/iOverlay/tests/crash_tests.rs @@ -51,6 +51,7 @@ mod tests { } } + #[allow(clippy::excessive_precision)] #[test] fn test_01() { let subj = [ diff --git a/iOverlay/tests/float_point_adapter.rs b/iOverlay/tests/float_point_adapter.rs index 3580ba82..1451ca34 100644 --- a/iOverlay/tests/float_point_adapter.rs +++ b/iOverlay/tests/float_point_adapter.rs @@ -58,7 +58,7 @@ mod tests { rect.max_y + 0.1, ); - let adapter_100 = FloatPointAdapter::<_, i32>::with_scale(buffer_rect.clone(), 100.0); + let adapter_100 = FloatPointAdapter::<_, i32>::with_scale(buffer_rect, 100.0); let adapter_1000 = FloatPointAdapter::<_, i32>::with_scale(buffer_rect, 1000.0); let subj_100 = FloatOverlay::with_adapter(adapter_100, shape.len()) @@ -79,4 +79,119 @@ mod tests { println!("100: {:?}", c100); println!("1000: {:?}", c1000); } + + #[test] + fn nan_coordinates_do_not_panic() { + let nan_contour: Vec<[f64; 2]> = vec![[f64::NAN, f64::NAN]]; + + let result = FloatOverlay::<[f64; 2]>::from_subj(&nan_contour) + .overlay(OverlayRule::Subject, FillRule::NonZero); + + assert!(result.is_empty()); + } + + #[test] + fn nan_in_mixed_contour_does_not_panic() { + let contour: Vec<[f64; 2]> = vec![[0.0, 0.0], [f64::NAN, 1.0], [1.0, 1.0], [1.0, 0.0]]; + + let result = + FloatOverlay::<[f64; 2]>::from_subj(&contour).overlay(OverlayRule::Subject, FillRule::NonZero); + + assert!(result.is_empty()); + } + + #[test] + fn infinity_coordinates_do_not_panic() { + let inf_contour: Vec<[f64; 2]> = vec![[f64::INFINITY, f64::NEG_INFINITY]]; + + let result = FloatOverlay::<[f64; 2]>::from_subj(&inf_contour) + .overlay(OverlayRule::Subject, FillRule::NonZero); + + assert!(result.is_empty()); + } + + #[test] + fn single_point_degenerate_does_not_panic() { + let contour: Vec<[f64; 2]> = vec![[1.0, 1.0]]; + + let result = + FloatOverlay::<[f64; 2]>::from_subj(&contour).overlay(OverlayRule::Subject, FillRule::NonZero); + + assert!(result.is_empty()); + } + + #[test] + fn two_point_degenerate_does_not_panic() { + let contour: Vec<[f64; 2]> = vec![[1.0, 1.0], [2.0, 2.0]]; + + let result = + FloatOverlay::<[f64; 2]>::from_subj(&contour).overlay(OverlayRule::Subject, FillRule::NonZero); + + assert!(result.is_empty()); + } + + #[test] + fn all_same_points_degenerate_does_not_panic() { + let contour: Vec<[f64; 2]> = vec![[1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]]; + + let result = + FloatOverlay::<[f64; 2]>::from_subj(&contour).overlay(OverlayRule::Subject, FillRule::NonZero); + + assert!(result.is_empty()); + } + + #[test] + fn nan_overlay_with_valid_clip_does_not_panic() { + let nan_contour: Vec<[f64; 2]> = vec![[f64::NAN, f64::NAN]]; + let valid_contour: Vec<[f64; 2]> = vec![[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]; + + let result = FloatOverlay::<[f64; 2]>::from_subj_and_clip(&nan_contour, &valid_contour) + .overlay(OverlayRule::Intersect, FillRule::NonZero); + + assert!(result.is_empty()); + } + + #[test] + fn nan_self_intersection_does_not_panic() { + let nan_contour: Vec<[f64; 2]> = vec![[f64::NAN, f64::NAN]]; + + let result = FloatOverlay::<[f64; 2]>::from_subj_and_clip(&nan_contour, &nan_contour) + .overlay(OverlayRule::Union, FillRule::NonZero); + + assert!(result.is_empty()); + } + + #[test] + fn f32_nan_coordinates_do_not_panic() { + let nan_contour: Vec<[f32; 2]> = vec![[f32::NAN, f32::NAN]]; + + let result = FloatOverlay::<[f32; 2]>::from_subj(&nan_contour) + .overlay(OverlayRule::Subject, FillRule::NonZero); + + assert!(result.is_empty()); + } + + #[test] + fn shapes_with_nan_contour_does_not_panic() { + let shapes: Vec> = vec![ + vec![[f64::NAN, f64::NAN]], + vec![[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]], + ]; + + let result = + FloatOverlay::<[f64; 2]>::from_subj(&shapes).overlay(OverlayRule::Subject, FillRule::NonZero); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].len(), 1); + } + + #[test] + fn empty_contour_does_not_panic() { + let contour: Vec<[f64; 2]> = vec![]; + + let result = + FloatOverlay::<[f64; 2]>::from_subj(&contour).overlay(OverlayRule::Subject, FillRule::NonZero); + + assert!(result.is_empty()); + } } diff --git a/iOverlay/tests/overlay_tests.rs b/iOverlay/tests/overlay_tests.rs index 4f7f99d0..a0b72135 100644 --- a/iOverlay/tests/overlay_tests.rs +++ b/iOverlay/tests/overlay_tests.rs @@ -101,7 +101,7 @@ mod tests { let graph = overlay.build_graph_view(fill_rule).unwrap(); let result = graph.extract_shapes(overlay_rule, &mut Default::default()); - println!("{}: {}", &overlay_rule, result.json_print()); + println!("{}: {}", overlay_rule, result.json_print()); match overlay_rule { OverlayRule::Subject => { assert_eq!(true, overlay::is_group_of_shapes_one_of(&result, &test.subject))