use crate::error::PdfError;
use crate::objects::{Dict, Object, ObjectId, Stream};
use crate::reader::lex::{Lexer, Token, TokenKind};
pub trait LengthResolver {
fn resolve_length(&mut self, length_ref: ObjectId) -> Result<i64, PdfError>;
}
pub struct NoLengthResolver;
impl LengthResolver for NoLengthResolver {
fn resolve_length(&mut self, _length_ref: ObjectId) -> Result<i64, PdfError> {
Err(PdfError::other(
"PDF parser: stream /Length is an indirect reference but no resolver \
was supplied (call parse_indirect_with_length_resolver)",
))
}
}
impl<F> LengthResolver for F
where
F: FnMut(ObjectId) -> Result<i64, PdfError>,
{
fn resolve_length(&mut self, length_ref: ObjectId) -> Result<i64, PdfError> {
(self)(length_ref)
}
}
pub struct Parser<'a> {
lex: Lexer<'a>,
peeked: Option<Token<'a>>,
}
impl<'a> Parser<'a> {
pub fn new(input: &'a [u8]) -> Self {
Self {
lex: Lexer::new(input),
peeked: None,
}
}
pub fn from_lexer(lex: Lexer<'a>) -> Self {
Self { lex, peeked: None }
}
pub fn lexer_mut(&mut self) -> &mut Lexer<'a> {
self.peeked = None;
&mut self.lex
}
pub fn position(&self) -> usize {
if let Some(t) = &self.peeked {
t.start
} else {
self.lex.position()
}
}
fn next(&mut self) -> Result<Option<Token<'a>>, PdfError> {
if let Some(t) = self.peeked.take() {
return Ok(Some(t));
}
self.lex.next_token()
}
fn peek(&mut self) -> Result<Option<&Token<'a>>, PdfError> {
if self.peeked.is_none() {
self.peeked = self.lex.next_token()?;
}
Ok(self.peeked.as_ref())
}
pub fn parse_object(&mut self) -> Result<Option<Object>, PdfError> {
self.parse_object_with_length_resolver(&mut NoLengthResolver)
}
pub fn parse_object_with_length_resolver(
&mut self,
resolver: &mut dyn LengthResolver,
) -> Result<Option<Object>, PdfError> {
let Some(tok) = self.next()? else {
return Ok(None);
};
Ok(Some(self.object_from_token(tok, resolver)?))
}
fn object_from_token(
&mut self,
tok: Token<'a>,
resolver: &mut dyn LengthResolver,
) -> Result<Object, PdfError> {
match tok.kind {
TokenKind::Integer(n) => self.maybe_indirect_ref(n, tok.end),
TokenKind::Real(f) => Ok(Object::Real(f)),
TokenKind::Name(bytes) => Ok(Object::Name(String::from_utf8(bytes).map_err(|_| {
PdfError::other(format!("PDF parser: non-UTF-8 name at byte {}", tok.start))
})?)),
TokenKind::LiteralString(bytes) => Ok(Object::LiteralString(bytes)),
TokenKind::HexString(bytes) => Ok(Object::HexString(bytes)),
TokenKind::ArrayStart => self.parse_array(resolver),
TokenKind::DictStart => self.parse_dict_or_stream(tok.start, resolver),
TokenKind::Keyword(kw) => match kw {
b"true" => Ok(Object::Bool(true)),
b"false" => Ok(Object::Bool(false)),
b"null" => Ok(Object::Null),
other => Err(PdfError::other(format!(
"PDF parser: unexpected keyword `{}` at byte {}",
String::from_utf8_lossy(other),
tok.start
))),
},
TokenKind::ArrayEnd => Err(PdfError::other(format!(
"PDF parser: unexpected `]` at byte {}",
tok.start
))),
TokenKind::DictEnd => Err(PdfError::other(format!(
"PDF parser: unexpected `>>` at byte {}",
tok.start
))),
}
}
fn maybe_indirect_ref(&mut self, n: i64, _start_end: usize) -> Result<Object, PdfError> {
let saved_pos = self.lex.position();
let saved_peeked = self.peeked.clone();
let t2 = self.next()?;
let Some(t2) = t2 else {
return Ok(Object::Integer(n));
};
let TokenKind::Integer(gen) = t2.kind else {
self.peeked = Some(t2);
let _ = saved_pos;
let _ = saved_peeked;
return Ok(Object::Integer(n));
};
let t3 = self.next()?;
let Some(t3) = t3 else {
self.lex.seek(saved_pos);
self.peeked = saved_peeked;
return Ok(Object::Integer(n));
};
let TokenKind::Keyword(b"R") = t3.kind else {
self.lex.seek(saved_pos);
self.peeked = saved_peeked;
return Ok(Object::Integer(n));
};
if n < 1 || n > u32::MAX as i64 || gen < 0 || gen > u16::MAX as i64 {
return Err(PdfError::other(format!(
"PDF parser: indirect ref out of range `{n} {gen} R`"
)));
}
Ok(Object::Reference(ObjectId {
number: n as u32,
generation: gen as u16,
}))
}
fn parse_array(&mut self, resolver: &mut dyn LengthResolver) -> Result<Object, PdfError> {
let mut items = Vec::new();
loop {
let tok = self.next()?.ok_or_else(|| {
PdfError::other("PDF parser: unterminated array (EOF before `]`)")
})?;
if let TokenKind::ArrayEnd = tok.kind {
return Ok(Object::Array(items));
}
items.push(self.object_from_token(tok, resolver)?);
}
}
fn parse_dict_or_stream(
&mut self,
start: usize,
resolver: &mut dyn LengthResolver,
) -> Result<Object, PdfError> {
let mut dict = Dict::new();
loop {
let tok = self.next()?.ok_or_else(|| {
PdfError::other(format!(
"PDF parser: unterminated dict starting at byte {start} (EOF before `>>`)"
))
})?;
if let TokenKind::DictEnd = tok.kind {
break;
}
let TokenKind::Name(key_bytes) = tok.kind else {
return Err(PdfError::other(format!(
"PDF parser: dict key must be a Name at byte {} (got {:?})",
tok.start, tok.kind
)));
};
let key = String::from_utf8(key_bytes).map_err(|_| {
PdfError::other(format!(
"PDF parser: non-UTF-8 dict key at byte {}",
tok.start
))
})?;
let val = self.parse_object()?.ok_or_else(|| {
PdfError::other(format!(
"PDF parser: dict key `{key}` at byte {} has no value",
tok.start
))
})?;
dict.set(&key, val);
}
let after = self.peek()?.cloned();
if let Some(t) = after {
if let TokenKind::Keyword(b"stream") = t.kind {
let _ = self.next()?;
let raw = self.lex.input();
let mut data_start = t.end;
if data_start < raw.len() && raw[data_start] == b'\r' {
data_start += 1;
}
if data_start < raw.len() && raw[data_start] == b'\n' {
data_start += 1;
}
let length_obj = dict.entries().iter().find_map(|(k, v)| {
if k == "Length" {
Some(v.clone())
} else {
None
}
});
let len: usize = match length_obj {
Some(Object::Integer(n)) if n >= 0 => n as usize,
Some(Object::Reference(id)) => {
let resolved = resolver.resolve_length(id)?;
if resolved < 0 {
return Err(PdfError::other(format!(
"PDF parser: indirect /Length {id:?} resolved to \
negative integer {resolved}"
)));
}
dict.set("Length", Object::Integer(resolved));
resolved as usize
}
Some(other) => {
return Err(PdfError::other(format!(
"PDF parser: stream /Length must be a non-negative integer (got {other:?})"
)));
}
None => {
return Err(PdfError::other(
"PDF parser: stream object missing required /Length entry",
));
}
};
let data_end = data_start.saturating_add(len).min(raw.len());
let data = self.lex.slice(data_start, data_end).to_vec();
self.lex.seek(data_end);
self.peeked = None;
let endstream = self
.next()?
.ok_or_else(|| PdfError::other("PDF parser: stream missing `endstream`"))?;
let TokenKind::Keyword(b"endstream") = endstream.kind else {
return Err(PdfError::other(format!(
"PDF parser: expected `endstream` after stream body at byte {} (got {:?})",
endstream.start, endstream.kind
)));
};
return Ok(Object::Stream(Stream::new(dict, data)));
}
}
Ok(Object::Dict(dict))
}
pub fn parse_indirect(&mut self) -> Result<(ObjectId, Object), PdfError> {
self.parse_indirect_with_length_resolver(&mut NoLengthResolver)
}
pub fn parse_indirect_with_length_resolver(
&mut self,
resolver: &mut dyn LengthResolver,
) -> Result<(ObjectId, Object), PdfError> {
let n = self.expect_integer("indirect-object number")?;
let gen = self.expect_integer("indirect-object generation")?;
let obj_kw = self
.next()?
.ok_or_else(|| PdfError::other("PDF parser: unexpected EOF before `obj` keyword"))?;
let TokenKind::Keyword(b"obj") = obj_kw.kind else {
return Err(PdfError::other(format!(
"PDF parser: expected `obj` keyword at byte {} (got {:?})",
obj_kw.start, obj_kw.kind
)));
};
let body = self
.parse_object_with_length_resolver(resolver)?
.ok_or_else(|| PdfError::other("PDF parser: indirect object missing body"))?;
let endobj = self
.next()?
.ok_or_else(|| PdfError::other("PDF parser: indirect object missing `endobj`"))?;
let TokenKind::Keyword(b"endobj") = endobj.kind else {
return Err(PdfError::other(format!(
"PDF parser: expected `endobj` at byte {} (got {:?})",
endobj.start, endobj.kind
)));
};
if n < 1 || n > u32::MAX as i64 || !(0..=u16::MAX as i64).contains(&gen) {
return Err(PdfError::other(format!(
"PDF parser: indirect-object id `{n} {gen}` out of range"
)));
}
Ok((
ObjectId {
number: n as u32,
generation: gen as u16,
},
body,
))
}
fn expect_integer(&mut self, what: &str) -> Result<i64, PdfError> {
let tok = self
.next()?
.ok_or_else(|| PdfError::other(format!("PDF parser: expected {what}, got EOF")))?;
match tok.kind {
TokenKind::Integer(n) => Ok(n),
other => Err(PdfError::other(format!(
"PDF parser: expected {what} (integer), got {other:?} at byte {}",
tok.start
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_one(input: &[u8]) -> Object {
Parser::new(input)
.parse_object()
.unwrap()
.expect("parsed object")
}
#[test]
fn primitives_parse_to_objects() {
assert!(matches!(parse_one(b"true"), Object::Bool(true)));
assert!(matches!(parse_one(b"false"), Object::Bool(false)));
assert!(matches!(parse_one(b"null"), Object::Null));
assert!(matches!(parse_one(b"42"), Object::Integer(42)));
match parse_one(b"2.5") {
Object::Real(f) => assert!((f - 2.5).abs() < 1e-9),
other => panic!("expected real, got {other:?}"),
}
match parse_one(b"(hello)") {
Object::LiteralString(b) => assert_eq!(b, b"hello".to_vec()),
other => panic!("expected literal string, got {other:?}"),
}
match parse_one(b"<48656C6C6F>") {
Object::HexString(b) => assert_eq!(b, b"Hello".to_vec()),
other => panic!("expected hex string, got {other:?}"),
}
match parse_one(b"/Pages") {
Object::Name(s) => assert_eq!(s, "Pages"),
other => panic!("expected name, got {other:?}"),
}
}
#[test]
fn array_parses_recursively() {
let arr = parse_one(b"[1 2.5 (a) /b [3 4]]");
let Object::Array(items) = arr else {
panic!("expected array")
};
assert_eq!(items.len(), 5);
assert!(matches!(items[0], Object::Integer(1)));
assert!(matches!(items[1], Object::Real(_)));
assert!(matches!(items[2], Object::LiteralString(_)));
assert!(matches!(items[3], Object::Name(_)));
assert!(matches!(items[4], Object::Array(_)));
}
#[test]
fn dict_parses_with_named_keys() {
let d = parse_one(b"<< /Type /Page /Count 3 /Kids [1 2 3] >>");
let Object::Dict(d) = d else {
panic!("expected dict")
};
let entries = d.entries();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].0, "Type");
assert_eq!(entries[1].0, "Count");
assert_eq!(entries[2].0, "Kids");
}
#[test]
fn indirect_reference_recognised() {
let r = parse_one(b"5 0 R");
let Object::Reference(id) = r else {
panic!("expected ref")
};
assert_eq!(id.number, 5);
assert_eq!(id.generation, 0);
}
#[test]
fn integer_followed_by_non_ref_does_not_become_ref() {
let arr = parse_one(b"[5 0]");
let Object::Array(items) = arr else {
panic!("expected array")
};
assert_eq!(items.len(), 2);
assert!(matches!(items[0], Object::Integer(5)));
assert!(matches!(items[1], Object::Integer(0)));
}
#[test]
fn indirect_object_round_trip() {
let input = b"3 0 obj\n<< /Type /Page >>\nendobj\n";
let mut p = Parser::new(input);
let (id, body) = p.parse_indirect().unwrap();
assert_eq!(id.number, 3);
assert!(matches!(body, Object::Dict(_)));
}
#[test]
fn stream_object_extracts_body_per_length() {
let input = b"4 0 obj\n<< /Length 5 >>\nstream\nABCDE\nendstream\nendobj\n";
let mut p = Parser::new(input);
let (_, body) = p.parse_indirect().unwrap();
let Object::Stream(s) = body else {
panic!("expected stream")
};
assert_eq!(s.data, b"ABCDE".to_vec());
}
#[test]
fn stream_with_crlf_eol_marker() {
let mut input = Vec::new();
input.extend_from_slice(b"4 0 obj\n<< /Length 3 >>\nstream\r\nXYZ\nendstream\nendobj\n");
let mut p = Parser::new(&input);
let (_, body) = p.parse_indirect().unwrap();
let Object::Stream(s) = body else {
panic!("expected stream")
};
assert_eq!(s.data, b"XYZ".to_vec());
}
#[test]
fn stream_indirect_length_without_resolver_is_rejected() {
let input = b"4 0 obj\n<< /Length 7 0 R >>\nstream\nXYZ\nendstream\nendobj\n";
let mut p = Parser::new(input);
let err = p.parse_indirect().unwrap_err();
assert!(format!("{err}").contains("indirect reference"));
}
#[test]
fn stream_indirect_length_with_resolver_resolves() {
let input = b"4 0 obj\n<< /Length 7 0 R >>\nstream\nXYZ\nendstream\nendobj\n";
let mut p = Parser::new(input);
let mut resolver = |id: ObjectId| -> Result<i64, PdfError> {
assert_eq!(id, ObjectId::new(7));
Ok(3)
};
let (id, body) = p
.parse_indirect_with_length_resolver(&mut resolver)
.unwrap();
assert_eq!(id, ObjectId::new(4));
let Object::Stream(s) = body else {
panic!("expected stream, got {body:?}")
};
assert_eq!(s.data, b"XYZ".to_vec());
let length = s
.dict
.entries()
.iter()
.find(|(k, _)| k == "Length")
.map(|(_, v)| v.clone());
assert!(matches!(length, Some(Object::Integer(3))));
}
#[test]
fn stream_indirect_length_negative_resolution_errors() {
let input = b"4 0 obj\n<< /Length 7 0 R >>\nstream\nXYZ\nendstream\nendobj\n";
let mut p = Parser::new(input);
let mut resolver = |_id: ObjectId| -> Result<i64, PdfError> { Ok(-1) };
let err = p
.parse_indirect_with_length_resolver(&mut resolver)
.unwrap_err();
assert!(format!("{err}").contains("negative"));
}
#[test]
fn stream_indirect_length_resolver_error_propagates() {
let input = b"4 0 obj\n<< /Length 7 0 R >>\nstream\nXYZ\nendstream\nendobj\n";
let mut p = Parser::new(input);
let mut resolver =
|_id: ObjectId| -> Result<i64, PdfError> { Err(PdfError::other("test: not found")) };
let err = p
.parse_indirect_with_length_resolver(&mut resolver)
.unwrap_err();
assert!(format!("{err}").contains("not found"));
}
#[test]
fn stream_missing_length_is_rejected() {
let input = b"4 0 obj\n<< /Type /XObject >>\nstream\nXYZ\nendstream\nendobj\n";
let mut p = Parser::new(input);
let err = p.parse_indirect().unwrap_err();
assert!(format!("{err}").contains("/Length"));
}
#[test]
fn nested_dict_in_array() {
let arr = parse_one(b"[<< /A 1 >> << /B 2 >>]");
let Object::Array(items) = arr else {
panic!("expected array")
};
assert_eq!(items.len(), 2);
assert!(matches!(items[0], Object::Dict(_)));
assert!(matches!(items[1], Object::Dict(_)));
}
#[test]
fn dict_with_indirect_reference_value() {
let d = parse_one(b"<< /Pages 2 0 R >>");
let Object::Dict(d) = d else {
panic!("expected dict")
};
let entries = d.entries();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0, "Pages");
assert!(matches!(entries[0].1, Object::Reference(_)));
}
}