diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index a56c952d..61845527 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -125,12 +125,19 @@ pub fn parse(data: &[u8]) -> Parsed { start.get_or_insert(at); operands.push(Object::Null); } - b"BI" => { - let (dict, img, end) = inline_image(data, lx.pos); - lx.pos = end; - out.ops.push(Op { op: b"BI".to_vec(), operands: Vec::new(), inline: Some((dict, img)), span: start.take().unwrap_or(at)..end }); - operands.clear(); - } + b"BI" => match inline_image(data, lx.pos) { + Ok((dict, img, end)) => { + lx.pos = end; + out.ops.push(Op { op: b"BI".to_vec(), operands: Vec::new(), inline: Some((dict, img)), span: start.take().unwrap_or(at)..end }); + operands.clear(); + } + Err(end) => { + out.skipped += end.saturating_sub(at); + lx.pos = end; + start = None; + operands.clear(); + } + }, _ => { let s = start.take().unwrap_or(at); out.ops.push(Op { op: kw.to_vec(), operands: std::mem::take(&mut operands), inline: None, span: s..lx.pos }); @@ -141,8 +148,9 @@ pub fn parse(data: &[u8]) -> Parsed { out } -/// Read an inline image starting after `BI`: (parameters, data, position after `EI`). -fn inline_image(data: &[u8], pos: usize) -> (Dict, Vec, usize) { +/// Read an inline image starting after `BI`: (parameters, data, position after `EI`). `Err` holds +/// the position after the `EI` of an image whose parameters can't be read. +fn inline_image(data: &[u8], pos: usize) -> Result<(Dict, Vec, usize), usize> { let mut lx = Lexer::new(data, pos); let mut dict = Dict::new(); loop { @@ -150,17 +158,31 @@ fn inline_image(data: &[u8], pos: usize) -> (Dict, Vec, usize) { if lx.eat_keyword(b"ID") { break; } - let Ok(Object::Name(k)) = lx.object() else { - return (dict, Vec::new(), data.len()); + let at = lx.pos; + let entry = match lx.object() { + Ok(Object::Name(k)) => lx.object().ok().map(|v| (k, v)), + _ => None, }; - let Ok(v) = lx.object() else { - return (dict, Vec::new(), data.len()); + let Some((k, v)) = entry else { + // Without an `EI` after it, the image runs to the end of the stream, as before. + return match end_of_image(data, at) { + Some((_, end)) => Err(end), + None => Ok((dict, Vec::new(), data.len())), + }; }; dict.set(k, v); } // One whitespace byte separates ID from the data. let begin = (lx.pos + 1).min(data.len()); - // The data ends at whitespace + `EI` + (whitespace or end). Search for that pattern. + Ok(match end_of_image(data, begin) { + Some((end_data, end)) => (dict, data[begin..end_data].to_vec(), end), + None => (dict, data[begin..].to_vec(), data.len()), + }) +} + +/// Where inline image data starting at `begin` ends: (end of the data, position after `EI`). The +/// data ends at whitespace + `EI` + (whitespace, a delimiter or the end). +fn end_of_image(data: &[u8], begin: usize) -> Option<(usize, usize)> { let mut i = begin; while i + 2 <= data.len() { if data[i] == b'E' @@ -169,11 +191,11 @@ fn inline_image(data: &[u8], pos: usize) -> (Dict, Vec, usize) { && data.get(i + 2).is_none_or(|b| is_ws(*b) || is_delim(*b)) { let end_data = if i > begin && is_ws(data[i - 1]) { i - 1 } else { i }; - return (dict, data[begin..end_data].to_vec(), i + 2); + return Some((end_data, i + 2)); } i += 1; } - (dict, data[begin..].to_vec(), data.len()) + None } /// Write one operator (with a trailing newline). diff --git a/crates/content/src/tests.rs b/crates/content/src/tests.rs index e3c6d8d3..858c0e11 100644 --- a/crates/content/src/tests.rs +++ b/crates/content/src/tests.rs @@ -48,6 +48,20 @@ fn damage_is_skipped_not_fatal() { assert!(parse(b"BI /W 1").ops.len() == 1); } +/// Issue #1318: an inline image whose dictionary can't be read is skipped up to its `EI`, counted, +/// and what follows it is parsed. +#[test] +fn an_unreadable_inline_image_is_skipped_to_its_end() { + let src = b"q BI /W 4 H 4 ID aa EI Q BT (After) Tj ET"; + let p = parse(src); + let names: Vec<&[u8]> = p.ops.iter().map(|o| o.op.as_slice()).collect(); + assert_eq!(names, [&b"q"[..], b"Q", b"BT", b"Tj", b"ET"]); + assert_eq!(p.skipped, b"BI /W 4 H 4 ID aa EI".len()); + let control = parse(b"q BI /W 4 /H 4 ID aa EI Q BT (After) Tj ET"); + assert_eq!(control.skipped, 0); + assert_eq!(control.ops.len(), 6); +} + #[test] fn matrices_compose_and_invert() { let a = Matrix([2.0, 0.0, 0.0, 3.0, 10.0, 20.0]);