mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 04:14:26 +00:00
EPS import: charge scanned strings, end failed filter decodes, bound data sources and JPEG/TIFF headers
This commit is contained in:
@@ -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)) }
|
||||
}
|
||||
|
||||
@@ -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)?],
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user