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, Default)]
27pub struct Xref {
28 map: FastMap<u32, XrefEntry>,
29 pub trailer: Dict,
31}
32
33impl Xref {
34 pub fn get(&self, num: u32) -> Option<XrefEntry> {
36 self.map.get(&num).copied()
37 }
38
39 fn add(&mut self, num: u32, entry: XrefEntry) {
41 self.map.entry(num).or_insert(entry);
42 }
43
44 pub fn merge(&mut self, older: Xref) {
48 for (num, entry) in older.map {
49 self.map.entry(num).or_insert(entry);
50 }
51 for (key, value) in older.trailer.iter() {
52 if self.trailer.get(&key.0).is_none() {
53 self.trailer.insert(key.clone(), value.clone());
54 }
55 }
56 }
57
58 pub fn iter(&self) -> impl Iterator<Item = (u32, XrefEntry)> + '_ {
60 self.map.iter().map(|(&num, &entry)| (num, entry))
61 }
62
63 pub fn len(&self) -> usize {
65 self.map.len()
66 }
67
68 pub fn is_empty(&self) -> bool {
70 self.map.is_empty()
71 }
72}
73
74pub fn load_xref(data: &[u8]) -> Result<Xref> {
80 let chained = find_startxref(data).and_then(|start| load_chain(data, start).ok());
81 match chained {
82 Some(xref) if xref.trailer.get("Root").is_some() => Ok(xref),
83 chained => recovery_scan(data).or_else(|err| chained.ok_or(err)),
84 }
85}
86
87fn find_startxref(data: &[u8]) -> Option<usize> {
91 for window in [1024usize, 64 * 1024] {
92 let tail = data.len().saturating_sub(window);
93 if let Some(rel) = memchr::memmem::rfind(&data[tail..], b"startxref") {
94 let mut lexer = Lexer::at(data, tail + rel + b"startxref".len());
95 if let Ok(Token::Int(v)) = lexer.next_token() {
96 if let Some(offset) = to_offset(v, data) {
97 return Some(offset);
98 }
99 }
100 }
101 if window >= data.len() {
102 break;
103 }
104 }
105 None
106}
107
108fn to_offset(v: i64, data: &[u8]) -> Option<usize> {
110 usize::try_from(v).ok().filter(|&o| o < data.len())
111}
112
113fn load_chain(data: &[u8], start: usize) -> Result<Xref> {
119 let mut acc = Xref::default();
120 let mut visited: FastSet<usize> = FastSet::default();
121 let mut next = Some(start);
122 while let Some(off) = next {
123 if !visited.insert(off) {
124 break;
125 }
126 let info = parse_section_at(data, off)?;
127 if let Some(xs) = info.xrefstm.and_then(|v| to_offset(v, data)) {
128 if visited.insert(xs) {
129 if let Ok(stream_info) = parse_section_at(data, xs) {
131 acc.merge(stream_info.xref);
132 }
133 }
134 }
135 acc.merge(info.xref);
136 next = info.prev.and_then(|v| to_offset(v, data));
137 }
138 if acc.map.is_empty() {
139 Err(Error::InvalidXref)
140 } else {
141 Ok(acc)
142 }
143}
144
145fn read_be(bytes: &[u8]) -> u64 {
147 bytes.iter().fold(0, |acc, &b| (acc << 8) | u64::from(b))
148}
149
150#[derive(Debug, Clone)]
153pub struct XrefSectionInfo {
154 pub xref: Xref,
156 pub kind: XrefKind,
157 pub prev: Option<i64>,
159 pub xrefstm: Option<i64>,
161 pub span: Span,
164 pub trailer_span: Option<Span>,
167}
168
169pub fn parse_section_at(data: &[u8], off: usize) -> Result<XrefSectionInfo> {
172 let mut lexer = Lexer::at(data, off);
173 if matches!(lexer.peek_token(), Ok(Token::Keyword(ref k)) if k.as_slice() == b"xref") {
174 parse_classic(data, off)
175 } else {
176 parse_stream_section(data, off)
177 }
178}
179
180fn parse_classic(data: &[u8], off: usize) -> Result<XrefSectionInfo> {
185 let mut lexer = Lexer::at(data, off);
186 match lexer.next_token()? {
187 Token::Keyword(ref k) if k.as_slice() == b"xref" => {}
188 _ => return Err(Error::InvalidXref),
189 }
190 let mut section = Xref::default();
191 loop {
192 match lexer.next_token()? {
193 Token::Int(start) if start >= 0 => {
194 let count = match lexer.next_token()? {
195 Token::Int(c) if c >= 0 => c as u64,
196 _ => return Err(Error::InvalidXref),
197 };
198 if count > data.len() as u64 / 11 + 1 {
201 return Err(Error::InvalidXref);
202 }
203 for i in 0..count {
204 let f1 = match lexer.next_token()? {
205 Token::Int(v) if v >= 0 => v as u64,
206 _ => return Err(Error::InvalidXref),
207 };
208 let f2 = match lexer.next_token()? {
209 Token::Int(v) if v >= 0 => v,
210 _ => return Err(Error::InvalidXref),
211 };
212 let entry = match lexer.next_token()? {
213 Token::Keyword(ref k) if k.as_slice() == b"n" => XrefEntry::InFile {
214 offset: f1,
215 gen: f2.min(65535) as u16,
216 },
217 Token::Keyword(ref k) if k.as_slice() == b"f" => XrefEntry::Free,
218 _ => return Err(Error::InvalidXref),
219 };
220 if let Ok(num) = u32::try_from(start as u64 + i) {
221 section.add(num, entry);
222 }
223 }
224 }
225 Token::Keyword(ref k) if k.as_slice() == b"trailer" => {
226 let trailer_start = lexer.pos() - b"trailer".len();
229 let mut parser = Parser::at(data, lexer.pos());
230 let trailer = match parser.parse_object(&NoResolve)? {
231 Object::Dict(d) => d,
232 _ => return Err(Error::InvalidXref),
233 };
234 let prev = trailer.get_int("Prev");
235 let xrefstm = trailer.get_int("XRefStm");
236 section.trailer = trailer;
237 return Ok(XrefSectionInfo {
238 xref: section,
239 kind: XrefKind::Table,
240 prev,
241 xrefstm,
242 span: Span::new(off as u64, trailer_start as u64),
243 trailer_span: Some(Span::new(trailer_start as u64, parser.pos() as u64)),
244 });
245 }
246 _ => return Err(Error::InvalidXref),
247 }
248 }
249}
250
251fn parse_stream_section(data: &[u8], off: usize) -> Result<XrefSectionInfo> {
256 let mut parser = Parser::at(data, off);
257 let (_, obj) = parser.parse_indirect(&NoResolve)?;
258 let end = parser.pos();
259 let stream = match obj {
260 Object::Stream(s) => s,
261 _ => return Err(Error::InvalidXref),
262 };
263 let decoded = decode_stream(&stream, &NoResolve).map_err(|_| Error::InvalidXref)?;
264 let dict = stream.dict;
265 let widths: Vec<usize> = dict
266 .get_array("W")
267 .ok_or(Error::InvalidXref)?
268 .iter()
269 .map(|v| {
270 v.as_int()
271 .filter(|&n| (0..=8).contains(&n))
272 .map(|n| n as usize)
273 })
274 .collect::<Option<Vec<_>>>()
275 .ok_or(Error::InvalidXref)?;
276 let w1 = widths.first().copied().unwrap_or(0);
277 let w2 = widths.get(1).copied().unwrap_or(0);
278 let w3 = widths.get(2).copied().unwrap_or(0);
279 let entry_len = w1 + w2 + w3;
280 if entry_len == 0 {
281 return Err(Error::InvalidXref);
282 }
283 let size = dict.get_int("Size").unwrap_or(0).max(0) as u64;
284 let subsections: Vec<(u64, u64)> = match dict.get_array("Index") {
285 Some(index) => index
286 .chunks(2)
287 .filter_map(|pair| {
288 let start = pair.first()?.as_int()?;
289 let count = pair.get(1)?.as_int()?;
290 (start >= 0 && count >= 0).then_some((start as u64, count as u64))
291 })
292 .collect(),
293 None => vec![(0, size)],
294 };
295 let mut section = Xref::default();
296 let mut pos = 0usize;
297 'subsections: for (start, count) in subsections {
298 for i in 0..count {
299 if pos + entry_len > decoded.len() {
300 break 'subsections; }
302 let kind = if w1 == 0 {
303 1
304 } else {
305 read_be(&decoded[pos..pos + w1])
306 };
307 let f2 = read_be(&decoded[pos + w1..pos + w1 + w2]);
308 let f3 = read_be(&decoded[pos + w1 + w2..pos + entry_len]);
309 pos += entry_len;
310 let entry = match kind {
311 1 => XrefEntry::InFile {
312 offset: f2,
313 gen: f3.min(65535) as u16,
314 },
315 2 => match (u32::try_from(f2), u32::try_from(f3)) {
316 (Ok(stream_num), Ok(index)) => XrefEntry::InStream { stream_num, index },
317 _ => XrefEntry::Free,
318 },
319 _ => XrefEntry::Free,
322 };
323 if let Ok(num) = u32::try_from(start + i) {
324 section.add(num, entry);
325 }
326 }
327 }
328 let prev = dict.get_int("Prev");
329 section.trailer = dict;
330 Ok(XrefSectionInfo {
331 xref: section,
332 kind: XrefKind::Stream,
333 prev,
334 xrefstm: None,
335 span: Span::new(off as u64, end as u64),
336 trailer_span: None,
337 })
338}
339
340fn recovery_scan(data: &[u8]) -> Result<Xref> {
345 let mut xref = Xref::default();
346 for pos in memchr::memmem::find_iter(data, b"obj") {
347 if let Some((num, gen, start)) = obj_header_before(data, pos) {
348 xref.map.insert(
350 num,
351 XrefEntry::InFile {
352 offset: start as u64,
353 gen,
354 },
355 );
356 }
357 }
358 if xref.map.is_empty() {
359 return Err(Error::InvalidXref);
360 }
361 let trailers: Vec<usize> = memchr::memmem::find_iter(data, b"trailer").collect();
362 for &tp in trailers.iter().rev() {
363 let mut parser = Parser::at(data, tp + b"trailer".len());
364 if let Ok(Object::Dict(dict)) = parser.parse_object(&NoResolve) {
365 let has_root = dict.get("Root").is_some();
366 if xref.trailer.is_empty() || has_root {
367 xref.trailer = dict;
368 }
369 if has_root {
370 break;
371 }
372 }
373 }
374 if xref.trailer.get("Root").is_none() {
375 if let Some(catalog) = find_catalog(data, &xref) {
376 xref.trailer
377 .insert(Name("Root".to_string()), Object::Ref(catalog));
378 }
379 }
380 if xref.trailer.get("Size").is_none() {
381 let size = xref.map.keys().max().map_or(0, |&m| i64::from(m) + 1);
382 xref.trailer
383 .insert(Name("Size".to_string()), Object::Int(size));
384 }
385 Ok(xref)
386}
387
388fn find_catalog(data: &[u8], xref: &Xref) -> Option<ObjRef> {
391 let mut nums: Vec<u32> = xref.map.keys().copied().collect();
392 nums.sort_unstable();
393 for num in nums {
394 let XrefEntry::InFile { offset, .. } = xref.map[&num] else {
395 continue;
396 };
397 let mut parser = Parser::at(data, offset as usize);
398 let Ok((r, obj)) = parser.parse_indirect(&NoResolve) else {
399 continue;
400 };
401 let is_catalog = obj
402 .as_dict()
403 .and_then(|d| d.get_name("Type"))
404 .is_some_and(|n| n.0 == "Catalog");
405 if is_catalog {
406 return Some(r);
407 }
408 }
409 None
410}
411
412fn obj_header_before(data: &[u8], kw: usize) -> Option<(u32, u16, usize)> {
417 if let Some(&after) = data.get(kw + 3) {
418 if !is_token_boundary(after) {
419 return None;
420 }
421 }
422 let gen_end = strip_ws_back(data, kw);
423 let gen_start = strip_digits_back(data, gen_end);
424 let num_end = strip_ws_back(data, gen_start);
425 let num_start = strip_digits_back(data, num_end);
426 if gen_end == kw || gen_start == gen_end || num_end == gen_start || num_start == num_end {
427 return None;
428 }
429 if num_start > 0 && !is_token_boundary(data[num_start - 1]) {
430 return None;
431 }
432 let gen: u16 = ascii_int(&data[gen_start..gen_end])?;
433 let num: u32 = ascii_int(&data[num_start..num_end])?;
434 Some((num, gen, num_start))
435}
436
437fn strip_ws_back(data: &[u8], mut end: usize) -> usize {
439 while end > 0 && is_pdf_whitespace(data[end - 1]) {
440 end -= 1;
441 }
442 end
443}
444
445fn strip_digits_back(data: &[u8], mut end: usize) -> usize {
447 while end > 0 && data[end - 1].is_ascii_digit() {
448 end -= 1;
449 }
450 end
451}
452
453fn ascii_int<T: std::str::FromStr>(bytes: &[u8]) -> Option<T> {
455 std::str::from_utf8(bytes).ok()?.parse().ok()
456}
457
458fn is_token_boundary(b: u8) -> bool {
460 is_pdf_whitespace(b)
461 || matches!(
462 b,
463 b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
464 )
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use pdfboss_testkit::{objstm_payload, simple_doc, PdfBuilder};
471
472 fn pos_of(data: &[u8], needle: &[u8]) -> usize {
474 memchr::memmem::find(data, needle).unwrap()
475 }
476
477 fn assert_points_at_header(xref: &Xref, data: &[u8], num: u32) {
480 let header = format!("{num} 0 obj");
481 let expected = pos_of(data, header.as_bytes()) as u64;
482 assert_eq!(
483 xref.get(num),
484 Some(XrefEntry::InFile {
485 offset: expected,
486 gen: 0
487 }),
488 "entry for object {num}"
489 );
490 }
491
492 fn corrupt_startxref(data: &mut [u8]) {
495 let mut i = memchr::memmem::rfind(data, b"startxref").unwrap() + b"startxref".len();
496 while !data[i].is_ascii_digit() {
497 i += 1;
498 }
499 while data[i].is_ascii_digit() {
500 data[i] = b'9';
501 i += 1;
502 }
503 }
504
505 #[test]
506 fn classic_table_loads_all_entries() {
507 let data = simple_doc("Hello");
508 let xref = load_xref(&data).unwrap();
509 assert_eq!(xref.get(0), Some(XrefEntry::Free));
510 for num in 1..=5 {
511 assert_points_at_header(&xref, &data, num);
512 }
513 assert_eq!(xref.get(6), None);
514 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
515 assert_eq!(xref.trailer.get_int("Size"), Some(6));
516 }
517
518 #[test]
519 fn xref_stream_loads_infile_and_instream_entries() {
520 let (dict, payload) = objstm_payload(&[
521 (1, "<< /Type /Catalog /Pages 2 0 R >>"),
522 (2, "<< /Type /Pages /Kids [] /Count 0 >>"),
523 (5, "(text)"),
524 ]);
525 let mut b = PdfBuilder::new();
526 b.stream(6, &dict, &payload);
527 b.stream(4, "", b"BT ET");
528 let data = b.build_xref_stream(1);
529 let xref = load_xref(&data).unwrap();
530 assert_eq!(xref.get(0), Some(XrefEntry::Free));
531 for (num, index) in [(1, 0), (2, 1), (5, 2)] {
532 assert_eq!(
533 xref.get(num),
534 Some(XrefEntry::InStream {
535 stream_num: 6,
536 index
537 }),
538 "type-2 entry for object {num}"
539 );
540 }
541 for num in [4, 6, 7] {
542 assert_points_at_header(&xref, &data, num);
543 }
544 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
545 assert_eq!(
546 xref.trailer.get_name("Type").map(|n| n.0.as_str()),
547 Some("XRef"),
548 "the stream's own dictionary is the trailer"
549 );
550 }
551
552 #[test]
553 fn recovery_scan_after_corrupt_startxref() {
554 let mut data = simple_doc("rescue me");
555 corrupt_startxref(&mut data);
556 let xref = load_xref(&data).unwrap();
557 for num in 1..=5 {
558 assert_points_at_header(&xref, &data, num);
559 }
560 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
561 }
562
563 #[test]
564 fn recovery_finds_root_via_catalog_when_trailer_is_unreadable() {
565 let mut data = simple_doc("x");
566 corrupt_startxref(&mut data);
567 let tp = memchr::memmem::rfind(&data, b"trailer").unwrap();
568 data[tp..tp + b"trailer".len()].copy_from_slice(b"trai1er");
569 let xref = load_xref(&data).unwrap();
570 for num in 1..=5 {
571 assert_points_at_header(&xref, &data, num);
572 }
573 let root = xref.trailer.get_ref("Root").unwrap();
574 assert_eq!((root.num, root.gen), (1, 0), "object 1 is the catalog");
575 assert_eq!(xref.trailer.get_int("Size"), Some(6), "synthesized /Size");
576 }
577
578 #[test]
579 fn recovery_last_occurrence_of_an_object_wins() {
580 let mut data = simple_doc("x");
581 corrupt_startxref(&mut data);
582 let redefinition = data.len() as u64;
583 data.extend_from_slice(b"5 0 obj\n<< /Replaced true >>\nendobj\n");
584 let xref = load_xref(&data).unwrap();
585 assert_eq!(
586 xref.get(5),
587 Some(XrefEntry::InFile {
588 offset: redefinition,
589 gen: 0
590 })
591 );
592 for num in 1..=4 {
593 assert_points_at_header(&xref, &data, num);
594 }
595 }
596
597 #[test]
598 fn recovery_ignores_endobj_and_bad_headers() {
599 let data = b"garbage endobj more\n7 2 obj\n<< /Type /Catalog >>\nendobj\nxobj 9 9";
600 let xref = load_xref(data).unwrap();
601 let offset = pos_of(data, b"7 2 obj") as u64;
602 assert_eq!(xref.get(7), Some(XrefEntry::InFile { offset, gen: 2 }));
603 assert_eq!(xref.map.len(), 1, "only the real header is recovered");
604 let root = xref.trailer.get_ref("Root").unwrap();
605 assert_eq!((root.num, root.gen), (7, 2));
606 }
607
608 #[test]
609 fn unrecoverable_garbage_is_invalid_xref() {
610 assert!(matches!(load_xref(b"not a pdf"), Err(Error::InvalidXref)));
611 assert!(matches!(load_xref(b""), Err(Error::InvalidXref)));
612 }
613
614 #[test]
615 fn classic_table_with_19_and_21_byte_lines() {
616 let mut data = b"%PDF-1.4\n".to_vec();
617 let obj1 = data.len();
618 data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
619 let obj2 = data.len();
620 data.extend_from_slice(b"2 0 obj\n(hi)\nendobj\n");
621 let xref_off = data.len();
622 data.extend_from_slice(b"xref\n0 3\n");
623 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");
627 data.extend_from_slice(format!("startxref\n{xref_off}\n%%EOF\n").as_bytes());
628 let xref = load_xref(&data).unwrap();
629 assert_eq!(xref.get(0), Some(XrefEntry::Free));
631 for (num, offset) in [(1, obj1), (2, obj2)] {
632 assert_eq!(
633 xref.get(num),
634 Some(XrefEntry::InFile {
635 offset: offset as u64,
636 gen: 0
637 })
638 );
639 }
640 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
641 }
642
643 #[test]
644 fn xref_stream_prev_chains_to_classic_section() {
645 let mut data = b"%PDF-1.5\n".to_vec();
646 let obj1 = data.len();
647 data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
648 let obj2_old = data.len();
649 data.extend_from_slice(b"2 0 obj\n(old)\nendobj\n");
650 let classic_off = data.len();
651 data.extend_from_slice(b"xref\n0 3\n0000000000 65535 f\r\n");
652 data.extend_from_slice(format!("{obj1:010} 00000 n\r\n").as_bytes());
653 data.extend_from_slice(format!("{obj2_old:010} 00000 n\r\n").as_bytes());
654 data.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
655 let obj2_new = data.len();
657 data.extend_from_slice(b"2 0 obj\n(new)\nendobj\n");
658 let obj3 = data.len();
659 data.extend_from_slice(b"3 0 obj\n42\nendobj\n");
660 let stream_off = data.len();
661 let mut fields = Vec::new();
662 for offset in [obj2_new, obj3, stream_off] {
663 fields.push(1u8);
664 fields.extend_from_slice(&(offset as u32).to_be_bytes());
665 fields.extend_from_slice(&0u16.to_be_bytes());
666 }
667 data.extend_from_slice(
668 format!(
669 "4 0 obj\n<< /Type /XRef /Size 5 /W [1 4 2] /Index [2 3] \
670 /Prev {} /Root 1 0 R /Length {} >>\nstream\n",
671 classic_off,
672 fields.len()
673 )
674 .as_bytes(),
675 );
676 data.extend_from_slice(&fields);
677 data.extend_from_slice(b"\nendstream\nendobj\n");
678 data.extend_from_slice(format!("startxref\n{stream_off}\n%%EOF\n").as_bytes());
679
680 let xref = load_xref(&data).unwrap();
681 assert_eq!(xref.get(0), Some(XrefEntry::Free));
682 let infile = |offset: usize| {
683 Some(XrefEntry::InFile {
684 offset: offset as u64,
685 gen: 0,
686 })
687 };
688 assert_eq!(xref.get(1), infile(obj1), "only the classic section has 1");
689 assert_eq!(xref.get(2), infile(obj2_new), "newest section wins");
690 assert_eq!(xref.get(3), infile(obj3));
691 assert_eq!(xref.get(4), infile(stream_off));
692 assert_eq!(xref.trailer.get_int("Size"), Some(5), "newest trailer wins");
693 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
694 }
695
696 #[test]
697 fn hybrid_xrefstm_overrides_entries_the_table_marks_free() {
698 let mut data = b"%PDF-1.5\n".to_vec();
699 let obj1 = data.len();
700 data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
701 let obj2 = data.len();
702 data.extend_from_slice(b"2 0 obj\n(hidden)\nendobj\n");
703 let stm_off = data.len();
704 let mut fields = Vec::new();
705 for offset in [obj2, stm_off] {
706 fields.push(1u8);
707 fields.extend_from_slice(&(offset as u32).to_be_bytes());
708 fields.extend_from_slice(&0u16.to_be_bytes());
709 }
710 data.extend_from_slice(
711 format!(
712 "3 0 obj\n<< /Type /XRef /Size 4 /W [1 4 2] /Index [2 1 3 1] \
713 /Root 1 0 R /Length {} >>\nstream\n",
714 fields.len()
715 )
716 .as_bytes(),
717 );
718 data.extend_from_slice(&fields);
719 data.extend_from_slice(b"\nendstream\nendobj\n");
720 let classic_off = data.len();
721 data.extend_from_slice(b"xref\n0 3\n0000000000 65535 f\r\n");
722 data.extend_from_slice(format!("{obj1:010} 00000 n\r\n").as_bytes());
723 data.extend_from_slice(b"0000000000 00001 f\r\n"); data.extend_from_slice(
725 format!("trailer\n<< /Size 4 /Root 1 0 R /XRefStm {stm_off} >>\n").as_bytes(),
726 );
727 data.extend_from_slice(format!("startxref\n{classic_off}\n%%EOF\n").as_bytes());
728
729 let xref = load_xref(&data).unwrap();
730 assert_eq!(
731 xref.get(2),
732 Some(XrefEntry::InFile {
733 offset: obj2 as u64,
734 gen: 0
735 }),
736 "the hybrid stream entry beats the table's free entry"
737 );
738 assert_eq!(
739 xref.get(3),
740 Some(XrefEntry::InFile {
741 offset: stm_off as u64,
742 gen: 0
743 })
744 );
745 assert_eq!(xref.trailer.get_ref("Root").map(|r| r.num), Some(1));
746 }
747
748 #[test]
749 fn prev_loop_is_broken_by_the_visited_guard() {
750 let mut data = b"%PDF-1.4\n".to_vec();
751 let obj1 = data.len();
752 data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
753 let xref_off = data.len();
754 data.extend_from_slice(b"xref\n0 2\n0000000000 65535 f\r\n");
755 data.extend_from_slice(format!("{obj1:010} 00000 n\r\n").as_bytes());
756 data.extend_from_slice(
757 format!("trailer\n<< /Size 2 /Root 1 0 R /Prev {xref_off} >>\n").as_bytes(),
758 );
759 data.extend_from_slice(format!("startxref\n{xref_off}\n%%EOF\n").as_bytes());
760 let xref = load_xref(&data).unwrap();
761 assert_eq!(
762 xref.get(1),
763 Some(XrefEntry::InFile {
764 offset: obj1 as u64,
765 gen: 0
766 })
767 );
768 }
769
770 #[test]
771 fn startxref_found_beyond_the_last_1_kib() {
772 let mut data = simple_doc("padded");
773 data.extend_from_slice(b"999 0 obj\n<< >>\nendobj\n");
775 data.extend_from_slice(&vec![b' '; 2048]);
776 let xref = load_xref(&data).unwrap();
777 assert_eq!(xref.get(999), None, "chain path used, not recovery");
778 for num in 1..=5 {
779 assert_points_at_header(&xref, &data, num);
780 }
781 }
782
783 #[test]
784 fn merge_keeps_first_seen_entries_and_trailer_keys() {
785 let mut newer = Xref::default();
786 newer.map.insert(1, XrefEntry::Free);
787 newer
788 .map
789 .insert(2, XrefEntry::InFile { offset: 20, gen: 0 });
790 newer
791 .trailer
792 .insert(Name("Size".to_string()), Object::Int(3));
793 let mut older = Xref::default();
794 older
795 .map
796 .insert(1, XrefEntry::InFile { offset: 10, gen: 0 });
797 older
798 .map
799 .insert(3, XrefEntry::InFile { offset: 30, gen: 1 });
800 older
801 .trailer
802 .insert(Name("Size".to_string()), Object::Int(9));
803 older
804 .trailer
805 .insert(Name("Info".to_string()), Object::Int(7));
806 newer.merge(older);
807 assert_eq!(newer.get(1), Some(XrefEntry::Free), "deletion is kept");
808 assert_eq!(newer.get(2), Some(XrefEntry::InFile { offset: 20, gen: 0 }));
809 assert_eq!(newer.get(3), Some(XrefEntry::InFile { offset: 30, gen: 1 }));
810 assert_eq!(newer.trailer.get_int("Size"), Some(3));
811 assert_eq!(newer.trailer.get_int("Info"), Some(7));
812 }
813
814 #[test]
815 fn iter_reports_every_entry() {
816 let data = pdfboss_testkit::simple_doc("iter");
817 let xref = load_xref(&data).unwrap();
818 let mut nums: Vec<u32> = xref.iter().map(|pair| pair.0).collect();
819 nums.sort_unstable();
820 assert_eq!(nums.len(), xref.len());
821 assert!(!xref.is_empty());
822 for (num, entry) in xref.iter() {
823 assert_eq!(xref.get(num), Some(entry));
824 }
825 }
826
827 #[test]
828 fn parse_section_at_classic_reports_spans() {
829 let data = pdfboss_testkit::simple_doc("spans");
830 let off = memchr::memmem::find(&data, b"xref").unwrap();
833 let info = parse_section_at(&data, off).unwrap();
834 assert_eq!(info.kind, crate::elements::XrefKind::Table);
835 assert!(info.prev.is_none());
836 assert!(info.xrefstm.is_none());
837 assert!(!info.xref.is_empty());
838 assert_eq!(info.span.start, off as u64);
840 let tspan = info.trailer_span.expect("classic sections have a trailer");
841 assert_eq!(info.span.end, tspan.start);
842 assert!(data[tspan.start as usize..].starts_with(b"trailer"));
843 let mut parser = Parser::at(&data, tspan.start as usize + b"trailer".len());
845 let reparsed = parser.parse_object(&NoResolve).unwrap();
846 assert_eq!(
847 reparsed.as_dict().unwrap().get("Root"),
848 info.xref.trailer.get("Root")
849 );
850 assert!(tspan.end as usize <= data.len());
851 }
852
853 #[test]
854 fn parse_section_at_stream_reports_spans() {
855 let mut builder = pdfboss_testkit::PdfBuilder::new();
856 builder.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
857 builder.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
858 builder.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>");
859 let data = builder.build_xref_stream(1);
860 let startxref = memchr::memmem::rfind(&data, b"startxref").unwrap();
861 let mut lexer = Lexer::at(&data, startxref + b"startxref".len());
862 let off = match lexer.next_token().unwrap() {
863 Token::Int(v) => v as usize,
864 other => panic!("expected startxref offset, got {other:?}"),
865 };
866 let info = parse_section_at(&data, off).unwrap();
867 assert_eq!(info.kind, crate::elements::XrefKind::Stream);
868 assert!(info.trailer_span.is_none());
869 assert_eq!(info.span.start, off as u64);
870 assert!(info.span.end > info.span.start && info.span.end as u64 <= data.len() as u64);
871 let body = &data[info.span.start as usize..info.span.end as usize];
873 assert!(memchr::memmem::find(body, b"endstream").is_some());
874 }
875}