EPS import: charge scanned strings, end failed filter decodes, bound data sources and JPEG/TIFF headers

This commit is contained in:
Dylan M. Taylor
2026-11-11 17:26:05 +00:00
parent 9fc43f2a35
commit e4c7688428
6 changed files with 117 additions and 1 deletions
+10
View File
@@ -131,6 +131,10 @@ fn lzw(raw: &[u8]) -> Res<(Vec<u8>, usize)> {
}
}
/// Most pixels a DCTDecode JPEG may have: twice the most an image is read with, so every image that
/// was read before is still read.
const MAX_DCT_PIXELS: u64 = 1 << 27;
/// A JPEG's pixels (gray, RGB or CMYK, 8 bits) with its channel count, and how many bytes of
/// `raw` it took. A CMYK JPEG gives its samples as stored, for the image's Decode array to read
/// (Adobe's CMYK JPEGs store them inverted, which a Decode array of `[1 0 1 0 1 0 1 0]` undoes).
@@ -143,6 +147,12 @@ fn dct(raw: &[u8]) -> Res<(Vec<u8>, usize, u8)> {
{
return Ok((inks.data.into_iter().map(|v| 255 - v).collect(), end, 4));
}
// The header's size is checked before the decoder allocates for it (a progressive JPEG keeps its
// coefficients besides its output).
let size = image::ImageReader::with_format(std::io::Cursor::new(jpeg), image::ImageFormat::Jpeg).into_dimensions();
if size.is_ok_and(|(w, h)| u64::from(w) * u64::from(h) > MAX_DCT_PIXELS) {
return Err(PsError::Limit("decoded data is too large"));
}
let img = image::load_from_memory_with_format(jpeg, image::ImageFormat::Jpeg).map_err(|_| PsError::Ps("ioerror", "DCTDecode".into()))?;
if img.color().channel_count() == 1 { Ok((img.to_luma8().into_raw(), end, 1)) } else { Ok((img.to_rgb8().into_raw(), end, 3)) }
}
+24
View File
@@ -7,6 +7,9 @@ use super::{
/// Most bytes one filter or image reads.
pub(super) const MAX_DATA: usize = 1 << 28;
/// Most data sources one image may name (a DeviceN space has up to 32 components).
const MAX_SOURCES: usize = 32;
/// The longest chain of filters over one pending file (real programs stack two or three).
const MAX_FILTER_CHAIN: usize = 64;
@@ -389,6 +392,25 @@ impl Interp<'_> {
/// 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 input = match &*f.borrow() {
Stream::Pending { from: Some(r), .. } => match &*r.borrow() {
Stream::Buf { data, pos, .. } => data.len().saturating_sub(*pos),
_ => 0,
},
Stream::Pending { from: None, .. } => self.lex.data().len(),
_ => return Ok(()),
};
let r = self.decode_pending(f);
// A decode that failed is over: the stream is empty from now on, and the work it did is
// charged, so a program that catches the error can't have it run again and again.
if r.is_err() && matches!(&*f.borrow(), Stream::Pending { .. }) {
*f.borrow_mut() = Stream::Buf { data: vec![], pos: 0, rgb: None };
self.spend_bytes(input)?;
}
r
}
fn decode_pending(&mut self, f: &Rc<RefCell<Stream>>) -> Res {
let (chain, from) = match &*f.borrow() {
Stream::Pending { chain, from } => (chain.clone(), from.clone()),
_ => return Ok(()),
@@ -663,6 +685,8 @@ impl Interp<'_> {
let multi = get("MultipleDataSources").is_some_and(|o| matches!(o, Obj::Bool(true)));
let sources = match (get("DataSource"), multi) {
(None, _) => vec![],
// An image has a data source for each of its components, so many more is no image: refused before any copy.
(Some(Obj::Array { items, exec: false }), true) if items.len() > MAX_SOURCES => return ps_err("rangecheck", "DataSource"),
(Some(Obj::Array { items, exec: false }), true) => items.borrow().iter().map(source).collect::<Res<Vec<_>>>()?,
(Some(src), _) => vec![source(&src)?],
};
+21 -1
View File
@@ -550,10 +550,30 @@ impl<'a> Interp<'a> {
/// An object as the scanner gives it: with `immediate`, its `//name`s replaced by their
/// values (down through procedures), as they are when read.
pub fn scanned(&self, o: Obj, immediate: bool) -> Res<Obj> {
pub fn scanned(&mut self, o: Obj, immediate: bool) -> Res<Obj> {
// Every scan builds a fresh string or procedure, so a loop that rescans one text is charged
// for each copy it keeps, not only for the text.
self.charge_scanned(&o, 0)?;
if immediate { self.resolve(o, 0) } else { Ok(o) }
}
/// Count the storage of the string or procedure the scanner just made against [`MAX_MEMORY`],
/// and the time to build it.
fn charge_scanned(&mut self, o: &Obj, depth: usize) -> Res {
match o {
Obj::Str(s) => {
self.alloc(s.len())?;
self.spend_bytes(s.len())
}
Obj::Array { items, .. } if depth < MAX_DEPTH as usize => {
self.alloc(items.len().saturating_mul(std::mem::size_of::<Obj>()))?;
self.spend_bytes(items.len())?;
items.borrow().iter().try_for_each(|i| self.charge_scanned(i, depth + 1))
}
_ => Ok(()),
}
}
fn resolve(&self, o: Obj, depth: usize) -> Res<Obj> {
match o {
Obj::Exec(n) if n.starts_with(IMMEDIATE) => {
+7
View File
@@ -28,7 +28,14 @@ pub(super) fn preview(bytes: &[u8], ps: &[u8], frame: Rect) -> Option<Preview> {
epsi(ps).and_then(|img| image_doc(img, frame)).map(|document| Preview { document, what: IMAGE })
}
/// Most pixels a TIFF preview may have: its RGBA copy is four bytes each besides the decoded image.
const MAX_PREVIEW_PIXELS: u64 = 1 << 26;
fn tiff_rgba(tiff: &[u8]) -> Option<image::RgbaImage> {
let size = image::ImageReader::with_format(std::io::Cursor::new(tiff), image::ImageFormat::Tiff).into_dimensions();
if size.is_ok_and(|(w, h)| u64::from(w) * u64::from(h) > MAX_PREVIEW_PIXELS) {
return None;
}
match image::load_from_memory_with_format(tiff, image::ImageFormat::Tiff) {
Ok(img) => Some(img.to_rgba8()),
Err(_) => crate::tiff::palette_rgba(tiff),
+37
View File
@@ -1588,3 +1588,40 @@ fn abutting_blocks_of_distinct_colours_stay_apart() {
let gaps: String = (0..10).map(|i| format!("{} setgray {} 10 2 80 rectfill ", f64::from(i) / 40.0, 10 + i * 3)).collect();
assert_eq!(objects(&read(&gaps).document).len(), 10);
}
/// A string a program scans again and again (`token`) makes a fresh copy each time, and the copies
/// count against the memory budget.
#[test]
fn rescanned_strings_are_charged() {
let body = "/s 1048576 string def s 0 40 put s 1048575 41 put\n1100 { s token pop pop pop } repeat\n";
let e = import(&eps(body)).unwrap_err();
assert!(e.contains("too much memory"), "{e}");
// A few scans stay far within it.
let r = import(&eps("/s 1048576 string def s 0 40 put s 1048575 41 put\n20 { s token pop pop pop } repeat\n")).unwrap();
assert!(!r.warnings.iter().any(|w| w.contains("too much memory")), "{:?}", r.warnings);
}
/// An image names no more data sources than a color space has components: thousands of references
/// to one string are refused before any is copied.
#[test]
fn an_image_with_thousands_of_data_sources_is_refused() {
let sources = "<80> ".repeat(2048);
let e = import(&eps(&format!(
"/DeviceCMYK setcolorspace << /ImageType 1 /Width 1 /Height 1 /BitsPerComponent 8 \
/MultipleDataSources true /ImageMatrix [1 0 0 1 0 0] /DataSource [{sources}] >> image"
)))
.unwrap_err();
assert!(e.contains("rangecheck") && e.contains("DataSource"), "{e}");
}
/// A filter over the program's own file that fails to decode is spent: a program that catches the
/// error and reads again gets the end of the file, not the same decode over again.
#[test]
fn a_failed_filter_decode_is_not_tried_again() {
let body = "/f currentfile /FlateDecode filter def\n/n 0 def 3 { { f read { pop } if } stopped { /n n 1 add def } if } repeat\n\
n 1 eq { 0 0 10 10 rectfill } if\n";
let r = import(&eps(body)).unwrap();
let mut paths = 0;
r.document.walk(|n| paths += usize::from(matches!(n.kind, NodeKind::Path { .. })));
assert_eq!(paths, 1, "{:?}", r.warnings);
}
+18
View File
@@ -548,3 +548,21 @@ fn the_data_of_an_image_too_large_to_read_is_skipped() {
assert_eq!(super::tests::fill(&o[0]), Some(&vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb(0.0, 0.0, 1.0))), "{program}");
}
}
/// A JPEG whose header claims far more pixels than an image may have is refused before the decoder
/// allocates for them.
#[test]
fn a_jpeg_with_a_huge_header_is_refused_before_it_is_decoded() {
let mut jpeg = vec![];
image::codecs::jpeg::JpegEncoder::new(&mut jpeg).encode(&[128; 64], 8, 8, image::ExtendedColorType::L8).unwrap();
// The frame header (marker, length, precision, height, width) now says 65535 × 65535.
let sof = jpeg.windows(2).position(|w| w == [0xFF, 0xC0]).unwrap();
jpeg[sof + 5..sof + 9].fill(0xFF);
let jpeg: String = jpeg.iter().map(|b| format!("{b:02x}")).collect();
let e = import(&eps(&format!(
"/DeviceGray setcolorspace << /ImageType 1 /Width 1 /Height 1 /BitsPerComponent 8 /ImageMatrix [1 0 0 1 0 0] \
/DataSource currentfile /ASCIIHexDecode filter /DCTDecode filter >> image\n{jpeg}>"
)))
.unwrap_err();
assert!(e.contains("too large"), "{e}");
}