1use std::collections::HashMap;
37
38use crate::error::PdfError;
39use crate::objects::{Dict, Object, ObjectId};
40use crate::reader::lex::{Lexer, TokenKind};
41use crate::reader::parse::Parser;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum XrefEntry {
48 Free { next: u32, generation: u16 },
53 InUse { offset: u64, generation: u16 },
56 Compressed {
62 obj_stream_id: u32,
63 index_within_stream: u32,
64 },
65}
66
67#[derive(Debug, Clone, Default)]
69pub struct XrefTable {
70 pub entries: HashMap<u32, XrefEntry>,
73 pub trailer: Dict,
76}
77
78impl XrefTable {
79 pub fn offset_of(&self, id: ObjectId) -> Option<u64> {
83 match self.entries.get(&id.number)? {
84 XrefEntry::InUse { offset, generation } if *generation == id.generation => {
85 Some(*offset)
86 }
87 _ => None,
88 }
89 }
90
91 pub fn root(&self) -> Result<ObjectId, PdfError> {
95 match self
96 .trailer
97 .entries()
98 .iter()
99 .find(|(k, _)| k == "Root")
100 .map(|(_, v)| v)
101 {
102 Some(Object::Reference(id)) => Ok(*id),
103 Some(other) => Err(PdfError::other(format!(
104 "PDF reader: trailer /Root must be an indirect reference (got {other:?})"
105 ))),
106 None => Err(PdfError::other(
107 "PDF reader: trailer is missing the required /Root entry",
108 )),
109 }
110 }
111
112 pub fn info(&self) -> Option<ObjectId> {
115 self.trailer
116 .entries()
117 .iter()
118 .find(|(k, _)| k == "Info")
119 .and_then(|(_, v)| match v {
120 Object::Reference(id) => Some(*id),
121 _ => None,
122 })
123 }
124}
125
126pub fn find_startxref_offset(input: &[u8]) -> Result<u64, PdfError> {
131 if !input.contains(&b'%') {
132 return Err(PdfError::other(
133 "PDF reader: input has no `%` byte — does not look like a PDF",
134 ));
135 }
136 let scan_start = input.len().saturating_sub(4096);
137 let tail = &input[scan_start..];
138 let needle = b"startxref";
139 let local_pos = (0..tail.len().saturating_sub(needle.len()))
140 .rev()
141 .find(|&i| &tail[i..i + needle.len()] == needle)
142 .ok_or_else(|| {
143 PdfError::other(
144 "PDF reader: no `startxref` keyword in last 4096 bytes — file truncated?",
145 )
146 })?;
147 let mut p = Parser::new(&input[scan_start + local_pos + needle.len()..]);
149 let obj = p.parse_object()?.ok_or_else(|| {
150 PdfError::other("PDF reader: `startxref` keyword has no offset following it")
151 })?;
152 let Object::Integer(n) = obj else {
153 return Err(PdfError::other(format!(
154 "PDF reader: `startxref` offset must be an integer (got {obj:?})"
155 )));
156 };
157 if n < 0 {
158 return Err(PdfError::other(format!(
159 "PDF reader: `startxref` offset is negative ({n})"
160 )));
161 }
162 Ok(n as u64)
163}
164
165pub fn parse_xref_at(input: &[u8], xref_offset: u64) -> Result<XrefTable, PdfError> {
169 let xref_pos = xref_offset as usize;
170 if xref_pos >= input.len() {
171 return Err(PdfError::other(format!(
172 "PDF reader: startxref offset {xref_offset} past end of file ({} bytes)",
173 input.len()
174 )));
175 }
176
177 let mut lex = Lexer::new(input);
178 lex.seek(xref_pos);
179
180 let kw = lex
183 .next_token()?
184 .ok_or_else(|| PdfError::other("PDF reader: empty xref table"))?;
185 if let TokenKind::Integer(_) = kw.kind {
186 return parse_xref_stream_at(input, xref_pos);
189 }
190 let TokenKind::Keyword(b"xref") = kw.kind else {
191 return Err(PdfError::other(format!(
192 "PDF reader: expected `xref` keyword or XRef stream object at offset {xref_offset} (got {:?})",
193 kw.kind
194 )));
195 };
196
197 let mut entries: HashMap<u32, XrefEntry> = HashMap::new();
198 loop {
199 let next_tok = lex
202 .next_token()?
203 .ok_or_else(|| PdfError::other("PDF reader: truncated xref table"))?;
204 let first = match next_tok.kind {
205 TokenKind::Integer(n) => n,
206 TokenKind::Keyword(b"trailer") => break,
207 other => {
208 return Err(PdfError::other(format!(
209 "PDF reader: expected xref subsection header or `trailer` (got {other:?}) at byte {}",
210 next_tok.start
211 )));
212 }
213 };
214 let count_tok = lex
215 .next_token()?
216 .ok_or_else(|| PdfError::other("PDF reader: xref subsection has no count"))?;
217 let TokenKind::Integer(count) = count_tok.kind else {
218 return Err(PdfError::other(format!(
219 "PDF reader: xref subsection count must be an integer at byte {} (got {:?})",
220 count_tok.start, count_tok.kind
221 )));
222 };
223 if first < 0 || count < 0 {
224 return Err(PdfError::other(format!(
225 "PDF reader: negative xref subsection header `{first} {count}`"
226 )));
227 }
228 skip_whitespace(input, &mut lex);
234 for i in 0..count {
235 let off = lex.position();
236 if off + 20 > input.len() {
237 return Err(PdfError::other(format!(
238 "PDF reader: xref entry {first}+{i} truncated at byte {off}"
239 )));
240 }
241 let entry = &input[off..off + 20];
242 let parsed = parse_xref_entry(entry, off)?;
243 entries.insert(first as u32 + i as u32, parsed);
244 lex.seek(off + 20);
245 }
246 }
247
248 let mut p = Parser::from_lexer(lex);
250 let dict_obj = p
251 .parse_object()?
252 .ok_or_else(|| PdfError::other("PDF reader: trailer dict missing"))?;
253 let Object::Dict(trailer) = dict_obj else {
254 return Err(PdfError::other(format!(
255 "PDF reader: trailer dict must be a dictionary (got {dict_obj:?})"
256 )));
257 };
258
259 Ok(XrefTable { entries, trailer })
260}
261
262pub fn parse_xref(input: &[u8]) -> Result<XrefTable, PdfError> {
271 let mut current_off = find_startxref_offset(input)?;
272 let mut newest = parse_xref_at(input, current_off)?;
273 let mut visited: std::collections::HashSet<u64> = std::collections::HashSet::new();
277 visited.insert(current_off);
278 let xrefstm_visited = &mut visited.clone();
290 merge_xrefstm_if_present(
291 input,
292 &newest.trailer.clone(),
293 &mut newest.entries,
294 xrefstm_visited,
295 )?;
296 loop {
297 let prev_off = newest
298 .trailer
299 .entries()
300 .iter()
301 .find(|(k, _)| k == "Prev")
302 .and_then(|(_, v)| match v {
303 Object::Integer(n) if *n >= 0 => Some(*n as u64),
304 _ => None,
305 });
306 let Some(po) = prev_off else { break };
307 if !visited.insert(po) {
308 return Err(PdfError::other(
310 "PDF reader: /Prev xref-section chain has a cycle",
311 ));
312 }
313 if visited.len() > 32 {
314 return Err(PdfError::other(
315 "PDF reader: /Prev xref-section chain exceeds 32 hops — refusing",
316 ));
317 }
318 let older = parse_xref_at(input, po)?;
319 for (id, entry) in older.entries {
323 newest.entries.entry(id).or_insert(entry);
324 }
325 merge_xrefstm_if_present(input, &older.trailer, &mut newest.entries, xrefstm_visited)?;
330 let mut next_trailer = older.trailer.clone();
334 next_trailer.set("Prev", Object::Null);
339 let older_prev = older
342 .trailer
343 .entries()
344 .iter()
345 .find(|(k, _)| k == "Prev")
346 .and_then(|(_, v)| match v {
347 Object::Integer(n) if *n >= 0 => Some(*n as u64),
348 _ => None,
349 });
350 let mut new_trailer = Dict::new();
353 for (k, v) in newest.trailer.entries() {
354 if k != "Prev" {
355 new_trailer.set(k, v.clone());
356 }
357 }
358 if let Some(op) = older_prev {
359 new_trailer.set("Prev", Object::Integer(op as i64));
360 }
361 newest.trailer = new_trailer;
362 current_off = po;
363 }
364 let _ = current_off;
365 Ok(newest)
366}
367
368fn merge_xrefstm_if_present(
383 input: &[u8],
384 trailer: &Dict,
385 into: &mut HashMap<u32, XrefEntry>,
386 visited: &mut std::collections::HashSet<u64>,
387) -> Result<(), PdfError> {
388 let xrefstm_off = trailer
389 .entries()
390 .iter()
391 .find(|(k, _)| k == "XRefStm")
392 .and_then(|(_, v)| match v {
393 Object::Integer(n) if *n >= 0 => Some(*n as u64),
394 _ => None,
395 });
396 let Some(off) = xrefstm_off else {
397 return Ok(());
398 };
399 if !visited.insert(off) {
403 return Err(PdfError::other(
404 "PDF reader: /XRefStm offset already visited (cycle in hybrid-reference chain)",
405 ));
406 }
407 if visited.len() > 32 {
408 return Err(PdfError::other(
409 "PDF reader: /XRefStm chain exceeds 32 hops — refusing",
410 ));
411 }
412 if off as usize >= input.len() {
413 return Err(PdfError::other(format!(
414 "PDF reader: /XRefStm offset {off} past end of file ({} bytes)",
415 input.len()
416 )));
417 }
418 let supp = parse_xref_stream_at(input, off as usize)?;
419 for (id, entry) in supp.entries {
420 into.entry(id).or_insert(entry);
421 }
422 Ok(())
423}
424
425fn skip_whitespace(input: &[u8], lex: &mut Lexer<'_>) {
426 let mut p = lex.position();
427 while p < input.len()
428 && (input[p] == b' ' || input[p] == b'\t' || input[p] == b'\r' || input[p] == b'\n')
429 {
430 p += 1;
431 }
432 lex.seek(p);
433}
434
435fn parse_xref_entry(bytes: &[u8], at: usize) -> Result<XrefEntry, PdfError> {
436 debug_assert_eq!(bytes.len(), 20);
437 if bytes[10] != b' ' || bytes[16] != b' ' {
439 return Err(PdfError::other(format!(
440 "PDF reader: malformed xref entry at byte {at} (missing space separators)"
441 )));
442 }
443 let off_str = std::str::from_utf8(&bytes[..10])
444 .map_err(|_| PdfError::other(format!("PDF reader: non-ASCII xref offset at byte {at}")))?;
445 let off: u64 = off_str.trim().parse().map_err(|_| {
446 PdfError::other(format!(
447 "PDF reader: invalid xref offset `{off_str}` at byte {at}"
448 ))
449 })?;
450 let gen_str = std::str::from_utf8(&bytes[11..16]).map_err(|_| {
451 PdfError::other(format!(
452 "PDF reader: non-ASCII xref generation at byte {at}"
453 ))
454 })?;
455 let generation: u16 = gen_str.trim().parse().map_err(|_| {
456 PdfError::other(format!(
457 "PDF reader: invalid xref generation `{gen_str}` at byte {at}"
458 ))
459 })?;
460 let kind = bytes[17];
461 match kind {
462 b'n' => Ok(XrefEntry::InUse {
463 offset: off,
464 generation,
465 }),
466 b'f' => Ok(XrefEntry::Free {
467 next: off as u32,
468 generation,
469 }),
470 other => Err(PdfError::other(format!(
471 "PDF reader: xref entry kind must be `n` or `f` at byte {at} (got `{}`)",
472 other as char
473 ))),
474 }
475}
476
477fn parse_xref_stream_at(input: &[u8], xref_pos: usize) -> Result<XrefTable, PdfError> {
484 let mut p = Parser::new(input);
485 p.lexer_mut().seek(xref_pos);
486 let (_obj_id, body) = p.parse_indirect()?;
487 let stream = match body {
488 Object::Stream(s) => s,
489 other => {
490 return Err(PdfError::other(format!(
491 "PDF reader: XRef stream object must be a Stream (got {other:?})"
492 )));
493 }
494 };
495
496 let dict = &stream.dict;
505 let lookup = |k: &str| {
506 dict.entries()
507 .iter()
508 .find(|(kk, _)| kk == k)
509 .map(|(_, v)| v.clone())
510 };
511
512 if !matches!(lookup("Type"), Some(Object::Name(ref n)) if n == "XRef") {
513 return Err(PdfError::other(
514 "PDF reader: XRef stream object missing /Type /XRef",
515 ));
516 }
517 let size = match lookup("Size") {
518 Some(Object::Integer(n)) if n >= 0 => n as u32,
519 _ => return Err(PdfError::other("PDF reader: XRef stream missing /Size")),
520 };
521 let w = match lookup("W") {
522 Some(Object::Array(items)) if items.len() == 3 => {
523 let mut out = [0usize; 3];
524 for (i, it) in items.iter().enumerate() {
525 let Object::Integer(v) = it else {
526 return Err(PdfError::other(format!(
527 "PDF reader: XRef /W[{i}] must be an integer (got {it:?})"
528 )));
529 };
530 if *v < 0 || *v > 8 {
531 return Err(PdfError::other(format!(
532 "PDF reader: XRef /W[{i}] = {v} out of range [0..=8]"
533 )));
534 }
535 out[i] = *v as usize;
536 }
537 out
538 }
539 other => {
540 return Err(PdfError::other(format!(
541 "PDF reader: XRef stream /W must be a 3-array (got {other:?})"
542 )));
543 }
544 };
545 let index: Vec<(u32, u32)> = match lookup("Index") {
546 Some(Object::Array(items)) => {
547 if items.len() % 2 != 0 {
548 return Err(PdfError::other(
549 "PDF reader: XRef /Index array length must be even",
550 ));
551 }
552 items
553 .chunks_exact(2)
554 .map(|chunk| {
555 let (Object::Integer(s), Object::Integer(c)) = (&chunk[0], &chunk[1]) else {
556 return Err(PdfError::other(
557 "PDF reader: XRef /Index entries must be integers",
558 ));
559 };
560 if *s < 0 || *c < 0 {
561 return Err(PdfError::other(
562 "PDF reader: XRef /Index entries must be non-negative",
563 ));
564 }
565 Ok((*s as u32, *c as u32))
566 })
567 .collect::<Result<_, _>>()?
568 }
569 Some(other) => {
570 return Err(PdfError::other(format!(
571 "PDF reader: XRef /Index must be an array (got {other:?})"
572 )));
573 }
574 None => vec![(0, size)],
575 };
576
577 let raw = decode_xref_stream_body(&stream)?;
579
580 let table_bytes = apply_predictor(&raw, dict, w[0] + w[1] + w[2])?;
582
583 let entry_size = w[0] + w[1] + w[2];
585 if entry_size == 0 {
586 return Err(PdfError::other(
587 "PDF reader: XRef stream /W = [0 0 0] is degenerate",
588 ));
589 }
590 let mut entries: HashMap<u32, XrefEntry> = HashMap::new();
591 let mut cursor = 0usize;
592 for (start, count) in &index {
593 for offset_in_section in 0..*count {
594 if cursor + entry_size > table_bytes.len() {
595 return Err(PdfError::other(format!(
596 "PDF reader: XRef stream truncated at entry {start}+{offset_in_section} \
597 (cursor {cursor}, need {entry_size}, have {})",
598 table_bytes.len()
599 )));
600 }
601 let chunk = &table_bytes[cursor..cursor + entry_size];
602 cursor += entry_size;
603 let (f1, f2, f3) = split_fields(chunk, w[0], w[1], w[2]);
604 let kind = if w[0] == 0 { 1 } else { f1 };
608 let id = start + offset_in_section;
609 let entry = match kind {
610 0 => XrefEntry::Free {
611 next: f2 as u32,
613 generation: f3 as u16,
614 },
615 1 => XrefEntry::InUse {
616 offset: f2,
617 generation: f3 as u16,
621 },
622 2 => XrefEntry::Compressed {
623 obj_stream_id: f2 as u32,
624 index_within_stream: f3 as u32,
625 },
626 _ => {
627 XrefEntry::Free {
636 next: 0,
637 generation: 65535,
638 }
639 }
640 };
641 entries.insert(id, entry);
642 }
643 }
644
645 let trailer = filter_trailer_dict(dict);
649
650 Ok(XrefTable { entries, trailer })
651}
652
653fn decode_xref_stream_body(stream: &crate::objects::Stream) -> Result<Vec<u8>, PdfError> {
655 let filter = stream
656 .dict
657 .entries()
658 .iter()
659 .find(|(k, _)| k == "Filter")
660 .map(|(_, v)| v.clone());
661 match filter {
662 None => Ok(stream.data.clone()),
663 Some(Object::Name(n)) if n == "FlateDecode" => crate::zlib::flate_decompress(&stream.data)
664 .map_err(|e| {
665 PdfError::other(format!("PDF reader: XRef stream FlateDecode failed: {e}"))
666 }),
667 Some(Object::Array(items)) => {
668 let mut data = stream.data.clone();
670 for it in items {
671 let Object::Name(n) = it else {
672 return Err(PdfError::other(
673 "PDF reader: XRef stream /Filter chain item must be a Name",
674 ));
675 };
676 if n != "FlateDecode" {
677 return Err(PdfError::other(format!(
678 "PDF reader: XRef stream filter `{n}` not supported"
679 )));
680 }
681 data = crate::zlib::flate_decompress(&data).map_err(|e| {
682 PdfError::other(format!("PDF reader: XRef stream FlateDecode failed: {e}"))
683 })?;
684 }
685 Ok(data)
686 }
687 Some(Object::Name(n)) => Err(PdfError::other(format!(
688 "PDF reader: XRef stream filter `{n}` not supported"
689 ))),
690 Some(other) => Err(PdfError::other(format!(
691 "PDF reader: XRef stream /Filter must be a Name or array (got {other:?})"
692 ))),
693 }
694}
695
696fn apply_predictor(raw: &[u8], dict: &Dict, entry_width: usize) -> Result<Vec<u8>, PdfError> {
708 let parms = dict.entries().iter().find(|(k, _)| k == "DecodeParms");
709 let Some((_, parms_obj)) = parms else {
710 return Ok(raw.to_vec());
712 };
713 let Object::Dict(parms_dict) = parms_obj else {
714 return Err(PdfError::other("PDF reader: /DecodeParms must be a dict"));
715 };
716 let predictor = parms_dict
717 .entries()
718 .iter()
719 .find(|(k, _)| k == "Predictor")
720 .map(|(_, v)| v.clone());
721 let columns = parms_dict
722 .entries()
723 .iter()
724 .find(|(k, _)| k == "Columns")
725 .map(|(_, v)| v.clone());
726 let p = match predictor {
727 Some(Object::Integer(n)) => n,
728 None => 1,
729 other => {
730 return Err(PdfError::other(format!(
731 "PDF reader: /Predictor must be an integer (got {other:?})"
732 )));
733 }
734 };
735 if p == 1 {
736 return Ok(raw.to_vec());
737 }
738 let columns = match columns {
739 Some(Object::Integer(n)) if n > 0 => n as usize,
740 Some(other) => {
741 return Err(PdfError::other(format!(
742 "PDF reader: /Columns must be a positive integer (got {other:?})"
743 )));
744 }
745 None => entry_width,
748 };
749 if !(10..=15).contains(&p) {
750 return Err(PdfError::other(format!(
751 "PDF reader: /Predictor {p} not yet supported (only PNG predictors 10..=15)"
752 )));
753 }
754 let row_size = columns + 1;
756 if raw.len() % row_size != 0 {
757 return Err(PdfError::other(format!(
758 "PDF reader: predictor row size {row_size} doesn't divide raw len {}",
759 raw.len()
760 )));
761 }
762 let row_count = raw.len() / row_size;
763 let mut out = Vec::with_capacity(row_count * columns);
764 let mut prev_row = vec![0u8; columns];
765 for row_idx in 0..row_count {
766 let row = &raw[row_idx * row_size..(row_idx + 1) * row_size];
767 let tag = row[0];
768 let data = &row[1..];
769 let mut decoded_row = vec![0u8; columns];
770 match tag {
771 0 => {
772 decoded_row.copy_from_slice(data);
774 }
775 1 => {
776 for i in 0..columns {
778 let left = if i == 0 { 0 } else { decoded_row[i - 1] };
779 decoded_row[i] = data[i].wrapping_add(left);
780 }
781 }
782 2 => {
783 for i in 0..columns {
785 decoded_row[i] = data[i].wrapping_add(prev_row[i]);
786 }
787 }
788 3 => {
789 for i in 0..columns {
791 let left = if i == 0 {
792 0u16
793 } else {
794 decoded_row[i - 1] as u16
795 };
796 let up = prev_row[i] as u16;
797 decoded_row[i] = data[i].wrapping_add(((left + up) / 2) as u8);
798 }
799 }
800 4 => {
801 for i in 0..columns {
803 let left = if i == 0 {
804 0i16
805 } else {
806 decoded_row[i - 1] as i16
807 };
808 let up = prev_row[i] as i16;
809 let upper_left = if i == 0 { 0i16 } else { prev_row[i - 1] as i16 };
810 let p_pred = paeth_predictor(left, up, upper_left);
811 decoded_row[i] = data[i].wrapping_add(p_pred);
812 }
813 }
814 other => {
815 return Err(PdfError::other(format!(
816 "PDF reader: PNG predictor row tag {other} unknown"
817 )));
818 }
819 }
820 out.extend_from_slice(&decoded_row);
821 prev_row = decoded_row;
822 }
823 Ok(out)
824}
825
826fn paeth_predictor(a: i16, b: i16, c: i16) -> u8 {
827 let p = a + b - c;
828 let pa = (p - a).abs();
829 let pb = (p - b).abs();
830 let pc = (p - c).abs();
831 let r = if pa <= pb && pa <= pc {
832 a
833 } else if pb <= pc {
834 b
835 } else {
836 c
837 };
838 r as u8
839}
840
841fn split_fields(chunk: &[u8], w1: usize, w2: usize, w3: usize) -> (u64, u64, u64) {
845 fn read_be(s: &[u8]) -> u64 {
846 let mut v: u64 = 0;
847 for &b in s {
848 v = (v << 8) | (b as u64);
849 }
850 v
851 }
852 let f1 = read_be(&chunk[..w1]);
853 let f2 = read_be(&chunk[w1..w1 + w2]);
854 let f3 = read_be(&chunk[w1 + w2..w1 + w2 + w3]);
855 (f1, f2, f3)
856}
857
858fn filter_trailer_dict(dict: &Dict) -> Dict {
862 let stream_only = [
863 "Type",
864 "Filter",
865 "DecodeParms",
866 "Length",
867 "F",
868 "FFilter",
869 "FDecodeParms",
870 "DL",
871 "W",
872 "Index",
873 ];
874 let mut out = Dict::new();
875 for (k, v) in dict.entries() {
876 if !stream_only.contains(&k.as_str()) {
877 out.set(k, v.clone());
878 }
879 }
880 out
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
886 use crate::writer::write_pdf;
887 use oxideav_core::time::TimeBase;
888 use oxideav_core::vector::{
889 FillRule, Group, Node, Paint, Path, PathCommand, PathNode, Point, Rgba, VectorFrame,
890 };
891
892 fn sample_pdf_bytes() -> Vec<u8> {
893 let mut p = Path::new();
894 p.commands.push(PathCommand::MoveTo(Point::new(10.0, 10.0)));
895 p.commands.push(PathCommand::LineTo(Point::new(90.0, 10.0)));
896 p.commands.push(PathCommand::LineTo(Point::new(90.0, 90.0)));
897 p.commands.push(PathCommand::Close);
898 let frame = VectorFrame {
899 width: 100.0,
900 height: 100.0,
901 view_box: None,
902 root: Group {
903 children: vec![Node::Path(PathNode {
904 path: p,
905 fill: Some(Paint::Solid(Rgba::opaque(0, 128, 255))),
906 stroke: None,
907 fill_rule: FillRule::NonZero,
908 })],
909 ..Group::default()
910 },
911 pts: None,
912 time_base: TimeBase::new(1, 1),
913 };
914 write_pdf(&frame).expect("write_pdf")
915 }
916
917 #[test]
918 fn finds_startxref_in_writer_output() {
919 let pdf = sample_pdf_bytes();
920 let off = find_startxref_offset(&pdf).expect("startxref");
921 assert!(off > 0);
922 assert_eq!(&pdf[off as usize..off as usize + 4], b"xref");
924 }
925
926 #[test]
927 fn parses_xref_table_for_writer_output() {
928 let pdf = sample_pdf_bytes();
929 let table = parse_xref(&pdf).expect("parse_xref");
930 assert!(table.entries.len() >= 5);
934 assert!(matches!(
936 table.entries.get(&0),
937 Some(XrefEntry::Free {
938 generation: 65535,
939 ..
940 })
941 ));
942 for i in 1..=5 {
944 assert!(
945 matches!(table.entries.get(&i), Some(XrefEntry::InUse { .. })),
946 "entry {i} should be InUse"
947 );
948 }
949 let root = table.root().expect("trailer /Root");
951 assert_eq!(root.number, 1);
952 }
953
954 #[test]
955 fn xref_offset_lookup_round_trips() {
956 let pdf = sample_pdf_bytes();
957 let table = parse_xref(&pdf).expect("parse_xref");
958 for (id_num, entry) in &table.entries {
961 if let XrefEntry::InUse { offset, generation } = entry {
962 let pos = *offset as usize;
963 assert!(pos < pdf.len(), "offset out of range for id {id_num}");
964 let expected = format!("{} {} obj", id_num, generation);
965 let slice = &pdf[pos..(pos + expected.len()).min(pdf.len())];
966 assert_eq!(
967 slice,
968 expected.as_bytes(),
969 "object {id_num} {generation} obj should be at offset {offset}"
970 );
971 }
972 }
973 }
974
975 #[test]
976 fn root_required_for_well_formed_pdf() {
977 let pdf = sample_pdf_bytes();
978 let table = parse_xref(&pdf).expect("parse_xref");
979 let _ = table.root().expect("/Root must resolve");
980 }
981
982 #[test]
983 fn info_optional() {
984 let pdf = sample_pdf_bytes();
987 let table = parse_xref(&pdf).expect("parse_xref");
988 assert!(table.info().is_none());
989 }
990
991 #[test]
992 fn rejects_truncated_input() {
993 let pdf = b"not even a pdf";
994 let r = parse_xref(pdf);
995 assert!(r.is_err());
996 }
997
998 #[test]
999 fn rejects_startxref_off_end_of_file() {
1000 let mut pdf = sample_pdf_bytes();
1004 let needle = b"startxref";
1005 let pos = pdf
1006 .windows(needle.len())
1007 .rposition(|w| w == needle)
1008 .expect("startxref present");
1009 pdf.truncate(pos);
1011 pdf.extend_from_slice(b"startxref\n999999999\n%%EOF\n");
1012 let r = parse_xref(&pdf);
1013 assert!(r.is_err());
1014 }
1015}