1use std::collections::HashMap;
2
3use zpdf_core::{Error, ObjectId, ParseLimits, PdfDict, PdfObject, Result};
4
5use crate::lexer::Lexer;
6
7#[derive(Debug, Clone)]
8pub enum XrefEntry {
9 InUse {
10 offset: u64,
11 gen: u16,
12 },
13 Free {
14 next: u32,
15 gen: u16,
16 },
17 Compressed {
18 stream_obj: u32,
19 index_in_stream: u32,
20 },
21}
22
23#[derive(Debug, Clone, Default)]
24pub struct XrefTable {
25 entries: HashMap<ObjectId, XrefEntry>,
26}
27
28impl XrefTable {
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn get(&self, id: ObjectId) -> Option<&XrefEntry> {
34 self.entries.get(&id)
35 }
36
37 pub fn insert(&mut self, id: ObjectId, entry: XrefEntry) {
38 self.entries.entry(id).or_insert(entry);
39 }
40
41 pub fn insert_overwrite(&mut self, id: ObjectId, entry: XrefEntry) {
46 self.entries.insert(id, entry);
47 }
48
49 pub fn len(&self) -> usize {
50 self.entries.len()
51 }
52
53 pub fn is_empty(&self) -> bool {
54 self.entries.is_empty()
55 }
56
57 pub fn object_ids(&self) -> impl Iterator<Item = ObjectId> + '_ {
58 self.entries.keys().copied()
59 }
60}
61
62pub fn parse_xref_and_trailer(data: &[u8], limits: &ParseLimits) -> Result<(XrefTable, PdfDict)> {
63 let startxref_offset = find_startxref(data)?;
64 let mut table = XrefTable::new();
65
66 let xref_offset = startxref_offset;
67 let (trailer, next_prev) = parse_xref_section(data, xref_offset, &mut table, limits)?;
68
69 let mut visited = std::collections::HashSet::new();
73 visited.insert(xref_offset);
74 parse_hybrid_xrefstm(data, &trailer, &mut table, limits, &mut visited);
77 let mut prev = next_prev;
78 while let Some(prev_offset) = prev {
79 if !visited.insert(prev_offset as usize) {
80 break;
81 }
82 let (section_trailer, next) =
83 parse_xref_section(data, prev_offset as usize, &mut table, limits)?;
84 parse_hybrid_xrefstm(data, §ion_trailer, &mut table, limits, &mut visited);
85 prev = next;
86 }
87
88 Ok((table, trailer))
89}
90
91fn parse_hybrid_xrefstm(
99 data: &[u8],
100 trailer: &PdfDict,
101 table: &mut XrefTable,
102 limits: &ParseLimits,
103 visited: &mut std::collections::HashSet<usize>,
104) {
105 let Some(PdfObject::Integer(off)) = trailer.get("XRefStm") else {
106 return;
107 };
108 let Ok(off) = usize::try_from(*off) else {
109 tracing::warn!("/XRefStm offset {off} is negative; ignoring");
110 return;
111 };
112 if !visited.insert(off) {
114 return;
115 }
116 if let Err(e) = parse_xref_stream(data, off, table, limits) {
117 tracing::warn!("failed to parse /XRefStm at offset {off}: {e}");
118 }
119}
120
121fn parse_xref_section(
122 data: &[u8],
123 offset: usize,
124 table: &mut XrefTable,
125 limits: &ParseLimits,
126) -> Result<(PdfDict, Option<u64>)> {
127 if offset >= data.len() {
131 return Err(Error::InvalidXref(offset as u64));
132 }
133 if data[offset..].starts_with(b"xref") {
134 parse_traditional_xref(data, offset, table, limits)
135 } else {
136 parse_xref_stream(data, offset, table, limits)
137 }
138}
139
140fn parse_xref_stream(
141 data: &[u8],
142 offset: usize,
143 table: &mut XrefTable,
144 limits: &ParseLimits,
145) -> Result<(PdfDict, Option<u64>)> {
146 use crate::filters;
147 use crate::object_parser::ObjectParser;
148
149 let parser = ObjectParser::new(data, limits);
150 let obj = parser.parse_indirect_at(offset)?;
151 let stream = match obj {
152 PdfObject::Stream(s) => s,
153 _ => return Err(Error::InvalidXref(offset as u64)),
154 };
155
156 let dict = &stream.dict;
157 if dict.get_name("Type").unwrap_or("") != "XRef" {
158 return Err(Error::InvalidXref(offset as u64));
159 }
160
161 let size = dict.get_i64("Size")? as u32;
162
163 let w_arr = dict.get_array("W")?;
167 if w_arr.len() != 3 {
168 return Err(Error::InvalidXref(offset as u64));
169 }
170 let field_width = |obj: &PdfObject| -> Result<usize> {
171 let w = obj.as_i64()?;
172 if !(0..=8).contains(&w) {
173 return Err(Error::InvalidXref(offset as u64));
174 }
175 Ok(w as usize)
176 };
177 let w1 = field_width(&w_arr[0])?;
178 let w2 = field_width(&w_arr[1])?;
179 let w3 = field_width(&w_arr[2])?;
180 let entry_size = w1 + w2 + w3;
181 if entry_size == 0 {
182 return Err(Error::InvalidXref(offset as u64));
183 }
184
185 let decoded = filters::decode_stream_with_limits(&stream.data, dict, limits)?;
187
188 let index_ranges: Vec<(u32, u32)> = if let Ok(idx_arr) = dict.get_array("Index") {
190 idx_arr
191 .chunks(2)
192 .filter_map(|pair| {
193 if pair.len() == 2 {
194 Some((pair[0].as_i64().ok()? as u32, pair[1].as_i64().ok()? as u32))
195 } else {
196 None
197 }
198 })
199 .collect()
200 } else {
201 vec![(0, size)]
202 };
203
204 let mut pos = 0usize;
205 for &(start, count) in &index_ranges {
206 let _range_end = start
208 .checked_add(count)
209 .ok_or(Error::InvalidXref(offset as u64))?;
210
211 let new_total = table
213 .len()
214 .checked_add(count as usize)
215 .ok_or(Error::InvalidXref(offset as u64))?;
216 if new_total > limits.max_objects as usize {
217 return Err(Error::InvalidXref(offset as u64));
218 }
219
220 for i in 0..count {
221 if pos + entry_size > decoded.len() {
222 break;
223 }
224 let obj_num = start + i;
225
226 let field1 = read_field(&decoded[pos..], w1);
227 let field2 = read_field(&decoded[pos + w1..], w2);
228 let field3 = read_field(&decoded[pos + w1 + w2..], w3);
229 pos += entry_size;
230
231 let entry_type = if w1 == 0 { 1 } else { field1 as u8 };
232 let id = ObjectId(obj_num, field3 as u16);
233
234 match entry_type {
235 0 => {
236 table.insert(
237 id,
238 XrefEntry::Free {
239 next: field2 as u32,
240 gen: field3 as u16,
241 },
242 );
243 }
244 1 => {
245 table.insert(
246 id,
247 XrefEntry::InUse {
248 offset: field2,
249 gen: field3 as u16,
250 },
251 );
252 }
253 2 => {
254 table.insert(
255 ObjectId(obj_num, 0),
256 XrefEntry::Compressed {
257 stream_obj: field2 as u32,
258 index_in_stream: field3 as u32,
259 },
260 );
261 }
262 _ => {}
263 }
264 }
265 }
266
267 let trailer = dict.clone();
269 let prev = trailer.get("Prev").and_then(|obj| match obj {
270 PdfObject::Integer(n) => Some(*n as u64),
271 _ => None,
272 });
273
274 Ok((trailer, prev))
275}
276
277fn read_field(data: &[u8], width: usize) -> u64 {
278 let mut val = 0u64;
279 for &byte in &data[..width] {
280 val = (val << 8) | byte as u64;
281 }
282 val
283}
284
285fn parse_traditional_xref(
286 data: &[u8],
287 offset: usize,
288 table: &mut XrefTable,
289 limits: &ParseLimits,
290) -> Result<(PdfDict, Option<u64>)> {
291 let mut pos = offset + 4; skip_eol(data, &mut pos);
293
294 loop {
296 skip_whitespace(data, &mut pos);
297
298 if pos >= data.len() {
302 return Err(Error::InvalidXref(pos as u64));
303 }
304
305 if data[pos..].starts_with(b"trailer") {
306 pos += 7;
307 break;
308 }
309
310 let (first_obj, count) = parse_subsection_header(data, &mut pos)?;
312
313 let range_end = first_obj
315 .checked_add(count)
316 .ok_or(Error::InvalidXref(pos as u64))?;
317 if range_end > limits.max_objects {
318 return Err(Error::InvalidXref(pos as u64));
319 }
320
321 let new_total = table
323 .len()
324 .checked_add(count as usize)
325 .ok_or(Error::InvalidXref(pos as u64))?;
326 if new_total > limits.max_objects as usize {
327 return Err(Error::InvalidXref(pos as u64));
328 }
329
330 for i in 0..count {
331 skip_whitespace(data, &mut pos);
332 let (entry_offset, gen, in_use) = parse_xref_entry_at(data, &mut pos)?;
338 let id = ObjectId(first_obj + i, gen);
339
340 if in_use {
341 table.insert(
342 id,
343 XrefEntry::InUse {
344 offset: entry_offset,
345 gen,
346 },
347 );
348 } else {
349 table.insert(
350 id,
351 XrefEntry::Free {
352 next: entry_offset as u32,
353 gen,
354 },
355 );
356 }
357 }
358 }
359
360 let mut lex = Lexer::new(data, pos, limits);
362 let trailer_obj = lex.next_token()?;
363 let trailer = match trailer_obj {
364 PdfObject::Dict(d) => d,
365 _ => return Err(Error::InvalidXref(pos as u64)),
366 };
367
368 let prev = trailer.get("Prev").and_then(|obj| match obj {
369 PdfObject::Integer(n) => Some(*n as u64),
370 _ => None,
371 });
372
373 Ok((trailer, prev))
374}
375
376fn find_startxref(data: &[u8]) -> Result<usize> {
377 let marker = b"startxref";
384 let marker_pos = data
385 .windows(marker.len())
386 .rposition(|w| w == marker)
387 .ok_or(Error::InvalidXref(0))?;
388
389 let after_marker = marker_pos + marker.len();
390 let num_start = data[after_marker..]
391 .iter()
392 .position(|b| b.is_ascii_digit())
393 .ok_or(Error::InvalidXref(0))?;
394
395 let num_bytes = &data[after_marker + num_start..];
396 let num_end = num_bytes
397 .iter()
398 .position(|b| !b.is_ascii_digit())
399 .unwrap_or(num_bytes.len());
400
401 let offset_str =
402 std::str::from_utf8(&num_bytes[..num_end]).map_err(|_| Error::InvalidXref(0))?;
403 let offset: usize = offset_str.parse().map_err(|_| Error::InvalidXref(0))?;
404
405 Ok(offset)
406}
407
408fn parse_subsection_header(data: &[u8], pos: &mut usize) -> Result<(u32, u32)> {
409 let start = *pos;
410 while *pos < data.len() && data[*pos].is_ascii_digit() {
411 *pos += 1;
412 }
413 let first: u32 = std::str::from_utf8(&data[start..*pos])
414 .map_err(|_| Error::InvalidXref(start as u64))?
415 .parse()
416 .map_err(|_| Error::InvalidXref(start as u64))?;
417
418 skip_whitespace(data, pos);
419
420 let count_start = *pos;
421 while *pos < data.len() && data[*pos].is_ascii_digit() {
422 *pos += 1;
423 }
424 let count: u32 = std::str::from_utf8(&data[count_start..*pos])
425 .map_err(|_| Error::InvalidXref(count_start as u64))?
426 .parse()
427 .map_err(|_| Error::InvalidXref(count_start as u64))?;
428
429 skip_eol(data, pos);
430 Ok((first, count))
431}
432
433fn parse_xref_entry_at(data: &[u8], pos: &mut usize) -> Result<(u64, u16, bool)> {
438 let start = *pos as u64;
439 let offset = read_decimal(data, pos).ok_or(Error::InvalidXref(start))?;
440 skip_whitespace(data, pos);
441 let gen = read_decimal(data, pos)
442 .and_then(|g| u16::try_from(g).ok())
443 .ok_or(Error::InvalidXref(start))?;
444 skip_whitespace(data, pos);
445 let in_use = match data.get(*pos) {
446 Some(b'n') => true,
447 Some(b'f') => false,
448 _ => return Err(Error::InvalidXref(start)),
449 };
450 *pos += 1;
451 Ok((offset, gen, in_use))
452}
453
454fn read_decimal(data: &[u8], pos: &mut usize) -> Option<u64> {
457 let start = *pos;
458 while *pos < data.len() && data[*pos].is_ascii_digit() {
459 *pos += 1;
460 }
461 if *pos == start {
462 return None;
463 }
464 std::str::from_utf8(&data[start..*pos]).ok()?.parse().ok()
465}
466
467fn skip_whitespace(data: &[u8], pos: &mut usize) {
468 while *pos < data.len() && matches!(data[*pos], b' ' | b'\t' | b'\r' | b'\n') {
469 *pos += 1;
470 }
471}
472
473fn skip_eol(data: &[u8], pos: &mut usize) {
474 while *pos < data.len() && matches!(data[*pos], b' ' | b'\t' | b'\r' | b'\n') {
475 *pos += 1;
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn find_startxref_offset() {
485 let data = b"%PDF-1.4\n...lots of content...\nstartxref\n1234\n%%EOF";
486 let offset = find_startxref(data).unwrap();
487 assert_eq!(offset, 1234);
488 }
489
490 #[test]
491 fn parse_xref_entry_in_use() {
492 let mut pos = 0usize;
493 let (offset, gen, in_use) =
494 parse_xref_entry_at(b"0000000010 00000 n \r\n", &mut pos).unwrap();
495 assert_eq!(offset, 10);
496 assert_eq!(gen, 0);
497 assert!(in_use);
498 assert_eq!(pos, 18, "cursor stops just past the type letter");
499 }
500
501 #[test]
502 fn parse_xref_entry_free() {
503 let mut pos = 0usize;
504 let (offset, gen, in_use) =
505 parse_xref_entry_at(b"0000000000 65535 f \r\n", &mut pos).unwrap();
506 assert_eq!(offset, 0);
507 assert_eq!(gen, 65535);
508 assert!(!in_use);
509 }
510
511 #[test]
512 fn parse_xref_entry_truncated_errors() {
513 let mut pos = 0usize;
514 assert!(parse_xref_entry_at(b"0000000010 000", &mut pos).is_err());
515 }
516
517 #[test]
518 fn traditional_xref_with_19_byte_entries() {
519 let mut d = Vec::new();
521 d.extend_from_slice(b"%PDF-1.4\n");
522 let off1 = d.len();
523 d.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
524 let off2 = d.len();
525 d.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
526 let xref_off = d.len();
527 d.extend_from_slice(b"xref\n0 3\n");
528 d.extend_from_slice(b"0000000000 65535 f\n"); d.extend_from_slice(format!("{off1:010} 00000 n\n").as_bytes()); d.extend_from_slice(format!("{off2:010} 00000 n\n").as_bytes()); d.extend_from_slice(
532 format!("trailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{xref_off}\n%%EOF\n")
533 .as_bytes(),
534 );
535
536 let (table, trailer) = parse_xref_and_trailer(&d, &ParseLimits::default()).unwrap();
537 assert_eq!(trailer.get_ref("Root").unwrap(), ObjectId(1, 0));
538 match table.get(ObjectId(1, 0)).unwrap() {
539 XrefEntry::InUse { offset, .. } => assert_eq!(*offset as usize, off1),
540 other => panic!("expected InUse, got {other:?}"),
541 }
542 match table.get(ObjectId(2, 0)).unwrap() {
543 XrefEntry::InUse { offset, .. } => assert_eq!(*offset as usize, off2),
544 other => panic!("expected InUse, got {other:?}"),
545 }
546 }
547
548 fn xref_stream_bytes(w: &str, size: u32, index: &str, body: &[u8]) -> Vec<u8> {
551 let mut d = format!(
552 "9 0 obj\n<< /Type /XRef /Size {size} /W {w} {index} /Length {} >>\nstream\n",
553 body.len()
554 )
555 .into_bytes();
556 d.extend_from_slice(body);
557 d.extend_from_slice(b"\nendstream\nendobj\n");
558 d
559 }
560
561 #[test]
562 fn xref_stream_rejects_negative_w_width() {
563 let d = xref_stream_bytes("[1 -2 2]", 1, "", &[]);
564 let mut table = XrefTable::new();
565 assert!(parse_xref_stream(&d, 0, &mut table, &ParseLimits::default()).is_err());
566 }
567
568 #[test]
569 fn xref_stream_rejects_oversized_w_width() {
570 let d = xref_stream_bytes("[9 4 2]", 1, "", &[]);
571 let mut table = XrefTable::new();
572 assert!(parse_xref_stream(&d, 0, &mut table, &ParseLimits::default()).is_err());
573 }
574
575 #[test]
576 fn xref_stream_rejects_zero_entry_size() {
577 let d = xref_stream_bytes("[0 0 0]", 1, "", &[]);
578 let mut table = XrefTable::new();
579 assert!(parse_xref_stream(&d, 0, &mut table, &ParseLimits::default()).is_err());
580 }
581
582 #[test]
583 fn hybrid_xrefstm_is_parsed_with_correct_precedence() {
584 let mut d = Vec::new();
589 d.extend_from_slice(b"%PDF-1.4\n");
590 let off1 = d.len();
591 d.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
592 let off4_table = d.len();
593 d.extend_from_slice(b"4 0 obj\n<< /Marker /FromTable >>\nendobj\n");
594 let off4_stm = d.len();
595 d.extend_from_slice(b"4 0 obj\n<< /Marker /FromStm >>\nendobj\n");
596 let off5 = d.len();
597 d.extend_from_slice(b"5 0 obj\n<< /Marker /StmOnly >>\nendobj\n");
598
599 let mut body = Vec::new();
601 for (off, gen) in [(off4_stm as u32, 0u16), (off5 as u32, 0)] {
602 body.push(1u8); body.extend_from_slice(&off.to_be_bytes());
604 body.extend_from_slice(&gen.to_be_bytes());
605 }
606 let off6 = d.len();
607 d.extend_from_slice(
608 format!(
609 "6 0 obj\n<< /Type /XRef /Size 7 /W [1 4 2] /Index [4 2] /Length {} >>\nstream\n",
610 body.len()
611 )
612 .as_bytes(),
613 );
614 d.extend_from_slice(&body);
615 d.extend_from_slice(b"\nendstream\nendobj\n");
616
617 let xref_off = d.len();
618 d.extend_from_slice(b"xref\n0 2\n0000000000 65535 f \n");
619 d.extend_from_slice(format!("{off1:010} 00000 n \n").as_bytes());
620 d.extend_from_slice(b"4 1\n");
621 d.extend_from_slice(format!("{off4_table:010} 00000 n \n").as_bytes());
622 d.extend_from_slice(
623 format!(
624 "trailer\n<< /Size 7 /Root 1 0 R /XRefStm {off6} >>\nstartxref\n{xref_off}\n%%EOF\n"
625 )
626 .as_bytes(),
627 );
628
629 let (table, trailer) = parse_xref_and_trailer(&d, &ParseLimits::default()).unwrap();
630 assert_eq!(trailer.get_ref("Root").unwrap(), ObjectId(1, 0));
631 match table.get(ObjectId(5, 0)).unwrap() {
633 XrefEntry::InUse { offset, .. } => assert_eq!(*offset as usize, off5),
634 other => panic!("expected InUse from XRefStm, got {other:?}"),
635 }
636 match table.get(ObjectId(4, 0)).unwrap() {
638 XrefEntry::InUse { offset, .. } => assert_eq!(*offset as usize, off4_table),
639 other => panic!("expected InUse, got {other:?}"),
640 }
641
642 let file = crate::PdfFile::parse(d).unwrap();
644 let o5 = file.resolve(ObjectId(5, 0)).unwrap();
645 assert_eq!(o5.as_dict().unwrap().get_name("Marker").unwrap(), "StmOnly");
646 let o4 = file.resolve(ObjectId(4, 0)).unwrap();
647 assert_eq!(
648 o4.as_dict().unwrap().get_name("Marker").unwrap(),
649 "FromTable"
650 );
651 }
652}