diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index 5e8d12661..12232624f 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -333,6 +333,12 @@ impl<'a> Interp<'a> { self.spend(1) } + /// Charge for work on `n` bytes or objects in one operator: one operation per 256, as `search` + /// charges per 64 bytes, so a loop of big copies runs out of the budget. + fn spend_bytes(&mut self, n: usize) -> Res { + self.spend((n / 256) as u64) + } + /// Count `ops` operations' worth of work against [`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 { @@ -661,6 +667,7 @@ impl<'a> Interp<'a> { let n = self.pop_count()?; let at = self.stack.len().checked_sub(n).ok_or(PsError::Ps("rangecheck", String::new()))?; if n > 0 { + self.spend_bytes(n)?; let k = j.rem_euclid(n as i64) as usize; if let Some(s) = self.stack.get_mut(at..) { s.rotate_right(k); @@ -687,6 +694,7 @@ impl<'a> Interp<'a> { self.pop_to_mark()?; } CountToMark => { + self.spend_bytes(self.stack.len())?; let n = self.stack.iter().rev().position(|o| matches!(o, Obj::Mark)).ok_or(PsError::Ps("unmatchedmark", String::new()))?; self.push(Obj::Int(n as i64))?; } @@ -860,6 +868,7 @@ impl<'a> Interp<'a> { } Cvn => { let s = self.pop_str()?; + self.spend_bytes(s.len())?; // The name is a copy of the string (a bigger one when bytes are replaced). self.alloc(s.len().saturating_mul(3))?; let n: Rc = Rc::from(String::from_utf8_lossy(&s.borrow()).as_ref()); @@ -966,8 +975,14 @@ impl<'a> Interp<'a> { let src = self.pop()?; let at = self.pop_count()?; let written = match (self.pop()?, src) { - (Obj::Array { items, .. }, Obj::Array { items: from, .. }) => items.write(at, &from.to_vec()), - (Obj::Str(s), Obj::Str(from)) => s.write(at, &from.to_vec()), + (Obj::Array { items, .. }, Obj::Array { items: from, .. }) => { + self.spend_bytes(from.len())?; + items.write(at, &from.to_vec()) + } + (Obj::Str(s), Obj::Str(from)) => { + self.spend_bytes(from.len())?; + s.write(at, &from.to_vec()) + } _ => return ps_err("typecheck", ""), }; if !written { @@ -1171,6 +1186,7 @@ impl<'a> Interp<'a> { /// The objects above the topmost mark, which is removed. fn pop_to_mark(&mut self) -> Res> { + self.spend_bytes(self.stack.len())?; let at = self.stack.iter().rposition(|o| matches!(o, Obj::Mark)).ok_or(PsError::Ps("unmatchedmark", String::new()))?; let items = self.stack.split_off(at + 1); self.stack.pop(); @@ -1202,6 +1218,7 @@ impl<'a> Interp<'a> { } Obj::Str(s) => { let from = self.pop_str()?.to_vec(); + self.spend_bytes(from.len())?; if !s.write(0, &from) { return ps_err("rangecheck", "copy"); } diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index d722078b7..aa97f32cb 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -1307,6 +1307,21 @@ fn search_in_a_loop_runs_out_of_the_budget() { runs_out_of_budget(&format!("{make} {{ h n search {{ pop pop pop }} {{ pop }} ifelse }} loop")); } +/// #3584: operators that touch up to 4 MiB or 100,000 objects are charged for it, so a loop of them +/// runs out of the operation budget instead of keeping the importer busy for hours. +#[test] +fn big_operators_in_a_loop_run_out_of_the_budget() { + let strings = "/a 4194304 string def /b 4194304 string def"; + for body in [ + format!("{strings} {{ a 0 b putinterval }} loop"), + format!("{strings} {{ b a copy pop }} loop"), + "99990 { 0 } repeat { 99990 1 roll } loop".to_string(), + "mark 99990 { 0 } repeat { counttomark pop } loop".to_string(), + ] { + runs_out_of_budget(&body); + } +} + /// #3568: an image dictionary with more data sources than colour components is a `rangecheck`. #[test] fn an_image_with_more_data_sources_than_components_is_refused() {