diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index fe0c63919..13382e533 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -15,8 +15,11 @@ use super::graphics::{GState, Out}; use super::lex::{IMMEDIATE, Lexer, number}; use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err}; -/// Most operations one file may run. +/// Most operations one file may run, whatever its length. const MAX_OPS: u64 = 50_000_000; +/// More operations a file may run for each byte of its program: a large file that draws a lot +/// runs proportionally more, and stays bounded by its length. +const OPS_PER_BYTE: u64 = 64; /// Deepest procedure nesting. const MAX_DEPTH: u32 = 400; /// Most operands on the stack. @@ -55,6 +58,8 @@ pub(crate) struct Interp<'a> { resources: DictRef, depth: u32, ops: u64, + /// Most operations this program may run: [`MAX_OPS`] and [`OPS_PER_BYTE`] for each byte. + max_ops: u64, /// Bytes asked for so far (see [`MAX_MEMORY`]). allocated: usize, seed: u32, @@ -177,6 +182,7 @@ impl<'a> Interp<'a> { resources, depth: 0, ops: 0, + max_ops: MAX_OPS.saturating_add((src.len() as u64).saturating_mul(OPS_PER_BYTE)), allocated: 0, seed: 1, saves: vec![], @@ -360,11 +366,11 @@ impl<'a> Interp<'a> { self.spend((n / 256) as u64) } - /// Count `ops` operations' worth of work against [`MAX_OPS`]: what an operator that costs + /// Count `ops` operations' worth of work against the budget (`max_ops`): what an operator that costs /// far more than one operation (laying out type) does in a loop has to end too. pub fn spend(&mut self, ops: u64) -> Res { self.ops = self.ops.saturating_add(ops); - if self.ops > MAX_OPS { + if self.ops > self.max_ops { return Err(PsError::Limit("the program runs too long")); } Ok(()) diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 2bda9c230..80fe1125c 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -1131,6 +1131,23 @@ fn runs_out_of_budget(body: &str) { assert!(r.warnings.iter().any(|w| w.contains("the program runs too long")), "{body}: {:?}", r.warnings); } +/// A large file that runs far more operations than a small one may, but only a few hundred for +/// each object it draws, reads to its end: the operation budget grows with the program's length. +#[test] +fn a_large_file_with_many_objects_reads_to_its_end() { + let n = 200_000; + let mut body = String::from( + "/sq { 4 dict begin /y exch def /x exch def 0 1 200 { pop } for \ + x y moveto x 1 add y lineto x y 1 add lineto closepath fill end } def\n", + ); + for i in 0..n { + body.push_str(&format!("{} {} sq\n", i % 100, i / 2000)); + } + let r = read_within(&body, 300); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); + assert_eq!(objects(&r.document).len(), n); +} + /// A loop of operators whose work grows with the path they make runs out of the operation budget /// (PLRM 3rd ed., chapter 8): `arc` and `arct` with a huge radius, which make thousands of curve /// segments, `strokepath` with a dash far finer than the line, dashes of length zero included, and