1use crate::hash::{FastMap, FastSet};
6
7use crate::elements::{Span, XrefKind};
8use crate::error::{Error, Result};
9use crate::filters::{decode_stream, is_pdf_whitespace};
10use crate::lexer::{Lexer, Token};
11use crate::object::{Dict, Name, ObjRef, Object};
12use crate::parser::{NoResolve, Parser};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum XrefEntry {
17 Free,
19 InFile { offset: u64, gen: u16 },
21 InStream { stream_num: u32, index: u32 },
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct NewestSection {
29 pub offset: u64,
31 pub kind: XrefKind,
33}
34
35#[derive(Debug, Clone, Default)]
37pub struct Xref {
38 map: FastMap<u32, XrefEntry>,
39 pub trailer: Dict,
41 newest: Option<NewestSection>,
44}
45
46impl Xref {
47 pub fn get(&self, num: u32) -> Option<XrefEntry> {
49 self.map.get(&num).copied()
50 }
51
52 pub fn newest_section(&self) -> Option<NewestSection> {
57 self.newest
58 }
59
60 fn add(&mut self, num: u32, entry: XrefEntry) {
64 self.map.entry(num).or_insert(entry);
65 }
66
67 pub fn merge(&mut self, older: Xref) {
73 for (num, entry) in older.map {
74 self.map.entry(num).or_insert(entry);
75 }
76 for (key, value) in older.trailer.iter() {
77 if self.trailer.get(&key.0).is_none() {
78 self.trailer.insert(key.clone(), value.clone());
79 }
80 }
81 if self.newest.is_none() {
82 self.newest = older.newest;
83 }
84 }
85
86 pub fn iter(&self) -> impl Iterator<Item = (u32, XrefEntry)> + '_ {
88 self.map.iter().map(|(&num, &entry)| (num, entry))
89 }
90
91 pub fn len(&self) -> usize {
93 self.map.len()
94 }
95
96 pub fn is_empty(&self) -> bool {
98 self.map.is_empty()
99 }
100}
101
102pub fn load_xref(data: &[u8]) -> Result<Xref> {
108 let chained = find_startxref(data).and_then(|start| load_chain(data, start).ok());
109 match chained {
110 Some(xref) if xref.trailer.get("Root").is_some() => Ok(xref),
111 chained => recovery_scan(data).or_else(|err| chained.ok_or(err)),
112 }
113}
114
115pub fn startxref(data: &[u8]) -> Option<usize> {
119 find_startxref(data)
120}
121
122fn find_startxref(data: &[u8]) -> Option<usize> {
128 for window in [1024usize, 64 * 1024] {
129 let tail = data.len().saturating_sub(window);
130 if let Some(rel) = memchr::memmem::rfind(&data[tail..], b"startxref") {
131 let mut lexer = Lexer::at(data, tail + rel + b"startxref".len());
132 if let Ok(Token::Int(v)) = lexer.next_token() {
133 if let Some(offset) = to_offset(v, data) {
134 return Some(offset);
135 }
136 }
137 }
138 if window >= data.len() {
139 break;
140 }
141 }
142 None
143}
144
145fn to_offset(v: i64, data: &[u8]) -> Option<usize> {
147 usize::try_from(v).ok().filter(|&o| o < data.len())
148}
149
150fn load_chain(data: &[u8], start: usize) -> Result<Xref> {
158 let mut acc = Xref::default();
159 let mut visited: FastSet<usize> = FastSet::default();
160 let mut next = Some(start);
161 let mut first = true;
162 while let Some(off) = next {
163 if !visited.insert(off) {
164 break;
165 }
166 let info = parse_section_at(data, off)?;
167 if first {
168 acc.newest = Some(NewestSection {
169 offset: off as u64,
170 kind: info.kind,
171 });
172 first = false;
173 }
174 if let Some(xs) = info.xrefstm.and_then(|v| to_offset(v, data)) {
175 if visited.insert(xs) {
176 if let Ok(stream_info) = parse_section_at(data, xs) {
178 acc.merge(stream_info.xref);
179 }
180 }
181 }
182 acc.merge(info.xref);
183 next = info.prev.and_then(|v| to_offset(v, data));
184 }
185 if acc.map.is_empty() {
186 Err(Error::InvalidXref)
187 } else {
188 Ok(acc)
189 }
190}
191
192fn read_be(bytes: &[u8]) -> u64 {
196 bytes.iter().fold(0, |acc, &b| (acc << 8) | u64::from(b))
197}
198
199#[derive(Debug, Clone)]
202pub struct XrefSectionInfo {
203 pub xref: Xref,
205 pub kind: XrefKind,
206 pub prev: Option<i64>,
208 pub xrefstm: Option<i64>,
210 pub span: Span,
213 pub trailer_span: Option<Span>,
216}
217
218pub fn parse_section_at(data: &[u8], off: usize) -> Result<XrefSectionInfo> {
223 let mut lexer = Lexer::at(data, off);
224 if matches!(lexer.peek_token(), Ok(Token::Keyword(ref k)) if k.as_slice() == b"xref") {
225 parse_classic(data, off)
226 } else {
227 parse_stream_section(data, off)
228 }
229}
230
231fn parse_classic(data: &[u8], off: usize) -> Result<XrefSectionInfo> {
238 let mut lexer = Lexer::at(data, off);
239 match lexer.next_token()? {
240 Token::Keyword(ref k) if k.as_slice() == b"xref" => {}
241 _ => return Err(Error::InvalidXref),
242 }
243 let mut section = Xref::default();
244 loop {
245 match lexer.next_token()? {
246 Token::Int(start) if start >= 0 => {
247 let count = match lexer.next_token()? {
248 Token::Int(c) if c >= 0 => c as u64,
249 _ => return Err(Error::InvalidXref),
250 };
251 if count > data.len() as u64 / 11 + 1 {
254 return Err(Error::InvalidXref);
255 }
256 for i in 0..count {
257 let f1 = match lexer.next_token()? {
258 Token::Int(v) if v >= 0 => v as u64,
259 _ => return Err(Error::InvalidXref),
260 };
261 let f2 = match lexer.next_token()? {
262 Token::Int(v) if v >= 0 => v,
263 _ => return Err(Error::InvalidXref),
264 };
265 let entry = match lexer.next_token()? {
266 Token::Keyword(ref k) if k.as_slice() == b"n" => XrefEntry::InFile {
267 offset: f1,
268 gen: f2.min(65535) as u16,
269 },
270 Token::Keyword(ref k) if k.as_slice() == b"f" => XrefEntry::Free,
271 _ => return Err(Error::InvalidXref),
272 };
273 if let Ok(num) = u32::try_from(start as u64 + i) {
274 section.add(num, entry);
275 }
276 }
277 }
278 Token::Keyword(ref k) if k.as_slice() == b"trailer" => {
279 let trailer_start = lexer.pos() - b"trailer".len();
282 let mut parser = Parser::at(data, lexer.pos());
283 let trailer = match parser.parse_object(&NoResolve)? {
284 Object::Dict(d) => d,
285 _ => return Err(Error::InvalidXref),
286 };
287 let prev = trailer.get_int("Prev");
288 let xrefstm = trailer.get_int("XRefStm");
289 section.trailer = trailer;
290 return Ok(XrefSectionInfo {
291 xref: section,
292 kind: XrefKind::Table,
293 prev,
294 xrefstm,
295 span: Span::new(off as u64, trailer_start as u64),
296 trailer_span: Some(Span::new(trailer_start as u64, parser.pos() as u64)),
297 });
298 }
299 _ => return Err(Error::InvalidXref),
300 }
301 }
302}
303
304fn parse_stream_section(data: &[u8], off: usize) -> Result<XrefSectionInfo> {
311 let mut parser = Parser::at(data, off);
312 let (_, obj) = parser.parse_indirect(&NoResolve)?;
313 let end = parser.pos();
314 let stream = match obj {
315 Object::Stream(s) => s,
316 _ => return Err(Error::InvalidXref),
317 };
318 let decoded = decode_stream(&stream, &NoResolve).map_err(|_| Error::InvalidXref)?;
319 let dict = stream.dict;
320 let widths: Vec<usize> = dict
321 .get_array("W")
322 .ok_or(Error::InvalidXref)?
323 .iter()
324 .map(|v| {
325 v.as_int()
326 .filter(|&n| (0..=8).contains(&n))
327 .map(|n| n as usize)
328 })
329 .collect::<Option<Vec<_>>>()
330 .ok_or(Error::InvalidXref)?;
331 let w1 = widths.first().copied().unwrap_or(0);
332 let w2 = widths.get(1).copied().unwrap_or(0);
333 let w3 = widths.get(2).copied().unwrap_or(0);
334 let entry_len = w1 + w2 + w3;
335 if entry_len == 0 {
336 return Err(Error::InvalidXref);
337 }
338 let size = dict.get_int("Size").unwrap_or(0).max(0) as u64;
339 let subsections: Vec<(u64, u64)> = match dict.get_array("Index") {
340 Some(index) => index
341 .chunks(2)
342 .filter_map(|pair| {
343 let start = pair.first()?.as_int()?;
344 let count = pair.get(1)?.as_int()?;
345 (start >= 0 && count >= 0).then_some((start as u64, count as u64))
346 })
347 .collect(),
348 None => vec![(0, size)],
349 };
350 let mut section = Xref::default();
351 let mut pos = 0usize;
352 'subsections: for (start, count) in subsections {
353 for i in 0..count {
354 if pos + entry_len > decoded.len() {
355 break 'subsections; }
357 let kind = if w1 == 0 {
358 1
359 } else {
360 read_be(&decoded[pos..pos + w1])
361 };
362 let f2 = read_be(&decoded[pos + w1..pos + w1 + w2]);
363 let f3 = read_be(&decoded[pos + w1 + w2..pos + entry_len]);
364 pos += entry_len;
365 let entry = match kind {
366 1 => XrefEntry::InFile {
367 offset: f2,
368 gen: f3.min(65535) as u16,
369 },
370 2 => match (u32::try_from(f2), u32::try_from(f3)) {
371 (Ok(stream_num), Ok(index)) => XrefEntry::InStream { stream_num, index },
372 _ => XrefEntry::Free,
373 },
374 _ => XrefEntry::Free,
377 };
378 if let Ok(num) = u32::try_from(start + i) {
379 section.add(num, entry);
380 }
381 }
382 }
383 let prev = dict.get_int("Prev");
384 section.trailer = dict;
385 Ok(XrefSectionInfo {
386 xref: section,
387 kind: XrefKind::Stream,
388 prev,
389 xrefstm: None,
390 span: Span::new(off as u64, end as u64),
391 trailer_span: None,
392 })
393}
394
395fn recovery_scan(data: &[u8]) -> Result<Xref> {
400 let mut xref = Xref::default();
401 for pos in memchr::memmem::find_iter(data, b"obj") {
402 if let Some((num, gen, start)) = obj_header_before(data, pos) {
403 xref.map.insert(
405 num,
406 XrefEntry::InFile {
407 offset: start as u64,
408 gen,
409 },
410 );
411 }
412 }
413 if xref.map.is_empty() {
414 return Err(Error::InvalidXref);
415 }
416 let trailers: Vec<usize> = memchr::memmem::find_iter(data, b"trailer").collect();
417 for &tp in trailers.iter().rev() {
418 let mut parser = Parser::at(data, tp + b"trailer".len());
419 if let Ok(Object::Dict(dict)) = parser.parse_object(&NoResolve) {
420 let has_root = dict.get("Root").is_some();
421 if xref.trailer.is_empty() || has_root {
422 xref.trailer = dict;
423 }
424 if has_root {
425 break;
426 }
427 }
428 }
429 if xref.trailer.get("Root").is_none() {
430 if let Some(catalog) = find_catalog(data, &xref) {
431 xref.trailer
432 .insert(Name("Root".to_string()), Object::Ref(catalog));
433 }
434 }
435 if xref.trailer.get("Size").is_none() {
436 let size = xref.map.keys().max().map_or(0, |&m| i64::from(m) + 1);
437 xref.trailer
438 .insert(Name("Size".to_string()), Object::Int(size));
439 }
440 Ok(xref)
441}
442
443fn find_catalog(data: &[u8], xref: &Xref) -> Option<ObjRef> {
446 let mut nums: Vec<u32> = xref.map.keys().copied().collect();
447 nums.sort_unstable();
448 for num in nums {
449 let XrefEntry::InFile { offset, .. } = xref.map[&num] else {
450 continue;
451 };
452 let mut parser = Parser::at(data, offset as usize);
453 let Ok((r, obj)) = parser.parse_indirect(&NoResolve) else {
454 continue;
455 };
456 let is_catalog = obj
457 .as_dict()
458 .and_then(|d| d.get_name("Type"))
459 .is_some_and(|n| n.0 == "Catalog");
460 if is_catalog {
461 return Some(r);
462 }
463 }
464 None
465}
466
467fn obj_header_before(data: &[u8], kw: usize) -> Option<(u32, u16, usize)> {
472 if let Some(&after) = data.get(kw + 3) {
473 if !is_token_boundary(after) {
474 return None;
475 }
476 }
477 let gen_end = strip_ws_back(data, kw);
478 let gen_start = strip_digits_back(data, gen_end);
479 let num_end = strip_ws_back(data, gen_start);
480 let num_start = strip_digits_back(data, num_end);
481 if gen_end == kw || gen_start == gen_end || num_end == gen_start || num_start == num_end {
482 return None;
483 }
484 if num_start > 0 && !is_token_boundary(data[num_start - 1]) {
485 return None;
486 }
487 let gen: u16 = ascii_int(&data[gen_start..gen_end])?;
488 let num: u32 = ascii_int(&data[num_start..num_end])?;
489 Some((num, gen, num_start))
490}
491
492fn strip_ws_back(data: &[u8], mut end: usize) -> usize {
494 while end > 0 && is_pdf_whitespace(data[end - 1]) {
495 end -= 1;
496 }
497 end
498}
499
500fn strip_digits_back(data: &[u8], mut end: usize) -> usize {
502 while end > 0 && data[end - 1].is_ascii_digit() {
503 end -= 1;
504 }
505 end
506}
507
508fn ascii_int<T: std::str::FromStr>(bytes: &[u8]) -> Option<T> {
510 std::str::from_utf8(bytes).ok()?.parse().ok()
511}
512
513fn is_token_boundary(b: u8) -> bool {
515 is_pdf_whitespace(b)
516 || matches!(
517 b,
518 b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
519 )
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525 use pdfboss_testkit::{objstm_payload, simple_doc, PdfBuilder};
526
527 fn pos_of(data: &[u8], needle: &[u8]) -> usize {
529 memchr::memmem::find(data, needle).unwrap()
530 }
531
532 fn assert_points_at_header(xref: &Xref, data: &[u8], num: u32) {
535 let header = format!("{num} 0 obj");
536 let expected = pos_of(data, header.as_bytes()) as u64;
537 assert_eq!(
538 xref.get(num),
539 Some(XrefEntry::InFile {
540 offset: expected,
541 gen: 0
542 }),
543 "entry for object {num}"
544 );
545 }
546
547 fn corrupt_startxref(data: &mut [u8]) {
550 let mut i = memchr::memmem::rfind(data, b"startxref").unwrap() + b"startxref".len();
551 while !data[i].is_ascii_digit() {
552 i += 1;
553 }
554 while data[i].is_ascii_digit() {
555 data[i] = b'9';
556 i += 1;
557 }
558 }
559
560 #[test]
562 fn classic_table_loads_all_entries() {
563 let data = simple_doc("Hello");
564 let xref = load_xref(&data).unwrap();
565 assert_eq!(xref.get(0), Some(XrefEntry::Free));
566 for num in 1..=5 {
567 assert_points_at_header(&xref, &data, num);
568 }
569 assert_eq!(xref.get(6), None);
570 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
571 assert_eq!(xref.trailer.get_int("Size"), Some(6));
572 }
573
574 #[test]
576 fn xref_stream_loads_infile_and_instream_entries() {
577 let (dict, payload) = objstm_payload(&[
578 (1, "<< /Type /Catalog /Pages 2 0 R >>"),
579 (2, "<< /Type /Pages /Kids [] /Count 0 >>"),
580 (5, "(text)"),
581 ]);
582 let mut b = PdfBuilder::new();
583 b.stream(6, &dict, &payload);
584 b.stream(4, "", b"BT ET");
585 let data = b.build_xref_stream(1);
586 let xref = load_xref(&data).unwrap();
587 assert_eq!(xref.get(0), Some(XrefEntry::Free));
588 for (num, index) in [(1, 0), (2, 1), (5, 2)] {
589 assert_eq!(
590 xref.get(num),
591 Some(XrefEntry::InStream {
592 stream_num: 6,
593 index
594 }),
595 "type-2 entry for object {num}"
596 );
597 }
598 for num in [4, 6, 7] {
599 assert_points_at_header(&xref, &data, num);
600 }
601 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
602 assert_eq!(
603 xref.trailer.get_name("Type").map(|n| n.0.as_str()),
604 Some("XRef"),
605 "the stream's own dictionary is the trailer"
606 );
607 }
608
609 #[test]
611 fn recovery_scan_after_corrupt_startxref() {
612 let mut data = simple_doc("rescue me");
613 corrupt_startxref(&mut data);
614 let xref = load_xref(&data).unwrap();
615 for num in 1..=5 {
616 assert_points_at_header(&xref, &data, num);
617 }
618 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
619 }
620
621 #[test]
622 fn recovery_finds_root_via_catalog_when_trailer_is_unreadable() {
623 let mut data = simple_doc("x");
624 corrupt_startxref(&mut data);
625 let tp = memchr::memmem::rfind(&data, b"trailer").unwrap();
626 data[tp..tp + b"trailer".len()].copy_from_slice(b"trai1er");
627 let xref = load_xref(&data).unwrap();
628 for num in 1..=5 {
629 assert_points_at_header(&xref, &data, num);
630 }
631 let root = xref.trailer.get_ref("Root").unwrap();
632 assert_eq!((root.num, root.gen), (1, 0), "object 1 is the catalog");
633 assert_eq!(xref.trailer.get_int("Size"), Some(6), "synthesized /Size");
634 }
635
636 #[test]
638 fn recovery_last_occurrence_of_an_object_wins() {
639 let mut data = simple_doc("x");
640 corrupt_startxref(&mut data);
641 let redefinition = data.len() as u64;
642 data.extend_from_slice(b"5 0 obj\n<< /Replaced true >>\nendobj\n");
643 let xref = load_xref(&data).unwrap();
644 assert_eq!(
645 xref.get(5),
646 Some(XrefEntry::InFile {
647 offset: redefinition,
648 gen: 0
649 })
650 );
651 for num in 1..=4 {
652 assert_points_at_header(&xref, &data, num);
653 }
654 }
655
656 #[test]
658 fn recovery_ignores_endobj_and_bad_headers() {
659 let data = b"garbage endobj more\n7 2 obj\n<< /Type /Catalog >>\nendobj\nxobj 9 9";
660 let xref = load_xref(data).unwrap();
661 let offset = pos_of(data, b"7 2 obj") as u64;
662 assert_eq!(xref.get(7), Some(XrefEntry::InFile { offset, gen: 2 }));
663 assert_eq!(xref.map.len(), 1, "only the real header is recovered");
664 let root = xref.trailer.get_ref("Root").unwrap();
665 assert_eq!((root.num, root.gen), (7, 2));
666 }
667
668 #[test]
669 fn unrecoverable_garbage_is_invalid_xref() {
670 assert!(matches!(load_xref(b"not a pdf"), Err(Error::InvalidXref)));
671 assert!(matches!(load_xref(b""), Err(Error::InvalidXref)));
672 }
673
674 #[test]
676 fn classic_table_with_19_and_21_byte_lines() {
677 let mut data = b"%PDF-1.4\n".to_vec();
678 let obj1 = data.len();
679 data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
680 let obj2 = data.len();
681 data.extend_from_slice(b"2 0 obj\n(hi)\nendobj\n");
682 let xref_off = data.len();
683 data.extend_from_slice(b"xref\n0 3\n");
684 data.extend_from_slice(b"0000000000 65535 f\n"); data.extend_from_slice(format!("{obj1:010} 00000 n\n").as_bytes()); data.extend_from_slice(format!("{obj2:010} 00000 n\r\n").as_bytes()); data.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
688 data.extend_from_slice(format!("startxref\n{xref_off}\n%%EOF\n").as_bytes());
689 let xref = load_xref(&data).unwrap();
690 assert_eq!(xref.get(0), Some(XrefEntry::Free));
692 for (num, offset) in [(1, obj1), (2, obj2)] {
693 assert_eq!(
694 xref.get(num),
695 Some(XrefEntry::InFile {
696 offset: offset as u64,
697 gen: 0
698 })
699 );
700 }
701 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
702 }
703
704 #[test]
706 fn xref_stream_prev_chains_to_classic_section() {
707 let mut data = b"%PDF-1.5\n".to_vec();
708 let obj1 = data.len();
709 data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
710 let obj2_old = data.len();
711 data.extend_from_slice(b"2 0 obj\n(old)\nendobj\n");
712 let classic_off = data.len();
713 data.extend_from_slice(b"xref\n0 3\n0000000000 65535 f\r\n");
714 data.extend_from_slice(format!("{obj1:010} 00000 n\r\n").as_bytes());
715 data.extend_from_slice(format!("{obj2_old:010} 00000 n\r\n").as_bytes());
716 data.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
717 let obj2_new = data.len();
719 data.extend_from_slice(b"2 0 obj\n(new)\nendobj\n");
720 let obj3 = data.len();
721 data.extend_from_slice(b"3 0 obj\n42\nendobj\n");
722 let stream_off = data.len();
723 let mut fields = Vec::new();
724 for offset in [obj2_new, obj3, stream_off] {
725 fields.push(1u8);
726 fields.extend_from_slice(&(offset as u32).to_be_bytes());
727 fields.extend_from_slice(&0u16.to_be_bytes());
728 }
729 data.extend_from_slice(
730 format!(
731 "4 0 obj\n<< /Type /XRef /Size 5 /W [1 4 2] /Index [2 3] \
732 /Prev {} /Root 1 0 R /Length {} >>\nstream\n",
733 classic_off,
734 fields.len()
735 )
736 .as_bytes(),
737 );
738 data.extend_from_slice(&fields);
739 data.extend_from_slice(b"\nendstream\nendobj\n");
740 data.extend_from_slice(format!("startxref\n{stream_off}\n%%EOF\n").as_bytes());
741
742 let xref = load_xref(&data).unwrap();
743 assert_eq!(xref.get(0), Some(XrefEntry::Free));
744 let infile = |offset: usize| {
745 Some(XrefEntry::InFile {
746 offset: offset as u64,
747 gen: 0,
748 })
749 };
750 assert_eq!(xref.get(1), infile(obj1), "only the classic section has 1");
751 assert_eq!(xref.get(2), infile(obj2_new), "newest section wins");
752 assert_eq!(xref.get(3), infile(obj3));
753 assert_eq!(xref.get(4), infile(stream_off));
754 assert_eq!(xref.trailer.get_int("Size"), Some(5), "newest trailer wins");
755 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
756 }
757
758 #[test]
760 fn hybrid_xrefstm_overrides_entries_the_table_marks_free() {
761 let mut data = b"%PDF-1.5\n".to_vec();
762 let obj1 = data.len();
763 data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
764 let obj2 = data.len();
765 data.extend_from_slice(b"2 0 obj\n(hidden)\nendobj\n");
766 let stm_off = data.len();
767 let mut fields = Vec::new();
768 for offset in [obj2, stm_off] {
769 fields.push(1u8);
770 fields.extend_from_slice(&(offset as u32).to_be_bytes());
771 fields.extend_from_slice(&0u16.to_be_bytes());
772 }
773 data.extend_from_slice(
774 format!(
775 "3 0 obj\n<< /Type /XRef /Size 4 /W [1 4 2] /Index [2 1 3 1] \
776 /Root 1 0 R /Length {} >>\nstream\n",
777 fields.len()
778 )
779 .as_bytes(),
780 );
781 data.extend_from_slice(&fields);
782 data.extend_from_slice(b"\nendstream\nendobj\n");
783 let classic_off = data.len();
784 data.extend_from_slice(b"xref\n0 3\n0000000000 65535 f\r\n");
785 data.extend_from_slice(format!("{obj1:010} 00000 n\r\n").as_bytes());
786 data.extend_from_slice(b"0000000000 00001 f\r\n"); data.extend_from_slice(
788 format!("trailer\n<< /Size 4 /Root 1 0 R /XRefStm {stm_off} >>\n").as_bytes(),
789 );
790 data.extend_from_slice(format!("startxref\n{classic_off}\n%%EOF\n").as_bytes());
791
792 let xref = load_xref(&data).unwrap();
793 assert_eq!(
794 xref.get(2),
795 Some(XrefEntry::InFile {
796 offset: obj2 as u64,
797 gen: 0
798 }),
799 "the hybrid stream entry beats the table's free entry"
800 );
801 assert_eq!(
802 xref.get(3),
803 Some(XrefEntry::InFile {
804 offset: stm_off as u64,
805 gen: 0
806 })
807 );
808 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
809 }
810
811 #[test]
813 fn prev_loop_is_broken_by_the_visited_guard() {
814 let mut data = b"%PDF-1.4\n".to_vec();
815 let obj1 = data.len();
816 data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
817 let xref_off = data.len();
818 data.extend_from_slice(b"xref\n0 2\n0000000000 65535 f\r\n");
819 data.extend_from_slice(format!("{obj1:010} 00000 n\r\n").as_bytes());
820 data.extend_from_slice(
821 format!("trailer\n<< /Size 2 /Root 1 0 R /Prev {xref_off} >>\n").as_bytes(),
822 );
823 data.extend_from_slice(format!("startxref\n{xref_off}\n%%EOF\n").as_bytes());
824 let xref = load_xref(&data).unwrap();
825 assert_eq!(
826 xref.get(1),
827 Some(XrefEntry::InFile {
828 offset: obj1 as u64,
829 gen: 0
830 })
831 );
832 }
833
834 #[test]
836 fn startxref_found_beyond_the_last_1_kib() {
837 let mut data = simple_doc("padded");
838 data.extend_from_slice(b"999 0 obj\n<< >>\nendobj\n");
840 data.extend_from_slice(&vec![b' '; 2048]);
841 let xref = load_xref(&data).unwrap();
842 assert_eq!(xref.get(999), None, "chain path used, not recovery");
843 for num in 1..=5 {
844 assert_points_at_header(&xref, &data, num);
845 }
846 }
847
848 #[test]
850 fn merge_keeps_first_seen_entries_and_trailer_keys() {
851 let mut newer = Xref::default();
852 newer.map.insert(1, XrefEntry::Free);
853 newer
854 .map
855 .insert(2, XrefEntry::InFile { offset: 20, gen: 0 });
856 newer
857 .trailer
858 .insert(Name("Size".to_string()), Object::Int(3));
859 let mut older = Xref::default();
860 older
861 .map
862 .insert(1, XrefEntry::InFile { offset: 10, gen: 0 });
863 older
864 .map
865 .insert(3, XrefEntry::InFile { offset: 30, gen: 1 });
866 older
867 .trailer
868 .insert(Name("Size".to_string()), Object::Int(9));
869 older
870 .trailer
871 .insert(Name("Info".to_string()), Object::Int(7));
872 newer.merge(older);
873 assert_eq!(newer.get(1), Some(XrefEntry::Free), "deletion is kept");
874 assert_eq!(newer.get(2), Some(XrefEntry::InFile { offset: 20, gen: 0 }));
875 assert_eq!(newer.get(3), Some(XrefEntry::InFile { offset: 30, gen: 1 }));
876 assert_eq!(newer.trailer.get_int("Size"), Some(3));
877 assert_eq!(newer.trailer.get_int("Info"), Some(7));
878 }
879
880 #[test]
881 fn iter_reports_every_entry() {
882 let data = pdfboss_testkit::simple_doc("iter");
883 let xref = load_xref(&data).unwrap();
884 let mut nums: Vec<u32> = xref.iter().map(|pair| pair.0).collect();
885 nums.sort_unstable();
886 assert_eq!(nums.len(), xref.len());
887 assert!(!xref.is_empty());
888 for (num, entry) in xref.iter() {
889 assert_eq!(xref.get(num), Some(entry));
890 }
891 }
892
893 #[test]
895 fn parse_section_at_classic_reports_spans() {
896 let data = pdfboss_testkit::simple_doc("spans");
897 let off = memchr::memmem::find(&data, b"xref").unwrap();
900 let info = parse_section_at(&data, off).unwrap();
901 assert_eq!(info.kind, crate::elements::XrefKind::Table);
902 assert!(info.prev.is_none());
903 assert!(info.xrefstm.is_none());
904 assert!(!info.xref.is_empty());
905 assert_eq!(info.span.start, off as u64);
907 let tspan = info.trailer_span.expect("classic sections have a trailer");
908 assert_eq!(info.span.end, tspan.start);
909 assert!(data[tspan.start as usize..].starts_with(b"trailer"));
910 let mut parser = Parser::at(&data, tspan.start as usize + b"trailer".len());
912 let reparsed = parser.parse_object(&NoResolve).unwrap();
913 assert_eq!(
914 reparsed.as_dict().unwrap().get("Root"),
915 info.xref.trailer.get("Root")
916 );
917 assert!(tspan.end as usize <= data.len());
918 }
919
920 #[test]
922 fn newest_section_reports_table_offset_and_kind() {
923 let data = simple_doc("newest section");
924 let off = startxref(&data).unwrap() as u64;
925 let xref = load_xref(&data).unwrap();
926 assert_eq!(
927 xref.newest_section(),
928 Some(NewestSection {
929 offset: off,
930 kind: XrefKind::Table
931 })
932 );
933 }
934
935 #[test]
937 fn newest_section_reports_stream_offset_and_kind() {
938 let (dict, payload) = objstm_payload(&[
939 (1, "<< /Type /Catalog /Pages 2 0 R >>"),
940 (2, "<< /Type /Pages /Kids [] /Count 0 >>"),
941 ]);
942 let mut b = PdfBuilder::new();
943 b.stream(5, &dict, &payload);
944 let data = b.build_xref_stream(1);
945 let off = startxref(&data).unwrap() as u64;
946 let xref = load_xref(&data).unwrap();
947 assert_eq!(
948 xref.newest_section(),
949 Some(NewestSection {
950 offset: off,
951 kind: XrefKind::Stream
952 })
953 );
954 }
955
956 #[test]
958 fn newest_section_hybrid_reports_table_at_table_offset() {
959 let data = pdfboss_testkit::hybrid_doc();
960 let off = startxref(&data).unwrap() as u64;
961 let xref = load_xref(&data).unwrap();
962 assert_eq!(
963 xref.newest_section(),
964 Some(NewestSection {
965 offset: off,
966 kind: XrefKind::Table
967 }),
968 "a hybrid base's newest section is its table, not the /XRefStm"
969 );
970 }
971
972 #[test]
973 fn newest_section_is_none_after_recovery_scan() {
974 let mut data = simple_doc("recovered");
975 corrupt_startxref(&mut data);
976 let xref = load_xref(&data).unwrap();
977 assert_eq!(xref.newest_section(), None);
978 }
979
980 #[test]
981 fn merge_keeps_the_newer_sides_newest_section() {
982 let mut newer = Xref {
983 newest: Some(NewestSection {
984 offset: 10,
985 kind: XrefKind::Stream,
986 }),
987 ..Xref::default()
988 };
989 let older = Xref {
990 newest: Some(NewestSection {
991 offset: 1,
992 kind: XrefKind::Table,
993 }),
994 ..Xref::default()
995 };
996 newer.merge(older);
997 assert_eq!(
998 newer.newest_section(),
999 Some(NewestSection {
1000 offset: 10,
1001 kind: XrefKind::Stream
1002 }),
1003 "first-write-wins, like trailer keys"
1004 );
1005
1006 let mut newer_without = Xref::default();
1007 let older_with = Xref {
1008 newest: Some(NewestSection {
1009 offset: 1,
1010 kind: XrefKind::Table,
1011 }),
1012 ..Xref::default()
1013 };
1014 newer_without.merge(older_with);
1015 assert_eq!(
1016 newer_without.newest_section(),
1017 Some(NewestSection {
1018 offset: 1,
1019 kind: XrefKind::Table
1020 }),
1021 "older's value adopted when the newer side has none"
1022 );
1023 }
1024
1025 #[test]
1027 fn parse_section_at_stream_reports_spans() {
1028 let mut builder = pdfboss_testkit::PdfBuilder::new();
1029 builder.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1030 builder.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1031 builder.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>");
1032 let data = builder.build_xref_stream(1);
1033 let startxref = memchr::memmem::rfind(&data, b"startxref").unwrap();
1034 let mut lexer = Lexer::at(&data, startxref + b"startxref".len());
1035 let off = match lexer.next_token().unwrap() {
1036 Token::Int(v) => v as usize,
1037 other => panic!("expected startxref offset, got {other:?}"),
1038 };
1039 let info = parse_section_at(&data, off).unwrap();
1040 assert_eq!(info.kind, crate::elements::XrefKind::Stream);
1041 assert!(info.trailer_span.is_none());
1042 assert_eq!(info.span.start, off as u64);
1043 assert!(info.span.end > info.span.start && info.span.end as u64 <= data.len() as u64);
1044 let body = &data[info.span.start as usize..info.span.end as usize];
1046 assert!(memchr::memmem::find(body, b"endstream").is_some());
1047 }
1048}