diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index 746514e4d..6028bb422 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -1077,6 +1077,7 @@ impl<'a> Reader<'a> { /// `kids` with the clipping paths among them (indexes `clips`) first, several as one compound /// path → the children and whether they clip. fn clip_first(&mut self, kids: Vec, clips: &[usize]) -> (Vec>, bool) { + let clips: std::collections::HashSet = clips.iter().copied().collect(); let (mut clip, rest): (Vec, Vec) = kids.into_iter().enumerate().partition(|(i, _)| clips.contains(i)); let clip_node = match clip.len() { 0 => None, diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index fc8c9de37..744431fad 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -381,7 +381,13 @@ impl<'a> Interp<'a> { None => Ok(()), }, Obj::ExecStr(s) => self.run_program(&s.to_vec()), - Obj::Op(op) => self.op(op), + // Through the depth guard: `stopped` and the like call back into here. + Obj::Op(op) => { + self.enter()?; + let r = self.op(op); + self.depth -= 1; + r + } Obj::Exec(name) => { self.enter()?; let r = match self.lookup(&name) { @@ -819,6 +825,8 @@ impl<'a> Interp<'a> { } Cvn => { let s = self.pop_str()?; + // 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 = Rc::from(String::from_utf8_lossy(&s.borrow()).as_ref()); self.push(Obj::Name(n))?; } @@ -1055,7 +1063,7 @@ impl<'a> Interp<'a> { Bind => { let o = self.pop()?; if let Obj::Array { items, .. } = &o { - self.bind(items, 0); + self.bind(items, 0)?; } self.push(o)?; } @@ -1171,7 +1179,11 @@ impl<'a> Interp<'a> { } } Obj::Array { items, exec } => { - let from = self.pop_array()?.to_vec(); + let source = self.pop_array()?; + // Its elements are read and written: work in proportion, and a copy held meanwhile. + self.spend(source.len() as u64)?; + self.alloc(source.len().saturating_mul(std::mem::size_of::()))?; + let from = source.to_vec(); if !items.write(0, &from) { return ps_err("rangecheck", "copy"); } @@ -1246,11 +1258,13 @@ impl<'a> Interp<'a> { /// Replace the names in a procedure that stand for operators by the operators, down through /// nested procedures. - fn bind(&mut self, items: &Shared, depth: u32) { + fn bind(&mut self, items: &Shared, depth: u32) -> Res { if depth > 64 { - return; + return Ok(()); } for i in 0..items.len() { + // Procedures that hold one another twice would be visited exponentially often. + self.tick()?; match items.get(i) { Some(Obj::Exec(name)) => { if let Some(o @ Obj::Op(_)) = self.lookup(&name) { @@ -1258,10 +1272,11 @@ impl<'a> Interp<'a> { items.write(i, &[o]); } } - Some(Obj::Array { items: inner, exec: true }) if !inner.shares(items) => self.bind(&inner, depth + 1), + Some(Obj::Array { items: inner, exec: true }) if !inner.shares(items) => self.bind(&inner, depth + 1)?, _ => {} } } + Ok(()) } /// Put `v` under `k` in `d` (`None`: remove it), within [`MAX_ALLOC`] entries; while a `save` diff --git a/crates/eps/src/import/preview.rs b/crates/eps/src/import/preview.rs index 0e1c09750..9a3f5b751 100644 --- a/crates/eps/src/import/preview.rs +++ b/crates/eps/src/import/preview.rs @@ -99,7 +99,16 @@ fn epsi(ps: &[u8]) -> Option { } hex.extend(l.strip_prefix('%')?.bytes().filter(u8::is_ascii_hexdigit)); } + hex.truncate(row * h * 2); + // A trailing half byte would decode padded with zero: the preview is cut short, so none. + if hex.len() % 2 != 0 { + return None; + } let bytes = super::lex::hex_decode(&hex)?; + // A preview with its samples missing is none, not a blank image of the size it claims. + if bytes.len() < row * h { + return None; + } let max = (1u32 << depth) - 1; let mut img = image::RgbaImage::new(w as u32, h as u32); for (x, y, px) in img.enumerate_pixels_mut() { diff --git a/crates/eps/src/print/mod.rs b/crates/eps/src/print/mod.rs index 6fe7c657a..f9df227dc 100644 --- a/crates/eps/src/print/mod.rs +++ b/crates/eps/src/print/mod.rs @@ -75,7 +75,8 @@ fn sheet(p: &PrintPage, job: &PrintJob) -> Result<(String, Vec), String> { let (wi, hi) = (w.ceil() as i64, h.ceil() as i64); let _ = writeln!(s, "%%PageBoundingBox: 0 0 {wi} {hi}"); if let Some((name, _, _)) = p.ink { - let _ = writeln!(s, "%%PlateColor: {name}"); + // A line break would end the comment and the rest would run as PostScript. + let _ = writeln!(s, "%%PlateColor: {}", name.replace(['\r', '\n'], " ")); } s.push_str("%%BeginPageSetup\n"); let _ = writeln!(s, "/setpagedevice where {{pop << /PageSize [{} {}] >> setpagedevice}} if", ps::num(w), ps::num(h)); diff --git a/crates/eps/src/scene.rs b/crates/eps/src/scene.rs index 78cb4588f..83d3e7e01 100644 --- a/crates/eps/src/scene.rs +++ b/crates/eps/src/scene.rs @@ -39,6 +39,10 @@ const UNDECODED: &str = "an image couldn't be decoded and is missing from the pi const LARGE_IMAGE: &str = "an image that is too large for EPS was left out"; const SPOT_GRADIENTS: &str = "gradients of spot colours are written in process colours"; const MISSING_PATTERN: &str = "missing patterns are written as mid-grey"; +const TOO_LARGE: &str = "the artwork is too large for EPS: what follows 256 MB of PostScript was left out"; +/// The most PostScript the page body may grow to: symbols and patterns that hold one another +/// expand into every instance, so nesting depth alone doesn't bound the output. +const MAX_BODY: usize = 256 << 20; /// What the walk wrote: definitions for the setup, the page, the spot colours it uses (name and /// CMYK equivalent) and the warnings. @@ -264,12 +268,17 @@ impl<'a> Scene<'a> { if let NodeKind::Layer { template: true, .. } = n.kind { return; } + if self.body.len() >= MAX_BODY { + self.warn(TOO_LARGE); + return; + } match n.visual_bounds_with(&self.symbols) { Some(b) if !force && !overlaps(b.inflate(1.0, 1.0), self.o.region) => return, None if !n.is_container() => return, _ => {} } self.note_losses(n); + let was = self.body.len(); match &n.kind { NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => { children.iter().for_each(|c| self.node(c, false)); @@ -313,6 +322,10 @@ impl<'a> Scene<'a> { } } } + if self.body.len() > MAX_BODY { + self.body.truncate(was); + self.warn(TOO_LARGE); + } } /// A clipping group: the art inside its first child's outline. The clipping path's fills @@ -891,6 +904,10 @@ impl<'a> Scene<'a> { if !invertible(place) { return; } + if self.body.len().saturating_add(encoded.len()).saturating_add(64) > MAX_BODY { + self.warn(TOO_LARGE); + return; + } self.line("q"); let _ = writeln!(self.body, "{} cm", ps::matrix(place)); self.body.push_str(encoded);