Charge PostScript filter output to the EPS interpreter budget

Fixes #3567

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-24 01:43:54 +07:00
committed by bflatastic
co-authored by Claude Haiku 5.5
parent 3867e87036
commit 0b6097e572
2 changed files with 27 additions and 2 deletions
+11 -2
View File
@@ -731,6 +731,12 @@ impl Interp<'_> {
self.push_interval(Obj::Str(s), 0, got.len())
}
/// Count `n` decoded bytes against the memory budget, and the time to decode them.
fn charge_decoded(&mut self, n: usize) -> Res {
self.alloc(n)?;
self.spend((n / 4096) as u64)
}
/// `filter`: a file that decodes its source. Over the program's own data it decodes when
/// first read (the data follows the operator that reads it); over other data, at once.
fn filter(&mut self) -> Res {
@@ -775,6 +781,7 @@ impl Interp<'_> {
let mut b = f.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())?;
self.charge_decoded(out.len())?;
*pos = pos.saturating_add(used);
Stream::Buf { data: out, pos: 0, rgb }
}
@@ -783,11 +790,13 @@ impl Interp<'_> {
Obj::Str(s) => {
self.alloc(s.borrow().len())?;
let (data, _, rgb) = decode(&spec, &s.borrow())?;
self.charge_decoded(data.len())?;
Stream::Buf { data, pos: 0, rgb }
}
p @ Obj::Array { .. } => {
let raw = self.gather(&p, MAX_DATA)?;
let (data, _, rgb) = decode(&spec, &raw)?;
self.charge_decoded(data.len())?;
Stream::Buf { data, pos: 0, rgb }
}
_ => return ps_err("typecheck", "filter"),
@@ -830,13 +839,13 @@ impl Interp<'_> {
None => {
let raw = self.lex.data();
let (data, used, rgb) = decode(spec, raw)?;
self.alloc(data.len())?;
self.charge_decoded(data.len())?;
self.lex.advance(used);
(data, rgb)
}
Some((prev, was)) => {
let (data, _, rgb) = decode(spec, &prev)?;
self.alloc(data.len())?;
self.charge_decoded(data.len())?;
(data, rgb.or(was))
}
});
+16
View File
@@ -1103,3 +1103,19 @@ fn restore_puts_dictionaries_back() {
// take, is positive (chapter 8), and programs divide by it.
super::tests::check("cachestatus 7 1 roll 6 { pop } repeat 0 gt");
}
/// #3567: what a `filter` decodes counts against the memory budget, not just its input. A
/// small string that inflates to 16 MiB, decoded over and over (each result dropped), ends
/// the program; a single one does not.
#[test]
fn filter_output_counts_against_the_memory_budget() {
fn hex(data: &[u8]) -> String {
data.iter().map(|b| format!("{b:02x}")).collect()
}
let z = hex(&deflate(&vec![0u8; 16 << 20]));
let once = import(&file("t", &format!("/z <{z}> def z /FlateDecode filter pop 0 0 1 1 rectfill"))).unwrap();
assert!(once.warnings.iter().all(|w| !w.contains("memory")), "{:?}", once.warnings);
let many = import(&file("t", &format!("/z <{z}> def 100 {{ z /FlateDecode filter pop }} repeat 0 0 1 1 rectfill")));
let Err(err) = many else { panic!("the budget ends the program") };
assert!(err.contains("too much memory"), "{err}");
}