EPS import: big operators are charged for the bytes and objects they touch (#3584)

putinterval, copy of a string, roll, counttomark, clear to mark and cvn
worked on up to 4 MiB or 100,000 objects for the one operation each is charged,
so a small file looped over them could keep the importer busy for hours within
the operation budget. Each now spends one operation per 256 bytes or objects.

Fixes #3584
This commit is contained in:
marshiyar
2026-10-31 19:43:17 +07:00
committed by bflatastic
parent 9efe6dc764
commit bd0601c780
2 changed files with 34 additions and 2 deletions
+19 -2
View File
@@ -335,6 +335,12 @@ impl<'a> Interp<'a> {
/// 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.
/// 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)
}
pub fn spend(&mut self, ops: u64) -> Res {
self.ops = self.ops.saturating_add(ops);
if self.ops > MAX_OPS {
@@ -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<str> = 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<Vec<Obj>> {
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");
}
+15
View File
@@ -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() {