1use super::*;
8
9impl PdfDocument {
10 pub fn version(&self) -> (u8, u8) {
25 self.version
26 }
27
28 pub fn trailer(&self) -> &Object {
51 &self.trailer
52 }
53
54 pub fn all_object_ids(&self) -> Vec<u32> {
66 let mut ids: Vec<u32> = self.xref.all_object_numbers().collect();
67 for r in self.object_cache.lock_or_recover().keys() {
68 ids.push(r.id);
69 }
70 ids.sort_unstable();
71 ids.dedup();
72 ids
73 }
74
75 pub(crate) fn all_page_refs(&self) -> Result<Vec<ObjectRef>> {
87 let catalog = self.catalog()?;
88 let catalog_dict = catalog.as_dict().ok_or_else(|| Error::InvalidObjectType {
89 expected: "Dictionary".to_string(),
90 found: catalog.type_name().to_string(),
91 })?;
92 let pages_ref = catalog_dict
93 .get("Pages")
94 .and_then(|p| p.as_reference())
95 .ok_or_else(|| Error::InvalidPdf("Catalog missing /Pages entry".to_string()))?;
96
97 let mut out: Vec<ObjectRef> = Vec::new();
98 let mut visited: HashSet<ObjectRef> = HashSet::new();
99 self.collect_page_refs(pages_ref, &mut out, &mut visited, 0)?;
100 Ok(out)
101 }
102
103 fn collect_page_refs(
104 &self,
105 node_ref: ObjectRef,
106 out: &mut Vec<ObjectRef>,
107 visited: &mut HashSet<ObjectRef>,
108 depth: u32,
109 ) -> Result<()> {
110 if depth >= MAX_PAGE_TREE_DEPTH {
111 tracing::warn!(target: LOG_TARGET,
112 object_id = node_ref.id,
113 max_depth = MAX_PAGE_TREE_DEPTH,
114 "page tree depth limit exceeded while collecting page references"
115 );
116 return Err(Error::RecursionLimitExceeded(MAX_PAGE_TREE_DEPTH));
117 }
118 if !visited.insert(node_ref) {
119 return Ok(());
120 }
121 let node = self.load_object(node_ref)?;
122 let dict = match node.as_dict() {
123 Some(d) => d,
124 None => return Ok(()),
125 };
126
127 let kids = match dict.get("Kids").and_then(|k| k.as_array()) {
128 Some(k) => k,
129 None => {
130 out.push(node_ref);
131 return Ok(());
132 }
133 };
134
135 let count = dict.get("Count").and_then(|c| c.as_integer()).unwrap_or(-1);
137 if count >= 0 && (count as usize) == kids.len() {
138 for kid in kids {
139 if let Some(kid_ref) = kid.as_reference() {
140 out.push(kid_ref);
141 }
142 }
143 return Ok(());
144 }
145
146 for kid in kids {
152 if let Some(kid_ref) = kid.as_reference()
153 && let Err(error) = self.collect_page_refs(kid_ref, out, visited, depth + 1)
154 {
155 tracing::warn!(target: LOG_TARGET,
156 error_code = error.telemetry_code(),
157 error_offset = ?error.telemetry_offset(),
158 "error collecting page ref from tree; skipping branch"
159 );
160 }
161 }
162 Ok(())
163 }
164
165 pub(super) fn bytes_at(&self, offset: u64, len: usize) -> Option<&[u8]> {
170 let start = usize::try_from(offset).ok()?;
171 let bytes = self.source_bytes.get(start..)?;
172 Some(&bytes[..len.min(bytes.len())])
173 }
174
175 fn scan_for_object(&self, obj_ref: ObjectRef) -> Result<u64> {
182 {
183 let scan_cache = self.scanned_object_offsets.lock_or_recover();
184 if let Some(offsets) = scan_cache.as_ref() {
185 if let Some(&offset) = offsets.get(&obj_ref.id) {
186 return Ok(offset);
187 }
188 return Err(Error::ObjectNotFound(obj_ref.id, obj_ref.generation));
189 }
190 }
191
192 tracing::info!(target: LOG_TARGET,
193 "Building object offset map from file scan (triggered by object {} {})",
194 obj_ref.id,
195 obj_ref.generation
196 );
197
198 let content: &[u8] = &self.source_bytes;
199
200 let mut offsets = HashMap::new();
201
202 let mut pos = 0;
203 while pos < content.len() {
204 let valid_start = pos == 0 || content[pos - 1] == b'\n' || content[pos - 1] == b'\r';
205 if !valid_start || !content[pos].is_ascii_digit() {
206 pos += 1;
207 continue;
208 }
209
210 let start = pos;
211 while pos < content.len() && content[pos].is_ascii_digit() {
212 pos += 1;
213 }
214 if pos >= content.len() || content[pos] != b' ' {
215 continue;
216 }
217 let obj_num_str = std::str::from_utf8(&content[start..pos]).unwrap_or("");
218 let obj_num: u32 = match obj_num_str.parse() {
219 Ok(n) => n,
220 Err(_) => continue,
221 };
222
223 pos += 1;
224
225 let gen_start = pos;
226 while pos < content.len() && content[pos].is_ascii_digit() {
227 pos += 1;
228 }
229 if pos >= content.len() || content[pos] != b' ' {
230 continue;
231 }
232 let _gen_str = std::str::from_utf8(&content[gen_start..pos]).unwrap_or("");
233
234 pos += 1;
235
236 if pos + 3 <= content.len() && &content[pos..pos + 3] == b"obj" {
237 let after_obj = pos + 3;
238 let valid_end = after_obj >= content.len() || {
239 let c = content[after_obj];
240 c == b'\n' || c == b'\r' || c == b' ' || c == b'\t' || c == b'<'
241 };
242 if valid_end {
243 offsets.entry(obj_num).or_insert(start as u64);
244 pos = after_obj;
245 continue;
246 }
247 }
248 pos = start + 1;
250 }
251
252 tracing::debug!(target: LOG_TARGET, "File scan found {} objects", offsets.len());
253
254 let result = offsets.get(&obj_ref.id).copied();
255 *self.scanned_object_offsets.lock_or_recover() = Some(offsets);
256
257 match result {
258 Some(offset) => Ok(offset),
259 None => Err(Error::ObjectNotFound(obj_ref.id, obj_ref.generation)),
260 }
261 }
262
263 pub(super) fn recover_from_object_streams(&self) {
280 use crate::objstm::parse_object_stream_with_decryption_outcome;
281
282 {
283 let done = self.objstm_recovery_done.lock_or_recover();
284 if *done {
285 return;
286 }
287 }
288
289 tracing::debug!(target: LOG_TARGET, "Sweeping object streams to recover xref-flagged-free objects");
290
291 let candidates = find_objstm_candidates(&self.source_bytes);
303
304 let mut objstms_found = 0usize;
305 let mut recovered = 0usize;
306 for (stream_obj_num, offset) in &candidates {
307 let stream_ref = ObjectRef::new(*stream_obj_num, 0);
308 let stream_obj = match self.load_uncompressed_object(stream_ref, *offset) {
309 Ok(obj) => obj,
310 Err(_) => continue,
311 };
312
313 let is_objstm = stream_obj
314 .as_dict()
315 .and_then(|d| d.get("Type"))
316 .and_then(|t| t.as_name())
317 .is_some_and(|n| n == "ObjStm");
318 if !is_objstm {
319 continue;
320 }
321 objstms_found += 1;
322
323 let outcome = match parse_object_stream_with_decryption_outcome(&stream_obj, None, 0, 0) {
327 Ok(outcome) => outcome,
328 Err(error) => {
329 tracing::warn!(
330 target: crate::LOG_TARGET_ROOT,
331 operation = "recover_object_stream",
332 stream_object_id = stream_obj_num,
333 error_code = error.telemetry_code(),
334 error_offset = ?error.telemetry_offset(),
335 "skipping malformed object stream during recovery"
336 );
337 continue;
338 }
339 };
340 self.trace_object_stream_recovery_once(*stream_obj_num, &outcome);
341 let objects_map = Arc::new(outcome.objects);
342 self.object_stream_cache
343 .lock_or_recover()
344 .insert(ObjectRef::new(*stream_obj_num, 0), Arc::clone(&objects_map));
345
346 let mut cache = self.object_cache.lock_or_recover();
347 for (obj_num, object) in objects_map.iter() {
348 let cache_ref = ObjectRef::new(*obj_num, 0);
349 match cache.get(&cache_ref) {
353 Some(Object::Null) | None => {
354 cache.insert(cache_ref, object.clone());
355 recovered += 1;
356 }
357 _ => {}
358 }
359 }
360 }
361
362 tracing::debug!(target: LOG_TARGET,
363 "Object-stream recovery sweep: {} candidate positions, {} ObjStms, {} objects cached",
364 candidates.len(),
365 objstms_found,
366 recovered
367 );
368
369 *self.objstm_recovery_done.lock_or_recover() = true;
370 }
371
372 pub(super) fn recover_object_streams_if_reconstructed(&self) {
388 if self.xref_reconstructed {
389 self.recover_from_object_streams();
390 }
391 }
392
393 pub fn load_object(&self, obj_ref: ObjectRef) -> Result<Object> {
423 tracing::trace!(target: LOG_TARGET,
424 object_id = obj_ref.id,
425 generation = obj_ref.generation,
426 "loading object"
427 );
428
429 {
431 let depth = RECURSION_DEPTH.with(|d| *d.borrow());
432 if depth >= MAX_RECURSION_DEPTH {
433 tracing::error!(target: LOG_TARGET,
434 "Recursion depth limit exceeded ({}) while loading object {} gen {}",
435 MAX_RECURSION_DEPTH,
436 obj_ref.id,
437 obj_ref.generation
438 );
439 return Err(Error::RecursionLimitExceeded(MAX_RECURSION_DEPTH));
440 }
441 }
442
443 if RESOLVING_STACK.with(|s| s.borrow().contains(&obj_ref)) {
446 tracing::error!(target: LOG_TARGET,
447 "Circular reference detected for object {} gen {} (depth: {})",
448 obj_ref.id,
449 obj_ref.generation,
450 RECURSION_DEPTH.with(|d| *d.borrow())
451 );
452 return Err(Error::CircularReference(obj_ref));
453 }
454
455 let cached_opt = self.object_cache.lock_or_recover().get(&obj_ref).cloned();
457 if let Some(cached) = cached_opt {
458 return Ok(cached);
459 }
460
461 let entry = match self.xref.get(obj_ref.id) {
462 Some(entry) => entry,
463 None => {
464 self.recovery
467 .xref_misses
468 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
469 let available: Vec<u32> = self.xref.entries.keys().copied().take(20).collect();
470 tracing::trace!(target: LOG_TARGET,
471 "Object {} not in xref table. Total entries: {}. First 20 objects: {:?}",
472 obj_ref.id,
473 self.xref.len(),
474 available
475 );
476
477 match self.scan_for_object(obj_ref) {
478 Ok(offset) => {
479 self.recovery
480 .file_scan_recoveries
481 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
482 tracing::trace!(target: LOG_TARGET,
483 "Successfully found object {} via file scan at offset {}",
484 obj_ref.id,
485 offset
486 );
487
488 RESOLVING_STACK.with(|s| {
489 s.borrow_mut().insert(obj_ref);
490 });
491 RECURSION_DEPTH.with(|d| *d.borrow_mut() += 1);
492
493 let result = self.load_uncompressed_object(obj_ref, offset);
494
495 RECURSION_DEPTH.with(|d| *d.borrow_mut() -= 1);
496 RESOLVING_STACK.with(|s| {
497 s.borrow_mut().remove(&obj_ref);
498 });
499
500 return result;
501 }
502 Err(_) => {
503 self.recovery
506 .objects_treated_as_null
507 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
508 tracing::trace!(target: LOG_TARGET,
509 "Object {} gen {} not found (xref + file scan failed), treating as Null per §7.3.10",
510 obj_ref.id,
511 obj_ref.generation
512 );
513 if self.xref_reconstructed {
531 let msg = format!(
532 "Object {} {} referenced but absent from the reconstructed xref table \
533 (not found by header scan or object-stream recovery); treating as Null \
534 per §7.3.10, but this document's xref was reconstructed and content may \
535 be missing as a result",
536 obj_ref.id, obj_ref.generation
537 );
538 tracing::warn!(target: LOG_TARGET, "{}", msg);
539 self.push_structured_warning(crate::extractors::warnings::Warning {
540 category: crate::extractors::warnings::WarningCategory::XrefRecovery,
541 page: None,
542 message: msg,
543 spec_section: Some("7.3.10"),
544 });
545 }
546 self.object_cache.lock_or_recover().insert(obj_ref, Object::Null);
547 return Ok(Object::Null);
548 }
549 }
550 }
551 };
552
553 tracing::trace!(target: LOG_TARGET,
554 " → Found in xref: type={:?}, offset={}, gen={}, in_use={}",
555 entry.entry_type,
556 entry.offset,
557 entry.generation,
558 entry.in_use
559 );
560
561 if !entry.in_use {
562 tracing::debug!(target: LOG_TARGET,
563 "Object {} is marked as free (not in use). This may be due to a corrupted xref table.",
564 obj_ref.id
565 );
566
567 if let Ok(scanned_offset) = self.scan_for_object(obj_ref) {
582 tracing::warn!(target: LOG_TARGET,
583 "Object {} marked free in xref but found in file scan at offset {}; recovering",
584 obj_ref.id,
585 scanned_offset
586 );
587 RESOLVING_STACK.with(|s| {
588 s.borrow_mut().insert(obj_ref);
589 });
590 RECURSION_DEPTH.with(|d| *d.borrow_mut() += 1);
591 let result = self.load_uncompressed_object(obj_ref, scanned_offset);
592 RECURSION_DEPTH.with(|d| *d.borrow_mut() -= 1);
593 RESOLVING_STACK.with(|s| {
594 s.borrow_mut().remove(&obj_ref);
595 });
596 return result;
597 }
598
599 self.recover_from_object_streams();
604 if let Some(obj) = self.object_cache.lock_or_recover().get(&obj_ref).cloned()
605 && !matches!(obj, Object::Null)
606 {
607 tracing::warn!(target: LOG_TARGET, "Object {} recovered from object-stream sweep", obj_ref.id);
608 return Ok(obj);
609 }
610
611 tracing::warn!(target: LOG_TARGET,
613 "Free object {} gen {}, treating as Null per §7.3.10",
614 obj_ref.id,
615 obj_ref.generation
616 );
617 self.object_cache.lock_or_recover().insert(obj_ref, Object::Null);
618 return Ok(Object::Null);
619 }
620
621 RESOLVING_STACK.with(|s| {
622 s.borrow_mut().insert(obj_ref);
623 });
624 RECURSION_DEPTH.with(|d| *d.borrow_mut() += 1);
625
626 use crate::xref::XRefEntryType;
627 let entry_type = entry.entry_type;
628 let entry_offset = entry.offset;
629 let entry_gen = entry.generation;
630 let result = match entry_type {
631 XRefEntryType::Compressed => {
632 tracing::trace!(target: LOG_TARGET, " → Compressed object in stream {}, index {}", entry_offset, entry_gen);
636 self.load_compressed_object(obj_ref, entry_offset as u32, entry_gen)
637 }
638 XRefEntryType::Uncompressed => {
639 tracing::trace!(target: LOG_TARGET, " → Uncompressed object at offset {}", entry_offset);
640 self.load_uncompressed_object(obj_ref, entry_offset)
641 }
642 XRefEntryType::Free => {
643 tracing::warn!(target: LOG_TARGET,
646 "Object {} has type Free despite in_use=true, treating as Null",
647 obj_ref.id
648 );
649 self.object_cache.lock_or_recover().insert(obj_ref, Object::Null);
650 Ok(Object::Null)
651 }
652 };
653
654 RECURSION_DEPTH.with(|d| *d.borrow_mut() -= 1);
655 RESOLVING_STACK.with(|s| {
656 s.borrow_mut().remove(&obj_ref);
657 });
658
659 result
660 }
661
662 pub fn resolve_references(&self, obj: &Object, max_depth: usize) -> Result<Object> {
690 if max_depth == 0 {
691 return Ok(obj.clone());
692 }
693
694 match obj {
695 Object::Reference(obj_ref) => match self.load_object(*obj_ref) {
696 Ok(resolved) => self.resolve_references(&resolved, max_depth - 1),
697 Err(error) => {
698 tracing::warn!(target: LOG_TARGET,
699 operation = "resolve_reference",
700 object_id = obj_ref.id,
701 generation = obj_ref.generation,
702 error_code = error.telemetry_code(),
703 error_offset = ?error.telemetry_offset(),
704 "failed to resolve reference"
705 );
706 Ok(obj.clone())
707 }
708 },
709
710 Object::Dictionary(dict) => {
711 let mut resolved_dict = std::collections::HashMap::new();
712 for (key, value) in dict.iter() {
713 let resolved_value = self.resolve_references(value, max_depth - 1)?;
714 resolved_dict.insert(key.clone(), resolved_value);
715 }
716 Ok(Object::Dictionary(resolved_dict))
717 }
718
719 Object::Array(arr) => {
720 let resolved_arr: Result<Vec<Object>> = arr
721 .iter()
722 .map(|item| self.resolve_references(item, max_depth - 1))
723 .collect();
724 Ok(Object::Array(resolved_arr?))
725 }
726
727 _ => Ok(obj.clone()),
728 }
729 }
730
731 pub(super) fn resolve_obj_ref(&self, obj: &Object) -> Object {
738 if let Some(obj_ref) = obj.as_reference() {
739 match self.load_object(obj_ref) {
740 Ok(resolved) => resolved,
741 Err(error) => {
742 tracing::warn!(target: LOG_TARGET,
743 operation = "resolve_indirect_reference",
744 object_id = obj_ref.id,
745 generation = obj_ref.generation,
746 error_code = error.telemetry_code(),
747 error_offset = ?error.telemetry_offset(),
748 "failed to resolve indirect reference"
749 );
750 obj.clone()
751 }
752 }
753 } else {
754 obj.clone()
755 }
756 }
757
758 pub fn is_form_xobject(&self, obj_ref: ObjectRef) -> bool {
762 {
763 if self.image_xobject_cache.lock_or_recover().contains(&obj_ref) {
764 return false;
765 }
766 }
767
768 let cached_opt = self.object_cache.lock_or_recover().get(&obj_ref).cloned();
769 if let Some(cached) = cached_opt {
770 let is_form = cached
771 .as_dict()
772 .and_then(|d| d.get("Subtype"))
773 .and_then(|s| s.as_name())
774 == Some("Form");
775 if !is_form {
776 self.image_xobject_cache.lock_or_recover().insert(obj_ref);
777 }
778 return is_form;
779 }
780
781 let entry = match self.xref.get(obj_ref.id) {
782 Some(e) => e,
783 None => return true,
784 };
785
786 use crate::xref::XRefEntryType;
788 if entry.entry_type != XRefEntryType::Uncompressed || !entry.in_use {
789 return true;
790 }
791
792 let Some(data) = self.bytes_at(entry.offset, 1024) else {
793 return true;
794 };
795
796 if let Some(pos) = data.windows(8).position(|w| w == b"/Subtype") {
797 let after = &data[pos + 8..];
798 let trimmed = after
799 .iter()
800 .position(|&b| b != b' ' && b != b'\t' && b != b'\r' && b != b'\n');
801 if let Some(start) = trimmed {
802 let name_data = &after[start..];
803 if name_data.starts_with(b"/Form") {
804 return true;
805 }
806 self.image_xobject_cache.lock_or_recover().insert(obj_ref);
807 return false;
808 }
809 }
810
811 true
812 }
813
814 fn load_uncompressed_object(&self, obj_ref: ObjectRef, offset: u64) -> Result<Object> {
816 self.load_uncompressed_object_impl(obj_ref, offset, false)
817 }
818
819 fn decrypt_strings_in_object(handler: &EncryptionHandler, obj: &mut Object, obj_num: u32, gen_num: u32) {
832 let mut failed = 0usize;
838 Self::decrypt_strings_in_object_inner(handler, obj, obj_num, gen_num, &mut failed);
839 if failed > 0 {
840 tracing::warn!(target: LOG_TARGET,
841 object_id = obj_num,
842 generation = gen_num,
843 count = failed,
844 "string decryption failed; ciphertext left in place"
845 );
846 }
847 }
848
849 fn decrypt_strings_in_object_inner(
850 handler: &EncryptionHandler,
851 obj: &mut Object,
852 obj_num: u32,
853 gen_num: u32,
854 failed: &mut usize,
855 ) {
856 match obj {
857 Object::String(bytes) => match handler.decrypt_string(bytes, obj_num, gen_num) {
858 Ok(decrypted) => *bytes = decrypted,
859 Err(e) => {
860 *failed += 1;
861 tracing::trace!(target: LOG_TARGET,
862 object_id = obj_num,
863 generation = gen_num,
864 "string decryption failed: {}",
865 e
866 );
867 }
868 },
869 Object::Array(items) => {
870 for item in items {
871 Self::decrypt_strings_in_object_inner(handler, item, obj_num, gen_num, failed);
872 }
873 }
874 Object::Dictionary(dict) => {
875 for value in dict.values_mut() {
876 Self::decrypt_strings_in_object_inner(handler, value, obj_num, gen_num, failed);
877 }
878 }
879 Object::Stream { dict, .. } => {
880 for value in dict.values_mut() {
884 Self::decrypt_strings_in_object_inner(handler, value, obj_num, gen_num, failed);
885 }
886 }
887 _ => {}
888 }
889 }
890
891 pub(super) fn load_uncompressed_object_impl(
893 &self,
894 obj_ref: ObjectRef,
895 offset: u64,
896 already_corrected: bool,
897 ) -> Result<Object> {
898 const MAX_HEADER_LINE_BYTES: u64 = 64 * 1024; let mut cursor = Cursor::new(self.source_bytes.as_slice());
914 let (header_bytes, full_header) = {
915 let reader = &mut cursor;
916 reader.seek(SeekFrom::Start(offset))?;
917
918 let mut header_bytes = Vec::new();
919 let bytes_read = reader
920 .by_ref()
921 .take(MAX_HEADER_LINE_BYTES)
922 .read_until(b'\n', &mut header_bytes)?;
923
924 if bytes_read == 0 {
925 let msg = format!("Unexpected EOF while reading object {} header", obj_ref.id);
926 tracing::warn!(target: LOG_TARGET, "{}", msg);
927 self.push_structured_warning(crate::extractors::warnings::Warning {
930 category: crate::extractors::warnings::WarningCategory::EofPremature,
931 page: None,
932 message: msg,
933 spec_section: Some("7.5"),
934 });
935 return Err(Error::UnexpectedEof);
936 }
937
938 let line = String::from_utf8_lossy(&header_bytes);
939
940 let mut full_header = line.to_string();
942 let max_header_lines = 5;
943 let mut lines_read = 1;
944
945 while !has_standalone_obj_keyword(full_header.as_bytes()) && lines_read < max_header_lines {
946 let mut next_bytes = Vec::new();
947 let next_read = reader
952 .by_ref()
953 .take(MAX_HEADER_LINE_BYTES)
954 .read_until(b'\n', &mut next_bytes)?;
955 if next_read == 0 {
956 break;
957 }
958 let next_line = String::from_utf8_lossy(&next_bytes);
959 full_header.push(' ');
960 full_header.push_str(&next_line);
961 lines_read += 1;
962 }
963 (header_bytes, full_header)
964 };
965
966 let parts: Vec<&str> = full_header.split_whitespace().collect();
967
968 let obj_pos = parts
969 .iter()
970 .position(|&p| p == "obj" || (p.starts_with("obj") && !p.starts_with("endobj")));
971
972 let obj_pos = match obj_pos {
973 Some(pos) if pos >= 2 => pos,
974 _ => {
975 if !already_corrected {
977 tracing::warn!(target: LOG_TARGET,
980 "No object header at offset {}, searching backwards for object {} {} obj",
981 offset,
982 obj_ref.id,
983 obj_ref.generation
984 );
985
986 if let Ok(corrected_offset) = self.find_object_header_backwards(obj_ref, offset) {
987 tracing::warn!(target: LOG_TARGET,
988 "Found object header at offset {} (xref said {})",
989 corrected_offset,
990 offset
991 );
992 return self.load_uncompressed_object_impl(obj_ref, corrected_offset, true);
993 }
994 }
995
996 tracing::warn!(target: LOG_TARGET,
997 operation = "load_uncompressed_object",
998 error_code = "malformed_object_header",
999 error_offset = offset,
1000 object_id = obj_ref.id,
1001 generation = obj_ref.generation,
1002 "PDF object recovery failed"
1003 );
1004 return Err(Error::ParseError {
1005 offset: offset as usize,
1006 reason: format!("Expected object header, found: {}", full_header.trim()),
1007 });
1008 }
1009 };
1010
1011 let _obj_pos = obj_pos;
1012
1013 let obj_num_parsed = parts[0].parse::<u32>();
1022 let gen_num_parsed = parts[1].parse::<u16>();
1023 if !already_corrected
1024 && (obj_num_parsed.is_err() || gen_num_parsed.is_err())
1025 && let Ok(scan_offset) = self.scan_for_object(obj_ref)
1026 && scan_offset != offset
1027 {
1028 tracing::warn!(target: LOG_TARGET,
1029 operation = "load_uncompressed_object",
1030 error_code = "invalid_object_header_number",
1031 error_offset = offset,
1032 recovered_offset = scan_offset,
1033 object_id = obj_ref.id,
1034 generation = obj_ref.generation,
1035 "retrying PDF object load at scan-reported offset"
1036 );
1037 return self.load_uncompressed_object_impl(obj_ref, scan_offset, true);
1038 }
1039 let obj_num: u32 = obj_num_parsed.map_err(|_| Error::ParseError {
1040 offset: offset as usize,
1041 reason: format!("Invalid object number in header: {}", parts[0]),
1042 })?;
1043 let gen_num: u16 = gen_num_parsed.map_err(|_| Error::ParseError {
1044 offset: offset as usize,
1045 reason: format!("Invalid generation number in header: {}", parts[1]),
1046 })?;
1047
1048 if obj_num != obj_ref.id || gen_num != obj_ref.generation {
1053 if !already_corrected
1054 && let Ok(scan_offset) = self.scan_for_object(obj_ref)
1055 && scan_offset != offset
1056 {
1057 self.recovery
1058 .file_scan_recoveries
1059 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1060 tracing::warn!(target: LOG_TARGET,
1061 operation = "load_uncompressed_object",
1062 error_code = "object_reference_mismatch",
1063 error_offset = offset,
1064 recovered_offset = scan_offset,
1065 expected_object_id = obj_ref.id,
1066 expected_generation = obj_ref.generation,
1067 found_object_id = obj_num,
1068 found_generation = gen_num,
1069 "retrying PDF object load at scan-reported offset"
1070 );
1071 return self.load_uncompressed_object_impl(obj_ref, scan_offset, true);
1072 }
1073
1074 tracing::warn!(target: LOG_TARGET,
1075 operation = "load_uncompressed_object",
1076 error_code = "object_reference_mismatch",
1077 error_offset = offset,
1078 expected_object_id = obj_ref.id,
1079 expected_generation = obj_ref.generation,
1080 found_object_id = obj_num,
1081 found_generation = gen_num,
1082 "PDF object header does not match requested reference"
1083 );
1084 return Err(Error::ParseError {
1085 offset: offset as usize,
1086 reason: format!(
1087 "Object reference mismatch: expected {} {} obj, found {} {} obj",
1088 obj_ref.id, obj_ref.generation, obj_num, gen_num
1089 ),
1090 });
1091 }
1092
1093 let mut data = Vec::new();
1096
1097 if let Some(obj_keyword_pos) = header_bytes.windows(3).position(|w| w == b"obj") {
1098 let after_obj_pos = obj_keyword_pos + 3;
1099
1100 let mut content_start = after_obj_pos;
1101 while content_start < header_bytes.len()
1102 && (header_bytes[content_start] == b' '
1103 || header_bytes[content_start] == b'\t'
1104 || header_bytes[content_start] == b'\r')
1105 {
1106 content_start += 1;
1107 }
1108
1109 if content_start < header_bytes.len() && header_bytes[content_start] != b'\n' {
1112 data.extend_from_slice(&header_bytes[content_start..]);
1113 tracing::trace!(target: LOG_TARGET,
1114 "Object {} has content after 'obj' on header line ({} bytes)",
1115 obj_ref.id,
1116 header_bytes.len() - content_start
1117 );
1118 }
1119 }
1120
1121 const MAX_BYTES: usize = 100 * 1024 * 1024; {
1127 let reader = &mut cursor;
1128 loop {
1129 let mut chunk = Vec::new();
1130 let bytes_read = reader.read_until(b'\n', &mut chunk)?;
1131
1132 if data.len() > MAX_BYTES {
1133 tracing::warn!(target: LOG_TARGET,
1134 "Object {} exceeded maximum byte limit ({} bytes), truncating",
1135 obj_ref.id,
1136 MAX_BYTES
1137 );
1138 break;
1139 }
1140
1141 if bytes_read == 0 {
1142 let msg = format!(
1143 "Unexpected EOF while reading object {} (no endobj found after {} bytes)",
1144 obj_ref.id,
1145 data.len()
1146 );
1147 tracing::warn!(target: LOG_TARGET, "{}", msg);
1148 self.push_structured_warning(crate::extractors::warnings::Warning {
1150 category: crate::extractors::warnings::WarningCategory::EofPremature,
1151 page: None,
1152 message: msg,
1153 spec_section: Some("7.5"),
1154 });
1155 break;
1157 }
1158
1159 if chunk.contains(&b'e')
1160 && let Some(endobj_pos) = find_substring(&chunk, b"endobj")
1161 {
1162 data.extend_from_slice(&chunk[..endobj_pos]);
1163 break;
1164 }
1165
1166 data.extend_from_slice(&chunk);
1167 }
1168 }
1169
1170 tracing::trace!(target: LOG_TARGET,
1171 "About to parse object {} gen {} ({} bytes)",
1172 obj_ref.id,
1173 obj_ref.generation,
1174 data.len()
1175 );
1176
1177 let mut obj = match parse_object(&data) {
1178 Ok((_, parsed_obj)) => parsed_obj,
1179 Err(e) => {
1180 let error_kind = match &e {
1181 nom::Err::Incomplete(_) => "Incomplete data",
1182 nom::Err::Error(err) | nom::Err::Failure(err) => match err.code {
1183 nom::error::ErrorKind::Eof => "Unexpected EOF",
1184 nom::error::ErrorKind::Tag => "Expected tag not found",
1185 nom::error::ErrorKind::Fail => "Parse failed",
1186 _ => "Parse error",
1187 },
1188 };
1189 tracing::warn!(target: LOG_TARGET,
1190 "Object {} at offset {} is corrupted ({}), using Null placeholder. \
1191 This may result in missing content from the PDF.",
1192 obj_ref.id,
1193 offset,
1194 error_kind
1195 );
1196 Object::Null
1197 }
1198 };
1199
1200 let is_encrypt_dict = *self.encrypt_dict_ref.lock_or_recover() == Some(obj_ref);
1201 if !is_encrypt_dict {
1202 let handler_guard = self.encryption_handler.lock_or_recover();
1203 if let Some(handler) = handler_guard.as_ref()
1204 && handler.is_authenticated()
1205 {
1206 Self::decrypt_strings_in_object(handler, &mut obj, obj_ref.id, obj_ref.generation as u32);
1207 }
1208 }
1209
1210 self.object_cache.lock_or_recover().insert(obj_ref, obj.clone());
1211
1212 Ok(obj)
1213 }
1214
1215 pub(super) fn load_compressed_object(
1223 &self,
1224 obj_ref: ObjectRef,
1225 stream_obj_num: u32,
1226 _index_in_stream: u16,
1227 ) -> Result<Object> {
1228 tracing::trace!(target: LOG_TARGET,
1229 object_id = obj_ref.id,
1230 stream_object_id = stream_obj_num,
1231 "loading compressed object from object stream"
1232 );
1233
1234 if let Err(error) = self.ensure_encryption_initialized() {
1235 trace_recoverable_pdf_error("initialize_object_stream_encryption", &error);
1236 }
1237
1238 let Some((objects_map, newly_parsed)) = self.load_object_stream_objects(stream_obj_num)? else {
1239 self.object_cache.lock_or_recover().insert(obj_ref, Object::Null);
1240 return Ok(Object::Null);
1241 };
1242
1243 let obj = match objects_map.get(&obj_ref.id) {
1244 Some(o) => o.clone(),
1245 None => {
1246 tracing::warn!(
1247 target: crate::LOG_TARGET_ROOT,
1248 operation = "load_compressed_object",
1249 error_code = "missing_stream_object",
1250 object_id = obj_ref.id,
1251 stream_object_id = stream_obj_num,
1252 "treating missing compressed object as null"
1253 );
1254 Object::Null
1255 }
1256 };
1257
1258 if newly_parsed {
1259 self.cache_object_stream_entries(stream_obj_num, &objects_map);
1260 }
1261 Ok(obj)
1262 }
1263
1264 fn load_object_stream_objects(&self, stream_obj_num: u32) -> Result<Option<(Arc<HashMap<u32, Object>>, bool)>> {
1265 use crate::objstm::parse_object_stream_with_decryption_outcome;
1266
1267 let stream_ref = ObjectRef::new(stream_obj_num, 0);
1268 if let Some(cached) = self.object_stream_cache.lock_or_recover().get(&stream_ref).cloned() {
1269 return Ok(Some((cached, false)));
1270 }
1271 let Some(stream_entry) = self.xref.get(stream_obj_num) else {
1272 tracing::warn!(
1273 target: crate::LOG_TARGET_ROOT,
1274 operation = "load_object_stream",
1275 error_code = "missing_xref_entry",
1276 stream_object_id = stream_obj_num,
1277 "treating compressed object as null"
1278 );
1279 return Ok(None);
1280 };
1281 if stream_entry.entry_type != crate::xref::XRefEntryType::Uncompressed {
1282 return Err(Error::InvalidPdf(format!(
1283 "object stream {} is not an uncompressed object",
1284 stream_obj_num
1285 )));
1286 }
1287 let stream_obj = self.load_uncompressed_object(stream_ref, stream_entry.offset)?;
1288 let handler_ref = self.encryption_handler.lock_or_recover();
1289 let outcome = if let Some(handler) = handler_ref.as_ref() {
1290 match parse_object_stream_with_decryption_outcome(&stream_obj, None, 0, 0) {
1291 Ok(outcome) => outcome,
1292 Err(_) => {
1293 let decrypt_fn =
1294 |data: &[u8]| -> Result<Vec<u8>> { handler.decrypt_stream(data, stream_obj_num, 0) };
1295 parse_object_stream_with_decryption_outcome(&stream_obj, Some(&decrypt_fn), stream_obj_num, 0)?
1296 }
1297 }
1298 } else {
1299 parse_object_stream_with_decryption_outcome(&stream_obj, None, 0, 0)?
1300 };
1301 drop(handler_ref);
1302 self.trace_object_stream_recovery_once(stream_obj_num, &outcome);
1303 let parsed = Arc::new(outcome.objects);
1304 self.object_stream_cache
1305 .lock_or_recover()
1306 .insert(stream_ref, Arc::clone(&parsed));
1307 Ok(Some((parsed, true)))
1308 }
1309
1310 pub(super) fn trace_object_stream_recovery_once(
1311 &self,
1312 stream_obj_num: u32,
1313 outcome: &crate::objstm::ObjectStreamParseOutcome,
1314 ) {
1315 if !outcome.has_recovery() {
1316 return;
1317 }
1318 if self
1319 .object_stream_telemetry_seen
1320 .lock_or_recover()
1321 .should_emit(stream_obj_num)
1322 {
1323 outcome.trace_recovery();
1324 }
1325 }
1326
1327 fn cache_object_stream_entries(&self, stream_obj_num: u32, objects_map: &HashMap<u32, Object>) {
1328 for (obj_num, object) in objects_map {
1329 let cache_ref = ObjectRef::new(*obj_num, 0);
1330 let should_cache = if let Some(entry) = self.xref.get(*obj_num) {
1331 entry.entry_type == crate::xref::XRefEntryType::Compressed && entry.offset == stream_obj_num as u64
1332 } else {
1333 true
1334 };
1335 if should_cache {
1336 self.object_cache.lock_or_recover().insert(cache_ref, object.clone());
1337 } else {
1338 tracing::trace!(target: LOG_TARGET,
1339 object_id = obj_num,
1340 stream_object_id = stream_obj_num,
1341 "not caching object from stream — xref points elsewhere"
1342 );
1343 }
1344 }
1345 }
1346
1347 fn find_object_header_backwards(&self, obj_ref: ObjectRef, wrong_offset: u64) -> Result<u64> {
1356 if wrong_offset == 0 {
1357 return Err(Error::ParseError {
1358 offset: wrong_offset as usize,
1359 reason: "Cannot search backwards from offset 0".to_string(),
1360 });
1361 }
1362
1363 let search_distance = std::cmp::min(100, wrong_offset);
1364 let search_start = wrong_offset - search_distance;
1365
1366 let buffer = self
1367 .bytes_at(search_start, search_distance as usize + 100)
1368 .unwrap_or_default();
1369 let bytes_read = buffer.len();
1370
1371 if bytes_read == 0 {
1372 return Err(Error::ParseError {
1373 offset: wrong_offset as usize,
1374 reason: "Could not read backwards search region".to_string(),
1375 });
1376 }
1377
1378 let expected_header = format!("{} {} obj", obj_ref.id, obj_ref.generation);
1380 let pattern_bytes = expected_header.as_bytes();
1381
1382 let mut best_match: Option<(usize, i64)> = None;
1386
1387 for (i, window) in buffer[..bytes_read].windows(pattern_bytes.len()).enumerate() {
1388 if window == pattern_bytes {
1389 let candidate_offset = search_start + i as u64;
1390 let distance = (candidate_offset as i64) - (wrong_offset as i64);
1391
1392 if (-100..=10).contains(&distance) {
1395 let is_better = best_match
1396 .as_ref()
1397 .is_none_or(|(_, best_dist)| distance.abs() < best_dist.abs());
1398
1399 if is_better {
1400 best_match = Some((i, distance));
1401 }
1402 }
1403 }
1404 }
1405
1406 if let Some((pos, distance)) = best_match {
1407 let absolute_offset = search_start + pos as u64;
1408 tracing::warn!(target: LOG_TARGET,
1409 "Found object header '{}' at offset {} ({:+} bytes from xref at {})",
1410 expected_header,
1411 absolute_offset,
1412 distance,
1413 wrong_offset
1414 );
1415 return Ok(absolute_offset);
1416 }
1417
1418 let patterns = [
1420 format!("{} {} obj", obj_ref.id, obj_ref.generation).into_bytes(),
1421 format!("{} {} obj", obj_ref.id, obj_ref.generation).into_bytes(),
1422 format!("{}\t{} obj", obj_ref.id, obj_ref.generation).into_bytes(),
1423 format!("{} {}\tobj", obj_ref.id, obj_ref.generation).into_bytes(),
1424 ];
1425
1426 for pattern in &patterns {
1427 let mut best_match: Option<(usize, i64)> = None;
1428
1429 for (i, window) in buffer[..bytes_read].windows(pattern.len()).enumerate() {
1430 if window == pattern.as_slice() {
1431 let candidate_offset = search_start + i as u64;
1432 let distance = (candidate_offset as i64) - (wrong_offset as i64);
1433
1434 if (-100..=10).contains(&distance) {
1435 let is_better = best_match
1436 .as_ref()
1437 .is_none_or(|(_, best_dist)| distance.abs() < best_dist.abs());
1438
1439 if is_better {
1440 best_match = Some((i, distance));
1441 }
1442 }
1443 }
1444 }
1445
1446 if let Some((pos, distance)) = best_match {
1447 let absolute_offset = search_start + pos as u64;
1448 tracing::warn!(target: LOG_TARGET,
1449 "Found object header '{}' at offset {} ({:+} bytes, pattern match)",
1450 expected_header,
1451 absolute_offset,
1452 distance
1453 );
1454 return Ok(absolute_offset);
1455 }
1456 }
1457
1458 Err(Error::ParseError {
1459 offset: wrong_offset as usize,
1460 reason: format!(
1461 "Could not find object header '{}' within {} bytes before offset",
1462 expected_header, search_distance
1463 ),
1464 })
1465 }
1466}