1use super::xref_stream;
6use super::xref_types::{XRefEntryInfo, XRefEntryType};
7use super::{ParseError, ParseOptions, ParseResult};
8use crate::parser::reader::PDFLines;
9use std::collections::HashMap;
10use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
11
12pub(crate) fn find_byte_pattern(buffer: &[u8], pattern: &[u8]) -> Option<usize> {
25 buffer
26 .windows(pattern.len())
27 .position(|window| window == pattern)
28}
29
30fn rfind_byte_pattern(buffer: &[u8], pattern: &[u8]) -> Option<usize> {
32 buffer
33 .windows(pattern.len())
34 .rposition(|window| window == pattern)
35}
36
37fn parse_obj_header_bytes(line_bytes: &[u8]) -> Option<(u32, u16)> {
42 let line = String::from_utf8_lossy(line_bytes);
44 let parts: Vec<&str> = line.trim().split_whitespace().collect();
45
46 if parts.len() >= 3 && parts[2] == "obj" {
47 let obj_num = parts[0].parse::<u32>().ok()?;
48 let gen_num = parts[1].parse::<u16>().ok()?;
49 return Some((obj_num, gen_num));
50 }
51 None
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56struct ObjHeader {
57 obj_num: u32,
58 generation: u16,
59 offset: u64,
61}
62
63fn scan_window_for_headers(
68 window: &[u8],
69 window_base: u64,
70 out: &mut Vec<ObjHeader>,
71 seen: &mut std::collections::BTreeSet<u64>,
72) {
73 let mut pos = 0;
74 while pos < window.len() {
75 let Some(obj_rel) = find_byte_pattern(&window[pos..], b"obj") else {
76 break;
77 };
78 let abs = pos + obj_rel; if abs < 4 {
82 pos = abs + 3;
83 continue;
84 }
85
86 let line_start = window[..abs]
87 .iter()
88 .rposition(|&b| b == b'\n' || b == b'\r')
89 .map(|p| p + 1)
90 .unwrap_or(0);
91 let line_bytes = &window[line_start..abs + 3];
92
93 if let Some((obj_num, generation)) = parse_obj_header_bytes(line_bytes) {
94 let offset = window_base + line_start as u64;
95 if seen.insert(offset) {
96 out.push(ObjHeader {
97 obj_num,
98 generation,
99 offset,
100 });
101 }
102 }
103
104 pos = abs + 3;
105 }
106}
107
108fn scan_object_headers<R: Read + Seek>(reader: &mut R) -> ParseResult<Vec<ObjHeader>> {
116 scan_object_headers_chunked(reader, 64 * 1024)
117}
118
119fn scan_object_headers_chunked<R: Read + Seek>(
123 reader: &mut R,
124 chunk_size: usize,
125) -> ParseResult<Vec<ObjHeader>> {
126 let chunk_size = chunk_size.max(1);
127 const CARRY_CAP: usize = 1024;
130
131 reader.seek(SeekFrom::Start(0))?;
132
133 let mut headers: Vec<ObjHeader> = Vec::new();
134 let mut seen: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
135 let mut carry: Vec<u8> = Vec::new();
136 let mut window_base: u64 = 0; let mut chunk = vec![0u8; chunk_size];
138
139 loop {
140 let mut filled = 0;
142 while filled < chunk_size {
143 let n = reader.read(&mut chunk[filled..])?;
144 if n == 0 {
145 break;
146 }
147 filled += n;
148 }
149 let eof = filled == 0;
150 if eof && carry.is_empty() {
151 break;
152 }
153
154 let mut window = std::mem::take(&mut carry);
156 window.extend_from_slice(&chunk[..filled]);
157
158 scan_window_for_headers(&window, window_base, &mut headers, &mut seen);
159
160 if eof {
161 break;
162 }
163
164 let last_nl = window.iter().rposition(|&b| b == b'\n' || b == b'\r');
167 let mut start = last_nl.map(|p| p + 1).unwrap_or(0);
168 if window.len() - start > CARRY_CAP {
169 start = window.len() - CARRY_CAP;
170 }
171 window_base += start as u64;
172 carry = window[start..].to_vec();
173 }
174
175 headers.sort_by_key(|h| h.offset);
176 Ok(headers)
177}
178
179pub(crate) fn read_window_at<R: Read + Seek>(
182 reader: &mut R,
183 offset: u64,
184 max: usize,
185) -> ParseResult<Vec<u8>> {
186 reader.seek(SeekFrom::Start(offset))?;
187 let mut buf = vec![0u8; max];
188 let mut filled = 0;
189 while filled < max {
190 let n = reader.read(&mut buf[filled..])?;
191 if n == 0 {
192 break;
193 }
194 filled += n;
195 }
196 buf.truncate(filled);
197 Ok(buf)
198}
199
200fn read_tail<R: Read + Seek>(reader: &mut R, max: usize) -> ParseResult<(u64, Vec<u8>)> {
203 let len = reader.seek(SeekFrom::End(0))?;
204 let start = len.saturating_sub(max as u64);
205 let bytes = read_window_at(reader, start, (len - start) as usize)?;
206 Ok((start, bytes))
207}
208
209fn read_object_content<R: Read + Seek>(
213 reader: &mut R,
214 obj_num: u32,
215 offset: u64,
216) -> ParseResult<Option<String>> {
217 const OBJ_WINDOW: usize = 64 * 1024;
218 let window = read_window_at(reader, offset, OBJ_WINDOW)?;
219 let obj_pattern = format!("{obj_num} 0 obj");
220 let Some(obj_start) = find_byte_pattern(&window, obj_pattern.as_bytes()) else {
221 return Ok(None);
222 };
223 let Some(endobj_rel) = find_byte_pattern(&window[obj_start..], b"endobj") else {
224 return Ok(None);
225 };
226 let content_bytes = &window[obj_start..obj_start + endobj_rel];
227 Ok(Some(String::from_utf8_lossy(content_bytes).into_owned()))
228}
229
230fn find_object_offset<R: Read + Seek>(reader: &mut R, obj_num: u32) -> ParseResult<Option<u64>> {
242 const CHUNK_SIZE: usize = 64 * 1024;
243 const CARRY_CAP: usize = 1024;
244
245 reader.seek(SeekFrom::Start(0))?;
246
247 let mut carry: Vec<u8> = Vec::new();
248 let mut window_base: u64 = 0; let mut chunk = vec![0u8; CHUNK_SIZE];
250
251 loop {
252 let mut filled = 0;
253 while filled < CHUNK_SIZE {
254 let n = reader.read(&mut chunk[filled..])?;
255 if n == 0 {
256 break;
257 }
258 filled += n;
259 }
260 let eof = filled == 0;
261 if eof && carry.is_empty() {
262 break;
263 }
264
265 let mut window = std::mem::take(&mut carry);
266 window.extend_from_slice(&chunk[..filled]);
267
268 let mut headers = Vec::new();
270 let mut seen = std::collections::BTreeSet::new();
271 scan_window_for_headers(&window, window_base, &mut headers, &mut seen);
272 if let Some(header) = headers.iter().find(|h| h.obj_num == obj_num) {
273 return Ok(Some(header.offset));
274 }
275
276 if eof {
277 break;
278 }
279
280 let last_nl = window.iter().rposition(|&b| b == b'\n' || b == b'\r');
283 let mut start = last_nl.map(|p| p + 1).unwrap_or(0);
284 if window.len() - start > CARRY_CAP {
285 start = window.len() - CARRY_CAP;
286 }
287 window_base += start as u64;
288 carry = window[start..].to_vec();
289 }
290
291 Ok(None)
292}
293
294pub(crate) fn read_object_window<R: Read + Seek>(
302 reader: &mut R,
303 obj_num: u32,
304 max: usize,
305) -> ParseResult<Option<(u64, Vec<u8>)>> {
306 let Some(offset) = find_object_offset(reader, obj_num)? else {
307 return Ok(None);
308 };
309 let window = read_window_at(reader, offset, max)?;
310 Ok(Some((offset, window)))
311}
312
313pub(crate) fn scan_page_object_refs<R: Read + Seek>(
322 reader: &mut R,
323) -> ParseResult<Vec<(u32, u16)>> {
324 const PROBE: usize = 4 * 1024;
325
326 let headers = scan_object_headers(reader)?;
327 let mut pages = Vec::new();
328 for header in &headers {
329 let window = read_window_at(reader, header.offset, PROBE)?;
330 let region = match find_byte_pattern(&window, b"endobj") {
333 Some(end) => &window[..end],
334 None => &window[..],
335 };
336 let text = String::from_utf8_lossy(region);
337 let is_page = text.contains("/Type /Page") || text.contains("/Type/Page");
341 let is_pages = text.contains("/Type /Pages") || text.contains("/Type/Pages");
342 if is_page && !is_pages {
343 pages.push((header.obj_num, 0));
344 }
345 }
346 pages.sort_unstable();
347 pages.dedup();
348 Ok(pages)
349}
350
351fn read_pdf_line<R: BufRead>(reader: &mut R, buf: &mut String) -> std::io::Result<usize> {
358 buf.clear();
359 let mut total_bytes = 0;
360
361 loop {
362 let available = reader.fill_buf()?;
363 if available.is_empty() {
364 break;
366 }
367
368 let mut found_terminator = false;
370 let mut consume_len = 0;
371
372 for (i, &byte) in available.iter().enumerate() {
373 if byte == b'\r' || byte == b'\n' {
374 let content = &available[..i];
377 buf.push_str(&String::from_utf8_lossy(content));
378 consume_len = i + 1; if byte == b'\r' && i + 1 < available.len() && available[i + 1] == b'\n' {
382 consume_len += 1; }
384
385 found_terminator = true;
386 break;
387 }
388 }
389
390 if found_terminator {
391 reader.consume(consume_len);
392 total_bytes += consume_len;
393 break;
394 } else {
395 let len = available.len();
397 buf.push_str(&String::from_utf8_lossy(available));
398 reader.consume(len);
399 total_bytes += len;
400 }
401 }
402
403 Ok(total_bytes)
404}
405
406#[derive(Debug, Clone, Copy, PartialEq)]
410pub struct XRefEntry {
411 pub offset: u64,
413 pub generation: u16,
415 pub in_use: bool,
417}
418
419#[derive(Debug, Clone, PartialEq)]
421pub struct XRefEntryExt {
422 pub basic: XRefEntry,
424 pub compressed_info: Option<(u32, u32)>, }
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub(crate) struct RevisionXRefEntry {
431 pub(crate) object_number: u32,
432 pub(crate) generation: u16,
433 pub(crate) in_use: bool,
434}
435
436#[derive(Debug, Clone, PartialEq, Eq)]
438pub(crate) struct XRefRevision {
439 pub(crate) xref_offset: u64,
440 pub(crate) entries: Vec<RevisionXRefEntry>,
441}
442
443#[derive(Debug, Clone)]
445pub struct XRefTable {
446 entries: HashMap<u32, XRefEntry>,
448 extended_entries: HashMap<u32, XRefEntryExt>,
450 trailer: Option<super::objects::PdfDictionary>,
452 xref_offset: u64,
454 revisions: Vec<XRefRevision>,
456}
457
458impl Default for XRefTable {
459 fn default() -> Self {
460 Self::new()
461 }
462}
463
464impl XRefTable {
465 pub fn new() -> Self {
467 Self {
468 entries: HashMap::new(),
469 extended_entries: HashMap::new(),
470 trailer: None,
471 xref_offset: 0,
472 revisions: Vec::new(),
473 }
474 }
475
476 pub fn entries(&self) -> &HashMap<u32, XRefEntry> {
478 &self.entries
479 }
480
481 pub(crate) fn in_use_references(&self) -> Vec<(u32, u16)> {
484 let mut references: Vec<_> = self
485 .entries
486 .iter()
487 .filter(|(_, entry)| entry.in_use)
488 .map(|(number, entry)| (*number, entry.generation))
489 .chain(
490 self.extended_entries
491 .iter()
492 .filter(|(_, entry)| entry.basic.in_use)
493 .map(|(number, entry)| (*number, entry.basic.generation)),
494 )
495 .collect();
496 references.sort_unstable();
497 references.dedup();
498 references
499 }
500
501 pub(crate) fn object_storage_offset(&self, object_number: u32) -> Option<u64> {
504 if let Some((stream_number, _)) = self
505 .extended_entries
506 .get(&object_number)
507 .and_then(|entry| entry.compressed_info)
508 {
509 return self.entries.get(&stream_number).map(|entry| entry.offset);
510 }
511 self.entries
512 .get(&object_number)
513 .filter(|entry| entry.in_use)
514 .map(|entry| entry.offset)
515 }
516
517 pub fn parse<R: Read + Seek>(reader: &mut BufReader<R>) -> ParseResult<Self> {
519 Self::parse_with_options(reader, &super::ParseOptions::default())
520 }
521
522 pub fn parse_with_options<R: Read + Seek>(
524 reader: &mut BufReader<R>,
525 options: &super::ParseOptions,
526 ) -> ParseResult<Self> {
527 match Self::parse_with_incremental_updates_options(reader, options) {
529 Ok(table) => Ok(table),
530 Err(e) => {
531 if options.max_recovery_attempts > 0 {
541 tracing::warn!("Primary XRef parsing failed: {e:?}, attempting recovery");
542
543 reader.seek(SeekFrom::Start(0))?;
545 Self::parse_with_recovery_options(reader, options)
546 } else {
547 Err(e)
548 }
549 }
550 }
551 }
552
553 #[allow(dead_code)]
555 fn parse_with_incremental_updates<R: Read + Seek>(
556 reader: &mut BufReader<R>,
557 ) -> ParseResult<Self> {
558 Self::parse_with_incremental_updates_options(reader, &super::ParseOptions::default())
559 }
560
561 fn parse_with_incremental_updates_options<R: Read + Seek>(
563 reader: &mut BufReader<R>,
564 options: &super::ParseOptions,
565 ) -> ParseResult<Self> {
566 let xref_offset = Self::find_xref_offset(reader)?;
568
569 let mut merged_table = Self::new();
571 let mut current_offset = Some(xref_offset);
572 let mut visited_offsets = std::collections::HashSet::new();
573
574 while let Some(offset) = current_offset {
575 if visited_offsets.contains(&offset) {
577 tracing::debug!(
578 "Circular reference in XRef chain at offset {} (already visited)",
579 offset
580 );
581 break;
582 }
583 visited_offsets.insert(offset);
584
585 reader.seek(SeekFrom::Start(offset))?;
587 let table = Self::parse_primary_with_options(reader, options)?;
588
589 let mut revision_entries: Vec<_> = table
590 .entries
591 .iter()
592 .map(|(object_number, entry)| RevisionXRefEntry {
593 object_number: *object_number,
594 generation: entry.generation,
595 in_use: entry.in_use,
596 })
597 .chain(table.extended_entries.iter().map(|(object_number, entry)| {
598 RevisionXRefEntry {
599 object_number: *object_number,
600 generation: entry.basic.generation,
601 in_use: entry.basic.in_use,
602 }
603 }))
604 .collect();
605 revision_entries.sort_by_key(|entry| (entry.object_number, entry.generation));
606 merged_table.revisions.push(XRefRevision {
607 xref_offset: table.xref_offset,
608 entries: revision_entries,
609 });
610
611 let prev_offset = table
613 .trailer
614 .as_ref()
615 .and_then(|t| t.get("Prev"))
616 .and_then(|obj| obj.as_integer())
617 .map(|i| i as u64);
618
619 if let Some(_prev) = prev_offset {
620 } else {
621 }
622
623 let _regular_count = table.entries.len();
625 let _extended_count = table.extended_entries.len();
626 let current_compressed: std::collections::HashSet<u32> =
627 table.extended_entries.keys().copied().collect();
628
629 for (obj_num, entry) in table.entries {
630 if !current_compressed.contains(&obj_num)
635 && !merged_table.extended_entries.contains_key(&obj_num)
636 {
637 merged_table.entries.entry(obj_num).or_insert(entry);
638 }
639 }
640 for (obj_num, ext_entry) in table.extended_entries {
641 if !merged_table.entries.contains_key(&obj_num) {
644 merged_table
645 .extended_entries
646 .entry(obj_num)
647 .or_insert(ext_entry);
648 }
649 }
650
651 if merged_table.trailer.is_none() {
653 merged_table.trailer = table.trailer;
654 merged_table.xref_offset = table.xref_offset;
655 }
656
657 current_offset = prev_offset;
658 }
659
660 if options.lenient_syntax || options.collect_warnings {
664 if let Err(e) = Self::scan_and_fill_missing_objects(reader, &mut merged_table) {
672 tracing::debug!("scan_and_fill_missing_objects failed (non-fatal): {e}");
673 }
674 }
675
676 Ok(merged_table)
677 }
678
679 #[allow(dead_code)]
681 fn parse_primary<R: Read + Seek>(reader: &mut BufReader<R>) -> ParseResult<Self> {
682 Self::parse_primary_with_options(reader, &super::ParseOptions::default())
683 }
684
685 fn parse_primary_with_options<R: Read + Seek>(
691 reader: &mut BufReader<R>,
692 options: &super::ParseOptions,
693 ) -> ParseResult<Self> {
694 let mut table = Self::new();
695
696 let xref_offset = reader.stream_position()?;
700 table.xref_offset = xref_offset;
701
702 let mut line = String::new();
705 let pos = reader.stream_position()?;
706 read_pdf_line(reader, &mut line)?;
707
708 if line.trim() == "xref" {
709 Self::parse_traditional_xref_with_options(reader, &mut table, options)?;
711 } else {
712 tracing::debug!(
713 "Not a traditional xref, checking for xref stream. Line: {:?}",
714 line.trim()
715 );
716
717 reader.seek(SeekFrom::Start(pos))?;
719
720 let mut lexer = super::lexer::Lexer::new_with_options(&mut *reader, options.clone());
722
723 let obj_num = match lexer.next_token()? {
725 super::lexer::Token::Integer(n) => n as u32,
726 _ => return Err(ParseError::InvalidXRef),
727 };
728
729 tracing::debug!("Found object {obj_num} at xref position");
730
731 let _gen_num = match lexer.next_token()? {
732 super::lexer::Token::Integer(n) => n as u16,
733 _ => return Err(ParseError::InvalidXRef),
734 };
735
736 match lexer.next_token()? {
737 super::lexer::Token::Obj => {}
738 _ => return Err(ParseError::InvalidXRef),
739 };
740
741 let obj = super::objects::PdfObject::parse_with_options(&mut lexer, options)?;
743
744 if let Some(stream) = obj.as_stream() {
745 if stream
747 .dict
748 .get("Type")
749 .and_then(|o| o.as_name())
750 .map(|n| n.as_str())
751 == Some("XRef")
752 {
753 tracing::debug!("Parsing XRef stream");
754
755 let decoded_data = match stream.decode(options) {
762 Ok(data) => data,
763 Err(e) => {
764 tracing::warn!(
765 "XRef stream decode failed: {e:?}, triggering recovery mode"
766 );
767 return Err(e);
768 }
769 };
770
771 let xref_stream_parser = xref_stream::XRefStream::parse(
773 &mut *reader,
774 stream.dict.clone(),
775 decoded_data,
776 options,
777 )?;
778
779 let entries = xref_stream_parser.to_xref_entries()?;
781 tracing::debug!("XRef stream parsed, found {} entries", entries.len());
782
783 for (obj_num, entry) in entries {
785 match entry {
786 xref_stream::XRefEntry::Free {
787 next_free_object,
788 generation,
789 } => {
790 table.entries.insert(
791 obj_num,
792 XRefEntry {
793 offset: next_free_object as u64,
794 generation,
795 in_use: false,
796 },
797 );
798 }
799 xref_stream::XRefEntry::InUse { offset, generation } => {
800 table.entries.insert(
801 obj_num,
802 XRefEntry {
803 offset,
804 generation,
805 in_use: true,
806 },
807 );
808 }
809 xref_stream::XRefEntry::Compressed {
810 stream_object_number,
811 index_within_stream,
812 } => {
813 let ext_entry = XRefEntryExt {
815 basic: XRefEntry {
816 offset: 0,
817 generation: 0,
818 in_use: true,
819 },
820 compressed_info: Some((
821 stream_object_number,
822 index_within_stream,
823 )),
824 };
825 table.extended_entries.insert(obj_num, ext_entry);
826 table.entries.insert(
827 obj_num,
828 XRefEntry {
829 offset: 0,
830 generation: 0,
831 in_use: true,
832 },
833 );
834 }
835 }
836 }
837
838 table.trailer = Some(xref_stream_parser.trailer_dict().clone());
840 } else {
841 return Err(ParseError::InvalidXRef);
842 }
843 } else {
844 return Err(ParseError::InvalidXRef);
845 }
846 }
847
848 Ok(table)
849 }
850
851 #[allow(dead_code)]
853 fn parse_traditional_xref<R: Read + Seek>(
854 reader: &mut BufReader<R>,
855 table: &mut XRefTable,
856 ) -> ParseResult<()> {
857 Self::parse_traditional_xref_with_options(reader, table, &super::ParseOptions::default())
858 }
859
860 fn parse_traditional_xref_with_options<R: Read + Seek>(
862 reader: &mut BufReader<R>,
863 table: &mut XRefTable,
864 options: &super::ParseOptions,
865 ) -> ParseResult<()> {
866 let mut line = String::new();
867 let mut trailer_dict_offset: Option<u64> = None;
868
869 loop {
872 line.clear();
873 let line_start_pos = reader.stream_position()?;
874 read_pdf_line(reader, &mut line)?;
875 let trimmed_line = line.trim();
876
877 if trimmed_line.is_empty() || trimmed_line.starts_with('%') {
879 continue;
880 }
881
882 if trimmed_line == "trailer" {
886 break;
888 }
889 if let Some(dict_pos) = trimmed_line.find("<<") {
890 if trimmed_line.starts_with("trailer") {
891 let trailer_keyword_start =
894 trimmed_line.as_ptr() as usize - line.as_ptr() as usize;
895 trailer_dict_offset =
896 Some(line_start_pos + (trailer_keyword_start + dict_pos) as u64);
897 break;
898 }
899 }
900
901 if trimmed_line.starts_with("<<") {
903 tracing::warn!(" Found trailer dictionary without 'trailer' keyword");
904 trailer_dict_offset = Some(line_start_pos);
906 break;
907 }
908
909 let parts: Vec<&str> = trimmed_line.split_whitespace().collect();
911 if parts.len() != 2 {
912 return Err(ParseError::InvalidXRef);
914 }
915
916 let first_obj_num = parts[0]
917 .parse::<u32>()
918 .map_err(|_| ParseError::InvalidXRef)?;
919 let count = parts[1]
920 .parse::<u32>()
921 .map_err(|_| ParseError::InvalidXRef)?;
922
923 let mut entries_parsed = 0;
926 let mut i = 0;
927 while i < count {
928 line.clear();
929 let bytes_read = read_pdf_line(reader, &mut line)?;
930 let trimmed = line.trim();
931
932 if trimmed.starts_with('%') {
934 continue;
935 }
936
937 if bytes_read == 0 || trimmed == "trailer" {
939 tracing::debug!(
940 "Warning: XRef subsection incomplete - expected {count} entries but found only {entries_parsed}"
941 );
942 if line.trim() == "trailer" {
944 break;
946 }
947 break;
948 }
949
950 match Self::parse_xref_entry(&line) {
951 Ok(entry) => {
952 table.entries.insert(first_obj_num + i, entry);
953 entries_parsed += 1;
954 }
955 Err(_) => {
956 tracing::debug!(
957 "Warning: Invalid XRef entry at position {}: {:?}",
958 i,
959 line.trim()
960 );
961 }
963 }
964 i += 1;
965 }
966 }
968
969 if let Some(offset) = trailer_dict_offset {
973 reader.seek(SeekFrom::Start(offset))?;
974 }
975 let mut lexer = super::lexer::Lexer::new_with_options(reader, options.clone());
976 let trailer_obj = super::objects::PdfObject::parse_with_options(&mut lexer, options)?;
977 table.trailer = trailer_obj.as_dict().cloned();
980
981 if let Some(trailer) = &table.trailer {
983 if let Some(size_obj) = trailer.get("Size") {
984 if let Some(expected_size) = size_obj.as_integer() {
985 if let Some(max_obj_num) = table.entries.keys().max() {
988 let max_expected = (*max_obj_num + 1) as i64;
989 if max_expected > expected_size {
990 tracing::debug!(
991 "Warning: XRef table has object {} but trailer Size is only {}",
992 max_obj_num,
993 expected_size
994 );
995 return Err(ParseError::InvalidXRef);
997 }
998 }
999 }
1000 }
1001 }
1002
1003 Ok(())
1007 }
1008
1009 #[allow(dead_code)]
1016 fn find_linearized_xref<R: Read + Seek>(reader: &mut BufReader<R>) -> ParseResult<u64> {
1017 reader.seek(SeekFrom::Start(0))?;
1019 let mut header = String::new();
1020 reader.read_line(&mut header)?;
1021
1022 if !header.starts_with("%PDF-") {
1023 return Err(ParseError::InvalidHeader);
1024 }
1025
1026 let mut line = String::new();
1028 reader.read_line(&mut line)?;
1029
1030 let pos = reader.stream_position()?;
1033 let mut buffer = vec![0u8; 1024];
1034 let bytes_read = reader.read(&mut buffer)?;
1035 buffer.truncate(bytes_read);
1036
1037 tracing::debug!(
1041 "Checking for linearized PDF, first 100 bytes: {:?}",
1042 String::from_utf8_lossy(&buffer[..buffer.len().min(100)])
1043 );
1044
1045 if find_byte_pattern(&buffer, b"/Linearized").is_some() {
1047 if let Some(xref_pos) = find_byte_pattern(&buffer, b"xref") {
1053 return Ok(pos + xref_pos as u64);
1054 }
1055
1056 if find_byte_pattern(&buffer, b"/Type/XRef").is_some()
1058 || find_byte_pattern(&buffer, b"/Type /XRef").is_some()
1059 {
1060 if let Some(obj_pos) = find_byte_pattern(&buffer, b" obj") {
1063 let search_from = obj_pos + 4;
1065 if search_from < buffer.len() {
1066 let after_first_obj = &buffer[search_from..];
1067 if let Some(next_obj) = find_byte_pattern(after_first_obj, b" obj") {
1068 let second_obj_start =
1070 pos + (search_from + next_obj).saturating_sub(10) as u64;
1071 return Ok(second_obj_start);
1072 }
1073 }
1074 }
1075 }
1076 }
1077
1078 Err(ParseError::InvalidXRef)
1079 }
1080
1081 fn find_xref_offset<R: Read + Seek>(reader: &mut BufReader<R>) -> ParseResult<u64> {
1083 reader.seek(SeekFrom::End(0))?;
1085 let file_size = reader.stream_position()?;
1086
1087 let read_size = std::cmp::min(1024, file_size);
1089 reader.seek(SeekFrom::End(-(read_size as i64)))?;
1090
1091 let mut buffer = vec![0u8; read_size as usize];
1092 reader.read_exact(&mut buffer)?;
1093
1094 let content = String::from_utf8_lossy(&buffer);
1096
1097 let debug_content = content.chars().take(200).collect::<String>();
1099 tracing::debug!("XRef search in last {read_size} bytes: {debug_content:?}");
1100
1101 let mut lines = content.pdf_lines();
1102
1103 let mut last_offset = None;
1110 while let Some(line) = lines.next() {
1111 if line.trim() == "startxref" {
1112 if let Some(offset_line) = lines.next() {
1114 if let Ok(offset) = offset_line.trim().parse::<u64>() {
1115 last_offset = Some(offset);
1116 }
1117 }
1118 }
1119 }
1120
1121 last_offset.ok_or(ParseError::InvalidXRef)
1122 }
1123
1124 fn scan_and_fill_missing_objects<R: Read + Seek>(
1126 reader: &mut BufReader<R>,
1127 table: &mut Self,
1128 ) -> ParseResult<()> {
1129 let scanned = scan_object_headers(reader)?;
1133 table.add_headers_latest_wins(&scanned, true);
1134
1135 Ok(())
1136 }
1137
1138 #[allow(dead_code)]
1140 fn parse_with_recovery<R: Read + Seek>(reader: &mut BufReader<R>) -> ParseResult<Self> {
1141 Self::parse_with_recovery_options(reader, &super::ParseOptions::default())
1142 }
1143
1144 fn parse_with_recovery_options<R: Read + Seek>(
1146 reader: &mut BufReader<R>,
1147 options: &super::ParseOptions,
1148 ) -> ParseResult<Self> {
1149 const ROOT_TAIL: usize = 256 * 1024;
1153 const CATALOG_TAIL: usize = 100 * 1024;
1154
1155 let mut table = Self::new();
1156
1157 let headers = scan_object_headers(reader)?;
1161 table.add_headers_latest_wins(&headers, false);
1162
1163 if table.entries.is_empty() {
1164 return Err(ParseError::InvalidXRef);
1165 }
1166 tracing::debug!("XRef recovery: found {} objects", table.len());
1167
1168 let (_, root_tail) = read_tail(reader, ROOT_TAIL)?;
1171 let root_tail_str = String::from_utf8_lossy(&root_tail);
1172 let xref_root_candidate = extract_root_from_xref_stream(&root_tail_str);
1173
1174 let mut trailer = super::objects::PdfDictionary::new();
1176 trailer.insert(
1177 "Size".to_string(),
1178 super::objects::PdfObject::Integer(table.len() as i64),
1179 );
1180
1181 let (encrypt, id) = extract_encrypt_and_id_from_trailer(&root_tail, options);
1187 if let Some(encrypt) = encrypt {
1188 trailer.insert("Encrypt".to_string(), encrypt);
1189 }
1190 if let Some(id) = id {
1191 trailer.insert("ID".to_string(), id);
1192 }
1193
1194 let mut catalog_candidate = None;
1196
1197 if let Some(xref_root) = xref_root_candidate {
1199 if table.entries.contains_key(&xref_root) {
1200 catalog_candidate = Some(xref_root);
1201 tracing::debug!("Using Root {} from XRef stream as catalog", xref_root);
1202 } else {
1203 tracing::debug!(
1204 "Warning: XRef Root {} not found in object table, searching manually",
1205 xref_root
1206 );
1207 }
1208 }
1209
1210 if catalog_candidate.is_none() {
1212 catalog_candidate = find_catalog_by_content(reader, &table)?;
1213 }
1214
1215 if catalog_candidate.is_none() {
1217 for obj_num in [1, 2, 3, 4, 5] {
1218 let offset = match table.entries.get(&obj_num) {
1219 Some(entry) if entry.in_use => entry.offset,
1220 _ => continue,
1221 };
1222 if let Some(content) = read_object_content(reader, obj_num, offset)? {
1223 if content.contains("/Type/Sig") || content.contains("/Type /Sig") {
1225 tracing::debug!("Skipping object {} (Type: Sig)", obj_num);
1226 continue;
1227 }
1228 if content.contains("/Type/Catalog")
1229 || content.contains("/Type /Catalog")
1230 || content.contains("/Pages")
1231 {
1232 catalog_candidate = Some(obj_num);
1233 tracing::debug!(
1234 "Using fallback catalog candidate: object {} (validated)",
1235 obj_num
1236 );
1237 break;
1238 }
1239 }
1240 }
1241 }
1242
1243 if catalog_candidate.is_none() && !table.entries.is_empty() {
1245 tracing::debug!(
1246 "Last resort: Scanning all {} objects for any with /Pages or /Catalog",
1247 table.entries.len()
1248 );
1249
1250 let mut obj_numbers: Vec<u32> = table.entries.keys().copied().collect();
1251 obj_numbers.sort_unstable();
1252
1253 for obj_num in obj_numbers {
1254 let offset = match table.entries.get(&obj_num) {
1255 Some(entry) if entry.in_use => entry.offset,
1256 _ => continue,
1257 };
1258 if let Some(content) = read_object_content(reader, obj_num, offset)? {
1259 if content.contains("/Type/Sig") || content.contains("/Type /Sig") {
1260 continue;
1261 }
1262 if content.contains("/Type/Catalog") || content.contains("/Type /Catalog") {
1263 catalog_candidate = Some(obj_num);
1264 tracing::debug!(
1265 "Last resort: Found catalog at object {} (/Type/Catalog)",
1266 obj_num
1267 );
1268 break;
1269 } else if content.contains("/Pages") {
1270 catalog_candidate = Some(obj_num);
1271 tracing::debug!(
1272 "Last resort: Found catalog at object {} (has /Pages)",
1273 obj_num
1274 );
1275 break;
1276 }
1277 }
1278 }
1279
1280 if catalog_candidate.is_none() {
1283 tracing::debug!("Extreme last resort: Scanning last 100KB for /Type/Catalog");
1284
1285 let (_, search_buffer) = read_tail(reader, CATALOG_TAIL)?;
1286 if let Some(catalog_pos) = rfind_byte_pattern(&search_buffer, b"/Type/Catalog") {
1287 let local_search_start = catalog_pos.saturating_sub(200);
1288 let search_area = &search_buffer[local_search_start..catalog_pos];
1289
1290 if let Some(obj_pattern_pos) = rfind_byte_pattern(search_area, b" 0 obj") {
1291 let before_obj = &search_area[..obj_pattern_pos];
1292 let before_obj_str = String::from_utf8_lossy(before_obj);
1293 let trimmed = before_obj_str.trim_end();
1294
1295 if let Some((digit_start, ch)) = trimmed
1296 .char_indices()
1297 .rev()
1298 .find(|(_, c)| !c.is_ascii_digit())
1299 {
1300 let num_str = trimmed[digit_start + ch.len_utf8()..].trim();
1301 if !num_str.is_empty() {
1302 if let Ok(obj_num) = num_str.parse::<u32>() {
1303 tracing::debug!(
1304 "Extreme last resort: Found /Type/Catalog at object {}",
1305 obj_num
1306 );
1307 catalog_candidate = Some(obj_num);
1308 }
1309 }
1310 } else if let Ok(obj_num) = trimmed.trim().parse::<u32>() {
1311 tracing::debug!(
1312 "Extreme last resort: Found /Type/Catalog at object {}",
1313 obj_num
1314 );
1315 catalog_candidate = Some(obj_num);
1316 }
1317 }
1318 } else {
1319 tracing::debug!("Extreme last resort: No /Type/Catalog found in last 100KB");
1320 }
1321 }
1322
1323 if catalog_candidate.is_none() {
1325 tracing::warn!(" Could not find any catalog object, using first non-signature object as absolute last resort");
1326 let mut obj_numbers: Vec<u32> = table.entries.keys().copied().collect();
1327 obj_numbers.sort_unstable();
1328 for obj_num in obj_numbers {
1329 let offset = match table.entries.get(&obj_num) {
1330 Some(entry) => entry.offset,
1331 None => continue,
1332 };
1333 if let Some(content) = read_object_content(reader, obj_num, offset)? {
1334 if !content.contains("/Type/Sig") && !content.contains("/Type /Sig") {
1335 catalog_candidate = Some(obj_num);
1336 tracing::debug!("Using object {} as absolute last resort", obj_num);
1337 break;
1338 }
1339 }
1340 }
1341 }
1342 }
1343
1344 if let Some(root_obj) = catalog_candidate {
1345 trailer.insert(
1346 "Root".to_string(),
1347 super::objects::PdfObject::Reference(root_obj, 0),
1348 );
1349 }
1350
1351 table.set_trailer(trailer);
1352
1353 Ok(table)
1354 }
1355
1356 #[allow(dead_code)]
1362 fn validate_offset<R: Read + Seek>(reader: &mut BufReader<R>, offset: u64) -> ParseResult<()> {
1363 let file_size = reader.seek(SeekFrom::End(0))?;
1365
1366 if offset >= file_size {
1367 #[cfg(debug_assertions)]
1368 tracing::warn!(" XRef offset {offset} exceeds file size {file_size}");
1369 return Err(ParseError::InvalidXRef);
1370 }
1371
1372 reader.seek(SeekFrom::Start(offset))?;
1374 let mut peek = [0u8; 20];
1375 let read_bytes = reader.read(&mut peek)?;
1376
1377 if read_bytes == 0 {
1378 #[cfg(debug_assertions)]
1379 tracing::warn!(" XRef offset {offset} points to EOF");
1380 return Err(ParseError::InvalidXRef);
1381 }
1382
1383 let peek_slice = &peek[..read_bytes];
1386 let starts_with_xref = peek_slice.len() >= 4 && &peek_slice[..4] == b"xref";
1387 let starts_with_digit = peek_slice.first().map_or(false, |&b| b.is_ascii_digit());
1388
1389 if !starts_with_xref && !starts_with_digit {
1390 #[cfg(debug_assertions)]
1391 {
1392 let debug_len = std::cmp::min(10, read_bytes);
1393 let debug_content = String::from_utf8_lossy(&peek[..debug_len]);
1394 tracing::debug!(
1395 "Warning: XRef offset {} does not point to valid XRef content: {:?}",
1396 offset,
1397 debug_content
1398 );
1399 }
1400 }
1402
1403 Ok(())
1404 }
1405
1406 fn parse_xref_entry(line: &str) -> ParseResult<XRefEntry> {
1408 let line = line.trim();
1409
1410 if line.len() >= 18 {
1412 if let Ok(entry) = Self::parse_xref_entry_standard(line) {
1413 return Ok(entry);
1414 }
1415 }
1416
1417 Self::parse_xref_entry_flexible(line)
1419 }
1420
1421 fn parse_xref_entry_standard(line: &str) -> ParseResult<XRefEntry> {
1423 if line.len() < 18 {
1426 return Err(ParseError::InvalidXRef);
1427 }
1428
1429 let offset_str = &line[0..10];
1430 let gen_str = &line[11..16];
1431 let flag = line.chars().nth(17);
1432
1433 let offset = offset_str
1434 .trim()
1435 .parse::<u64>()
1436 .map_err(|_| ParseError::InvalidXRef)?;
1437 let generation = gen_str
1438 .trim()
1439 .parse::<u16>()
1440 .map_err(|_| ParseError::InvalidXRef)?;
1441
1442 let in_use = match flag {
1443 Some('n') => true,
1444 Some('f') => false,
1445 _ => return Err(ParseError::InvalidXRef),
1446 };
1447
1448 Ok(XRefEntry {
1449 offset,
1450 generation,
1451 in_use,
1452 })
1453 }
1454
1455 fn parse_xref_entry_flexible(line: &str) -> ParseResult<XRefEntry> {
1457 let parts: Vec<&str> = line.split_whitespace().collect();
1465
1466 if parts.is_empty() {
1467 return Err(ParseError::InvalidXRef);
1468 }
1469
1470 let offset = parts[0]
1472 .parse::<u64>()
1473 .map_err(|_| ParseError::InvalidXRef)?;
1474
1475 let (generation, flag_from_gen) = if parts.len() >= 2 {
1477 let gen_part = parts[1];
1478 if gen_part == "n" || gen_part == "f" {
1480 (0, gen_part.chars().next())
1482 } else if gen_part.ends_with('n') || gen_part.ends_with('f') {
1483 let flag_char = gen_part.chars().last().ok_or(ParseError::InvalidXRef)?;
1485 let gen_str = &gen_part[..gen_part.len() - 1];
1486 if gen_str.is_empty() {
1487 (0, Some(flag_char))
1489 } else {
1490 let gen = gen_str
1491 .parse::<u16>()
1492 .map_err(|_| ParseError::InvalidXRef)?;
1493 (gen, Some(flag_char))
1494 }
1495 } else {
1496 let gen = gen_part
1498 .parse::<u16>()
1499 .map_err(|_| ParseError::InvalidXRef)?;
1500 (gen, None)
1501 }
1502 } else {
1503 (0, None)
1504 };
1505
1506 let in_use = if let Some(flag_char) = flag_from_gen {
1508 match flag_char {
1510 'n' => true,
1511 'f' => false,
1512 _ => true, }
1514 } else if parts.len() >= 3 {
1515 match parts[2].chars().next() {
1517 Some('n') => true,
1518 Some('f') => false,
1519 _ => {
1520 #[cfg(debug_assertions)]
1522 tracing::warn!(" Invalid xref flag '{}', assuming 'n'", parts[2]);
1523 true
1524 }
1525 }
1526 } else {
1527 true
1529 };
1530
1531 Ok(XRefEntry {
1532 offset,
1533 generation,
1534 in_use,
1535 })
1536 }
1537
1538 pub fn get_entry(&self, obj_num: u32) -> Option<&XRefEntry> {
1540 self.entries.get(&obj_num)
1541 }
1542
1543 pub fn get_entry_mut(&mut self, obj_num: u32) -> Option<&mut XRefEntry> {
1545 self.entries.get_mut(&obj_num)
1546 }
1547
1548 pub fn trailer(&self) -> Option<&super::objects::PdfDictionary> {
1550 self.trailer.as_ref()
1551 }
1552
1553 pub fn xref_offset(&self) -> u64 {
1555 self.xref_offset
1556 }
1557
1558 pub(crate) fn revisions_oldest_first(&self) -> Vec<XRefRevision> {
1559 self.revisions.iter().rev().cloned().collect()
1560 }
1561
1562 pub fn len(&self) -> usize {
1564 self.entries.len()
1565 }
1566
1567 pub fn is_empty(&self) -> bool {
1569 self.entries.is_empty()
1570 }
1571
1572 pub fn iter(&self) -> impl Iterator<Item = (&u32, &XRefEntry)> {
1574 self.entries.iter()
1575 }
1576
1577 pub fn get_extended_entry(&self, obj_num: u32) -> Option<&XRefEntryExt> {
1579 self.extended_entries.get(&obj_num)
1580 }
1581
1582 pub fn is_compressed(&self, obj_num: u32) -> bool {
1584 self.extended_entries
1585 .get(&obj_num)
1586 .map(|e| e.compressed_info.is_some())
1587 .unwrap_or(false)
1588 }
1589
1590 pub fn add_entry(&mut self, obj_num: u32, entry: XRefEntry) {
1592 self.entries.insert(obj_num, entry);
1593 }
1594
1595 fn add_headers_latest_wins(&mut self, headers: &[ObjHeader], check_extended: bool) {
1602 let mut latest: HashMap<u32, &ObjHeader> = HashMap::new();
1603 for h in headers {
1604 latest.insert(h.obj_num, h); }
1606 for (obj_num, h) in latest {
1607 let occupied = self.entries.contains_key(&obj_num)
1608 || (check_extended && self.extended_entries.contains_key(&obj_num));
1609 if !occupied {
1610 self.add_entry(
1611 obj_num,
1612 XRefEntry {
1613 offset: h.offset,
1614 generation: h.generation,
1615 in_use: true,
1616 },
1617 );
1618 }
1619 }
1620 }
1621
1622 pub fn set_trailer(&mut self, trailer: super::objects::PdfDictionary) {
1624 self.trailer = Some(trailer);
1625 }
1626
1627 pub fn add_extended_entry(&mut self, obj_num: u32, entry: XRefEntryExt) {
1629 self.extended_entries.insert(obj_num, entry);
1630 }
1631}
1632
1633#[derive(Debug, Clone)]
1636pub struct XRefStream {
1637 stream: super::objects::PdfStream,
1639 entries: HashMap<u32, XRefEntry>,
1641 extended_entries: HashMap<u32, XRefEntryExt>,
1643}
1644
1645impl XRefStream {
1646 pub fn parse(stream: super::objects::PdfStream) -> ParseResult<Self> {
1648 let mut xref_stream = Self {
1649 stream,
1650 entries: HashMap::new(),
1651 extended_entries: HashMap::new(),
1652 };
1653
1654 xref_stream.decode_entries()?;
1655 Ok(xref_stream)
1656 }
1657
1658 fn decode_entries(&mut self) -> ParseResult<()> {
1660 let dict = &self.stream.dict;
1662
1663 let size = dict
1665 .get("Size")
1666 .and_then(|obj| obj.as_integer())
1667 .ok_or_else(|| ParseError::MissingKey("Size".to_string()))?;
1668
1669 let index = match dict.get("Index") {
1671 Some(obj) => {
1672 let array = obj.as_array().ok_or_else(|| ParseError::SyntaxError {
1673 position: 0,
1674 message: "Index must be an array".to_string(),
1675 })?;
1676
1677 let mut pairs = Vec::new();
1679 for chunk in array.0.chunks(2) {
1680 if chunk.len() != 2 {
1681 return Err(ParseError::SyntaxError {
1682 position: 0,
1683 message: "Index array must have even number of elements".to_string(),
1684 });
1685 }
1686 let first = chunk[0]
1687 .as_integer()
1688 .ok_or_else(|| ParseError::SyntaxError {
1689 position: 0,
1690 message: "Index values must be integers".to_string(),
1691 })? as u32;
1692 let count = chunk[1]
1693 .as_integer()
1694 .ok_or_else(|| ParseError::SyntaxError {
1695 position: 0,
1696 message: "Index values must be integers".to_string(),
1697 })? as u32;
1698 pairs.push((first, count));
1699 }
1700 pairs
1701 }
1702 None => {
1703 vec![(0, size as u32)]
1705 }
1706 };
1707
1708 let w_array = dict
1710 .get("W")
1711 .and_then(|obj| obj.as_array())
1712 .ok_or_else(|| ParseError::MissingKey("W".to_string()))?;
1713
1714 if w_array.len() != 3 {
1715 return Err(ParseError::SyntaxError {
1716 position: 0,
1717 message: "W array must have exactly 3 elements".to_string(),
1718 });
1719 }
1720
1721 let w: Vec<usize> = w_array
1722 .0
1723 .iter()
1724 .map(|obj| {
1725 obj.as_integer()
1726 .ok_or_else(|| ParseError::SyntaxError {
1727 position: 0,
1728 message: "W values must be integers".to_string(),
1729 })
1730 .map(|i| i as usize)
1731 })
1732 .collect::<ParseResult<Vec<_>>>()?;
1733
1734 let data = self.stream.decode(&ParseOptions::default())?;
1736 let mut offset = 0;
1737
1738 for (first_obj_num, count) in index {
1740 for i in 0..count {
1741 if offset + w[0] + w[1] + w[2] > data.len() {
1742 return Err(ParseError::SyntaxError {
1743 position: 0,
1744 message: "Xref stream data truncated".to_string(),
1745 });
1746 }
1747
1748 let field1 = Self::read_field(&data[offset..], w[0]);
1750 offset += w[0];
1751
1752 let field2 = Self::read_field(&data[offset..], w[1]);
1753 offset += w[1];
1754
1755 let field3 = Self::read_field(&data[offset..], w[2]);
1756 offset += w[2];
1757
1758 let entry_info =
1760 XRefEntryInfo::new(XRefEntryType::from_value(field1), field2, field3);
1761
1762 let entry = match entry_info.entry_type {
1764 XRefEntryType::Free => XRefEntry {
1765 offset: entry_info.field2,
1766 generation: entry_info.field3 as u16,
1767 in_use: false,
1768 },
1769 XRefEntryType::Uncompressed => XRefEntry {
1770 offset: entry_info.field2,
1771 generation: entry_info.field3 as u16,
1772 in_use: true,
1773 },
1774 XRefEntryType::Compressed => {
1775 let ext_entry = XRefEntryExt {
1777 basic: XRefEntry {
1778 offset: 0,
1779 generation: 0,
1780 in_use: true,
1781 },
1782 compressed_info: entry_info.get_compressed_info(),
1783 };
1784 self.extended_entries
1785 .insert(first_obj_num + i, ext_entry.clone());
1786 ext_entry.basic
1787 }
1788 XRefEntryType::Custom(_type_num) => {
1789 #[cfg(debug_assertions)]
1792 tracing::debug!(
1793 "Note: Custom xref entry type {} for object {} (treating as in-use)",
1794 _type_num,
1795 first_obj_num + i
1796 );
1797
1798 let ext_entry = XRefEntryExt {
1800 basic: XRefEntry {
1801 offset: entry_info.field2,
1802 generation: entry_info.field3 as u16,
1803 in_use: entry_info.entry_type.is_in_use(),
1804 },
1805 compressed_info: None,
1806 };
1807 self.extended_entries
1808 .insert(first_obj_num + i, ext_entry.clone());
1809 ext_entry.basic
1810 }
1811 };
1812
1813 self.entries.insert(first_obj_num + i, entry);
1814 }
1815 }
1816
1817 Ok(())
1818 }
1819
1820 fn read_field(data: &[u8], width: usize) -> u64 {
1822 let mut value = 0u64;
1823 for i in 0..width {
1824 if i < data.len() {
1825 value = (value << 8) | (data[i] as u64);
1826 }
1827 }
1828 value
1829 }
1830
1831 pub fn get_entry(&self, obj_num: u32) -> Option<&XRefEntry> {
1833 self.entries.get(&obj_num)
1834 }
1835
1836 pub fn trailer(&self) -> &super::objects::PdfDictionary {
1838 &self.stream.dict
1839 }
1840}
1841
1842#[cfg(test)]
1843mod tests {
1844 use super::*;
1845
1846 use crate::parser::objects::{PdfDictionary, PdfObject};
1847 use std::io::Cursor;
1848
1849 #[test]
1852 fn test_scan_object_headers_finds_simple_headers() {
1853 let mut buf = Vec::new();
1854 buf.extend_from_slice(b"%PDF-1.7\n");
1855 let off1 = buf.len() as u64;
1856 buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
1857 let off2 = buf.len() as u64;
1858 buf.extend_from_slice(b"2 0 obj\n<< /Type /Pages >>\nendobj\n");
1859 let off10 = buf.len() as u64;
1860 buf.extend_from_slice(b"10 0 obj\n<< /Length 0 >>\nendobj\n");
1861
1862 let mut cursor = Cursor::new(buf);
1863 let headers = scan_object_headers(&mut cursor).unwrap();
1864
1865 assert_eq!(
1866 headers,
1867 vec![
1868 ObjHeader {
1869 obj_num: 1,
1870 generation: 0,
1871 offset: off1
1872 },
1873 ObjHeader {
1874 obj_num: 2,
1875 generation: 0,
1876 offset: off2
1877 },
1878 ObjHeader {
1879 obj_num: 10,
1880 generation: 0,
1881 offset: off10
1882 },
1883 ]
1884 );
1885 }
1886
1887 #[test]
1888 fn test_scan_object_headers_chunk_invariant_across_boundaries() {
1889 let mut buf = Vec::new();
1892 buf.extend_from_slice(b"%PDF-1.7\n");
1893 let mut expected: Vec<(u32, u64)> = Vec::new();
1894 for i in 1..=50u32 {
1895 for _ in 0..(i as usize % 7) {
1896 buf.push(b' ');
1897 }
1898 buf.push(b'\n');
1899 expected.push((i, buf.len() as u64));
1900 buf.extend_from_slice(format!("{i} 0 obj\n<< /N {i} >>\nendobj\n").as_bytes());
1901 }
1902
1903 let reference =
1905 scan_object_headers_chunked(&mut Cursor::new(buf.clone()), buf.len().max(1)).unwrap();
1906
1907 let got: Vec<(u32, u64)> = reference.iter().map(|h| (h.obj_num, h.offset)).collect();
1908 assert_eq!(
1909 got, expected,
1910 "reference scan disagrees with hand-computed offsets"
1911 );
1912
1913 for cs in [1usize, 2, 3, 7, 13, 16, 64, 256] {
1914 let chunked = scan_object_headers_chunked(&mut Cursor::new(buf.clone()), cs).unwrap();
1915 assert_eq!(chunked, reference, "scan mismatch at chunk_size={cs}");
1916 }
1917 }
1918
1919 #[test]
1920 fn test_scan_object_headers_ignores_endobj_keyword() {
1921 let mut buf = Vec::new();
1923 buf.extend_from_slice(b"%PDF\n");
1924 let off = buf.len() as u64;
1925 buf.extend_from_slice(b"7 0 obj\n<< >>\nendobj\nendobj\n");
1926
1927 let headers = scan_object_headers(&mut Cursor::new(buf)).unwrap();
1928 assert_eq!(
1929 headers,
1930 vec![ObjHeader {
1931 obj_num: 7,
1932 generation: 0,
1933 offset: off
1934 }]
1935 );
1936 }
1937
1938 #[test]
1939 fn test_scan_object_headers_carry_truncation_no_newline_run() {
1940 let mut buf = Vec::new();
1945 buf.extend_from_slice(b"%PDF-1.7\n");
1946
1947 let filler: Vec<u8> = (0..2000u32)
1949 .map(|i| if i % 5 == 0 { b' ' } else { b'7' })
1950 .collect();
1951
1952 buf.extend_from_slice(&filler);
1954 buf.push(b'\n');
1955 let off_a = buf.len() as u64;
1956 buf.extend_from_slice(b"5 0 obj\n<< >>\nendobj\n");
1957
1958 buf.extend_from_slice(&filler);
1961 buf.extend_from_slice(b"7 0 obj\n<< >>\nendobj\n");
1962
1963 let reference =
1964 scan_object_headers_chunked(&mut Cursor::new(buf.clone()), buf.len().max(1)).unwrap();
1965
1966 for cs in [16usize, 64, 256] {
1968 let chunked = scan_object_headers_chunked(&mut Cursor::new(buf.clone()), cs).unwrap();
1969 assert_eq!(
1970 chunked, reference,
1971 "carry-truncation mismatch at chunk_size={cs}"
1972 );
1973 }
1974
1975 assert!(reference
1978 .iter()
1979 .any(|h| h.obj_num == 5 && h.offset == off_a));
1980 assert!(!reference.iter().any(|h| h.obj_num == 7));
1981 }
1982
1983 #[test]
1984 fn test_scan_object_headers_empty_input() {
1985 let headers = scan_object_headers(&mut Cursor::new(Vec::new())).unwrap();
1986 assert!(headers.is_empty());
1987 }
1988
1989 #[test]
1990 fn test_scan_object_headers_reads_in_bounded_chunks() {
1991 struct MaxReadReader<R> {
1993 inner: R,
1994 max_read: usize,
1995 }
1996 impl<R: Read> Read for MaxReadReader<R> {
1997 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1998 self.max_read = self.max_read.max(buf.len());
1999 self.inner.read(buf)
2000 }
2001 }
2002 impl<R: Seek> Seek for MaxReadReader<R> {
2003 fn seek(&mut self, p: SeekFrom) -> std::io::Result<u64> {
2004 self.inner.seek(p)
2005 }
2006 }
2007
2008 let mut buf = Vec::new();
2009 buf.extend_from_slice(b"%PDF\n");
2010 for i in 1..=2000u32 {
2011 buf.extend_from_slice(format!("{i} 0 obj\n<< >>\nendobj\n").as_bytes());
2012 }
2013 let total = buf.len();
2014 assert!(
2015 total > 8192,
2016 "fixture must exceed the chunk size to be meaningful"
2017 );
2018
2019 let mut r = MaxReadReader {
2020 inner: Cursor::new(buf),
2021 max_read: 0,
2022 };
2023 let headers = scan_object_headers_chunked(&mut r, 4096).unwrap();
2024
2025 assert_eq!(headers.len(), 2000);
2026 assert_eq!(headers[0].obj_num, 1);
2027 assert_eq!(headers[1999].obj_num, 2000);
2028 assert!(
2031 r.max_read <= 4096,
2032 "scanner requested {} bytes in a single read (chunk=4096, file={total}); not bounded",
2033 r.max_read
2034 );
2035 }
2036
2037 #[test]
2038 fn test_read_object_window_bounded_and_correct() {
2039 struct MaxReadReader<R> {
2047 inner: R,
2048 max_read: usize,
2049 }
2050 impl<R: Read> Read for MaxReadReader<R> {
2051 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2052 self.max_read = self.max_read.max(buf.len());
2053 self.inner.read(buf)
2054 }
2055 }
2056 impl<R: Seek> Seek for MaxReadReader<R> {
2057 fn seek(&mut self, p: SeekFrom) -> std::io::Result<u64> {
2058 self.inner.seek(p)
2059 }
2060 }
2061
2062 let mut buf = Vec::new();
2063 buf.extend_from_slice(b"%PDF-1.7\n");
2064 let cat_off = buf.len() as u64;
2065 buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
2066 buf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
2067 buf.extend_from_slice(b"3 0 obj\n<< /Type /Page >>\nstream\n");
2070 buf.extend(std::iter::repeat(b'x').take(200 * 1024));
2071 buf.extend_from_slice(b"\nendstream\nendobj\n");
2072 let total = buf.len();
2073 assert!(
2074 total > 64 * 1024,
2075 "fixture must exceed the scan chunk size to be meaningful"
2076 );
2077
2078 let mut r = MaxReadReader {
2079 inner: Cursor::new(buf),
2080 max_read: 0,
2081 };
2082
2083 let (offset, window) = read_object_window(&mut r, 1, 64 * 1024)
2084 .unwrap()
2085 .expect("object 1 must be locatable by bounded scan");
2086
2087 assert_eq!(offset, cat_off, "header offset must be the real line start");
2089 assert!(
2090 find_byte_pattern(&window, b"/Type /Catalog").is_some(),
2091 "window must contain the catalog dict"
2092 );
2093 assert!(
2094 find_byte_pattern(&window, b"/Pages 2 0 R").is_some(),
2095 "window must contain the /Pages reference"
2096 );
2097 assert!(
2099 r.max_read <= 64 * 1024,
2100 "locate requested {} bytes in a single read (file={total}); not bounded",
2101 r.max_read
2102 );
2103 }
2104
2105 #[test]
2106 fn test_scan_page_object_refs_matches_compact_type_page() {
2107 let mut buf = Vec::new();
2111 buf.extend_from_slice(b"%PDF-1.7\n");
2112 buf.extend_from_slice(b"1 0 obj\n<< /Type/Catalog /Pages 2 0 R >>\nendobj\n");
2113 buf.extend_from_slice(b"2 0 obj\n<< /Type/Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
2114 buf.extend_from_slice(b"3 0 obj\n<< /Type/Page /Parent 2 0 R >>\nendobj\n");
2115
2116 let mut r = Cursor::new(buf);
2117 let pages = scan_page_object_refs(&mut r).unwrap();
2118 assert_eq!(
2119 pages,
2120 vec![(3, 0)],
2121 "compact /Type/Page must be detected and compact /Type/Pages excluded"
2122 );
2123 }
2124
2125 #[test]
2126 fn test_scan_and_fill_adds_missing_preserves_present() {
2127 let mut buf = Vec::new();
2128 buf.extend_from_slice(b"%PDF-1.7\n");
2129 let off1 = buf.len() as u64;
2130 buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
2131 buf.extend_from_slice(b"2 0 obj\n<< /Type /Pages >>\nendobj\n");
2132 let off3 = buf.len() as u64;
2133 buf.extend_from_slice(b"3 0 obj\n<< >>\nendobj\n");
2134
2135 let mut table = XRefTable::new();
2136 table.add_entry(
2138 2,
2139 XRefEntry {
2140 offset: 99999,
2141 generation: 0,
2142 in_use: true,
2143 },
2144 );
2145
2146 let mut reader = BufReader::new(Cursor::new(buf));
2147 XRefTable::scan_and_fill_missing_objects(&mut reader, &mut table).unwrap();
2148
2149 assert_eq!(table.get_entry(1).map(|e| e.offset), Some(off1));
2151 assert_eq!(table.get_entry(3).map(|e| e.offset), Some(off3));
2152 assert_eq!(table.get_entry(2).map(|e| e.offset), Some(99999));
2154 }
2155
2156 #[test]
2157 fn test_recovery_finds_objects_and_catalog_root() {
2158 let mut buf = Vec::new();
2159 buf.extend_from_slice(b"%PDF-1.7\n");
2160 let off1 = buf.len() as u64;
2161 buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
2162 let off2 = buf.len() as u64;
2163 buf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
2164 let off3 = buf.len() as u64;
2165 buf.extend_from_slice(b"3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\n");
2166
2167 let mut reader = BufReader::new(Cursor::new(buf));
2168 let table =
2169 XRefTable::parse_with_recovery_options(&mut reader, &ParseOptions::default()).unwrap();
2170
2171 assert_eq!(table.get_entry(1).map(|e| e.offset), Some(off1));
2172 assert_eq!(table.get_entry(2).map(|e| e.offset), Some(off2));
2173 assert_eq!(table.get_entry(3).map(|e| e.offset), Some(off3));
2174
2175 let root = table.trailer().and_then(|t| t.get("Root")).cloned();
2177 assert_eq!(root, Some(PdfObject::Reference(1, 0)));
2178 }
2179
2180 #[test]
2181 fn test_recovery_uses_root_from_xref_stream() {
2182 let mut buf = Vec::new();
2183 buf.extend_from_slice(b"%PDF-1.7\n");
2184 buf.extend_from_slice(b"5 0 obj\n<< /Type /Catalog /Pages 6 0 R >>\nendobj\n");
2185 buf.extend_from_slice(b"6 0 obj\n<< /Type /Pages /Count 0 >>\nendobj\n");
2186 buf.extend_from_slice(
2188 b"9 0 obj\n<< /Type /XRef /Root 5 0 R /Size 10 >>\nstream\n....\nendstream\nendobj\n",
2189 );
2190
2191 let mut reader = BufReader::new(Cursor::new(buf));
2192 let table =
2193 XRefTable::parse_with_recovery_options(&mut reader, &ParseOptions::default()).unwrap();
2194
2195 let root = table.trailer().and_then(|t| t.get("Root")).cloned();
2196 assert_eq!(root, Some(PdfObject::Reference(5, 0)));
2197 }
2198
2199 #[test]
2200 fn test_recovery_empty_when_no_objects() {
2201 let mut reader = BufReader::new(Cursor::new(b"%PDF-1.7\nnothing useful here\n".to_vec()));
2202 let result = XRefTable::parse_with_recovery_options(&mut reader, &ParseOptions::default());
2203 assert!(matches!(result, Err(ParseError::InvalidXRef)));
2204 }
2205
2206 #[test]
2207 fn test_parse_xref_entry() {
2208 let entry1 = XRefTable::parse_xref_entry("0000000000 65535 f ").unwrap();
2209 assert_eq!(entry1.offset, 0);
2210 assert_eq!(entry1.generation, 65535);
2211 assert!(!entry1.in_use);
2212
2213 let entry2 = XRefTable::parse_xref_entry("0000000017 00000 n ").unwrap();
2214 assert_eq!(entry2.offset, 17);
2215 assert_eq!(entry2.generation, 0);
2216 assert!(entry2.in_use);
2217 }
2218
2219 #[test]
2220 fn test_parse_xref_entry_flexible() {
2221 let entry1 = XRefTable::parse_xref_entry("17 0 n").unwrap();
2225 assert_eq!(entry1.offset, 17);
2226 assert_eq!(entry1.generation, 0);
2227 assert!(entry1.in_use);
2228
2229 let entry2 = XRefTable::parse_xref_entry("123 5 f").unwrap();
2231 assert_eq!(entry2.offset, 123);
2232 assert_eq!(entry2.generation, 5);
2233 assert!(!entry2.in_use);
2234
2235 let entry3 = XRefTable::parse_xref_entry("456 n").unwrap();
2237 assert_eq!(entry3.offset, 456);
2238 assert_eq!(entry3.generation, 0);
2239 assert!(entry3.in_use);
2240
2241 let entry4 = XRefTable::parse_xref_entry("789 2").unwrap();
2243 assert_eq!(entry4.offset, 789);
2244 assert_eq!(entry4.generation, 2);
2245 assert!(entry4.in_use);
2246
2247 let entry5 = XRefTable::parse_xref_entry("1000 0n").unwrap();
2249 assert_eq!(entry5.offset, 1000);
2250 assert_eq!(entry5.generation, 0);
2251 assert!(entry5.in_use);
2252
2253 let entry6 = XRefTable::parse_xref_entry("2000 1f").unwrap();
2254 assert_eq!(entry6.offset, 2000);
2255 assert_eq!(entry6.generation, 1);
2256 assert!(!entry6.in_use);
2257
2258 let entry7 = XRefTable::parse_xref_entry("3000\t0\tn").unwrap();
2260 assert_eq!(entry7.offset, 3000);
2261 assert_eq!(entry7.generation, 0);
2262 assert!(entry7.in_use);
2263 }
2264
2265 #[test]
2266 fn test_parse_xref_entry_invalid_flag_fallback() {
2267 let entry = XRefTable::parse_xref_entry("100 0 x").unwrap();
2269 assert_eq!(entry.offset, 100);
2270 assert_eq!(entry.generation, 0);
2271 assert!(entry.in_use); }
2273
2274 #[test]
2275 fn test_parse_xref_entry_malformed() {
2276 let result = XRefTable::parse_xref_entry("");
2278 assert!(result.is_err());
2279
2280 let result = XRefTable::parse_xref_entry("abc 0 n");
2282 assert!(result.is_err());
2283
2284 let result = XRefTable::parse_xref_entry(" ");
2286 assert!(result.is_err());
2287 }
2288
2289 #[test]
2290 fn test_xref_table_new() {
2291 let table = XRefTable::new();
2292 assert!(table.entries.is_empty());
2293 assert!(table.extended_entries.is_empty());
2294 assert!(table.trailer.is_none());
2295 assert_eq!(table.xref_offset, 0);
2296 }
2297
2298 #[test]
2299 fn test_xref_table_default() {
2300 let table = XRefTable::default();
2301 assert!(table.entries.is_empty());
2302 assert!(table.extended_entries.is_empty());
2303 assert!(table.trailer.is_none());
2304 }
2305
2306 #[test]
2307 fn test_xref_entry_struct() {
2308 let entry = XRefEntry {
2309 offset: 12345,
2310 generation: 7,
2311 in_use: true,
2312 };
2313 assert_eq!(entry.offset, 12345);
2314 assert_eq!(entry.generation, 7);
2315 assert!(entry.in_use);
2316 }
2317
2318 #[test]
2319 fn test_xref_entry_equality() {
2320 let entry1 = XRefEntry {
2321 offset: 100,
2322 generation: 0,
2323 in_use: true,
2324 };
2325 let entry2 = XRefEntry {
2326 offset: 100,
2327 generation: 0,
2328 in_use: true,
2329 };
2330 assert_eq!(entry1, entry2);
2331 }
2332
2333 #[test]
2334 fn test_xref_entry_clone() {
2335 let entry = XRefEntry {
2336 offset: 999,
2337 generation: 3,
2338 in_use: false,
2339 };
2340 let cloned = entry;
2341 assert_eq!(cloned.offset, 999);
2342 assert_eq!(cloned.generation, 3);
2343 assert!(!cloned.in_use);
2344 }
2345
2346 #[test]
2347 fn test_xref_entry_ext() {
2348 let ext_entry = XRefEntryExt {
2349 basic: XRefEntry {
2350 offset: 500,
2351 generation: 0,
2352 in_use: true,
2353 },
2354 compressed_info: Some((10, 5)),
2355 };
2356 assert_eq!(ext_entry.basic.offset, 500);
2357 assert_eq!(ext_entry.compressed_info, Some((10, 5)));
2358 }
2359
2360 #[test]
2361 fn test_xref_entry_ext_no_compression() {
2362 let ext_entry = XRefEntryExt {
2363 basic: XRefEntry {
2364 offset: 1000,
2365 generation: 1,
2366 in_use: true,
2367 },
2368 compressed_info: None,
2369 };
2370 assert!(ext_entry.compressed_info.is_none());
2371 }
2372
2373 #[test]
2374 fn test_add_entry() {
2375 let mut table = XRefTable::new();
2376 table.add_entry(
2377 5,
2378 XRefEntry {
2379 offset: 1000,
2380 generation: 0,
2381 in_use: true,
2382 },
2383 );
2384 assert_eq!(table.entries.len(), 1);
2385 assert!(table.entries.contains_key(&5));
2386 }
2387
2388 #[test]
2389 fn test_get_entry() {
2390 let mut table = XRefTable::new();
2391 let entry = XRefEntry {
2392 offset: 2000,
2393 generation: 1,
2394 in_use: true,
2395 };
2396 table.add_entry(10, entry);
2397
2398 let retrieved = table.get_entry(10);
2399 assert!(retrieved.is_some());
2400 assert_eq!(retrieved.unwrap().offset, 2000);
2401
2402 let missing = table.get_entry(999);
2403 assert!(missing.is_none());
2404 }
2405
2406 #[test]
2407 fn test_set_trailer() {
2408 let mut table = XRefTable::new();
2409 let mut trailer = PdfDictionary::new();
2410 trailer.insert("Size".to_string(), PdfObject::Integer(10));
2411
2412 table.set_trailer(trailer.clone());
2413 assert!(table.trailer.is_some());
2414 assert_eq!(
2415 table.trailer().unwrap().get("Size"),
2416 Some(&PdfObject::Integer(10))
2417 );
2418 }
2419
2420 #[test]
2421 fn test_parse_xref_entry_invalid() {
2422 let result = XRefTable::parse_xref_entry("0000000000 65535");
2424 assert!(result.is_ok()); let result = XRefTable::parse_xref_entry("not_a_number 65535 f ");
2428 assert!(result.is_err());
2429
2430 let result = XRefTable::parse_xref_entry("0000000000 65535 x ");
2432 assert!(result.is_ok()); assert!(result.unwrap().in_use); }
2435
2436 #[test]
2437 fn test_parse_xref_entry_various_offsets() {
2438 let entry = XRefTable::parse_xref_entry("0000000001 00000 n ").unwrap();
2440 assert_eq!(entry.offset, 1);
2441
2442 let entry = XRefTable::parse_xref_entry("9999999999 00000 n ").unwrap();
2444 assert_eq!(entry.offset, 9999999999);
2445
2446 let entry = XRefTable::parse_xref_entry("0000000000 65535 f ").unwrap();
2448 assert_eq!(entry.generation, 65535);
2449 }
2450
2451 #[test]
2452 fn test_add_extended_entry() {
2453 let mut table = XRefTable::new();
2454 let ext_entry = XRefEntryExt {
2455 basic: XRefEntry {
2456 offset: 0,
2457 generation: 0,
2458 in_use: true,
2459 },
2460 compressed_info: Some((5, 10)),
2461 };
2462
2463 table.add_extended_entry(15, ext_entry);
2464 assert_eq!(table.extended_entries.len(), 1);
2465 assert!(table.extended_entries.contains_key(&15));
2466 }
2467
2468 #[test]
2469 fn test_get_extended_entry() {
2470 let mut table = XRefTable::new();
2471 let ext_entry = XRefEntryExt {
2472 basic: XRefEntry {
2473 offset: 0,
2474 generation: 0,
2475 in_use: true,
2476 },
2477 compressed_info: Some((20, 3)),
2478 };
2479
2480 table.add_extended_entry(7, ext_entry);
2481
2482 let retrieved = table.get_extended_entry(7);
2483 assert!(retrieved.is_some());
2484 assert_eq!(retrieved.unwrap().compressed_info, Some((20, 3)));
2485 }
2486
2487 #[test]
2488 fn test_xref_offset() {
2489 let mut table = XRefTable::new();
2490 assert_eq!(table.xref_offset(), 0);
2491
2492 table.xref_offset = 12345;
2493 assert_eq!(table.xref_offset(), 12345);
2494 }
2495
2496 #[test]
2497 fn test_find_xref_offset_simple() {
2498 let pdf_data = b"startxref\n12345\n%%EOF";
2499 let cursor = Cursor::new(pdf_data.to_vec());
2500 let mut reader = BufReader::new(cursor);
2501
2502 let offset = XRefTable::find_xref_offset(&mut reader).unwrap();
2503 assert_eq!(offset, 12345);
2504 }
2505
2506 #[test]
2507 fn test_find_xref_offset_with_spaces() {
2508 let pdf_data = b"startxref \n 12345 \n%%EOF";
2509 let cursor = Cursor::new(pdf_data.to_vec());
2510 let mut reader = BufReader::new(cursor);
2511
2512 let offset = XRefTable::find_xref_offset(&mut reader).unwrap();
2513 assert_eq!(offset, 12345);
2514 }
2515
2516 #[test]
2517 fn test_find_xref_offset_missing() {
2518 let pdf_data = b"no startxref here";
2519 let cursor = Cursor::new(pdf_data.to_vec());
2520 let mut reader = BufReader::new(cursor);
2521
2522 let result = XRefTable::find_xref_offset(&mut reader);
2523 assert!(result.is_err());
2524 }
2525
2526 #[test]
2527 fn test_trailer_getter() {
2528 let mut table = XRefTable::new();
2529 assert!(table.trailer().is_none());
2530
2531 let trailer = PdfDictionary::new();
2532 table.set_trailer(trailer);
2533 assert!(table.trailer().is_some());
2534 }
2535
2536 #[test]
2537 fn test_xref_table_clone() {
2538 let mut table = XRefTable::new();
2539 table.add_entry(
2540 1,
2541 XRefEntry {
2542 offset: 100,
2543 generation: 0,
2544 in_use: true,
2545 },
2546 );
2547 table.xref_offset = 5000;
2548
2549 let cloned = table.clone();
2550 assert_eq!(cloned.entries.len(), 1);
2551 assert_eq!(cloned.xref_offset, 5000);
2552 }
2553
2554 #[test]
2555 fn test_parse_obj_header() {
2556 assert_eq!(parse_obj_header_bytes(b"1 0 obj"), Some((1, 0)));
2558 assert_eq!(parse_obj_header_bytes(b"123 5 obj"), Some((123, 5)));
2559 assert_eq!(parse_obj_header_bytes(b" 42 3 obj "), Some((42, 3)));
2560
2561 assert_eq!(parse_obj_header_bytes(b"1 obj"), None);
2563 assert_eq!(parse_obj_header_bytes(b"abc 0 obj"), None);
2564 assert_eq!(parse_obj_header_bytes(b"1 0 object"), None);
2565 assert_eq!(parse_obj_header_bytes(b""), None);
2566 }
2567
2568 #[test]
2569 fn test_xref_recovery_parsing() {
2570 let pdf_content =
2572 b"1 0 obj\n<< /Type /Catalog >>\nendobj\n2 0 obj\n<< /Type /Page >>\nendobj\n";
2573 let mut reader = BufReader::new(Cursor::new(pdf_content));
2574
2575 let table = XRefTable::parse_with_recovery(&mut reader).unwrap();
2576
2577 assert_eq!(table.len(), 2);
2579 assert!(table.get_entry(1).is_some());
2580 assert!(table.get_entry(2).is_some());
2581
2582 assert!(table.get_entry(1).unwrap().in_use);
2584 assert!(table.get_entry(2).unwrap().in_use);
2585 }
2586
2587 #[test]
2588 fn test_xref_recovery_no_objects() {
2589 let pdf_content = b"This is not a PDF file\nNo objects here\n";
2591 let mut reader = BufReader::new(Cursor::new(pdf_content));
2592
2593 let result = XRefTable::parse_with_recovery(&mut reader);
2594 assert!(result.is_err());
2595 }
2596
2597 #[test]
2598 fn test_offset_validation() {
2599 let pdf_data = b"small file";
2600 let mut reader = BufReader::new(Cursor::new(pdf_data));
2601
2602 assert!(XRefTable::validate_offset(&mut reader, 5).is_ok());
2604
2605 assert!(XRefTable::validate_offset(&mut reader, 100).is_err());
2607
2608 assert!(XRefTable::validate_offset(&mut reader, 10).is_err());
2610 }
2611
2612 #[test]
2613 fn test_xref_parse_with_fallback() {
2614 let pdf_content =
2618 b"1 0 obj\n<< /Type /Catalog >>\nendobj\n2 0 obj\n<< /Type /Page >>\nendobj\n";
2619
2620 let mut reader = BufReader::new(Cursor::new(pdf_content.to_vec()));
2622 let table = XRefTable::parse(&mut reader).expect("recovery rebuilds xref from object scan");
2623 assert!(table.get_entry(1).is_some(), "object 1 indexed by scan");
2624 assert!(table.get_entry(2).is_some(), "object 2 indexed by scan");
2625
2626 let mut reader = BufReader::new(Cursor::new(pdf_content.to_vec()));
2629 match XRefTable::parse_with_options(&mut reader, &ParseOptions::strict()) {
2630 Err(ParseError::InvalidXRef) => {}
2631 Ok(_) => panic!("strict() must fail on missing xref, not recover"),
2632 Err(other) => panic!("strict() must fail with InvalidXRef, got: {other:?}"),
2633 }
2634 }
2635
2636 #[test]
2637 fn test_xref_entry_creation() {
2638 let entry = XRefEntry {
2639 offset: 1234,
2640 generation: 5,
2641 in_use: true,
2642 };
2643
2644 assert_eq!(entry.offset, 1234);
2645 assert_eq!(entry.generation, 5);
2646 assert!(entry.in_use);
2647 }
2648
2649 #[test]
2650 fn test_xref_entry_ext_creation() {
2651 let basic = XRefEntry {
2652 offset: 5000,
2653 generation: 0,
2654 in_use: true,
2655 };
2656
2657 let ext = XRefEntryExt {
2658 basic: basic.clone(),
2659 compressed_info: Some((10, 3)),
2660 };
2661
2662 assert_eq!(ext.basic.offset, 5000);
2663 assert_eq!(ext.compressed_info, Some((10, 3)));
2664 }
2665
2666 #[test]
2667 fn test_xref_table_new_advanced() {
2668 let table = XRefTable::new();
2669 assert_eq!(table.entries.len(), 0);
2670 assert_eq!(table.extended_entries.len(), 0);
2671 assert!(table.trailer.is_none());
2672 assert_eq!(table.xref_offset, 0);
2673 }
2674
2675 #[test]
2676 fn test_xref_table_default_advanced() {
2677 let table = XRefTable::default();
2678 assert_eq!(table.entries.len(), 0);
2679 assert!(table.trailer.is_none());
2680 }
2681
2682 #[test]
2683 fn test_xref_table_add_entry() {
2684 let mut table = XRefTable::new();
2685
2686 let entry1 = XRefEntry {
2687 offset: 100,
2688 generation: 0,
2689 in_use: true,
2690 };
2691 table.add_entry(1, entry1);
2692 let entry2 = XRefEntry {
2693 offset: 200,
2694 generation: 1,
2695 in_use: false,
2696 };
2697 table.add_entry(2, entry2);
2698
2699 assert_eq!(table.len(), 2);
2700
2701 let entry1 = table.get_entry(1).unwrap();
2702 assert_eq!(entry1.offset, 100);
2703 assert_eq!(entry1.generation, 0);
2704 assert!(entry1.in_use);
2705
2706 let entry2 = table.get_entry(2).unwrap();
2707 assert_eq!(entry2.offset, 200);
2708 assert_eq!(entry2.generation, 1);
2709 assert!(!entry2.in_use);
2710 }
2711
2712 #[test]
2713 fn test_xref_table_add_extended_entry() {
2714 let mut table = XRefTable::new();
2715
2716 let basic_entry = XRefEntry {
2717 offset: 0,
2718 generation: 0,
2719 in_use: true,
2720 };
2721
2722 let extended_entry = XRefEntryExt {
2723 basic: basic_entry,
2724 compressed_info: Some((10, 2)),
2725 };
2726
2727 table.add_extended_entry(5, extended_entry);
2728
2729 let ext = table.get_extended_entry(5);
2731 assert!(ext.is_some());
2732 if let Some(ext) = ext {
2733 assert_eq!(ext.compressed_info, Some((10, 2)));
2734 }
2735
2736 assert!(table.is_compressed(5));
2737 }
2738
2739 #[test]
2740 fn test_xref_table_get_nonexistent() {
2741 let table = XRefTable::new();
2742 assert!(table.get_entry(999).is_none());
2743 assert!(table.get_extended_entry(999).is_none());
2744 }
2745
2746 #[test]
2747 fn test_xref_table_update_entry() {
2748 let mut table = XRefTable::new();
2749
2750 let entry1 = XRefEntry {
2752 offset: 100,
2753 generation: 0,
2754 in_use: true,
2755 };
2756 table.add_entry(1, entry1);
2757
2758 let entry2 = XRefEntry {
2760 offset: 200,
2761 generation: 1,
2762 in_use: false,
2763 };
2764 table.add_entry(1, entry2);
2765
2766 let entry = table.get_entry(1).unwrap();
2768 assert_eq!(entry.offset, 200);
2769 assert_eq!(entry.generation, 1);
2770 assert!(!entry.in_use);
2771 }
2772
2773 #[test]
2774 fn test_xref_table_set_trailer() {
2775 let mut table = XRefTable::new();
2776 assert!(table.trailer.is_none());
2777
2778 let mut trailer = PdfDictionary::new();
2779 trailer.insert("Size".to_string(), PdfObject::Integer(10));
2780
2781 table.set_trailer(trailer.clone());
2782 assert!(table.trailer.is_some());
2783 assert_eq!(table.trailer(), Some(&trailer));
2784 }
2785
2786 #[test]
2787 fn test_xref_table_offset() {
2788 let table = XRefTable::new();
2789 assert_eq!(table.xref_offset(), 0);
2790 }
2791
2792 #[test]
2793 fn test_parse_xref_entry_invalid_static() {
2794 let invalid_lines = vec![
2795 "not a valid entry".to_string(),
2796 "12345 abcde n".to_string(), ];
2798
2799 for line in invalid_lines {
2800 let result = XRefTable::parse_xref_entry(&line);
2801 assert!(result.is_err());
2802 }
2803
2804 let result = XRefTable::parse_xref_entry("12345 00000");
2806 assert!(result.is_ok());
2807 let entry = result.unwrap();
2808 assert_eq!(entry.offset, 12345);
2809 assert_eq!(entry.generation, 0);
2810 assert!(entry.in_use); }
2812
2813 #[test]
2814 fn test_xref_entry_operations() {
2815 let mut table = XRefTable::new();
2816
2817 let entry1 = XRefEntry {
2819 offset: 1234,
2820 generation: 5,
2821 in_use: true,
2822 };
2823
2824 let entry2 = XRefEntry {
2825 offset: 5678,
2826 generation: 10,
2827 in_use: false,
2828 };
2829
2830 table.add_entry(1, entry1);
2831 table.add_entry(2, entry2);
2832
2833 assert_eq!(table.len(), 2);
2834
2835 let retrieved1 = table.get_entry(1).unwrap();
2836 assert_eq!(retrieved1.offset, 1234);
2837 assert_eq!(retrieved1.generation, 5);
2838 assert!(retrieved1.in_use);
2839
2840 let retrieved2 = table.get_entry(2).unwrap();
2841 assert_eq!(retrieved2.offset, 5678);
2842 assert_eq!(retrieved2.generation, 10);
2843 assert!(!retrieved2.in_use);
2844 }
2845
2846 #[test]
2847 fn test_parse_xref_with_comments() {
2848 let pdf_content = b"%PDF-1.4\n\
28491 0 obj\n<< /Type /Catalog >>\nendobj\n\
2850xref\n\
2851% This is a comment\n\
28520 2\n\
28530000000000 65535 f \n\
28540000000015 00000 n \n\
2855% Another comment\n\
2856trailer\n\
2857<< /Size 2 /Root 1 0 R >>\n\
2858startxref\n\
285945\n\
2860%%EOF";
2861
2862 let mut reader = BufReader::new(Cursor::new(pdf_content));
2863 reader.seek(SeekFrom::Start(45)).unwrap(); let result = XRefTable::parse(&mut reader);
2866 assert!(result.is_ok());
2867 let table = result.unwrap();
2868 assert_eq!(table.len(), 2);
2869 }
2870
2871 #[test]
2872 fn test_parse_multiple_xref_sections() {
2873 let pdf_content = b"%PDF-1.4\n\
28741 0 obj\n<< /Type /Catalog >>\nendobj\n\
28752 0 obj\n<< /Type /Page >>\nendobj\n\
2876xref\n\
28770 2\n\
28780000000000 65535 f \n\
28790000000015 00000 n \n\
28805 2\n\
28810000000100 00000 n \n\
28820000000200 00000 n \n\
2883trailer\n\
2884<< /Size 7 /Root 1 0 R >>\n\
2885startxref\n\
288678\n\
2887%%EOF";
2888
2889 let mut reader = BufReader::new(Cursor::new(pdf_content));
2890 reader.seek(SeekFrom::Start(78)).unwrap(); let result = XRefTable::parse(&mut reader);
2893 assert!(result.is_ok());
2894 let table = result.unwrap();
2895 assert_eq!(table.len(), 4);
2897 assert!(table.get_entry(0).is_some());
2898 assert!(table.get_entry(1).is_some());
2899 assert!(table.get_entry(5).is_some());
2900 assert!(table.get_entry(6).is_some());
2901 }
2902
2903 #[test]
2904 fn test_parse_xref_with_prev() {
2905 let pdf_content = b"%PDF-1.4\n\
2907% First xref at 15\n\
2908xref\n\
29090 2\n\
29100000000000 65535 f \n\
29110000000100 00000 n \n\
2912trailer\n\
2913<< /Size 2 >>\n\
2914% Second xref at 100\n\
2915xref\n\
29162 1\n\
29170000000200 00000 n \n\
2918trailer\n\
2919<< /Size 3 /Prev 15 >>\n\
2920startxref\n\
2921100\n\
2922%%EOF";
2923
2924 let mut reader = BufReader::new(Cursor::new(pdf_content));
2925 let options = ParseOptions {
2926 lenient_syntax: true,
2927 ..Default::default()
2928 };
2929
2930 let result = XRefTable::parse_with_options(&mut reader, &options);
2931 assert!(result.is_ok() || result.is_err());
2933 }
2934
2935 #[test]
2936 fn test_invalid_xref_format() {
2937 let pdf_content = b"xref\ninvalid content\ntrailer";
2938 let mut reader = BufReader::new(Cursor::new(pdf_content));
2939
2940 let result = XRefTable::parse(&mut reader);
2941 assert!(result.is_err());
2942 }
2943
2944 #[test]
2945 fn test_xref_entry_overflow() {
2946 let mut table = XRefTable::new();
2947
2948 let entry = XRefEntry {
2950 offset: u64::MAX,
2951 generation: u16::MAX,
2952 in_use: true,
2953 };
2954 table.add_entry(u32::MAX, entry);
2955
2956 let entry = table.get_entry(u32::MAX).unwrap();
2957 assert_eq!(entry.offset, u64::MAX);
2958 assert_eq!(entry.generation, u16::MAX);
2959 }
2960
2961 #[test]
2962 fn test_xref_table_operations() {
2963 let mut table = XRefTable::new();
2964
2965 let entry1 = XRefEntry {
2967 offset: 100,
2968 generation: 0,
2969 in_use: true,
2970 };
2971
2972 let entry2 = XRefEntry {
2973 offset: 200,
2974 generation: 0,
2975 in_use: true,
2976 };
2977
2978 table.add_entry(1, entry1);
2979 table.add_entry(2, entry2);
2980
2981 assert_eq!(table.len(), 2);
2982 assert!(table.get_entry(1).is_some());
2983 assert!(table.get_entry(2).is_some());
2984 assert!(table.get_entry(3).is_none());
2985 }
2986
2987 #[test]
2988 fn test_xref_table_merge() {
2989 let mut table1 = XRefTable::new();
2990 let entry1 = XRefEntry {
2991 offset: 100,
2992 generation: 0,
2993 in_use: true,
2994 };
2995 table1.add_entry(1, entry1);
2996 let entry2 = XRefEntry {
2997 offset: 200,
2998 generation: 0,
2999 in_use: true,
3000 };
3001 table1.add_entry(2, entry2);
3002
3003 let mut table2 = XRefTable::new();
3004 let entry3 = XRefEntry {
3005 offset: 250,
3006 generation: 1,
3007 in_use: true,
3008 }; table2.add_entry(2, entry3);
3010 let entry4 = XRefEntry {
3011 offset: 300,
3012 generation: 0,
3013 in_use: true,
3014 }; table2.add_entry(3, entry4);
3016
3017 for i in 2..=3 {
3020 if let Some(entry) = table2.get_entry(i) {
3021 table1.add_entry(
3022 i,
3023 XRefEntry {
3024 offset: entry.offset,
3025 generation: entry.generation,
3026 in_use: entry.in_use,
3027 },
3028 );
3029 }
3030 }
3031
3032 assert_eq!(table1.len(), 3);
3033
3034 let entry2 = table1.get_entry(2).unwrap();
3036 assert_eq!(entry2.offset, 250);
3037 assert_eq!(entry2.generation, 1);
3038
3039 assert!(table1.get_entry(3).is_some());
3041 }
3042
3043 #[test]
3044 fn test_xref_recovery_with_stream() {
3045 let pdf_content = b"1 0 obj\n<< /Type /ObjStm /N 2 /First 10 >>\nstream\n12345678901 0 2 0\nendstream\nendobj\n";
3046 let mut reader = BufReader::new(Cursor::new(pdf_content));
3047
3048 let result = XRefTable::parse_with_recovery(&mut reader);
3049 assert!(result.is_ok() || result.is_err());
3051 }
3052
3053 #[test]
3054 fn test_xref_entry_equality_advanced() {
3055 let entry1 = XRefEntry {
3056 offset: 100,
3057 generation: 0,
3058 in_use: true,
3059 };
3060
3061 let entry2 = XRefEntry {
3062 offset: 100,
3063 generation: 0,
3064 in_use: true,
3065 };
3066
3067 let entry3 = XRefEntry {
3068 offset: 200,
3069 generation: 0,
3070 in_use: true,
3071 };
3072
3073 assert_eq!(entry1, entry2);
3074 assert_ne!(entry1, entry3);
3075 }
3076
3077 #[test]
3078 fn test_parse_options_effect() {
3079 let pdf_content = b"xref 0 1 invalid";
3080 let mut reader = BufReader::new(Cursor::new(pdf_content));
3081
3082 let strict_options = ParseOptions {
3084 lenient_syntax: false,
3085 ..Default::default()
3086 };
3087 let result = XRefTable::parse_with_options(&mut reader, &strict_options);
3088 assert!(result.is_err());
3089
3090 reader.seek(SeekFrom::Start(0)).unwrap();
3092 let lenient_options = ParseOptions {
3093 lenient_syntax: true,
3094 ..Default::default()
3095 };
3096 let result = XRefTable::parse_with_options(&mut reader, &lenient_options);
3097 assert!(result.is_err() || result.is_ok());
3099 }
3100
3101 #[test]
3102 fn test_circular_reference_detection() {
3103 let pdf_content = b"%PDF-1.4\n\
3105xref\n\
31060 1\n\
31070000000000 65535 f \n\
3108trailer\n\
3109<< /Size 1 /Prev 10 >>\n\
3110startxref\n\
311110\n\
3112%%EOF";
3113
3114 let mut reader = BufReader::new(Cursor::new(pdf_content));
3115
3116 let result = XRefTable::parse_with_incremental_updates(&mut reader);
3118 assert!(result.is_ok() || result.is_err());
3120 }
3121
3122 #[test]
3123 fn test_linearized_xref_detection() {
3124 let pdf_content = b"%PDF-1.4\n\
31261 0 obj\n\
3127<< /Linearized 1 /L 1234 /H [100 200] /O 5 /E 500 /N 10 /T 600 >>\n\
3128endobj\n\
3129xref\n\
31300 2\n\
31310000000000 65535 f \n\
31320000000009 00000 n \n\
3133trailer\n\
3134<< /Size 2 >>\n\
3135startxref\n\
313663\n\
3137%%EOF";
3138
3139 let mut reader = BufReader::new(Cursor::new(pdf_content));
3140
3141 let result = XRefTable::find_linearized_xref(&mut reader);
3143 assert!(result.is_ok());
3144
3145 let xref_pos = result.unwrap();
3148 assert_eq!(
3149 xref_pos, 90,
3150 "Expected xref at position 90, got {}",
3151 xref_pos
3152 );
3153 }
3154
3155 #[test]
3156 fn test_xref_stream_parsing() {
3157 let pdf_content = b"%PDF-1.5\n\
31601 0 obj\n\
3161<< /Type /XRef /Size 3 /W [1 2 1] /Length 12 >>\n\
3162stream\n\
3163\x00\x00\x00\x00\
3164\x01\x00\x10\x00\
3165\x01\x00\x20\x00\
3166endstream\n\
3167endobj\n\
3168startxref\n\
31699\n\
3170%%EOF";
3171
3172 let mut reader = BufReader::new(Cursor::new(pdf_content));
3173 reader.seek(SeekFrom::Start(9)).unwrap();
3174
3175 let result = XRefTable::parse(&mut reader);
3177 assert!(result.is_err() || result.is_ok());
3179 }
3180
3181 #[test]
3182 fn test_xref_validation_max_object_exceeds_size() {
3183 let pdf_content = b"%PDF-1.4\n\
3185xref\n\
31860 1\n\
31870000000000 65535 f \n\
318810 1\n\
31890000000100 00000 n \n\
3190trailer\n\
3191<< /Size 5 /Root 1 0 R >>\n\
3192startxref\n\
31939\n\
3194%%EOF";
3195
3196 let mut reader = BufReader::new(Cursor::new(pdf_content));
3197 reader.seek(SeekFrom::Start(9)).unwrap();
3198
3199 let result = XRefTable::parse(&mut reader);
3201 assert!(result.is_err());
3202 }
3203
3204 #[test]
3205 fn test_parse_with_options_lenient_vs_strict() {
3206 let pdf_content = b"%PDF-1.4\n\
3208xref\n\
32090 2\n\
32100000000000 65535 f \n\
32110000000015 00000 n \n\
3212trailer\n\
3213<< /Size 2 >>\n\
3214startxref\n\
32159\n\
3216%%EOF";
3217
3218 let mut reader = BufReader::new(Cursor::new(pdf_content));
3219
3220 let strict_options = ParseOptions {
3222 lenient_syntax: false,
3223 recover_from_stream_errors: false,
3224 ..Default::default()
3225 };
3226 reader.seek(SeekFrom::Start(9)).unwrap();
3227 let strict_result = XRefTable::parse_with_options(&mut reader, &strict_options);
3228
3229 let lenient_options = ParseOptions {
3231 lenient_syntax: true,
3232 recover_from_stream_errors: true,
3233 ..Default::default()
3234 };
3235 reader.seek(SeekFrom::Start(9)).unwrap();
3236 let lenient_result = XRefTable::parse_with_options(&mut reader, &lenient_options);
3237
3238 assert!(strict_result.is_ok());
3240 assert!(lenient_result.is_ok());
3241 }
3242
3243 #[test]
3244 fn test_xref_entry_with_attached_flag() {
3245 let entry1 = XRefTable::parse_xref_entry("12345 0n");
3247 assert!(entry1.is_ok());
3248 let entry1 = entry1.unwrap();
3249 assert_eq!(entry1.offset, 12345);
3250 assert_eq!(entry1.generation, 0);
3251 assert!(entry1.in_use);
3252
3253 let entry2 = XRefTable::parse_xref_entry("54321 1f");
3254 assert!(entry2.is_ok());
3255 let entry2 = entry2.unwrap();
3256 assert_eq!(entry2.offset, 54321);
3257 assert_eq!(entry2.generation, 1);
3258 assert!(!entry2.in_use);
3259 }
3260
3261 #[test]
3262 fn test_find_xref_offset_edge_cases() {
3263 use std::io::{BufReader, Cursor};
3265
3266 let content = b"garbage\nstartxref \n 123 \n%%EOF";
3268 let mut reader = BufReader::new(Cursor::new(content));
3269 let result = XRefTable::find_xref_offset(&mut reader);
3270 assert_eq!(result.unwrap(), 123);
3271
3272 let content = b"startxref\n999\n%%EOF";
3274 let mut reader = BufReader::new(Cursor::new(content));
3275 let result = XRefTable::find_xref_offset(&mut reader);
3276 assert_eq!(result.unwrap(), 999);
3277
3278 let content = b"startxref\n456";
3280 let mut reader = BufReader::new(Cursor::new(content));
3281 let result = XRefTable::find_xref_offset(&mut reader);
3282 assert!(result.is_ok() || result.is_err());
3284
3285 let content = b"some content\n%%EOF";
3287 let mut reader = BufReader::new(Cursor::new(content));
3288 let result = XRefTable::find_xref_offset(&mut reader);
3289 assert!(result.is_err());
3290 }
3291
3292 #[test]
3293 fn test_xref_subsection_incomplete() {
3294 let pdf_content = b"%PDF-1.4\n\
3296xref\n\
32970 5\n\
32980000000000 65535 f \n\
32990000000015 00000 n \n\
3300trailer\n\
3301<< /Size 5 >>\n\
3302startxref\n\
33039\n\
3304%%EOF";
3305
3306 let mut reader = BufReader::new(Cursor::new(pdf_content));
3307 reader.seek(SeekFrom::Start(9)).unwrap();
3308
3309 let result = XRefTable::parse(&mut reader);
3311 assert!(result.is_err() || result.is_ok());
3313 }
3314}
3315
3316type EncryptAndId = (
3317 Option<super::objects::PdfObject>,
3318 Option<super::objects::PdfObject>,
3319);
3320
3321fn extract_encrypt_and_id_from_trailer(tail: &[u8], options: &ParseOptions) -> EncryptAndId {
3340 if let Some(pos) = rfind_byte_pattern(tail, b"trailer") {
3342 let after = &tail[pos + b"trailer".len()..];
3343 let mut lexer =
3344 super::lexer::Lexer::new_with_options(std::io::Cursor::new(after), options.clone());
3345 if let Ok(super::objects::PdfObject::Dictionary(dict)) =
3346 super::objects::PdfObject::parse_with_options(&mut lexer, options)
3347 {
3348 let encrypt = dict.get("Encrypt").cloned();
3349 let id = dict.get("ID").cloned();
3350 if encrypt.is_some() || id.is_some() {
3351 return (encrypt, id);
3352 }
3353 }
3354 }
3355
3356 extract_encrypt_and_id_from_xref_stream(tail, options)
3358}
3359
3360fn extract_encrypt_and_id_from_xref_stream(tail: &[u8], options: &ParseOptions) -> EncryptAndId {
3368 let type_pos = match rfind_byte_pattern(tail, b"/Type/XRef")
3369 .or_else(|| rfind_byte_pattern(tail, b"/Type /XRef"))
3370 {
3371 Some(p) => p,
3372 None => return (None, None),
3373 };
3374
3375 let Some(obj_pos) = rfind_byte_pattern(&tail[..type_pos], b"obj") else {
3378 return (None, None);
3379 };
3380 let Some(rel) = find_byte_pattern(&tail[obj_pos..type_pos], b"<<") else {
3381 return (None, None);
3382 };
3383 let dict_start = obj_pos + rel;
3384
3385 let region = &tail[dict_start..];
3394 let mut lexer =
3395 super::lexer::Lexer::new_with_options(std::io::Cursor::new(region), options.clone());
3396 if !matches!(lexer.next_token(), Ok(super::lexer::Token::DictStart)) {
3398 return (None, None);
3399 }
3400 match super::objects::PdfObject::parse_dictionary_inner_with_options(&mut lexer, options) {
3401 Ok(dict) => (dict.get("Encrypt").cloned(), dict.get("ID").cloned()),
3402 Err(_) => (None, None),
3403 }
3404}
3405
3406fn extract_root_from_xref_stream(content: &str) -> Option<u32> {
3408 let lines: Vec<&str> = content.lines().collect();
3413 let mut in_xref_obj = false;
3414
3415 for (i, line) in lines.iter().enumerate() {
3416 if line.contains(" obj")
3418 && lines
3419 .get(i + 1)
3420 .map_or(false, |next| next.contains("/Type /XRef"))
3421 {
3422 in_xref_obj = true;
3423 continue;
3424 }
3425
3426 if in_xref_obj {
3428 if line.contains("endobj") {
3429 in_xref_obj = false;
3430 continue;
3431 }
3432
3433 if let Some(root_pos) = line.find("/Root ") {
3435 let after_root = &line[root_pos + 6..]; if let Some(space_pos) = after_root.find(' ') {
3439 let number_part = &after_root[..space_pos];
3440 if let Ok(root_obj) = number_part.parse::<u32>() {
3441 tracing::debug!("Extracted Root {} from XRef stream", root_obj);
3442 return Some(root_obj);
3443 }
3444 }
3445 }
3446 }
3447 }
3448
3449 None
3450}
3451
3452fn find_catalog_by_content<R: Read + Seek>(
3455 reader: &mut R,
3456 table: &XRefTable,
3457) -> ParseResult<Option<u32>> {
3458 let mut obj_numbers: Vec<u32> = table.entries.keys().copied().collect();
3461 obj_numbers.sort_unstable();
3462
3463 for obj_num in obj_numbers {
3464 let offset = match table.entries.get(&obj_num) {
3465 Some(entry) if entry.in_use => entry.offset,
3466 _ => continue,
3467 };
3468 if let Some(content) = read_object_content(reader, obj_num, offset)? {
3470 if content.contains("/Type /Catalog") {
3471 tracing::debug!(
3472 "Found catalog candidate at object {} (validated structure)",
3473 obj_num
3474 );
3475 return Ok(Some(obj_num));
3476 }
3477 }
3478 }
3479
3480 tracing::debug!("No valid catalog found by content search");
3481 Ok(None)
3482}