use log::{error, warn};
use std::cmp;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::convert::TryInto;
#[cfg(not(feature = "async"))]
use std::fs::File;
#[cfg(not(feature = "async"))]
use std::io::Read;
use std::path::Path;
use std::sync::Mutex;
#[cfg(feature = "rayon")]
use rayon::prelude::*;
#[cfg(feature = "async")]
use tokio::fs::File;
#[cfg(feature = "async")]
use tokio::io::{AsyncRead, AsyncReadExt};
#[cfg(feature = "async")]
use tokio::pin;
use crate::common_data_structures;
use crate::encryption::{self, EncryptionState};
use crate::error::{ParseError, XrefError};
use crate::load_options::{FilterFunc, LoadOptions};
use crate::object_stream::ObjectStream;
use crate::parser;
use crate::xref::{Xref, XrefEntry, XrefType};
use crate::{Dictionary, Document, Error, IncrementalDocument, Object, ObjectId, Result};
#[cfg(not(feature = "async"))]
impl Document {
#[inline]
pub fn load<P: AsRef<Path>>(path: P) -> Result<Document> {
Self::load_with_options(path, LoadOptions::default())
}
#[inline]
pub fn load_with_options<P: AsRef<Path>>(path: P, options: LoadOptions) -> Result<Document> {
let file = File::open(path)?;
let capacity = Some(file.metadata()?.len() as usize);
Self::load_internal(file, capacity, options)
}
#[inline]
pub fn load_with_password<P: AsRef<Path>>(path: P, password: &str) -> Result<Document> {
Self::load_with_options(path, LoadOptions::with_password(password))
}
#[deprecated(since = "0.41.0", note = "Use load_with_options instead")]
#[inline]
pub fn load_filtered<P: AsRef<Path>>(path: P, filter_func: FilterFunc) -> Result<Document> {
Self::load_with_options(path, LoadOptions::with_filter(filter_func))
}
#[inline]
pub fn load_from<R: Read>(source: R) -> Result<Document> {
Self::load_from_with_options(source, LoadOptions::default())
}
#[inline]
pub fn load_from_with_options<R: Read>(source: R, options: LoadOptions) -> Result<Document> {
Self::load_internal(source, None, options)
}
#[deprecated(since = "0.41.0", note = "Use load_from_with_options instead")]
#[inline]
pub fn load_from_with_password<R: Read>(source: R, password: &str) -> Result<Document> {
Self::load_from_with_options(source, LoadOptions::with_password(password))
}
fn load_internal<R: Read>(mut source: R, capacity: Option<usize>, options: LoadOptions) -> Result<Document> {
let mut buffer = capacity.map(Vec::with_capacity).unwrap_or_default();
source.read_to_end(&mut buffer)?;
Reader {
buffer: &buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: options.password,
strict: options.strict,
max_decompressed_size: options.max_decompressed_size,
normal_offsets: Vec::new(),
}
.read(options.filter)
}
pub fn load_mem(buffer: &[u8]) -> Result<Document> {
Self::load_mem_with_options(buffer, LoadOptions::default())
}
pub fn load_mem_with_options(buffer: &[u8], options: LoadOptions) -> Result<Document> {
Reader {
buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: options.password,
strict: options.strict,
max_decompressed_size: options.max_decompressed_size,
normal_offsets: Vec::new(),
}
.read(options.filter)
}
#[deprecated(since = "0.41.0", note = "Use load_mem_with_options instead")]
pub fn load_mem_with_password(buffer: &[u8], password: &str) -> Result<Document> {
Self::load_mem_with_options(buffer, LoadOptions::with_password(password))
}
#[inline]
pub fn load_metadata<P: AsRef<Path>>(path: P) -> Result<PdfMetadata> {
let file = File::open(path)?;
let capacity = Some(file.metadata()?.len() as usize);
Self::load_metadata_internal(file, capacity, None)
}
#[inline]
pub fn load_metadata_with_password<P: AsRef<Path>>(path: P, password: &str) -> Result<PdfMetadata> {
let file = File::open(path)?;
let capacity = Some(file.metadata()?.len() as usize);
Self::load_metadata_internal(file, capacity, Some(password.to_string()))
}
#[inline]
pub fn load_metadata_from<R: Read>(source: R) -> Result<PdfMetadata> {
Self::load_metadata_internal(source, None, None)
}
#[inline]
pub fn load_metadata_from_with_password<R: Read>(source: R, password: &str) -> Result<PdfMetadata> {
Self::load_metadata_internal(source, None, Some(password.to_string()))
}
#[inline]
pub fn load_metadata_mem(buffer: &[u8]) -> Result<PdfMetadata> {
Reader {
buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: None,
strict: false,
max_decompressed_size: None,
normal_offsets: Vec::new(),
}
.read_metadata()
}
#[inline]
pub fn load_metadata_mem_with_password(buffer: &[u8], password: &str) -> Result<PdfMetadata> {
Reader {
buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: Some(password.to_string()),
strict: false,
max_decompressed_size: None,
normal_offsets: Vec::new(),
}
.read_metadata()
}
fn load_metadata_internal<R: Read>(
mut source: R, capacity: Option<usize>, password: Option<String>,
) -> Result<PdfMetadata> {
let mut buffer = capacity.map(Vec::with_capacity).unwrap_or_default();
source.read_to_end(&mut buffer)?;
Reader {
buffer: &buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password,
strict: false,
max_decompressed_size: None,
normal_offsets: Vec::new(),
}
.read_metadata()
}
}
#[cfg(feature = "async")]
impl Document {
pub async fn load<P: AsRef<Path>>(path: P) -> Result<Document> {
Self::load_with_options(path, LoadOptions::default()).await
}
pub async fn load_with_options<P: AsRef<Path>>(path: P, options: LoadOptions) -> Result<Document> {
let file = File::open(path).await?;
let metadata = file.metadata().await?;
let capacity = Some(metadata.len() as usize);
Self::load_internal(file, capacity, options).await
}
pub async fn load_with_password<P: AsRef<Path>>(path: P, password: &str) -> Result<Document> {
Self::load_with_options(path, LoadOptions::with_password(password)).await
}
#[deprecated(since = "0.41.0", note = "Use load_with_options instead")]
pub async fn load_filtered<P: AsRef<Path>>(path: P, filter_func: FilterFunc) -> Result<Document> {
Self::load_with_options(path, LoadOptions::with_filter(filter_func)).await
}
async fn load_internal<R: AsyncRead>(source: R, capacity: Option<usize>, options: LoadOptions) -> Result<Document> {
pin!(source);
let mut buffer = capacity.map(Vec::with_capacity).unwrap_or_default();
source.read_to_end(&mut buffer).await?;
Reader {
buffer: &buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: options.password,
strict: options.strict,
max_decompressed_size: options.max_decompressed_size,
normal_offsets: Vec::new(),
}
.read(options.filter)
}
pub fn load_mem(buffer: &[u8]) -> Result<Document> {
Self::load_mem_with_options(buffer, LoadOptions::default())
}
pub fn load_mem_with_options(buffer: &[u8], options: LoadOptions) -> Result<Document> {
Reader {
buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: options.password,
strict: options.strict,
max_decompressed_size: options.max_decompressed_size,
normal_offsets: Vec::new(),
}
.read(options.filter)
}
#[inline]
pub async fn load_metadata<P: AsRef<Path>>(path: P) -> Result<PdfMetadata> {
let file = File::open(path).await?;
let metadata = file.metadata().await?;
let capacity = Some(metadata.len() as usize);
Self::load_metadata_internal(file, capacity, None).await
}
#[inline]
pub async fn load_metadata_with_password<P: AsRef<Path>>(path: P, password: &str) -> Result<PdfMetadata> {
let file = File::open(path).await?;
let metadata = file.metadata().await?;
let capacity = Some(metadata.len() as usize);
Self::load_metadata_internal(file, capacity, Some(password.to_string())).await
}
#[inline]
pub async fn load_metadata_from<R: AsyncRead>(source: R) -> Result<PdfMetadata> {
Self::load_metadata_internal(source, None, None).await
}
#[inline]
pub async fn load_metadata_from_with_password<R: AsyncRead>(source: R, password: &str) -> Result<PdfMetadata> {
Self::load_metadata_internal(source, None, Some(password.to_string())).await
}
#[inline]
pub fn load_metadata_mem(buffer: &[u8]) -> Result<PdfMetadata> {
Reader {
buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: None,
strict: false,
max_decompressed_size: None,
normal_offsets: Vec::new(),
}
.read_metadata()
}
#[inline]
pub fn load_metadata_mem_with_password(buffer: &[u8], password: &str) -> Result<PdfMetadata> {
Reader {
buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: Some(password.to_string()),
strict: false,
max_decompressed_size: None,
normal_offsets: Vec::new(),
}
.read_metadata()
}
async fn load_metadata_internal<R: AsyncRead>(
source: R, capacity: Option<usize>, password: Option<String>,
) -> Result<PdfMetadata> {
pin!(source);
let mut buffer = capacity.map(Vec::with_capacity).unwrap_or_default();
source.read_to_end(&mut buffer).await?;
Reader {
buffer: &buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password,
strict: false,
max_decompressed_size: None,
normal_offsets: Vec::new(),
}
.read_metadata()
}
}
impl TryInto<Document> for &[u8] {
type Error = Error;
fn try_into(self) -> Result<Document> {
Reader {
buffer: self,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: None,
strict: false,
max_decompressed_size: None,
normal_offsets: Vec::new(),
}
.read(None)
}
}
#[cfg(not(feature = "async"))]
impl IncrementalDocument {
#[inline]
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path)?;
let capacity = Some(file.metadata()?.len() as usize);
Self::load_internal(file, capacity)
}
#[inline]
pub fn load_from<R: Read>(source: R) -> Result<Self> {
Self::load_internal(source, None)
}
fn load_internal<R: Read>(mut source: R, capacity: Option<usize>) -> Result<Self> {
let mut buffer = capacity.map(Vec::with_capacity).unwrap_or_default();
source.read_to_end(&mut buffer)?;
let document = Reader {
buffer: &buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: None,
strict: false,
max_decompressed_size: None,
normal_offsets: Vec::new(),
}
.read(None)?;
Ok(IncrementalDocument::create_from(buffer, document))
}
pub fn load_mem(buffer: &[u8]) -> Result<Document> {
buffer.try_into()
}
}
#[cfg(feature = "async")]
impl IncrementalDocument {
#[inline]
pub async fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path).await?;
let metadata = file.metadata().await?;
let capacity = Some(metadata.len() as usize);
Self::load_internal(file, capacity).await
}
#[inline]
pub async fn load_from<R: AsyncRead>(source: R) -> Result<Self> {
Self::load_internal(source, None).await
}
async fn load_internal<R: AsyncRead>(source: R, capacity: Option<usize>) -> Result<Self> {
pin!(source);
let mut buffer = capacity.map(Vec::with_capacity).unwrap_or_default();
source.read_to_end(&mut buffer).await?;
let document = Reader {
buffer: &buffer,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: None,
strict: false,
max_decompressed_size: None,
normal_offsets: Vec::new(),
}
.read(None)?;
Ok(IncrementalDocument::create_from(buffer, document))
}
pub fn load_mem(buffer: &[u8]) -> Result<Document> {
buffer.try_into()
}
}
impl TryInto<IncrementalDocument> for &[u8] {
type Error = Error;
fn try_into(self) -> Result<IncrementalDocument> {
let document = Reader {
buffer: self,
document: Document::new(),
encryption_state: None,
raw_objects: BTreeMap::new(),
password: None,
strict: false,
max_decompressed_size: None,
normal_offsets: Vec::new(),
}
.read(None)?;
Ok(IncrementalDocument::create_from(self.to_vec(), document))
}
}
pub struct Reader<'a> {
pub buffer: &'a [u8],
pub document: Document,
pub encryption_state: Option<EncryptionState>,
pub raw_objects: BTreeMap<ObjectId, Vec<u8>>, pub password: Option<String>, pub strict: bool, pub max_decompressed_size: Option<usize>,
normal_offsets: Vec<usize>,
}
pub const MAX_BRACKET: usize = 100;
pub const MAX_NESTING_DEPTH: usize = 100;
const MAX_RECONSTRUCTED_OBJECTS: usize = 1_000_000;
const MAX_TRAILER_CANDIDATES: usize = 16;
#[derive(Debug, Clone)]
pub struct PdfMetadata {
pub title: Option<String>,
pub author: Option<String>,
pub subject: Option<String>,
pub keywords: Option<String>,
pub creator: Option<String>,
pub producer: Option<String>,
pub creation_date: Option<String>,
pub modification_date: Option<String>,
pub custom: HashMap<Vec<u8>, Object>,
pub page_count: u32,
pub version: String,
pub encrypted: bool,
}
struct InfoMetadata {
title: Option<String>,
author: Option<String>,
subject: Option<String>,
keywords: Option<String>,
creator: Option<String>,
producer: Option<String>,
creation_date: Option<String>,
modification_date: Option<String>,
custom: HashMap<Vec<u8>, Object>,
}
impl InfoMetadata {
fn empty() -> Self {
Self {
title: None,
author: None,
subject: None,
keywords: None,
creator: None,
producer: None,
creation_date: None,
modification_date: None,
custom: HashMap::new(),
}
}
}
const STANDARD_INFO_KEYS: &[&[u8]] = &[
b"Title",
b"Author",
b"Subject",
b"Keywords",
b"Creator",
b"Producer",
b"CreationDate",
b"ModDate",
b"Trapped",
];
impl Reader<'_> {
pub fn read_metadata(mut self) -> Result<PdfMetadata> {
let offset = self.buffer.windows(5).position(|w| w == b"%PDF-").unwrap_or(0);
self.buffer = &self.buffer[offset..];
let version = parser::header(self.buffer, self.strict).ok_or(ParseError::InvalidFileHeader)?;
let (xref, trailer) = match self.resolve_xref_and_trailer() {
Ok(resolved) => resolved,
Err(err) => match self.reconstruct_xref_and_trailer() {
Some(reconstructed) => {
warn!("cross-reference resolution failed ({err}); recovered by scanning for indirect objects");
reconstructed
}
None => return Err(err),
},
};
self.set_reference_table(xref);
self.document.trailer = trailer.clone();
let encrypted = self.document.trailer.get(b"Encrypt").is_ok();
let needs_password = if encrypted {
match self.setup_encryption_for_metadata() {
Ok(()) => false,
Err(Error::Unimplemented(_)) => true,
Err(e) => return Err(e),
}
} else {
false
};
let info_metadata = if needs_password {
InfoMetadata::empty()
} else {
self.extract_info_metadata()?
};
let page_count = if needs_password { 0 } else { self.extract_page_count()? };
Ok(PdfMetadata {
title: info_metadata.title,
author: info_metadata.author,
subject: info_metadata.subject,
keywords: info_metadata.keywords,
creator: info_metadata.creator,
producer: info_metadata.producer,
creation_date: info_metadata.creation_date,
modification_date: info_metadata.modification_date,
custom: info_metadata.custom,
page_count,
version,
encrypted,
})
}
fn extract_info_metadata(&self) -> Result<InfoMetadata> {
let info_ref = match self.document.trailer.get(b"Info") {
Ok(obj) => obj.as_reference().ok(),
Err(_) => return Ok(InfoMetadata::empty()),
};
let info_id = match info_ref {
Some(id) => id,
None => return Ok(InfoMetadata::empty()),
};
let mut already_seen = HashSet::new();
let info_obj = match self.get_object(info_id, &mut already_seen) {
Ok(obj) => obj,
Err(_) => return Ok(InfoMetadata::empty()),
};
let info_dict = match info_obj.as_dict() {
Ok(dict) => dict,
Err(_) => return Ok(InfoMetadata::empty()),
};
let mut custom = HashMap::new();
for (key, value) in info_dict.iter() {
if STANDARD_INFO_KEYS.contains(&key.as_slice()) {
continue;
}
custom.insert(key.clone(), value.clone());
}
Ok(InfoMetadata {
title: Self::extract_string_field(info_dict, b"Title"),
author: Self::extract_string_field(info_dict, b"Author"),
subject: Self::extract_string_field(info_dict, b"Subject"),
keywords: Self::extract_string_field(info_dict, b"Keywords"),
creator: Self::extract_string_field(info_dict, b"Creator"),
producer: Self::extract_string_field(info_dict, b"Producer"),
creation_date: Self::extract_string_field(info_dict, b"CreationDate"),
modification_date: Self::extract_string_field(info_dict, b"ModDate"),
custom,
})
}
fn extract_string_field(dict: &Dictionary, key: &[u8]) -> Option<String> {
match dict.get(key) {
Ok(obj) => match obj {
Object::String(_bytes, _) => common_data_structures::decode_text_string(obj).ok(),
_ => None,
},
Err(_) => None,
}
}
fn extract_page_count(&self) -> Result<u32> {
let root_ref = match self.document.trailer.get(b"Root").and_then(Object::as_reference) {
Ok(id) => id,
Err(_) => return Ok(0),
};
let mut already_seen = HashSet::new();
let catalog_obj = match self.get_object(root_ref, &mut already_seen) {
Ok(obj) => obj,
Err(_) => return Ok(0),
};
let catalog_dict = match catalog_obj.as_dict() {
Ok(dict) => dict,
Err(_) => return Ok(0),
};
let pages_ref = match catalog_dict.get(b"Pages").and_then(Object::as_reference) {
Ok(id) => id,
Err(_) => return Ok(0),
};
self.get_pages_tree_count(pages_ref, &mut HashSet::new(), 0).or(Ok(0))
}
fn get_pages_tree_count(&self, pages_id: ObjectId, seen: &mut HashSet<ObjectId>, depth: usize) -> Result<u32> {
if depth >= MAX_NESTING_DEPTH {
return Ok(0);
}
if seen.contains(&pages_id) {
return Err(Error::ReferenceCycle(pages_id));
}
seen.insert(pages_id);
let mut already_seen = HashSet::new();
let pages_obj = match self.get_object(pages_id, &mut already_seen) {
Ok(obj) => obj,
Err(_) => return Ok(0),
};
let pages_dict = match pages_obj.as_dict() {
Ok(dict) => dict,
Err(_) => return Ok(0),
};
match pages_dict.get_type() {
Ok(type_name) if type_name == b"Page" => Ok(1),
Ok(type_name) if type_name == b"Pages" => {
if let Ok(count_obj) = pages_dict.get(b"Count")
&& let Ok(count) = count_obj.as_i64()
&& count >= 0
{
return Ok(count as u32);
}
let kids = match pages_dict.get(b"Kids").and_then(Object::as_array) {
Ok(arr) => arr,
Err(_) => return Ok(0),
};
let mut total = 0u32;
for kid in kids.iter() {
if let Ok(kid_ref) = kid.as_reference()
&& let Ok(count) = self.get_pages_tree_count(kid_ref, seen, depth + 1)
{
total += count;
}
}
Ok(total)
}
_ => Ok(1),
}
}
pub fn read(mut self, filter_func: Option<FilterFunc>) -> Result<Document> {
let offset = self.buffer.windows(5).position(|w| w == b"%PDF-").unwrap_or(0);
self.buffer = &self.buffer[offset..];
let version = parser::header(self.buffer, self.strict).ok_or(ParseError::InvalidFileHeader)?;
if let Some(pos) = self.buffer.iter().position(|&byte| byte == b'\n')
&& let Some(binary_mark) = parser::binary_mark(&self.buffer[pos + 1..])
&& binary_mark.iter().all(|&byte| byte >= 128)
{
self.document.binary_mark = binary_mark;
}
let (xref, trailer) = match self.resolve_xref_and_trailer() {
Ok(resolved) => resolved,
Err(err) => match self.reconstruct_xref_and_trailer() {
Some(reconstructed) => {
warn!("cross-reference resolution failed ({err}); recovered by scanning for indirect objects");
reconstructed
}
None => return Err(err),
},
};
self.document.version = version;
self.document.max_id = xref.size - 1;
self.document.trailer = trailer;
self.set_reference_table(xref);
let is_encrypted = self.document.trailer.get(b"Encrypt").is_ok();
if is_encrypted {
self.load_encrypted_document(filter_func)?;
} else {
self.load_objects_raw(filter_func)?;
}
if let Some(&max_loaded_id) = self.document.objects.keys().next_back() {
self.document.max_id = self.document.max_id.max(max_loaded_id.0);
}
Ok(self.document)
}
fn load_encrypted_document(&mut self, _filter_func: Option<FilterFunc>) -> Result<()> {
let entries: Vec<_> = self
.document
.reference_table
.entries
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect();
let mut object_streams = Vec::new();
for (obj_num, entry) in entries {
match entry {
XrefEntry::Normal { offset, .. } => {
if let Ok((obj_id, raw_bytes)) = self.extract_raw_object(offset as usize) {
self.raw_objects.insert(obj_id, raw_bytes);
}
}
XrefEntry::Compressed { container, index } => {
object_streams.push((obj_num, container, index));
}
XrefEntry::Free | XrefEntry::UnusableFree => {
}
}
}
self.parse_encryption_dictionary()?;
if self.authenticate_and_setup_encryption(false)?.is_none() {
return Ok(());
}
if let Some(ref state) = self.encryption_state {
let encrypt_ref = self
.document
.trailer
.get(b"Encrypt")
.ok()
.and_then(|o| o.as_reference().ok());
for (obj_id, raw_bytes) in &self.raw_objects {
if let Some(enc_ref) = encrypt_ref
&& *obj_id == enc_ref
{
continue;
}
if let Ok((id, mut obj)) = self.parse_raw_object(raw_bytes) {
let _ = encryption::decrypt_object(state, *obj_id, &mut obj);
self.document.objects.insert(id, obj);
}
}
let mut streams_to_process: std::collections::HashMap<u32, Vec<(u32, u16)>> =
std::collections::HashMap::new();
for (obj_num, container_id, index) in object_streams {
streams_to_process
.entry(container_id)
.or_default()
.push((obj_num, index));
}
for (container_id, objects_in_stream) in streams_to_process {
if let Some(container_obj) = self.document.objects.get(&(container_id, 0))
&& let Ok(stream) = container_obj.as_stream()
{
match ObjectStream::new_with_limit(stream, self.max_decompressed_size) {
Ok(object_stream) => {
for (obj_num, _index) in objects_in_stream {
let obj_id = (obj_num, 0);
if let Some(obj) = object_stream.objects.get(&obj_id) {
self.document.objects.insert(obj_id, obj.clone());
}
}
}
Err(_e) => {}
}
}
}
let mut stored_state = state.clone();
stored_state.encrypt_object_id = encrypt_ref;
self.document.encryption_state = Some(stored_state);
if let Some(enc_ref) = encrypt_ref {
self.document.objects.remove(&enc_ref);
}
self.document.trailer.remove(b"Encrypt");
}
Ok(())
}
fn parse_raw_object(&self, raw_bytes: &[u8]) -> Result<(ObjectId, Object)> {
parser::indirect_object(raw_bytes, 0, None, self, &mut HashSet::new(), None)
}
fn set_reference_table(&mut self, xref: Xref) {
let mut normal_offsets: Vec<usize> = xref
.entries
.values()
.filter_map(|entry| match entry {
XrefEntry::Normal { offset, .. } => Some(*offset as usize),
_ => None,
})
.collect();
normal_offsets.sort_unstable();
normal_offsets.dedup();
self.normal_offsets = normal_offsets;
self.document.reference_table = xref;
}
fn load_objects_raw(&mut self, filter_func: Option<FilterFunc>) -> Result<()> {
let is_encrypted = self.document.trailer.get(b"Encrypt").is_ok();
let zero_length_streams = Mutex::new(vec![]);
let object_streams = Mutex::new(vec![]);
let compressed_obj_containers: BTreeMap<u32, u32> = self
.document
.reference_table
.entries
.iter()
.filter_map(|(&id, entry)| {
if let XrefEntry::Compressed { container, .. } = entry {
Some((id, *container))
} else {
None
}
})
.collect();
let normal_offsets = &self.normal_offsets;
let entries_filter_map = |(_, entry): (&_, &_)| -> Result<Option<(ObjectId, Object)>> {
if let XrefEntry::Normal { offset, .. } = *entry {
let offset = offset as usize;
let next_index = normal_offsets.partition_point(|&next| next <= offset);
let next_object = normal_offsets.get(next_index).copied();
let end = self.object_end(offset, next_object);
let result = self.read_object_to(offset, end, None, &mut HashSet::new());
let (object_id, mut object) = match result {
Ok(obj) => obj,
Err(e) => {
if is_encrypted {
warn!("Skipping encrypted object at offset {}: {:?}", offset, e);
return Ok(None);
}
error!("Object load error at offset {}: {e:?}", offset);
return if self.strict { Err(e) } else { Ok(None) };
}
};
if let Some(filter_func) = filter_func
&& filter_func(object_id, &mut object).is_none()
{
return Ok(None);
}
if let Ok(stream) = object.as_stream() {
if stream.dict.has_type(b"ObjStm") && !is_encrypted {
let obj_stream = match ObjectStream::new_with_limit(stream, self.max_decompressed_size) {
Ok(obj_stream) => obj_stream,
Err(e) => {
error!("Object stream load error for {object_id:?}: {e:?}");
return if self.strict { Err(e) } else { Ok(None) };
}
};
let container_id = object_id.0;
let mut object_streams = object_streams.lock().unwrap();
if let Some(filter_func) = filter_func {
let objects: BTreeMap<(u32, u16), Object> = obj_stream
.objects
.into_iter()
.filter(|((obj_num, _), _)| {
compressed_obj_containers
.get(obj_num)
.is_none_or(|&c| c == container_id)
})
.filter_map(|(object_id, mut object)| filter_func(object_id, &mut object))
.collect();
object_streams.extend(objects);
} else {
object_streams.extend(obj_stream.objects.into_iter().filter(|((obj_num, _), _)| {
compressed_obj_containers
.get(obj_num)
.is_none_or(|&c| c == container_id)
}));
}
} else if stream.content.is_empty() {
let mut zero_length_streams = zero_length_streams.lock().unwrap();
zero_length_streams.push(object_id);
}
}
Ok(Some((object_id, object)))
} else {
Ok(None)
}
};
#[cfg(feature = "rayon")]
{
self.document.objects = self
.document
.reference_table
.entries
.par_iter()
.map(entries_filter_map)
.filter_map(Result::transpose)
.collect::<Result<BTreeMap<_, _>>>()?;
}
#[cfg(not(feature = "rayon"))]
{
self.document.objects = self
.document
.reference_table
.entries
.iter()
.map(entries_filter_map)
.filter_map(Result::transpose)
.collect::<Result<BTreeMap<_, _>>>()?;
}
for (id, entry) in object_streams.into_inner().unwrap() {
self.document.objects.entry(id).or_insert(entry);
}
for object_id in zero_length_streams.into_inner().unwrap() {
let _ = self.read_stream_content(object_id);
}
Ok(())
}
fn read_stream_content(&mut self, object_id: ObjectId) -> Result<()> {
let length = self.get_stream_length(object_id)?;
let stream = self
.document
.get_object_mut(object_id)
.and_then(Object::as_stream_mut)?;
let start = stream
.start_position
.ok_or(Error::InvalidStream("missing start position".to_string()))?;
if length < 0 {
return Err(Error::InvalidStream("negative stream length.".to_string()));
}
let length = usize::try_from(length).map_err(|e| Error::NumericCast(e.to_string()))?;
let end = start + length;
if end > self.buffer.len() {
return Err(Error::InvalidStream("stream extends after document end.".to_string()));
}
stream.set_content(self.buffer[start..end].to_vec());
Ok(())
}
fn get_stream_length(&self, object_id: ObjectId) -> Result<i64> {
let object = self.document.get_object(object_id)?;
let stream = object.as_stream()?;
stream
.dict
.get(b"Length")
.and_then(|value| self.document.dereference(value))
.and_then(|(_id, obj)| match obj.as_i64() {
Ok(length) => Ok(length),
Err(err) => match obj.as_f32() {
Ok(value) if value.fract() == 0.0 && value >= -(2f32.powi(63)) && value < 2f32.powi(63) => {
Ok(value as i64)
}
_ => Err(err),
},
})
.inspect_err(|_err| {
error!(
"stream dictionary of '{} {} R' is missing the Length entry",
object_id.0, object_id.1
);
})
}
fn get_offset(&self, id: ObjectId) -> Result<u32> {
let entry = self.document.reference_table.get(id.0).ok_or(Error::MissingXrefEntry)?;
match *entry {
XrefEntry::Normal { offset, generation } if generation == id.1 => Ok(offset),
_ => Err(Error::MissingXrefEntry),
}
}
fn get_compressed_object(&self, id: ObjectId) -> Result<Object> {
let entry = self.document.reference_table.get(id.0).ok_or(Error::MissingXrefEntry)?;
let container_id = match entry {
XrefEntry::Compressed { container, .. } => *container,
_ => return Err(Error::MissingXrefEntry),
};
let container_id = (container_id, 0);
let mut already_seen = HashSet::new();
let container_obj = self.get_object(container_id, &mut already_seen)?;
let container_stream = container_obj.as_stream()?;
let object_stream = ObjectStream::new_with_limit(container_stream, self.max_decompressed_size)?;
object_stream.objects.get(&id).cloned().ok_or(Error::MissingXrefEntry)
}
pub fn get_object(&self, id: ObjectId, already_seen: &mut HashSet<ObjectId>) -> Result<Object> {
if already_seen.contains(&id) {
warn!("reference cycle detected resolving object {} {}", id.0, id.1);
return Err(Error::ReferenceCycle(id));
}
already_seen.insert(id);
if let Some(entry) = self.document.reference_table.get(id.0)
&& matches!(entry, XrefEntry::Compressed { .. })
{
return self.get_compressed_object(id);
}
let offset = self.get_offset(id)?;
let (_, mut obj) = self.read_object(offset as usize, Some(id), already_seen)?;
if let Some(ref state) = self.encryption_state {
let encrypt_ref = self
.document
.trailer
.get(b"Encrypt")
.ok()
.and_then(|o| o.as_reference().ok());
if let Some(enc_ref) = encrypt_ref
&& id != enc_ref
{
encryption::decrypt_object(state, id, &mut obj).map_err(Error::Decryption)?;
}
}
Ok(obj)
}
fn parse_encryption_dictionary(&mut self) -> Result<()> {
if let Ok(encrypt_ref) = self.document.trailer.get(b"Encrypt").and_then(|o| o.as_reference()) {
if self.raw_objects.is_empty() {
let offset = self.get_offset(encrypt_ref)?;
let (_, encrypt_obj) = self.read_object(offset as usize, Some(encrypt_ref), &mut HashSet::new())?;
self.document.objects.insert(encrypt_ref, encrypt_obj);
} else if let Some(raw_bytes) = self.raw_objects.get(&encrypt_ref)
&& let Ok((_, obj)) = self.parse_raw_object(raw_bytes)
{
self.document.objects.insert(encrypt_ref, obj);
}
}
Ok(())
}
fn authenticate_and_setup_encryption(&mut self, require_password: bool) -> Result<Option<String>> {
let password_to_use: Option<String> = if self.document.authenticate_password("").is_ok() {
Some(String::new())
} else if let Some(ref pwd) = self.password {
if self.document.authenticate_password(pwd).is_ok() {
Some(pwd.clone())
} else if require_password {
return Err(Error::InvalidPassword);
} else {
warn!("Invalid password provided for encrypted PDF");
return Err(Error::InvalidPassword);
}
} else if require_password {
return Err(Error::Unimplemented(
"PDF is encrypted and requires a password. Use Document::load_metadata_with_password() instead.",
));
} else {
warn!("PDF is encrypted and requires a password");
return Ok(None);
};
if let Some(ref password) = password_to_use {
let state = EncryptionState::decode(&self.document, password)?;
self.encryption_state = Some(state);
}
Ok(password_to_use)
}
fn setup_encryption_for_metadata(&mut self) -> Result<()> {
self.parse_encryption_dictionary()?;
self.authenticate_and_setup_encryption(true)?;
Ok(())
}
fn extract_raw_object(&mut self, offset: usize) -> Result<(ObjectId, Vec<u8>)> {
if offset > self.buffer.len() {
return Err(Error::InvalidOffset(offset));
}
let slice = &self.buffer[offset..];
let mut pos = 0;
while pos < slice.len() && slice[pos].is_ascii_whitespace() {
pos += 1;
}
let num_start = pos;
while pos < slice.len() && slice[pos].is_ascii_digit() {
pos += 1;
}
let obj_num: u32 = std::str::from_utf8(&slice[num_start..pos])
.ok()
.and_then(|s| s.parse().ok())
.ok_or(Error::Parse(ParseError::InvalidXref))?;
while pos < slice.len() && slice[pos].is_ascii_whitespace() {
pos += 1;
}
let gen_start = pos;
while pos < slice.len() && slice[pos].is_ascii_digit() {
pos += 1;
}
let obj_gen: u16 = std::str::from_utf8(&slice[gen_start..pos])
.ok()
.and_then(|s| s.parse().ok())
.ok_or(Error::Parse(ParseError::InvalidXref))?;
while pos < slice.len() && slice[pos].is_ascii_whitespace() {
pos += 1;
}
if pos + 3 > slice.len() || &slice[pos..pos + 3] != b"obj" {
return Err(Error::Parse(ParseError::InvalidXref));
}
pos += 3;
let endobj_pattern = b"endobj";
let mut end_pos = pos;
while end_pos + endobj_pattern.len() <= slice.len() {
if &slice[end_pos..end_pos + endobj_pattern.len()] == endobj_pattern {
end_pos += endobj_pattern.len();
break;
}
end_pos += 1;
}
if end_pos > slice.len() {
return Err(Error::Parse(ParseError::InvalidXref));
}
let raw_bytes = slice[0..end_pos].to_vec();
Ok(((obj_num, obj_gen), raw_bytes))
}
fn read_object(
&self, offset: usize, expected_id: Option<ObjectId>, already_seen: &mut HashSet<ObjectId>,
) -> Result<(ObjectId, Object)> {
let next_index = self.normal_offsets.partition_point(|&next| next <= offset);
let end = self.object_end(offset, self.normal_offsets.get(next_index).copied());
self.read_object_to(offset, end, expected_id, already_seen)
}
fn object_end(&self, offset: usize, next_object: Option<usize>) -> usize {
let xref_start = (self.document.xref_start > offset).then_some(self.document.xref_start);
next_object
.into_iter()
.chain(xref_start)
.min()
.unwrap_or(self.buffer.len())
.min(self.buffer.len())
}
fn read_object_to(
&self, offset: usize, end: usize, expected_id: Option<ObjectId>, already_seen: &mut HashSet<ObjectId>,
) -> Result<(ObjectId, Object)> {
if offset > end || end > self.buffer.len() {
return Err(Error::InvalidOffset(offset));
}
parser::indirect_object(self.buffer, offset, expected_id, self, already_seen, Some(end))
}
fn xref_and_trailer_at(&self, offset: usize) -> Result<(Xref, Dictionary)> {
let offset = self.correct_xref_offset(offset);
parser::xref_and_trailer(&self.buffer[offset..], self)
}
fn resolve_xref_and_trailer(&mut self) -> Result<(Xref, Dictionary)> {
let xref_start = Self::get_xref_start(self.buffer)?;
if xref_start > self.buffer.len() {
return Err(Error::Xref(XrefError::Start));
}
let xref_start = self.correct_xref_offset(xref_start);
self.document.xref_start = xref_start;
let (mut xref, mut trailer) = self.xref_and_trailer_at(xref_start)?;
let mut already_seen = HashSet::new();
let mut prev_xref_start = trailer.remove(b"Prev");
while let Some(prev) = prev_xref_start.and_then(|offset| offset.as_i64().ok()) {
if already_seen.contains(&prev) {
break;
}
already_seen.insert(prev);
if prev < 0 || prev as usize > self.buffer.len() {
return Err(Error::Xref(XrefError::PrevStart));
}
let (prev_xref, prev_trailer) = self.xref_and_trailer_at(prev as usize)?;
xref.merge(prev_xref);
let prev_xref_stream_start = trailer.remove(b"XRefStm");
if let Some(prev) = prev_xref_stream_start.and_then(|offset| offset.as_i64().ok()) {
if prev < 0 || prev as usize > self.buffer.len() {
return Err(Error::Xref(XrefError::StreamStart));
}
let (prev_xref, _) = self.xref_and_trailer_at(prev as usize)?;
xref.merge(prev_xref);
}
prev_xref_start = prev_trailer.get(b"Prev").cloned().ok();
}
let xref_entry_count = xref.max_id().checked_add(1).ok_or(ParseError::InvalidXref)?;
if xref.size != xref_entry_count {
warn!(
"Size entry of trailer dictionary is {}, correct value is {}.",
xref.size, xref_entry_count
);
xref.size = xref_entry_count;
}
Ok((xref, trailer))
}
fn reconstruct_xref_and_trailer(&mut self) -> Option<(Xref, Dictionary)> {
if self.strict {
return None;
}
u32::try_from(self.buffer.len()).ok()?;
let markers = Self::scan_object_markers(self.buffer);
if markers.is_empty() {
return None;
}
let mut xref = Xref::new(markers.len() as u32, XrefType::CrossReferenceTable);
for (offset, (number, generation)) in markers {
xref.insert(number, XrefEntry::Normal { offset, generation });
}
xref.size = xref.max_id().saturating_add(1);
let (_trailer_pos, trailer) = self.find_latest_trailer(&xref)?;
self.document.xref_start = 0;
warn!(
"reconstructed cross-reference table with {} objects by scanning for indirect objects",
xref.entries.len()
);
Some((xref, trailer))
}
fn scan_object_markers(buffer: &[u8]) -> Vec<(u32, ObjectId)> {
const STREAM_KEYWORD: &[u8] = b"stream";
const END_STREAM_KEYWORD: &[u8] = b"endstream";
let mut markers = Vec::new();
let mut oversized_number_warned = false;
let mut at_line_start = true;
let mut pos = 0;
while pos < buffer.len() {
if buffer[pos..].starts_with(STREAM_KEYWORD)
&& !buffer[..pos].ends_with(b"end")
&& matches!(buffer.get(pos + STREAM_KEYWORD.len()), Some(b'\r' | b'\n'))
{
let after_keyword = pos + STREAM_KEYWORD.len();
if let Some(relative) = buffer[after_keyword..]
.windows(END_STREAM_KEYWORD.len())
.position(|window| window == END_STREAM_KEYWORD)
{
pos = after_keyword + relative + END_STREAM_KEYWORD.len();
at_line_start = false;
continue;
}
if let Some(resume) = Self::payload_end_by_length(buffer, pos) {
pos = resume;
at_line_start = false;
continue;
}
}
if at_line_start
&& buffer[pos].is_ascii_digit()
&& let Some(id) = Self::parse_object_header(&buffer[pos..])
{
if id.0 > MAX_RECONSTRUCTED_OBJECTS as u32 {
if !oversized_number_warned {
warn!("ignoring object headers numbered above {MAX_RECONSTRUCTED_OBJECTS}");
oversized_number_warned = true;
}
} else {
if markers.len() == MAX_RECONSTRUCTED_OBJECTS {
warn!(
"object marker scan stopped at the {MAX_RECONSTRUCTED_OBJECTS}-marker cap; reconstruction may be incomplete"
);
break;
}
markers.push((pos as u32, id));
}
}
match buffer[pos] {
b'\r' | b'\n' => at_line_start = true,
b' ' | b'\t' => {}
_ => at_line_start = false,
}
pos += 1;
}
markers
}
fn payload_end_by_length(buffer: &[u8], stream_pos: usize) -> Option<usize> {
const LENGTH_KEY: &[u8] = b"/Length";
const OBJ_KEYWORD: &[u8] = b"obj";
const STREAM_KEYWORD_LEN: usize = b"stream".len();
let head = &buffer[..stream_pos];
let obj_pos = head
.windows(OBJ_KEYWORD.len())
.rposition(|window| window == OBJ_KEYWORD)?;
let dict_region = &buffer[obj_pos + OBJ_KEYWORD.len()..stream_pos];
let key_pos = dict_region
.windows(LENGTH_KEY.len())
.rposition(|window| window == LENGTH_KEY)?;
let rest = &dict_region[key_pos + LENGTH_KEY.len()..];
let digits_start = rest.iter().position(u8::is_ascii_digit)?;
let digits_len = rest[digits_start..]
.iter()
.take_while(|byte| byte.is_ascii_digit())
.count();
if digits_len > 10 {
return None;
}
match rest[digits_start + digits_len..]
.iter()
.copied()
.find(|byte| !byte.is_ascii_whitespace())
{
Some(b'/') | Some(b'>') => {}
_ => return None,
}
let length: usize = std::str::from_utf8(&rest[digits_start..digits_start + digits_len])
.ok()?
.parse()
.ok()?;
let eol_len = match (
buffer.get(stream_pos + STREAM_KEYWORD_LEN),
buffer.get(stream_pos + STREAM_KEYWORD_LEN + 1),
) {
(Some(b'\r'), Some(b'\n')) => 2,
_ => 1,
};
let end = (stream_pos + STREAM_KEYWORD_LEN + eol_len).checked_add(length)?;
(buffer.len() >= end).then_some(end)
}
fn parse_object_header(input: &[u8]) -> Option<ObjectId> {
fn digits(input: &[u8], cap: usize) -> Option<(&[u8], &[u8])> {
let n = input.iter().take_while(|b| b.is_ascii_digit()).count();
(0 < n && n <= cap).then(|| input.split_at(n))
}
fn spaces(input: &[u8]) -> Option<&[u8]> {
let n = input
.iter()
.take_while(|&&b| matches!(b, b' ' | b'\t' | b'\r' | b'\n'))
.count();
(n > 0).then(|| &input[n..])
}
let (number, rest) = digits(input, 10)?;
let number: u32 = std::str::from_utf8(number).ok()?.parse().ok()?;
let rest = spaces(rest)?;
let (generation, rest) = digits(rest, 5)?;
let generation: u16 = std::str::from_utf8(generation).ok()?.parse().ok()?;
let rest = spaces(rest)?;
let tail = rest.strip_prefix(b"obj")?;
match tail.first() {
None => {}
Some(&byte) if !byte.is_ascii_alphanumeric() => {}
_ => return None,
}
Some((number, generation))
}
fn find_latest_trailer(&self, xref: &Xref) -> Option<(usize, Dictionary)> {
let mut bound = self.buffer.len();
for _ in 0..MAX_TRAILER_CANDIDATES {
let Some(pos) = Reader::search_substring(&self.buffer[..bound], b"trailer", 0) else {
break;
};
bound = pos;
let after_keyword = &self.buffer[pos + b"trailer".len()..];
let Some(dict_start) = after_keyword.iter().position(|byte| !byte.is_ascii_whitespace()) else {
continue;
};
let Ok((_, dict)) = parser::dictionary(&after_keyword[dict_start..]) else {
continue;
};
if dict
.get(b"Root")
.and_then(Object::as_reference)
.is_ok_and(|root| xref.entries.contains_key(&root.0))
{
return Some((pos, dict));
}
}
None
}
fn correct_xref_offset(&self, offset: usize) -> usize {
const RECOVERY_WINDOW: usize = 64;
if self.strict || offset >= self.buffer.len() {
return offset;
}
let rest = &self.buffer[offset..];
if rest.starts_with(b"xref") || Self::starts_indirect_object(rest) {
return offset;
}
let window_start = offset.saturating_sub(RECOVERY_WINDOW);
let window_end = cmp::min(self.buffer.len(), offset + RECOVERY_WINDOW);
let mut corrected: Option<usize> = None;
for pos in window_start..window_end.saturating_sub(4) {
if !self.buffer[pos..].starts_with(b"xref") {
continue;
}
if pos >= 5 && &self.buffer[pos - 5..pos] == b"start" {
continue;
}
if corrected.is_none_or(|best: usize| pos.abs_diff(offset) < best.abs_diff(offset)) {
corrected = Some(pos);
}
}
match corrected {
Some(pos) => {
warn!(
"Cross-reference offset {} does not point at an xref section; using nearby offset {} instead.",
offset, pos
);
pos
}
None => offset,
}
}
fn starts_indirect_object(input: &[u8]) -> bool {
Self::parse_object_header(input).is_some()
}
fn get_xref_start(buffer: &[u8]) -> Result<usize> {
let seek_pos = buffer.len() - cmp::min(buffer.len(), 512);
Self::search_substring(buffer, b"%%EOF", seek_pos)
.filter(|&eof_pos| eof_pos > 25)
.and_then(|eof_pos| Self::search_substring(&buffer[..eof_pos], b"startxref", eof_pos - 25))
.ok_or(Error::Xref(XrefError::Start))
.and_then(|xref_pos| {
if xref_pos <= buffer.len() {
match parser::xref_start(&buffer[xref_pos..]) {
Some(startxref) => Ok(startxref as usize),
None => Err(Error::Xref(XrefError::Start)),
}
} else {
Err(Error::Xref(XrefError::Start))
}
})
}
fn search_substring(buffer: &[u8], pattern: &[u8], start_pos: usize) -> Option<usize> {
buffer
.get(start_pos..)?
.windows(pattern.len())
.rposition(|window| window == pattern)
.map(|pos| start_pos + pos)
}
}
#[cfg(all(test, not(feature = "async")))]
#[test]
fn load_document() {
let mut doc = Document::load("assets/example.pdf").unwrap();
assert_eq!(doc.version, "1.5");
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_2_load.pdf");
doc.save(file_path).unwrap();
}
#[cfg(all(test, feature = "async"))]
#[tokio::test]
async fn load_document() {
let mut doc = Document::load("assets/example.pdf").await.unwrap();
assert_eq!(doc.version, "1.5");
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_2_load.pdf");
doc.save(file_path).unwrap();
}
#[test]
#[should_panic(expected = "Xref(Start)")]
fn load_short_document() {
let _doc = Document::load_mem(b"%PDF-1.5\n%%EOF\n").unwrap();
}
#[test]
fn load_document_with_preceding_bytes() {
let mut content = Vec::new();
content.extend(b"garbage");
content.extend(include_bytes!("../assets/example.pdf"));
let doc = Document::load_mem(&content).unwrap();
assert_eq!(doc.version, "1.5");
}
#[test]
fn load_many_shallow_brackets() {
let content: String = std::iter::repeat_n("()", MAX_BRACKET * 10)
.flat_map(|x| x.chars())
.collect();
const STREAM_CRUFT: usize = 33;
let doc = format!(
"%PDF-1.5
1 0 obj<</Type/Pages/Kids[5 0 R]/Count 1/Resources 3 0 R/MediaBox[0 0 595 842]>>endobj
2 0 obj<</Type/Font/Subtype/Type1/BaseFont/Courier>>endobj
3 0 obj<</Font<</F1 2 0 R>>>>endobj
5 0 obj<</Type/Page/Parent 1 0 R/Contents[4 0 R]>>endobj
6 0 obj<</Type/Catalog/Pages 1 0 R>>endobj
4 0 obj<</Length {}>>stream
BT
/F1 48 Tf
100 600 Td
({}) Tj
ET
endstream endobj\n",
content.len() + STREAM_CRUFT,
content
);
let doc = format!(
"{}xref
0 7
0000000000 65535 f
0000000009 00000 n
0000000096 00000 n
0000000155 00000 n
0000000291 00000 n
0000000191 00000 n
0000000248 00000 n
trailer
<</Root 6 0 R/Size 7>>
startxref
{}
%%EOF",
doc,
doc.len()
);
let _doc = Document::load_mem(doc.as_bytes()).unwrap();
}
#[test]
fn load_too_deep_brackets() {
let content: Vec<u8> = std::iter::repeat_n(b'(', MAX_BRACKET + 1)
.chain(std::iter::repeat_n(b')', MAX_BRACKET + 1))
.collect();
let content = String::from_utf8(content).unwrap();
const STREAM_CRUFT: usize = 33;
let doc = format!(
"%PDF-1.5
1 0 obj<</Type/Pages/Kids[5 0 R]/Count 1/Resources 3 0 R/MediaBox[0 0 595 842]>>endobj
2 0 obj<</Type/Font/Subtype/Type1/BaseFont/Courier>>endobj
3 0 obj<</Font<</F1 2 0 R>>>>endobj
5 0 obj<</Type/Page/Parent 1 0 R/Contents[7 0 R 4 0 R]>>endobj
6 0 obj<</Type/Catalog/Pages 1 0 R>>endobj
7 0 obj<</Length 45>>stream
BT /F1 48 Tf 100 600 Td (Hello World!) Tj ET
endstream
endobj
4 0 obj<</Length {}>>stream
BT
/F1 48 Tf
100 600 Td
({}) Tj
ET
endstream endobj\n",
content.len() + STREAM_CRUFT,
content
);
let doc = format!(
"{}xref
0 7
0000000000 65535 f
0000000009 00000 n
0000000096 00000 n
0000000155 00000 n
0000000387 00000 n
0000000191 00000 n
0000000254 00000 n
0000000297 00000 n
trailer
<</Root 6 0 R/Size 7>>
startxref
{}
%%EOF",
doc,
doc.len()
);
let doc = Document::load_mem(doc.as_bytes()).unwrap();
let pages = doc.get_pages().keys().cloned().collect::<Vec<_>>();
assert_eq!("Hello World!\n", doc.extract_text(&pages).unwrap());
}
#[cfg(all(test, not(feature = "async")))]
#[test]
fn search_substring_finds_last_occurrence() {
assert_eq!(Reader::search_substring(b"hello world", b"xyz", 0), None);
assert_eq!(Reader::search_substring(b"hello world", b"world", 0), Some(6));
let buffer = b"%%EOF\ntest%%EOF\nend";
assert_eq!(Reader::search_substring(buffer, b"%%EOF", 0), Some(10));
assert_eq!(Reader::search_substring(buffer, b"%%EOF", 6), Some(10));
assert_eq!(Reader::search_substring(buffer, b"%%EOF", 15), None);
assert_eq!(Reader::search_substring(b"%%EOF", b"%%EOF", 0), Some(0));
let buffer_with_many_percents = b"%%%PDF-1.3%%%comment%%%more%%EOF";
assert_eq!(
Reader::search_substring(buffer_with_many_percents, b"%%EOF", 0),
Some(27)
);
}
#[cfg(all(test, not(feature = "async")))]
#[test]
fn get_xref_start_ignores_startxref_past_eof() {
let mut buf = Vec::new();
buf.extend_from_slice(&[b' '; 200]);
let xref_offset = 100usize;
let startxref_block = format!("startxref\n{}\n%%EOF\n", xref_offset);
let _startxref_pos = buf.len();
buf.extend_from_slice(startxref_block.as_bytes());
let bad_block = "startxref\n999\n%%EO\x00\n".to_string();
buf.extend_from_slice(bad_block.as_bytes());
let result = Reader::get_xref_start(&buf).unwrap();
assert_eq!(result, xref_offset);
assert_ne!(result, 999);
}
#[cfg(all(test, not(feature = "async")))]
#[test]
fn recovers_from_miswritten_startxref_offset() {
let header = "%PDF-1.5\n";
let obj1 = "1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n";
let obj2 = "2 0 obj<</Type/Pages/Kids[]/Count 0>>endobj\n";
let o1 = header.len();
let o2 = o1 + obj1.len();
let body = format!("{header}{obj1}{obj2}");
let xref_pos = body.len();
let doc = format!(
"{body}xref
0 3
0000000000 65535 f
{o1:010} 00000 n
{o2:010} 00000 n
trailer
<</Root 1 0 R/Size 3>>
startxref
{}
%%EOF",
xref_pos + 4 );
let loaded = Document::load_mem(doc.as_bytes()).unwrap();
assert!(loaded.trailer.get(b"Root").is_ok());
assert_eq!(loaded.xref_start, xref_pos);
let strict = Document::load_mem_with_options(
doc.as_bytes(),
LoadOptions {
strict: true,
..Default::default()
},
);
assert!(strict.is_err());
}
#[cfg(all(test, not(feature = "async")))]
#[test]
fn stream_length_written_as_real() {
let obj4 = "4 0 obj\n<< /Length 42. >>\nstream\nBT /F1 12 Tf 20 100 Td (Hello World) Tj ET\nendstream\nendobj\n";
let header = "%PDF-1.7\n";
let obj1 = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
let obj2 = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n";
let obj3 = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >>\nendobj\n";
let o1 = header.len();
let o2 = o1 + obj1.len();
let o3 = o2 + obj2.len();
let o4 = o3 + obj3.len();
let body = format!("{header}{obj1}{obj2}{obj3}{obj4}");
let xref_pos = body.len();
let doc = format!(
"{body}xref
0 5
0000000000 65535 f
{o1:010} 00000 n
{o2:010} 00000 n
{o3:010} 00000 n
{o4:010} 00000 n
trailer
<< /Size 5 /Root 1 0 R >>
startxref
{xref_pos}
%%EOF
"
);
let loaded = Document::load_mem(doc.as_bytes()).unwrap();
let stream = loaded.get_object((4, 0)).unwrap().as_stream().unwrap();
assert_eq!(stream.content, b"BT /F1 12 Tf 20 100 Td (Hello World) Tj ET");
}
#[cfg(all(test, not(feature = "async")))]
#[test]
fn object_marker_parsing_accepts_only_real_headers() {
assert_eq!(Reader::parse_object_header(b"12 0 obj<<"), Some((12, 0)));
assert_eq!(Reader::parse_object_header(b"007 0 obj\n"), Some((7, 0)));
assert_eq!(Reader::parse_object_header(b"5 2 obj>>"), Some((5, 2)));
assert_eq!(Reader::parse_object_header(b"12\t3\r\nobj["), Some((12, 3)));
assert_eq!(Reader::parse_object_header(b"2 0 objects"), None);
assert_eq!(Reader::parse_object_header(b"99 88 objx"), None);
assert_eq!(Reader::parse_object_header(b"12345678901 0 obj"), None);
assert_eq!(Reader::parse_object_header(b"1 70000 obj"), None);
assert_eq!(Reader::parse_object_header(b"1 0obj"), None);
assert_eq!(Reader::parse_object_header(b"1 obj"), None);
assert_eq!(Reader::parse_object_header(b"x 0 obj"), None);
}