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