Part of #505: restore puts dictionaries back as they were at the save
The PostScript Language Reference (3rd ed., §3.7.3 "Save and Restore") has `restore` reset composite objects in local VM to their values at the `save`. The reader only restored the graphics state, so dictionary entries changed between `save` and `restore` stayed changed: a program that counts its open saves in a dictionary and refuses to go past a depth stopped after that many saves in sequence, and the file fell back to its preview. EPS files with many images, each drawn between `save` and `restore`, are a likely case of #505; its reporter hasn't sent a file, so this is part of it. - While a `save` is open, `def`, `store`, `put`, `undef` and dictionary `copy` keep each entry's old value in a journal (counted against the memory budget); `restore` undoes it, newest change first, down to its save. A save restored already, itself or through an outer one, is no longer valid (`invalidrestore`, §3.7.3). - `cachestatus` reports a device's cache sizes (chapter 8) instead of zeros: its last value, the most bytes one cached glyph may take, is what programs divide by. - Tests (programs written from the reference): 100 saves counted in a dictionary under a depth limit, each dictionary change undone, saves restored twice, `cachestatus`'s glyph limit. Mutation-checked: without the journal or with a zero `cachestatus` the test fails. The import fuzz vocabulary has saves and restores around dictionary changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1d265951df
commit
b43b15a6e8
File diff suppressed because one or more lines are too long
|
|
@ -957,6 +957,8 @@ fn arb_ps_token() -> impl Strategy<Value = String> {
|
|||
"/f currentfile /ASCIIHexDecode filter def", "{f 3 string readstring pop}", "{f 0 string readstring pop}", "true 3 colorimage",
|
||||
"3 2 8 [3 0 0 2 0 0] {f 5 string readstring pop} {(ab)} {f 1 string readstring pop} true 3 colorimage",
|
||||
"<< /ImageType 1 /Width 3 /Height 2 /BitsPerComponent 4 /Decode [0 1 0 1 0 1 0 1] /ImageMatrix [3 0 0 2 0 0] /MultipleDataSources true /DataSource [(a) (bc) {f 2 string readstring pop} (d)] >>",
|
||||
// Local VM restored: dictionary changes undone, saves restored twice.
|
||||
"/a 1 def save /a 2 def", "restore", "save dup restore", "1 dict save exch /k 1 put", "cachestatus", "/n 0 def save /n n 1 add store",
|
||||
// Executable strings and keys other than names (PLRM 3rd ed., `cvx`, `load`).
|
||||
"(>>) cvx", "(1 2 add) cvx exec", "(x) cvx dup exec", "0 load", "1 { } def", "(mark) cvx cvlit"
|
||||
])
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ pub(crate) struct Interp<'a> {
|
|||
/// Bytes asked for so far (see [`MAX_MEMORY`]).
|
||||
allocated: usize,
|
||||
seed: u32,
|
||||
/// The `save`s not restored yet, oldest first.
|
||||
saves: Vec<SaveMark>,
|
||||
/// What dictionary entries were before they changed since the oldest `save` (dictionary,
|
||||
/// key, old value): `restore` puts them back, as restoring local VM does (PLRM 3rd ed.,
|
||||
/// §3.7.3 "Save and Restore": composite objects get the values they had at the `save`).
|
||||
journal: Vec<(DictRef, Key, Option<Obj>)>,
|
||||
/// Where the error that is unwinding was raised (see [`Fault`]).
|
||||
pub fault: Option<Fault>,
|
||||
/// The width a Type 3 glyph procedure gave (`setcachedevice`, `setcharwidth`).
|
||||
|
|
@ -72,6 +78,13 @@ pub(crate) struct Fault {
|
|||
/// Most procedure names a [`Fault`] keeps.
|
||||
const FAULT_PROCS: usize = 3;
|
||||
|
||||
/// Where a `save` was made: the graphics states saved and the journal's length then.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct SaveMark {
|
||||
gstates: usize,
|
||||
journal: usize,
|
||||
}
|
||||
|
||||
fn new_dict() -> DictRef {
|
||||
Rc::new(RefCell::new(Dict::new()))
|
||||
}
|
||||
|
|
@ -143,6 +156,8 @@ impl<'a> Interp<'a> {
|
|||
ops: 0,
|
||||
allocated: 0,
|
||||
seed: 1,
|
||||
saves: vec![],
|
||||
journal: vec![],
|
||||
fault: None,
|
||||
glyph_width: None,
|
||||
g,
|
||||
|
|
@ -190,6 +205,7 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
self.dicts.clear();
|
||||
self.stack.clear();
|
||||
self.journal.clear();
|
||||
}
|
||||
|
||||
// ---------- the operand stack ----------
|
||||
|
|
@ -879,7 +895,7 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
Obj::Dict(d) => {
|
||||
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
insert(&d, key, v)?;
|
||||
self.insert(&d, key, Some(v))?;
|
||||
true
|
||||
}
|
||||
_ => return ps_err("typecheck", ""),
|
||||
|
|
@ -968,7 +984,7 @@ impl<'a> Interp<'a> {
|
|||
let v = self.pop()?;
|
||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
||||
insert(&d, k, v)?;
|
||||
self.insert(&d, k, Some(v))?;
|
||||
}
|
||||
Load => {
|
||||
let k = self.pop()?;
|
||||
|
|
@ -981,7 +997,7 @@ impl<'a> Interp<'a> {
|
|||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
let d = self.dicts.iter().rev().find(|d| d.borrow().contains_key(&k)).or(self.dicts.last()).cloned();
|
||||
let d = d.ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
||||
insert(&d, k, v)?;
|
||||
self.insert(&d, k, Some(v))?;
|
||||
}
|
||||
Known => {
|
||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
|
|
@ -1001,7 +1017,8 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
Undef => {
|
||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
self.pop_dict()?.borrow_mut().remove(&k);
|
||||
let d = self.pop_dict()?;
|
||||
self.insert(&d, k, None)?;
|
||||
}
|
||||
CurrentDict => {
|
||||
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
||||
|
|
@ -1034,11 +1051,24 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
Save => {
|
||||
self.gsave()?;
|
||||
self.push(Obj::Save(self.saved.len()))?;
|
||||
self.push(Obj::Save(self.saves.len()))?;
|
||||
self.saves.push(SaveMark { gstates: self.saved.len(), journal: self.journal.len() });
|
||||
}
|
||||
Restore => {
|
||||
let Obj::Save(level) = self.pop()? else { return ps_err("typecheck", "") };
|
||||
while self.saved.len() >= level && !self.saved.is_empty() {
|
||||
let Obj::Save(i) = self.pop()? else { return ps_err("typecheck", "") };
|
||||
// A save restored already (or through an outer one) is no longer valid: §3.7.3,
|
||||
// `invalidrestore`.
|
||||
let mark = self.saves.get(i).copied().ok_or(PsError::Ps("invalidrestore", String::new()))?;
|
||||
self.saves.truncate(i);
|
||||
// The dictionaries as they were at the save, newest change undone first.
|
||||
for (d, k, old) in self.journal.drain(mark.journal..).rev() {
|
||||
let mut d = d.borrow_mut();
|
||||
match old {
|
||||
Some(v) => d.insert(k, v),
|
||||
None => d.remove(&k),
|
||||
};
|
||||
}
|
||||
while self.saved.len() >= mark.gstates && !self.saved.is_empty() {
|
||||
self.grestore();
|
||||
}
|
||||
}
|
||||
|
|
@ -1083,9 +1113,12 @@ impl<'a> Interp<'a> {
|
|||
self.push(Obj::Int(1 << 20))?;
|
||||
}
|
||||
}
|
||||
// An empty font cache of a device's sizes, in `cachestatus`'s order (PLRM 3rd ed.,
|
||||
// chapter 8): bytes used and most, fonts used and most, glyphs used and most, and the
|
||||
// most bytes one cached glyph may take (positive on a real device: programs divide by it).
|
||||
CacheStatus => {
|
||||
for _ in 0..7 {
|
||||
self.push(Obj::Int(0))?;
|
||||
for v in [0, 1 << 20, 0, 1000, 0, 10_000, 25_000] {
|
||||
self.push(Obj::Int(v))?;
|
||||
}
|
||||
}
|
||||
StartJob => {
|
||||
|
|
@ -1143,7 +1176,9 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
Obj::Dict(d) => {
|
||||
let from = self.pop_dict()?.borrow().clone();
|
||||
d.borrow_mut().extend(from);
|
||||
for (k, v) in from {
|
||||
self.insert(&d, k, Some(v))?;
|
||||
}
|
||||
self.push(Obj::Dict(d))?;
|
||||
}
|
||||
_ => return ps_err("typecheck", "copy"),
|
||||
|
|
@ -1219,6 +1254,26 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Put `v` under `k` in `d` (`None`: remove it), within [`MAX_ALLOC`] entries; while a `save`
|
||||
/// is open the old value is kept for `restore`.
|
||||
fn insert(&mut self, d: &DictRef, k: Key, v: Option<Obj>) -> Res {
|
||||
let old = {
|
||||
let mut dm = d.borrow_mut();
|
||||
if v.is_some() && dm.len() >= MAX_ALLOC && !dm.contains_key(&k) {
|
||||
return Err(PsError::Limit("a dictionary grew too large"));
|
||||
}
|
||||
match v {
|
||||
Some(v) => dm.insert(k.clone(), v),
|
||||
None => dm.remove(&k),
|
||||
}
|
||||
};
|
||||
if !self.saves.is_empty() {
|
||||
self.alloc(2 * std::mem::size_of::<Obj>())?;
|
||||
self.journal.push((d.clone(), k, old));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------- resources ----------
|
||||
|
||||
/// The resources of `category`.
|
||||
|
|
@ -1293,16 +1348,6 @@ impl<'a> Interp<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Put `v` under `k` in `d`, within [`MAX_ALLOC`] entries.
|
||||
fn insert(d: &DictRef, k: Key, v: Obj) -> Res {
|
||||
let mut d = d.borrow_mut();
|
||||
if d.len() >= MAX_ALLOC && !d.contains_key(&k) {
|
||||
return Err(PsError::Limit("a dictionary grew too large"));
|
||||
}
|
||||
d.insert(k, v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An array or string index.
|
||||
fn index(k: &Obj) -> Res<usize> {
|
||||
match *k {
|
||||
|
|
|
|||
|
|
@ -932,3 +932,30 @@ showpage"##;
|
|||
assert!(all(d).iter().any(|n| fill(n).and_then(Paint::color) == Some(Color::rgb(0.0, 1.0, 0.0))));
|
||||
assert_eq!(objects(d).len(), 3);
|
||||
}
|
||||
|
||||
/// `restore` puts local VM back as it was at the `save` (PLRM 3rd ed., §3.7.3 "Save and
|
||||
/// Restore"): dictionaries get back the entries they had, whatever changed them. A program that
|
||||
/// keeps a count of the saves it has open in a dictionary, and refuses to go past a depth, reads
|
||||
/// to its end only if each `restore` puts the count back (part of #505: files with many images,
|
||||
/// each drawn between `save` and `restore`).
|
||||
#[test]
|
||||
fn restore_puts_dictionaries_back() {
|
||||
let r = read(
|
||||
"/Depth 2 dict def Depth /open 0 put \
|
||||
/enter { save Depth /open get 1 add dup 8 gt { rangecheck } if Depth exch /open exch put } def \
|
||||
1 1 100 { pop enter 0 0 1 1 rectfill restore } for Depth /open get 0 eq { 10 10 5 5 rectfill } if",
|
||||
);
|
||||
clean(&r);
|
||||
assert_eq!(objects(&r.document).len(), 101);
|
||||
// `def`, `store`, `put`, `undef` and dictionary `copy` are undone, newest first.
|
||||
super::tests::check(
|
||||
"/a 1 def /d 2 dict def d /k 1 put save /a 2 def /b 3 def d /k 2 put d /k undef 1 dict dup /z 9 put d copy pop \
|
||||
/a 4 store restore a 1 eq /b where not and d /k get 1 eq and d /z known not and",
|
||||
);
|
||||
// A save restored (itself, or through an outer one) is no longer valid: `invalidrestore`.
|
||||
super::tests::check("save dup restore { restore } stopped");
|
||||
super::tests::check("save save exch restore { restore } stopped");
|
||||
// `cachestatus` gives a device's cache sizes; its last, the most bytes one cached glyph may
|
||||
// take, is positive (chapter 8), and programs divide by it.
|
||||
super::tests::check("cachestatus 7 1 roll 6 { pop } repeat 0 gt");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue