use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
use pdfrum_crypt::{CryptClass, SecurityHandler};
use pdfrum_object::{
Array, ByteSpan, Dict, Name, ObjRef, Object, PdfString, Resolve, Stream, names,
};
use crate::error::Error;
use crate::lexer::Lexer;
use crate::objstm::ObjStm;
use crate::syntax::{Context, Strictness, indirect};
use crate::xref::{Entry, Xref};
#[derive(Debug)]
pub struct ObjectStore {
bytes: ByteSpan,
xref: Arc<Xref>,
limits: Limits,
cells: Mutex<Cells>,
in_progress: Mutex<Vec<u32>>,
containers: Mutex<Containers>,
security: SecurityHandler,
metadata_exempt: Option<u32>,
diags: Mutex<Diagnostics>,
}
type Cells = HashMap<u32, Arc<OnceLock<Arc<Object>>>, pdfrum_common::FxBuildHasher>;
type Containers = HashMap<u32, Option<Arc<ObjStm>>, pdfrum_common::FxBuildHasher>;
impl ObjectStore {
pub(crate) fn new(
bytes: ByteSpan,
xref: Arc<Xref>,
limits: Limits,
security: SecurityHandler,
) -> Self {
Self {
bytes,
xref,
limits,
cells: Mutex::new(HashMap::default()),
in_progress: Mutex::new(Vec::new()),
containers: Mutex::new(HashMap::default()),
security,
metadata_exempt: None,
diags: Mutex::new(Diagnostics::default()),
}
}
pub(crate) fn exempt_from_decryption(&mut self, num: u32) {
self.metadata_exempt = Some(num);
}
pub(crate) fn xref(&self) -> &Xref {
&self.xref
}
pub(crate) fn limits(&self) -> &Limits {
&self.limits
}
pub(crate) fn security(&self) -> &SecurityHandler {
&self.security
}
pub(crate) fn drain_diags(&self) -> Diagnostics {
match self.diags.lock() {
Ok(mut guard) => std::mem::take(&mut *guard),
Err(_) => Diagnostics::default(),
}
}
pub(crate) fn peek_diags(&self) -> Diagnostics {
match self.diags.lock() {
Ok(guard) => guard.clone(),
Err(_) => Diagnostics::default(),
}
}
pub(crate) fn note(&self, severity: Severity, what: DiagKind, at: Option<u64>) {
if let Ok(mut guard) = self.diags.lock() {
guard.record(severity, what, at);
}
}
fn with_diags<T>(&self, f: impl FnOnce(&mut Diagnostics) -> T) -> T {
let mut local = Diagnostics::default();
let out = f(&mut local);
if let Ok(mut guard) = self.diags.lock() {
guard.extend(&local);
}
out
}
pub fn get(&self, num: u32) -> Result<Arc<Object>, Error> {
let reference = ObjRef::new(num, self.xref.generation(num));
if num == 0 || reference.is_invalid() {
return Err(Error::Unresolved(reference));
}
if !self.xref.is_valid_object_number(num) {
return Err(Error::Unresolved(reference));
}
let cell = self.cell(num);
if let Some(object) = cell.get() {
return Ok(Arc::clone(object));
}
let guard = Guard::enter(self, num).ok_or(Error::Cycle(reference))?;
if let Some(object) = cell.get() {
return Ok(Arc::clone(object));
}
let object = self
.parse(num, guard.nesting())
.ok_or(Error::Unresolved(reference))?;
let object = Arc::new(object);
let _ = cell.set(Arc::clone(&object));
Ok(cell.get().map_or(object, Arc::clone))
}
fn cell(&self, num: u32) -> Arc<OnceLock<Arc<Object>>> {
match self.cells.lock() {
Ok(mut cells) => Arc::clone(cells.entry(num).or_default()),
Err(_) => Arc::new(OnceLock::new()),
}
}
fn parse(&self, num: u32, depth: u32) -> Option<Object> {
match self.xref.entry(num)? {
Entry::Offset(pos) if pos > 0 => self.parse_at(num, pos, depth),
Entry::Free | Entry::Offset(_) => None,
Entry::InObjStream { stream, index } => {
self.parse_member(num, stream.num, index, depth)
}
}
}
fn parse_at(&self, num: u32, pos: u64, depth: u32) -> Option<Object> {
let pos = usize::try_from(pos).ok()?;
if pos >= self.bytes.len() {
return None;
}
let parsed = self.with_diags(|diags| {
let mut ctx = Context {
limits: &self.limits,
diags,
file: Some(&self.bytes),
store: Some(self),
};
let mut lx = Lexer::at(&self.bytes, pos);
indirect(&mut lx, &mut ctx, Strictness::Loose, depth).ok()
})?;
if parsed.num != num {
self.note(
Severity::Suspicious,
DiagKind::ObjNumMismatch,
Some(pos as u64),
);
return None;
}
Some(self.decrypted(ObjRef::new(parsed.num, parsed.generation), parsed.object))
}
fn parse_member(&self, num: u32, archive: u32, index: u32, depth: u32) -> Option<Object> {
let container = self.container(archive)?;
self.with_diags(|diags| container.member(num, index, &self.limits, diags, self, depth))
}
fn container(&self, archive: u32) -> Option<Arc<ObjStm>> {
if !self.xref.is_object_stream(archive) {
return None;
}
if let Ok(cache) = self.containers.lock()
&& let Some(hit) = cache.get(&archive)
{
return hit.clone();
}
let built = self.build_container(archive);
if let Ok(mut cache) = self.containers.lock() {
cache.insert(archive, built.clone());
}
built
}
fn build_container(&self, archive: u32) -> Option<Arc<ObjStm>> {
let Object::Stream(stream) = &*self.get(archive).ok()? else {
return None;
};
self.with_diags(|diags| ObjStm::build(stream, &self.limits, diags, self).map(Arc::new))
}
fn decrypted(&self, obj: ObjRef, object: Object) -> Object {
if matches!(self.security, SecurityHandler::Identity)
|| self.metadata_exempt == Some(obj.num)
{
return object;
}
let mut deferred = Vec::new();
let out = decrypt_node(&self.security, obj, object, &mut deferred, false);
resolve_deferred(&self.security, obj, out, &deferred)
}
}
impl Resolve for ObjectStore {
fn fetch(&self, r: ObjRef) -> Result<Arc<Object>, pdfrum_object::Error> {
self.get(r.num).map_err(Into::into)
}
}
struct Guard<'s> {
store: &'s ObjectStore,
num: u32,
nesting: u32,
}
impl<'s> Guard<'s> {
fn enter(store: &'s ObjectStore, num: u32) -> Option<Self> {
let mut running = store.in_progress.lock().ok()?;
if running.contains(&num) {
return None;
}
let nesting = u32::try_from(running.len()).unwrap_or(u32::MAX);
running.push(num);
drop(running);
Some(Self {
store,
num,
nesting,
})
}
fn nesting(&self) -> u32 {
self.nesting
}
}
impl Drop for Guard<'_> {
fn drop(&mut self) {
if let Ok(mut running) = self.store.in_progress.lock() {
running.retain(|&n| n != self.num);
}
}
}
#[derive(Debug, Clone)]
struct Deferred {
parent: Dict,
path: Vec<Step>,
}
#[derive(Debug, Clone)]
enum Step {
Key(Name),
Index(usize),
}
fn decrypt_node(
handler: &SecurityHandler,
obj: ObjRef,
object: Object,
deferred: &mut Vec<Deferred>,
in_sig: bool,
) -> Object {
match object {
Object::Str(s) => {
if in_sig {
Object::Str(s)
} else {
let plain = handler.decrypt(obj, CryptClass::String, &s.bytes);
Object::Str(PdfString::new(plain, syntax_of(&s)))
}
}
Object::Array(a) => Object::Array(
a.iter()
.map(|v| decrypt_node(handler, obj, v.clone(), deferred, in_sig))
.collect(),
),
Object::Dict(d) => Object::Dict(decrypt_dict(handler, obj, &d, deferred, in_sig, &[])),
Object::Stream(s) => {
let dict = decrypt_dict(handler, obj, &s.dict, deferred, in_sig, &[]);
let class = if s.dict.name(names::TYPE) == Some(names::EMBEDDED_FILE) {
CryptClass::Embedded
} else {
CryptClass::Stream
};
let plain = handler.decrypt(obj, class, &s.data);
Object::Stream(Box::new(Stream::new(
dict,
pdfrum_object::ByteSpan::from(plain),
)))
}
other => other,
}
}
fn decrypt_dict(
handler: &SecurityHandler,
obj: ObjRef,
dict: &Dict,
deferred: &mut Vec<Deferred>,
in_sig: bool,
path: &[Step],
) -> Dict {
let suspicious = dict.contains_key(names::TYPE) || dict.contains_key(names::FT);
let mut out = Dict::new();
let mut skipped: Vec<(Name, Object)> = Vec::new();
for (key, value) in dict.iter() {
if suspicious && key == names::CONTENTS && !in_sig {
skipped.push((key.clone(), value.clone()));
out.push(key.clone(), value.clone());
continue;
}
let mut child = path.to_vec();
child.push(Step::Key(key.clone()));
out.push(
key.clone(),
decrypt_value(handler, obj, value.clone(), deferred, in_sig, &child),
);
}
for (key, _) in skipped {
let mut child = path.to_vec();
child.push(Step::Key(key));
deferred.push(Deferred {
parent: out.clone(),
path: child,
});
}
out
}
fn decrypt_value(
handler: &SecurityHandler,
obj: ObjRef,
value: Object,
deferred: &mut Vec<Deferred>,
in_sig: bool,
path: &[Step],
) -> Object {
match value {
Object::Dict(d) => Object::Dict(decrypt_dict(handler, obj, &d, deferred, in_sig, path)),
Object::Array(a) => {
let mut out = Array::new();
for (i, v) in a.iter().enumerate() {
let mut child = path.to_vec();
child.push(Step::Index(i));
out.push(decrypt_value(
handler,
obj,
v.clone(),
deferred,
in_sig,
&child,
));
}
Object::Array(out)
}
other => decrypt_node(handler, obj, other, deferred, in_sig),
}
}
fn resolve_deferred(
handler: &SecurityHandler,
obj: ObjRef,
object: Object,
deferred: &[Deferred],
) -> Object {
let mut out = object;
for entry in deferred {
if pdfrum_crypt::is_signature_dict(&entry.parent) {
continue;
}
out = rewrite_at(handler, obj, out, &entry.path);
}
out
}
fn rewrite_at(handler: &SecurityHandler, obj: ObjRef, object: Object, path: &[Step]) -> Object {
let Some((step, rest)) = path.split_first() else {
let mut ignored = Vec::new();
return decrypt_node(handler, obj, object, &mut ignored, false);
};
match (object, step) {
(Object::Dict(d), Step::Key(key)) => {
Object::Dict(Dict::from_pairs(d.iter().map(|(k, v)| {
if k == key {
(k.clone(), rewrite_at(handler, obj, v.clone(), rest))
} else {
(k.clone(), v.clone())
}
})))
}
(Object::Stream(s), Step::Key(key)) => {
let dict = Dict::from_pairs(s.dict.iter().map(|(k, v)| {
if k == key {
(k.clone(), rewrite_at(handler, obj, v.clone(), rest))
} else {
(k.clone(), v.clone())
}
}));
Object::Stream(Box::new(Stream::new(dict, s.data)))
}
(Object::Array(a), Step::Index(index)) => Object::Array(
a.iter()
.enumerate()
.map(|(i, v)| {
if i == *index {
rewrite_at(handler, obj, v.clone(), rest)
} else {
v.clone()
}
})
.collect(),
),
(other, _) => other,
}
}
fn syntax_of(s: &PdfString) -> pdfrum_object::StringSyntax {
if s.hex {
pdfrum_object::StringSyntax::Hex
} else {
pdfrum_object::StringSyntax::Literal
}
}
#[cfg(test)]
mod tests {
use super::ObjectStore;
use crate::error::Error;
use crate::xref::Xref;
use pdfrum_common::Limits;
use pdfrum_crypt::SecurityHandler;
use pdfrum_object::ByteSpan;
use pdfrum_object::{ObjRef, Resolve, names};
use std::sync::Arc;
fn store(file: &[u8], build: impl FnOnce(&mut Xref)) -> ObjectStore {
let mut xref = Xref::new();
build(&mut xref);
ObjectStore::new(
ByteSpan::from(file.to_vec()),
Arc::new(xref),
Limits::default(),
SecurityHandler::Identity,
)
}
#[test]
fn fetches_and_caches() {
let file = b"%PDF-1.7\n1 0 obj << /Type /Page >> endobj\n";
let s = store(file, |x| {
x.add_normal(1, 0, false, 9, &Limits::default());
});
let first = s.get(1).expect("object");
assert!(first.as_dict().is_some());
let second = s.get(1).expect("object");
assert!(Arc::ptr_eq(&first, &second));
}
#[test]
fn nested_fetches_share_one_nesting_budget() {
let count: u32 = 80;
let mut file = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for i in 1..=count {
offsets.push(file.len());
if i == count {
file.extend_from_slice(format!("{i} 0 obj 4 endobj\n").as_bytes());
} else {
file.extend_from_slice(
format!(
"{i} 0 obj << /Length {} 0 R >> stream\nDATA\nendstream endobj\n",
i + 1
)
.as_bytes(),
);
}
}
let s = store(&file, |x| {
for (i, offset) in offsets.iter().enumerate() {
let num = u32::try_from(i).unwrap_or(0) + 1;
x.add_normal(num, 0, false, *offset as u64, &Limits::default());
}
});
let first = s.get(1).expect("object 1");
assert_eq!(&*first.as_stream().expect("stream").data, b"DATA");
}
#[test]
fn object_zero_never_resolves() {
let s = store(b"", |x| {
x.add_normal(1, 0, false, 0, &Limits::default());
});
assert!(matches!(s.get(0), Err(Error::Unresolved(_))));
}
#[test]
fn numbers_past_the_table_are_unfetchable() {
let file = b"%PDF-1.7\n1 0 obj 5 endobj\n9 0 obj 7 endobj\n";
let s = store(file, |x| {
x.add_normal(1, 0, false, 9, &Limits::default());
});
assert!(matches!(s.get(9), Err(Error::Unresolved(_))));
}
#[test]
fn a_free_entry_resolves_to_nothing() {
let s = store(b"1 0 obj 5 endobj", |x| {
x.add_normal(2, 0, false, 0, &Limits::default());
x.set_free(1, 1);
});
assert!(matches!(s.get(1), Err(Error::Unresolved(_))));
}
#[test]
fn a_header_naming_another_object_fails_the_fetch() {
let file = b"%PDF-1.7\n7 0 obj << >> endobj\n";
let s = store(file, |x| {
x.add_normal(1, 0, false, 9, &Limits::default());
});
assert!(matches!(s.get(1), Err(Error::Unresolved(_))));
assert!(
s.drain_diags()
.contains(&pdfrum_common::DiagKind::ObjNumMismatch)
);
}
#[test]
fn a_self_referential_length_ends_as_a_keyword_scan() {
let file = b"%PDF-1.7\n1 0 obj << /Length 1 0 R >> stream\nDATA\nendstream endobj\n";
let s = store(file, |x| {
x.add_normal(1, 0, false, 9, &Limits::default());
});
let obj = s.get(1).expect("object");
assert_eq!(&*obj.as_stream().expect("stream").data, b"DATA");
}
#[test]
fn a_failed_fetch_is_retried_rather_than_remembered() {
let file = b"%PDF-1.7\nnot an object\n";
let s = store(file, |x| {
x.add_normal(1, 0, false, 9, &Limits::default());
});
assert!(s.get(1).is_err());
assert!(s.get(1).is_err());
}
#[test]
fn resolving_reads_through_the_trait() {
let file = b"%PDF-1.7\n1 0 obj << /Count 4 >> endobj\n";
let s = store(file, |x| {
x.add_normal(1, 0, false, 9, &Limits::default());
});
let fetched = s.fetch(ObjRef::new(1, 0)).expect("object");
assert_eq!(
fetched.as_dict().and_then(|d| d.direct_int(names::COUNT)),
Some(4)
);
}
#[test]
fn the_store_is_send_and_sync() {
fn assert_both<T: Send + Sync>() {}
assert_both::<ObjectStore>();
}
}