Files
pdfcraft/vendor/hayro-postscript/src/object.rs
T
aiongg fc1372a64c fix(render): vendor hayro-postscript so a run of << in a CMap can't overflow the stack (#4363)
hayro-cmap reads CMap streams with hayro-postscript's token scanner,
whose object::read skipped each `<<` and `>>` by calling itself again.
A CMap holding a long run of `<` or `>` used one stack frame per pair,
overflowed the stack and aborted the process.

Vendor hayro-postscript 0.1.0 with read() looping over the delimiters
instead (from storytold/craft-libs#263), patched in beside hayro-cmap.
Four object::tests cover long `<<` and `>>` runs and the skipped
delimiters, plain and nested.
2026-10-24 09:38:10 -04:00

227 lines
6.4 KiB
Rust

use crate::array::{self, Array};
use crate::error::{Error, Result};
use crate::name::{self, Name};
use crate::number::{self, Number};
use crate::reader::Reader;
use crate::string::{self, String};
/// A PostScript object.
#[derive(Debug, Clone, PartialEq)]
pub enum Object<'a> {
/// A number object.
Number(Number),
/// A name object.
Name(Name<'a>),
/// A string object.
String(String<'a>),
/// An array object.
Array(Array<'a>),
}
pub(crate) fn read<'a>(r: &mut Reader<'a>) -> Result<Object<'a>> {
// PdfCraft patch: `<<` and `>>` are skipped by looping instead of
// recursing once per delimiter, so a long run cannot overflow the stack.
loop {
skip_whitespace_and_comments(r);
let b = r.peek_byte().ok_or(Error::SyntaxError)?;
return match b {
b'(' => string::parse_literal(r)
.map(|s| Object::String(String::from_literal(s)))
.ok_or(Error::SyntaxError),
b'<' => {
if r.peek_bytes(2) == Some(b"<~") {
string::parse_ascii85(r)
.map(|s| Object::String(String::from_ascii85(s)))
.ok_or(Error::SyntaxError)
} else if r.peek_bytes(2) == Some(b"<<") {
// TODO: Proper dict support. For now, skip `<<` and return
// the next object so callers see the inner tokens.
r.forward();
r.forward();
continue;
} else {
string::parse_hex(r)
.map(|s| Object::String(String::from_hex(s)))
.ok_or(Error::SyntaxError)
}
}
b'/' => name::parse_literal(r)
.map(|s| Object::Name(Name::new(s, true)))
.ok_or(Error::SyntaxError),
b'[' => array::parse(r).map(|d| Object::Array(Array::new(d))),
b'>' => {
if r.peek_bytes(2) == Some(b">>") {
// TODO: Proper dict support. Skip `>>` closing delimiter.
r.forward();
r.forward();
continue;
} else {
Err(Error::SyntaxError)
}
}
b'{' => {
r.forward();
Err(Error::UnsupportedType)
}
b'.' | b'+' | b'-' | b'0'..=b'9' => number::read(r).map(Object::Number),
_ => name::parse_executable(r)
.map(|s| Object::Name(Name::new(s, false)))
.ok_or(Error::SyntaxError),
};
}
}
pub(crate) fn at_end(r: &mut Reader<'_>) -> bool {
skip_whitespace_and_comments(r);
r.peek_byte().is_none()
}
pub(crate) fn skip_whitespace_and_comments(r: &mut Reader<'_>) {
loop {
match r.peek_byte() {
Some(b) if crate::reader::is_whitespace(b) => {
r.forward();
}
Some(b'%') => {
r.forward();
r.forward_while(|b| !crate::reader::is_eol(b));
}
_ => return,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn read_one(input: &[u8]) -> Result<Object<'_>> {
let mut r = Reader::new(input);
read(&mut r)
}
fn read_ok(input: &[u8]) -> Object<'_> {
read_one(input).unwrap()
}
fn read_err(input: &[u8]) -> Error {
read_one(input).unwrap_err()
}
#[test]
fn integer() {
assert_eq!(read_ok(b"42 "), Object::Number(Number::Integer(42)));
}
#[test]
fn negative_integer() {
assert_eq!(read_ok(b"-7 "), Object::Number(Number::Integer(-7)));
}
#[test]
fn literal_name() {
assert_eq!(
read_ok(b"/CMapName "),
Object::Name(Name::new(b"CMapName", true))
);
}
#[test]
fn executable_name() {
let obj = read_ok(b"beginbfchar ");
assert_eq!(obj, Object::Name(Name::new(b"beginbfchar", false)));
}
#[test]
fn literal_string() {
assert_eq!(
read_ok(b"(Hello)"),
Object::String(String::from_literal(b"Hello"))
);
}
#[test]
fn hex_string() {
assert_eq!(
read_ok(b"<48656C6C6F>"),
Object::String(String::from_hex(b"48656C6C6F"))
);
}
#[test]
fn ascii85_string() {
assert_eq!(
read_ok(b"<~87cURDZ~>"),
Object::String(String::from_ascii85(b"87cURDZ"))
);
}
#[test]
fn array_simple() {
let obj = read_ok(b"[1 2 3]");
assert_eq!(obj, Object::Array(Array::new(b"1 2 3")));
}
#[test]
fn stray_close_bracket() {
assert_eq!(read_err(b"]"), Error::SyntaxError);
}
#[test]
fn stray_gt() {
assert_eq!(read_err(b">x"), Error::SyntaxError);
}
#[test]
fn skips_whitespace() {
assert_eq!(read_ok(b" \t\n 42 "), Object::Number(Number::Integer(42)));
}
#[test]
fn skips_comments() {
assert_eq!(
read_ok(b"% this is a comment\n42 "),
Object::Number(Number::Integer(42))
);
}
#[test]
fn eof() {
assert!(read_one(b"").is_err());
assert!(read_one(b" ").is_err());
assert!(read_one(b"% comment only\n").is_err());
}
// PdfCraft patch: `<<` and `>>` runs are skipped without recursion.
#[test]
fn long_open_dict_run_does_not_overflow() {
let input = alloc::vec![b'<'; 1_000_000];
assert_eq!(read_err(&input), Error::SyntaxError);
}
#[test]
fn long_close_dict_run_does_not_overflow() {
let input = b">>".repeat(500_000);
assert_eq!(read_err(&input), Error::SyntaxError);
}
#[test]
fn dict_delimiters_are_skipped() {
let mut r = Reader::new(b"<< /A 1 >>");
assert_eq!(read(&mut r), Ok(Object::Name(Name::new(b"A", true))));
assert_eq!(read(&mut r), Ok(Object::Number(Number::Integer(1))));
assert_eq!(read(&mut r), Err(Error::SyntaxError));
}
#[test]
fn nested_dict_delimiters_are_skipped() {
let mut r = Reader::new(b"<<<< /X >>>> 7 <<\n% c\n<< >> 8");
assert_eq!(read(&mut r), Ok(Object::Name(Name::new(b"X", true))));
assert_eq!(read(&mut r), Ok(Object::Number(Number::Integer(7))));
assert_eq!(read(&mut r), Ok(Object::Number(Number::Integer(8))));
assert!(at_end(&mut r));
}
}