1mod ccitt;
2mod crypt;
3pub use crypt::Decryptor;
4pub mod filters;
5mod header;
6mod jbig2;
7mod lexer;
8mod object_parser;
9mod recovery;
10mod xref;
11
12pub use header::PdfHeader;
13pub use lexer::Lexer;
14pub use object_parser::ObjectParser;
15pub use xref::{XrefEntry, XrefTable};
16
17use std::cell::{Cell, OnceCell, RefCell};
18use std::collections::HashMap;
19use std::sync::Arc;
20use zpdf_core::{ObjectId, ParseLimits, PdfDict, PdfName, PdfObject, PdfStream, Result};
21
22struct DecodedObjStm {
25 data: Arc<[u8]>,
27 first: usize,
29 entries: Vec<(u32, usize)>,
32}
33
34pub struct PdfFile {
35 data: Arc<[u8]>,
36 pub header: PdfHeader,
37 pub xref: XrefTable,
38 pub trailer: zpdf_core::PdfDict,
39 limits: ParseLimits,
40 decryptor: Option<crypt::Decryptor>,
44 object_cache: RefCell<HashMap<ObjectId, PdfObject>>,
48 object_cache_bytes: Cell<u64>,
51 objstm_cache: RefCell<HashMap<u32, Arc<DecodedObjStm>>>,
54 objstm_cache_bytes: Cell<u64>,
56 repair_table: OnceCell<Option<XrefTable>>,
61}
62
63impl PdfFile {
64 pub fn parse(data: impl Into<Arc<[u8]>>) -> Result<Self> {
65 Self::parse_with_limits(data, ParseLimits::default())
66 }
67
68 pub fn parse_with_limits(data: impl Into<Arc<[u8]>>, limits: ParseLimits) -> Result<Self> {
69 Self::parse_with_password_and_limits(data, b"", limits)
70 }
71
72 pub fn parse_with_password(data: impl Into<Arc<[u8]>>, password: &[u8]) -> Result<Self> {
76 Self::parse_with_password_and_limits(data, password, ParseLimits::default())
77 }
78
79 pub fn parse_with_password_and_limits(
80 data: impl Into<Arc<[u8]>>,
81 password: &[u8],
82 limits: ParseLimits,
83 ) -> Result<Self> {
84 let data: Arc<[u8]> = data.into();
85 let header_res = header::parse_header(&data);
90
91 let normal = xref::parse_xref_and_trailer(&data, &limits);
94 let (xref, trailer) = match normal {
95 Ok((xref, trailer)) if root_resolves(&data, &xref, &trailer, &limits) => {
96 (xref, trailer)
97 }
98 other => {
99 match &other {
100 Err(e) => {
101 tracing::warn!("xref parse failed ({e}); attempting tail-scan recovery")
102 }
103 Ok(_) => {
104 tracing::warn!("xref /Root did not resolve; attempting tail-scan recovery")
105 }
106 }
107 match recovery::scan_all_objects(&data, &limits) {
108 Ok(recovered) => recovered,
109 Err(rec_err) => match other {
114 Ok(parsed) => parsed,
115 Err(_) if header_res.is_err() => return Err(zpdf_core::Error::NotAPdf),
116 Err(_) => return Err(rec_err),
117 },
118 }
119 }
120 };
121 let header = header_res.unwrap_or(PdfHeader { major: 1, minor: 7 });
125
126 let mut file = Self {
127 data,
128 header,
129 xref,
130 trailer,
131 limits,
132 decryptor: None,
133 object_cache: RefCell::new(HashMap::new()),
134 object_cache_bytes: Cell::new(0),
135 objstm_cache: RefCell::new(HashMap::new()),
136 objstm_cache_bytes: Cell::new(0),
137 repair_table: OnceCell::new(),
138 };
139 file.decryptor = file.build_decryptor(password)?;
143 Ok(file)
144 }
145
146 pub fn is_encrypted(&self) -> bool {
149 self.trailer.get("Encrypt").is_some()
150 }
151
152 pub fn decryptor(&self) -> Option<&crypt::Decryptor> {
155 self.decryptor.as_ref()
156 }
157
158 fn build_decryptor(&self, password: &[u8]) -> Result<Option<crypt::Decryptor>> {
163 let Some(enc) = self.trailer.get("Encrypt") else {
167 return Ok(None);
168 };
169 let (enc_obj, encrypt_ref) = match enc {
170 PdfObject::Ref(r) => match self.resolve(*r) {
171 Ok(o) => (o, Some(*r)),
172 Err(_) => return Ok(None),
173 },
174 direct => (direct.clone(), None),
175 };
176 let Ok(enc_dict) = enc_obj.as_dict() else {
177 return Ok(None);
178 };
179 let id_first = self.first_id_bytes();
180 match crypt::Decryptor::from_encrypt_dict(enc_dict, &id_first, encrypt_ref, password) {
181 crypt::BuildResult::Decryptor(d) => Ok(Some(d)),
182 crypt::BuildResult::Degrade => Ok(None),
183 crypt::BuildResult::WrongPassword => Err(zpdf_core::Error::WrongPassword),
184 }
185 }
186
187 fn first_id_bytes(&self) -> Vec<u8> {
192 let arr = match self.trailer.get("ID") {
193 Some(PdfObject::Array(a)) => Some(std::borrow::Cow::Borrowed(a.as_slice())),
194 Some(PdfObject::Ref(r)) => self.resolve(*r).ok().and_then(|o| {
195 o.as_array()
196 .ok()
197 .map(|a| std::borrow::Cow::Owned(a.to_vec()))
198 }),
199 _ => None,
200 };
201 match arr.as_deref().and_then(|a| a.first()) {
202 Some(PdfObject::String(s)) => s.0.clone(),
203 _ => Vec::new(),
204 }
205 }
206
207 pub fn resolve(&self, id: zpdf_core::ObjectId) -> Result<PdfObject> {
208 self.resolve_depth(id, 0)
209 }
210
211 fn resolve_depth(&self, id: ObjectId, depth: u32) -> Result<PdfObject> {
212 const MAX_REF_CHAIN: u32 = 32;
216 if depth > MAX_REF_CHAIN {
217 tracing::warn!(
218 "indirect reference chain longer than {MAX_REF_CHAIN} at {id}; treating as null"
219 );
220 return Ok(PdfObject::Null);
221 }
222
223 if let Some(obj) = self.object_cache.borrow().get(&id) {
225 return Ok(obj.clone());
226 }
227
228 let obj = match self.xref.get(id) {
237 Some(XrefEntry::InUse { offset, .. }) => self.parse_at_offset_checked(*offset, id)?,
238 Some(XrefEntry::Compressed {
239 stream_obj,
240 index_in_stream,
241 }) => self.extract_from_object_stream(*stream_obj, *index_in_stream)?,
242 Some(XrefEntry::Free { .. }) => match self.repaired_object(id) {
243 Some(obj) => obj,
244 None => {
245 tracing::warn!("reference to free object {id}; treating as null");
246 PdfObject::Null
247 }
248 },
249 None => match self.repaired_object(id) {
250 Some(obj) => obj,
251 None => {
252 tracing::warn!("reference to missing object {id}; treating as null");
253 PdfObject::Null
254 }
255 },
256 };
257
258 let obj = match obj {
261 PdfObject::Ref(next) => self.resolve_depth(next, depth + 1)?,
262 other => other,
263 };
264
265 self.cache_object(id, &obj);
266 Ok(obj)
267 }
268
269 fn cache_object(&self, id: ObjectId, obj: &PdfObject) {
273 if self.object_cache.borrow().contains_key(&id) {
274 return;
275 }
276 let cost = estimate_cached_object_bytes(obj);
277 let used = self.object_cache_bytes.get();
278 if cost > self.limits.max_object_cache_bytes.saturating_sub(used) {
279 return;
280 }
281 self.object_cache.borrow_mut().insert(id, obj.clone());
282 self.object_cache_bytes.set(used.saturating_add(cost));
283 }
284
285 fn parse_at_offset_checked(&self, offset: u64, id: ObjectId) -> Result<PdfObject> {
290 let parser = ObjectParser::new(&self.data, &self.limits);
291 let file_offset = usize::try_from(offset).map_err(|_| {
292 zpdf_core::Error::InvalidObject(offset, "xref offset exceeds address space".into())
293 })?;
294 match parser.parse_indirect_with_id(file_offset) {
295 Ok((pid, mut obj)) if pid == id => {
296 if let Some(dec) = &self.decryptor {
302 dec.decrypt_object(&mut obj, id);
303 }
304 Ok(obj)
305 }
306 Ok((pid, _)) => {
307 tracing::warn!("xref offset {offset} for {id} holds object {pid}; trying repair");
308 self.repaired_object(id).ok_or_else(|| {
309 zpdf_core::Error::InvalidObject(
310 offset,
311 format!("xref entry for {id} points at object {pid}"),
312 )
313 })
314 }
315 Err(e) => {
316 tracing::warn!("failed to parse {id} at xref offset {offset} ({e}); trying repair");
317 match self.repaired_object(id) {
318 Some(obj) => Ok(obj),
319 None => Err(e),
320 }
321 }
322 }
323 }
324
325 fn repaired_object(&self, id: ObjectId) -> Option<PdfObject> {
330 let table = self
331 .repair_table
332 .get_or_init(
333 || match recovery::scan_all_objects(&self.data, &self.limits) {
334 Ok((table, _trailer)) => Some(table),
335 Err(e) => {
336 tracing::warn!("repair object scan failed: {e}");
337 None
338 }
339 },
340 )
341 .as_ref()?;
342 match table.get(id)? {
343 XrefEntry::InUse { offset, .. } => {
344 let parser = ObjectParser::new(&self.data, &self.limits);
345 let file_offset = usize::try_from(*offset).ok()?;
346 let (pid, mut obj) = parser.parse_indirect_with_id(file_offset).ok()?;
347 if pid != id {
348 return None;
349 }
350 if let Some(dec) = &self.decryptor {
351 dec.decrypt_object(&mut obj, id);
352 }
353 Some(obj)
354 }
355 XrefEntry::Compressed {
356 stream_obj,
357 index_in_stream,
358 } => self
359 .extract_from_object_stream(*stream_obj, *index_in_stream)
360 .ok(),
361 XrefEntry::Free { .. } => None,
362 }
363 }
364
365 pub fn resolve_stream_data(&self, id: zpdf_core::ObjectId) -> Result<Vec<u8>> {
370 self.resolve_stream_data_inner(id, true)
371 }
372
373 fn resolve_stream_data_inner(
374 &self,
375 id: zpdf_core::ObjectId,
376 inline_globals: bool,
377 ) -> Result<Vec<u8>> {
378 let obj = self.resolve(id)?;
379 let stream = obj.as_stream()?;
380 match self.dict_with_resolved_filters(&stream.dict, inline_globals) {
381 Some(resolved) => {
382 filters::decode_stream_with_limits(&stream.data, &resolved, &self.limits)
383 }
384 None => filters::decode_stream_with_limits(&stream.data, &stream.dict, &self.limits),
385 }
386 }
387
388 fn dict_with_resolved_filters(&self, dict: &PdfDict, inline_globals: bool) -> Option<PdfDict> {
395 const KEYS: [&str; 3] = ["Filter", "DecodeParms", "DP"];
396 let dict_needs_globals = |obj: &PdfObject| {
399 inline_globals
400 && matches!(obj, PdfObject::Dict(d)
401 if matches!(d.get("JBIG2Globals"), Some(PdfObject::Ref(_))))
402 };
403 let needs_resolve = |obj: &PdfObject| match obj {
404 PdfObject::Ref(_) => true,
405 PdfObject::Array(a) => a
406 .iter()
407 .any(|e| matches!(e, PdfObject::Ref(_)) || dict_needs_globals(e)),
408 other => dict_needs_globals(other),
409 };
410 if !KEYS.iter().any(|k| dict.get(k).is_some_and(needs_resolve)) {
411 return None;
412 }
413
414 let resolve_shallow = |obj: &PdfObject| match obj {
415 PdfObject::Ref(r) => self.resolve(*r).unwrap_or(PdfObject::Null),
416 other => other.clone(),
417 };
418 let inline = |obj: PdfObject| {
419 if inline_globals {
420 self.inline_jbig2_globals(obj)
421 } else {
422 obj
423 }
424 };
425 let mut out = dict.clone();
426 for key in KEYS {
427 let Some(value) = dict.get(key) else { continue };
428 let resolved = match resolve_shallow(value) {
429 PdfObject::Array(a) => {
431 PdfObject::Array(a.iter().map(resolve_shallow).map(inline).collect())
432 }
433 other => inline(other),
434 };
435 out.insert(PdfName::new(key), resolved);
436 }
437 Some(out)
438 }
439
440 fn inline_jbig2_globals(&self, obj: PdfObject) -> PdfObject {
447 let PdfObject::Dict(mut d) = obj else {
448 return obj;
449 };
450 if let Some(PdfObject::Ref(r)) = d.get("JBIG2Globals") {
451 let r = *r;
452 let value = match self.resolve_stream_data_inner(r, false) {
453 Ok(bytes) => PdfObject::String(zpdf_core::PdfString(bytes)),
454 Err(e) => {
455 tracing::warn!("failed to decode /JBIG2Globals stream {r}: {e}");
456 PdfObject::Null
457 }
458 };
459 d.insert(PdfName::new("JBIG2Globals"), value);
460 }
461 PdfObject::Dict(d)
462 }
463
464 fn extract_from_object_stream(
466 &self,
467 stream_obj_num: u32,
468 index_in_stream: u32,
469 ) -> Result<PdfObject> {
470 let objstm = self.get_or_decode_objstm(stream_obj_num)?;
471
472 let idx = index_in_stream as usize;
473 if idx >= objstm.entries.len() {
474 return Err(zpdf_core::Error::InvalidObject(
475 0,
476 format!(
477 "object stream index {idx} out of range (n={})",
478 objstm.entries.len()
479 ),
480 ));
481 }
482
483 let (_, obj_offset) = objstm.entries[idx];
484 let oob = || {
485 zpdf_core::Error::InvalidObject(0, "object stream member offset out of range".into())
486 };
487 let data_start = objstm.first.checked_add(obj_offset).ok_or_else(oob)?;
488 let data_end = if idx + 1 < objstm.entries.len() {
489 objstm
490 .first
491 .checked_add(objstm.entries[idx + 1].1)
492 .ok_or_else(oob)?
493 } else {
494 objstm.data.len()
495 };
496
497 let data_end = data_end.min(objstm.data.len());
501 if data_start > data_end {
502 return Err(zpdf_core::Error::InvalidObject(
503 0,
504 "object stream member offsets out of order".into(),
505 ));
506 }
507
508 let obj_data = &objstm.data[data_start..data_end];
509 let mut lexer = Lexer::new(obj_data, 0, &self.limits);
510 lexer.next_token()
511 }
512
513 fn get_or_decode_objstm(&self, stream_obj_num: u32) -> Result<Arc<DecodedObjStm>> {
518 if let Some(hit) = self.objstm_cache.borrow().get(&stream_obj_num) {
519 return Ok(Arc::clone(hit));
520 }
521
522 let stream_id = zpdf_core::ObjectId(stream_obj_num, 0);
523 let stream_entry = self
524 .xref
525 .get(stream_id)
526 .ok_or(zpdf_core::Error::ObjectNotFound(stream_id))?;
527 let stream_obj = match stream_entry {
528 XrefEntry::InUse { offset, .. } => {
529 let parser = ObjectParser::new(&self.data, &self.limits);
530 let file_offset = usize::try_from(*offset).map_err(|_| {
531 zpdf_core::Error::InvalidObject(
532 *offset,
533 "object-stream offset exceeds address space".into(),
534 )
535 })?;
536 parser.parse_indirect_at(file_offset)?
537 }
538 _ => return Err(zpdf_core::Error::ObjectNotFound(stream_id)),
539 };
540
541 let stream: &PdfStream = stream_obj.as_stream()?;
542 let neg =
546 |what: &str| zpdf_core::Error::InvalidObject(0, format!("ObjStm {what} is negative"));
547 let n = usize::try_from(stream.dict.get_i64("N")?).map_err(|_| neg("/N"))?;
548 let first = usize::try_from(stream.dict.get_i64("First")?).map_err(|_| neg("/First"))?;
549 if n > self.limits.max_objects as usize {
550 return Err(zpdf_core::Error::StreamDecode(format!(
551 "ObjStm /N {n} exceeds object limit {}",
552 self.limits.max_objects
553 )));
554 }
555
556 let raw: std::borrow::Cow<[u8]> = match &self.decryptor {
560 Some(dec) => std::borrow::Cow::Owned(
561 dec.decrypt_stream_bytes(zpdf_core::ObjectId(stream_obj_num, 0), &stream.data),
562 ),
563 None => std::borrow::Cow::Borrowed(&stream.data),
564 };
565 let decoded = filters::decode_stream_with_limits(&raw, &stream.dict, &self.limits)?;
566
567 let header = &decoded[..first.min(decoded.len())];
570 let mut header_lexer = Lexer::new(header, 0, &self.limits);
571 let mut entries = Vec::with_capacity(n.min(header.len()));
572 for _ in 0..n {
573 let obj_num_tok = header_lexer.next_token()?;
574 let offset_tok = header_lexer.next_token()?;
575 let obj_num = u32::try_from(obj_num_tok.as_i64()?).map_err(|_| {
576 zpdf_core::Error::StreamDecode("ObjStm: object number out of range".into())
577 })?;
578 let offset = usize::try_from(offset_tok.as_i64()?).map_err(|_| {
579 zpdf_core::Error::StreamDecode("ObjStm: member offset out of range".into())
580 })?;
581 entries.push((obj_num, offset));
582 }
583
584 let decoded_arc = Arc::new(DecodedObjStm {
585 data: Arc::<[u8]>::from(decoded),
586 first,
587 entries,
588 });
589 let cost = estimate_objstm_bytes(&decoded_arc);
590 let used = self.objstm_cache_bytes.get();
591 if cost <= self.limits.max_objstm_cache_bytes.saturating_sub(used) {
592 self.objstm_cache
593 .borrow_mut()
594 .insert(stream_obj_num, Arc::clone(&decoded_arc));
595 self.objstm_cache_bytes.set(used.saturating_add(cost));
596 }
597 Ok(decoded_arc)
598 }
599
600 pub fn data(&self) -> &[u8] {
601 &self.data
602 }
603
604 pub fn limits(&self) -> &ParseLimits {
606 &self.limits
607 }
608
609 pub fn force_repair_scan(&self) -> Option<&XrefTable> {
613 self.repair_table
614 .get_or_init(
615 || match recovery::scan_all_objects(&self.data, &self.limits) {
616 Ok((table, _trailer)) => Some(table),
617 Err(e) => {
618 tracing::warn!("repair object scan failed: {e}");
619 None
620 }
621 },
622 )
623 .as_ref()
624 }
625
626 pub fn all_object_ids(&self) -> Vec<ObjectId> {
629 let mut ids: Vec<ObjectId> = self.xref.object_ids().collect();
630 if let Some(table) = self.force_repair_scan() {
631 ids.extend(table.object_ids());
632 }
633 ids.sort_by_key(|id| (id.0, id.1));
634 ids.dedup();
635 ids
636 }
637
638 pub fn find_objects_by_type(&self, ty: &str) -> Vec<ObjectId> {
645 let mut out = Vec::new();
646 for id in self.all_object_ids() {
647 if out.len() as u32 >= self.limits.max_objects {
648 break;
649 }
650 let obj = match self.resolve(id) {
651 Ok(PdfObject::Null) | Err(_) => self.repaired_object(id),
652 Ok(o) => Some(o),
653 };
654 let is_match = obj
655 .as_ref()
656 .and_then(|o| o.as_dict().ok())
657 .map(|d| d.get_name("Type").map(|t| t == ty).unwrap_or(false))
658 .unwrap_or(false);
659 if is_match {
660 out.push(id);
661 }
662 }
663 out
664 }
665}
666
667fn estimate_cached_object_bytes(obj: &PdfObject) -> u64 {
672 const ENTRY_OVERHEAD: u64 = 64;
673
674 fn dict_bytes(dict: &PdfDict) -> u64 {
675 dict.0.iter().fold(0u64, |sum, (key, value)| {
676 sum.saturating_add(key.0.len() as u64)
677 .saturating_add(48) .saturating_add(object_bytes(value))
679 })
680 }
681
682 fn object_bytes(obj: &PdfObject) -> u64 {
683 let base = std::mem::size_of::<PdfObject>() as u64;
684 let payload = match obj {
685 PdfObject::String(s) => s.0.len() as u64,
686 PdfObject::Name(n) => n.0.len() as u64,
687 PdfObject::Array(items) => items.iter().fold(
688 (items.len() * std::mem::size_of::<PdfObject>()) as u64,
689 |sum, item| sum.saturating_add(object_bytes(item)),
690 ),
691 PdfObject::Dict(dict) => dict_bytes(dict),
692 PdfObject::Stream(stream) => {
693 (stream.data.len() as u64).saturating_add(dict_bytes(&stream.dict))
694 }
695 PdfObject::Null
696 | PdfObject::Bool(_)
697 | PdfObject::Integer(_)
698 | PdfObject::Real(_)
699 | PdfObject::Ref(_) => 0,
700 };
701 base.saturating_add(payload)
702 }
703
704 ENTRY_OVERHEAD.saturating_add(object_bytes(obj))
705}
706
707fn estimate_objstm_bytes(stream: &DecodedObjStm) -> u64 {
708 const ENTRY_OVERHEAD: u64 = 64;
709 ENTRY_OVERHEAD
710 .saturating_add(std::mem::size_of::<DecodedObjStm>() as u64)
711 .saturating_add(stream.data.len() as u64)
712 .saturating_add((stream.entries.len() * std::mem::size_of::<(u32, usize)>()) as u64)
713}
714
715fn root_resolves(
723 data: &[u8],
724 xref: &XrefTable,
725 trailer: &zpdf_core::PdfDict,
726 limits: &ParseLimits,
727) -> bool {
728 let Ok(root_ref) = trailer.get_ref("Root") else {
729 return false;
730 };
731 match xref.get(root_ref) {
732 Some(XrefEntry::InUse { offset, .. }) => {
733 let parser = ObjectParser::new(data, limits);
734 let Some(file_offset) = usize::try_from(*offset).ok() else {
735 return false;
736 };
737 matches!(
738 parser
739 .parse_indirect_at(file_offset)
740 .ok()
741 .and_then(|o| o
742 .as_dict()
743 .ok()
744 .map(|d| d.get_name("Type").unwrap_or("").to_string())),
745 Some(t) if t == "Catalog"
746 )
747 }
748 Some(_) => true, None => false,
750 }
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 #[test]
761 fn objstm_header_and_slicing_math() {
762 let limits = ParseLimits::default();
763 let o10 = b"<< /Type /Catalog /Pages 2 0 R >>";
764 let o11 = b"42";
765 let header = format!("10 0 11 {} ", o10.len() + 1);
766 let first = header.len();
767 let mut decoded = header.into_bytes();
768 decoded.extend_from_slice(o10);
769 decoded.push(b' ');
770 decoded.extend_from_slice(o11);
771
772 let mut hx = Lexer::new(&decoded[..first], 0, &limits);
774 let mut entries = Vec::new();
775 for _ in 0..2 {
776 let num = hx.next_token().unwrap().as_i64().unwrap() as u32;
777 let off = hx.next_token().unwrap().as_i64().unwrap() as usize;
778 entries.push((num, off));
779 }
780 assert_eq!(entries, vec![(10, 0), (11, o10.len() + 1)]);
781
782 let (start0, end0) = (first + entries[0].1, first + entries[1].1);
784 let obj = Lexer::new(&decoded[start0..end0], 0, &limits)
785 .next_token()
786 .unwrap();
787 assert!(obj.as_dict().is_ok(), "obj 10 should lex as a dict");
788
789 let start1 = first + entries[1].1;
791 let n = Lexer::new(&decoded[start1..], 0, &limits)
792 .next_token()
793 .unwrap();
794 assert_eq!(n.as_i64().unwrap(), 42);
795 }
796
797 fn build_pdf(objects: &[(u32, &str)], root: u32) -> Vec<u8> {
801 let mut d = Vec::from(&b"%PDF-1.4\n"[..]);
802 let mut offsets = Vec::new();
803 for (num, body) in objects {
804 offsets.push((*num, d.len()));
805 d.extend_from_slice(format!("{num} 0 obj\n{body}\nendobj\n").as_bytes());
806 }
807 let xref_off = d.len();
808 d.extend_from_slice(b"xref\n0 1\n0000000000 65535 f \n");
809 for (num, off) in &offsets {
810 d.extend_from_slice(format!("{num} 1\n{off:010} 00000 n \n").as_bytes());
811 }
812 let size = objects.iter().map(|(n, _)| n + 1).max().unwrap_or(1);
813 d.extend_from_slice(
814 format!("trailer\n<< /Size {size} /Root {root} 0 R >>\nstartxref\n{xref_off}\n%%EOF\n")
815 .as_bytes(),
816 );
817 d
818 }
819
820 #[test]
821 fn dangling_ref_resolves_to_null() {
822 let pdf = build_pdf(&[(1, "<< /Type /Catalog /Pages 9 0 R >>")], 1);
825 let file = PdfFile::parse(pdf).unwrap();
826 assert_eq!(file.resolve(ObjectId(9, 0)).unwrap(), PdfObject::Null);
827 assert_eq!(file.resolve(ObjectId(9, 0)).unwrap(), PdfObject::Null);
829 }
830
831 #[test]
832 fn free_entry_resolves_to_null() {
833 let mut d = Vec::from(&b"%PDF-1.4\n"[..]);
834 let off1 = d.len();
835 d.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
836 let xref_off = d.len();
837 d.extend_from_slice(b"xref\n0 1\n0000000000 65535 f \n1 1\n");
838 d.extend_from_slice(format!("{off1:010} 00000 n \n").as_bytes());
839 d.extend_from_slice(b"2 1\n0000000000 00000 f \n");
840 d.extend_from_slice(
841 format!("trailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF\n")
842 .as_bytes(),
843 );
844
845 let file = PdfFile::parse(d).unwrap();
846 assert!(matches!(
847 file.xref.get(ObjectId(2, 0)),
848 Some(XrefEntry::Free { .. })
849 ));
850 assert_eq!(file.resolve(ObjectId(2, 0)).unwrap(), PdfObject::Null);
851 }
852
853 #[test]
854 fn header_mismatch_triggers_lazy_repair() {
855 let mut d = Vec::from(&b"%PDF-1.4\n"[..]);
858 let off1 = d.len();
859 d.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
860 let off2 = d.len();
861 d.extend_from_slice(b"2 0 obj\n<< /Marker /Wrong >>\nendobj\n");
862 d.extend_from_slice(b"3 0 obj\n<< /Marker /Real >>\nendobj\n");
864 let xref_off = d.len();
865 d.extend_from_slice(b"xref\n0 1\n0000000000 65535 f \n");
866 d.extend_from_slice(format!("1 1\n{off1:010} 00000 n \n").as_bytes());
867 d.extend_from_slice(format!("2 1\n{off2:010} 00000 n \n").as_bytes());
868 d.extend_from_slice(format!("3 1\n{off2:010} 00000 n \n").as_bytes()); d.extend_from_slice(
870 format!("trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF\n")
871 .as_bytes(),
872 );
873
874 let file = PdfFile::parse(d).unwrap();
875 let obj = file.resolve(ObjectId(3, 0)).unwrap();
876 assert_eq!(obj.as_dict().unwrap().get_name("Marker").unwrap(), "Real");
877 let obj2 = file.resolve(ObjectId(2, 0)).unwrap();
879 assert_eq!(obj2.as_dict().unwrap().get_name("Marker").unwrap(), "Wrong");
880 }
881
882 #[test]
883 fn ref_to_ref_chain_resolves() {
884 let pdf = build_pdf(
885 &[
886 (1, "<< /Type /Catalog /Pages 2 0 R >>"),
887 (4, "5 0 R"),
888 (5, "42"),
889 ],
890 1,
891 );
892 let file = PdfFile::parse(pdf).unwrap();
893 assert_eq!(
894 file.resolve(ObjectId(4, 0)).unwrap(),
895 PdfObject::Integer(42)
896 );
897 }
898
899 #[test]
900 fn ref_cycle_resolves_to_null() {
901 let pdf = build_pdf(
904 &[
905 (1, "<< /Type /Catalog /Pages 2 0 R >>"),
906 (4, "5 0 R"),
907 (5, "4 0 R"),
908 ],
909 1,
910 );
911 let file = PdfFile::parse(pdf).unwrap();
912 assert_eq!(file.resolve(ObjectId(4, 0)).unwrap(), PdfObject::Null);
913 }
914
915 #[test]
916 fn indirect_filter_is_resolved() {
917 use flate2::write::ZlibEncoder;
918 use flate2::Compression;
919 use std::io::Write;
920
921 let payload = b"indirect filter payload";
922 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
923 enc.write_all(payload).unwrap();
924 let compressed = enc.finish().unwrap();
925
926 let mut d = Vec::from(&b"%PDF-1.4\n"[..]);
927 let off1 = d.len();
928 d.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
929 let off3 = d.len();
930 d.extend_from_slice(
931 format!(
932 "3 0 obj\n<< /Length {} /Filter 4 0 R >>\nstream\n",
933 compressed.len()
934 )
935 .as_bytes(),
936 );
937 d.extend_from_slice(&compressed);
938 d.extend_from_slice(b"\nendstream\nendobj\n");
939 let off4 = d.len();
940 d.extend_from_slice(b"4 0 obj\n/FlateDecode\nendobj\n");
941 let xref_off = d.len();
942 d.extend_from_slice(b"xref\n0 1\n0000000000 65535 f \n");
943 d.extend_from_slice(format!("1 1\n{off1:010} 00000 n \n").as_bytes());
944 d.extend_from_slice(format!("3 1\n{off3:010} 00000 n \n").as_bytes());
945 d.extend_from_slice(format!("4 1\n{off4:010} 00000 n \n").as_bytes());
946 d.extend_from_slice(
947 format!("trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF\n")
948 .as_bytes(),
949 );
950
951 let file = PdfFile::parse(d).unwrap();
952 let data = file.resolve_stream_data(ObjectId(3, 0)).unwrap();
953 assert_eq!(data, payload);
954 }
955
956 #[test]
957 fn resolve_stream_uses_file_decode_limits() {
958 let pdf = build_pdf(
959 &[
960 (1, "<< /Type /Catalog >>"),
961 (3, "<< /Length 4 >>\nstream\nfour\nendstream"),
962 ],
963 1,
964 );
965 let limits = ParseLimits {
966 max_decoded_stream_bytes: 3,
967 ..ParseLimits::default()
968 };
969 let file = PdfFile::parse_with_limits(pdf, limits).unwrap();
970 assert!(matches!(
971 file.resolve_stream_data(ObjectId(3, 0)),
972 Err(zpdf_core::Error::StreamSizeLimit(3))
973 ));
974 }
975
976 #[test]
977 fn zero_object_cache_budget_retains_nothing() {
978 let pdf = build_pdf(&[(1, "<< /Type /Catalog /Marker (large) >>")], 1);
979 let limits = ParseLimits {
980 max_object_cache_bytes: 0,
981 ..ParseLimits::default()
982 };
983 let file = PdfFile::parse_with_limits(pdf, limits).unwrap();
984 assert_eq!(
985 file.resolve(ObjectId(1, 0))
986 .unwrap()
987 .as_dict()
988 .unwrap()
989 .get_name("Type")
990 .unwrap(),
991 "Catalog"
992 );
993 assert!(file.object_cache.borrow().is_empty());
994 assert_eq!(file.object_cache_bytes.get(), 0);
995 }
996
997 #[test]
998 fn zero_objstm_cache_budget_decodes_without_retention() {
999 let pdf = build_pdf(
1000 &[
1001 (1, "<< /Type /Catalog >>"),
1002 (
1003 5,
1004 "<< /Type /ObjStm /N 1 /First 4 /Length 6 >>\nstream\n6 0 42\nendstream",
1005 ),
1006 ],
1007 1,
1008 );
1009 let limits = ParseLimits {
1010 max_objstm_cache_bytes: 0,
1011 ..ParseLimits::default()
1012 };
1013 let mut file = PdfFile::parse_with_limits(pdf, limits).unwrap();
1014 file.xref.insert_overwrite(
1015 ObjectId(6, 0),
1016 XrefEntry::Compressed {
1017 stream_obj: 5,
1018 index_in_stream: 0,
1019 },
1020 );
1021 assert_eq!(
1022 file.resolve(ObjectId(6, 0)).unwrap(),
1023 PdfObject::Integer(42)
1024 );
1025 assert!(file.objstm_cache.borrow().is_empty());
1026 assert_eq!(file.objstm_cache_bytes.get(), 0);
1027 }
1028
1029 #[test]
1035 fn jbig2_globals_stream_is_resolved_and_decoded() {
1036 use flate2::write::ZlibEncoder;
1037 use flate2::Compression;
1038 use std::io::Write;
1039
1040 let globals: Vec<u8> = [
1042 &[0, 0, 0, 0, 0x30, 0x00, 0x01, 0, 0, 0, 19][..], &[0, 0, 0, 8, 0, 0, 0, 2][..], &[0; 8][..], &[0x00, 0, 0][..], ]
1047 .concat();
1048 let mut gz = ZlibEncoder::new(Vec::new(), Compression::default());
1049 gz.write_all(&globals).unwrap();
1050 let globals_z = gz.finish().unwrap();
1051
1052 let image: Vec<u8> = [
1055 &[0, 0, 0, 1, 0x26, 0x00, 0x01, 0, 0, 0, 20][..], &[0, 0, 0, 8, 0, 0, 0, 2][..], &[0, 0, 0, 0, 0, 0, 0, 0, 0x00][..], &[0x01, 0x31, 0xF8][..], ]
1060 .concat();
1061
1062 let mut d = Vec::from(&b"%PDF-1.4\n"[..]);
1063 let off1 = d.len();
1064 d.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1065 let off3 = d.len();
1066 d.extend_from_slice(
1067 format!(
1068 "3 0 obj\n<< /Length {} /Filter /JBIG2Decode \
1069 /DecodeParms << /JBIG2Globals 4 0 R >> >>\nstream\n",
1070 image.len()
1071 )
1072 .as_bytes(),
1073 );
1074 d.extend_from_slice(&image);
1075 d.extend_from_slice(b"\nendstream\nendobj\n");
1076 let off4 = d.len();
1077 d.extend_from_slice(
1078 format!(
1079 "4 0 obj\n<< /Length {} /Filter /FlateDecode >>\nstream\n",
1080 globals_z.len()
1081 )
1082 .as_bytes(),
1083 );
1084 d.extend_from_slice(&globals_z);
1085 d.extend_from_slice(b"\nendstream\nendobj\n");
1086 let xref_off = d.len();
1087 d.extend_from_slice(b"xref\n0 1\n0000000000 65535 f \n");
1088 d.extend_from_slice(format!("1 1\n{off1:010} 00000 n \n").as_bytes());
1089 d.extend_from_slice(format!("3 1\n{off3:010} 00000 n \n").as_bytes());
1090 d.extend_from_slice(format!("4 1\n{off4:010} 00000 n \n").as_bytes());
1091 d.extend_from_slice(
1092 format!("trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF\n")
1093 .as_bytes(),
1094 );
1095
1096 let file = PdfFile::parse(d).unwrap();
1097 let data = file.resolve_stream_data(ObjectId(3, 0)).unwrap();
1098 assert_eq!(data, vec![0xE7, 0xE7]);
1100 }
1101}