Raise undefinedresult for non-finite EPS arithmetic instead of panicking

Fixes #3585

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-24 01:43:52 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent b9819ebd89
commit b3df3d2069
5 changed files with 51 additions and 10 deletions
+2 -2
View File
@@ -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()) {
+6 -2
View File
@@ -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)?;
+9 -2
View File
@@ -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<f64> {
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<T = ()> = Result<T, PsError>;
/// `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<T>(name: &'static str, at: &str) -> Res<T> {
Err(PsError::Ps(name, at.to_string()))
+4 -4
View File
@@ -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));
+30
View File
@@ -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