use std::collections::BTreeMap;
use std::path::Path;
use crate::error::{PdfError, PdfResult};
use crate::object::*;
use crate::stream::{StreamDecoder, StreamFilter};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ParseMode {
Strict,
#[default]
Lenient,
}
#[derive(Debug, Clone, Copy)]
pub struct ParserLimits {
pub max_objects: usize,
pub max_string_length: usize,
pub max_array_length: usize,
pub max_dict_entries: usize,
pub max_recursion_depth: u32,
pub max_stream_size: usize,
pub max_decoded_stream_size: usize,
}
impl Default for ParserLimits {
fn default() -> Self {
Self {
max_objects: 1_000_000,
max_string_length: 1_048_576,
max_array_length: 100_000,
max_dict_entries: 10_000,
max_recursion_depth: 64,
max_stream_size: 100 * 1024 * 1024,
max_decoded_stream_size: 100 * 1024 * 1024,
}
}
}
#[derive(Debug, Clone)]
pub struct Document {
version: String,
objects: BTreeMap<ObjectId, PdfObject>,
catalog: Option<ObjectId>,
info: Option<ObjectId>,
}
impl Document {
pub fn new() -> Self {
Self {
version: "1.7".to_string(),
objects: BTreeMap::new(),
catalog: None,
info: None,
}
}
pub fn version(&self) -> &str {
&self.version
}
fn set_version(&mut self, version: String) {
self.version = version;
}
pub fn objects(&self) -> &BTreeMap<ObjectId, PdfObject> {
&self.objects
}
pub fn add_object(&mut self, id: ObjectId, obj: PdfObject) {
self.objects.entry(id).or_insert(obj);
}
pub fn get_object(&self, id: ObjectId) -> Option<&PdfObject> {
self.objects.get(&id)
}
pub fn get_object_mut(&mut self, id: ObjectId) -> Option<&mut PdfObject> {
self.objects.get_mut(&id)
}
pub fn set_catalog(&mut self, id: ObjectId) {
self.catalog = Some(id);
}
pub fn catalog(&self) -> Option<ObjectId> {
self.catalog
}
pub fn set_info(&mut self, id: ObjectId) {
self.info = Some(id);
}
pub fn info(&self) -> Option<ObjectId> {
self.info
}
}
impl Default for Document {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] enum XrefEntry {
Free { generation: u16 },
Used { offset: u64, generation: u16 },
Compressed { stream_number: u32, index: u32 },
}
type XrefSection = (BTreeMap<u32, (u16, XrefEntry)>, Option<PdfDictionary>);
pub struct Parser {
mode: ParseMode,
limits: ParserLimits,
}
impl Parser {
pub fn new() -> Self {
Self {
mode: ParseMode::default(),
limits: ParserLimits::default(),
}
}
pub fn with_mode(mode: ParseMode) -> Self {
Self {
mode,
limits: ParserLimits::default(),
}
}
pub fn with_limits(limits: ParserLimits) -> Self {
Self {
mode: ParseMode::default(),
limits,
}
}
pub fn parse(&self, input: &[u8]) -> PdfResult<Document> {
let mut doc = Document::new();
if let Some(eol) = input.iter().position(|&b| b == b'\n' || b == b'\r') {
if input.starts_with(b"%PDF-") {
doc.set_version(String::from_utf8_lossy(&input[5..eol]).trim().to_string());
}
}
let kw_pos = find_startxref(input)?;
let mut lx = Lexer::new(input, kw_pos as usize, ParseMode::Lenient, self.limits);
lx.skip_ws();
lx.match_kw(b"startxref");
let mut offset = lx.parse_unsigned()?;
let mut xref: BTreeMap<u32, (u16, XrefEntry)> = BTreeMap::new();
let mut trailer: Option<PdfDictionary> = None;
loop {
let (section, dict) = self.parse_xref_section(input, offset)?;
for (number, entry) in section {
xref.entry(number).or_insert(entry);
}
if xref.len() > self.limits.max_objects {
return Err(PdfError::Parse {
offset,
message: format!(
"object count {} exceeds limit {}",
xref.len(),
self.limits.max_objects
),
});
}
if dict.is_some() {
trailer = dict;
}
let prev = trailer
.as_ref()
.and_then(|d| d.get_integer("Prev"))
.filter(|p| *p > 0 && (*p as u64) < offset);
match prev {
Some(p) => offset = p as u64,
None => break,
}
}
if let Some(t) = &trailer {
if let Some(id) = t.get("Root").and_then(|o| o.as_reference()) {
doc.set_catalog(id);
}
if let Some(id) = t.get("Info").and_then(|o| o.as_reference()) {
doc.set_info(id);
}
}
let mut object_streams: Vec<(u32, u32)> = Vec::new();
for (&number, &(generation, entry)) in &xref {
match entry {
XrefEntry::Free { .. } => {}
XrefEntry::Used { offset, .. } => {
let obj = self.parse_object_at(input, offset);
match obj {
Ok(obj) => doc.add_object(ObjectId::new(number, generation), obj),
Err(e) => {
if self.mode == ParseMode::Strict {
return Err(e);
}
}
}
}
XrefEntry::Compressed {
stream_number,
index,
} => {
object_streams.push((stream_number, index));
}
}
}
let mut resolved = BTreeMap::new();
for &(stream_number, index) in &object_streams {
let obj = doc.get_object(ObjectId::new(stream_number, 0));
if let Some(PdfObject::Stream(stream)) = obj {
let data = self.decode_stream(stream)?;
let n = stream.dictionary.get_integer("N").unwrap_or(0) as usize;
let first = stream.dictionary.get_integer("First").unwrap_or(0) as usize;
if let Some((obj_number, body)) =
parse_object_stream(&data, n, first, index, self.limits)
{
if let Some(header) = xref.get(&obj_number).copied() {
resolved.insert(obj_number, (header.0, body));
if resolved.len() > self.limits.max_objects {
return Err(PdfError::Parse {
offset: 0,
message: "compressed object count exceeds limit".to_string(),
});
}
}
}
}
}
for (number, (generation, body)) in resolved {
doc.add_object(ObjectId::new(number, generation), body);
}
Ok(doc)
}
pub fn parse_file(&self, path: &Path) -> PdfResult<Document> {
let data = std::fs::read(path)?;
self.parse(&data)
}
fn decode_stream(&self, stream: &PdfStream) -> PdfResult<Vec<u8>> {
let filter = stream.dictionary.get_name("Filter");
match filter {
Some(f) if f.as_str() == "FlateDecode" || f.as_str() == "Fl" => {
let decoded = StreamDecoder::new().decode_with_limit(
&stream.data,
StreamFilter::Flate,
self.limits.max_decoded_stream_size,
)?;
if decoded.len() > self.limits.max_decoded_stream_size {
return Err(PdfError::Parse {
offset: 0,
message: "decoded stream exceeds size limit".to_string(),
});
}
Ok(decoded)
}
Some(_) => Err(PdfError::NotImplemented(
"unsupported stream filter".to_string(),
)),
None => Ok(stream.data.clone()),
}
}
fn parse_xref_section(&self, input: &[u8], offset: u64) -> PdfResult<XrefSection> {
let mut lx = Lexer::new(input, offset as usize, self.mode, self.limits);
lx.skip_ws();
if lx.match_kw(b"xref") {
self.parse_classic_xref(&mut lx)
} else {
let obj = self.parse_object_at(input, offset)?;
match obj {
PdfObject::Stream(stream) => {
let dict = stream.dictionary.clone();
if dict.get_name("Type").map(|n| n.as_str()) != Some("XRef") {
return Err(PdfError::Parse {
offset,
message: "expected /Type /XRef in xref stream".to_string(),
});
}
let data = self.decode_stream(&stream)?;
let entries = parse_xref_stream_entries(&data, &dict, offset)?;
Ok((entries, Some(dict)))
}
_ => Err(PdfError::Parse {
offset,
message: "expected xref keyword or XRef stream".to_string(),
}),
}
}
}
fn parse_classic_xref(&self, lx: &mut Lexer<'_>) -> PdfResult<XrefSection> {
let mut entries = BTreeMap::new();
let max_entries = self.limits.max_objects;
loop {
lx.skip_ws();
let start = lx.parse_unsigned()?;
let count = lx.parse_unsigned()?;
for i in 0..count {
if entries.len() >= max_entries {
return Err(lx.err(format!(
"xref entry count {} exceeds limit {}",
entries.len(),
max_entries
)));
}
let n = (start + i) as u32;
lx.skip_ws();
let field1 = lx.parse_unsigned()?;
lx.skip_ws();
let field2 = lx.parse_unsigned()?;
lx.skip_ws();
match lx.bump() {
Some(b'n') => {
entries.insert(
n,
(
field2 as u16,
XrefEntry::Used {
offset: field1,
generation: field2 as u16,
},
),
);
}
Some(b'f') => {
entries.insert(
n,
(
field2 as u16,
XrefEntry::Free {
generation: field2 as u16,
},
),
);
}
_ => return Err(lx.err("expected `n` or `f` in xref entry")),
}
}
lx.skip_ws();
if lx.match_kw(b"trailer") {
break;
}
}
lx.skip_ws();
let dict = if lx.peek() == Some(b'<') {
let mut lx2 = Lexer::new(lx.data, lx.pos, self.mode, self.limits);
let d = lx2.parse_dict()?;
lx.pos = lx2.pos;
Some(d)
} else {
None
};
Ok((entries, dict))
}
fn parse_object_at(&self, input: &[u8], offset: u64) -> PdfResult<PdfObject> {
let mut lx = Lexer::new(input, offset as usize, self.mode, self.limits);
lx.skip_ws();
let _obj_number = lx.parse_unsigned()?;
lx.skip_ws();
let _generation = lx.parse_unsigned()?;
lx.skip_ws();
if !lx.match_kw(b"obj") {
return Err(lx.err("expected `obj` after object header"));
}
lx.skip_ws();
let obj = lx.parse_object()?;
lx.skip_ws();
if self.mode == ParseMode::Strict && !lx.match_kw(b"endobj") {
return Err(lx.err("expected `endobj`"));
}
Ok(obj)
}
}
impl Default for Parser {
fn default() -> Self {
Self::new()
}
}
fn parse_object_stream(
data: &[u8],
n: usize,
first: usize,
index: u32,
limits: ParserLimits,
) -> Option<(u32, PdfObject)> {
let mut header_lexer = Lexer::new(data, 0, ParseMode::Lenient, limits);
let mut pairs = Vec::with_capacity(n.min(limits.max_objects));
for _ in 0..n.min(limits.max_objects) {
let num = header_lexer.parse_unsigned().ok()?;
let off = header_lexer.parse_unsigned().ok()?;
pairs.push((num as u32, off as usize));
}
let (num, off) = *pairs.get(index as usize)?;
let mut body = Lexer::new(data, first + off, ParseMode::Lenient, limits);
let obj = body.parse_object().ok()?;
Some((num, obj))
}
fn parse_xref_stream_entries(
data: &[u8],
dict: &PdfDictionary,
offset: u64,
) -> PdfResult<BTreeMap<u32, (u16, XrefEntry)>> {
let w = dict.get_array("W").ok_or_else(|| PdfError::Parse {
offset,
message: "xref stream missing /W".to_string(),
})?;
let mut widths = Vec::new();
for item in w.0.iter() {
let i = item.as_integer().ok_or_else(|| PdfError::Parse {
offset,
message: "invalid /W entry".to_string(),
})?;
widths.push(i as usize);
}
let first = match dict.get_array("Index") {
Some(arr) => {
let mut out = Vec::new();
let mut it = arr.0.iter();
while let Some(f) = it.next() {
let count = it.next().ok_or_else(|| PdfError::Parse {
offset,
message: "invalid /Index".to_string(),
})?;
out.push((
f.as_integer().unwrap_or(0) as u32,
count.as_integer().unwrap_or(0) as u32,
));
}
out
}
None => vec![(0, dict.get_integer("Size").unwrap_or(0) as u32)],
};
let record = widths[0] + widths[1] + widths[2];
if record == 0 {
return Ok(BTreeMap::new());
}
let mut entries = BTreeMap::new();
let mut pos = 0usize;
let field = |offset: usize, width: usize| -> u64 {
if width == 0 || offset + width > data.len() {
return 0;
}
let mut v: u64 = 0;
for &b in &data[offset..offset + width] {
v = (v << 8) | b as u64;
}
v
};
for (first, count) in first {
for i in 0..count {
if pos + record > data.len() {
break;
}
let typ = field(pos, widths[0]);
let f2 = field(pos + widths[0], widths[1]);
let f3 = field(pos + widths[0] + widths[1], widths[2]);
pos += record;
let number = first + i;
match typ {
0 => {
entries.insert(
number,
(
f3 as u16,
XrefEntry::Free {
generation: f3 as u16,
},
),
);
}
1 => {
entries.insert(
number,
(
f3 as u16,
XrefEntry::Used {
offset: f2,
generation: f3 as u16,
},
),
);
}
2 => {
entries.insert(
number,
(
0,
XrefEntry::Compressed {
stream_number: f2 as u32,
index: f3 as u32,
},
),
);
}
_ => {}
}
}
}
Ok(entries)
}
fn find_startxref(input: &[u8]) -> PdfResult<u64> {
let needle = b"startxref";
let mut search = input.len();
loop {
let end = input[..search].len();
let idx = input[..end]
.windows(needle.len())
.rposition(|w| w == needle);
let i = match idx {
Some(i) => i,
None => {
return Err(PdfError::Parse {
offset: 0,
message: "no startxref keyword found".to_string(),
});
}
};
let prev_ok = i == 0 || matches!(input[i - 1], b' ' | b'\t' | b'\r' | b'\n' | b'\x0c' | 0);
let next = input.get(i + needle.len()).copied();
let next_ok = next
.map(|b| matches!(b, b' ' | b'\t' | b'\r' | b'\n' | b'\x0c' | 0))
.unwrap_or(true);
if prev_ok && next_ok {
return Ok(i as u64);
}
search = i;
if search == 0 {
break;
}
}
Err(PdfError::Parse {
offset: 0,
message: "no startxref keyword found".to_string(),
})
}
struct Lexer<'a> {
data: &'a [u8],
pos: usize,
strict: bool,
max_depth: u32,
depth: u32,
limits: ParserLimits,
}
impl<'a> Lexer<'a> {
fn new(data: &'a [u8], pos: usize, mode: ParseMode, limits: ParserLimits) -> Self {
Self {
data,
pos,
strict: mode == ParseMode::Strict,
max_depth: limits.max_recursion_depth,
depth: 0,
limits,
}
}
fn is_ws(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\r' | b'\n' | 0x0c | 0)
}
fn is_delim(b: u8) -> bool {
matches!(
b,
b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
)
}
fn peek(&self) -> Option<u8> {
self.data.get(self.pos).copied()
}
fn bump(&mut self) -> Option<u8> {
let b = self.peek();
if b.is_some() {
self.pos += 1;
}
b
}
fn err(&self, message: impl Into<String>) -> PdfError {
PdfError::Parse {
offset: self.pos as u64,
message: message.into(),
}
}
fn skip_ws(&mut self) {
loop {
while self
.data
.get(self.pos)
.copied()
.map(Self::is_ws)
.unwrap_or(false)
{
self.pos += 1;
}
if self.data.get(self.pos) == Some(&b'%') {
while self.pos < self.data.len() && self.data[self.pos] != b'\n' {
self.pos += 1;
}
} else {
break;
}
}
}
fn match_kw(&mut self, kw: &[u8]) -> bool {
if self
.data
.get(self.pos..)
.is_some_and(|rest| rest.starts_with(kw))
{
let after = self.data.get(self.pos + kw.len()).copied().unwrap_or(b' ');
if Self::is_ws(after) || Self::is_delim(after) {
self.pos += kw.len();
return true;
}
}
false
}
fn parse_unsigned(&mut self) -> PdfResult<u64> {
self.skip_ws();
let start = self.pos;
while self
.data
.get(self.pos)
.map(|b| b.is_ascii_digit())
.unwrap_or(false)
{
self.pos += 1;
}
if start == self.pos {
return Err(self.err("expected number"));
}
let mut value: u64 = 0;
for &b in &self.data[start..self.pos] {
value = match value
.checked_mul(10)
.and_then(|v| v.checked_add(u64::from(b - b'0')))
{
Some(v) => v,
None => return Err(self.err("number out of range")),
};
}
Ok(value)
}
fn parse_number_object(&mut self) -> PdfResult<PdfObject> {
self.skip_ws();
let start = self.pos;
while self
.data
.get(self.pos)
.is_some_and(|b| b.is_ascii_digit() || matches!(b, b'+' | b'-' | b'.' | b'e' | b'E'))
{
self.pos += 1;
}
if start == self.pos {
return Err(self.err("expected number"));
}
let tok = &self.data[start..self.pos];
if !tok.contains(&b'.') && !tok.contains(&b'e') && !tok.contains(&b'E') {
let (negative, digits) = match tok.first() {
Some(b'-') => (true, &tok[1..]),
Some(b'+') => (false, &tok[1..]),
_ => (false, tok),
};
if !digits.is_empty() && digits.iter().all(u8::is_ascii_digit) {
let mut acc: i64 = 0;
let mut overflow = false;
for &b in digits {
match acc
.checked_mul(10)
.and_then(|v| v.checked_sub(i64::from(b - b'0')))
{
Some(v) => acc = v,
None => {
overflow = true;
break;
}
}
}
if !overflow {
let value = if negative {
Some(acc)
} else {
acc.checked_neg()
};
if let Some(value) = value {
return Ok(PdfObject::Integer(value));
}
}
}
}
let s = std::str::from_utf8(tok).map_err(|_| self.err("invalid number bytes"))?;
let f: f64 = s
.parse()
.map_err(|_| self.err(format!("invalid number `{s}`")))?;
Ok(PdfObject::Real(f))
}
fn parse_name(&mut self) -> PdfResult<PdfName> {
if self.peek() != Some(b'/') {
return Err(self.err("expected name"));
}
self.bump();
let mut bytes = Vec::with_capacity(8);
loop {
match self.peek() {
None => break,
Some(b) if Self::is_ws(b) || Self::is_delim(b) => break,
Some(b'#') => {
self.bump();
let hex = [
self.bump().ok_or_else(|| self.err("truncated #-escape"))?,
self.bump().ok_or_else(|| self.err("truncated #-escape"))?,
];
let pair = std::str::from_utf8(&hex).map_err(|_| self.err("bad #-escape"))?;
let v = u8::from_str_radix(pair, 16).map_err(|_| self.err("bad #-escape"))?;
bytes.push(v);
}
Some(b) => {
self.bump();
bytes.push(b);
}
}
}
let name = match String::from_utf8(bytes) {
Ok(s) => s,
Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
};
Ok(PdfName(name))
}
fn enter_nested(&mut self) -> PdfResult<()> {
self.depth += 1;
if self.depth > self.max_depth {
return Err(self.err("recursion limit exceeded"));
}
Ok(())
}
fn exit_nested(&mut self) {
self.depth = self.depth.saturating_sub(1);
}
fn parse_literal_string(&mut self) -> PdfResult<PdfString> {
self.bump(); let mut out = Vec::new();
let mut depth = 1u32;
loop {
if out.len() >= self.limits.max_string_length {
return Err(self.err("string length exceeds limit"));
}
match self.bump() {
None => return Err(self.err("unterminated string")),
Some(b'(') => {
depth += 1;
out.push(b'(');
}
Some(b')') => {
depth -= 1;
if depth == 0 {
break;
}
out.push(b')');
}
Some(b'\\') => match self.bump() {
None => return Err(self.err("truncated escape")),
Some(b'n') => out.push(b'\n'),
Some(b'r') => out.push(b'\r'),
Some(b't') => out.push(b'\t'),
Some(b'b') => out.push(8),
Some(b'f') => out.push(12),
Some(b'(') => out.push(b'('),
Some(b')') => out.push(b')'),
Some(b'\\') => out.push(b'\\'),
Some(d @ b'0'..=b'7') => {
let mut v = d - b'0';
for _ in 0..2 {
match self.peek() {
Some(e @ b'0'..=b'7') => {
v = v * 8 + (e - b'0');
self.bump();
}
_ => break,
}
}
out.push(v);
}
Some(b'\r') => {
if self.peek() == Some(b'\n') {
self.bump();
}
}
Some(b'\n') => {}
Some(other) => out.push(other),
},
Some(b'\r') => {
if self.peek() == Some(b'\n') {
self.bump();
}
out.push(b'\n');
}
Some(b) => out.push(b),
}
}
Ok(PdfString(out))
}
fn parse_hex_string(&mut self) -> PdfResult<PdfString> {
self.bump(); let mut out = Vec::new();
let mut hi: Option<u8> = None;
loop {
if out.len() >= self.limits.max_string_length {
return Err(self.err("string length exceeds limit"));
}
match self.bump() {
None => return Err(self.err("unterminated hex string")),
Some(b'>') => {
if let Some(h) = hi {
out.push(h << 4);
}
break;
}
Some(b) if b.is_ascii_whitespace() => {}
Some(b) => {
let v = match b {
b'0'..=b'9' => b - b'0',
b'a'..=b'f' => b - b'a' + 10,
b'A'..=b'F' => b - b'A' + 10,
_ => return Err(self.err("invalid hex digit")),
};
match hi {
None => hi = Some(v),
Some(h) => {
out.push((h << 4) | v);
hi = None;
}
}
}
}
}
Ok(PdfString(out))
}
fn parse_array(&mut self) -> PdfResult<PdfArray> {
self.bump(); self.enter_nested()?;
let mut arr = PdfArray::new();
loop {
self.skip_ws();
match self.peek() {
None => return Err(self.err("unterminated array")),
Some(b']') => {
self.bump();
self.exit_nested();
break;
}
_ => {
if arr.len() >= self.limits.max_array_length {
return Err(self.err("array length exceeds limit"));
}
arr.push(self.parse_object()?);
}
}
}
Ok(arr)
}
fn parse_dict(&mut self) -> PdfResult<PdfDictionary> {
self.bump();
self.bump(); self.enter_nested()?;
let mut dict = PdfDictionary::new();
loop {
self.skip_ws();
match self.peek() {
None => return Err(self.err("unterminated dictionary")),
Some(b'>') => {
if self.data.get(self.pos + 1) == Some(&b'>') {
self.bump();
self.bump();
self.exit_nested();
break;
}
return Err(self.err("single `>` inside dictionary"));
}
Some(b'/') => {
if dict.len() >= self.limits.max_dict_entries {
return Err(self.err("dictionary entry count exceeds limit"));
}
let key = self.parse_name()?;
self.skip_ws();
let val = self.parse_object()?;
dict.insert(&key.0, val);
}
_ => return Err(self.err("expected /Name in dictionary")),
}
}
Ok(dict)
}
fn parse_object(&mut self) -> PdfResult<PdfObject> {
self.skip_ws();
match self.peek() {
None => Err(self.err("expected object")),
Some(b'[') => Ok(PdfObject::Array(self.parse_array()?)),
Some(b'<') => {
if self.data.get(self.pos + 1) == Some(&b'<') {
let dict = self.parse_dict()?;
self.skip_ws();
if self.match_kw(b"stream") {
if self.peek() == Some(b'\r') {
self.bump();
}
if self.peek() == Some(b'\n') {
self.bump();
}
let len = dict
.get_integer("Length")
.filter(|l| *l >= 0)
.ok_or_else(|| self.err("stream missing valid /Length"))?
as usize;
if len > self.limits.max_stream_size {
return Err(self.err("stream length exceeds limit"));
}
if self.pos + len > self.data.len() {
return Err(self.err("stream /Length exceeds file"));
}
let data = self.data[self.pos..self.pos + len].to_vec();
self.pos += len;
self.skip_ws();
if self.strict && !self.match_kw(b"endstream") {
return Err(self.err("expected `endstream`"));
}
Ok(PdfObject::Stream(PdfStream::with_dict(dict, data)))
} else {
Ok(PdfObject::Dictionary(dict))
}
} else {
Ok(PdfObject::String(self.parse_hex_string()?))
}
}
Some(b'(') => Ok(PdfObject::String(self.parse_literal_string()?)),
Some(b'/') => Ok(PdfObject::Name(self.parse_name()?)),
Some(b'+') | Some(b'-') | Some(b'.') | Some(b'0'..=b'9') => {
let first = self.parse_number_object()?;
self.skip_ws();
let next_is_number = matches!(
self.peek(),
Some(b'+') | Some(b'-') | Some(b'.') | Some(b'0'..=b'9')
);
if next_is_number {
let before_second = self.pos;
let second = self.parse_number_object()?;
self.skip_ws();
if self.peek() == Some(b'R') {
self.bump();
let num = first
.as_integer()
.ok_or_else(|| self.err("non-integer object number in reference"))?
as u32;
let gen = second
.as_integer()
.ok_or_else(|| self.err("non-integer generation in reference"))?
as u16;
return Ok(PdfObject::Reference(ObjectId::new(num, gen)));
}
self.pos = before_second;
}
Ok(first)
}
Some(b'T' | b't' | b'F' | b'f' | b'N' | b'n') => {
for kw in [b"true".as_slice(), b"True".as_slice(), b"TRUE".as_slice()] {
if self.match_kw(kw) {
return Ok(PdfObject::Boolean(true));
}
}
for kw in [
b"false".as_slice(),
b"False".as_slice(),
b"FALSE".as_slice(),
] {
if self.match_kw(kw) {
return Ok(PdfObject::Boolean(false));
}
}
for kw in [b"null".as_slice(), b"Null".as_slice(), b"NULL".as_slice()] {
if self.match_kw(kw) {
return Ok(PdfObject::Null);
}
}
Err(self.err("unknown keyword"))
}
Some(_) => Err(self.err("unexpected token")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serializer::Serializer;
#[test]
fn roundtrip_via_serializer() {
let mut doc = Document::new();
doc.set_catalog(ObjectId::new(1, 0));
doc.set_info(ObjectId::new(4, 0));
let mut catalog = PdfDictionary::new();
catalog.insert("Type", PdfObject::Name(PdfName::new("Catalog")));
catalog.insert("Pages", PdfObject::Reference(ObjectId::new(2, 0)));
doc.add_object(ObjectId::new(1, 0), PdfObject::Dictionary(catalog));
let mut pages = PdfDictionary::new();
pages.insert("Type", PdfObject::Name(PdfName::new("Pages")));
pages.insert("Count", PdfObject::Integer(1));
let mut kids = PdfArray::new();
kids.push(PdfObject::Reference(ObjectId::new(3, 0)));
pages.insert("Kids", PdfObject::Array(kids));
doc.add_object(ObjectId::new(2, 0), PdfObject::Dictionary(pages));
let mut page = PdfDictionary::new();
page.insert("Type", PdfObject::Name(PdfName::new("Page")));
let mut media = PdfArray::new();
media.push(PdfObject::Integer(0));
media.push(PdfObject::Integer(0));
media.push(PdfObject::Integer(612));
media.push(PdfObject::Integer(792));
page.insert("MediaBox", PdfObject::Array(media));
doc.add_object(ObjectId::new(3, 0), PdfObject::Dictionary(page));
let mut info = PdfDictionary::new();
info.insert("Title", PdfObject::String(PdfString::from_literal("t")));
doc.add_object(ObjectId::new(4, 0), PdfObject::Dictionary(info));
let mut buf = std::io::Cursor::new(Vec::new());
Serializer::new().serialize(&doc, &mut buf).unwrap();
let parsed = Parser::new().parse(buf.get_ref()).unwrap();
assert_eq!(parsed.catalog(), Some(ObjectId::new(1, 0)));
assert_eq!(parsed.info(), Some(ObjectId::new(4, 0)));
assert_eq!(parsed.objects().len(), 4);
let media_data = parsed
.get_object(ObjectId::new(3, 0))
.unwrap()
.as_dict()
.unwrap()
.get_array("MediaBox")
.unwrap()
.clone();
assert_eq!(
media_data.0,
vec![
PdfObject::Integer(0),
PdfObject::Integer(0),
PdfObject::Integer(612),
PdfObject::Integer(792),
]
);
}
#[test]
fn string_escaping_survives_serialize_parse_roundtrip() {
let cases: &[&[u8]] = &[
b"plain",
b"with (parens) and \\ backslash",
b"line1\nline2\r\nline3\rline4",
"café ☕ 漢字 𝄞".as_bytes(),
&[0x01, 0x07, 0x0b, 0x1b, 0x7f], b"tab\there\t",
b"",
];
for &input in cases {
let mut doc = Document::new();
doc.set_catalog(ObjectId::new(1, 0));
let mut dict = PdfDictionary::new();
dict.insert("S", PdfObject::String(PdfString::from_bytes(input)));
dict.insert("N", PdfObject::Real(1.5));
doc.add_object(ObjectId::new(1, 0), PdfObject::Dictionary(dict));
let mut buf = std::io::Cursor::new(Vec::new());
Serializer::new().serialize(&doc, &mut buf).unwrap();
let parsed = Parser::new().parse(buf.get_ref()).unwrap();
let round = parsed
.get_object(ObjectId::new(1, 0))
.unwrap()
.as_dict()
.unwrap()
.get_string_bytes("S")
.unwrap();
assert_eq!(round, input, "roundtrip failed for {input:?}");
}
}
#[test]
fn text_operator_escaping_roundtrips_tj_string() {
let tricky = vec![
b"(a)".to_vec(),
b"line1\nline2".to_vec(),
"café ☕".as_bytes().to_vec(),
b"\\back\\slash".to_vec(),
];
for text in tricky {
let mut doc = Document::new();
doc.set_catalog(ObjectId::new(1, 0));
let mut dict = PdfDictionary::new();
dict.insert("Length", PdfObject::Integer(text.len() as i64));
let stream = PdfStream::with_dict(dict, text.clone());
doc.add_object(ObjectId::new(1, 0), PdfObject::Stream(stream));
let mut buf = std::io::Cursor::new(Vec::new());
Serializer::new().serialize(&doc, &mut buf).unwrap();
let parsed = Parser::new().parse(buf.get_ref()).unwrap();
let round = parsed
.get_object(ObjectId::new(1, 0))
.unwrap()
.as_stream()
.unwrap()
.data
.clone();
assert_eq!(round, text, "stream roundtrip failed for {text:?}");
}
}
#[test]
fn parses_xref_stream_entries() {
let mut dict = PdfDictionary::new();
dict.insert("Type", PdfObject::Name(PdfName::new("XRef")));
dict.insert("Size", PdfObject::Integer(4));
let mut w = PdfArray::new();
w.push(PdfObject::Integer(1));
w.push(PdfObject::Integer(4));
w.push(PdfObject::Integer(2));
dict.insert("W", PdfObject::Array(w));
let mut data = Vec::new();
data.extend_from_slice(&[0, 0, 0, 0, 0, 0xff, 0xff]);
data.extend_from_slice(&[1, 0, 0, 0, 15, 0, 0]);
data.extend_from_slice(&[2, 0, 0, 0, 10, 0, 3]);
data.extend_from_slice(&[1, 0, 0, 3, 232, 0, 0]);
let entries = parse_xref_stream_entries(&data, &dict, 0).unwrap();
assert_eq!(entries.len(), 4);
assert!(matches!(
entries[&0].1,
XrefEntry::Free { generation: 65535 }
));
assert!(matches!(
entries[&1].1,
XrefEntry::Used {
offset: 15,
generation: 0
}
));
assert!(matches!(
entries[&2].1,
XrefEntry::Compressed {
stream_number: 10,
index: 3
}
));
assert!(matches!(
entries[&3].1,
XrefEntry::Used {
offset: 1000,
generation: 0
}
));
}
#[test]
fn decodes_flate_streams() {
let plain = b"BT /F1 12 Tf 10 20 Td (hi) Tj ET";
let mut dict = PdfDictionary::new();
dict.insert("Filter", PdfObject::Name(PdfName::new("FlateDecode")));
let encoded = crate::stream::StreamEncoder::new()
.encode(plain, crate::stream::StreamFilter::Flate)
.unwrap();
let stream = PdfStream::with_dict(dict, encoded);
let decoded = Parser::new().decode_stream(&stream).unwrap();
assert_eq!(decoded, plain);
}
#[test]
fn parses_object_stream_objects() {
let mut data = Vec::new();
data.extend_from_slice(b"7 0 8 6");
while data.len() < 12 {
data.push(b' ');
}
data.extend_from_slice(b"42 ");
while data.len() < 18 {
data.push(b' ');
}
data.extend_from_slice(b"<< /A (x) >>");
let (num, obj) = parse_object_stream(&data, 2, 12, 0, ParserLimits::default()).unwrap();
assert_eq!(num, 7);
assert_eq!(obj, PdfObject::Integer(42));
let (num, obj) = parse_object_stream(&data, 2, 12, 1, ParserLimits::default()).unwrap();
assert_eq!(num, 8);
assert_eq!(
obj.as_dict().unwrap().get("A"),
Some(&PdfObject::String(PdfString::from_literal("x")))
);
assert!(parse_object_stream(&data, 2, 12, 9, ParserLimits::default()).is_none());
}
#[test]
fn parses_strings_names_and_numbers() {
let mut lx = Lexer::new(
b"(a\\(b\\)c) /A#20B 42 -1.5e2 true null <48656c6c6f>",
0,
ParseMode::Strict,
ParserLimits::default(),
);
assert_eq!(
lx.parse_object().unwrap(),
PdfObject::String(PdfString::from_bytes(&b"a(b)c"[..]))
);
lx.skip_ws();
assert_eq!(
lx.parse_object().unwrap(),
PdfObject::Name(PdfName::new("A B"))
);
lx.skip_ws();
assert_eq!(lx.parse_object().unwrap(), PdfObject::Integer(42));
lx.skip_ws();
match lx.parse_object().unwrap() {
PdfObject::Real(f) => assert!((f - -150.0).abs() < 0.001),
_ => panic!("expected real"),
}
lx.skip_ws();
assert_eq!(lx.parse_object().unwrap(), PdfObject::Boolean(true));
lx.skip_ws();
assert_eq!(lx.parse_object().unwrap(), PdfObject::Null);
lx.skip_ws();
assert_eq!(
lx.parse_object().unwrap(),
PdfObject::String(PdfString::from_bytes(&b"Hello"[..]))
);
}
fn tight_limits() -> ParserLimits {
ParserLimits {
max_objects: 4,
max_string_length: 8,
max_array_length: 4,
max_dict_entries: 4,
max_recursion_depth: 3,
max_stream_size: 16,
max_decoded_stream_size: 16,
}
}
#[test]
fn limits_reject_overlong_string() {
let mut lx = Lexer::new(b"(0123456789)", 0, ParseMode::Strict, tight_limits());
assert!(lx.parse_object().is_err());
}
#[test]
fn limits_reject_deep_nesting() {
let deep = b"[[[[[[[[[0]]]]]]]]]";
let mut lx = Lexer::new(deep, 0, ParseMode::Strict, tight_limits());
assert!(lx.parse_object().is_err());
}
#[test]
fn limits_reject_huge_array() {
let mut lx = Lexer::new(b"[1 2 3 4 5]", 0, ParseMode::Strict, tight_limits());
assert!(lx.parse_object().is_err());
}
#[test]
fn limits_reject_huge_stream_length() {
let input = b"<< /Length 1000 >>\nstream\n0123456789\nendstream";
let mut lx = Lexer::new(input, 0, ParseMode::Strict, tight_limits());
assert!(lx.parse_object().is_err());
}
#[test]
fn limits_reject_too_many_objects() {
let mut doc = Document::new();
for i in 1..=6u32 {
doc.add_object(ObjectId::new(i, 0), PdfObject::Integer(i as i64));
}
let mut buf = std::io::Cursor::new(Vec::new());
Serializer::new().serialize(&doc, &mut buf).unwrap();
let limits = ParserLimits {
max_objects: 4,
..tight_limits()
};
assert!(Parser::with_limits(limits).parse(buf.get_ref()).is_err());
}
#[test]
fn limits_reject_too_many_xref_entries() {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.7\n");
pdf.extend_from_slice(b"1 0 obj\n<<>>\nendobj\n");
let xref_at = pdf.len() as u64;
pdf.extend_from_slice(b"xref\n0 6\n");
pdf.extend_from_slice(b"0000000000 65535 f \n");
for i in 1..=5u64 {
pdf.extend_from_slice(format!("{i:010} 00000 n \n").as_bytes());
}
pdf.extend_from_slice(b"trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n");
pdf.extend_from_slice(format!("{xref_at}\n").as_bytes());
pdf.extend_from_slice(b"%%EOF");
let limits = ParserLimits {
max_objects: 4,
..tight_limits()
};
let err = Parser::with_limits(limits).parse(&pdf).unwrap_err();
assert!(
err.to_string().contains("xref entry count"),
"unexpected error: {err}"
);
}
struct XorShift(u64);
impl XorShift {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
}
#[test]
fn random_bytes_never_panic() {
let mut rng = XorShift(0x9E37_79B9_7F4A_7C15);
for _ in 0..5000 {
let len = (rng.next() % 512) as usize;
let bytes: Vec<u8> = (0..len).map(|_| (rng.next() & 0xff) as u8).collect();
let _ = Parser::new().parse(&bytes);
}
}
#[test]
fn byte_flipped_pdf_never_panics() {
let mut doc = Document::new();
doc.set_catalog(ObjectId::new(1, 0));
for i in 1..=4u32 {
let mut dict = PdfDictionary::new();
dict.insert("Type", PdfObject::Name(PdfName::new(&format!("T{i}"))));
dict.insert("N", PdfObject::Integer(i as i64));
doc.add_object(ObjectId::new(i, 0), PdfObject::Dictionary(dict));
}
let mut buf = std::io::Cursor::new(Vec::new());
Serializer::new().serialize(&doc, &mut buf).unwrap();
let original = buf.into_inner();
let mut rng = XorShift(0xD1B5_4A32_D192_ED03);
for _ in 0..1000 {
let mut mutated = original.clone();
let flips = 1 + (rng.next() % 4) as usize;
for _ in 0..flips {
let idx = (rng.next() as usize) % mutated.len().max(1);
mutated[idx] ^= 1 << (rng.next() % 8);
}
let _ = Parser::new().parse(&mutated);
}
}
#[test]
fn malformed_inputs_error_without_panicking() {
let cases: &[&[u8]] = &[
b"",
b"%PDF-1.7",
b"garbage",
b"%PDF-1.7\n1 0 obj<<>>endobj\nxref\n0 1\ntrailer\nstartxref\n0\n%%EOF",
b"%PDF-1.7\n1 0 obj\n(abc",
b"%PDF-1.7\n1 0 obj\n<< /Length -5 >>\nstream\nx",
&b"%PDF-1.7\n".repeat(2),
&[0xff, 0xfe, 0x00, 0x00, 0x41, 0x42],
];
for input in cases {
let _ = Parser::new().parse(input);
}
}
#[test]
fn stream_roundtrip_with_small_and_large_data() {
for size in [0, 1, 15, 16, 17, 1000] {
let payload: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let mut dict = PdfDictionary::new();
dict.insert("Filter", PdfObject::Name(PdfName::new("FlateDecode")));
let encoded = crate::stream::StreamEncoder::new()
.encode(&payload, crate::stream::StreamFilter::Flate)
.unwrap();
let stream = PdfStream::with_dict(dict, encoded);
let decoded = Parser::new().decode_stream(&stream).unwrap();
assert_eq!(decoded, payload, "size {size} mismatch");
}
}
#[test]
fn decoded_stream_size_is_bounded() {
let payload = vec![0u8; 1_000_000];
let mut dict = PdfDictionary::new();
dict.insert("Filter", PdfObject::Name(PdfName::new("FlateDecode")));
let encoded = crate::stream::StreamEncoder::new()
.encode(&payload, crate::stream::StreamFilter::Flate)
.unwrap();
assert!(encoded.len() < 2000, "test premise: bomb must compress");
let stream = PdfStream::with_dict(dict, encoded);
let limits = ParserLimits {
max_decoded_stream_size: 64 * 1024,
..ParserLimits::default()
};
if let Ok(d) = Parser::with_limits(limits).decode_stream(&stream) {
assert!(d.len() <= 64 * 1024 + 1);
}
}
}