1use crate::error::PdfError;
8use crate::filters;
9use crate::lexer::{Lexer, Token, parse_dict_body};
10use crate::objects::PdfDict;
11
12#[derive(Debug, Clone, Copy)]
14pub enum XrefEntry {
15 InFile { offset: usize, generation: u16 },
17 InStream {
19 stream_obj_num: u32,
20 index_within: u16,
21 },
22 Free,
24}
25
26#[derive(Clone)]
28pub struct XrefTable {
29 entries: Vec<Option<XrefEntry>>,
31 pub trailer: PdfDict,
33}
34
35impl XrefTable {
36 #[cfg(test)]
38 pub(crate) fn empty() -> Self {
39 Self {
40 entries: Vec::new(),
41 trailer: PdfDict::new(),
42 }
43 }
44
45 pub fn get(&self, obj_num: u32) -> Option<&XrefEntry> {
47 self.entries.get(obj_num as usize).and_then(|e| e.as_ref())
48 }
49
50 pub fn entries(&self) -> &[Option<XrefEntry>] {
52 &self.entries
53 }
54
55 pub fn len(&self) -> usize {
57 self.entries.len()
58 }
59
60 pub fn is_empty(&self) -> bool {
62 self.entries.is_empty()
63 }
64}
65
66pub fn parse_xref(data: &[u8]) -> Result<XrefTable, PdfError> {
68 let startxref = match find_startxref(data) {
69 Ok(v) => v,
70 Err(PdfError::NoStartXref) => return rebuild_xref_from_scan(data),
71 Err(e) => return Err(e),
72 };
73
74 let header_offset = data.windows(5).position(|w| w == b"%PDF-").unwrap_or(0);
78
79 let pdf_headers: Vec<usize> = find_all_pdf_headers(data);
82
83 let mut sections: Vec<(Vec<(u32, XrefEntry)>, PdfDict, usize)> = Vec::new();
86 let mut offset = startxref + header_offset;
87 let mut visited = std::collections::HashSet::new();
88
89 let mut xref_failed = false;
90
91 if offset < data.len() && parse_xref_section(data, offset).is_err() && pdf_headers.len() > 1 {
94 for &h in pdf_headers.iter().skip(1).rev() {
95 let try_offset = startxref + h;
96 if try_offset < data.len() {
97 if let Ok((mut entries, trailer)) = parse_xref_section(data, try_offset) {
98 shift_xref_entries(&mut entries, h);
100 visited.insert(try_offset);
101 let prev = trailer.get_int(b"Prev").map(|v| v as usize + h);
102 sections.push((entries, trailer, try_offset));
103 if let Some(p) = prev {
104 offset = p;
105 } else {
106 xref_failed = false;
107 }
108 break;
109 }
110 }
111 }
112 }
113
114 loop {
115 if !visited.insert(offset) {
116 break; }
118
119 match parse_xref_section(data, offset) {
120 Ok((entries, trailer)) => {
121 let prev = trailer.get_int(b"Prev").map(|v| v as usize + header_offset);
122 sections.push((entries, trailer, offset));
123 match prev {
124 Some(p) => offset = p,
125 None => break,
126 }
127 }
128 Err(_) => {
129 xref_failed = true;
130 break;
131 }
132 }
133 }
134
135 if xref_failed && sections.is_empty() {
140 discover_orphaned_xref_sections(
141 data,
142 &mut sections,
143 &mut visited,
144 header_offset,
145 &pdf_headers,
146 );
147 if sections.is_empty() {
148 return rebuild_xref_from_scan(data);
149 }
150 }
151
152 discover_orphaned_xref_sections(
157 data,
158 &mut sections,
159 &mut visited,
160 header_offset,
161 &pdf_headers,
162 );
163
164 sections.sort_by_key(|(_, _, file_offset)| *file_offset);
170 let mut combined_entries: Vec<Option<XrefEntry>> = Vec::new();
171 let mut final_trailer = PdfDict::new();
172
173 for (entries, trailer, _) in sections {
174 for (num, entry) in entries {
175 let idx = num as usize;
176 if idx >= combined_entries.len() {
177 combined_entries.resize(idx + 1, None);
178 }
179 combined_entries[idx] = Some(entry);
180 }
181 for (key, val) in trailer.into_entries() {
185 final_trailer.insert(key, val);
186 }
187 }
188
189 if final_trailer.get(b"Root").is_none() {
193 return rebuild_xref_from_scan(data);
194 }
195
196 if header_offset > 0 {
199 for entry in combined_entries.iter_mut().flatten() {
200 if let XrefEntry::InFile { offset, .. } = entry {
201 *offset += header_offset;
202 }
203 }
204 }
205
206 supplement_xref_from_scan(data, &mut combined_entries);
210
211 Ok(XrefTable {
212 entries: combined_entries,
213 trailer: final_trailer,
214 })
215}
216
217fn discover_orphaned_xref_sections(
221 data: &[u8],
222 sections: &mut Vec<(Vec<(u32, XrefEntry)>, PdfDict, usize)>,
223 visited: &mut std::collections::HashSet<usize>,
224 header_offset: usize,
225 pdf_headers: &[usize],
226) {
227 let mut candidates = Vec::new();
228
229 let mut pos = 0;
231 while pos + 7 < data.len() {
232 if &data[pos..pos + 7] == b"trailer" {
233 let end = (pos + 500).min(data.len());
234 if let Some(prev_off) = find_int_after_key(&data[pos..end], b"/Prev") {
235 candidates.push(prev_off + header_offset);
237 }
238 }
239 if pos + 9 < data.len() && &data[pos..pos + 9] == b"startxref" {
241 let end = (pos + 50).min(data.len());
242 if let Some(sx_off) = find_int_after_key(&data[pos..end], b"startxref") {
243 if sx_off > 0 {
244 candidates.push(sx_off + header_offset);
247 for &h in pdf_headers.iter().skip(1) {
248 let adjusted = sx_off + h;
249 if adjusted < data.len() {
250 candidates.push(adjusted);
251 }
252 }
253 }
254 }
255 }
256 if pos + 4 <= data.len() && &data[pos..pos + 4] == b"xref" {
259 if pos == 0 || data[pos - 1] != b't' {
261 candidates.push(pos);
263 }
264 }
265 pos += 1;
266 }
267
268 for candidate in candidates {
270 let mut offset = candidate;
271 loop {
272 if !visited.insert(offset) {
273 break;
274 }
275 match parse_xref_section(data, offset) {
276 Ok((mut entries, trailer)) => {
277 let section_header = owning_pdf_header(offset, pdf_headers);
280 if section_header > 0 {
281 shift_xref_entries(&mut entries, section_header);
282 }
283 let prev = trailer.get_int(b"Prev").map(|v| v as usize + header_offset);
284 sections.push((entries, trailer, offset));
285 match prev {
286 Some(p) => offset = p,
287 None => break,
288 }
289 }
290 Err(_) => break,
291 }
292 }
293 }
294}
295
296fn supplement_xref_from_scan(data: &[u8], entries: &mut Vec<Option<XrefEntry>>) {
300 let mut pos = 0;
301 while pos < data.len() {
302 if pos > 0 && data[pos - 1] != b'\n' && data[pos - 1] != b'\r' {
304 while pos < data.len() && data[pos] != b'\n' && data[pos] != b'\r' {
305 pos += 1;
306 }
307 if pos < data.len() {
308 if data[pos] == b'\r' && pos + 1 < data.len() && data[pos + 1] == b'\n' {
309 pos += 2;
310 } else {
311 pos += 1;
312 }
313 }
314 continue;
315 }
316
317 if pos < data.len()
318 && data[pos].is_ascii_digit()
319 && let Some((obj_num, generation, obj_offset)) = try_parse_obj_header(data, pos)
320 {
321 let idx = obj_num as usize;
322 if idx < 100_000 {
323 let dominated_by_existing = entries.get(idx).is_some_and(|e| {
327 matches!(
328 e,
329 Some(XrefEntry::InFile { .. } | XrefEntry::InStream { .. })
330 )
331 });
332 if !dominated_by_existing {
333 if idx >= entries.len() {
334 entries.resize(idx + 1, None);
335 }
336 entries[idx] = Some(XrefEntry::InFile {
337 offset: obj_offset,
338 generation,
339 });
340 }
341 }
342 }
343
344 while pos < data.len() && data[pos] != b'\n' && data[pos] != b'\r' {
345 pos += 1;
346 }
347 if pos < data.len() {
348 if data[pos] == b'\r' && pos + 1 < data.len() && data[pos + 1] == b'\n' {
349 pos += 2;
350 } else {
351 pos += 1;
352 }
353 }
354 }
355}
356
357fn find_int_after_key(data: &[u8], key: &[u8]) -> Option<usize> {
360 let s = std::str::from_utf8(data).ok()?;
361 let key_str = std::str::from_utf8(key).ok()?;
362 let idx = s.find(key_str)?;
363 let after = &s[idx + key_str.len()..];
364 let after = after.trim_start();
365 let end = after
366 .find(|c: char| !c.is_ascii_digit())
367 .unwrap_or(after.len());
368 if end == 0 {
369 return None;
370 }
371 after[..end].parse().ok()
372}
373
374fn rebuild_xref_from_scan(data: &[u8]) -> Result<XrefTable, PdfError> {
377 let mut entries: Vec<Option<XrefEntry>> = Vec::new();
378
379 let mut pos = 0;
381 while pos < data.len() {
382 if pos > 0 && data[pos - 1] != b'\n' && data[pos - 1] != b'\r' {
384 while pos < data.len() && data[pos] != b'\n' && data[pos] != b'\r' {
386 pos += 1;
387 }
388 if pos < data.len() {
390 if data[pos] == b'\r' && pos + 1 < data.len() && data[pos + 1] == b'\n' {
391 pos += 2;
392 } else {
393 pos += 1;
394 }
395 }
396 continue;
397 }
398
399 if pos < data.len()
401 && data[pos].is_ascii_digit()
402 && let Some((obj_num, generation, obj_offset)) = try_parse_obj_header(data, pos)
403 {
404 let idx = obj_num as usize;
405 if idx < 100_000 {
406 if idx >= entries.len() {
408 entries.resize(idx + 1, None);
409 }
410 entries[idx] = Some(XrefEntry::InFile {
411 offset: obj_offset,
412 generation,
413 });
414 }
415 }
416
417 while pos < data.len() && data[pos] != b'\n' && data[pos] != b'\r' {
419 pos += 1;
420 }
421 if pos < data.len() {
422 if data[pos] == b'\r' && pos + 1 < data.len() && data[pos + 1] == b'\n' {
423 pos += 2;
424 } else {
425 pos += 1;
426 }
427 }
428 }
429
430 for entry in entries.iter().flatten() {
434 if let XrefEntry::InFile { offset, .. } = entry {
435 let search_end = (*offset + 512).min(data.len());
436 let slice = &data[*offset..search_end];
437 if slice.windows(10).any(|w| w == b"/Type/XRef")
438 || slice.windows(11).any(|w| w == b"/Type /XRef")
439 {
440 if let Ok((xref_entries, xref_trailer)) = parse_xref_stream(data, *offset) {
441 let mut merged = entries;
443 for (num, xentry) in xref_entries {
444 let idx = num as usize;
445 if idx < 100_000 {
446 if idx >= merged.len() {
447 merged.resize(idx + 1, None);
448 }
449 if merged[idx].is_none() {
450 merged[idx] = Some(xentry);
451 }
452 }
453 }
454 return Ok(XrefTable {
455 entries: merged,
456 trailer: xref_trailer,
457 });
458 }
459 }
460 }
461 }
462
463 let trailer = find_trailer_dict(data).unwrap_or_default();
465
466 let trailer = if trailer.get(b"Root").is_none() {
468 let mut t = trailer;
469 if let Some(root_num) = find_catalog_obj(data, &entries) {
470 t.insert(b"Root".to_vec(), crate::objects::PdfObj::Ref(root_num, 0));
471 }
472 t
473 } else {
474 trailer
475 };
476
477 Ok(XrefTable { entries, trailer })
478}
479
480fn try_parse_obj_header(data: &[u8], pos: usize) -> Option<(u32, u16, usize)> {
483 let mut p = pos;
484
485 let num_start = p;
487 while p < data.len() && data[p].is_ascii_digit() {
488 p += 1;
489 }
490 if p == num_start || p >= data.len() {
491 return None;
492 }
493 let obj_num: u32 = std::str::from_utf8(&data[num_start..p])
494 .ok()?
495 .parse()
496 .ok()?;
497
498 while p < data.len() && data[p] == b' ' {
500 p += 1;
501 }
502
503 let gen_start = p;
505 while p < data.len() && data[p].is_ascii_digit() {
506 p += 1;
507 }
508 if p == gen_start || p >= data.len() {
509 return None;
510 }
511 let generation: u16 = std::str::from_utf8(&data[gen_start..p])
512 .ok()?
513 .parse()
514 .ok()?;
515
516 while p < data.len() && data[p] == b' ' {
518 p += 1;
519 }
520
521 if p + 3 > data.len() || &data[p..p + 3] != b"obj" {
523 return None;
524 }
525 let after_obj = p + 3;
527 if after_obj < data.len()
528 && !is_whitespace(data[after_obj])
529 && !matches!(data[after_obj], b'<' | b'[' | b'(' | b'/')
530 {
531 return None;
532 }
533
534 Some((obj_num, generation, pos))
535}
536
537fn find_trailer_dict(data: &[u8]) -> Option<PdfDict> {
539 let needle = b"trailer";
541 let mut pos = data.len().saturating_sub(needle.len());
542 while pos > 0 {
543 if &data[pos..pos + needle.len()] == needle {
544 let dict_start = pos + needle.len();
545 let mut lexer = Lexer::at(data, dict_start);
546 if let Ok(Token::DictBegin) = lexer.next_token()
547 && let Ok(dict) = parse_dict_body(&mut lexer)
548 {
549 return Some(dict);
550 }
551 }
552 pos -= 1;
553 }
554 None
555}
556
557fn find_catalog_obj(data: &[u8], entries: &[Option<XrefEntry>]) -> Option<u32> {
559 for (num, entry) in entries.iter().enumerate() {
560 if let Some(XrefEntry::InFile { offset, .. }) = entry {
561 let search_end = (*offset + 512).min(data.len());
563 let slice = &data[*offset..search_end];
564 if let Some(idx) = slice.windows(8).position(|w| w == b"/Catalog") {
565 if idx > 5 && slice[..idx].windows(5).any(|w| w == b"/Type") {
567 return Some(num as u32);
568 }
569 }
570 if slice.windows(5).any(|w| w == b"/Root")
574 && slice.windows(10).any(|w| w == b"/Type/XRef")
575 {
576 let obj_start = slice
578 .windows(3)
579 .position(|w| w == b"obj")
580 .map(|p| p + 3)
581 .unwrap_or(0);
582 let mut lexer = Lexer::at(slice, obj_start);
583 if let Ok(Token::DictBegin) = lexer.next_token()
584 && let Ok(dict) = parse_dict_body(&mut lexer)
585 {
586 if let Some((root_num, _)) = dict.get_ref(b"Root") {
587 return Some(root_num);
588 }
589 }
590 }
591 }
592 }
593 None
594}
595
596fn parse_xref_section(
598 data: &[u8],
599 offset: usize,
600) -> Result<(Vec<(u32, XrefEntry)>, PdfDict), PdfError> {
601 let mut pos = offset;
605 while pos < data.len() && is_whitespace(data[pos]) {
606 pos += 1;
607 }
608
609 if pos + 4 <= data.len() && &data[pos..pos + 4] == b"xref" {
610 parse_classic_xref(data, pos)
611 } else {
612 let scan_start = offset.saturating_sub(20);
614 let found = (scan_start..offset)
615 .rev()
616 .find(|&off| off + 4 <= data.len() && &data[off..off + 4] == b"xref");
617 if let Some(xref_pos) = found {
618 parse_classic_xref(data, xref_pos)
619 } else {
620 match parse_xref_stream(data, offset) {
622 Ok(result) => Ok(result),
623 Err(_) => {
624 let scan_start = offset.saturating_sub(256);
628 let scan_end = offset.min(data.len());
629 for search in (scan_start..scan_end).rev() {
630 if data[search].is_ascii_digit() {
631 if let Some((_, _, obj_offset)) = try_parse_obj_header(data, search) {
632 if obj_offset == search {
633 if let Ok(result) = parse_xref_stream(data, search) {
634 return Ok(result);
635 }
636 }
637 }
638 }
639 }
640 parse_xref_stream(data, offset) }
642 }
643 }
644 }
645}
646
647fn parse_classic_xref(
649 data: &[u8],
650 offset: usize,
651) -> Result<(Vec<(u32, XrefEntry)>, PdfDict), PdfError> {
652 let mut pos = offset + 4; while pos < data.len() && is_whitespace(data[pos]) {
656 pos += 1;
657 }
658
659 let mut entries = Vec::new();
660
661 let mut has_trailer = false;
664 loop {
665 if pos + 7 <= data.len() && &data[pos..pos + 7] == b"trailer" {
667 pos += 7;
668 has_trailer = true;
669 break;
670 }
671
672 let Ok((first_obj, new_pos)) = parse_int_at(data, pos) else {
675 break;
676 };
677 pos = new_pos;
678 while pos < data.len() && (data[pos] == b' ' || data[pos] == b'\t') {
679 pos += 1;
680 }
681 let Ok((count, new_pos)) = parse_int_at(data, pos) else {
682 break;
683 };
684 pos = new_pos;
685
686 while pos < data.len() && is_whitespace(data[pos]) {
688 pos += 1;
689 }
690
691 let mut entry_error = false;
695 for i in 0..count as u32 {
696 if pos + 18 > data.len() {
697 break;
698 }
699
700 let Ok(off_str) = std::str::from_utf8(&data[pos..pos + 10]) else {
702 entry_error = true;
703 break;
704 };
705 let Ok(off) = off_str.trim().parse::<usize>() else {
706 entry_error = true;
707 break;
708 };
709
710 let Ok(gen_str) = std::str::from_utf8(&data[pos + 11..pos + 16]) else {
711 entry_error = true;
712 break;
713 };
714 let Ok(gen_val) = gen_str.trim().parse::<u32>() else {
717 entry_error = true;
718 break;
719 };
720 let generation = gen_val.min(65535) as u16;
721
722 let type_byte = data[pos + 17];
723 let obj_num = first_obj as u32 + i;
724
725 let entry = match type_byte {
726 b'n' => XrefEntry::InFile {
727 offset: off,
728 generation,
729 },
730 b'f' => XrefEntry::Free,
731 _ => XrefEntry::Free,
732 };
733 entries.push((obj_num, entry));
734
735 pos += 18;
737 while pos < data.len()
738 && (data[pos] == b' ' || data[pos] == b'\r' || data[pos] == b'\n')
739 {
740 pos += 1;
741 }
742 }
743
744 if entry_error {
746 break;
747 }
748
749 while pos < data.len() && is_whitespace(data[pos]) {
751 pos += 1;
752 }
753 }
754
755 let trailer = if has_trailer {
758 let mut lexer = Lexer::at(data, pos);
759 lexer.set_pos(pos);
760 let tok = lexer.next_token()?;
761 match tok {
762 Token::DictBegin => parse_dict_body(&mut lexer)?,
763 _ => return Err(PdfError::MalformedTrailer),
764 }
765 } else if entries.is_empty() {
766 return Err(PdfError::MalformedXref(offset));
767 } else {
768 PdfDict::new()
769 };
770
771 Ok((entries, trailer))
772}
773
774fn parse_xref_stream(
776 data: &[u8],
777 offset: usize,
778) -> Result<(Vec<(u32, XrefEntry)>, PdfDict), PdfError> {
779 let mut lexer = Lexer::at(data, offset);
781
782 let _obj_num = match lexer.next_token()? {
784 Token::Int(n) => n,
785 t => {
786 return Err(PdfError::UnexpectedToken {
787 expected: "object number".into(),
788 got: format!("{t:?}"),
789 });
790 }
791 };
792
793 match lexer.next_token()? {
795 Token::Int(_) => {}
796 t => {
797 return Err(PdfError::UnexpectedToken {
798 expected: "generation number".into(),
799 got: format!("{t:?}"),
800 });
801 }
802 }
803
804 match lexer.next_token()? {
806 Token::Keyword(ref kw) if kw == b"obj" => {}
807 t => {
808 return Err(PdfError::UnexpectedToken {
809 expected: "obj".into(),
810 got: format!("{t:?}"),
811 });
812 }
813 }
814
815 match lexer.next_token()? {
817 Token::DictBegin => {}
818 t => {
819 return Err(PdfError::UnexpectedToken {
820 expected: "<<".into(),
821 got: format!("{t:?}"),
822 });
823 }
824 }
825 let dict = parse_dict_body(&mut lexer)?;
826
827 let tok = lexer.next_token()?;
829 if !matches!(tok, Token::Keyword(ref kw) if kw == b"stream") {
830 return Err(PdfError::UnexpectedToken {
831 expected: "stream".into(),
832 got: format!("{tok:?}"),
833 });
834 }
835
836 let mut data_start = lexer.pos();
838 if data_start < data.len() && data[data_start] == b'\r' {
839 data_start += 1;
840 }
841 if data_start < data.len() && data[data_start] == b'\n' {
842 data_start += 1;
843 }
844
845 let length = match dict.get_int(b"Length") {
850 Some(n) => n as usize,
851 None => {
852 let needle = b"endstream";
853 let search_end = data.len().saturating_sub(needle.len());
854 let mut pos = data_start;
855 let mut found = None;
856 while pos <= search_end {
857 if &data[pos..pos + needle.len()] == needle {
858 let mut end = pos;
859 while end > data_start && matches!(data[end - 1], b' ' | b'\r' | b'\n') {
860 end -= 1;
861 }
862 found = Some(end - data_start);
863 break;
864 }
865 pos += 1;
866 }
867 found.ok_or(PdfError::StreamMissingLength)?
868 }
869 };
870 let raw_data = &data[data_start..std::cmp::min(data_start + length, data.len())];
871
872 let (filter_list, parms) = filters::parse_filters(&dict, None)?;
874 let stream_data = if filter_list.is_empty() {
875 raw_data.to_vec()
876 } else {
877 filters::decode_stream(raw_data, &filter_list, &parms, None)?
878 };
879
880 let w = dict.get_array(b"W").ok_or(PdfError::MissingKey("W"))?;
882 if w.len() != 3 {
883 return Err(PdfError::Other(
884 "xref stream /W must have 3 elements".into(),
885 ));
886 }
887 let w1 = w[0].as_int().unwrap_or(0) as usize;
888 let w2 = w[1].as_int().unwrap_or(0) as usize;
889 let w3 = w[2].as_int().unwrap_or(0) as usize;
890 let entry_size = w1 + w2 + w3;
891
892 if entry_size == 0 {
893 return Err(PdfError::Other("xref stream entry size is 0".into()));
894 }
895
896 let size = dict.get_int(b"Size").ok_or(PdfError::MissingKey("Size"))? as u32;
898
899 let index_pairs: Vec<(u32, u32)> = if let Some(index_arr) = dict.get_array(b"Index") {
900 index_arr
901 .chunks(2)
902 .filter_map(|pair| {
903 if pair.len() == 2 {
904 Some((pair[0].as_int()? as u32, pair[1].as_int()? as u32))
905 } else {
906 None
907 }
908 })
909 .collect()
910 } else {
911 vec![(0, size)]
912 };
913
914 let mut entries = Vec::new();
915 let mut stream_pos = 0;
916
917 for (first_obj, count) in &index_pairs {
918 for i in 0..*count {
919 if stream_pos + entry_size > stream_data.len() {
920 break;
921 }
922
923 let field1 = read_field(&stream_data[stream_pos..], w1);
924 let field2 = read_field(&stream_data[stream_pos + w1..], w2);
925 let field3 = read_field(&stream_data[stream_pos + w1 + w2..], w3);
926 stream_pos += entry_size;
927
928 let entry_type = if w1 == 0 { 1 } else { field1 };
930 let obj_num = first_obj + i;
931
932 let entry = match entry_type {
933 0 => XrefEntry::Free,
934 1 => XrefEntry::InFile {
935 offset: field2 as usize,
936 generation: field3 as u16,
937 },
938 2 => XrefEntry::InStream {
939 stream_obj_num: field2 as u32,
940 index_within: field3 as u16,
941 },
942 _ => XrefEntry::Free, };
944
945 entries.push((obj_num, entry));
946 }
947 }
948
949 Ok((entries, dict))
951}
952
953fn read_field(data: &[u8], width: usize) -> u64 {
955 let mut val: u64 = 0;
956 for i in 0..width {
957 if i < data.len() {
958 val = (val << 8) | data[i] as u64;
959 }
960 }
961 val
962}
963
964fn find_all_pdf_headers(data: &[u8]) -> Vec<usize> {
970 let mut headers = Vec::new();
971 let mut pos = 0;
972 while pos + 5 <= data.len() {
973 if &data[pos..pos + 5] == b"%PDF-" {
974 headers.push(pos);
975 pos += 5;
976 } else {
977 pos += 1;
978 }
979 }
980 headers
981}
982
983fn owning_pdf_header(pos: usize, pdf_headers: &[usize]) -> usize {
988 match pdf_headers.iter().rposition(|&h| h <= pos) {
990 Some(idx) if idx > 0 => pdf_headers[idx],
991 _ => 0,
992 }
993}
994
995fn shift_xref_entries(entries: &mut [(u32, XrefEntry)], base: usize) {
1000 for (_, entry) in entries.iter_mut() {
1001 if let XrefEntry::InFile { offset, .. } = entry {
1002 *offset += base;
1003 }
1004 }
1005}
1006
1007fn find_startxref(data: &[u8]) -> Result<usize, PdfError> {
1013 let needle = b"startxref";
1014
1015 let eof_marker = b"%%EOF";
1018 let mut eof_pos = None;
1019 for i in (0..data.len().saturating_sub(eof_marker.len())).rev() {
1020 if &data[i..i + eof_marker.len()] == eof_marker {
1021 eof_pos = Some(i);
1022 break;
1023 }
1024 }
1025
1026 if let Some(eof) = eof_pos {
1027 let search_start = eof.saturating_sub(1024);
1029 let region = &data[search_start..eof];
1030 let mut found = None;
1031 for i in 0..region.len().saturating_sub(needle.len()) {
1032 if ®ion[i..i + needle.len()] == needle {
1033 found = Some(search_start + i);
1034 }
1035 }
1036 if let Some(pos) = found {
1037 let mut p = pos + needle.len();
1038 while p < data.len() && is_whitespace(data[p]) {
1039 p += 1;
1040 }
1041 let (offset, _) = parse_int_at(data, p)?;
1042 return Ok(offset as usize);
1043 }
1044 }
1045
1046 let search_start = data.len().saturating_sub(1024);
1049 let tail = &data[search_start..];
1050 let mut found = None;
1051 for i in 0..tail.len().saturating_sub(needle.len()) {
1052 if &tail[i..i + needle.len()] == needle {
1053 found = Some(search_start + i);
1054 }
1055 }
1056
1057 if let Some(pos) = found {
1058 let mut p = pos + needle.len();
1059 while p < data.len() && is_whitespace(data[p]) {
1060 p += 1;
1061 }
1062 let (offset, _) = parse_int_at(data, p)?;
1063 return Ok(offset as usize);
1064 }
1065
1066 let xref_kw = b"xref";
1069 let mut last_xref = None;
1070 for i in (0..data.len().saturating_sub(xref_kw.len())).rev() {
1071 if &data[i..i + xref_kw.len()] == xref_kw
1072 && (i == 0 || is_whitespace(data[i - 1]) || data[i - 1] == b'\n')
1073 {
1074 last_xref = Some(i);
1075 break;
1076 }
1077 }
1078 last_xref.ok_or(PdfError::NoStartXref)
1079}
1080
1081fn parse_int_at(data: &[u8], pos: usize) -> Result<(i64, usize), PdfError> {
1083 let mut p = pos;
1084 while p < data.len() && is_whitespace(data[p]) {
1086 p += 1;
1087 }
1088 let start = p;
1089 if p < data.len() && (data[p] == b'+' || data[p] == b'-') {
1090 p += 1;
1091 }
1092 while p < data.len() && data[p].is_ascii_digit() {
1093 p += 1;
1094 }
1095 if p == start {
1096 return Err(PdfError::Other(format!("expected integer at offset {pos}")));
1097 }
1098 let s = std::str::from_utf8(&data[start..p])
1099 .map_err(|_| PdfError::Other(format!("invalid integer at offset {pos}")))?;
1100 let n: i64 = s
1101 .parse()
1102 .map_err(|_| PdfError::Other(format!("invalid integer '{s}' at offset {pos}")))?;
1103 Ok((n, p))
1104}
1105
1106fn is_whitespace(b: u8) -> bool {
1107 matches!(b, b' ' | b'\t' | b'\r' | b'\n' | 0x0C | 0x00)
1108}
1109
1110#[cfg(test)]
1111mod tests {
1112 use super::*;
1113
1114 #[test]
1115 fn find_startxref_simple() {
1116 let data = b"%PDF-1.4\nstartxref\n1234\n%%EOF\n";
1117 let offset = find_startxref(data).unwrap();
1118 assert_eq!(offset, 1234);
1119 }
1120
1121 #[test]
1122 fn find_startxref_trailing_garbage() {
1123 let mut data = b"%PDF-1.4\nstartxref\n5678\n%%EOF\n".to_vec();
1125 data.extend_from_slice(&[0xFFu8; 2000]);
1127 let offset = find_startxref(&data).unwrap();
1128 assert_eq!(offset, 5678);
1129 }
1130
1131 #[test]
1132 fn parse_int_at_basic() {
1133 let (val, pos) = parse_int_at(b" 42 rest", 0).unwrap();
1134 assert_eq!(val, 42);
1135 assert_eq!(pos, 4);
1136 }
1137
1138 #[test]
1139 fn read_field_sizes() {
1140 assert_eq!(read_field(&[0x01], 1), 1);
1141 assert_eq!(read_field(&[0x01, 0x00], 2), 256);
1142 assert_eq!(read_field(&[0x00, 0x01, 0x00], 3), 256);
1143 }
1144}