From b3df3d2069b8eaab72a03045d664e02419ce554a Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 07:53:45 +0200 Subject: [PATCH] Raise undefinedresult for non-finite EPS arithmetic instead of panicking Fixes #3585 Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/graphics.rs | 4 ++-- crates/eps/src/import/interp.rs | 8 ++++++-- crates/eps/src/import/obj.rs | 11 +++++++++-- crates/eps/src/import/shading.rs | 8 ++++---- crates/eps/src/import/tests.rs | 30 ++++++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/crates/eps/src/import/graphics.rs b/crates/eps/src/import/graphics.rs index 613ed3afd..c11c4e1d1 100644 --- a/crates/eps/src/import/graphics.rs +++ b/crates/eps/src/import/graphics.rs @@ -15,7 +15,7 @@ use vectorcraft_doc::{Appearance, AppearanceItem, Dash, Document, FillLayer, Lin use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Rect, Vec2}; use super::interp::{Interp, MAX_GSAVE, matrix_obj}; -use super::obj::{DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err}; +use super::obj::{DictRef, Key, Obj, Op, PsError, Res, Shared, clamp_between, ps_err}; /// Most objects one file makes. const MAX_NODES: usize = 1 << 20; @@ -1192,7 +1192,7 @@ impl Interp<'_> { let domain = nums(get("Domain")); let t = x.first().copied().unwrap_or(0.0); let t = match domain.as_slice() { - [a, b, ..] => t.clamp(a.min(*b), a.max(*b)), + [a, b, ..] => clamp_between(t, *a, *b), _ => t, }; match get("FunctionType").and_then(|o| o.as_num()) { diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index b60aac39d..547d66a11 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -567,11 +567,15 @@ impl<'a> Interp<'a> { None => { let (x, y) = (a.as_num().ok_or(PsError::Ps("typecheck", String::new()))?, b.as_num().ok_or(PsError::Ps("typecheck", String::new()))?); - Obj::Real(match op { + let r = match op { Add => x + y, Sub => x - y, _ => x * y, - }) + }; + if !r.is_finite() { + return ps_err("undefinedresult", ""); + } + Obj::Real(r) } }; self.push(r)?; diff --git a/crates/eps/src/import/obj.rs b/crates/eps/src/import/obj.rs index f540500c4..b4dc34364 100644 --- a/crates/eps/src/import/obj.rs +++ b/crates/eps/src/import/obj.rs @@ -231,11 +231,11 @@ impl Obj { Self::Dict(DictRef::new(d)) } - /// The number this is (integers and reals). + /// The number this is (integers and finite reals: a NaN or infinite real is not a number). pub fn as_num(&self) -> Option { match *self { Self::Int(i) => Some(i as f64), - Self::Real(r) => Some(r), + Self::Real(r) if r.is_finite() => Some(r), _ => None, } } @@ -406,6 +406,13 @@ pub(crate) enum PsError { pub(crate) type Res = Result; +/// `v` limited to the range between `a` and `b` (in either order). Unlike `f64::clamp` this never +/// panics: when one bound is NaN the range collapses to the other bound (both NaN gives `v`), and a +/// NaN `v` gives the lower bound. +pub(crate) fn clamp_between(v: f64, a: f64, b: f64) -> f64 { + v.max(a.min(b)).min(a.max(b)) +} + /// A PostScript error raised at `at`. pub(crate) fn ps_err(name: &'static str, at: &str) -> Res { Err(PsError::Ps(name, at.to_string())) diff --git a/crates/eps/src/import/shading.rs b/crates/eps/src/import/shading.rs index 2e2a523ad..c59881a98 100644 --- a/crates/eps/src/import/shading.rs +++ b/crates/eps/src/import/shading.rs @@ -12,7 +12,7 @@ use vectorcraft_geom::{Affine, Point}; use super::graphics::Space; use super::interp::{Interp, matrix_of}; -use super::obj::{DictRef, Key, Obj, PsError, Res, ps_err}; +use super::obj::{DictRef, Key, Obj, PsError, Res, clamp_between, ps_err}; /// Most patches (or triangles) one shading makes: past it, it is left out with a warning. const MAX_PATCHES: usize = 4096; @@ -348,7 +348,7 @@ impl Interp<'_> { decode.get(2 * j + 1).or(range.get(2 * j + 1)).copied().unwrap_or(1.0), ); let (r0, r1) = (range.get(2 * j).copied().unwrap_or(0.0), range.get(2 * j + 1).copied().unwrap_or(1.0)); - (d0 + raw * (d1 - d0) / max).clamp(r0.min(r1), r0.max(r1)) + clamp_between(d0 + raw * (d1 - d0) / max, r0, r1) }; // Each input into sample space. let mut pos = Vec::with_capacity(m); @@ -356,9 +356,9 @@ impl Interp<'_> { let (a, b) = (domain.get(2 * i).copied().unwrap_or(0.0), domain.get(2 * i + 1).copied().unwrap_or(1.0)); let s = size.get(i).copied().unwrap_or(1.0); let (e0, e1) = (encode.get(2 * i).copied().unwrap_or(0.0), encode.get(2 * i + 1).copied().unwrap_or(s - 1.0)); - let xi = x.get(i).copied().unwrap_or(a).clamp(a.min(b), a.max(b)); + let xi = clamp_between(x.get(i).copied().unwrap_or(a), a, b); let e = if b != a { e0 + (xi - a) * (e1 - e0) / (b - a) } else { e0 }; - pos.push(e.clamp(0.0, s - 1.0)); + pos.push(clamp_between(e, 0.0, s - 1.0)); } if let ([p], Some(s)) = (pos.as_slice(), size.first()) { let (lo, hi) = (p.floor() as usize, (p.ceil() as usize).min(*s as usize - 1)); diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 5ace00ac2..c446ecca5 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -324,6 +324,36 @@ fn shadings_become_gradients() { assert!(near(stops[1].1.to_rgb(), blue.to_rgb()), "{stops:?}"); } +/// #3585: arithmetic that overflows to infinity or NaN is an `undefinedresult` error, so a +/// non-finite number never reaches a shading function's `Domain` (where it used to panic). +#[test] +fn non_finite_arithmetic_is_undefinedresult() { + let errs = |body: &str| { + let r = read(&format!("0 0 10 10 rectfill {body}")); + assert_eq!(objects(&r.document).len(), 1, "{body}: {:?}", r.warnings); + r.warnings.iter().any(|w| w.contains("undefinedresult")) + }; + for body in [ + "1.7e308 1.7e308 add pop", + "-1.7e308 1.7e308 sub pop", + "1e300 1e300 mul pop", + "-1 ln pop", + "0 ln pop", + "-1 log pop", + "-1 sqrt pop", + "10 400 exp pop", + ] { + assert!(errs(body), "{body}"); + } + let axial = + "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function << /FunctionType 2 /Domain [n n] /C0 [1 0 0] /C1 [0 0 1] /N 1 >> >>"; + assert!(errs(&format!("/n 1e300 1e300 mul dup sub def 0 0 100 100 rectclip {axial} shfill"))); + // A sampled function with a Size below 1 is a rangecheck, not a panic. + let sampled = "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function << /FunctionType 0 /Domain [0 1] /Range [0 1 0 1 0 1] /Size [0.5] /BitsPerSample 8 /DataSource (abc) >> >>"; + let r = read(&format!("0 0 10 10 rectfill gsave 0 0 100 100 rectclip {sampled} shfill grestore")); + assert!(!objects(&r.document).is_empty(), "{:?}", r.warnings); +} + /// Tiling patterns and spot inks are found again through a hash of what tells them apart, so a /// program that makes 200,000 patterns and 200,001 inks reads within [`read_soon`]'s 20 seconds. /// Patterns are named Pattern 1, Pattern 2 and so on in the order they are made. A pattern made