diff --git a/crates/eps/src/import/graphics.rs b/crates/eps/src/import/graphics.rs index 2a6c5358..79143dd9 100644 --- a/crates/eps/src/import/graphics.rs +++ b/crates/eps/src/import/graphics.rs @@ -37,6 +37,16 @@ const SHADING_SAMPLES: usize = 32; /// Most bounds of a stitched linear function that become a gradient's stops: past it, the /// function is sampled like any other (each stop evaluates the whole function). const MAX_STITCH_STOPS: usize = 1024; +/// Most functions one evaluation of a shading's function visits: the function itself, each entry +/// of an array of functions and, in each stitching function, the piece that applies to the input. +/// For every shading type, the `Function` entry is one function dictionary or an array of one for +/// each color component (PLRM 3rd ed., Tables 4.12 to 4.17; a type 7 shading has the entries of +/// type 6). The reader allows at most [`MAX_INKS`] components, and the depth limit allows each +/// entry a chain of at most 8 functions. +const MAX_FUNCTION_VISITS: usize = 1 + MAX_INKS * 8; +/// Work one function costs against the operation budget each time it is evaluated: about as much +/// as 8 operations (measured). +const FUNCTION_OPS: u64 = 8; pub(super) const FAR_AWAY: &str = "objects far outside the page were left out"; pub(super) const TOO_MUCH: &str = "the file draws more than VectorCraft reads: the rest was left out"; @@ -1064,16 +1074,34 @@ impl Interp<'_> { /// A function (types 0, 2 and 3; an array of them, one per component) at `x` (types 2 and 3 /// take one input). pub fn eval(&mut self, f: &Obj, x: &[f64], depth: u32) -> Res> { - if depth > 8 { + self.eval_inner(f, x, depth, &mut 0) + } + + /// [`Self::eval`], with `visits` (shared by the whole recursion, incremented on every call) + /// counting the functions visited against [`MAX_FUNCTION_VISITS`], as [`linear_points`] counts + /// its nodes. The depth limit alone stops a function that refers to itself, but not one that + /// fans out through shared arrays: when four levels of arrays each have 100 entries that all + /// refer to the same array of the level below (at the last level, the same function), one + /// evaluation would visit 101,010,101 functions. Each function visited also counts + /// [`FUNCTION_OPS`] against the operation budget. + fn eval_inner(&mut self, f: &Obj, x: &[f64], depth: u32, visits: &mut usize) -> Res> { + *visits += 1; + if depth > 8 || *visits > MAX_FUNCTION_VISITS { return ps_err("limitcheck", "Function"); } + self.spend(FUNCTION_OPS)?; let d = match f { Obj::Dict(d) => d.clone(), Obj::Array { items, .. } => { + // Each entry is at least one more function: an array with more entries than the + // visits left raises `limitcheck` before it is copied. + if items.len() > MAX_FUNCTION_VISITS - *visits { + return ps_err("limitcheck", "Function"); + } let fs = items.to_vec(); let mut out = vec![]; for g in &fs { - out.extend(self.eval(g, x, depth + 1)?.first().copied()); + out.extend(self.eval_inner(g, x, depth + 1, visits)?.first().copied()); } return Ok(out); } @@ -1115,7 +1143,7 @@ impl Interp<'_> { let (e0, e1) = (encode.get(2 * k).copied().unwrap_or(0.0), encode.get(2 * k + 1).copied().unwrap_or(1.0)); let u = if b != a { e0 + (t - a) / (b - a) * (e1 - e0) } else { e0 }; let g = fs.and_then(|fs| fs.get(k)).ok_or(PsError::Ps("rangecheck", "Functions".into()))?; - self.eval(&g, &[u], depth + 1) + self.eval_inner(&g, &[u], depth + 1, visits) } _ => { self.out.warn(UNKNOWN_FUNCTIONS); diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 5d6f4674..68d99db1 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -696,6 +696,36 @@ fn arcs_with_huge_angles_end_at_their_second_angle() { } } +/// An axial shading's function is one function dictionary or an array of one for each color +/// component (PLRM 3rd ed., Table 4.13). One evaluation visits at most 257 functions, and past +/// that the shading raises `limitcheck`: when four levels of arrays each have 100 entries that all +/// refer to the same array of the level below (at the last level, the same function), one +/// evaluation would visit 101,010,101 functions. Each function visited also counts against the +/// operation budget. +#[test] +fn shading_functions_in_shared_arrays_stop_at_the_limits() { + let axial = |function: &str| format!("<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 100 0] /Function {function} >>"); + let f = "/f << /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> def"; + // `a`: an array of `n` references to the function `f`. + let array = |n: usize| format!("{f} /a {n} array def 0 1 {} {{ a exch f put }} for", n - 1); + // Each level is an array of 100 references to the level below. + let levels = "/l2 100 array def 0 1 99 { l2 exch a put } for /l3 100 array def 0 1 99 { l3 exch l2 put } for \ + /l4 100 array def 0 1 99 { l4 exch l3 put } for"; + let limitcheck = |r: &crate::Imported| r.warnings.iter().any(|w| w.contains("limitcheck in `shfill`: Function")); + let r = read_soon(&format!("0 0 10 10 rectfill {} {levels} {} shfill", array(100), axial("l4"))); + assert!(limitcheck(&r) && objects(&r.document).len() == 1, "{:?}", r.warnings); + // 256 references are 257 functions for each sample, the most allowed; one more raises `limitcheck`. + let r = read_soon(&format!("{} {} shfill", array(256), axial("a"))); + let o = objects(&r.document); + assert!(r.warnings.is_empty() && matches!(fill(&o[0]), Some(Paint::Gradient(_))), "{:?}", r.warnings); + let r = read_soon(&format!("0 0 10 10 rectfill {} {} shfill", array(257), axial("a"))); + assert!(limitcheck(&r) && objects(&r.document).len() == 1, "{:?}", r.warnings); + // Each of these shadings visits 33 * 257 functions (33 samples), and 2000 of them count for + // more than the operation budget. + let r = read(&format!("{} /sh {} def 2000 {{ sh shfill }} repeat", array(256), axial("a"))); + assert!(r.warnings.iter().any(|w| w.contains("the program runs too long")), "{:?}", r.warnings); +} + #[test] fn cshow_runs_its_procedure_for_each_character() { check("/n 0 def { pop pop pop /n n 1 add def } (abc) cshow n 3 eq");