From 2cca29830e64c793d105da77ce4160b1c89da59b Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 01:43:40 +0000 Subject: [PATCH] Batch fix: EPS: load returns an operator value, so an executable-name alias runs what its name is bound to (#1909) --- crates/pdf/src/ps.rs | 23 +++++++++++++++++++---- crates/pdf/src/tests_import.rs | 11 +++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/pdf/src/ps.rs b/crates/pdf/src/ps.rs index e8612e42..dd735075 100644 --- a/crates/pdf/src/ps.rs +++ b/crates/pdf/src/ps.rs @@ -28,6 +28,8 @@ enum V { Name(Rc), /// A literal name (`/name`). Lit(Rc), + /// A built-in operator, as `load` returns it for a name no dictionary defines. + Op(Rc), Str(Rc>>), Proc(Rc>), Array(Rc>>), @@ -47,7 +49,7 @@ impl V { } fn key(&self) -> String { match self { - V::Name(n) | V::Lit(n) => n.to_string(), + V::Name(n) | V::Lit(n) | V::Op(n) => n.to_string(), V::Str(s) => String::from_utf8_lossy(&s.borrow()).into_owned(), V::Num(n) => format!("{n}"), V::Bool(b) => format!("{b}"), @@ -441,6 +443,12 @@ impl Interp { break; } } + V::Op(op) => { + r = self.spend(1).and_then(|()| self.builtin(op, src)); + if r.is_err() { + break; + } + } // Other items count against the budget as names do. other => { r = self.spend(1); @@ -465,6 +473,10 @@ impl Interp { self.calls -= 1; r } + V::Op(op) => { + self.spend(1)?; + self.builtin(&op, src) + } other => { self.push(other); Ok(()) @@ -480,7 +492,9 @@ impl Interp { // An operator stored under another name (`/mo /moveto load def`, as Illustrator's // EPS prolog does for every path operator) runs that operator. It pushed the // operator's name instead, so such files drew nothing (#955). - V::Name(op) => self.builtin(&op, src), + V::Op(op) => self.builtin(&op, src), + // An executable name (`/y /x cvx def`) runs what that name is bound to. + V::Name(_) => self.exec_obj(v, src), other => { self.push(other); Ok(()) @@ -977,7 +991,7 @@ impl Interp { } "load" => { let k = self.pop().key(); - let v = self.lookup(&k).unwrap_or(V::Name(k.into())); + let v = self.lookup(&k).unwrap_or(V::Op(k.into())); self.push(v); } "undef" => { @@ -1174,13 +1188,14 @@ impl Interp { } "xcheck" => { let v = self.pop(); - self.push(V::Bool(matches!(v, V::Proc(_) | V::Name(_)))); + self.push(V::Bool(matches!(v, V::Proc(_) | V::Name(_) | V::Op(_)))); } "type" => { let t = match self.pop() { V::Num(_) => "realtype", V::Bool(_) => "booleantype", V::Name(_) | V::Lit(_) => "nametype", + V::Op(_) => "operatortype", V::Str(_) => "stringtype", V::Proc(_) | V::Array(_) => "arraytype", V::Dict(_) => "dicttype", diff --git a/crates/pdf/src/tests_import.rs b/crates/pdf/src/tests_import.rs index ca5e2417..11ef72f8 100644 --- a/crates/pdf/src/tests_import.rs +++ b/crates/pdf/src/tests_import.rs @@ -55,6 +55,17 @@ fn eps_filter_on_a_string_source_does_not_skip_the_program() { assert!(coverage(&render(&doc), 50, 25, 150, 75) > 4900.0, "drawing after an image read from a string"); } +/// A loaded operator runs that operator even after its name is redefined (the prolog idiom +/// `/_m /moveto load def /moveto {… _m} def`), and an executable name bound with `cvx` runs what +/// its name is bound to. +#[test] +fn eps_loaded_operators_and_executable_name_aliases() { + let saved = "/_m /moveto load def /moveto {_m} def 1 0 0 setrgbcolor 50 25 moveto 150 25 lineto 150 75 lineto 50 75 lineto closepath fill\n"; + assert!(coverage(&render(&eps(saved.as_bytes())), 50, 25, 150, 75) > 4900.0, "a redefined moveto that calls the saved one"); + let alias = "/sq {50 25 moveto 150 25 lineto 150 75 lineto 50 75 lineto closepath fill} def /y /sq cvx def 1 0 0 setrgbcolor y\n"; + assert!(coverage(&render(&eps(alias.as_bytes())), 50, 25, 150, 75) > 4900.0, "an executable name bound to a procedure's name"); +} + /// Each `exec` of the executable name `exec` runs the next one: the chain is cut off at the call /// depth instead of overflowing the stack. #[test]