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)]
430pub struct XRefTable {
431 entries: HashMap<u32, XRefEntry>,
433 extended_entries: HashMap<u32, XRefEntryExt>,
435 trailer: Option<super::objects::PdfDictionary>,
437 xref_offset: u64,
439}
440
441impl Default for XRefTable {
442 fn default() -> Self {
443 Self::new()
444 }
445}
446
447impl XRefTable {
448 pub fn new() -> Self {
450 Self {
451 entries: HashMap::new(),
452 extended_entries: HashMap::new(),
453 trailer: None,
454 xref_offset: 0,
455 }
456 }
457
458 pub fn entries(&self) -> &HashMap<u32, XRefEntry> {
460 &self.entries
461 }
462
463 pub fn parse<R: Read + Seek>(reader: &mut BufReader<R>) -> ParseResult<Self> {
465 Self::parse_with_options(reader, &super::ParseOptions::default())
466 }
467
468 pub fn parse_with_options<R: Read + Seek>(
470 reader: &mut BufReader<R>,
471 options: &super::ParseOptions,
472 ) -> ParseResult<Self> {
473 match Self::parse_with_incremental_updates_options(reader, options) {
475 Ok(table) => Ok(table),
476 Err(e) => {
477 if options.max_recovery_attempts > 0 {
487 tracing::warn!("Primary XRef parsing failed: {e:?}, attempting recovery");
488
489 reader.seek(SeekFrom::Start(0))?;
491 Self::parse_with_recovery_options(reader, options)
492 } else {
493 Err(e)
494 }
495 }
496 }
497 }
498
499 #[allow(dead_code)]
501 fn parse_with_incremental_updates<R: Read + Seek>(
502 reader: &mut BufReader<R>,
503 ) -> ParseResult<Self> {
504 Self::parse_with_incremental_updates_options(reader, &super::ParseOptions::default())
505 }
506
507 fn parse_with_incremental_updates_options<R: Read + Seek>(
509 reader: &mut BufReader<R>,
510 options: &super::ParseOptions,
511 ) -> ParseResult<Self> {
512 let xref_offset = Self::find_xref_offset(reader)?;
514
515 let mut merged_table = Self::new();
517 let mut current_offset = Some(xref_offset);
518 let mut visited_offsets = std::collections::HashSet::new();
519
520 while let Some(offset) = current_offset {
521 if visited_offsets.contains(&offset) {
523 tracing::debug!(
524 "Circular reference in XRef chain at offset {} (already visited)",
525 offset
526 );
527 break;
528 }
529 visited_offsets.insert(offset);
530
531 reader.seek(SeekFrom::Start(offset))?;
533 let table = Self::parse_primary_with_options(reader, options)?;
534
535 let prev_offset = table
537 .trailer
538 .as_ref()
539 .and_then(|t| t.get("Prev"))
540 .and_then(|obj| obj.as_integer())
541 .map(|i| i as u64);
542
543 if let Some(_prev) = prev_offset {
544 } else {
545 }
546
547 let _regular_count = table.entries.len();
549 let _extended_count = table.extended_entries.len();
550
551 for (obj_num, entry) in table.entries {
552 merged_table.entries.entry(obj_num).or_insert(entry);
553 }
554 for (obj_num, ext_entry) in table.extended_entries {
555 merged_table
556 .extended_entries
557 .entry(obj_num)
558 .or_insert(ext_entry);
559 }
560
561 if merged_table.trailer.is_none() {
563 merged_table.trailer = table.trailer;
564 merged_table.xref_offset = table.xref_offset;
565 }
566
567 current_offset = prev_offset;
568 }
569
570 if options.lenient_syntax || options.collect_warnings {
574 if let Err(e) = Self::scan_and_fill_missing_objects(reader, &mut merged_table) {
582 tracing::debug!("scan_and_fill_missing_objects failed (non-fatal): {e}");
583 }
584 }
585
586 Ok(merged_table)
587 }
588
589 #[allow(dead_code)]
591 fn parse_primary<R: Read + Seek>(reader: &mut BufReader<R>) -> ParseResult<Self> {
592 Self::parse_primary_with_options(reader, &super::ParseOptions::default())
593 }
594
595 fn parse_primary_with_options<R: Read + Seek>(
601 reader: &mut BufReader<R>,
602 options: &super::ParseOptions,
603 ) -> ParseResult<Self> {
604 let mut table = Self::new();
605
606 let xref_offset = reader.stream_position()?;
610 table.xref_offset = xref_offset;
611
612 let mut line = String::new();
615 let pos = reader.stream_position()?;
616 read_pdf_line(reader, &mut line)?;
617
618 if line.trim() == "xref" {
619 Self::parse_traditional_xref_with_options(reader, &mut table, options)?;
621 } else {
622 tracing::debug!(
623 "Not a traditional xref, checking for xref stream. Line: {:?}",
624 line.trim()
625 );
626
627 reader.seek(SeekFrom::Start(pos))?;
629
630 let mut lexer = super::lexer::Lexer::new_with_options(&mut *reader, options.clone());
632
633 let obj_num = match lexer.next_token()? {
635 super::lexer::Token::Integer(n) => n as u32,
636 _ => return Err(ParseError::InvalidXRef),
637 };
638
639 tracing::debug!("Found object {obj_num} at xref position");
640
641 let _gen_num = match lexer.next_token()? {
642 super::lexer::Token::Integer(n) => n as u16,
643 _ => return Err(ParseError::InvalidXRef),
644 };
645
646 match lexer.next_token()? {
647 super::lexer::Token::Obj => {}
648 _ => return Err(ParseError::InvalidXRef),
649 };
650
651 let obj = super::objects::PdfObject::parse_with_options(&mut lexer, options)?;
653
654 if let Some(stream) = obj.as_stream() {
655 if stream
657 .dict
658 .get("Type")
659 .and_then(|o| o.as_name())
660 .map(|n| n.as_str())
661 == Some("XRef")
662 {
663 tracing::debug!("Parsing XRef stream");
664
665 let decoded_data = match stream.decode(options) {
672 Ok(data) => data,
673 Err(e) => {
674 tracing::warn!(
675 "XRef stream decode failed: {e:?}, triggering recovery mode"
676 );
677 return Err(e);
678 }
679 };
680
681 let xref_stream_parser = xref_stream::XRefStream::parse(
683 &mut *reader,
684 stream.dict.clone(),
685 decoded_data,
686 options,
687 )?;
688
689 let entries = xref_stream_parser.to_xref_entries()?;
691 tracing::debug!("XRef stream parsed, found {} entries", entries.len());
692
693 for (obj_num, entry) in entries {
695 match entry {
696 xref_stream::XRefEntry::Free {
697 next_free_object,
698 generation,
699 } => {
700 table.entries.insert(
701 obj_num,
702 XRefEntry {
703 offset: next_free_object as u64,
704 generation,
705 in_use: false,
706 },
707 );
708 }
709 xref_stream::XRefEntry::InUse { offset, generation } => {
710 table.entries.insert(
711 obj_num,
712 XRefEntry {
713 offset,
714 generation,
715 in_use: true,
716 },
717 );
718 }
719 xref_stream::XRefEntry::Compressed {
720 stream_object_number,
721 index_within_stream,
722 } => {
723 let ext_entry = XRefEntryExt {
725 basic: XRefEntry {
726 offset: 0,
727 generation: 0,
728 in_use: true,
729 },
730 compressed_info: Some((
731 stream_object_number,
732 index_within_stream,
733 )),
734 };
735 table.extended_entries.insert(obj_num, ext_entry);
736 table.entries.insert(
737 obj_num,
738 XRefEntry {
739 offset: 0,
740 generation: 0,
741 in_use: true,
742 },
743 );
744 }
745 }
746 }
747
748 table.trailer = Some(xref_stream_parser.trailer_dict().clone());
750 } else {
751 return Err(ParseError::InvalidXRef);
752 }
753 } else {
754 return Err(ParseError::InvalidXRef);
755 }
756 }
757
758 Ok(table)
759 }
760
761 #[allow(dead_code)]
763 fn parse_traditional_xref<R: Read + Seek>(
764 reader: &mut BufReader<R>,
765 table: &mut XRefTable,
766 ) -> ParseResult<()> {
767 Self::parse_traditional_xref_with_options(reader, table, &super::ParseOptions::default())
768 }
769
770 fn parse_traditional_xref_with_options<R: Read + Seek>(
772 reader: &mut BufReader<R>,
773 table: &mut XRefTable,
774 options: &super::ParseOptions,
775 ) -> ParseResult<()> {
776 let mut line = String::new();
777 let mut trailer_dict_offset: Option<u64> = None;
778
779 loop {
782 line.clear();
783 let line_start_pos = reader.stream_position()?;
784 read_pdf_line(reader, &mut line)?;
785 let trimmed_line = line.trim();
786
787 if trimmed_line.is_empty() || trimmed_line.starts_with('%') {
789 continue;
790 }
791
792 if trimmed_line == "trailer" {
796 break;
798 }
799 if let Some(dict_pos) = trimmed_line.find("<<") {
800 if trimmed_line.starts_with("trailer") {
801 let trailer_keyword_start =
804 trimmed_line.as_ptr() as usize - line.as_ptr() as usize;
805 trailer_dict_offset =
806 Some(line_start_pos + (trailer_keyword_start + dict_pos) as u64);
807 break;
808 }
809 }
810
811 if trimmed_line.starts_with("<<") {
813 tracing::warn!(" Found trailer dictionary without 'trailer' keyword");
814 trailer_dict_offset = Some(line_start_pos);
816 break;
817 }
818
819 let parts: Vec<&str> = trimmed_line.split_whitespace().collect();
821 if parts.len() != 2 {
822 return Err(ParseError::InvalidXRef);
824 }
825
826 let first_obj_num = parts[0]
827 .parse::<u32>()
828 .map_err(|_| ParseError::InvalidXRef)?;
829 let count = parts[1]
830 .parse::<u32>()
831 .map_err(|_| ParseError::InvalidXRef)?;
832
833 let mut entries_parsed = 0;
836 let mut i = 0;
837 while i < count {
838 line.clear();
839 let bytes_read = read_pdf_line(reader, &mut line)?;
840 let trimmed = line.trim();
841
842 if trimmed.starts_with('%') {
844 continue;
845 }
846
847 if bytes_read == 0 || trimmed == "trailer" {
849 tracing::debug!(
850 "Warning: XRef subsection incomplete - expected {count} entries but found only {entries_parsed}"
851 );
852 if line.trim() == "trailer" {
854 break;
856 }
857 break;
858 }
859
860 match Self::parse_xref_entry(&line) {
861 Ok(entry) => {
862 table.entries.insert(first_obj_num + i, entry);
863 entries_parsed += 1;
864 }
865 Err(_) => {
866 tracing::debug!(
867 "Warning: Invalid XRef entry at position {}: {:?}",
868 i,
869 line.trim()
870 );
871 }
873 }
874 i += 1;
875 }
876 }
878
879 if let Some(offset) = trailer_dict_offset {
883 reader.seek(SeekFrom::Start(offset))?;
884 }
885 let mut lexer = super::lexer::Lexer::new_with_options(reader, options.clone());
886 let trailer_obj = super::objects::PdfObject::parse_with_options(&mut lexer, options)?;
887 table.trailer = trailer_obj.as_dict().cloned();
890
891 if let Some(trailer) = &table.trailer {
893 if let Some(size_obj) = trailer.get("Size") {
894 if let Some(expected_size) = size_obj.as_integer() {
895 if let Some(max_obj_num) = table.entries.keys().max() {
898 let max_expected = (*max_obj_num + 1) as i64;
899 if max_expected > expected_size {
900 tracing::debug!(
901 "Warning: XRef table has object {} but trailer Size is only {}",
902 max_obj_num,
903 expected_size
904 );
905 return Err(ParseError::InvalidXRef);
907 }
908 }
909 }
910 }
911 }
912
913 Ok(())
917 }
918
919 #[allow(dead_code)]
926 fn find_linearized_xref<R: Read + Seek>(reader: &mut BufReader<R>) -> ParseResult<u64> {
927 reader.seek(SeekFrom::Start(0))?;
929 let mut header = String::new();
930 reader.read_line(&mut header)?;
931
932 if !header.starts_with("%PDF-") {
933 return Err(ParseError::InvalidHeader);
934 }
935
936 let mut line = String::new();
938 reader.read_line(&mut line)?;
939
940 let pos = reader.stream_position()?;
943 let mut buffer = vec![0u8; 1024];
944 let bytes_read = reader.read(&mut buffer)?;
945 buffer.truncate(bytes_read);
946
947 tracing::debug!(
951 "Checking for linearized PDF, first 100 bytes: {:?}",
952 String::from_utf8_lossy(&buffer[..buffer.len().min(100)])
953 );
954
955 if find_byte_pattern(&buffer, b"/Linearized").is_some() {
957 if let Some(xref_pos) = find_byte_pattern(&buffer, b"xref") {
963 return Ok(pos + xref_pos as u64);
964 }
965
966 if find_byte_pattern(&buffer, b"/Type/XRef").is_some()
968 || find_byte_pattern(&buffer, b"/Type /XRef").is_some()
969 {
970 if let Some(obj_pos) = find_byte_pattern(&buffer, b" obj") {
973 let search_from = obj_pos + 4;
975 if search_from < buffer.len() {
976 let after_first_obj = &buffer[search_from..];
977 if let Some(next_obj) = find_byte_pattern(after_first_obj, b" obj") {
978 let second_obj_start =
980 pos + (search_from + next_obj).saturating_sub(10) as u64;
981 return Ok(second_obj_start);
982 }
983 }
984 }
985 }
986 }
987
988 Err(ParseError::InvalidXRef)
989 }
990
991 fn find_xref_offset<R: Read + Seek>(reader: &mut BufReader<R>) -> ParseResult<u64> {
993 reader.seek(SeekFrom::End(0))?;
995 let file_size = reader.stream_position()?;
996
997 let read_size = std::cmp::min(1024, file_size);
999 reader.seek(SeekFrom::End(-(read_size as i64)))?;
1000
1001 let mut buffer = vec![0u8; read_size as usize];
1002 reader.read_exact(&mut buffer)?;
1003
1004 let content = String::from_utf8_lossy(&buffer);
1006
1007 let debug_content = content.chars().take(200).collect::<String>();
1009 tracing::debug!("XRef search in last {read_size} bytes: {debug_content:?}");
1010
1011 let mut lines = content.pdf_lines();
1012
1013 let mut last_offset = None;
1020 while let Some(line) = lines.next() {
1021 if line.trim() == "startxref" {
1022 if let Some(offset_line) = lines.next() {
1024 if let Ok(offset) = offset_line.trim().parse::<u64>() {
1025 last_offset = Some(offset);
1026 }
1027 }
1028 }
1029 }
1030
1031 last_offset.ok_or(ParseError::InvalidXRef)
1032 }
1033
1034 fn scan_and_fill_missing_objects<R: Read + Seek>(
1036 reader: &mut BufReader<R>,
1037 table: &mut Self,
1038 ) -> ParseResult<()> {
1039 let scanned = scan_object_headers(reader)?;
1043 table.add_headers_latest_wins(&scanned, true);
1044
1045 Ok(())
1046 }
1047
1048 #[allow(dead_code)]
1050 fn parse_with_recovery<R: Read + Seek>(reader: &mut BufReader<R>) -> ParseResult<Self> {
1051 Self::parse_with_recovery_options(reader, &super::ParseOptions::default())
1052 }
1053
1054 fn parse_with_recovery_options<R: Read + Seek>(
1056 reader: &mut BufReader<R>,
1057 options: &super::ParseOptions,
1058 ) -> ParseResult<Self> {
1059 const ROOT_TAIL: usize = 256 * 1024;
1063 const CATALOG_TAIL: usize = 100 * 1024;
1064
1065 let mut table = Self::new();
1066
1067 let headers = scan_object_headers(reader)?;
1071 table.add_headers_latest_wins(&headers, false);
1072
1073 if table.entries.is_empty() {
1074 return Err(ParseError::InvalidXRef);
1075 }
1076 tracing::debug!("XRef recovery: found {} objects", table.len());
1077
1078 let (_, root_tail) = read_tail(reader, ROOT_TAIL)?;
1081 let root_tail_str = String::from_utf8_lossy(&root_tail);
1082 let xref_root_candidate = extract_root_from_xref_stream(&root_tail_str);
1083
1084 let mut trailer = super::objects::PdfDictionary::new();
1086 trailer.insert(
1087 "Size".to_string(),
1088 super::objects::PdfObject::Integer(table.len() as i64),
1089 );
1090
1091 let (encrypt, id) = extract_encrypt_and_id_from_trailer(&root_tail, options);
1097 if let Some(encrypt) = encrypt {
1098 trailer.insert("Encrypt".to_string(), encrypt);
1099 }
1100 if let Some(id) = id {
1101 trailer.insert("ID".to_string(), id);
1102 }
1103
1104 let mut catalog_candidate = None;
1106
1107 if let Some(xref_root) = xref_root_candidate {
1109 if table.entries.contains_key(&xref_root) {
1110 catalog_candidate = Some(xref_root);
1111 tracing::debug!("Using Root {} from XRef stream as catalog", xref_root);
1112 } else {
1113 tracing::debug!(
1114 "Warning: XRef Root {} not found in object table, searching manually",
1115 xref_root
1116 );
1117 }
1118 }
1119
1120 if catalog_candidate.is_none() {
1122 catalog_candidate = find_catalog_by_content(reader, &table)?;
1123 }
1124
1125 if catalog_candidate.is_none() {
1127 for obj_num in [1, 2, 3, 4, 5] {
1128 let offset = match table.entries.get(&obj_num) {
1129 Some(entry) if entry.in_use => entry.offset,
1130 _ => continue,
1131 };
1132 if let Some(content) = read_object_content(reader, obj_num, offset)? {
1133 if content.contains("/Type/Sig") || content.contains("/Type /Sig") {
1135 tracing::debug!("Skipping object {} (Type: Sig)", obj_num);
1136 continue;
1137 }
1138 if content.contains("/Type/Catalog")
1139 || content.contains("/Type /Catalog")
1140 || content.contains("/Pages")
1141 {
1142 catalog_candidate = Some(obj_num);
1143 tracing::debug!(
1144 "Using fallback catalog candidate: object {} (validated)",
1145 obj_num
1146 );
1147 break;
1148 }
1149 }
1150 }
1151 }
1152
1153 if catalog_candidate.is_none() && !table.entries.is_empty() {
1155 tracing::debug!(
1156 "Last resort: Scanning all {} objects for any with /Pages or /Catalog",
1157 table.entries.len()
1158 );
1159
1160 let mut obj_numbers: Vec<u32> = table.entries.keys().copied().collect();
1161 obj_numbers.sort_unstable();
1162
1163 for obj_num in obj_numbers {
1164 let offset = match table.entries.get(&obj_num) {
1165 Some(entry) if entry.in_use => entry.offset,
1166 _ => continue,
1167 };
1168 if let Some(content) = read_object_content(reader, obj_num, offset)? {
1169 if content.contains("/Type/Sig") || content.contains("/Type /Sig") {
1170 continue;
1171 }
1172 if content.contains("/Type/Catalog") || content.contains("/Type /Catalog") {
1173 catalog_candidate = Some(obj_num);
1174 tracing::debug!(
1175 "Last resort: Found catalog at object {} (/Type/Catalog)",
1176 obj_num
1177 );
1178 break;
1179 } else if content.contains("/Pages") {
1180 catalog_candidate = Some(obj_num);
1181 tracing::debug!(
1182 "Last resort: Found catalog at object {} (has /Pages)",
1183 obj_num
1184 );
1185 break;
1186 }
1187 }
1188 }
1189
1190 if catalog_candidate.is_none() {
1193 tracing::debug!("Extreme last resort: Scanning last 100KB for /Type/Catalog");
1194
1195 let (_, search_buffer) = read_tail(reader, CATALOG_TAIL)?;
1196 if let Some(catalog_pos) = rfind_byte_pattern(&search_buffer, b"/Type/Catalog") {
1197 let local_search_start = catalog_pos.saturating_sub(200);
1198 let search_area = &search_buffer[local_search_start..catalog_pos];
1199
1200 if let Some(obj_pattern_pos) = rfind_byte_pattern(search_area, b" 0 obj") {
1201 let before_obj = &search_area[..obj_pattern_pos];
1202 let before_obj_str = String::from_utf8_lossy(before_obj);
1203 let trimmed = before_obj_str.trim_end();
1204
1205 if let Some((digit_start, ch)) = trimmed
1206 .char_indices()
1207 .rev()
1208 .find(|(_, c)| !c.is_ascii_digit())
1209 {
1210 let num_str = trimmed[digit_start + ch.len_utf8()..].trim();
1211 if !num_str.is_empty() {
1212 if let Ok(obj_num) = num_str.parse::<u32>() {
1213 tracing::debug!(
1214 "Extreme last resort: Found /Type/Catalog at object {}",
1215 obj_num
1216 );
1217 catalog_candidate = Some(obj_num);
1218 }
1219 }
1220 } else if let Ok(obj_num) = trimmed.trim().parse::<u32>() {
1221 tracing::debug!(
1222 "Extreme last resort: Found /Type/Catalog at object {}",
1223 obj_num
1224 );
1225 catalog_candidate = Some(obj_num);
1226 }
1227 }
1228 } else {
1229 tracing::debug!("Extreme last resort: No /Type/Catalog found in last 100KB");
1230 }
1231 }
1232
1233 if catalog_candidate.is_none() {
1235 tracing::warn!(" Could not find any catalog object, using first non-signature object as absolute last resort");
1236 let mut obj_numbers: Vec<u32> = table.entries.keys().copied().collect();
1237 obj_numbers.sort_unstable();
1238 for obj_num in obj_numbers {
1239 let offset = match table.entries.get(&obj_num) {
1240 Some(entry) => entry.offset,
1241 None => continue,
1242 };
1243 if let Some(content) = read_object_content(reader, obj_num, offset)? {
1244 if !content.contains("/Type/Sig") && !content.contains("/Type /Sig") {
1245 catalog_candidate = Some(obj_num);
1246 tracing::debug!("Using object {} as absolute last resort", obj_num);
1247 break;
1248 }
1249 }
1250 }
1251 }
1252 }
1253
1254 if let Some(root_obj) = catalog_candidate {
1255 trailer.insert(
1256 "Root".to_string(),
1257 super::objects::PdfObject::Reference(root_obj, 0),
1258 );
1259 }
1260
1261 table.set_trailer(trailer);
1262
1263 Ok(table)
1264 }
1265
1266 #[allow(dead_code)]
1272 fn validate_offset<R: Read + Seek>(reader: &mut BufReader<R>, offset: u64) -> ParseResult<()> {
1273 let file_size = reader.seek(SeekFrom::End(0))?;
1275
1276 if offset >= file_size {
1277 #[cfg(debug_assertions)]
1278 tracing::warn!(" XRef offset {offset} exceeds file size {file_size}");
1279 return Err(ParseError::InvalidXRef);
1280 }
1281
1282 reader.seek(SeekFrom::Start(offset))?;
1284 let mut peek = [0u8; 20];
1285 let read_bytes = reader.read(&mut peek)?;
1286
1287 if read_bytes == 0 {
1288 #[cfg(debug_assertions)]
1289 tracing::warn!(" XRef offset {offset} points to EOF");
1290 return Err(ParseError::InvalidXRef);
1291 }
1292
1293 let peek_slice = &peek[..read_bytes];
1296 let starts_with_xref = peek_slice.len() >= 4 && &peek_slice[..4] == b"xref";
1297 let starts_with_digit = peek_slice.first().map_or(false, |&b| b.is_ascii_digit());
1298
1299 if !starts_with_xref && !starts_with_digit {
1300 #[cfg(debug_assertions)]
1301 {
1302 let debug_len = std::cmp::min(10, read_bytes);
1303 let debug_content = String::from_utf8_lossy(&peek[..debug_len]);
1304 tracing::debug!(
1305 "Warning: XRef offset {} does not point to valid XRef content: {:?}",
1306 offset,
1307 debug_content
1308 );
1309 }
1310 }
1312
1313 Ok(())
1314 }
1315
1316 fn parse_xref_entry(line: &str) -> ParseResult<XRefEntry> {
1318 let line = line.trim();
1319
1320 if line.len() >= 18 {
1322 if let Ok(entry) = Self::parse_xref_entry_standard(line) {
1323 return Ok(entry);
1324 }
1325 }
1326
1327 Self::parse_xref_entry_flexible(line)
1329 }
1330
1331 fn parse_xref_entry_standard(line: &str) -> ParseResult<XRefEntry> {
1333 if line.len() < 18 {
1336 return Err(ParseError::InvalidXRef);
1337 }
1338
1339 let offset_str = &line[0..10];
1340 let gen_str = &line[11..16];
1341 let flag = line.chars().nth(17);
1342
1343 let offset = offset_str
1344 .trim()
1345 .parse::<u64>()
1346 .map_err(|_| ParseError::InvalidXRef)?;
1347 let generation = gen_str
1348 .trim()
1349 .parse::<u16>()
1350 .map_err(|_| ParseError::InvalidXRef)?;
1351
1352 let in_use = match flag {
1353 Some('n') => true,
1354 Some('f') => false,
1355 _ => return Err(ParseError::InvalidXRef),
1356 };
1357
1358 Ok(XRefEntry {
1359 offset,
1360 generation,
1361 in_use,
1362 })
1363 }
1364
1365 fn parse_xref_entry_flexible(line: &str) -> ParseResult<XRefEntry> {
1367 let parts: Vec<&str> = line.split_whitespace().collect();
1375
1376 if parts.is_empty() {
1377 return Err(ParseError::InvalidXRef);
1378 }
1379
1380 let offset = parts[0]
1382 .parse::<u64>()
1383 .map_err(|_| ParseError::InvalidXRef)?;
1384
1385 let (generation, flag_from_gen) = if parts.len() >= 2 {
1387 let gen_part = parts[1];
1388 if gen_part == "n" || gen_part == "f" {
1390 (0, gen_part.chars().next())
1392 } else if gen_part.ends_with('n') || gen_part.ends_with('f') {
1393 let flag_char = gen_part.chars().last().ok_or(ParseError::InvalidXRef)?;
1395 let gen_str = &gen_part[..gen_part.len() - 1];
1396 if gen_str.is_empty() {
1397 (0, Some(flag_char))
1399 } else {
1400 let gen = gen_str
1401 .parse::<u16>()
1402 .map_err(|_| ParseError::InvalidXRef)?;
1403 (gen, Some(flag_char))
1404 }
1405 } else {
1406 let gen = gen_part
1408 .parse::<u16>()
1409 .map_err(|_| ParseError::InvalidXRef)?;
1410 (gen, None)
1411 }
1412 } else {
1413 (0, None)
1414 };
1415
1416 let in_use = if let Some(flag_char) = flag_from_gen {
1418 match flag_char {
1420 'n' => true,
1421 'f' => false,
1422 _ => true, }
1424 } else if parts.len() >= 3 {
1425 match parts[2].chars().next() {
1427 Some('n') => true,
1428 Some('f') => false,
1429 _ => {
1430 #[cfg(debug_assertions)]
1432 tracing::warn!(" Invalid xref flag '{}', assuming 'n'", parts[2]);
1433 true
1434 }
1435 }
1436 } else {
1437 true
1439 };
1440
1441 Ok(XRefEntry {
1442 offset,
1443 generation,
1444 in_use,
1445 })
1446 }
1447
1448 pub fn get_entry(&self, obj_num: u32) -> Option<&XRefEntry> {
1450 self.entries.get(&obj_num)
1451 }
1452
1453 pub fn get_entry_mut(&mut self, obj_num: u32) -> Option<&mut XRefEntry> {
1455 self.entries.get_mut(&obj_num)
1456 }
1457
1458 pub fn trailer(&self) -> Option<&super::objects::PdfDictionary> {
1460 self.trailer.as_ref()
1461 }
1462
1463 pub fn xref_offset(&self) -> u64 {
1465 self.xref_offset
1466 }
1467
1468 pub fn len(&self) -> usize {
1470 self.entries.len()
1471 }
1472
1473 pub fn is_empty(&self) -> bool {
1475 self.entries.is_empty()
1476 }
1477
1478 pub fn iter(&self) -> impl Iterator<Item = (&u32, &XRefEntry)> {
1480 self.entries.iter()
1481 }
1482
1483 pub fn get_extended_entry(&self, obj_num: u32) -> Option<&XRefEntryExt> {
1485 self.extended_entries.get(&obj_num)
1486 }
1487
1488 pub fn is_compressed(&self, obj_num: u32) -> bool {
1490 self.extended_entries
1491 .get(&obj_num)
1492 .map(|e| e.compressed_info.is_some())
1493 .unwrap_or(false)
1494 }
1495
1496 pub fn add_entry(&mut self, obj_num: u32, entry: XRefEntry) {
1498 self.entries.insert(obj_num, entry);
1499 }
1500
1501 fn add_headers_latest_wins(&mut self, headers: &[ObjHeader], check_extended: bool) {
1508 let mut latest: HashMap<u32, &ObjHeader> = HashMap::new();
1509 for h in headers {
1510 latest.insert(h.obj_num, h); }
1512 for (obj_num, h) in latest {
1513 let occupied = self.entries.contains_key(&obj_num)
1514 || (check_extended && self.extended_entries.contains_key(&obj_num));
1515 if !occupied {
1516 self.add_entry(
1517 obj_num,
1518 XRefEntry {
1519 offset: h.offset,
1520 generation: h.generation,
1521 in_use: true,
1522 },
1523 );
1524 }
1525 }
1526 }
1527
1528 pub fn set_trailer(&mut self, trailer: super::objects::PdfDictionary) {
1530 self.trailer = Some(trailer);
1531 }
1532
1533 pub fn add_extended_entry(&mut self, obj_num: u32, entry: XRefEntryExt) {
1535 self.extended_entries.insert(obj_num, entry);
1536 }
1537}
1538
1539#[derive(Debug, Clone)]
1542pub struct XRefStream {
1543 stream: super::objects::PdfStream,
1545 entries: HashMap<u32, XRefEntry>,
1547 extended_entries: HashMap<u32, XRefEntryExt>,
1549}
1550
1551impl XRefStream {
1552 pub fn parse(stream: super::objects::PdfStream) -> ParseResult<Self> {
1554 let mut xref_stream = Self {
1555 stream,
1556 entries: HashMap::new(),
1557 extended_entries: HashMap::new(),
1558 };
1559
1560 xref_stream.decode_entries()?;
1561 Ok(xref_stream)
1562 }
1563
1564 fn decode_entries(&mut self) -> ParseResult<()> {
1566 let dict = &self.stream.dict;
1568
1569 let size = dict
1571 .get("Size")
1572 .and_then(|obj| obj.as_integer())
1573 .ok_or_else(|| ParseError::MissingKey("Size".to_string()))?;
1574
1575 let index = match dict.get("Index") {
1577 Some(obj) => {
1578 let array = obj.as_array().ok_or_else(|| ParseError::SyntaxError {
1579 position: 0,
1580 message: "Index must be an array".to_string(),
1581 })?;
1582
1583 let mut pairs = Vec::new();
1585 for chunk in array.0.chunks(2) {
1586 if chunk.len() != 2 {
1587 return Err(ParseError::SyntaxError {
1588 position: 0,
1589 message: "Index array must have even number of elements".to_string(),
1590 });
1591 }
1592 let first = chunk[0]
1593 .as_integer()
1594 .ok_or_else(|| ParseError::SyntaxError {
1595 position: 0,
1596 message: "Index values must be integers".to_string(),
1597 })? as u32;
1598 let count = chunk[1]
1599 .as_integer()
1600 .ok_or_else(|| ParseError::SyntaxError {
1601 position: 0,
1602 message: "Index values must be integers".to_string(),
1603 })? as u32;
1604 pairs.push((first, count));
1605 }
1606 pairs
1607 }
1608 None => {
1609 vec![(0, size as u32)]
1611 }
1612 };
1613
1614 let w_array = dict
1616 .get("W")
1617 .and_then(|obj| obj.as_array())
1618 .ok_or_else(|| ParseError::MissingKey("W".to_string()))?;
1619
1620 if w_array.len() != 3 {
1621 return Err(ParseError::SyntaxError {
1622 position: 0,
1623 message: "W array must have exactly 3 elements".to_string(),
1624 });
1625 }
1626
1627 let w: Vec<usize> = w_array
1628 .0
1629 .iter()
1630 .map(|obj| {
1631 obj.as_integer()
1632 .ok_or_else(|| ParseError::SyntaxError {
1633 position: 0,
1634 message: "W values must be integers".to_string(),
1635 })
1636 .map(|i| i as usize)
1637 })
1638 .collect::<ParseResult<Vec<_>>>()?;
1639
1640 let data = self.stream.decode(&ParseOptions::default())?;
1642 let mut offset = 0;
1643
1644 for (first_obj_num, count) in index {
1646 for i in 0..count {
1647 if offset + w[0] + w[1] + w[2] > data.len() {
1648 return Err(ParseError::SyntaxError {
1649 position: 0,
1650 message: "Xref stream data truncated".to_string(),
1651 });
1652 }
1653
1654 let field1 = Self::read_field(&data[offset..], w[0]);
1656 offset += w[0];
1657
1658 let field2 = Self::read_field(&data[offset..], w[1]);
1659 offset += w[1];
1660
1661 let field3 = Self::read_field(&data[offset..], w[2]);
1662 offset += w[2];
1663
1664 let entry_info =
1666 XRefEntryInfo::new(XRefEntryType::from_value(field1), field2, field3);
1667
1668 let entry = match entry_info.entry_type {
1670 XRefEntryType::Free => XRefEntry {
1671 offset: entry_info.field2,
1672 generation: entry_info.field3 as u16,
1673 in_use: false,
1674 },
1675 XRefEntryType::Uncompressed => XRefEntry {
1676 offset: entry_info.field2,
1677 generation: entry_info.field3 as u16,
1678 in_use: true,
1679 },
1680 XRefEntryType::Compressed => {
1681 let ext_entry = XRefEntryExt {
1683 basic: XRefEntry {
1684 offset: 0,
1685 generation: 0,
1686 in_use: true,
1687 },
1688 compressed_info: entry_info.get_compressed_info(),
1689 };
1690 self.extended_entries
1691 .insert(first_obj_num + i, ext_entry.clone());
1692 ext_entry.basic
1693 }
1694 XRefEntryType::Custom(_type_num) => {
1695 #[cfg(debug_assertions)]
1698 tracing::debug!(
1699 "Note: Custom xref entry type {} for object {} (treating as in-use)",
1700 _type_num,
1701 first_obj_num + i
1702 );
1703
1704 let ext_entry = XRefEntryExt {
1706 basic: XRefEntry {
1707 offset: entry_info.field2,
1708 generation: entry_info.field3 as u16,
1709 in_use: entry_info.entry_type.is_in_use(),
1710 },
1711 compressed_info: None,
1712 };
1713 self.extended_entries
1714 .insert(first_obj_num + i, ext_entry.clone());
1715 ext_entry.basic
1716 }
1717 };
1718
1719 self.entries.insert(first_obj_num + i, entry);
1720 }
1721 }
1722
1723 Ok(())
1724 }
1725
1726 fn read_field(data: &[u8], width: usize) -> u64 {
1728 let mut value = 0u64;
1729 for i in 0..width {
1730 if i < data.len() {
1731 value = (value << 8) | (data[i] as u64);
1732 }
1733 }
1734 value
1735 }
1736
1737 pub fn get_entry(&self, obj_num: u32) -> Option<&XRefEntry> {
1739 self.entries.get(&obj_num)
1740 }
1741
1742 pub fn trailer(&self) -> &super::objects::PdfDictionary {
1744 &self.stream.dict
1745 }
1746}
1747
1748#[cfg(test)]
1749mod tests {
1750 use super::*;
1751
1752 use crate::parser::objects::{PdfDictionary, PdfObject};
1753 use std::io::Cursor;
1754
1755 #[test]
1758 fn test_scan_object_headers_finds_simple_headers() {
1759 let mut buf = Vec::new();
1760 buf.extend_from_slice(b"%PDF-1.7\n");
1761 let off1 = buf.len() as u64;
1762 buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
1763 let off2 = buf.len() as u64;
1764 buf.extend_from_slice(b"2 0 obj\n<< /Type /Pages >>\nendobj\n");
1765 let off10 = buf.len() as u64;
1766 buf.extend_from_slice(b"10 0 obj\n<< /Length 0 >>\nendobj\n");
1767
1768 let mut cursor = Cursor::new(buf);
1769 let headers = scan_object_headers(&mut cursor).unwrap();
1770
1771 assert_eq!(
1772 headers,
1773 vec![
1774 ObjHeader {
1775 obj_num: 1,
1776 generation: 0,
1777 offset: off1
1778 },
1779 ObjHeader {
1780 obj_num: 2,
1781 generation: 0,
1782 offset: off2
1783 },
1784 ObjHeader {
1785 obj_num: 10,
1786 generation: 0,
1787 offset: off10
1788 },
1789 ]
1790 );
1791 }
1792
1793 #[test]
1794 fn test_scan_object_headers_chunk_invariant_across_boundaries() {
1795 let mut buf = Vec::new();
1798 buf.extend_from_slice(b"%PDF-1.7\n");
1799 let mut expected: Vec<(u32, u64)> = Vec::new();
1800 for i in 1..=50u32 {
1801 for _ in 0..(i as usize % 7) {
1802 buf.push(b' ');
1803 }
1804 buf.push(b'\n');
1805 expected.push((i, buf.len() as u64));
1806 buf.extend_from_slice(format!("{i} 0 obj\n<< /N {i} >>\nendobj\n").as_bytes());
1807 }
1808
1809 let reference =
1811 scan_object_headers_chunked(&mut Cursor::new(buf.clone()), buf.len().max(1)).unwrap();
1812
1813 let got: Vec<(u32, u64)> = reference.iter().map(|h| (h.obj_num, h.offset)).collect();
1814 assert_eq!(
1815 got, expected,
1816 "reference scan disagrees with hand-computed offsets"
1817 );
1818
1819 for cs in [1usize, 2, 3, 7, 13, 16, 64, 256] {
1820 let chunked = scan_object_headers_chunked(&mut Cursor::new(buf.clone()), cs).unwrap();
1821 assert_eq!(chunked, reference, "scan mismatch at chunk_size={cs}");
1822 }
1823 }
1824
1825 #[test]
1826 fn test_scan_object_headers_ignores_endobj_keyword() {
1827 let mut buf = Vec::new();
1829 buf.extend_from_slice(b"%PDF\n");
1830 let off = buf.len() as u64;
1831 buf.extend_from_slice(b"7 0 obj\n<< >>\nendobj\nendobj\n");
1832
1833 let headers = scan_object_headers(&mut Cursor::new(buf)).unwrap();
1834 assert_eq!(
1835 headers,
1836 vec![ObjHeader {
1837 obj_num: 7,
1838 generation: 0,
1839 offset: off
1840 }]
1841 );
1842 }
1843
1844 #[test]
1845 fn test_scan_object_headers_carry_truncation_no_newline_run() {
1846 let mut buf = Vec::new();
1851 buf.extend_from_slice(b"%PDF-1.7\n");
1852
1853 let filler: Vec<u8> = (0..2000u32)
1855 .map(|i| if i % 5 == 0 { b' ' } else { b'7' })
1856 .collect();
1857
1858 buf.extend_from_slice(&filler);
1860 buf.push(b'\n');
1861 let off_a = buf.len() as u64;
1862 buf.extend_from_slice(b"5 0 obj\n<< >>\nendobj\n");
1863
1864 buf.extend_from_slice(&filler);
1867 buf.extend_from_slice(b"7 0 obj\n<< >>\nendobj\n");
1868
1869 let reference =
1870 scan_object_headers_chunked(&mut Cursor::new(buf.clone()), buf.len().max(1)).unwrap();
1871
1872 for cs in [16usize, 64, 256] {
1874 let chunked = scan_object_headers_chunked(&mut Cursor::new(buf.clone()), cs).unwrap();
1875 assert_eq!(
1876 chunked, reference,
1877 "carry-truncation mismatch at chunk_size={cs}"
1878 );
1879 }
1880
1881 assert!(reference
1884 .iter()
1885 .any(|h| h.obj_num == 5 && h.offset == off_a));
1886 assert!(!reference.iter().any(|h| h.obj_num == 7));
1887 }
1888
1889 #[test]
1890 fn test_scan_object_headers_empty_input() {
1891 let headers = scan_object_headers(&mut Cursor::new(Vec::new())).unwrap();
1892 assert!(headers.is_empty());
1893 }
1894
1895 #[test]
1896 fn test_scan_object_headers_reads_in_bounded_chunks() {
1897 struct MaxReadReader<R> {
1899 inner: R,
1900 max_read: usize,
1901 }
1902 impl<R: Read> Read for MaxReadReader<R> {
1903 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1904 self.max_read = self.max_read.max(buf.len());
1905 self.inner.read(buf)
1906 }
1907 }
1908 impl<R: Seek> Seek for MaxReadReader<R> {
1909 fn seek(&mut self, p: SeekFrom) -> std::io::Result<u64> {
1910 self.inner.seek(p)
1911 }
1912 }
1913
1914 let mut buf = Vec::new();
1915 buf.extend_from_slice(b"%PDF\n");
1916 for i in 1..=2000u32 {
1917 buf.extend_from_slice(format!("{i} 0 obj\n<< >>\nendobj\n").as_bytes());
1918 }
1919 let total = buf.len();
1920 assert!(
1921 total > 8192,
1922 "fixture must exceed the chunk size to be meaningful"
1923 );
1924
1925 let mut r = MaxReadReader {
1926 inner: Cursor::new(buf),
1927 max_read: 0,
1928 };
1929 let headers = scan_object_headers_chunked(&mut r, 4096).unwrap();
1930
1931 assert_eq!(headers.len(), 2000);
1932 assert_eq!(headers[0].obj_num, 1);
1933 assert_eq!(headers[1999].obj_num, 2000);
1934 assert!(
1937 r.max_read <= 4096,
1938 "scanner requested {} bytes in a single read (chunk=4096, file={total}); not bounded",
1939 r.max_read
1940 );
1941 }
1942
1943 #[test]
1944 fn test_read_object_window_bounded_and_correct() {
1945 struct MaxReadReader<R> {
1953 inner: R,
1954 max_read: usize,
1955 }
1956 impl<R: Read> Read for MaxReadReader<R> {
1957 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1958 self.max_read = self.max_read.max(buf.len());
1959 self.inner.read(buf)
1960 }
1961 }
1962 impl<R: Seek> Seek for MaxReadReader<R> {
1963 fn seek(&mut self, p: SeekFrom) -> std::io::Result<u64> {
1964 self.inner.seek(p)
1965 }
1966 }
1967
1968 let mut buf = Vec::new();
1969 buf.extend_from_slice(b"%PDF-1.7\n");
1970 let cat_off = buf.len() as u64;
1971 buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1972 buf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
1973 buf.extend_from_slice(b"3 0 obj\n<< /Type /Page >>\nstream\n");
1976 buf.extend(std::iter::repeat(b'x').take(200 * 1024));
1977 buf.extend_from_slice(b"\nendstream\nendobj\n");
1978 let total = buf.len();
1979 assert!(
1980 total > 64 * 1024,
1981 "fixture must exceed the scan chunk size to be meaningful"
1982 );
1983
1984 let mut r = MaxReadReader {
1985 inner: Cursor::new(buf),
1986 max_read: 0,
1987 };
1988
1989 let (offset, window) = read_object_window(&mut r, 1, 64 * 1024)
1990 .unwrap()
1991 .expect("object 1 must be locatable by bounded scan");
1992
1993 assert_eq!(offset, cat_off, "header offset must be the real line start");
1995 assert!(
1996 find_byte_pattern(&window, b"/Type /Catalog").is_some(),
1997 "window must contain the catalog dict"
1998 );
1999 assert!(
2000 find_byte_pattern(&window, b"/Pages 2 0 R").is_some(),
2001 "window must contain the /Pages reference"
2002 );
2003 assert!(
2005 r.max_read <= 64 * 1024,
2006 "locate requested {} bytes in a single read (file={total}); not bounded",
2007 r.max_read
2008 );
2009 }
2010
2011 #[test]
2012 fn test_scan_page_object_refs_matches_compact_type_page() {
2013 let mut buf = Vec::new();
2017 buf.extend_from_slice(b"%PDF-1.7\n");
2018 buf.extend_from_slice(b"1 0 obj\n<< /Type/Catalog /Pages 2 0 R >>\nendobj\n");
2019 buf.extend_from_slice(b"2 0 obj\n<< /Type/Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
2020 buf.extend_from_slice(b"3 0 obj\n<< /Type/Page /Parent 2 0 R >>\nendobj\n");
2021
2022 let mut r = Cursor::new(buf);
2023 let pages = scan_page_object_refs(&mut r).unwrap();
2024 assert_eq!(
2025 pages,
2026 vec![(3, 0)],
2027 "compact /Type/Page must be detected and compact /Type/Pages excluded"
2028 );
2029 }
2030
2031 #[test]
2032 fn test_scan_and_fill_adds_missing_preserves_present() {
2033 let mut buf = Vec::new();
2034 buf.extend_from_slice(b"%PDF-1.7\n");
2035 let off1 = buf.len() as u64;
2036 buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
2037 buf.extend_from_slice(b"2 0 obj\n<< /Type /Pages >>\nendobj\n");
2038 let off3 = buf.len() as u64;
2039 buf.extend_from_slice(b"3 0 obj\n<< >>\nendobj\n");
2040
2041 let mut table = XRefTable::new();
2042 table.add_entry(
2044 2,
2045 XRefEntry {
2046 offset: 99999,
2047 generation: 0,
2048 in_use: true,
2049 },
2050 );
2051
2052 let mut reader = BufReader::new(Cursor::new(buf));
2053 XRefTable::scan_and_fill_missing_objects(&mut reader, &mut table).unwrap();
2054
2055 assert_eq!(table.get_entry(1).map(|e| e.offset), Some(off1));
2057 assert_eq!(table.get_entry(3).map(|e| e.offset), Some(off3));
2058 assert_eq!(table.get_entry(2).map(|e| e.offset), Some(99999));
2060 }
2061
2062 #[test]
2063 fn test_recovery_finds_objects_and_catalog_root() {
2064 let mut buf = Vec::new();
2065 buf.extend_from_slice(b"%PDF-1.7\n");
2066 let off1 = buf.len() as u64;
2067 buf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
2068 let off2 = buf.len() as u64;
2069 buf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");
2070 let off3 = buf.len() as u64;
2071 buf.extend_from_slice(b"3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\n");
2072
2073 let mut reader = BufReader::new(Cursor::new(buf));
2074 let table =
2075 XRefTable::parse_with_recovery_options(&mut reader, &ParseOptions::default()).unwrap();
2076
2077 assert_eq!(table.get_entry(1).map(|e| e.offset), Some(off1));
2078 assert_eq!(table.get_entry(2).map(|e| e.offset), Some(off2));
2079 assert_eq!(table.get_entry(3).map(|e| e.offset), Some(off3));
2080
2081 let root = table.trailer().and_then(|t| t.get("Root")).cloned();
2083 assert_eq!(root, Some(PdfObject::Reference(1, 0)));
2084 }
2085
2086 #[test]
2087 fn test_recovery_uses_root_from_xref_stream() {
2088 let mut buf = Vec::new();
2089 buf.extend_from_slice(b"%PDF-1.7\n");
2090 buf.extend_from_slice(b"5 0 obj\n<< /Type /Catalog /Pages 6 0 R >>\nendobj\n");
2091 buf.extend_from_slice(b"6 0 obj\n<< /Type /Pages /Count 0 >>\nendobj\n");
2092 buf.extend_from_slice(
2094 b"9 0 obj\n<< /Type /XRef /Root 5 0 R /Size 10 >>\nstream\n....\nendstream\nendobj\n",
2095 );
2096
2097 let mut reader = BufReader::new(Cursor::new(buf));
2098 let table =
2099 XRefTable::parse_with_recovery_options(&mut reader, &ParseOptions::default()).unwrap();
2100
2101 let root = table.trailer().and_then(|t| t.get("Root")).cloned();
2102 assert_eq!(root, Some(PdfObject::Reference(5, 0)));
2103 }
2104
2105 #[test]
2106 fn test_recovery_empty_when_no_objects() {
2107 let mut reader = BufReader::new(Cursor::new(b"%PDF-1.7\nnothing useful here\n".to_vec()));
2108 let result = XRefTable::parse_with_recovery_options(&mut reader, &ParseOptions::default());
2109 assert!(matches!(result, Err(ParseError::InvalidXRef)));
2110 }
2111
2112 #[test]
2113 fn test_parse_xref_entry() {
2114 let entry1 = XRefTable::parse_xref_entry("0000000000 65535 f ").unwrap();
2115 assert_eq!(entry1.offset, 0);
2116 assert_eq!(entry1.generation, 65535);
2117 assert!(!entry1.in_use);
2118
2119 let entry2 = XRefTable::parse_xref_entry("0000000017 00000 n ").unwrap();
2120 assert_eq!(entry2.offset, 17);
2121 assert_eq!(entry2.generation, 0);
2122 assert!(entry2.in_use);
2123 }
2124
2125 #[test]
2126 fn test_parse_xref_entry_flexible() {
2127 let entry1 = XRefTable::parse_xref_entry("17 0 n").unwrap();
2131 assert_eq!(entry1.offset, 17);
2132 assert_eq!(entry1.generation, 0);
2133 assert!(entry1.in_use);
2134
2135 let entry2 = XRefTable::parse_xref_entry("123 5 f").unwrap();
2137 assert_eq!(entry2.offset, 123);
2138 assert_eq!(entry2.generation, 5);
2139 assert!(!entry2.in_use);
2140
2141 let entry3 = XRefTable::parse_xref_entry("456 n").unwrap();
2143 assert_eq!(entry3.offset, 456);
2144 assert_eq!(entry3.generation, 0);
2145 assert!(entry3.in_use);
2146
2147 let entry4 = XRefTable::parse_xref_entry("789 2").unwrap();
2149 assert_eq!(entry4.offset, 789);
2150 assert_eq!(entry4.generation, 2);
2151 assert!(entry4.in_use);
2152
2153 let entry5 = XRefTable::parse_xref_entry("1000 0n").unwrap();
2155 assert_eq!(entry5.offset, 1000);
2156 assert_eq!(entry5.generation, 0);
2157 assert!(entry5.in_use);
2158
2159 let entry6 = XRefTable::parse_xref_entry("2000 1f").unwrap();
2160 assert_eq!(entry6.offset, 2000);
2161 assert_eq!(entry6.generation, 1);
2162 assert!(!entry6.in_use);
2163
2164 let entry7 = XRefTable::parse_xref_entry("3000\t0\tn").unwrap();
2166 assert_eq!(entry7.offset, 3000);
2167 assert_eq!(entry7.generation, 0);
2168 assert!(entry7.in_use);
2169 }
2170
2171 #[test]
2172 fn test_parse_xref_entry_invalid_flag_fallback() {
2173 let entry = XRefTable::parse_xref_entry("100 0 x").unwrap();
2175 assert_eq!(entry.offset, 100);
2176 assert_eq!(entry.generation, 0);
2177 assert!(entry.in_use); }
2179
2180 #[test]
2181 fn test_parse_xref_entry_malformed() {
2182 let result = XRefTable::parse_xref_entry("");
2184 assert!(result.is_err());
2185
2186 let result = XRefTable::parse_xref_entry("abc 0 n");
2188 assert!(result.is_err());
2189
2190 let result = XRefTable::parse_xref_entry(" ");
2192 assert!(result.is_err());
2193 }
2194
2195 #[test]
2196 fn test_xref_table_new() {
2197 let table = XRefTable::new();
2198 assert!(table.entries.is_empty());
2199 assert!(table.extended_entries.is_empty());
2200 assert!(table.trailer.is_none());
2201 assert_eq!(table.xref_offset, 0);
2202 }
2203
2204 #[test]
2205 fn test_xref_table_default() {
2206 let table = XRefTable::default();
2207 assert!(table.entries.is_empty());
2208 assert!(table.extended_entries.is_empty());
2209 assert!(table.trailer.is_none());
2210 }
2211
2212 #[test]
2213 fn test_xref_entry_struct() {
2214 let entry = XRefEntry {
2215 offset: 12345,
2216 generation: 7,
2217 in_use: true,
2218 };
2219 assert_eq!(entry.offset, 12345);
2220 assert_eq!(entry.generation, 7);
2221 assert!(entry.in_use);
2222 }
2223
2224 #[test]
2225 fn test_xref_entry_equality() {
2226 let entry1 = XRefEntry {
2227 offset: 100,
2228 generation: 0,
2229 in_use: true,
2230 };
2231 let entry2 = XRefEntry {
2232 offset: 100,
2233 generation: 0,
2234 in_use: true,
2235 };
2236 assert_eq!(entry1, entry2);
2237 }
2238
2239 #[test]
2240 fn test_xref_entry_clone() {
2241 let entry = XRefEntry {
2242 offset: 999,
2243 generation: 3,
2244 in_use: false,
2245 };
2246 let cloned = entry;
2247 assert_eq!(cloned.offset, 999);
2248 assert_eq!(cloned.generation, 3);
2249 assert!(!cloned.in_use);
2250 }
2251
2252 #[test]
2253 fn test_xref_entry_ext() {
2254 let ext_entry = XRefEntryExt {
2255 basic: XRefEntry {
2256 offset: 500,
2257 generation: 0,
2258 in_use: true,
2259 },
2260 compressed_info: Some((10, 5)),
2261 };
2262 assert_eq!(ext_entry.basic.offset, 500);
2263 assert_eq!(ext_entry.compressed_info, Some((10, 5)));
2264 }
2265
2266 #[test]
2267 fn test_xref_entry_ext_no_compression() {
2268 let ext_entry = XRefEntryExt {
2269 basic: XRefEntry {
2270 offset: 1000,
2271 generation: 1,
2272 in_use: true,
2273 },
2274 compressed_info: None,
2275 };
2276 assert!(ext_entry.compressed_info.is_none());
2277 }
2278
2279 #[test]
2280 fn test_add_entry() {
2281 let mut table = XRefTable::new();
2282 table.add_entry(
2283 5,
2284 XRefEntry {
2285 offset: 1000,
2286 generation: 0,
2287 in_use: true,
2288 },
2289 );
2290 assert_eq!(table.entries.len(), 1);
2291 assert!(table.entries.contains_key(&5));
2292 }
2293
2294 #[test]
2295 fn test_get_entry() {
2296 let mut table = XRefTable::new();
2297 let entry = XRefEntry {
2298 offset: 2000,
2299 generation: 1,
2300 in_use: true,
2301 };
2302 table.add_entry(10, entry);
2303
2304 let retrieved = table.get_entry(10);
2305 assert!(retrieved.is_some());
2306 assert_eq!(retrieved.unwrap().offset, 2000);
2307
2308 let missing = table.get_entry(999);
2309 assert!(missing.is_none());
2310 }
2311
2312 #[test]
2313 fn test_set_trailer() {
2314 let mut table = XRefTable::new();
2315 let mut trailer = PdfDictionary::new();
2316 trailer.insert("Size".to_string(), PdfObject::Integer(10));
2317
2318 table.set_trailer(trailer.clone());
2319 assert!(table.trailer.is_some());
2320 assert_eq!(
2321 table.trailer().unwrap().get("Size"),
2322 Some(&PdfObject::Integer(10))
2323 );
2324 }
2325
2326 #[test]
2327 fn test_parse_xref_entry_invalid() {
2328 let result = XRefTable::parse_xref_entry("0000000000 65535");
2330 assert!(result.is_ok()); let result = XRefTable::parse_xref_entry("not_a_number 65535 f ");
2334 assert!(result.is_err());
2335
2336 let result = XRefTable::parse_xref_entry("0000000000 65535 x ");
2338 assert!(result.is_ok()); assert!(result.unwrap().in_use); }
2341
2342 #[test]
2343 fn test_parse_xref_entry_various_offsets() {
2344 let entry = XRefTable::parse_xref_entry("0000000001 00000 n ").unwrap();
2346 assert_eq!(entry.offset, 1);
2347
2348 let entry = XRefTable::parse_xref_entry("9999999999 00000 n ").unwrap();
2350 assert_eq!(entry.offset, 9999999999);
2351
2352 let entry = XRefTable::parse_xref_entry("0000000000 65535 f ").unwrap();
2354 assert_eq!(entry.generation, 65535);
2355 }
2356
2357 #[test]
2358 fn test_add_extended_entry() {
2359 let mut table = XRefTable::new();
2360 let ext_entry = XRefEntryExt {
2361 basic: XRefEntry {
2362 offset: 0,
2363 generation: 0,
2364 in_use: true,
2365 },
2366 compressed_info: Some((5, 10)),
2367 };
2368
2369 table.add_extended_entry(15, ext_entry);
2370 assert_eq!(table.extended_entries.len(), 1);
2371 assert!(table.extended_entries.contains_key(&15));
2372 }
2373
2374 #[test]
2375 fn test_get_extended_entry() {
2376 let mut table = XRefTable::new();
2377 let ext_entry = XRefEntryExt {
2378 basic: XRefEntry {
2379 offset: 0,
2380 generation: 0,
2381 in_use: true,
2382 },
2383 compressed_info: Some((20, 3)),
2384 };
2385
2386 table.add_extended_entry(7, ext_entry);
2387
2388 let retrieved = table.get_extended_entry(7);
2389 assert!(retrieved.is_some());
2390 assert_eq!(retrieved.unwrap().compressed_info, Some((20, 3)));
2391 }
2392
2393 #[test]
2394 fn test_xref_offset() {
2395 let mut table = XRefTable::new();
2396 assert_eq!(table.xref_offset(), 0);
2397
2398 table.xref_offset = 12345;
2399 assert_eq!(table.xref_offset(), 12345);
2400 }
2401
2402 #[test]
2403 fn test_find_xref_offset_simple() {
2404 let pdf_data = b"startxref\n12345\n%%EOF";
2405 let cursor = Cursor::new(pdf_data.to_vec());
2406 let mut reader = BufReader::new(cursor);
2407
2408 let offset = XRefTable::find_xref_offset(&mut reader).unwrap();
2409 assert_eq!(offset, 12345);
2410 }
2411
2412 #[test]
2413 fn test_find_xref_offset_with_spaces() {
2414 let pdf_data = b"startxref \n 12345 \n%%EOF";
2415 let cursor = Cursor::new(pdf_data.to_vec());
2416 let mut reader = BufReader::new(cursor);
2417
2418 let offset = XRefTable::find_xref_offset(&mut reader).unwrap();
2419 assert_eq!(offset, 12345);
2420 }
2421
2422 #[test]
2423 fn test_find_xref_offset_missing() {
2424 let pdf_data = b"no startxref here";
2425 let cursor = Cursor::new(pdf_data.to_vec());
2426 let mut reader = BufReader::new(cursor);
2427
2428 let result = XRefTable::find_xref_offset(&mut reader);
2429 assert!(result.is_err());
2430 }
2431
2432 #[test]
2433 fn test_trailer_getter() {
2434 let mut table = XRefTable::new();
2435 assert!(table.trailer().is_none());
2436
2437 let trailer = PdfDictionary::new();
2438 table.set_trailer(trailer);
2439 assert!(table.trailer().is_some());
2440 }
2441
2442 #[test]
2443 fn test_xref_table_clone() {
2444 let mut table = XRefTable::new();
2445 table.add_entry(
2446 1,
2447 XRefEntry {
2448 offset: 100,
2449 generation: 0,
2450 in_use: true,
2451 },
2452 );
2453 table.xref_offset = 5000;
2454
2455 let cloned = table.clone();
2456 assert_eq!(cloned.entries.len(), 1);
2457 assert_eq!(cloned.xref_offset, 5000);
2458 }
2459
2460 #[test]
2461 fn test_parse_obj_header() {
2462 assert_eq!(parse_obj_header_bytes(b"1 0 obj"), Some((1, 0)));
2464 assert_eq!(parse_obj_header_bytes(b"123 5 obj"), Some((123, 5)));
2465 assert_eq!(parse_obj_header_bytes(b" 42 3 obj "), Some((42, 3)));
2466
2467 assert_eq!(parse_obj_header_bytes(b"1 obj"), None);
2469 assert_eq!(parse_obj_header_bytes(b"abc 0 obj"), None);
2470 assert_eq!(parse_obj_header_bytes(b"1 0 object"), None);
2471 assert_eq!(parse_obj_header_bytes(b""), None);
2472 }
2473
2474 #[test]
2475 fn test_xref_recovery_parsing() {
2476 let pdf_content =
2478 b"1 0 obj\n<< /Type /Catalog >>\nendobj\n2 0 obj\n<< /Type /Page >>\nendobj\n";
2479 let mut reader = BufReader::new(Cursor::new(pdf_content));
2480
2481 let table = XRefTable::parse_with_recovery(&mut reader).unwrap();
2482
2483 assert_eq!(table.len(), 2);
2485 assert!(table.get_entry(1).is_some());
2486 assert!(table.get_entry(2).is_some());
2487
2488 assert!(table.get_entry(1).unwrap().in_use);
2490 assert!(table.get_entry(2).unwrap().in_use);
2491 }
2492
2493 #[test]
2494 fn test_xref_recovery_no_objects() {
2495 let pdf_content = b"This is not a PDF file\nNo objects here\n";
2497 let mut reader = BufReader::new(Cursor::new(pdf_content));
2498
2499 let result = XRefTable::parse_with_recovery(&mut reader);
2500 assert!(result.is_err());
2501 }
2502
2503 #[test]
2504 fn test_offset_validation() {
2505 let pdf_data = b"small file";
2506 let mut reader = BufReader::new(Cursor::new(pdf_data));
2507
2508 assert!(XRefTable::validate_offset(&mut reader, 5).is_ok());
2510
2511 assert!(XRefTable::validate_offset(&mut reader, 100).is_err());
2513
2514 assert!(XRefTable::validate_offset(&mut reader, 10).is_err());
2516 }
2517
2518 #[test]
2519 fn test_xref_parse_with_fallback() {
2520 let pdf_content =
2524 b"1 0 obj\n<< /Type /Catalog >>\nendobj\n2 0 obj\n<< /Type /Page >>\nendobj\n";
2525
2526 let mut reader = BufReader::new(Cursor::new(pdf_content.to_vec()));
2528 let table = XRefTable::parse(&mut reader).expect("recovery rebuilds xref from object scan");
2529 assert!(table.get_entry(1).is_some(), "object 1 indexed by scan");
2530 assert!(table.get_entry(2).is_some(), "object 2 indexed by scan");
2531
2532 let mut reader = BufReader::new(Cursor::new(pdf_content.to_vec()));
2535 match XRefTable::parse_with_options(&mut reader, &ParseOptions::strict()) {
2536 Err(ParseError::InvalidXRef) => {}
2537 Ok(_) => panic!("strict() must fail on missing xref, not recover"),
2538 Err(other) => panic!("strict() must fail with InvalidXRef, got: {other:?}"),
2539 }
2540 }
2541
2542 #[test]
2543 fn test_xref_entry_creation() {
2544 let entry = XRefEntry {
2545 offset: 1234,
2546 generation: 5,
2547 in_use: true,
2548 };
2549
2550 assert_eq!(entry.offset, 1234);
2551 assert_eq!(entry.generation, 5);
2552 assert!(entry.in_use);
2553 }
2554
2555 #[test]
2556 fn test_xref_entry_ext_creation() {
2557 let basic = XRefEntry {
2558 offset: 5000,
2559 generation: 0,
2560 in_use: true,
2561 };
2562
2563 let ext = XRefEntryExt {
2564 basic: basic.clone(),
2565 compressed_info: Some((10, 3)),
2566 };
2567
2568 assert_eq!(ext.basic.offset, 5000);
2569 assert_eq!(ext.compressed_info, Some((10, 3)));
2570 }
2571
2572 #[test]
2573 fn test_xref_table_new_advanced() {
2574 let table = XRefTable::new();
2575 assert_eq!(table.entries.len(), 0);
2576 assert_eq!(table.extended_entries.len(), 0);
2577 assert!(table.trailer.is_none());
2578 assert_eq!(table.xref_offset, 0);
2579 }
2580
2581 #[test]
2582 fn test_xref_table_default_advanced() {
2583 let table = XRefTable::default();
2584 assert_eq!(table.entries.len(), 0);
2585 assert!(table.trailer.is_none());
2586 }
2587
2588 #[test]
2589 fn test_xref_table_add_entry() {
2590 let mut table = XRefTable::new();
2591
2592 let entry1 = XRefEntry {
2593 offset: 100,
2594 generation: 0,
2595 in_use: true,
2596 };
2597 table.add_entry(1, entry1);
2598 let entry2 = XRefEntry {
2599 offset: 200,
2600 generation: 1,
2601 in_use: false,
2602 };
2603 table.add_entry(2, entry2);
2604
2605 assert_eq!(table.len(), 2);
2606
2607 let entry1 = table.get_entry(1).unwrap();
2608 assert_eq!(entry1.offset, 100);
2609 assert_eq!(entry1.generation, 0);
2610 assert!(entry1.in_use);
2611
2612 let entry2 = table.get_entry(2).unwrap();
2613 assert_eq!(entry2.offset, 200);
2614 assert_eq!(entry2.generation, 1);
2615 assert!(!entry2.in_use);
2616 }
2617
2618 #[test]
2619 fn test_xref_table_add_extended_entry() {
2620 let mut table = XRefTable::new();
2621
2622 let basic_entry = XRefEntry {
2623 offset: 0,
2624 generation: 0,
2625 in_use: true,
2626 };
2627
2628 let extended_entry = XRefEntryExt {
2629 basic: basic_entry,
2630 compressed_info: Some((10, 2)),
2631 };
2632
2633 table.add_extended_entry(5, extended_entry);
2634
2635 let ext = table.get_extended_entry(5);
2637 assert!(ext.is_some());
2638 if let Some(ext) = ext {
2639 assert_eq!(ext.compressed_info, Some((10, 2)));
2640 }
2641
2642 assert!(table.is_compressed(5));
2643 }
2644
2645 #[test]
2646 fn test_xref_table_get_nonexistent() {
2647 let table = XRefTable::new();
2648 assert!(table.get_entry(999).is_none());
2649 assert!(table.get_extended_entry(999).is_none());
2650 }
2651
2652 #[test]
2653 fn test_xref_table_update_entry() {
2654 let mut table = XRefTable::new();
2655
2656 let entry1 = XRefEntry {
2658 offset: 100,
2659 generation: 0,
2660 in_use: true,
2661 };
2662 table.add_entry(1, entry1);
2663
2664 let entry2 = XRefEntry {
2666 offset: 200,
2667 generation: 1,
2668 in_use: false,
2669 };
2670 table.add_entry(1, entry2);
2671
2672 let entry = table.get_entry(1).unwrap();
2674 assert_eq!(entry.offset, 200);
2675 assert_eq!(entry.generation, 1);
2676 assert!(!entry.in_use);
2677 }
2678
2679 #[test]
2680 fn test_xref_table_set_trailer() {
2681 let mut table = XRefTable::new();
2682 assert!(table.trailer.is_none());
2683
2684 let mut trailer = PdfDictionary::new();
2685 trailer.insert("Size".to_string(), PdfObject::Integer(10));
2686
2687 table.set_trailer(trailer.clone());
2688 assert!(table.trailer.is_some());
2689 assert_eq!(table.trailer(), Some(&trailer));
2690 }
2691
2692 #[test]
2693 fn test_xref_table_offset() {
2694 let table = XRefTable::new();
2695 assert_eq!(table.xref_offset(), 0);
2696 }
2697
2698 #[test]
2699 fn test_parse_xref_entry_invalid_static() {
2700 let invalid_lines = vec![
2701 "not a valid entry".to_string(),
2702 "12345 abcde n".to_string(), ];
2704
2705 for line in invalid_lines {
2706 let result = XRefTable::parse_xref_entry(&line);
2707 assert!(result.is_err());
2708 }
2709
2710 let result = XRefTable::parse_xref_entry("12345 00000");
2712 assert!(result.is_ok());
2713 let entry = result.unwrap();
2714 assert_eq!(entry.offset, 12345);
2715 assert_eq!(entry.generation, 0);
2716 assert!(entry.in_use); }
2718
2719 #[test]
2720 fn test_xref_entry_operations() {
2721 let mut table = XRefTable::new();
2722
2723 let entry1 = XRefEntry {
2725 offset: 1234,
2726 generation: 5,
2727 in_use: true,
2728 };
2729
2730 let entry2 = XRefEntry {
2731 offset: 5678,
2732 generation: 10,
2733 in_use: false,
2734 };
2735
2736 table.add_entry(1, entry1);
2737 table.add_entry(2, entry2);
2738
2739 assert_eq!(table.len(), 2);
2740
2741 let retrieved1 = table.get_entry(1).unwrap();
2742 assert_eq!(retrieved1.offset, 1234);
2743 assert_eq!(retrieved1.generation, 5);
2744 assert!(retrieved1.in_use);
2745
2746 let retrieved2 = table.get_entry(2).unwrap();
2747 assert_eq!(retrieved2.offset, 5678);
2748 assert_eq!(retrieved2.generation, 10);
2749 assert!(!retrieved2.in_use);
2750 }
2751
2752 #[test]
2753 fn test_parse_xref_with_comments() {
2754 let pdf_content = b"%PDF-1.4\n\
27551 0 obj\n<< /Type /Catalog >>\nendobj\n\
2756xref\n\
2757% This is a comment\n\
27580 2\n\
27590000000000 65535 f \n\
27600000000015 00000 n \n\
2761% Another comment\n\
2762trailer\n\
2763<< /Size 2 /Root 1 0 R >>\n\
2764startxref\n\
276545\n\
2766%%EOF";
2767
2768 let mut reader = BufReader::new(Cursor::new(pdf_content));
2769 reader.seek(SeekFrom::Start(45)).unwrap(); let result = XRefTable::parse(&mut reader);
2772 assert!(result.is_ok());
2773 let table = result.unwrap();
2774 assert_eq!(table.len(), 2);
2775 }
2776
2777 #[test]
2778 fn test_parse_multiple_xref_sections() {
2779 let pdf_content = b"%PDF-1.4\n\
27801 0 obj\n<< /Type /Catalog >>\nendobj\n\
27812 0 obj\n<< /Type /Page >>\nendobj\n\
2782xref\n\
27830 2\n\
27840000000000 65535 f \n\
27850000000015 00000 n \n\
27865 2\n\
27870000000100 00000 n \n\
27880000000200 00000 n \n\
2789trailer\n\
2790<< /Size 7 /Root 1 0 R >>\n\
2791startxref\n\
279278\n\
2793%%EOF";
2794
2795 let mut reader = BufReader::new(Cursor::new(pdf_content));
2796 reader.seek(SeekFrom::Start(78)).unwrap(); let result = XRefTable::parse(&mut reader);
2799 assert!(result.is_ok());
2800 let table = result.unwrap();
2801 assert_eq!(table.len(), 4);
2803 assert!(table.get_entry(0).is_some());
2804 assert!(table.get_entry(1).is_some());
2805 assert!(table.get_entry(5).is_some());
2806 assert!(table.get_entry(6).is_some());
2807 }
2808
2809 #[test]
2810 fn test_parse_xref_with_prev() {
2811 let pdf_content = b"%PDF-1.4\n\
2813% First xref at 15\n\
2814xref\n\
28150 2\n\
28160000000000 65535 f \n\
28170000000100 00000 n \n\
2818trailer\n\
2819<< /Size 2 >>\n\
2820% Second xref at 100\n\
2821xref\n\
28222 1\n\
28230000000200 00000 n \n\
2824trailer\n\
2825<< /Size 3 /Prev 15 >>\n\
2826startxref\n\
2827100\n\
2828%%EOF";
2829
2830 let mut reader = BufReader::new(Cursor::new(pdf_content));
2831 let options = ParseOptions {
2832 lenient_syntax: true,
2833 ..Default::default()
2834 };
2835
2836 let result = XRefTable::parse_with_options(&mut reader, &options);
2837 assert!(result.is_ok() || result.is_err());
2839 }
2840
2841 #[test]
2842 fn test_invalid_xref_format() {
2843 let pdf_content = b"xref\ninvalid content\ntrailer";
2844 let mut reader = BufReader::new(Cursor::new(pdf_content));
2845
2846 let result = XRefTable::parse(&mut reader);
2847 assert!(result.is_err());
2848 }
2849
2850 #[test]
2851 fn test_xref_entry_overflow() {
2852 let mut table = XRefTable::new();
2853
2854 let entry = XRefEntry {
2856 offset: u64::MAX,
2857 generation: u16::MAX,
2858 in_use: true,
2859 };
2860 table.add_entry(u32::MAX, entry);
2861
2862 let entry = table.get_entry(u32::MAX).unwrap();
2863 assert_eq!(entry.offset, u64::MAX);
2864 assert_eq!(entry.generation, u16::MAX);
2865 }
2866
2867 #[test]
2868 fn test_xref_table_operations() {
2869 let mut table = XRefTable::new();
2870
2871 let entry1 = XRefEntry {
2873 offset: 100,
2874 generation: 0,
2875 in_use: true,
2876 };
2877
2878 let entry2 = XRefEntry {
2879 offset: 200,
2880 generation: 0,
2881 in_use: true,
2882 };
2883
2884 table.add_entry(1, entry1);
2885 table.add_entry(2, entry2);
2886
2887 assert_eq!(table.len(), 2);
2888 assert!(table.get_entry(1).is_some());
2889 assert!(table.get_entry(2).is_some());
2890 assert!(table.get_entry(3).is_none());
2891 }
2892
2893 #[test]
2894 fn test_xref_table_merge() {
2895 let mut table1 = XRefTable::new();
2896 let entry1 = XRefEntry {
2897 offset: 100,
2898 generation: 0,
2899 in_use: true,
2900 };
2901 table1.add_entry(1, entry1);
2902 let entry2 = XRefEntry {
2903 offset: 200,
2904 generation: 0,
2905 in_use: true,
2906 };
2907 table1.add_entry(2, entry2);
2908
2909 let mut table2 = XRefTable::new();
2910 let entry3 = XRefEntry {
2911 offset: 250,
2912 generation: 1,
2913 in_use: true,
2914 }; table2.add_entry(2, entry3);
2916 let entry4 = XRefEntry {
2917 offset: 300,
2918 generation: 0,
2919 in_use: true,
2920 }; table2.add_entry(3, entry4);
2922
2923 for i in 2..=3 {
2926 if let Some(entry) = table2.get_entry(i) {
2927 table1.add_entry(
2928 i,
2929 XRefEntry {
2930 offset: entry.offset,
2931 generation: entry.generation,
2932 in_use: entry.in_use,
2933 },
2934 );
2935 }
2936 }
2937
2938 assert_eq!(table1.len(), 3);
2939
2940 let entry2 = table1.get_entry(2).unwrap();
2942 assert_eq!(entry2.offset, 250);
2943 assert_eq!(entry2.generation, 1);
2944
2945 assert!(table1.get_entry(3).is_some());
2947 }
2948
2949 #[test]
2950 fn test_xref_recovery_with_stream() {
2951 let pdf_content = b"1 0 obj\n<< /Type /ObjStm /N 2 /First 10 >>\nstream\n12345678901 0 2 0\nendstream\nendobj\n";
2952 let mut reader = BufReader::new(Cursor::new(pdf_content));
2953
2954 let result = XRefTable::parse_with_recovery(&mut reader);
2955 assert!(result.is_ok() || result.is_err());
2957 }
2958
2959 #[test]
2960 fn test_xref_entry_equality_advanced() {
2961 let entry1 = XRefEntry {
2962 offset: 100,
2963 generation: 0,
2964 in_use: true,
2965 };
2966
2967 let entry2 = XRefEntry {
2968 offset: 100,
2969 generation: 0,
2970 in_use: true,
2971 };
2972
2973 let entry3 = XRefEntry {
2974 offset: 200,
2975 generation: 0,
2976 in_use: true,
2977 };
2978
2979 assert_eq!(entry1, entry2);
2980 assert_ne!(entry1, entry3);
2981 }
2982
2983 #[test]
2984 fn test_parse_options_effect() {
2985 let pdf_content = b"xref 0 1 invalid";
2986 let mut reader = BufReader::new(Cursor::new(pdf_content));
2987
2988 let strict_options = ParseOptions {
2990 lenient_syntax: false,
2991 ..Default::default()
2992 };
2993 let result = XRefTable::parse_with_options(&mut reader, &strict_options);
2994 assert!(result.is_err());
2995
2996 reader.seek(SeekFrom::Start(0)).unwrap();
2998 let lenient_options = ParseOptions {
2999 lenient_syntax: true,
3000 ..Default::default()
3001 };
3002 let result = XRefTable::parse_with_options(&mut reader, &lenient_options);
3003 assert!(result.is_err() || result.is_ok());
3005 }
3006
3007 #[test]
3008 fn test_circular_reference_detection() {
3009 let pdf_content = b"%PDF-1.4\n\
3011xref\n\
30120 1\n\
30130000000000 65535 f \n\
3014trailer\n\
3015<< /Size 1 /Prev 10 >>\n\
3016startxref\n\
301710\n\
3018%%EOF";
3019
3020 let mut reader = BufReader::new(Cursor::new(pdf_content));
3021
3022 let result = XRefTable::parse_with_incremental_updates(&mut reader);
3024 assert!(result.is_ok() || result.is_err());
3026 }
3027
3028 #[test]
3029 fn test_linearized_xref_detection() {
3030 let pdf_content = b"%PDF-1.4\n\
30321 0 obj\n\
3033<< /Linearized 1 /L 1234 /H [100 200] /O 5 /E 500 /N 10 /T 600 >>\n\
3034endobj\n\
3035xref\n\
30360 2\n\
30370000000000 65535 f \n\
30380000000009 00000 n \n\
3039trailer\n\
3040<< /Size 2 >>\n\
3041startxref\n\
304263\n\
3043%%EOF";
3044
3045 let mut reader = BufReader::new(Cursor::new(pdf_content));
3046
3047 let result = XRefTable::find_linearized_xref(&mut reader);
3049 assert!(result.is_ok());
3050
3051 let xref_pos = result.unwrap();
3054 assert_eq!(
3055 xref_pos, 90,
3056 "Expected xref at position 90, got {}",
3057 xref_pos
3058 );
3059 }
3060
3061 #[test]
3062 fn test_xref_stream_parsing() {
3063 let pdf_content = b"%PDF-1.5\n\
30661 0 obj\n\
3067<< /Type /XRef /Size 3 /W [1 2 1] /Length 12 >>\n\
3068stream\n\
3069\x00\x00\x00\x00\
3070\x01\x00\x10\x00\
3071\x01\x00\x20\x00\
3072endstream\n\
3073endobj\n\
3074startxref\n\
30759\n\
3076%%EOF";
3077
3078 let mut reader = BufReader::new(Cursor::new(pdf_content));
3079 reader.seek(SeekFrom::Start(9)).unwrap();
3080
3081 let result = XRefTable::parse(&mut reader);
3083 assert!(result.is_err() || result.is_ok());
3085 }
3086
3087 #[test]
3088 fn test_xref_validation_max_object_exceeds_size() {
3089 let pdf_content = b"%PDF-1.4\n\
3091xref\n\
30920 1\n\
30930000000000 65535 f \n\
309410 1\n\
30950000000100 00000 n \n\
3096trailer\n\
3097<< /Size 5 /Root 1 0 R >>\n\
3098startxref\n\
30999\n\
3100%%EOF";
3101
3102 let mut reader = BufReader::new(Cursor::new(pdf_content));
3103 reader.seek(SeekFrom::Start(9)).unwrap();
3104
3105 let result = XRefTable::parse(&mut reader);
3107 assert!(result.is_err());
3108 }
3109
3110 #[test]
3111 fn test_parse_with_options_lenient_vs_strict() {
3112 let pdf_content = b"%PDF-1.4\n\
3114xref\n\
31150 2\n\
31160000000000 65535 f \n\
31170000000015 00000 n \n\
3118trailer\n\
3119<< /Size 2 >>\n\
3120startxref\n\
31219\n\
3122%%EOF";
3123
3124 let mut reader = BufReader::new(Cursor::new(pdf_content));
3125
3126 let strict_options = ParseOptions {
3128 lenient_syntax: false,
3129 recover_from_stream_errors: false,
3130 ..Default::default()
3131 };
3132 reader.seek(SeekFrom::Start(9)).unwrap();
3133 let strict_result = XRefTable::parse_with_options(&mut reader, &strict_options);
3134
3135 let lenient_options = ParseOptions {
3137 lenient_syntax: true,
3138 recover_from_stream_errors: true,
3139 ..Default::default()
3140 };
3141 reader.seek(SeekFrom::Start(9)).unwrap();
3142 let lenient_result = XRefTable::parse_with_options(&mut reader, &lenient_options);
3143
3144 assert!(strict_result.is_ok());
3146 assert!(lenient_result.is_ok());
3147 }
3148
3149 #[test]
3150 fn test_xref_entry_with_attached_flag() {
3151 let entry1 = XRefTable::parse_xref_entry("12345 0n");
3153 assert!(entry1.is_ok());
3154 let entry1 = entry1.unwrap();
3155 assert_eq!(entry1.offset, 12345);
3156 assert_eq!(entry1.generation, 0);
3157 assert!(entry1.in_use);
3158
3159 let entry2 = XRefTable::parse_xref_entry("54321 1f");
3160 assert!(entry2.is_ok());
3161 let entry2 = entry2.unwrap();
3162 assert_eq!(entry2.offset, 54321);
3163 assert_eq!(entry2.generation, 1);
3164 assert!(!entry2.in_use);
3165 }
3166
3167 #[test]
3168 fn test_find_xref_offset_edge_cases() {
3169 use std::io::{BufReader, Cursor};
3171
3172 let content = b"garbage\nstartxref \n 123 \n%%EOF";
3174 let mut reader = BufReader::new(Cursor::new(content));
3175 let result = XRefTable::find_xref_offset(&mut reader);
3176 assert_eq!(result.unwrap(), 123);
3177
3178 let content = b"startxref\n999\n%%EOF";
3180 let mut reader = BufReader::new(Cursor::new(content));
3181 let result = XRefTable::find_xref_offset(&mut reader);
3182 assert_eq!(result.unwrap(), 999);
3183
3184 let content = b"startxref\n456";
3186 let mut reader = BufReader::new(Cursor::new(content));
3187 let result = XRefTable::find_xref_offset(&mut reader);
3188 assert!(result.is_ok() || result.is_err());
3190
3191 let content = b"some content\n%%EOF";
3193 let mut reader = BufReader::new(Cursor::new(content));
3194 let result = XRefTable::find_xref_offset(&mut reader);
3195 assert!(result.is_err());
3196 }
3197
3198 #[test]
3199 fn test_xref_subsection_incomplete() {
3200 let pdf_content = b"%PDF-1.4\n\
3202xref\n\
32030 5\n\
32040000000000 65535 f \n\
32050000000015 00000 n \n\
3206trailer\n\
3207<< /Size 5 >>\n\
3208startxref\n\
32099\n\
3210%%EOF";
3211
3212 let mut reader = BufReader::new(Cursor::new(pdf_content));
3213 reader.seek(SeekFrom::Start(9)).unwrap();
3214
3215 let result = XRefTable::parse(&mut reader);
3217 assert!(result.is_err() || result.is_ok());
3219 }
3220}
3221
3222type EncryptAndId = (
3223 Option<super::objects::PdfObject>,
3224 Option<super::objects::PdfObject>,
3225);
3226
3227fn extract_encrypt_and_id_from_trailer(tail: &[u8], options: &ParseOptions) -> EncryptAndId {
3246 if let Some(pos) = rfind_byte_pattern(tail, b"trailer") {
3248 let after = &tail[pos + b"trailer".len()..];
3249 let mut lexer =
3250 super::lexer::Lexer::new_with_options(std::io::Cursor::new(after), options.clone());
3251 if let Ok(super::objects::PdfObject::Dictionary(dict)) =
3252 super::objects::PdfObject::parse_with_options(&mut lexer, options)
3253 {
3254 let encrypt = dict.get("Encrypt").cloned();
3255 let id = dict.get("ID").cloned();
3256 if encrypt.is_some() || id.is_some() {
3257 return (encrypt, id);
3258 }
3259 }
3260 }
3261
3262 extract_encrypt_and_id_from_xref_stream(tail, options)
3264}
3265
3266fn extract_encrypt_and_id_from_xref_stream(tail: &[u8], options: &ParseOptions) -> EncryptAndId {
3274 let type_pos = match rfind_byte_pattern(tail, b"/Type/XRef")
3275 .or_else(|| rfind_byte_pattern(tail, b"/Type /XRef"))
3276 {
3277 Some(p) => p,
3278 None => return (None, None),
3279 };
3280
3281 let Some(obj_pos) = rfind_byte_pattern(&tail[..type_pos], b"obj") else {
3284 return (None, None);
3285 };
3286 let Some(rel) = find_byte_pattern(&tail[obj_pos..type_pos], b"<<") else {
3287 return (None, None);
3288 };
3289 let dict_start = obj_pos + rel;
3290
3291 let region = &tail[dict_start..];
3300 let mut lexer =
3301 super::lexer::Lexer::new_with_options(std::io::Cursor::new(region), options.clone());
3302 if !matches!(lexer.next_token(), Ok(super::lexer::Token::DictStart)) {
3304 return (None, None);
3305 }
3306 match super::objects::PdfObject::parse_dictionary_inner_with_options(&mut lexer, options) {
3307 Ok(dict) => (dict.get("Encrypt").cloned(), dict.get("ID").cloned()),
3308 Err(_) => (None, None),
3309 }
3310}
3311
3312fn extract_root_from_xref_stream(content: &str) -> Option<u32> {
3314 let lines: Vec<&str> = content.lines().collect();
3319 let mut in_xref_obj = false;
3320
3321 for (i, line) in lines.iter().enumerate() {
3322 if line.contains(" obj")
3324 && lines
3325 .get(i + 1)
3326 .map_or(false, |next| next.contains("/Type /XRef"))
3327 {
3328 in_xref_obj = true;
3329 continue;
3330 }
3331
3332 if in_xref_obj {
3334 if line.contains("endobj") {
3335 in_xref_obj = false;
3336 continue;
3337 }
3338
3339 if let Some(root_pos) = line.find("/Root ") {
3341 let after_root = &line[root_pos + 6..]; if let Some(space_pos) = after_root.find(' ') {
3345 let number_part = &after_root[..space_pos];
3346 if let Ok(root_obj) = number_part.parse::<u32>() {
3347 tracing::debug!("Extracted Root {} from XRef stream", root_obj);
3348 return Some(root_obj);
3349 }
3350 }
3351 }
3352 }
3353 }
3354
3355 None
3356}
3357
3358fn find_catalog_by_content<R: Read + Seek>(
3361 reader: &mut R,
3362 table: &XRefTable,
3363) -> ParseResult<Option<u32>> {
3364 let mut obj_numbers: Vec<u32> = table.entries.keys().copied().collect();
3367 obj_numbers.sort_unstable();
3368
3369 for obj_num in obj_numbers {
3370 let offset = match table.entries.get(&obj_num) {
3371 Some(entry) if entry.in_use => entry.offset,
3372 _ => continue,
3373 };
3374 if let Some(content) = read_object_content(reader, obj_num, offset)? {
3376 if content.contains("/Type /Catalog") {
3377 tracing::debug!(
3378 "Found catalog candidate at object {} (validated structure)",
3379 obj_num
3380 );
3381 return Ok(Some(obj_num));
3382 }
3383 }
3384 }
3385
3386 tracing::debug!("No valid catalog found by content search");
3387 Ok(None)
3388}