use crate::error::{Error, Result};
use crate::limits::Limits;
use super::cos::{
FilterClass, LengthValue, body_as_u64, dict_filter, dict_has_length, dict_int_or_ref,
dict_length, dict_name_value,
};
use super::lexer::lex;
use super::span::{Span, SpanKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhysicalKind {
Header,
Comment,
Whitespace,
ObjHeader,
ObjBody,
EndObj,
StreamData,
XrefSection,
Trailer,
StartXref,
Eof,
Unclassified,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PhysicalSpan {
pub start: u64,
pub len: u64,
pub kind: PhysicalKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjRole {
Generic,
XRefStream,
ObjectStream,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PdfObjectSpan {
pub number: u64,
pub generation: u64,
pub start: u64,
pub end: u64,
pub role: ObjRole,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LengthSource {
Direct,
Indirect,
Fallback,
Missing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PdfStreamSpan {
pub object: u64,
pub generation: u64,
pub data_start: u64,
pub data_len: u64,
pub length_source: LengthSource,
pub filter: FilterClass,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RevisionInfo {
pub index: u32,
pub start: u64,
pub end: u64,
pub startxref: Option<u64>,
pub prev: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PdfPhysical {
pub spans: Vec<PhysicalSpan>,
pub objects: Vec<PdfObjectSpan>,
pub streams: Vec<PdfStreamSpan>,
pub revisions: Vec<RevisionInfo>,
pub startxref: Vec<u64>,
pub eofs: Vec<u64>,
pub header: Option<(u64, u64)>,
}
impl PdfPhysical {
pub fn total_len(&self) -> u64 {
self.spans
.iter()
.fold(0u64, |acc, s| acc.saturating_add(s.len))
}
pub fn validate(&self, declared_len: u64) -> Result<()> {
let mut cursor: u64 = 0;
for (i, span) in self.spans.iter().enumerate() {
if span.len == 0 {
return Err(Error::invalid_pdf_structure(format!(
"physical span {i} has zero length at offset {}",
span.start
)));
}
if span.start != cursor {
let why = if span.start < cursor {
"overlap"
} else {
"gap"
};
return Err(Error::coverage_violation(format!(
"physical span {i} {why}: expected start {cursor}, found {}",
span.start
)));
}
cursor = cursor.checked_add(span.len).ok_or_else(|| {
Error::coverage_violation("physical span lengths overflow the address space")
})?;
}
if cursor != declared_len {
return Err(Error::coverage_violation(format!(
"physical cover ends at {cursor}, declared length is {declared_len}"
)));
}
Ok(())
}
}
pub fn scan(input: &[u8], limits: Limits) -> Result<PdfPhysical> {
let declared_len = input.len() as u64;
let lexed = lex(input, limits)?;
let spans = lexed.spans.spans;
let obj_bodies = collect_bodies(input, &spans);
let mut state = ScanState::new(limits, obj_bodies);
let mut i = 0usize;
while i < spans.len() {
let sp = spans[i];
let bytes = bytes_of(input, sp);
if state.header.is_none()
&& sp.start == 0
&& sp.kind == SpanKind::Comment
&& bytes.starts_with(b"%PDF-")
{
state.push(sp.start, sp.len, PhysicalKind::Header)?;
state.header = Some((sp.start, sp.len));
i += 1;
continue;
}
if sp.kind == SpanKind::Comment && bytes.starts_with(b"%%EOF") {
state.push(sp.start, sp.len, PhysicalKind::Eof)?;
state.eofs.push(sp.start);
i += 1;
continue;
}
if sp.kind == SpanKind::Regular {
if bytes == b"startxref" {
if let Some(next) = state.try_startxref(input, &spans, i)? {
i = next;
continue;
}
} else if bytes == b"xref" {
i = state.emit_xref(input, &spans, i)?;
continue;
} else if bytes == b"trailer" {
i = state.emit_trailer(&spans, i)?;
continue;
} else if let Some((number, generation)) = obj_header_at(input, &spans, i) {
i = state.emit_object(input, &spans, i, number, generation)?;
continue;
}
}
let kind = match sp.kind {
SpanKind::Comment => PhysicalKind::Comment,
SpanKind::Whitespace => PhysicalKind::Whitespace,
_ => PhysicalKind::Unclassified,
};
state.push(sp.start, sp.len, kind)?;
i += 1;
}
let physical = state.finish(input, &spans);
physical.validate(declared_len)?;
Ok(physical)
}
struct ScanState {
builder: Builder,
objects: Vec<PdfObjectSpan>,
streams: Vec<PdfStreamSpan>,
obj_bodies: Vec<ObjBody>,
startxref: Vec<(u64, u64)>,
eofs: Vec<u64>,
trailer_dicts: Vec<(u64, u64)>,
header: Option<(u64, u64)>,
}
impl ScanState {
fn new(limits: Limits, obj_bodies: Vec<ObjBody>) -> Self {
ScanState {
builder: Builder::new(limits),
objects: Vec::new(),
streams: Vec::new(),
obj_bodies,
startxref: Vec::new(),
eofs: Vec::new(),
trailer_dicts: Vec::new(),
header: None,
}
}
fn push(&mut self, start: u64, len: u64, kind: PhysicalKind) -> Result<()> {
self.builder.push(start, len, kind)
}
fn finish(self, input: &[u8], spans: &[Span]) -> PdfPhysical {
let revisions = build_revisions(
input,
spans,
&self.eofs,
&self.objects,
&self.startxref,
&self.trailer_dicts,
);
PdfPhysical {
spans: self.builder.spans,
objects: self.objects,
streams: self.streams,
revisions,
startxref: self.startxref.iter().map(|&(_, value)| value).collect(),
eofs: self.eofs,
header: self.header,
}
}
fn emit_object(
&mut self,
input: &[u8],
spans: &[Span],
i: usize,
number: u64,
generation: u64,
) -> Result<usize> {
let obj_header_start = spans[i].start;
let obj_kw_end = spans[i + 4].start + spans[i + 4].len;
let role = leading_dict_role(input, spans, i + 5);
self.push(
obj_header_start,
obj_kw_end - obj_header_start,
PhysicalKind::ObjHeader,
)?;
let mut stream: Option<ResolvedStream> = None;
let mut endobj: Option<usize> = None;
let mut j = i + 5;
while j < spans.len() {
if regular_eq(input, spans[j], b"endobj") {
endobj = Some(j);
break;
}
if stream.is_none()
&& regular_eq(input, spans[j], b"stream")
&& let Some(rs) = resolve_stream(input, spans, j, &self.obj_bodies, i + 5)
{
j = rs.endstream + 1;
stream = Some(rs);
continue;
}
j += 1;
}
match endobj {
Some(m) => {
if let Some(rs) = stream {
if rs.data_start > obj_kw_end {
self.push(
obj_kw_end,
rs.data_start - obj_kw_end,
PhysicalKind::ObjBody,
)?;
}
if rs.data_len > 0 {
self.push(rs.data_start, rs.data_len, PhysicalKind::StreamData)?;
}
let data_end = rs.data_start + rs.data_len;
let body_end = spans[m].start;
if body_end > data_end {
self.push(data_end, body_end - data_end, PhysicalKind::ObjBody)?;
}
self.streams.push(PdfStreamSpan {
object: number,
generation,
data_start: rs.data_start,
data_len: rs.data_len,
length_source: rs.source,
filter: rs.filter,
});
} else {
let body_end = spans[m].start;
if body_end > obj_kw_end {
self.push(obj_kw_end, body_end - obj_kw_end, PhysicalKind::ObjBody)?;
}
}
self.push(spans[m].start, spans[m].len, PhysicalKind::EndObj)?;
self.objects.push(PdfObjectSpan {
number,
generation,
start: obj_header_start,
end: spans[m].start + spans[m].len,
role,
});
Ok(m + 1)
}
None => {
let end = input.len() as u64;
if end > obj_kw_end {
self.push(obj_kw_end, end - obj_kw_end, PhysicalKind::ObjBody)?;
}
Ok(spans.len())
}
}
}
fn emit_xref(&mut self, input: &[u8], spans: &[Span], i: usize) -> Result<usize> {
let start = spans[i].start;
let mut end_idx = spans.len();
for (k, sp) in spans.iter().enumerate().skip(i + 1) {
let sp = *sp;
if regular_eq(input, sp, b"trailer") || regular_eq(input, sp, b"startxref") {
end_idx = k;
break;
}
if sp.kind == SpanKind::Comment && bytes_of(input, sp).starts_with(b"%%EOF") {
end_idx = k;
break;
}
}
let end = if end_idx < spans.len() {
spans[end_idx].start
} else {
input.len() as u64
};
if end > start {
self.push(start, end - start, PhysicalKind::XrefSection)?;
}
if end_idx < spans.len() && regular_eq(input, spans[end_idx], b"trailer") {
self.emit_trailer(spans, end_idx)
} else {
Ok(end_idx)
}
}
fn emit_trailer(&mut self, spans: &[Span], t: usize) -> Result<usize> {
let start = spans[t].start;
let mut end = spans[t].start + spans[t].len;
let mut next = t + 1;
let dict_idx = if next < spans.len() && spans[next].kind == SpanKind::Whitespace {
next + 1
} else {
next
};
if dict_idx < spans.len()
&& spans[dict_idx].kind == SpanKind::DictOpen
&& let Some(close) = matching_dict_close(spans, dict_idx)
{
let lo = spans[dict_idx].start;
let hi = spans[close].start + spans[close].len;
self.trailer_dicts.push((lo, hi));
end = hi;
next = close + 1;
}
if end > start {
self.push(start, end - start, PhysicalKind::Trailer)?;
}
Ok(next)
}
fn try_startxref(&mut self, input: &[u8], spans: &[Span], i: usize) -> Result<Option<usize>> {
if i + 2 < spans.len()
&& spans[i + 1].kind == SpanKind::Whitespace
&& spans[i + 2].kind == SpanKind::Regular
&& let Some(value) = parse_uint(bytes_of(input, spans[i + 2]), 19)
{
let start = spans[i].start;
let end = spans[i + 2].start + spans[i + 2].len;
self.push(start, end - start, PhysicalKind::StartXref)?;
self.startxref.push((start, value));
return Ok(Some(i + 3));
}
Ok(None)
}
}
struct Builder {
max: u32,
spans: Vec<PhysicalSpan>,
}
impl Builder {
fn new(limits: Limits) -> Self {
Builder {
max: limits.max_pdf_spans,
spans: Vec::new(),
}
}
fn push(&mut self, start: u64, len: u64, kind: PhysicalKind) -> Result<()> {
if len == 0 {
return Ok(());
}
if let Some(last) = self.spans.last_mut()
&& last.kind == kind
&& last.start.checked_add(last.len) == Some(start)
{
last.len = last
.len
.checked_add(len)
.ok_or_else(|| Error::coverage_violation("physical span length overflow"))?;
return Ok(());
}
if self.spans.len() as u64 >= self.max as u64 {
return Err(Error::resource_limit(format!(
"pdf physical span count exceeds limit {}",
self.max
)));
}
self.spans.push(PhysicalSpan { start, len, kind });
Ok(())
}
}
fn bytes_of(input: &[u8], sp: Span) -> &[u8] {
let Ok(start) = usize::try_from(sp.start) else {
return &[];
};
let Some(end) = sp
.start
.checked_add(sp.len)
.and_then(|e| usize::try_from(e).ok())
else {
return &[];
};
if start > end || end > input.len() {
return &[];
}
&input[start..end]
}
fn regular_eq(input: &[u8], sp: Span, keyword: &[u8]) -> bool {
sp.kind == SpanKind::Regular && bytes_of(input, sp) == keyword
}
fn parse_uint(bytes: &[u8], max_digits: usize) -> Option<u64> {
if bytes.is_empty() || bytes.len() > max_digits {
return None;
}
let mut value: u64 = 0;
for &b in bytes {
if !b.is_ascii_digit() {
return None;
}
value = value.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
}
Some(value)
}
fn eol_start_after(input: &[u8], offset: u64) -> Option<u64> {
let start = usize::try_from(offset).ok()?;
if start > input.len() {
return None;
}
(start..input.len())
.find(|&i| input[i] == b'\n' || input[i] == b'\r')
.map(|i| i as u64)
}
fn find_regular(input: &[u8], spans: &[Span], from: usize, keyword: &[u8]) -> Option<usize> {
if from >= spans.len() {
return None;
}
spans[from..]
.iter()
.position(|sp| regular_eq(input, *sp, keyword))
.map(|off| from + off)
}
fn matching_dict_close(spans: &[Span], open: usize) -> Option<usize> {
let mut depth: u64 = 0;
for (k, sp) in spans.iter().enumerate().skip(open) {
match sp.kind {
SpanKind::DictOpen => depth = depth.saturating_add(1),
SpanKind::DictClose => {
if depth == 0 {
return None;
}
depth -= 1;
if depth == 0 {
return Some(k);
}
}
_ => {}
}
}
None
}
fn obj_header_at(input: &[u8], spans: &[Span], i: usize) -> Option<(u64, u64)> {
if i + 4 >= spans.len() {
return None;
}
if spans[i].kind != SpanKind::Regular {
return None;
}
let number = parse_uint(bytes_of(input, spans[i]), 10)?;
if spans[i + 1].kind != SpanKind::Whitespace {
return None;
}
if spans[i + 2].kind != SpanKind::Regular {
return None;
}
let generation = parse_uint(bytes_of(input, spans[i + 2]), 10)?;
if spans[i + 3].kind != SpanKind::Whitespace {
return None;
}
if !regular_eq(input, spans[i + 4], b"obj") {
return None;
}
Some((number, generation))
}
#[derive(Debug, Clone, Copy)]
struct ObjBody {
number: u64,
generation: u64,
body_lo: u64,
body_hi: u64,
}
#[derive(Debug, Clone, Copy)]
struct ResolvedStream {
data_start: u64,
data_len: u64,
endstream: usize,
source: LengthSource,
filter: FilterClass,
}
fn collect_bodies(input: &[u8], spans: &[Span]) -> Vec<ObjBody> {
let mut out = Vec::new();
let mut i = 0usize;
while i < spans.len() {
if let Some((number, generation)) = obj_header_at(input, spans, i) {
let body_lo = spans[i + 4].start + spans[i + 4].len;
match find_endobj(input, spans, i + 5) {
Some(m) => {
out.push(ObjBody {
number,
generation,
body_lo,
body_hi: spans[m].start,
});
i = m + 1;
}
None => {
out.push(ObjBody {
number,
generation,
body_lo,
body_hi: input.len() as u64,
});
i = spans.len();
}
}
} else {
i += 1;
}
}
out
}
fn find_endobj(input: &[u8], spans: &[Span], from: usize) -> Option<usize> {
let mut j = from;
while j < spans.len() {
if regular_eq(input, spans[j], b"endobj") {
return Some(j);
}
if regular_eq(input, spans[j], b"stream") {
let kw_end = spans[j].start + spans[j].len;
if let Some(data_start) = eol_start_after(input, kw_end)
&& let Some(k) = find_regular(input, spans, j + 1, b"endstream")
&& spans[k].start >= data_start
{
j = k + 1;
continue;
}
}
j += 1;
}
None
}
fn lookup_body(bodies: &[ObjBody], number: u64, generation: u64) -> Option<(u64, u64)> {
bodies
.iter()
.find(|b| b.number == number && b.generation == generation)
.map(|b| (b.body_lo, b.body_hi))
}
fn resolve_stream(
input: &[u8],
spans: &[Span],
s: usize,
bodies: &[ObjBody],
lower: usize,
) -> Option<ResolvedStream> {
let kw_end = spans[s].start + spans[s].len;
let mut resolved: Option<ResolvedStream> = None;
let mut source = LengthSource::Fallback;
let mut filter = FilterClass::Absent;
if let Some((open, close)) = preceding_dict(spans, s, lower) {
let dict_lo = spans[open].start;
let dict_hi = spans[close].start + spans[close].len;
filter = dict_filter(input, spans, dict_lo, dict_hi);
match dict_length(input, spans, dict_lo, dict_hi) {
Some(LengthValue::Direct(n)) => {
if let Some(rs) = verify_direct(input, spans, kw_end, n, filter) {
resolved = Some(rs);
source = LengthSource::Direct;
}
}
Some(LengthValue::Indirect { number, generation }) => {
if let Some((lo, hi)) = lookup_body(bodies, number, generation)
&& let Some(n) = body_as_u64(input, spans, lo, hi)
&& let Some(rs) = verify_direct(input, spans, kw_end, n, filter)
{
resolved = Some(rs);
source = LengthSource::Indirect;
}
}
None => {
if !dict_has_length(input, spans, dict_lo, dict_hi) {
source = LengthSource::Missing;
}
}
}
} else {
source = LengthSource::Missing;
}
if let Some(mut rs) = resolved {
rs.source = source;
return Some(rs);
}
let data_start = eol_start_after(input, kw_end)?;
let k = find_regular(input, spans, s + 1, b"endstream")?;
if spans[k].start < data_start {
return None;
}
Some(ResolvedStream {
data_start,
data_len: spans[k].start - data_start,
endstream: k,
source,
filter,
})
}
fn preceding_dict(spans: &[Span], s: usize, lower: usize) -> Option<(usize, usize)> {
let mut j = s;
while j > lower {
j -= 1;
if spans[j].kind == SpanKind::DictOpen
&& let Some(close) = matching_dict_close(spans, j)
&& close < s
{
return Some((j, close));
}
}
None
}
fn verify_direct(
input: &[u8],
spans: &[Span],
kw_end: u64,
n: u64,
filter: FilterClass,
) -> Option<ResolvedStream> {
let eol = post_stream_eol_len(input, kw_end)?;
let data_start = kw_end + eol;
let data_end = data_start.checked_add(n)?;
let k = first_regular_at_or_after(input, spans, data_end, b"endstream")?;
let gap = spans[k].start.checked_sub(data_end)?;
let ok = gap == 0
|| (gap == 1 && byte_at(input, data_end) == Some(b'\n'))
|| (gap == 2
&& byte_at(input, data_end) == Some(b'\r')
&& byte_at(input, data_end + 1) == Some(b'\n'));
if !ok {
return None;
}
Some(ResolvedStream {
data_start,
data_len: n,
endstream: k,
source: LengthSource::Direct,
filter,
})
}
fn post_stream_eol_len(input: &[u8], kw_end: u64) -> Option<u64> {
match byte_at(input, kw_end) {
Some(b'\n') => Some(1),
Some(b'\r') if byte_at(input, kw_end + 1) == Some(b'\n') => Some(2),
_ => None,
}
}
fn first_regular_at_or_after(
input: &[u8],
spans: &[Span],
offset: u64,
keyword: &[u8],
) -> Option<usize> {
let idx = spans.partition_point(|sp| sp.start < offset);
spans[idx..]
.iter()
.position(|sp| regular_eq(input, *sp, keyword))
.map(|off| idx + off)
}
fn byte_at(input: &[u8], offset: u64) -> Option<u8> {
usize::try_from(offset)
.ok()
.and_then(|i| input.get(i).copied())
}
fn build_revisions(
input: &[u8],
spans: &[Span],
eofs: &[u64],
objects: &[PdfObjectSpan],
startxref: &[(u64, u64)],
trailers: &[(u64, u64)],
) -> Vec<RevisionInfo> {
let mut out = Vec::with_capacity(eofs.len());
let mut start = 0u64;
for (index, &e) in eofs.iter().enumerate() {
let end = span_end_at(spans, e).unwrap_or(e);
let startxref = startxref
.iter()
.find(|&&(keyword, _)| keyword >= start && keyword < end)
.map(|&(_, value)| value);
let prev = resolve_prev(input, spans, start, end, objects, trailers);
out.push(RevisionInfo {
index: index as u32,
start,
end,
startxref,
prev,
});
start = end + eol_len_after(input, end);
}
out
}
fn resolve_prev(
input: &[u8],
spans: &[Span],
rev_start: u64,
rev_end: u64,
objects: &[PdfObjectSpan],
trailers: &[(u64, u64)],
) -> Option<u64> {
let trailer = trailers
.iter()
.rev()
.find(|&&(lo, _)| lo >= rev_start && lo < rev_end);
let (dict_lo, dict_hi) = match trailer {
Some(&(lo, hi)) => (lo, hi),
None => {
let object = objects.iter().find(|o| {
o.role == ObjRole::XRefStream && o.start >= rev_start && o.start < rev_end
})?;
leading_dict_range_at(input, spans, object.start)?
}
};
match dict_int_or_ref(input, spans, dict_lo, dict_hi, b"Prev") {
Some(LengthValue::Direct(n)) => Some(n),
Some(LengthValue::Indirect { number, generation }) => objects
.iter()
.find(|o| o.number == number && o.generation == generation)
.map(|o| o.start),
None => None,
}
}
fn leading_dict_range(spans: &[Span], after: usize) -> Option<(u64, u64)> {
let mut j = after;
while j < spans.len() && matches!(spans[j].kind, SpanKind::Whitespace | SpanKind::Comment) {
j += 1;
}
if j >= spans.len() || spans[j].kind != SpanKind::DictOpen {
return None;
}
let close = matching_dict_close(spans, j)?;
Some((spans[j].start, spans[close].start + spans[close].len))
}
fn leading_dict_role(input: &[u8], spans: &[Span], after: usize) -> ObjRole {
let Some((lo, hi)) = leading_dict_range(spans, after) else {
return ObjRole::Generic;
};
match dict_name_value(input, spans, lo, hi, b"Type") {
Some(name) if name == b"XRef".as_slice() => ObjRole::XRefStream,
Some(name) if name == b"ObjStm".as_slice() => ObjRole::ObjectStream,
_ => ObjRole::Generic,
}
}
fn leading_dict_range_at(input: &[u8], spans: &[Span], start: u64) -> Option<(u64, u64)> {
let idx = spans.partition_point(|sp| sp.start < start);
if idx >= spans.len() || spans[idx].start != start {
return None;
}
obj_header_at(input, spans, idx)?;
leading_dict_range(spans, idx + 5)
}
fn span_end_at(spans: &[Span], offset: u64) -> Option<u64> {
let idx = spans.partition_point(|sp| sp.start <= offset);
if idx == 0 {
return None;
}
let sp = spans[idx - 1];
if offset < sp.start.saturating_add(sp.len) {
Some(sp.start.saturating_add(sp.len))
} else {
None
}
}
fn eol_len_after(input: &[u8], offset: u64) -> u64 {
match byte_at(input, offset) {
Some(b'\n') => 1,
Some(b'\r') if byte_at(input, offset + 1) == Some(b'\n') => 2,
Some(b'\r') => 1,
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorClass;
fn count(p: &PdfPhysical, kind: PhysicalKind) -> usize {
p.spans.iter().filter(|s| s.kind == kind).count()
}
fn canonical_pdf() -> Vec<u8> {
let mut s = String::new();
s.push_str("%PDF-1.7\n");
s.push_str("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
s.push_str("2 0 obj\n<< /Length 6 >>\nstream\nhello\nendstream\nendobj\n");
s.push_str("3 0 obj\n<< /Length 7 >>\nstream\nworld\nendstream\nendobj\n");
s.push_str("4 0 obj\n<< /Length 4 >>\nstream\nxyz\nendstream\nendobj\n");
s.push_str("xref\n0 5\n0000000000 65535 f \n0000000010 00000 n \n");
s.push_str("trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n321\n%%EOF");
s.into_bytes()
}
#[test]
fn canonical_pdf_is_fully_classified() {
let pdf = canonical_pdf();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
assert_eq!(p.header, Some((0, 8)));
assert_eq!(p.objects.len(), 4);
let nums: Vec<(u64, u64)> = p.objects.iter().map(|o| (o.number, o.generation)).collect();
assert_eq!(nums, [(1, 0), (2, 0), (3, 0), (4, 0)]);
assert_eq!(p.startxref, [321]);
assert_eq!(p.eofs.len(), 1);
assert_eq!(count(&p, PhysicalKind::EndObj), 4);
assert_eq!(count(&p, PhysicalKind::StreamData), 3);
assert_eq!(count(&p, PhysicalKind::XrefSection), 1);
assert_eq!(count(&p, PhysicalKind::Trailer), 1);
assert_eq!(count(&p, PhysicalKind::ObjHeader), 4);
assert_eq!(p.total_len(), pdf.len() as u64);
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn endobj_inside_literal_string_does_not_split_object() {
let pdf = b"%PDF-1.4\n1 0 obj\n(endobj)\nendobj".to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
assert_eq!(p.objects.len(), 1);
assert_eq!(p.objects[0].number, 1);
assert_eq!(p.objects[0].generation, 0);
assert_eq!(p.objects[0].end, pdf.len() as u64);
assert_eq!(count(&p, PhysicalKind::EndObj), 1);
assert_eq!(count(&p, PhysicalKind::StreamData), 0);
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn stream_data_with_keyword_spellings_stays_opaque() {
let pdf =
b"%PDF-1.4\n1 0 obj\n<< /Length 30 >>\nstream\nendobj stream bytes here\nendstream\nendobj"
.to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
assert_eq!(p.objects.len(), 1);
assert_eq!(p.objects[0].number, 1);
assert_eq!(p.objects[0].end, pdf.len() as u64);
assert_eq!(count(&p, PhysicalKind::EndObj), 1);
assert_eq!(count(&p, PhysicalKind::StreamData), 1);
let data = p
.spans
.iter()
.find(|s| s.kind == PhysicalKind::StreamData)
.unwrap();
let slice = &pdf[data.start as usize..(data.start + data.len) as usize];
assert!(slice.windows(6).any(|w| w == b"endobj"));
assert!(slice.windows(6).any(|w| w == b"stream"));
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn two_objects_with_xref_trailer_and_startxref() {
let pdf = b"%PDF-1.4\n1 0 obj\n<< >>\nendobj\n2 0 obj\n<< >>\nendobj\nxref\n0 3\n0000000000 65535 f \n0000000009 00000 n \ntrailer\n<< /Size 3 >>\nstartxref\n99\n%%EOF".to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
assert_eq!(p.objects.len(), 2);
assert_eq!(p.objects[0].number, 1);
assert_eq!(p.objects[1].number, 2);
assert!(p.objects[0].end <= p.objects[1].start);
assert_eq!(count(&p, PhysicalKind::XrefSection), 1);
assert_eq!(count(&p, PhysicalKind::Trailer), 1);
assert_eq!(p.startxref, [99]);
assert_eq!(p.eofs.len(), 1);
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn malformed_object_without_endobj_is_conservative() {
let pdf = b"1 0 obj".to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
assert!(p.objects.is_empty());
assert_eq!(count(&p, PhysicalKind::ObjHeader), 1);
assert_eq!(count(&p, PhysicalKind::EndObj), 0);
assert_eq!(p.total_len(), pdf.len() as u64);
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn missing_header_leaves_cover_total() {
let pdf = b"1 0 obj\nendobj\n".to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
assert_eq!(p.header, None);
assert_eq!(count(&p, PhysicalKind::EndObj), 1);
p.validate(pdf.len() as u64).unwrap();
}
fn xorshift64(state: &mut u64) -> u64 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*state = x;
x
}
#[test]
fn random_bytes_never_panic_and_keep_cover() {
let mut state: u64 = 0x1234_5678_9abc_def0;
for _ in 0..500 {
let len = (xorshift64(&mut state) % 96) as usize;
let mut buf = Vec::with_capacity(len);
for _ in 0..len {
buf.push((xorshift64(&mut state) & 0xff) as u8);
}
match scan(&buf, Limits::DEFAULT) {
Ok(p) => {
p.validate(buf.len() as u64).unwrap();
assert_eq!(p.total_len(), buf.len() as u64);
}
Err(e) => {
let _ = e.class();
}
}
}
}
#[test]
fn tiny_span_limit_is_resource_limit() {
let pdf = canonical_pdf();
let limits = Limits {
max_pdf_spans: 1,
..Limits::DEFAULT
};
let err = scan(&pdf, limits).unwrap_err();
assert_eq!(err.class(), ErrorClass::ResourceLimit);
}
fn offset_of(hay: &[u8], needle: &[u8]) -> u64 {
hay.windows(needle.len())
.position(|w| w == needle)
.expect("needle present") as u64
}
fn slice_of(pdf: &[u8], s: PdfStreamSpan) -> &[u8] {
&pdf[s.data_start as usize..(s.data_start + s.data_len) as usize]
}
fn only_stream(p: &PdfPhysical) -> PdfStreamSpan {
assert_eq!(p.streams.len(), 1, "expected exactly one stream");
p.streams[0]
}
#[test]
fn direct_length_yields_exact_span() {
let pdf =
b"%PDF-1.5\n1 0 obj\n<< /Length 5 >>\nstream\nhello\nendstream\nendobj\n".to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.object, 1);
assert_eq!(s.generation, 0);
assert_eq!(s.length_source, LengthSource::Direct);
assert_eq!(s.data_start, offset_of(&pdf, b"hello"));
assert_eq!(s.data_len, 5);
assert_eq!(slice_of(&pdf, s), b"hello");
let ds = p
.spans
.iter()
.find(|sp| sp.kind == PhysicalKind::StreamData)
.unwrap();
assert_eq!(ds.start, s.data_start);
assert_eq!(ds.len, s.data_len);
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn length_including_trailing_eol_is_accepted() {
let pdf =
b"%PDF-1.5\n1 0 obj\n<< /Length 6 >>\nstream\nhello\nendstream\nendobj\n".to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.length_source, LengthSource::Direct);
assert_eq!(slice_of(&pdf, s), b"hello\n");
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn crlf_and_lf_after_stream_are_both_handled() {
let crlf =
b"%PDF-1.5\n1 0 obj\n<< /Length 5 >>\r\nstream\r\nhello\r\nendstream\r\nendobj\n"
.to_vec();
let p = scan(&crlf, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.length_source, LengthSource::Direct);
assert_eq!(slice_of(&crlf, s), b"hello");
assert_eq!(s.data_start, offset_of(&crlf, b"hello"));
p.validate(crlf.len() as u64).unwrap();
let lf = b"%PDF-1.5\n1 0 obj\n<< /Length 5 >>\nstream\nhello\nendstream\nendobj\n".to_vec();
let p = scan(&lf, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.length_source, LengthSource::Direct);
assert_eq!(slice_of(&lf, s), b"hello");
p.validate(lf.len() as u64).unwrap();
}
#[test]
fn lone_cr_after_stream_falls_back() {
let pdf =
b"%PDF-1.5\n1 0 obj\n<< /Length 5 >>\nstream\rhello\r\nendstream\nendobj\n".to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.length_source, LengthSource::Fallback);
assert!(slice_of(&pdf, s).windows(5).any(|w| w == b"hello"));
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn indirect_length_is_resolved_forward_reference() {
let pdf =
b"%PDF-1.5\n1 0 obj\n<< /Length 5 0 R >>\nstream\nhello\nendstream\nendobj\n5 0 obj\n5\nendobj\n"
.to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.length_source, LengthSource::Indirect);
assert_eq!(s.data_start, offset_of(&pdf, b"hello"));
assert_eq!(s.data_len, 5);
assert_eq!(slice_of(&pdf, s), b"hello");
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn stream_filter_classification_reads_flate_and_absent() {
let flate = b"%PDF-1.5\n1 0 obj\n<< /Length 5 /Filter /FlateDecode >>\nstream\nhello\nendstream\nendobj\n"
.to_vec();
let p = scan(&flate, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.filter, FilterClass::FlateDecode);
assert_eq!(slice_of(&flate, s), b"hello");
p.validate(flate.len() as u64).unwrap();
let absent =
b"%PDF-1.5\n1 0 obj\n<< /Length 5 >>\nstream\nhello\nendstream\nendobj\n".to_vec();
let p = scan(&absent, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.filter, FilterClass::Absent);
assert_eq!(slice_of(&absent, s), b"hello");
p.validate(absent.len() as u64).unwrap();
}
#[test]
fn missing_length_uses_keyword_fallback() {
let pdf = b"%PDF-1.5\n1 0 obj\n<< /Type /X >>\nstream\nhello\nendstream\nendobj\n".to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.length_source, LengthSource::Missing);
assert!(slice_of(&pdf, s).windows(5).any(|w| w == b"hello"));
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn wrong_length_past_endstream_falls_back() {
let pdf =
b"%PDF-1.5\n1 0 obj\n<< /Length 100 >>\nstream\nhello\nendstream\nendobj\n".to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.length_source, LengthSource::Fallback);
assert!(slice_of(&pdf, s).windows(5).any(|w| w == b"hello"));
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn correct_length_beats_endstream_bytes_in_payload() {
let payload = b"endstream\nfoo";
let pdf =
b"%PDF-1.5\n1 0 obj\n<< /Length 13 >>\nstream\nendstream\nfoo\nendstream\nendobj\n"
.to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
let s = only_stream(&p);
assert_eq!(s.length_source, LengthSource::Direct);
assert_eq!(s.data_start, offset_of(&pdf, b"endstream\nfoo"));
assert_eq!(s.data_len, payload.len() as u64);
assert_eq!(slice_of(&pdf, s), payload);
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn two_revisions_have_correct_boundaries() {
let r1 = b"%PDF-1.4\n1 0 obj\n<< >>\nendobj\n%%EOF\n";
let r2 = b"2 0 obj\n<< >>\nendobj\n%%EOF";
let mut pdf = Vec::new();
pdf.extend_from_slice(r1);
pdf.extend_from_slice(r2);
let eof_positions: Vec<u64> = pdf
.windows(5)
.enumerate()
.filter(|(_, w)| *w == b"%%EOF")
.map(|(i, _)| i as u64)
.collect();
assert_eq!(eof_positions.len(), 2);
let p = scan(&pdf, Limits::DEFAULT).unwrap();
assert_eq!(p.eofs.len(), 2);
assert_eq!(p.revisions.len(), 2);
let rev1_end = eof_positions[0] + 5;
let rev2_end = eof_positions[1] + 5;
assert_eq!(
p.revisions,
vec![
RevisionInfo {
index: 0,
start: 0,
end: rev1_end,
startxref: None,
prev: None,
},
RevisionInfo {
index: 1,
start: rev1_end + 1,
end: rev2_end,
startxref: None,
prev: None,
},
]
);
assert_eq!(rev2_end, pdf.len() as u64);
assert_eq!(p.objects.len(), 2);
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn classic_xref_revision_records_startxref_and_no_prev() {
let pdf = canonical_pdf();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
assert_eq!(p.revisions.len(), 1);
let r = p.revisions[0];
assert_eq!(r.index, 0);
assert_eq!(r.start, 0);
assert_eq!(r.end, pdf.len() as u64);
assert_eq!(r.startxref, Some(321));
assert_eq!(r.prev, None);
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn incremental_classic_trailer_prev_resolves_to_first_xref() {
let rev1 = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\nxref\n0 2\n0000000000 65535 f \n0000000009 00000 n \ntrailer\n<< /Size 2 /Root 1 0 R >>\nstartxref\n9\n%%EOF\n";
let x1 = rev1
.windows(4)
.position(|w| w == b"xref")
.expect("xref present") as u64;
let rev2 = format!(
"2 0 obj\n<< /Type /Pages >>\nendobj\nxref\n0 3\n0000000000 65535 f \n0000000009 00000 n \n0000000042 00000 n \ntrailer\n<< /Size 3 /Prev {x1} /Root 1 0 R >>\nstartxref\n777\n%%EOF"
);
let mut pdf = Vec::new();
pdf.extend_from_slice(rev1);
pdf.extend_from_slice(rev2.as_bytes());
let p = scan(&pdf, Limits::DEFAULT).unwrap();
assert_eq!(p.revisions.len(), 2);
assert_eq!(p.revisions[0].index, 0);
assert_eq!(p.revisions[0].startxref, Some(9));
assert_eq!(p.revisions[0].prev, None);
assert_eq!(p.revisions[1].index, 1);
assert_eq!(p.revisions[1].startxref, Some(777));
assert_eq!(p.revisions[1].prev, Some(x1));
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn xref_stream_anchor_prev_reference_is_resolved() {
let rev1 = b"%PDF-1.5\n1 0 obj\n<< >>\nendobj\nstartxref\n0\n%%EOF\n";
let rev2 = b"2 0 obj\n<< /Type /XRef /Prev 3 0 R >>\nendobj\n3 0 obj\n<< >>\nendobj\nstartxref\n0\n%%EOF";
let mut pdf = Vec::new();
pdf.extend_from_slice(rev1);
pdf.extend_from_slice(rev2);
let obj3_start = pdf
.windows(8)
.position(|w| w == b"3 0 obj\n")
.expect("object 3 present") as u64;
let p = scan(&pdf, Limits::DEFAULT).unwrap();
assert_eq!(p.revisions.len(), 2);
assert_eq!(p.revisions[0].prev, None);
assert_eq!(p.revisions[1].prev, Some(obj3_start));
assert_eq!(
p.objects.iter().find(|o| o.number == 2).unwrap().role,
ObjRole::XRefStream
);
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn object_roles_are_classified_from_leading_dict() {
let pdf = b"%PDF-1.5\n1 0 obj\n<< /Type /Catalog >>\nendobj\n2 0 obj\n<< /Type /XRef >>\nendobj\n3 0 obj\n<< /Type /ObjStm /N 0 >>\nendobj\n4 0 obj\n<< /Foo /Bar >>\nendobj\n5 0 obj\n<< /Type 5 >>\nendobj\n6 0 obj\n42\nendobj\n7 0 obj\n<< /Foo ( /Type /XRef ) >>\nendobj\n".to_vec();
let p = scan(&pdf, Limits::DEFAULT).unwrap();
let role = |n: u64| p.objects.iter().find(|o| o.number == n).unwrap().role;
assert_eq!(role(1), ObjRole::Generic);
assert_eq!(role(2), ObjRole::XRefStream);
assert_eq!(role(3), ObjRole::ObjectStream);
assert_eq!(role(4), ObjRole::Generic);
assert_eq!(role(5), ObjRole::Generic);
assert_eq!(role(6), ObjRole::Generic);
assert_eq!(role(7), ObjRole::Generic);
p.validate(pdf.len() as u64).unwrap();
}
#[test]
fn random_inputs_keep_streams_and_revisions_consistent() {
let mut state: u64 = 0xdead_beef_cafe_f00d;
for _ in 0..500 {
let len = (xorshift64(&mut state) % 128) as usize;
let mut buf = Vec::with_capacity(len);
for _ in 0..len {
buf.push((xorshift64(&mut state) & 0xff) as u8);
}
let p = scan(&buf, Limits::DEFAULT).unwrap();
p.validate(buf.len() as u64).unwrap();
for s in &p.streams {
assert!(s.data_start + s.data_len <= buf.len() as u64);
}
for r in &p.revisions {
assert!(r.start <= r.end && r.end <= buf.len() as u64);
}
}
}
}