Merge pull request #5323 from JonRC/fix/eps-currentfile-executed-file

Fix #5321: `currentfile` in a file being run is that file
This commit is contained in:
BFlat
2026-11-03 16:29:55 +07:00
committed by GitHub
4 changed files with 157 additions and 51 deletions
+8 -4
View File
@@ -1,15 +1,19 @@
use super::ops::MAX_DATA;
use super::{
DecodeSettings, Decoder, DecoderContext, DictRef, EncodingMode, ImageBlob, Key, Obj, PsError, Rc, Res, find, hex_decode, jpeg_components, ps,
ps_err,
DecodeSettings, Decoder, DecoderContext, DictRef, EncodingMode, ImageBlob, Key, Obj, PsError, Rc, RefCell, Res, find, hex_decode,
jpeg_components, ps, ps_err,
};
/// A file object: the program itself, or data a filter decoded.
#[derive(Debug)]
pub(crate) enum Stream {
Current,
/// Filters over the program's own data, decoded when first read.
Pending(Vec<Filter>),
/// Filters over the data the program reads, decoded when first read, from the file they were
/// made over: a file being run (`from`), or else the program's own file.
Pending {
chain: Vec<Filter>,
from: Option<Rc<RefCell<Stream>>>,
},
Buf {
data: Vec<u8>,
pos: usize,
+59 -42
View File
@@ -29,7 +29,10 @@ impl Interp<'_> {
pub fn data_op(&mut self, op: Op) -> Res {
use Op::*;
match op {
CurrentFile => self.push(Obj::File { stream: Rc::new(RefCell::new(Stream::Current)), exec: false })?,
CurrentFile => {
let stream = self.running.last().cloned().unwrap_or_else(|| Rc::new(RefCell::new(Stream::Current)));
self.push(Obj::File { stream, exec: false })?;
}
Filter => self.filter()?,
ReadHexString | ReadString => {
let s = self.pop_str()?;
@@ -94,7 +97,7 @@ impl Interp<'_> {
let n = match &*f.borrow() {
Stream::Current => self.lex.rest().len(),
Stream::Buf { data, pos, .. } => data.len().saturating_sub(*pos),
Stream::Pending(_) => 0,
Stream::Pending { .. } => 0,
};
self.push(Obj::Int(n as i64))?;
}
@@ -163,18 +166,7 @@ impl Interp<'_> {
self.push(o)?;
}
Obj::File { stream: f, .. } => {
self.materialize(&f)?;
let (o, immediate) = match &mut *f.borrow_mut() {
Stream::Current => (self.lex.next()?, self.lex.immediate),
Stream::Buf { data, pos, .. } => {
let rest = data.get(*pos..).unwrap_or_default();
let mut lex = Lexer::new(rest);
let o = lex.next()?;
*pos += rest.len() - lex.data().len();
(o, lex.immediate)
}
Stream::Pending(_) => (None, false),
};
let (o, immediate) = self.next_in(&f)?;
let Some(o) = o else { return self.push(Obj::Bool(false)) };
let o = self.scanned(o, immediate)?;
self.push(o)?;
@@ -184,6 +176,23 @@ impl Interp<'_> {
self.push(Obj::Bool(true))
}
/// The next object in file `f` (`token`, and a file being run), read with the whitespace
/// after it, and whether it has `//name`s in it.
pub(in super::super) fn next_in(&mut self, f: &Rc<RefCell<Stream>>) -> Res<(Option<Obj>, bool)> {
self.materialize(f)?;
Ok(match &mut *f.borrow_mut() {
Stream::Current => (self.lex.next()?, self.lex.immediate),
Stream::Buf { data, pos, .. } => {
let rest = data.get(*pos..).unwrap_or_default();
let mut lex = Lexer::new(rest);
let o = lex.next()?;
*pos += rest.len() - lex.data().len();
(o, lex.immediate)
}
Stream::Pending { .. } => (None, false),
})
}
/// Up to `n` bytes of file `f`.
fn read(&mut self, f: &Rc<RefCell<Stream>>, n: usize) -> Res<Vec<u8>> {
self.materialize(f)?;
@@ -201,7 +210,7 @@ impl Interp<'_> {
*pos = end;
data.get(start..end).unwrap_or_default().to_vec()
}
Stream::Pending(_) => vec![],
Stream::Pending { .. } => vec![],
})
}
@@ -220,7 +229,7 @@ impl Interp<'_> {
*pos += 1;
}
}
Stream::Pending(_) => {}
Stream::Pending { .. } => {}
}
}
@@ -275,23 +284,28 @@ impl Interp<'_> {
let spec = Filter { name, sub, predictor, fax };
let stream = match self.pop()? {
Obj::File { stream: f, .. } => {
if let Stream::Buf { data, pos, .. } = &*f.borrow() {
self.alloc(data.len().saturating_sub(*pos))?;
// A filter over the program's own file or the file being run waits until it is
// first read, and keeps which of them it reads, wherever the program runs by then.
let running = self.running.last().is_some_and(|r| Rc::ptr_eq(r, &f));
match &*f.borrow() {
Stream::Buf { data, pos, .. } if !running => self.alloc(data.len().saturating_sub(*pos))?,
_ => {}
}
let chain = match &*f.borrow() {
Stream::Current => Some(vec![]),
Stream::Pending(c) => Some(c.clone()),
let pending = match &*f.borrow() {
Stream::Current => Some((vec![], None)),
Stream::Buf { .. } if running => Some((vec![], Some(f.clone()))),
Stream::Pending { chain, from } => Some((chain.clone(), from.clone())),
Stream::Buf { .. } => None,
};
match chain {
Some(mut c) => {
match pending {
Some((mut chain, from)) => {
// The new filter reads a pending one's data: it has none left of its own
// (`flushfile` on it skips nothing more).
if !c.is_empty() {
if !chain.is_empty() {
*f.borrow_mut() = Stream::Buf { data: vec![], pos: 0, rgb: None };
}
c.push(spec);
Stream::Pending(c)
chain.push(spec);
Stream::Pending { chain, from }
}
None => {
let mut b = f.borrow_mut();
@@ -325,15 +339,6 @@ impl Interp<'_> {
self.push(Obj::File { stream, exec: false })
}
/// The data an executable file runs as a program: what is left of it. `None` for the
/// program's own file, which is being run already.
pub(in super::super) fn program_of(&mut self, f: &Rc<RefCell<Stream>>) -> Res<Option<Vec<u8>>> {
if matches!(&*f.borrow(), Stream::Current) {
return Ok(None);
}
self.read(f, MAX_DATA).map(Some)
}
/// The decoded data left in file `f`, without reading it (a shading's or function's data).
pub(in super::super) fn peek_all(&mut self, f: &Rc<RefCell<Stream>>) -> Res<Vec<u8>> {
self.materialize(f)?;
@@ -343,20 +348,32 @@ impl Interp<'_> {
})
}
/// Decode the filters a file waits to run over the program's data, from where it is now.
fn materialize(&mut self, f: &Rc<RefCell<Stream>>) -> Res {
let chain = match &*f.borrow() {
Stream::Pending(c) => c.clone(),
/// Decode the filters a file waits to run over the data the program reads, from where it is
/// now in the file they were made over: a file being run, or the program's own file.
pub(in super::super) fn materialize(&mut self, f: &Rc<RefCell<Stream>>) -> Res {
let (chain, from) = match &*f.borrow() {
Stream::Pending { chain, from } => (chain.clone(), from.clone()),
_ => return Ok(()),
};
let mut buf: Option<(Vec<u8>, Option<u8>)> = None;
for spec in &chain {
buf = Some(match buf.take() {
None => {
let raw = self.lex.data();
let (data, used, rgb) = decode(spec, raw)?;
let (data, rgb) = match &from {
Some(r) => {
let mut b = r.borrow_mut();
let Stream::Buf { data, pos, .. } = &mut *b else { return io("filter") };
let (out, used, rgb) = decode(spec, data.get(*pos..).unwrap_or_default())?;
*pos = pos.saturating_add(used);
(out, rgb)
}
None => {
let (out, used, rgb) = decode(spec, self.lex.data())?;
self.lex.advance(used);
(out, rgb)
}
};
self.charge_decoded(data.len())?;
self.lex.advance(used);
(data, rgb)
}
Some((prev, was)) => {
+36 -5
View File
@@ -4,10 +4,12 @@
//! Every operation counts against a budget and procedures nest only so deep, so a looping or
//! recursing program ends with [`PsError::Limit`] instead of hanging.
use std::cell::RefCell;
use std::rc::Rc;
use vectorcraft_geom::Affine;
use super::data::Stream;
use super::graphics::{GState, Out};
use super::lex::{IMMEDIATE, Lexer};
use super::obj::{Dict, DictRef, Key, Obj, Op, PsError, Res, Shared, ps_err};
@@ -32,6 +34,9 @@ pub(crate) struct Interp<'a> {
/// The list of the arrays and dictionaries made since this started ([`super::obj::track`]).
made: u64,
pub lex: Lexer<'a>,
/// The files being run (`cvx exec`), innermost last: the program reads from the last one, and
/// it is `currentfile` while it runs.
pub running: Vec<Rc<RefCell<Stream>>>,
pub stack: Vec<Obj>,
pub dicts: Vec<DictRef>,
system: DictRef,
@@ -151,6 +156,7 @@ impl<'a> Interp<'a> {
Self {
made,
lex: Lexer::new(src),
running: vec![],
stack: vec![],
dicts: vec![system.clone(), global.clone(), user.clone()],
system,
@@ -388,10 +394,7 @@ impl<'a> Interp<'a> {
pub fn call(&mut self, v: Obj) -> Res {
match v {
Obj::Array { items, exec: true } => self.run_proc(&items),
Obj::File { stream, exec: true } => match self.program_of(&stream)? {
Some(program) => self.run_program(&program),
None => Ok(()),
},
Obj::File { stream, exec: true } => self.run_file(&stream),
Obj::ExecStr(s) => self.run_program(&s.to_vec()),
// Through the depth guard: `stopped` and the like call back into here.
Obj::Op(op) => {
@@ -428,7 +431,35 @@ impl<'a> Interp<'a> {
r
}
/// Run `program` (what is left of an executable file, an executable string), token by token.
/// Run file `f` (`cvx exec`), reading its objects from it as it goes. While it runs it is
/// `currentfile`, the topmost file being run (PLRM 3rd ed., `currentfile`, <https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf>),
/// so data the program reads from there (an inline image's samples) comes from `f`, after the
/// object that reads it. The program's own file is being run already.
fn run_file(&mut self, f: &Rc<RefCell<Stream>>) -> Res {
if matches!(&*f.borrow(), Stream::Current) {
return Ok(());
}
// Its data, decoded from the program it was made over, before it becomes the program.
self.materialize(f)?;
self.enter()?;
self.running.push(f.clone());
let r = loop {
match self.next_in(f) {
Ok((Some(o), immediate)) => {
if let Err(e) = self.scanned(o, immediate).and_then(|o| self.exec_token(o)) {
break Err(e);
}
}
Ok((None, _)) => break Ok(()),
Err(e) => break Err(e),
}
};
self.running.pop();
self.depth -= 1;
r
}
/// Run `program` (an executable string), token by token.
fn run_program(&mut self, program: &[u8]) -> Res {
self.enter()?;
let mut lex = Lexer::new(program);
+54
View File
@@ -77,6 +77,60 @@ fn separate_sources_keep_their_planes() {
assert_eq!(pixels(&r, 3, 2), [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [16, 64, 112, 255], [32, 80, 128, 255], [48, 96, 144, 255]]);
}
/// A program run from a file (`filter cvx exec`, as Ghostscript's `eps2write` runs each page's
/// content) reads data through `currentfile` from that file, right after the operator that reads
/// it (PLRM 3rd ed., `currentfile`, <https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf>): an image's
/// samples, kept after it as an inline image keeps them, plain or up to `~>` through
/// `SubFileDecode` and `ASCII85Decode`.
#[test]
fn image_data_comes_from_the_file_being_run() {
let r = read(
"currentfile 0 (endstream) /SubFileDecode filter cvx exec\n\
30 20 scale 3 2 8 [3 0 0 -2 0 2] currentfile /ASCIIHexDecode filter false 3 colorimage\n\
ff0000 00ff00 0000ff 102030 405060 708090>\n\
endstream\n",
);
assert_eq!(pixels(&r, 3, 2), [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [16, 32, 48, 255], [64, 80, 96, 255], [112, 128, 144, 255]]);
let r = read(
"/ID { 3 1 8 [3 0 0 -1 0 1] currentfile 0 (~>) /SubFileDecode filter /ASCII85Decode filter false 3 colorimage } def\n\
currentfile 0 (endstream) /SubFileDecode filter cvx exec\n\
30 20 scale ID\n\
rr<$!rr<$!rr~>\n\
endstream\n",
);
assert_eq!(pixels(&r, 3, 1), [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255]]);
super::tests::check(
"/ok false def currentfile 0 (endstream) /SubFileDecode filter cvx exec\n\
currentfile 4 string readstring WXYZ pop (WXYZ) eq /ok exch def\n\
endstream\nok",
);
}
/// A filter waits until it is first read, then reads the file it was made over, wherever the
/// program runs by then: one over the program's own file, read in a file being run, and one over
/// a file being run, read in a file that one runs.
#[test]
fn filters_read_the_file_they_were_made_over() {
super::tests::check(
"/f currentfile /ASCIIHexDecode filter def\n\
currentfile 0 (endstream) /SubFileDecode filter cvx exec\n\
/s f 3 string readstring pop def\n\
endstream\n\
616263>\n\
s (abc) eq",
);
super::tests::check(
"currentfile 0 (endstream) /SubFileDecode filter cvx exec\n\
/g currentfile /ASCIIHexDecode filter def\n\
currentfile 0 (endinner) /SubFileDecode filter cvx exec\n\
/t g 3 string readstring pop def\n\
endinner\n\
78797A>\n\
endstream\n\
t (xyz) eq",
);
}
/// CMYK samples keep their ink amounts in a CMYK TIFF: in the operand and dictionary forms, from
/// hex and binary data, from one source or one a component, at 4, 8 and 16 bits, read through
/// the Decode array. Gray and RGB images stay PNGs.