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.
This commit is contained in:
aiongg
2026-10-24 09:38:10 -04:00
committed by GitHub
parent b6b0bbab08
commit fc1372a64c
21 changed files with 1966 additions and 2 deletions
+127
View File
@@ -0,0 +1,127 @@
use crate::error::{Error, Result};
use crate::object;
use crate::reader::Reader;
use crate::string;
/// A PostScript array object.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Array<'a> {
data: &'a [u8],
}
impl<'a> Array<'a> {
pub(crate) fn new(data: &'a [u8]) -> Self {
Self { data }
}
/// Return a [`Scanner`](crate::Scanner) that iterates over the objects inside
/// this array.
pub fn objects(&self) -> crate::Scanner<'a> {
crate::Scanner::new(self.data)
}
}
pub(crate) fn parse<'a>(r: &mut Reader<'a>) -> Result<&'a [u8]> {
r.forward_tag(b"[").ok_or(Error::SyntaxError)?;
let start = r.offset();
skip_array(r)?;
let end = r.offset() - 1;
r.range(start..end).ok_or(Error::SyntaxError)
}
fn skip_array(r: &mut Reader<'_>) -> Result<()> {
let mut depth = 1_u32;
while depth > 0 {
match r.peek_byte().ok_or(Error::SyntaxError)? {
b'[' => {
r.forward();
depth += 1;
}
b']' => {
r.forward();
depth -= 1;
}
b'(' => {
let _ = string::parse_literal(r).ok_or(Error::SyntaxError)?;
}
b'<' => {
if r.peek_bytes(2) == Some(b"<~") {
let _ = string::parse_ascii85(r).ok_or(Error::SyntaxError)?;
} else if r.peek_bytes(2) == Some(b"<<") {
r.forward();
r.forward();
} else {
let _ = string::parse_hex(r).ok_or(Error::SyntaxError)?;
}
}
b'%' => object::skip_whitespace_and_comments(r),
_ => {
r.forward();
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_array(input: &[u8]) -> Result<&[u8]> {
let mut r = Reader::new(input);
parse(&mut r)
}
#[test]
fn empty() {
assert_eq!(parse_array(b"[]").unwrap(), b"");
}
#[test]
fn simple() {
assert_eq!(parse_array(b"[1 2 3]").unwrap(), b"1 2 3");
}
#[test]
fn nested() {
assert_eq!(parse_array(b"[1 [2 3] 4]").unwrap(), b"1 [2 3] 4");
}
#[test]
fn with_string() {
// The ']' inside the string should not close the array.
assert_eq!(parse_array(b"[1 (str]) 2]").unwrap(), b"1 (str]) 2");
}
#[test]
fn with_hex_string() {
assert_eq!(parse_array(b"[<48> /name]").unwrap(), b"<48> /name");
}
#[test]
fn with_ascii85_string() {
assert_eq!(parse_array(b"[<~87cURDZ~> 1]").unwrap(), b"<~87cURDZ~> 1");
}
#[test]
fn with_comment() {
assert_eq!(
parse_array(b"[1 % comment with ]\n2]").unwrap(),
b"1 % comment with ]\n2"
);
}
#[test]
fn unterminated() {
assert_eq!(parse_array(b"[1 2"), Err(Error::SyntaxError));
}
#[test]
fn not_an_array() {
assert_eq!(parse_array(b"1 2]"), Err(Error::SyntaxError));
}
}
+30
View File
@@ -0,0 +1,30 @@
//! Error types for the PostScript scanner.
use core::fmt;
/// A specialized [`Result`] type for PostScript scanner operations.
pub type Result<T> = core::result::Result<T, Error>;
/// An error encountered while scanning a PostScript token stream.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
/// A syntax error in the input.
SyntaxError,
/// A numeric value exceeded implementation limits.
LimitCheck,
/// An unsupported PostScript type was encountered (like dictionaries or
/// procedures, which will be added in the future).
UnsupportedType,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SyntaxError => f.write_str("syntaxerror"),
Self::LimitCheck => f.write_str("limitcheck"),
Self::UnsupportedType => f.write_str("unsupported type"),
}
}
}
impl core::error::Error for Error {}
+179
View File
@@ -0,0 +1,179 @@
/*!
A lightweight PostScript scanner.
This crate provides a scanner for tokenizing PostScript programs into typed objects.
It currently only implements a very small subset of the PostScript language,
with the main goal of being enough to parse CMAP files, but the scope _might_
be expanded upon in the future.
The supported types include integers and real numbers, name objects, strings and arrays.
Unsupported is anything else, including dictionaries, procedures, etc. An error
will be returned in case any of these is encountered.
## Safety
This crate forbids unsafe code via a crate-level attribute.
*/
#![no_std]
#![forbid(unsafe_code)]
#![allow(missing_docs)]
extern crate alloc;
mod array;
mod error;
mod name;
mod number;
mod object;
mod reader;
mod string;
pub use array::Array;
pub use error::{Error, Result};
pub use name::Name;
pub use number::Number;
pub use object::Object;
pub use string::String;
use reader::Reader;
/// A PostScript scanner that parses [`Object`]s from a byte stream.
pub struct Scanner<'a> {
reader: Reader<'a>,
}
impl<'a> Scanner<'a> {
/// Create a new scanner over the given bytes of a PostScript program.
pub fn new(data: &'a [u8]) -> Self {
Self {
reader: Reader::new(data),
}
}
/// Returns `true` if there are no more objects to parse.
pub fn at_end(&mut self) -> bool {
object::at_end(&mut self.reader)
}
/// Parse the next object.
pub fn parse_object(&mut self) -> Result<Object<'a>> {
object::read(&mut self.reader)
}
/// Parse the next object as a [`Number`].
pub fn parse_number(&mut self) -> Result<Number> {
match self.parse_object()? {
Object::Number(n) => Ok(n),
_ => Err(Error::SyntaxError),
}
}
/// Parse the next object as a [`Name`].
pub fn parse_name(&mut self) -> Result<Name<'a>> {
match self.parse_object()? {
Object::Name(n) => Ok(n),
_ => Err(Error::SyntaxError),
}
}
/// Parse the next object as a [`String`].
pub fn parse_string(&mut self) -> Result<String<'a>> {
match self.parse_object()? {
Object::String(s) => Ok(s),
_ => Err(Error::SyntaxError),
}
}
/// Parse the next object as an [`Array`].
pub fn parse_array(&mut self) -> Result<Array<'a>> {
match self.parse_object()? {
Object::Array(a) => Ok(a),
_ => Err(Error::SyntaxError),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cmap_snippet() {
let input = br#"/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CMapName /Test-H def
1 begincodespacerange
<00> <FF>
endcodespacerange
2 beginbfchar
<03> <0041>
<04> <0042>
endbfchar
endcmap"#;
let mut s = Scanner::new(input);
assert_eq!(s.parse_name().unwrap(), Name::new(b"CIDInit", true));
assert_eq!(s.parse_name().unwrap(), Name::new(b"ProcSet", true));
assert_eq!(s.parse_name().unwrap(), Name::new(b"findresource", false));
assert_eq!(s.parse_name().unwrap(), Name::new(b"begin", false));
assert_eq!(s.parse_number().unwrap(), Number::Integer(12));
assert_eq!(s.parse_name().unwrap(), Name::new(b"dict", false));
assert_eq!(s.parse_name().unwrap(), Name::new(b"begin", false));
assert_eq!(s.parse_name().unwrap(), Name::new(b"begincmap", false));
assert_eq!(s.parse_name().unwrap(), Name::new(b"CMapName", true));
assert_eq!(s.parse_name().unwrap(), Name::new(b"Test-H", true));
assert_eq!(s.parse_name().unwrap(), Name::new(b"def", false));
assert_eq!(s.parse_number().unwrap(), Number::Integer(1));
assert_eq!(
s.parse_name().unwrap(),
Name::new(b"begincodespacerange", false)
);
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"00"));
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"FF"));
assert_eq!(
s.parse_name().unwrap(),
Name::new(b"endcodespacerange", false)
);
assert_eq!(s.parse_number().unwrap(), Number::Integer(2));
assert_eq!(s.parse_name().unwrap(), Name::new(b"beginbfchar", false));
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"03"));
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"0041"));
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"04"));
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"0042"));
assert_eq!(s.parse_name().unwrap(), Name::new(b"endbfchar", false));
assert_eq!(s.parse_name().unwrap(), Name::new(b"endcmap", false));
assert!(s.at_end());
}
#[test]
fn array_round_trip() {
let input = b"[123 /abc (xyz)]";
let mut scanner = Scanner::new(input);
let arr = scanner.parse_array().unwrap();
assert!(scanner.at_end());
let mut inner = arr.objects();
assert_eq!(inner.parse_number().unwrap(), Number::Integer(123));
assert_eq!(inner.parse_name().unwrap(), Name::new(b"abc", true));
assert_eq!(inner.parse_string().unwrap(), String::from_literal(b"xyz"));
assert!(inner.at_end());
}
#[test]
fn comments_skipped() {
let input = b"% comment\n42 % another\n/Name";
let mut scanner = Scanner::new(input);
assert_eq!(scanner.parse_number().unwrap(), Number::Integer(42));
assert_eq!(scanner.parse_name().unwrap(), Name::new(b"Name", true));
assert!(scanner.at_end());
}
#[test]
fn wrong_type_is_error() {
let mut scanner = Scanner::new(b"42 ");
assert!(scanner.parse_name().is_err());
}
}
+191
View File
@@ -0,0 +1,191 @@
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::reader::{Reader, is_regular};
use crate::string::ascii_hex::decode_hex_digit;
/// A PostScript name object.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Name<'a> {
data: &'a [u8],
literal: bool,
}
impl<'a> Name<'a> {
pub(crate) fn new(data: &'a [u8], literal: bool) -> Self {
Self { data, literal }
}
/// Returns `true` if this is a literal name.
pub fn is_literal(&self) -> bool {
self.literal
}
/// Returns the name as a string if it is valid UTF-8, or
/// `None` if it contains non-ASCII bytes.
pub fn as_str(&self) -> Option<&'a str> {
core::str::from_utf8(self.data).ok()
}
/// Decode the name into `out`, replacing any previous contents.
pub fn decode_into(&self, out: &mut Vec<u8>) -> Result<()> {
out.clear();
// Fast path: no escape sequences.
if !self.data.contains(&b'#') {
out.extend_from_slice(self.data);
return Ok(());
}
// Slow path: Process escape sequences.
let mut inner = Reader::new(self.data);
while let Some(b) = inner.read_byte() {
if b == b'#' {
let hex = inner.read_bytes(2).ok_or(Error::SyntaxError)?;
let hi = decode_hex_digit(hex[0]).ok_or(Error::SyntaxError)?;
let lo = decode_hex_digit(hex[1]).ok_or(Error::SyntaxError)?;
out.push(hi << 4 | lo);
} else {
out.push(b);
}
}
Ok(())
}
/// Decode the name.
pub fn decode(&self) -> Result<Vec<u8>> {
let mut out = Vec::new();
self.decode_into(&mut out)?;
Ok(out)
}
}
pub(crate) fn parse_literal<'a>(r: &mut Reader<'a>) -> Option<&'a [u8]> {
r.forward_tag(b"/")?;
let start = r.offset();
while r.eat(is_regular).is_some() {}
r.range(start..r.offset())
}
pub(crate) fn parse_executable<'a>(r: &mut Reader<'a>) -> Option<&'a [u8]> {
let start = r.offset();
r.forward_while(is_regular);
if r.offset() == start {
return None;
}
r.range(start..r.offset())
}
#[cfg(test)]
mod tests {
use super::*;
fn read_literal(input: &[u8]) -> Option<Name<'_>> {
let mut r = Reader::new(input);
parse_literal(&mut r).map(|d| Name::new(d, true))
}
fn read_executable(input: &[u8]) -> Option<Name<'_>> {
let mut r = Reader::new(input);
parse_executable(&mut r).map(|d| Name::new(d, false))
}
#[test]
fn literal_simple() {
let n = read_literal(b"/Name1").unwrap();
assert_eq!(n.as_str().unwrap(), "Name1");
assert!(n.is_literal());
}
#[test]
fn literal_empty_name() {
let n = read_literal(b"/").unwrap();
assert_eq!(n.as_str().unwrap(), "");
assert!(n.is_literal());
}
#[test]
fn literal_with_hex_escape() {
let n = read_literal(b"/lime#20Green").unwrap();
assert_eq!(n.as_str().unwrap(), "lime#20Green");
assert_eq!(n.decode().unwrap(), b"lime Green");
}
#[test]
fn literal_multiple_hex_escapes() {
let n = read_literal(b"/paired#28#29parentheses").unwrap();
assert_eq!(n.decode().unwrap(), b"paired()parentheses");
}
#[test]
fn literal_special_chars() {
let n = read_literal(b"/A;Name_With-Various***Characters?").unwrap();
assert_eq!(n.as_str().unwrap(), "A;Name_With-Various***Characters?");
}
#[test]
fn literal_stops_at_delimiter() {
let mut r = Reader::new(b"/Name(rest");
let data = parse_literal(&mut r).unwrap();
assert_eq!(data, b"Name");
assert_eq!(r.peek_byte(), Some(b'('));
}
#[test]
fn literal_stops_at_whitespace() {
let mut r = Reader::new(b"/Name rest");
let data = parse_literal(&mut r).unwrap();
assert_eq!(data, b"Name");
assert_eq!(r.peek_byte(), Some(b' '));
}
#[test]
fn literal_not_a_name() {
assert!(read_literal(b"Name").is_none());
}
#[test]
fn executable_simple() {
let n = read_executable(b"beginbfchar ").unwrap();
assert_eq!(n.as_str().unwrap(), "beginbfchar");
assert!(!n.is_literal());
}
#[test]
fn executable_stops_at_delimiter() {
let mut r = Reader::new(b"def/name");
let data = parse_executable(&mut r).unwrap();
assert_eq!(data, b"def");
assert_eq!(r.peek_byte(), Some(b'/'));
}
#[test]
fn executable_at_eof() {
let n = read_executable(b"endcmap").unwrap();
assert_eq!(n.as_str().unwrap(), "endcmap");
}
#[test]
fn executable_empty() {
assert!(read_executable(b"").is_none());
}
#[test]
fn executable_starts_at_delimiter() {
assert!(read_executable(b"(foo)").is_none());
}
#[test]
fn decode_no_escapes() {
let n = Name::new(b"simple", true);
assert_eq!(n.decode().unwrap(), b"simple");
}
#[test]
fn decode_with_escapes() {
let n = Name::new(b"lime#20Green", true);
assert_eq!(n.decode().unwrap(), b"lime Green");
}
}
+177
View File
@@ -0,0 +1,177 @@
use crate::error::{Error, Result};
use crate::reader::{Reader, is_delimiter, is_whitespace};
/// A PostScript number object (integer or real).
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Number {
Integer(i32),
Real(f32),
}
impl Number {
/// Return the value as an `i32`. Reals are truncated.
pub fn as_i32(self) -> i32 {
match self {
Self::Integer(v) => v,
Self::Real(v) => v as i32,
}
}
/// Return the value as an `f32`.
pub fn as_f32(self) -> f32 {
match self {
Self::Integer(v) => v as f32,
Self::Real(v) => v,
}
}
/// Return the value as an `f64`.
pub fn as_f64(self) -> f64 {
match self {
Self::Integer(v) => v as f64,
Self::Real(v) => v as f64,
}
}
}
fn is_terminated(r: &Reader<'_>) -> bool {
match r.peek_byte() {
None => true,
Some(b) => is_whitespace(b) || is_delimiter(b),
}
}
pub(crate) fn read(r: &mut Reader<'_>) -> Result<Number> {
let saved = r.offset();
// Optional sign.
let first = r.peek_byte().ok_or(Error::SyntaxError)?;
let has_sign = first == b'+' || first == b'-';
if has_sign {
r.forward();
}
// Consume leading digits.
let digit_start = r.offset();
r.forward_while(|b| b.is_ascii_digit());
let has_digits = r.offset() > digit_start;
// Check if number is a radix number.
if !has_sign && has_digits && r.peek_byte() == Some(b'#') {
let base_bytes = r.range(digit_start..r.offset()).ok_or(Error::SyntaxError)?;
let base_str = core::str::from_utf8(base_bytes).map_err(|_| Error::SyntaxError)?;
let base = base_str.parse::<u32>().map_err(|_| Error::SyntaxError)?;
if !(2..=36).contains(&base) {
return Err(Error::SyntaxError);
}
// Skip `#`.
r.forward();
let num_start = r.offset();
r.forward_while(|b| b.is_ascii_alphanumeric());
if r.offset() == num_start || !is_terminated(r) {
return Err(Error::SyntaxError);
}
let num_bytes = r.range(num_start..r.offset()).ok_or(Error::SyntaxError)?;
let num_str = core::str::from_utf8(num_bytes).map_err(|_| Error::SyntaxError)?;
let value = i32::from_str_radix(num_str, base).map_err(|_| Error::SyntaxError)?;
return Ok(Number::Integer(value));
}
// Check for real number indicators: `.` or `e`/`E`.
let has_dot = r.peek_byte() == Some(b'.');
if has_dot {
r.forward(); // skip '.'
r.forward_while(|b| b.is_ascii_digit());
}
// At this point we need at least some digits (before or after the dot).
if !has_digits && !has_dot {
return Err(Error::SyntaxError);
}
let has_exponent = matches!(r.peek_byte(), Some(b'e' | b'E'));
if has_exponent {
r.forward();
// Optional exponent sign.
if matches!(r.peek_byte(), Some(b'+' | b'-')) {
r.forward();
}
r.forward_while(|b| b.is_ascii_digit());
}
if !is_terminated(r) {
return Err(Error::SyntaxError);
}
let token = r.range(saved..r.offset()).ok_or(Error::SyntaxError)?;
let str = core::str::from_utf8(token).map_err(|_| Error::SyntaxError)?;
if has_dot || has_exponent {
let value = str.parse::<f32>().map_err(|_| Error::SyntaxError)?;
Ok(Number::Real(value))
} else {
if !has_digits {
return Err(Error::SyntaxError);
}
let value = str.parse::<i32>().map_err(|_| Error::SyntaxError)?;
Ok(Number::Integer(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn read_num(input: &[u8]) -> Result<Number> {
let mut r = Reader::new(input);
read(&mut r)
}
#[test]
fn signed_integers() {
assert_eq!(read_num(b"123 ").unwrap(), Number::Integer(123));
assert_eq!(read_num(b"-98 ").unwrap(), Number::Integer(-98));
assert_eq!(read_num(b"43445 ").unwrap(), Number::Integer(43445));
assert_eq!(read_num(b"0 ").unwrap(), Number::Integer(0));
assert_eq!(read_num(b"+17 ").unwrap(), Number::Integer(17));
}
#[test]
fn real_numbers() {
assert_eq!(read_num(b"-.002 ").unwrap(), Number::Real(-0.002));
assert_eq!(read_num(b"34.5 ").unwrap(), Number::Real(34.5));
assert_eq!(read_num(b"-3.62 ").unwrap(), Number::Real(-3.62));
assert_eq!(read_num(b"123.6e10 ").unwrap(), Number::Real(123.6e10));
assert_eq!(read_num(b"1.0E-5 ").unwrap(), Number::Real(1.0E-5));
assert_eq!(read_num(b"1E6 ").unwrap(), Number::Real(1E6));
assert_eq!(read_num(b"-1. ").unwrap(), Number::Real(-1.0));
assert_eq!(read_num(b"0.0 ").unwrap(), Number::Real(0.0));
}
#[test]
fn radix_numbers() {
assert_eq!(read_num(b"8#1777 ").unwrap(), Number::Integer(0o1777));
assert_eq!(read_num(b"16#FFFE ").unwrap(), Number::Integer(0xFFFE));
assert_eq!(read_num(b"2#1000 ").unwrap(), Number::Integer(0b1000));
}
#[test]
fn invalid() {
assert!(read_num(b"abc").is_err());
assert!(read_num(b"+abc").is_err());
assert!(read_num(b"1a").is_err());
}
}
+226
View File
@@ -0,0 +1,226 @@
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));
}
}
+136
View File
@@ -0,0 +1,136 @@
use core::ops::Range;
#[derive(Clone, Debug)]
pub(crate) struct Reader<'a> {
pub(crate) data: &'a [u8],
pub(crate) offset: usize,
}
impl<'a> Reader<'a> {
#[inline]
pub(crate) fn new(data: &'a [u8]) -> Self {
Self { data, offset: 0 }
}
#[inline]
pub(crate) fn jump(&mut self, offset: usize) {
self.offset = offset;
}
#[inline]
pub(crate) fn range(&self, range: Range<usize>) -> Option<&'a [u8]> {
self.data.get(range)
}
#[inline]
pub(crate) fn offset(&self) -> usize {
self.offset
}
#[inline]
pub(crate) fn read_bytes(&mut self, len: usize) -> Option<&'a [u8]> {
let v = self.peek_bytes(len)?;
self.offset += len;
Some(v)
}
#[inline]
pub(crate) fn read_byte(&mut self) -> Option<u8> {
let v = self.peek_byte()?;
self.offset += 1;
Some(v)
}
#[inline]
pub(crate) fn peek_bytes(&self, len: usize) -> Option<&'a [u8]> {
self.data.get(self.offset..self.offset + len)
}
#[inline]
pub(crate) fn peek_byte(&self) -> Option<u8> {
self.data.get(self.offset).copied()
}
#[inline]
pub(crate) fn eat(&mut self, f: impl Fn(u8) -> bool) -> Option<u8> {
let val = self.peek_byte()?;
if f(val) {
self.forward();
Some(val)
} else {
None
}
}
#[inline]
pub(crate) fn forward(&mut self) {
self.offset += 1;
}
#[inline]
pub(crate) fn forward_while(&mut self, f: impl Fn(u8) -> bool) {
while let Some(b) = self.peek_byte() {
if f(b) {
self.forward();
} else {
break;
}
}
}
#[inline]
pub(crate) fn forward_tag(&mut self, tag: &[u8]) -> Option<()> {
self.peek_tag(tag)?;
self.offset += tag.len();
Some(())
}
#[inline]
pub(crate) fn peek_tag(&self, tag: &[u8]) -> Option<()> {
let mut cloned = self.clone();
for b in tag.iter().copied() {
if cloned.peek_byte() == Some(b) {
cloned.forward();
} else {
return None;
}
}
Some(())
}
#[inline]
pub(crate) fn skip_eol(&mut self) {
while let Some(b) = self.peek_byte() {
if is_eol(b) {
self.forward();
} else {
return;
}
}
}
}
#[inline(always)]
pub(crate) fn is_whitespace(b: u8) -> bool {
matches!(b, 0x00 | 0x09 | 0x0a | 0x0c | 0x0d | 0x20)
}
#[inline(always)]
pub(crate) fn is_delimiter(b: u8) -> bool {
matches!(
b,
b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
)
}
#[inline(always)]
pub(crate) fn is_regular(b: u8) -> bool {
!is_whitespace(b) && !is_delimiter(b)
}
#[inline(always)]
pub(crate) fn is_eol(b: u8) -> bool {
matches!(b, 0x0a | 0x0d)
}
+288
View File
@@ -0,0 +1,288 @@
mod ascii_85;
pub(crate) mod ascii_hex;
mod literal;
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::reader::Reader;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StringInner<'a> {
Literal(&'a [u8]),
Hex(&'a [u8]),
Ascii85(&'a [u8]),
}
/// A PostScript string object.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct String<'a> {
inner: StringInner<'a>,
}
impl<'a> String<'a> {
pub(crate) const fn from_literal(data: &'a [u8]) -> Self {
Self {
inner: StringInner::Literal(data),
}
}
pub(crate) const fn from_hex(data: &'a [u8]) -> Self {
Self {
inner: StringInner::Hex(data),
}
}
pub(crate) const fn from_ascii85(data: &'a [u8]) -> Self {
Self {
inner: StringInner::Ascii85(data),
}
}
/// Decode the string content into `out`, replacing any previous contents.
pub fn decode_into(&self, out: &mut Vec<u8>) -> Result<()> {
out.clear();
match self.inner {
StringInner::Literal(data) => literal::decode_into(data, out),
StringInner::Hex(data) => ascii_hex::decode_into(data, out),
StringInner::Ascii85(data) => ascii_85::decode_into(data, out),
}
.ok_or(Error::SyntaxError)
}
/// Decode the string content.
pub fn decode(&self) -> Result<Vec<u8>> {
let mut out = Vec::new();
self.decode_into(&mut out)?;
Ok(out)
}
}
pub(crate) fn parse_literal<'a>(r: &mut Reader<'a>) -> Option<&'a [u8]> {
let start = r.offset();
skip_literal(r)?;
let end = r.offset();
// Exclude outer parentheses.
r.range(start + 1..end - 1)
}
pub(crate) fn parse_hex<'a>(r: &mut Reader<'a>) -> Option<&'a [u8]> {
r.forward_tag(b"<")?;
let start = r.offset();
while let Some(b) = r.read_byte() {
if b == b'>' {
return r.range(start..r.offset() - 1);
}
}
None
}
pub(crate) fn parse_ascii85<'a>(r: &mut Reader<'a>) -> Option<&'a [u8]> {
r.forward_tag(b"<~")?;
let start = r.offset();
loop {
let b = r.read_byte()?;
if b == b'~' {
let end = r.offset() - 1;
r.forward_tag(b">")?;
return r.range(start..end);
}
}
}
fn skip_literal(r: &mut Reader<'_>) -> Option<()> {
r.forward_tag(b"(")?;
let mut depth = 1_u32;
while depth > 0 {
let byte = r.read_byte()?;
match byte {
b'\\' => {
let _ = r.read_byte()?;
}
b'(' => depth += 1,
b')' => depth -= 1,
_ => {}
}
}
Some(())
}
#[cfg(test)]
mod tests {
use super::*;
fn decode_literal(input: &[u8]) -> Result<Vec<u8>> {
let mut r = Reader::new(input);
let data = parse_literal(&mut r).ok_or(Error::SyntaxError)?;
String::from_literal(data).decode()
}
#[test]
fn literal_empty() {
assert_eq!(decode_literal(b"()").unwrap(), b"");
}
#[test]
fn literal_simple() {
assert_eq!(decode_literal(b"(Hello)").unwrap(), b"Hello");
}
#[test]
fn literal_nested_parens() {
assert_eq!(
decode_literal(b"(Hi (()) there)").unwrap(),
b"Hi (()) there"
);
}
#[test]
fn literal_escape_n() {
assert_eq!(decode_literal(b"(a\\nb)").unwrap(), b"a\nb");
}
#[test]
fn literal_escape_r() {
assert_eq!(decode_literal(b"(a\\rb)").unwrap(), b"a\rb");
}
#[test]
fn literal_escape_t() {
assert_eq!(decode_literal(b"(a\\tb)").unwrap(), b"a\tb");
}
#[test]
fn literal_escape_b() {
assert_eq!(decode_literal(b"(a\\bb)").unwrap(), &[b'a', 0x08, b'b']);
}
#[test]
fn literal_escape_f() {
assert_eq!(decode_literal(b"(a\\fb)").unwrap(), &[b'a', 0x0C, b'b']);
}
#[test]
fn literal_escape_backslash() {
assert_eq!(decode_literal(b"(a\\\\b)").unwrap(), b"a\\b");
}
#[test]
fn literal_escape_parens() {
assert_eq!(decode_literal(b"(Hi \\()").unwrap(), b"Hi (");
}
#[test]
fn literal_octal_three_digits() {
assert_eq!(decode_literal(b"(\\053)").unwrap(), b"+");
}
#[test]
fn literal_octal_two_digits() {
assert_eq!(decode_literal(b"(\\36)").unwrap(), b"\x1e");
}
#[test]
fn literal_octal_one_digit() {
assert_eq!(decode_literal(b"(\\3)").unwrap(), b"\x03");
}
#[test]
fn literal_line_continuation_lf() {
assert_eq!(decode_literal(b"(Hi \\\nthere)").unwrap(), b"Hi there");
}
#[test]
fn literal_line_continuation_cr() {
assert_eq!(decode_literal(b"(Hi \\\rthere)").unwrap(), b"Hi there");
}
#[test]
fn literal_line_continuation_crlf() {
assert_eq!(decode_literal(b"(Hi \\\r\nthere)").unwrap(), b"Hi there");
}
#[test]
fn literal_bare_eol_lf() {
assert_eq!(decode_literal(b"(a\nb)").unwrap(), b"a\nb");
}
#[test]
fn literal_bare_eol_cr() {
assert_eq!(decode_literal(b"(a\rb)").unwrap(), b"a\nb");
}
#[test]
fn literal_bare_eol_crlf() {
assert_eq!(decode_literal(b"(a\r\nb)").unwrap(), b"a\nb");
}
fn decode_hex(input: &[u8]) -> Result<Vec<u8>> {
let mut r = Reader::new(input);
let data = parse_hex(&mut r).ok_or(Error::SyntaxError)?;
String::from_hex(data).decode()
}
#[test]
fn hex_simple() {
assert_eq!(decode_hex(b"<48656C6C6F>").unwrap(), b"Hello");
}
#[test]
fn hex_with_whitespace() {
assert_eq!(decode_hex(b"<48 65 6C 6C 6F>").unwrap(), b"Hello");
}
#[test]
fn hex_odd_nibble() {
assert_eq!(decode_hex(b"<ABC>").unwrap(), &[0xAB, 0xC0]);
}
#[test]
fn hex_empty() {
assert_eq!(decode_hex(b"<>").unwrap(), b"");
}
#[test]
fn hex_lowercase() {
assert_eq!(decode_hex(b"<abcd>").unwrap(), &[0xAB, 0xCD]);
}
#[test]
fn hex_mixed_case() {
assert_eq!(decode_hex(b"<aB3E>").unwrap(), &[0xAB, 0x3E]);
}
fn decode_a85(input: &[u8]) -> Result<Vec<u8>> {
let mut r = Reader::new(input);
let data = parse_ascii85(&mut r).ok_or(Error::SyntaxError)?;
String::from_ascii85(data).decode()
}
#[test]
fn ascii85_simple() {
// "Hello" in ASCII85 is "87cURDZ"
assert_eq!(decode_a85(b"<~87cURDZ~>").unwrap(), b"Hello");
}
#[test]
fn ascii85_empty() {
assert_eq!(decode_a85(b"<~~>").unwrap(), b"");
}
#[test]
fn ascii85_z_shorthand() {
assert_eq!(decode_a85(b"<~z~>").unwrap(), &[0, 0, 0, 0]);
}
#[test]
fn ascii85_partial_group() {
let result = decode_a85(b"<~87~>").unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn ascii85_with_whitespace() {
assert_eq!(decode_a85(b"<~87cU RDZ~>").unwrap(), b"Hello");
}
}
+90
View File
@@ -0,0 +1,90 @@
// Keep in sync with `hayro-syntax/src/filter/ascii_85.rs`.
use crate::reader::{Reader, is_whitespace};
use alloc::vec::Vec;
pub(crate) fn decode_into(data: &[u8], out: &mut Vec<u8>) -> Option<()> {
const POW_85: [u32; 5] = [52200625, 614125, 7225, 85, 1];
let mut reader = Reader::new(data);
let mut read_byte = || -> Option<u8> {
loop {
let b = reader.read_byte()?;
// White space characters should be ignored.
if !is_whitespace(b) {
return Some(b);
}
}
};
let flush_group = |group: &mut Vec<u8>, decoded: &mut Vec<u8>| -> Option<()> {
let (digits, output_len): ([u32; 5], usize) = match group.len() {
0 => return Some(()),
1 => return None, // A single character is not valid.
2 => (
[group[0], group[1], b'u', b'u', b'u'].map(|b| (b - b'!') as u32),
1,
),
3 => (
[group[0], group[1], group[2], b'u', b'u'].map(|b| (b - b'!') as u32),
2,
),
4 => (
[group[0], group[1], group[2], group[3], b'u'].map(|b| (b - b'!') as u32),
3,
),
5 => (
[group[0], group[1], group[2], group[3], group[4]].map(|b| (b - b'!') as u32),
4,
),
_ => unreachable!(),
};
let value = digits[0]
.checked_mul(POW_85[0])?
.checked_add(digits[1].checked_mul(POW_85[1])?)?
.checked_add(digits[2].checked_mul(POW_85[2])?)?
.checked_add(digits[3].checked_mul(POW_85[3])?)?
.checked_add(digits[4])?;
decoded.extend_from_slice(&value.to_be_bytes()[..output_len]);
group.clear();
Some(())
};
out.reserve(data.len() * 4 / 5);
let mut group = Vec::with_capacity(5);
loop {
let Some(b) = read_byte() else {
// Be lenient and accept what we have (see PDFBOX-5910).
flush_group(&mut group, out)?;
return Some(());
};
match b {
b'!'..=b'u' => {
group.push(b);
if group.len() == 5 {
flush_group(&mut group, out)?;
}
}
b'z' => {
flush_group(&mut group, out)?;
out.extend_from_slice(&[0, 0, 0, 0]);
}
b'~' => {
// Technically requires a '>', but there is a PDF where it isn't
// appended and decodes fine in other viewers.
flush_group(&mut group, out)?;
return Some(());
}
_ => return None, // Invalid character.
}
}
}
+59
View File
@@ -0,0 +1,59 @@
// Keep in sync with `hayro-syntax/src/filter/ascii_hex.rs`.
use crate::reader::is_whitespace;
use alloc::vec::Vec;
pub(crate) fn decode_into(data: &[u8], out: &mut Vec<u8>) -> Option<()> {
let has_whitespace = data.iter().any(|&b| is_whitespace(b));
out.reserve(data.len().div_ceil(2));
if !has_whitespace {
// Fast path, don't need to worry about white spaces.
let mut i = 0;
while i + 1 < data.len() {
out.push(decode_hex_digit(data[i])? << 4 | decode_hex_digit(data[i + 1])?);
i += 2;
}
if i < data.len() {
out.push(decode_hex_digit(data[i])? << 4);
}
} else {
// Slow path, need to strip white spaces.
let mut iter = data.iter().copied();
let mut read_byte = || -> Option<u8> {
loop {
let b = iter.next()?;
if !is_whitespace(b) {
return Some(b);
}
}
};
loop {
match (read_byte(), read_byte()) {
(Some(hi), Some(lo)) => {
out.push(decode_hex_digit(hi)? << 4 | decode_hex_digit(lo)?);
}
(Some(hi), None) => {
out.push(decode_hex_digit(hi)? << 4);
break;
}
(None, _) => break,
}
}
}
Some(())
}
#[inline(always)]
pub(crate) fn decode_hex_digit(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'A'..=b'F' => Some(c - b'A' + 10),
b'a'..=b'f' => Some(c - b'a' + 10),
_ => None,
}
}
+85
View File
@@ -0,0 +1,85 @@
// Keep in sync with `hayro-syntax/src/object/string.rs` (`read_literal`).
use alloc::vec::Vec;
use crate::reader::Reader;
pub(crate) fn decode_into(data: &[u8], out: &mut Vec<u8>) -> Option<()> {
let mut r = Reader::new(data);
while let Some(byte) = r.read_byte() {
match byte {
b'\\' => {
let next = r.read_byte()?;
if is_octal_digit(next) {
let second = r.read_byte();
let third = r.read_byte();
let bytes = match (second, third) {
(Some(n1), Some(n2)) => match (is_octal_digit(n1), is_octal_digit(n2)) {
(true, true) => [next, n1, n2],
(true, _) => {
r.jump(r.offset() - 1);
[b'0', next, n1]
}
_ => {
r.jump(r.offset() - 2);
[b'0', b'0', next]
}
},
(Some(n1), None) => {
if is_octal_digit(n1) {
[b'0', next, n1]
} else {
r.jump(r.offset() - 1);
[b'0', b'0', next]
}
}
_ => [b'0', b'0', next],
};
let str = core::str::from_utf8(&bytes).unwrap();
if let Ok(num) = u8::from_str_radix(str, 8) {
out.push(num);
}
} else {
match next {
b'n' => out.push(0xA),
b'r' => out.push(0xD),
b't' => out.push(0x9),
b'b' => out.push(0x8),
b'f' => out.push(0xC),
b'(' => out.push(b'('),
b')' => out.push(b')'),
b'\\' => out.push(b'\\'),
b'\n' | b'\r' => {
// "If the \ is followed immediately by a newline
// (CR, LF, or CR-LF pair), the scanner ignores
// both the initial \ and the newline; this breaks
// a string into multiple lines without including
// the newline character as part of the string."
r.skip_eol();
}
_ => out.push(next),
}
}
}
b'(' | b')' => out.push(byte),
// "But if a newline appears without a preceding \, the result is
// equivalent to \n."
b'\n' | b'\r' => {
out.push(b'\n');
r.skip_eol();
}
other => out.push(other),
}
}
Some(())
}
fn is_octal_digit(byte: u8) -> bool {
matches!(byte, b'0'..=b'7')
}