use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
use pdfrum_object::{
Array, ByteSpan, Dict, Name, ObjRef, Object, PdfString, Resolve, Stream, name_decode, names,
};
use crate::error::Error;
use crate::lexer::{
Delim, Lexer, Token, WordBoundary, atoui, find_word, is_line_ending, is_whitespace,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Strictness {
Loose,
Strict,
}
pub struct Context<'r, R: Resolve + ?Sized> {
pub limits: &'r Limits,
pub diags: &'r mut Diagnostics,
pub file: Option<&'r ByteSpan>,
pub store: Option<&'r R>,
}
impl<R: Resolve + ?Sized> Context<'_, R> {
fn note(&mut self, severity: Severity, what: DiagKind, at: usize) {
self.diags.record(severity, what, Some(at as u64));
}
}
pub fn parse_object<R: Resolve + ?Sized>(
lx: &mut Lexer<'_>,
limits: &Limits,
diags: &mut Diagnostics,
strictness: Strictness,
store: &R,
) -> Result<Object, Error> {
let mut ctx = Context {
limits,
diags,
file: None,
store: Some(store),
};
body(lx, &mut ctx, strictness, 0)
}
pub(crate) fn body<R: Resolve + ?Sized>(
lx: &mut Lexer<'_>,
ctx: &mut Context<'_, R>,
strictness: Strictness,
depth: u32,
) -> Result<Object, Error> {
if depth >= ctx.limits.max_object_nesting {
return Err(Error::TooDeep(ctx.limits.max_object_nesting));
}
let start = lx.pos();
let token = lx.next_word(ctx.limits);
match token {
Token::Number(word) => number_or_reference(lx, ctx, word),
Token::Name(payload) => Ok(Object::Name(Name::decode(payload))),
Token::Keyword(b"true") => Ok(Object::Bool(true)),
Token::Keyword(b"false") => Ok(Object::Bool(false)),
Token::Keyword(b"null") => Ok(Object::Null),
Token::Delim(Delim::StringOpen) => Ok(Object::Str(PdfString::literal(
lx.read_literal_string().as_ref(),
))),
Token::Delim(Delim::HexOpen) => Ok(Object::Str(PdfString::hex(lx.read_hex_string()))),
Token::Delim(Delim::ArrayOpen) => array(lx, ctx, strictness, depth),
Token::Delim(Delim::DictOpen) => dictionary(lx, ctx, strictness, depth),
Token::Delim(Delim::DictClose) => {
lx.seek(start);
Err(Error::NoObject(start as u64))
}
_ => Err(Error::NoObject(start as u64)),
}
}
fn number_or_reference<R: Resolve + ?Sized>(
lx: &mut Lexer<'_>,
ctx: &mut Context<'_, R>,
word: &[u8],
) -> Result<Object, Error> {
let after_number = lx.pos();
let second = lx.next_word(ctx.limits);
if second.is_number() {
let generation = second.bytes();
if lx.next_word(ctx.limits).is_keyword(b"R") {
let num = atoui(word);
let generation = u16::try_from(atoui(generation)).unwrap_or(u16::MAX);
let reference = ObjRef::new(num, generation);
if reference.is_invalid() {
return Err(Error::NoObject(after_number as u64));
}
return Ok(Object::Ref(reference));
}
}
lx.seek(after_number);
Ok(parse_number(word))
}
fn parse_number(word: &[u8]) -> Object {
let text = String::from_utf8_lossy(word);
if word.contains(&b'.') {
Object::Real(parse_real(&text))
} else {
Object::Int(parse_int(&text))
}
}
fn split_sign(text: &str) -> (bool, &str) {
match text.as_bytes().first() {
Some(b'-') => (true, text.get(1..).unwrap_or_default()),
Some(b'+') => (false, text.get(1..).unwrap_or_default()),
_ => (false, text),
}
}
fn parse_int(text: &str) -> i64 {
let signed = matches!(text.as_bytes().first(), Some(b'-' | b'+'));
let (negative, digits) = split_sign(text);
let mut magnitude: Option<u32> = Some(0);
for byte in digits.bytes().take_while(u8::is_ascii_digit) {
magnitude = magnitude
.and_then(|acc| acc.checked_mul(10))
.and_then(|acc| acc.checked_add(u32::from(byte - b'0')));
}
let magnitude = magnitude.unwrap_or(0);
if !signed {
return i64::from(magnitude);
}
let limit = i64::from(i32::MAX) + i64::from(negative);
let magnitude = i64::from(magnitude);
if magnitude > limit {
return 0;
}
if negative { -magnitude } else { magnitude }
}
fn parse_real(text: &str) -> f32 {
let (negative, digits) = split_sign(text);
let mut cleaned = String::with_capacity(digits.len());
let mut seen_dot = false;
for c in digits.chars() {
match c {
'.' if seen_dot => break,
'.' => {
seen_dot = true;
cleaned.push('.');
}
'0'..='9' => cleaned.push(c),
_ => break,
}
}
let magnitude = cleaned.parse::<f32>().unwrap_or(0.0);
if negative { -magnitude } else { magnitude }
}
fn array<R: Resolve + ?Sized>(
lx: &mut Lexer<'_>,
ctx: &mut Context<'_, R>,
strictness: Strictness,
depth: u32,
) -> Result<Object, Error> {
let mut out = Array::new();
loop {
let before = lx.pos();
match body(lx, ctx, Strictness::Loose, depth + 1) {
Ok(Object::Stream(_)) => {
ctx.note(
Severity::Recovered,
DiagKind::StreamInCompositeDropped,
before,
);
}
Ok(value) => {
if out.len() >= ctx.limits.max_array_len {
return Ok(Object::Array(out));
}
out.push(value);
}
Err(_) => {
let mut probe = Lexer::at(lx.bytes(), before);
let stopper = probe.next_word(ctx.limits);
let closed = matches!(stopper, Token::Delim(Delim::ArrayClose));
if !closed {
ctx.note(Severity::Suspicious, DiagKind::MalformedArray, before);
if strictness == Strictness::Strict {
return Err(Error::NoObject(before as u64));
}
}
return Ok(Object::Array(out));
}
}
}
}
fn dictionary<R: Resolve + ?Sized>(
lx: &mut Lexer<'_>,
ctx: &mut Context<'_, R>,
strictness: Strictness,
depth: u32,
) -> Result<Object, Error> {
let mut dict = Dict::new();
loop {
let key_start = lx.pos();
let token = lx.next_word(ctx.limits);
match token {
Token::Eof => return Err(Error::NoObject(key_start as u64)),
Token::Delim(Delim::DictClose) => break,
Token::Keyword(b"endobj") => {
ctx.note(Severity::Suspicious, DiagKind::MalformedDict, key_start);
lx.seek(key_start);
break;
}
Token::Name(payload) => {
let key = name_decode(payload);
let value_start = lx.pos();
match body(lx, ctx, Strictness::Loose, depth + 1) {
Ok(Object::Stream(_)) => {
ctx.note(
Severity::Recovered,
DiagKind::StreamInCompositeDropped,
value_start,
);
}
Ok(value) => {
if key.is_empty() {
ctx.note(Severity::Suspicious, DiagKind::MalformedDict, key_start);
} else {
dict.push(Name::new(key), value);
}
}
Err(_) => {
ctx.note(Severity::Suspicious, DiagKind::MalformedDict, value_start);
if strictness == Strictness::Strict {
lx.to_next_line();
return Err(Error::NoObject(value_start as u64));
}
}
}
}
_ => {
ctx.note(Severity::Suspicious, DiagKind::MalformedDict, key_start);
}
}
}
let after_dict = lx.pos();
if lx.next_word(ctx.limits).is_keyword(b"stream") {
return read_stream(lx, ctx, dict);
}
lx.seek(after_dict);
Ok(Object::Dict(dict))
}
pub(crate) fn read_stream<R: Resolve + ?Sized>(
lx: &mut Lexer<'_>,
ctx: &mut Context<'_, R>,
dict: Dict,
) -> Result<Object, Error> {
let declared = declared_length(&dict, ctx);
lx.to_next_line();
let data_start = lx.pos();
let file_len = lx.bytes().len();
let mut length = declared.filter(|&n| {
n == 0 || data_start.checked_add(n).is_some_and(|end| end < file_len)
});
if let Some(n) = length {
lx.seek(data_start.saturating_add(n));
lx.skip_eol_marker();
let word = lx.next_word(ctx.limits);
if !word.bytes().starts_with(b"endstream") {
ctx.note(Severity::Recovered, DiagKind::LengthMismatch, data_start);
length = None;
lx.seek(data_start);
}
} else if declared.is_some() {
ctx.note(Severity::Recovered, DiagKind::LengthMismatch, data_start);
}
let length = match length {
Some(n) => n,
None => match scan_for_end(lx.bytes(), data_start) {
Some(end) => {
ctx.note(Severity::Recovered, DiagKind::KeywordResync, data_start);
end.saturating_sub(data_start)
}
None => return Err(Error::NoObject(data_start as u64)),
},
};
let data_end = data_start.saturating_add(length).min(file_len);
let data = match ctx.file {
Some(file) => file
.subspan(data_start..data_end)
.unwrap_or_else(|_| ByteSpan::empty()),
None => ByteSpan::from(
lx.bytes()
.get(data_start..data_end)
.unwrap_or_default()
.to_vec(),
),
};
lx.seek(data_end);
resync_after_stream(lx, ctx);
Ok(Object::Stream(Box::new(Stream::new(dict, data))))
}
fn declared_length<R: Resolve + ?Sized>(dict: &Dict, ctx: &mut Context<'_, R>) -> Option<usize> {
let raw = dict.raw(names::LENGTH)?;
let value = match raw {
Object::Ref(r) => {
let store = ctx.store?;
store.fetch(*r).ok()?.as_int()?
}
other => other.as_number()?.as_int()?,
};
usize::try_from(value).ok()
}
fn scan_for_end(bytes: &[u8], start: usize) -> Option<usize> {
let endstream = find_word(bytes, b"endstream", start, WordBoundary::WhitespaceOnly);
let endobj = find_word(bytes, b"endobj", start, WordBoundary::WhitespaceOnly);
let keyword = match (endstream, endobj) {
(Some(a), Some(b)) => a.min(b),
(Some(a), None) => a,
(None, Some(b)) => b,
(None, None) => return None,
};
let end = trim_trailing_eol(bytes, keyword);
(end >= start).then_some(end)
}
fn trim_trailing_eol(bytes: &[u8], pos: usize) -> usize {
match bytes.get(pos.wrapping_sub(1)) {
Some(b'\n') => {
if bytes.get(pos.wrapping_sub(2)) == Some(&b'\r') {
pos.saturating_sub(2)
} else {
pos.saturating_sub(1)
}
}
Some(b'\r') => pos.saturating_sub(1),
_ => pos,
}
}
fn resync_after_stream<R: Resolve + ?Sized>(lx: &mut Lexer<'_>, ctx: &mut Context<'_, R>) {
let before_keyword = lx.pos();
let word = lx.next_word(ctx.limits);
if word.bytes() != b"endobj" {
return;
}
let mut probe = Lexer::at(lx.bytes(), lx.pos());
while probe
.peek_byte()
.is_some_and(|b| is_whitespace(b) && !is_line_ending(b))
{
probe.seek(probe.pos() + 1);
}
if probe.skip_eol_marker() > 0 {
ctx.note(Severity::Recovered, DiagKind::KeywordResync, before_keyword);
lx.seek(before_keyword);
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Indirect {
pub num: u32,
pub generation: u16,
pub object: Object,
}
pub fn parse_indirect_object<R: Resolve + ?Sized>(
lx: &mut Lexer<'_>,
limits: &Limits,
diags: &mut Diagnostics,
strictness: Strictness,
store: &R,
) -> Result<Indirect, Error> {
let mut ctx = Context {
limits,
diags,
file: None,
store: Some(store),
};
indirect(lx, &mut ctx, strictness, 0)
}
pub(crate) fn indirect<R: Resolve + ?Sized>(
lx: &mut Lexer<'_>,
ctx: &mut Context<'_, R>,
strictness: Strictness,
depth: u32,
) -> Result<Indirect, Error> {
let start = lx.pos();
let fail = |lx: &mut Lexer<'_>| {
lx.seek(start);
Err(Error::NoObject(start as u64))
};
let Token::Number(num_word) = lx.next_word(ctx.limits) else {
return fail(lx);
};
let Token::Number(gen_word) = lx.next_word(ctx.limits) else {
return fail(lx);
};
if !lx.next_word(ctx.limits).is_keyword(b"obj") {
return fail(lx);
}
let object = match body(lx, ctx, strictness, depth) {
Ok(o) => o,
Err(_) if strictness == Strictness::Loose => Object::Null,
Err(e) => return Err(e),
};
Ok(Indirect {
num: atoui(num_word),
generation: u16::try_from(atoui(gen_word)).unwrap_or(u16::MAX),
object,
})
}
#[cfg(test)]
mod tests {
use super::{Context, Strictness, body, indirect, parse_object};
use crate::error::Error;
use crate::lexer::Lexer;
use pdfrum_common::{DiagKind, Diagnostics, Limits};
use pdfrum_object::ByteSpan;
use pdfrum_object::{NoResolve, ObjRef, Object, Resolve, names};
use std::sync::Arc;
fn parse(input: &[u8]) -> Result<Object, Error> {
let mut diags = Diagnostics::default();
parse_object(
&mut Lexer::new(input),
&Limits::default(),
&mut diags,
Strictness::Loose,
&NoResolve,
)
}
fn parse_strict(input: &[u8]) -> Result<Object, Error> {
let mut diags = Diagnostics::default();
parse_object(
&mut Lexer::new(input),
&Limits::default(),
&mut diags,
Strictness::Strict,
&NoResolve,
)
}
struct Store(std::collections::HashMap<u32, Arc<Object>>);
impl Resolve for Store {
fn fetch(&self, r: ObjRef) -> Result<Arc<Object>, pdfrum_object::Error> {
self.0
.get(&r.num)
.cloned()
.ok_or(pdfrum_object::Error::UnresolvedRef(r))
}
}
fn parse_in_file<R: Resolve + ?Sized>(
input: &[u8],
store: &R,
diags: &mut Diagnostics,
) -> Result<Object, Error> {
let file = ByteSpan::from(input.to_vec());
let limits = Limits::default();
let mut ctx = Context {
limits: &limits,
diags,
file: Some(&file),
store: Some(store),
};
let mut lx = Lexer::new(&file);
body(&mut lx, &mut ctx, Strictness::Loose, 0)
}
#[test]
fn scalars() {
assert_eq!(parse(b"true"), Ok(Object::Bool(true)));
assert_eq!(parse(b"false"), Ok(Object::Bool(false)));
assert_eq!(parse(b"null"), Ok(Object::Null));
assert_eq!(parse(b"42"), Ok(Object::Int(42)));
assert_eq!(parse(b"-17"), Ok(Object::Int(-17)));
assert_eq!(parse(b"3.5"), Ok(Object::Real(3.5)));
assert_eq!(parse(b"-0.25"), Ok(Object::Real(-0.25)));
assert_eq!(parse(b"1.2.3"), Ok(Object::Real(1.2)));
assert_eq!(parse(b"--37"), Ok(Object::Int(0)));
}
#[test]
fn integers_outside_the_c_int_range_are_zero() {
use pdfrum_object::INT_RANGE;
assert_eq!(parse(b"4294967295"), Ok(Object::Int(4_294_967_295)));
assert_eq!(parse(b"4294967296"), Ok(Object::Int(0)));
assert_eq!(parse(b"99999999999999999999999999"), Ok(Object::Int(0)));
assert_eq!(parse(b"+2147483647"), Ok(Object::Int(2_147_483_647)));
assert_eq!(parse(b"+2147483648"), Ok(Object::Int(0)));
assert_eq!(parse(b"+4294967295"), Ok(Object::Int(0)));
assert_eq!(parse(b"-2147483648"), Ok(Object::Int(-2_147_483_648)));
assert_eq!(parse(b"-2147483649"), Ok(Object::Int(0)));
assert_eq!(parse(b"-99999999999999999999"), Ok(Object::Int(0)));
for spelling in [
&b"0"[..],
b"-0",
b"+0",
b"2147483647",
b"2147483648",
b"4294967295",
b"4294967296",
b"-2147483648",
b"-2147483649",
b"18446744073709551616",
b"999999999999999999999999999999",
b"-999999999999999999999999999999",
] {
let Ok(Object::Int(v)) = parse(spelling) else {
panic!(
"{} did not parse as an integer",
String::from_utf8_lossy(spelling)
);
};
assert!(
INT_RANGE.contains(&v),
"{} parsed to {v}, outside INT_RANGE",
String::from_utf8_lossy(spelling)
);
let _ = pdfrum_object::narrow_to_signed32(v);
}
}
#[test]
fn names_decode_escapes() {
assert_eq!(
parse(b"/A#20B"),
Ok(Object::Name(pdfrum_object::Name::from("A B")))
);
assert_eq!(parse(b"/"), Ok(Object::Name(pdfrum_object::Name::from(""))));
}
#[test]
fn references_and_the_invalid_one() {
assert_eq!(parse(b"12 0 R"), Ok(Object::Ref(ObjRef::new(12, 0))));
assert_eq!(parse(b"3 7 R"), Ok(Object::Ref(ObjRef::new(3, 7))));
assert_eq!(parse(b"0 0 R"), Ok(Object::Ref(ObjRef::new(0, 0))));
assert!(parse(b"4294967295 0 R").is_err());
let mut lx = Lexer::new(b"12 0 X");
let mut diags = Diagnostics::default();
let obj = parse_object(
&mut lx,
&Limits::default(),
&mut diags,
Strictness::Loose,
&NoResolve,
);
assert_eq!(obj, Ok(Object::Int(12)));
assert_eq!(lx.pos(), 2);
}
#[test]
fn arrays() {
let obj = parse(b"[1 2 3]").expect("array");
let array = obj.as_array().expect("array");
assert_eq!(array.len(), 3);
assert_eq!(array.int_at(2), Some(3));
assert!(parse(b"[]").expect("array").as_array().is_some());
}
#[test]
fn a_loose_array_keeps_what_it_read() {
let mut diags = Diagnostics::default();
let obj = parse_object(
&mut Lexer::new(b"[1 2 endobj"),
&Limits::default(),
&mut diags,
Strictness::Loose,
&NoResolve,
)
.expect("partial array");
assert_eq!(obj.as_array().expect("array").len(), 2);
assert!(diags.contains(&DiagKind::MalformedArray));
}
#[test]
fn a_strict_array_needs_its_bracket() {
assert!(parse_strict(b"[1 2 endobj").is_err());
assert!(parse_strict(b"[1 2]").is_ok());
}
#[test]
fn dictionaries() {
let obj = parse(b"<< /Type /Page /Count 3 >>").expect("dict");
let dict = obj.as_dict().expect("dict");
assert_eq!(dict.name(names::TYPE), Some(names::PAGE));
assert_eq!(dict.direct_int(names::COUNT), Some(3));
}
#[test]
fn a_later_duplicate_key_wins() {
let obj = parse(b"<< /Size 3 /Size -1 >>").expect("dict");
assert_eq!(
obj.as_dict().expect("dict").direct_int(names::SIZE),
Some(-1)
);
}
#[test]
fn endobj_closes_an_unterminated_dictionary() {
let mut diags = Diagnostics::default();
let mut lx = Lexer::new(b"<< /A 1 endobj");
let obj = parse_object(
&mut lx,
&Limits::default(),
&mut diags,
Strictness::Loose,
&NoResolve,
)
.expect("dict");
assert_eq!(obj.as_dict().expect("dict").len(), 1);
assert!(diags.contains(&DiagKind::MalformedDict));
assert_eq!(lx.next_word(&Limits::default()).bytes(), b"endobj");
}
#[test]
fn junk_keys_are_skipped_and_shift_the_pairs() {
let obj = parse(b"<< junk /A 1 >>").expect("dict");
let dict = obj.as_dict().expect("dict");
assert_eq!(dict.len(), 1);
assert_eq!(dict.direct_int(&pdfrum_object::Name::from("A")), Some(1));
}
#[test]
fn a_bare_slash_key_is_dropped() {
let obj = parse(b"<< / 1 /A 2 >>").expect("dict");
assert_eq!(obj.as_dict().expect("dict").len(), 1);
}
#[test]
fn nesting_past_the_budget_loses_the_middle_not_the_object() {
fn depth_of(o: &Object) -> usize {
match o {
Object::Array(a) => 1 + a.iter().map(depth_of).max().unwrap_or(0),
_ => 0,
}
}
let nested = |n: usize| -> Vec<u8> {
let mut v: Vec<u8> = std::iter::repeat_n(b'[', n).collect();
v.extend(std::iter::repeat_n(b']', n));
v
};
assert_eq!(depth_of(&parse(&nested(63)).expect("array")), 63);
assert_eq!(depth_of(&parse(&nested(64)).expect("array")), 64);
assert_eq!(depth_of(&parse(&nested(65)).expect("array")), 64);
assert_eq!(depth_of(&parse(&nested(500)).expect("array")), 64);
}
#[test]
fn a_stream_reads_its_declared_length() {
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"<< /Length 5 >>\nstream\nHELLO\nendstream\nendobj\n",
&NoResolve,
&mut diags,
)
.expect("stream");
assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
assert!(!diags.contains(&DiagKind::LengthMismatch));
}
#[test]
fn a_wrong_length_falls_back_to_the_keyword() {
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"<< /Length 2 >>\nstream\nHELLO\nendstream\nendobj\n",
&NoResolve,
&mut diags,
)
.expect("stream");
assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
assert!(diags.contains(&DiagKind::LengthMismatch));
assert!(diags.contains(&DiagKind::KeywordResync));
}
#[test]
fn a_length_past_the_file_falls_back() {
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"<< /Length 9999 >>\nstream\nHELLO\nendstream\nendobj\n",
&NoResolve,
&mut diags,
)
.expect("stream");
assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
}
#[test]
fn endstream_is_matched_by_prefix() {
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"<< /Length 5 >>\nstream\nHELLO\nendstreamXYZ\nendobj\n",
&NoResolve,
&mut diags,
)
.expect("stream");
assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
assert!(!diags.contains(&DiagKind::LengthMismatch));
}
#[test]
fn a_space_before_the_newline_still_resyncs() {
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"<< /Length 99 >>\nstream\nHELLO\nendobj \n",
&NoResolve,
&mut diags,
)
.expect("stream");
assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
assert!(diags.contains(&DiagKind::KeywordResync));
}
#[test]
fn a_missing_endstream_resyncs_on_endobj() {
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"<< /Length 99 >>\nstream\nHELLO\nendobj\n",
&NoResolve,
&mut diags,
)
.expect("stream");
assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
assert!(diags.contains(&DiagKind::KeywordResync));
}
#[test]
fn a_delimiter_disqualifies_an_endstream_match() {
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"<< /Length 999 >>\nstream\nA>>endstream\nB\nendstream\nendobj\n",
&NoResolve,
&mut diags,
)
.expect("stream");
assert_eq!(&*obj.as_stream().expect("stream").data, b"A>>endstream\nB");
}
#[test]
fn a_zero_length_stream_is_legal() {
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"<< /Length 0 >>\nstream\nendstream\nendobj\n",
&NoResolve,
&mut diags,
)
.expect("stream");
assert!(obj.as_stream().expect("stream").data.is_empty());
}
#[test]
fn an_indirect_length_is_chased() {
let store = Store([(9u32, Arc::new(Object::Int(5)))].into_iter().collect());
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"<< /Length 9 0 R >>\nstream\nHELLO\nendstream\nendobj\n",
&store,
&mut diags,
)
.expect("stream");
assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
assert!(!diags.contains(&DiagKind::LengthMismatch));
}
#[test]
fn an_unresolvable_length_falls_back_to_the_scan() {
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"<< /Length 9 0 R >>\nstream\nHELLO\nendstream\nendobj\n",
&NoResolve,
&mut diags,
)
.expect("stream");
assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
assert!(diags.contains(&DiagKind::KeywordResync));
}
#[test]
fn streams_are_dropped_from_composites() {
let mut diags = Diagnostics::default();
let obj = parse_in_file(
b"[ 1 << /Length 5 >>\nstream\nHELLO\nendstream\n 2 ]",
&NoResolve,
&mut diags,
)
.expect("array");
let array = obj.as_array().expect("array");
assert_eq!(array.len(), 2);
assert_eq!(array.int_at(0), Some(1));
assert_eq!(array.int_at(1), Some(2));
assert!(diags.contains(&DiagKind::StreamInCompositeDropped));
}
#[test]
fn indirect_frames_need_their_header() {
let file = ByteSpan::from(b"7 0 obj << /A 1 >> endobj".to_vec());
let limits = Limits::default();
let mut diags = Diagnostics::default();
let mut ctx = Context {
limits: &limits,
diags: &mut diags,
file: Some(&file),
store: Some(&NoResolve),
};
let mut lx = Lexer::new(&file);
let parsed = indirect(&mut lx, &mut ctx, Strictness::Loose, 0).expect("indirect");
assert_eq!(parsed.num, 7);
assert_eq!(parsed.generation, 0);
assert!(parsed.object.as_dict().is_some());
let mut lx = Lexer::new(b"7 0 <<>>");
assert!(
indirect(
&mut lx,
&mut Context {
limits: &limits,
diags: &mut Diagnostics::default(),
file: None,
store: Some(&NoResolve),
},
Strictness::Loose,
0,
)
.is_err()
);
assert_eq!(lx.pos(), 0);
}
#[test]
fn never_panics_on_arbitrary_bytes() {
let seeds: &[&[u8]] = &[
b"<<<<<<<<",
b">>>>>>>>",
b"[[[[[[[[",
b"((((((((",
b"<<<</Length -1>>stream",
b"0 0 obj<</Length 99999999999999999999>>stream\n",
b"<</A",
b"/",
b"\xff\xfe\x00\x80",
b"1 0 R 2 0 R",
b"<</Length 1 0 R>>stream\nx",
];
for seed in seeds {
let _ = parse(seed);
let _ = parse_strict(seed);
let _ = parse_in_file(seed, &NoResolve, &mut Diagnostics::default());
}
}
}