1use crate::error::{Error, Result};
25use crate::limits::Limits;
26
27use super::cos::{
28 LengthValue, body_as_u64, dict_has_length, dict_int_or_ref, dict_length, dict_name_value,
29};
30use super::lexer::lex;
31use super::span::{Span, SpanKind};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum PhysicalKind {
36 Header,
38 Comment,
40 Whitespace,
42 ObjHeader,
44 ObjBody,
46 EndObj,
48 StreamData,
50 XrefSection,
52 Trailer,
54 StartXref,
56 Eof,
58 Unclassified,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct PhysicalSpan {
65 pub start: u64,
67 pub len: u64,
69 pub kind: PhysicalKind,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ObjRole {
81 Generic,
83 XRefStream,
86 ObjectStream,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct PdfObjectSpan {
93 pub number: u64,
95 pub generation: u64,
97 pub start: u64,
99 pub end: u64,
101 pub role: ObjRole,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum LengthSource {
108 Direct,
110 Indirect,
112 Fallback,
114 Missing,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct PdfStreamSpan {
121 pub object: u64,
123 pub generation: u64,
125 pub data_start: u64,
127 pub data_len: u64,
129 pub length_source: LengthSource,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub struct RevisionInfo {
136 pub index: u32,
138 pub start: u64,
140 pub end: u64,
142 pub startxref: Option<u64>,
144 pub prev: Option<u64>,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Default)]
151pub struct PdfPhysical {
152 pub spans: Vec<PhysicalSpan>,
154 pub objects: Vec<PdfObjectSpan>,
156 pub streams: Vec<PdfStreamSpan>,
158 pub revisions: Vec<RevisionInfo>,
160 pub startxref: Vec<u64>,
162 pub eofs: Vec<u64>,
164 pub header: Option<(u64, u64)>,
166}
167
168impl PdfPhysical {
169 pub fn total_len(&self) -> u64 {
171 self.spans
172 .iter()
173 .fold(0u64, |acc, s| acc.saturating_add(s.len))
174 }
175
176 pub fn validate(&self, declared_len: u64) -> Result<()> {
182 let mut cursor: u64 = 0;
183 for (i, span) in self.spans.iter().enumerate() {
184 if span.len == 0 {
185 return Err(Error::invalid_pdf_structure(format!(
186 "physical span {i} has zero length at offset {}",
187 span.start
188 )));
189 }
190 if span.start != cursor {
191 let why = if span.start < cursor {
192 "overlap"
193 } else {
194 "gap"
195 };
196 return Err(Error::coverage_violation(format!(
197 "physical span {i} {why}: expected start {cursor}, found {}",
198 span.start
199 )));
200 }
201 cursor = cursor.checked_add(span.len).ok_or_else(|| {
202 Error::coverage_violation("physical span lengths overflow the address space")
203 })?;
204 }
205 if cursor != declared_len {
206 return Err(Error::coverage_violation(format!(
207 "physical cover ends at {cursor}, declared length is {declared_len}"
208 )));
209 }
210 Ok(())
211 }
212}
213
214pub fn scan(input: &[u8], limits: Limits) -> Result<PdfPhysical> {
216 let declared_len = input.len() as u64;
217 let lexed = lex(input, limits)?;
218 let spans = lexed.spans.spans;
219
220 let obj_bodies = collect_bodies(input, &spans);
223 let mut state = ScanState::new(limits, obj_bodies);
224 let mut i = 0usize;
225 while i < spans.len() {
226 let sp = spans[i];
227 let bytes = bytes_of(input, sp);
228
229 if state.header.is_none()
231 && sp.start == 0
232 && sp.kind == SpanKind::Comment
233 && bytes.starts_with(b"%PDF-")
234 {
235 state.push(sp.start, sp.len, PhysicalKind::Header)?;
236 state.header = Some((sp.start, sp.len));
237 i += 1;
238 continue;
239 }
240
241 if sp.kind == SpanKind::Comment && bytes.starts_with(b"%%EOF") {
243 state.push(sp.start, sp.len, PhysicalKind::Eof)?;
244 state.eofs.push(sp.start);
245 i += 1;
246 continue;
247 }
248
249 if sp.kind == SpanKind::Regular {
251 if bytes == b"startxref" {
252 if let Some(next) = state.try_startxref(input, &spans, i)? {
253 i = next;
254 continue;
255 }
256 } else if bytes == b"xref" {
257 i = state.emit_xref(input, &spans, i)?;
258 continue;
259 } else if bytes == b"trailer" {
260 i = state.emit_trailer(&spans, i)?;
261 continue;
262 } else if let Some((number, generation)) = obj_header_at(input, &spans, i) {
263 i = state.emit_object(input, &spans, i, number, generation)?;
264 continue;
265 }
266 }
267
268 let kind = match sp.kind {
270 SpanKind::Comment => PhysicalKind::Comment,
271 SpanKind::Whitespace => PhysicalKind::Whitespace,
272 _ => PhysicalKind::Unclassified,
273 };
274 state.push(sp.start, sp.len, kind)?;
275 i += 1;
276 }
277
278 let physical = state.finish(input, &spans);
279 physical.validate(declared_len)?;
280 Ok(physical)
281}
282
283struct ScanState {
285 builder: Builder,
286 objects: Vec<PdfObjectSpan>,
287 streams: Vec<PdfStreamSpan>,
288 obj_bodies: Vec<ObjBody>,
289 startxref: Vec<(u64, u64)>,
290 eofs: Vec<u64>,
291 trailer_dicts: Vec<(u64, u64)>,
292 header: Option<(u64, u64)>,
293}
294
295impl ScanState {
296 fn new(limits: Limits, obj_bodies: Vec<ObjBody>) -> Self {
297 ScanState {
298 builder: Builder::new(limits),
299 objects: Vec::new(),
300 streams: Vec::new(),
301 obj_bodies,
302 startxref: Vec::new(),
303 eofs: Vec::new(),
304 trailer_dicts: Vec::new(),
305 header: None,
306 }
307 }
308
309 fn push(&mut self, start: u64, len: u64, kind: PhysicalKind) -> Result<()> {
310 self.builder.push(start, len, kind)
311 }
312
313 fn finish(self, input: &[u8], spans: &[Span]) -> PdfPhysical {
314 let revisions = build_revisions(
315 input,
316 spans,
317 &self.eofs,
318 &self.objects,
319 &self.startxref,
320 &self.trailer_dicts,
321 );
322 PdfPhysical {
323 spans: self.builder.spans,
324 objects: self.objects,
325 streams: self.streams,
326 revisions,
327 startxref: self.startxref.iter().map(|&(_, value)| value).collect(),
328 eofs: self.eofs,
329 header: self.header,
330 }
331 }
332
333 fn emit_object(
336 &mut self,
337 input: &[u8],
338 spans: &[Span],
339 i: usize,
340 number: u64,
341 generation: u64,
342 ) -> Result<usize> {
343 let obj_header_start = spans[i].start;
344 let obj_kw_end = spans[i + 4].start + spans[i + 4].len;
345 let role = leading_dict_role(input, spans, i + 5);
346 self.push(
347 obj_header_start,
348 obj_kw_end - obj_header_start,
349 PhysicalKind::ObjHeader,
350 )?;
351
352 let mut stream: Option<ResolvedStream> = None;
356 let mut endobj: Option<usize> = None;
357 let mut j = i + 5;
358 while j < spans.len() {
359 if regular_eq(input, spans[j], b"endobj") {
360 endobj = Some(j);
361 break;
362 }
363 if stream.is_none()
364 && regular_eq(input, spans[j], b"stream")
365 && let Some(rs) = resolve_stream(input, spans, j, &self.obj_bodies, i + 5)
366 {
367 j = rs.endstream + 1;
368 stream = Some(rs);
369 continue;
370 }
371 j += 1;
372 }
373
374 match endobj {
375 Some(m) => {
376 if let Some(rs) = stream {
377 if rs.data_start > obj_kw_end {
378 self.push(
379 obj_kw_end,
380 rs.data_start - obj_kw_end,
381 PhysicalKind::ObjBody,
382 )?;
383 }
384 if rs.data_len > 0 {
385 self.push(rs.data_start, rs.data_len, PhysicalKind::StreamData)?;
386 }
387 let data_end = rs.data_start + rs.data_len;
390 let body_end = spans[m].start;
391 if body_end > data_end {
392 self.push(data_end, body_end - data_end, PhysicalKind::ObjBody)?;
393 }
394 self.streams.push(PdfStreamSpan {
395 object: number,
396 generation,
397 data_start: rs.data_start,
398 data_len: rs.data_len,
399 length_source: rs.source,
400 });
401 } else {
402 let body_end = spans[m].start;
403 if body_end > obj_kw_end {
404 self.push(obj_kw_end, body_end - obj_kw_end, PhysicalKind::ObjBody)?;
405 }
406 }
407
408 self.push(spans[m].start, spans[m].len, PhysicalKind::EndObj)?;
409 self.objects.push(PdfObjectSpan {
410 number,
411 generation,
412 start: obj_header_start,
413 end: spans[m].start + spans[m].len,
414 role,
415 });
416 Ok(m + 1)
417 }
418 None => {
419 let end = input.len() as u64;
421 if end > obj_kw_end {
422 self.push(obj_kw_end, end - obj_kw_end, PhysicalKind::ObjBody)?;
423 }
424 Ok(spans.len())
425 }
426 }
427 }
428
429 fn emit_xref(&mut self, input: &[u8], spans: &[Span], i: usize) -> Result<usize> {
432 let start = spans[i].start;
433 let mut end_idx = spans.len();
434 for (k, sp) in spans.iter().enumerate().skip(i + 1) {
435 let sp = *sp;
436 if regular_eq(input, sp, b"trailer") || regular_eq(input, sp, b"startxref") {
437 end_idx = k;
438 break;
439 }
440 if sp.kind == SpanKind::Comment && bytes_of(input, sp).starts_with(b"%%EOF") {
441 end_idx = k;
442 break;
443 }
444 }
445
446 let end = if end_idx < spans.len() {
447 spans[end_idx].start
448 } else {
449 input.len() as u64
450 };
451 if end > start {
452 self.push(start, end - start, PhysicalKind::XrefSection)?;
453 }
454
455 if end_idx < spans.len() && regular_eq(input, spans[end_idx], b"trailer") {
456 self.emit_trailer(spans, end_idx)
457 } else {
458 Ok(end_idx)
459 }
460 }
461
462 fn emit_trailer(&mut self, spans: &[Span], t: usize) -> Result<usize> {
465 let start = spans[t].start;
466 let mut end = spans[t].start + spans[t].len;
467 let mut next = t + 1;
468 let dict_idx = if next < spans.len() && spans[next].kind == SpanKind::Whitespace {
469 next + 1
470 } else {
471 next
472 };
473 if dict_idx < spans.len()
474 && spans[dict_idx].kind == SpanKind::DictOpen
475 && let Some(close) = matching_dict_close(spans, dict_idx)
476 {
477 let lo = spans[dict_idx].start;
478 let hi = spans[close].start + spans[close].len;
479 self.trailer_dicts.push((lo, hi));
480 end = hi;
481 next = close + 1;
482 }
483 if end > start {
484 self.push(start, end - start, PhysicalKind::Trailer)?;
485 }
486 Ok(next)
487 }
488
489 fn try_startxref(&mut self, input: &[u8], spans: &[Span], i: usize) -> Result<Option<usize>> {
491 if i + 2 < spans.len()
492 && spans[i + 1].kind == SpanKind::Whitespace
493 && spans[i + 2].kind == SpanKind::Regular
494 && let Some(value) = parse_uint(bytes_of(input, spans[i + 2]), 19)
495 {
496 let start = spans[i].start;
497 let end = spans[i + 2].start + spans[i + 2].len;
498 self.push(start, end - start, PhysicalKind::StartXref)?;
499 self.startxref.push((start, value));
500 return Ok(Some(i + 3));
501 }
502 Ok(None)
503 }
504}
505
506struct Builder {
509 max: u32,
510 spans: Vec<PhysicalSpan>,
511}
512
513impl Builder {
514 fn new(limits: Limits) -> Self {
515 Builder {
516 max: limits.max_pdf_spans,
517 spans: Vec::new(),
518 }
519 }
520
521 fn push(&mut self, start: u64, len: u64, kind: PhysicalKind) -> Result<()> {
522 if len == 0 {
523 return Ok(());
524 }
525 if let Some(last) = self.spans.last_mut()
526 && last.kind == kind
527 && last.start.checked_add(last.len) == Some(start)
528 {
529 last.len = last
530 .len
531 .checked_add(len)
532 .ok_or_else(|| Error::coverage_violation("physical span length overflow"))?;
533 return Ok(());
534 }
535 if self.spans.len() as u64 >= self.max as u64 {
536 return Err(Error::resource_limit(format!(
537 "pdf physical span count exceeds limit {}",
538 self.max
539 )));
540 }
541 self.spans.push(PhysicalSpan { start, len, kind });
542 Ok(())
543 }
544}
545
546fn bytes_of(input: &[u8], sp: Span) -> &[u8] {
548 let Ok(start) = usize::try_from(sp.start) else {
549 return &[];
550 };
551 let Some(end) = sp
552 .start
553 .checked_add(sp.len)
554 .and_then(|e| usize::try_from(e).ok())
555 else {
556 return &[];
557 };
558 if start > end || end > input.len() {
559 return &[];
560 }
561 &input[start..end]
562}
563
564fn regular_eq(input: &[u8], sp: Span, keyword: &[u8]) -> bool {
566 sp.kind == SpanKind::Regular && bytes_of(input, sp) == keyword
567}
568
569fn parse_uint(bytes: &[u8], max_digits: usize) -> Option<u64> {
571 if bytes.is_empty() || bytes.len() > max_digits {
572 return None;
573 }
574 let mut value: u64 = 0;
575 for &b in bytes {
576 if !b.is_ascii_digit() {
577 return None;
578 }
579 value = value.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
580 }
581 Some(value)
582}
583
584fn eol_start_after(input: &[u8], offset: u64) -> Option<u64> {
586 let start = usize::try_from(offset).ok()?;
587 if start > input.len() {
588 return None;
589 }
590 (start..input.len())
591 .find(|&i| input[i] == b'\n' || input[i] == b'\r')
592 .map(|i| i as u64)
593}
594
595fn find_regular(input: &[u8], spans: &[Span], from: usize, keyword: &[u8]) -> Option<usize> {
597 if from >= spans.len() {
598 return None;
599 }
600 spans[from..]
601 .iter()
602 .position(|sp| regular_eq(input, *sp, keyword))
603 .map(|off| from + off)
604}
605
606fn matching_dict_close(spans: &[Span], open: usize) -> Option<usize> {
608 let mut depth: u64 = 0;
609 for (k, sp) in spans.iter().enumerate().skip(open) {
610 match sp.kind {
611 SpanKind::DictOpen => depth = depth.saturating_add(1),
612 SpanKind::DictClose => {
613 if depth == 0 {
614 return None;
615 }
616 depth -= 1;
617 if depth == 0 {
618 return Some(k);
619 }
620 }
621 _ => {}
622 }
623 }
624 None
625}
626
627fn obj_header_at(input: &[u8], spans: &[Span], i: usize) -> Option<(u64, u64)> {
629 if i + 4 >= spans.len() {
630 return None;
631 }
632 if spans[i].kind != SpanKind::Regular {
633 return None;
634 }
635 let number = parse_uint(bytes_of(input, spans[i]), 10)?;
636 if spans[i + 1].kind != SpanKind::Whitespace {
637 return None;
638 }
639 if spans[i + 2].kind != SpanKind::Regular {
640 return None;
641 }
642 let generation = parse_uint(bytes_of(input, spans[i + 2]), 10)?;
643 if spans[i + 3].kind != SpanKind::Whitespace {
644 return None;
645 }
646 if !regular_eq(input, spans[i + 4], b"obj") {
647 return None;
648 }
649 Some((number, generation))
650}
651
652#[derive(Debug, Clone, Copy)]
658struct ObjBody {
659 number: u64,
660 generation: u64,
661 body_lo: u64,
662 body_hi: u64,
663}
664
665#[derive(Debug, Clone, Copy)]
667struct ResolvedStream {
668 data_start: u64,
669 data_len: u64,
670 endstream: usize,
671 source: LengthSource,
672}
673
674fn collect_bodies(input: &[u8], spans: &[Span]) -> Vec<ObjBody> {
678 let mut out = Vec::new();
679 let mut i = 0usize;
680 while i < spans.len() {
681 if let Some((number, generation)) = obj_header_at(input, spans, i) {
682 let body_lo = spans[i + 4].start + spans[i + 4].len;
683 match find_endobj(input, spans, i + 5) {
684 Some(m) => {
685 out.push(ObjBody {
686 number,
687 generation,
688 body_lo,
689 body_hi: spans[m].start,
690 });
691 i = m + 1;
692 }
693 None => {
694 out.push(ObjBody {
695 number,
696 generation,
697 body_lo,
698 body_hi: input.len() as u64,
699 });
700 i = spans.len();
701 }
702 }
703 } else {
704 i += 1;
705 }
706 }
707 out
708}
709
710fn find_endobj(input: &[u8], spans: &[Span], from: usize) -> Option<usize> {
713 let mut j = from;
714 while j < spans.len() {
715 if regular_eq(input, spans[j], b"endobj") {
716 return Some(j);
717 }
718 if regular_eq(input, spans[j], b"stream") {
719 let kw_end = spans[j].start + spans[j].len;
720 if let Some(data_start) = eol_start_after(input, kw_end)
721 && let Some(k) = find_regular(input, spans, j + 1, b"endstream")
722 && spans[k].start >= data_start
723 {
724 j = k + 1;
725 continue;
726 }
727 }
728 j += 1;
729 }
730 None
731}
732
733fn lookup_body(bodies: &[ObjBody], number: u64, generation: u64) -> Option<(u64, u64)> {
735 bodies
736 .iter()
737 .find(|b| b.number == number && b.generation == generation)
738 .map(|b| (b.body_lo, b.body_hi))
739}
740
741fn resolve_stream(
747 input: &[u8],
748 spans: &[Span],
749 s: usize,
750 bodies: &[ObjBody],
751 lower: usize,
752) -> Option<ResolvedStream> {
753 let kw_end = spans[s].start + spans[s].len;
754
755 let mut resolved: Option<ResolvedStream> = None;
756 let mut source = LengthSource::Fallback;
757
758 if let Some((open, close)) = preceding_dict(spans, s, lower) {
759 let dict_lo = spans[open].start;
760 let dict_hi = spans[close].start + spans[close].len;
761 match dict_length(input, spans, dict_lo, dict_hi) {
762 Some(LengthValue::Direct(n)) => {
763 if let Some(rs) = verify_direct(input, spans, kw_end, n) {
764 resolved = Some(rs);
765 source = LengthSource::Direct;
766 }
767 }
768 Some(LengthValue::Indirect { number, generation }) => {
769 if let Some((lo, hi)) = lookup_body(bodies, number, generation)
770 && let Some(n) = body_as_u64(input, spans, lo, hi)
771 && let Some(rs) = verify_direct(input, spans, kw_end, n)
772 {
773 resolved = Some(rs);
774 source = LengthSource::Indirect;
775 }
776 }
777 None => {
778 if !dict_has_length(input, spans, dict_lo, dict_hi) {
779 source = LengthSource::Missing;
780 }
781 }
782 }
783 } else {
784 source = LengthSource::Missing;
785 }
786
787 if let Some(mut rs) = resolved {
788 rs.source = source;
789 return Some(rs);
790 }
791
792 let data_start = eol_start_after(input, kw_end)?;
795 let k = find_regular(input, spans, s + 1, b"endstream")?;
796 if spans[k].start < data_start {
797 return None;
798 }
799 Some(ResolvedStream {
800 data_start,
801 data_len: spans[k].start - data_start,
802 endstream: k,
803 source,
804 })
805}
806
807fn preceding_dict(spans: &[Span], s: usize, lower: usize) -> Option<(usize, usize)> {
811 let mut j = s;
812 while j > lower {
813 j -= 1;
814 if spans[j].kind == SpanKind::DictOpen
815 && let Some(close) = matching_dict_close(spans, j)
816 && close < s
817 {
818 return Some((j, close));
819 }
820 }
821 None
822}
823
824fn verify_direct(input: &[u8], spans: &[Span], kw_end: u64, n: u64) -> Option<ResolvedStream> {
828 let eol = post_stream_eol_len(input, kw_end)?;
829 let data_start = kw_end + eol;
830 let data_end = data_start.checked_add(n)?;
831 let k = first_regular_at_or_after(input, spans, data_end, b"endstream")?;
832 let gap = spans[k].start.checked_sub(data_end)?;
833 let ok = gap == 0
834 || (gap == 1 && byte_at(input, data_end) == Some(b'\n'))
835 || (gap == 2
836 && byte_at(input, data_end) == Some(b'\r')
837 && byte_at(input, data_end + 1) == Some(b'\n'));
838 if !ok {
839 return None;
840 }
841 Some(ResolvedStream {
842 data_start,
843 data_len: n,
844 endstream: k,
845 source: LengthSource::Direct,
846 })
847}
848
849fn post_stream_eol_len(input: &[u8], kw_end: u64) -> Option<u64> {
852 match byte_at(input, kw_end) {
853 Some(b'\n') => Some(1),
854 Some(b'\r') if byte_at(input, kw_end + 1) == Some(b'\n') => Some(2),
855 _ => None,
856 }
857}
858
859fn first_regular_at_or_after(
862 input: &[u8],
863 spans: &[Span],
864 offset: u64,
865 keyword: &[u8],
866) -> Option<usize> {
867 let idx = spans.partition_point(|sp| sp.start < offset);
868 spans[idx..]
869 .iter()
870 .position(|sp| regular_eq(input, *sp, keyword))
871 .map(|off| idx + off)
872}
873
874fn byte_at(input: &[u8], offset: u64) -> Option<u8> {
876 usize::try_from(offset)
877 .ok()
878 .and_then(|i| input.get(i).copied())
879}
880
881fn build_revisions(
885 input: &[u8],
886 spans: &[Span],
887 eofs: &[u64],
888 objects: &[PdfObjectSpan],
889 startxref: &[(u64, u64)],
890 trailers: &[(u64, u64)],
891) -> Vec<RevisionInfo> {
892 let mut out = Vec::with_capacity(eofs.len());
893 let mut start = 0u64;
894 for (index, &e) in eofs.iter().enumerate() {
895 let end = span_end_at(spans, e).unwrap_or(e);
896 let startxref = startxref
897 .iter()
898 .find(|&&(keyword, _)| keyword >= start && keyword < end)
899 .map(|&(_, value)| value);
900 let prev = resolve_prev(input, spans, start, end, objects, trailers);
901 out.push(RevisionInfo {
902 index: index as u32,
903 start,
904 end,
905 startxref,
906 prev,
907 });
908 start = end + eol_len_after(input, end);
909 }
910 out
911}
912
913fn resolve_prev(
919 input: &[u8],
920 spans: &[Span],
921 rev_start: u64,
922 rev_end: u64,
923 objects: &[PdfObjectSpan],
924 trailers: &[(u64, u64)],
925) -> Option<u64> {
926 let trailer = trailers
927 .iter()
928 .rev()
929 .find(|&&(lo, _)| lo >= rev_start && lo < rev_end);
930 let (dict_lo, dict_hi) = match trailer {
931 Some(&(lo, hi)) => (lo, hi),
932 None => {
933 let object = objects.iter().find(|o| {
934 o.role == ObjRole::XRefStream && o.start >= rev_start && o.start < rev_end
935 })?;
936 leading_dict_range_at(input, spans, object.start)?
937 }
938 };
939 match dict_int_or_ref(input, spans, dict_lo, dict_hi, b"Prev") {
940 Some(LengthValue::Direct(n)) => Some(n),
941 Some(LengthValue::Indirect { number, generation }) => objects
942 .iter()
943 .find(|o| o.number == number && o.generation == generation)
944 .map(|o| o.start),
945 None => None,
946 }
947}
948
949fn leading_dict_range(spans: &[Span], after: usize) -> Option<(u64, u64)> {
953 let mut j = after;
954 while j < spans.len() && matches!(spans[j].kind, SpanKind::Whitespace | SpanKind::Comment) {
955 j += 1;
956 }
957 if j >= spans.len() || spans[j].kind != SpanKind::DictOpen {
958 return None;
959 }
960 let close = matching_dict_close(spans, j)?;
961 Some((spans[j].start, spans[close].start + spans[close].len))
962}
963
964fn leading_dict_role(input: &[u8], spans: &[Span], after: usize) -> ObjRole {
967 let Some((lo, hi)) = leading_dict_range(spans, after) else {
968 return ObjRole::Generic;
969 };
970 match dict_name_value(input, spans, lo, hi, b"Type") {
971 Some(name) if name == b"XRef".as_slice() => ObjRole::XRefStream,
972 Some(name) if name == b"ObjStm".as_slice() => ObjRole::ObjectStream,
973 _ => ObjRole::Generic,
974 }
975}
976
977fn leading_dict_range_at(input: &[u8], spans: &[Span], start: u64) -> Option<(u64, u64)> {
980 let idx = spans.partition_point(|sp| sp.start < start);
981 if idx >= spans.len() || spans[idx].start != start {
982 return None;
983 }
984 obj_header_at(input, spans, idx)?;
985 leading_dict_range(spans, idx + 5)
986}
987
988fn span_end_at(spans: &[Span], offset: u64) -> Option<u64> {
990 let idx = spans.partition_point(|sp| sp.start <= offset);
991 if idx == 0 {
992 return None;
993 }
994 let sp = spans[idx - 1];
995 if offset < sp.start.saturating_add(sp.len) {
996 Some(sp.start.saturating_add(sp.len))
997 } else {
998 None
999 }
1000}
1001
1002fn eol_len_after(input: &[u8], offset: u64) -> u64 {
1005 match byte_at(input, offset) {
1006 Some(b'\n') => 1,
1007 Some(b'\r') if byte_at(input, offset + 1) == Some(b'\n') => 2,
1008 Some(b'\r') => 1,
1009 _ => 0,
1010 }
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016 use crate::error::ErrorClass;
1017
1018 fn count(p: &PdfPhysical, kind: PhysicalKind) -> usize {
1019 p.spans.iter().filter(|s| s.kind == kind).count()
1020 }
1021
1022 fn canonical_pdf() -> Vec<u8> {
1023 let mut s = String::new();
1024 s.push_str("%PDF-1.7\n");
1025 s.push_str("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1026 s.push_str("2 0 obj\n<< /Length 6 >>\nstream\nhello\nendstream\nendobj\n");
1027 s.push_str("3 0 obj\n<< /Length 7 >>\nstream\nworld\nendstream\nendobj\n");
1028 s.push_str("4 0 obj\n<< /Length 4 >>\nstream\nxyz\nendstream\nendobj\n");
1029 s.push_str("xref\n0 5\n0000000000 65535 f \n0000000010 00000 n \n");
1030 s.push_str("trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n321\n%%EOF");
1031 s.into_bytes()
1032 }
1033
1034 #[test]
1035 fn canonical_pdf_is_fully_classified() {
1036 let pdf = canonical_pdf();
1037 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1038
1039 assert_eq!(p.header, Some((0, 8)));
1040 assert_eq!(p.objects.len(), 4);
1041 let nums: Vec<(u64, u64)> = p.objects.iter().map(|o| (o.number, o.generation)).collect();
1042 assert_eq!(nums, [(1, 0), (2, 0), (3, 0), (4, 0)]);
1043 assert_eq!(p.startxref, [321]);
1044 assert_eq!(p.eofs.len(), 1);
1045 assert_eq!(count(&p, PhysicalKind::EndObj), 4);
1046 assert_eq!(count(&p, PhysicalKind::StreamData), 3);
1047 assert_eq!(count(&p, PhysicalKind::XrefSection), 1);
1048 assert_eq!(count(&p, PhysicalKind::Trailer), 1);
1049 assert_eq!(count(&p, PhysicalKind::ObjHeader), 4);
1050 assert_eq!(p.total_len(), pdf.len() as u64);
1051 p.validate(pdf.len() as u64).unwrap();
1052 }
1053
1054 #[test]
1055 fn endobj_inside_literal_string_does_not_split_object() {
1056 let pdf = b"%PDF-1.4\n1 0 obj\n(endobj)\nendobj".to_vec();
1057 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1058
1059 assert_eq!(p.objects.len(), 1);
1060 assert_eq!(p.objects[0].number, 1);
1061 assert_eq!(p.objects[0].generation, 0);
1062 assert_eq!(p.objects[0].end, pdf.len() as u64);
1063 assert_eq!(count(&p, PhysicalKind::EndObj), 1);
1064 assert_eq!(count(&p, PhysicalKind::StreamData), 0);
1065 p.validate(pdf.len() as u64).unwrap();
1066 }
1067
1068 #[test]
1069 fn stream_data_with_keyword_spellings_stays_opaque() {
1070 let pdf =
1071 b"%PDF-1.4\n1 0 obj\n<< /Length 30 >>\nstream\nendobj stream bytes here\nendstream\nendobj"
1072 .to_vec();
1073 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1074
1075 assert_eq!(p.objects.len(), 1);
1076 assert_eq!(p.objects[0].number, 1);
1077 assert_eq!(p.objects[0].end, pdf.len() as u64);
1078 assert_eq!(count(&p, PhysicalKind::EndObj), 1);
1079 assert_eq!(count(&p, PhysicalKind::StreamData), 1);
1080
1081 let data = p
1083 .spans
1084 .iter()
1085 .find(|s| s.kind == PhysicalKind::StreamData)
1086 .unwrap();
1087 let slice = &pdf[data.start as usize..(data.start + data.len) as usize];
1088 assert!(slice.windows(6).any(|w| w == b"endobj"));
1089 assert!(slice.windows(6).any(|w| w == b"stream"));
1090 p.validate(pdf.len() as u64).unwrap();
1091 }
1092
1093 #[test]
1094 fn two_objects_with_xref_trailer_and_startxref() {
1095 let pdf = b"%PDF-1.4\n1 0 obj\n<< >>\nendobj\n2 0 obj\n<< >>\nendobj\nxref\n0 3\n0000000000 65535 f \n0000000009 00000 n \ntrailer\n<< /Size 3 >>\nstartxref\n99\n%%EOF".to_vec();
1096 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1097
1098 assert_eq!(p.objects.len(), 2);
1099 assert_eq!(p.objects[0].number, 1);
1100 assert_eq!(p.objects[1].number, 2);
1101 assert!(p.objects[0].end <= p.objects[1].start);
1102 assert_eq!(count(&p, PhysicalKind::XrefSection), 1);
1103 assert_eq!(count(&p, PhysicalKind::Trailer), 1);
1104 assert_eq!(p.startxref, [99]);
1105 assert_eq!(p.eofs.len(), 1);
1106 p.validate(pdf.len() as u64).unwrap();
1107 }
1108
1109 #[test]
1110 fn malformed_object_without_endobj_is_conservative() {
1111 let pdf = b"1 0 obj".to_vec();
1112 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1113
1114 assert!(p.objects.is_empty());
1115 assert_eq!(count(&p, PhysicalKind::ObjHeader), 1);
1116 assert_eq!(count(&p, PhysicalKind::EndObj), 0);
1117 assert_eq!(p.total_len(), pdf.len() as u64);
1118 p.validate(pdf.len() as u64).unwrap();
1119 }
1120
1121 #[test]
1122 fn missing_header_leaves_cover_total() {
1123 let pdf = b"1 0 obj\nendobj\n".to_vec();
1124 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1125
1126 assert_eq!(p.header, None);
1127 assert_eq!(count(&p, PhysicalKind::EndObj), 1);
1128 p.validate(pdf.len() as u64).unwrap();
1129 }
1130
1131 fn xorshift64(state: &mut u64) -> u64 {
1132 let mut x = *state;
1133 x ^= x << 13;
1134 x ^= x >> 7;
1135 x ^= x << 17;
1136 *state = x;
1137 x
1138 }
1139
1140 #[test]
1141 fn random_bytes_never_panic_and_keep_cover() {
1142 let mut state: u64 = 0x1234_5678_9abc_def0;
1143 for _ in 0..500 {
1144 let len = (xorshift64(&mut state) % 96) as usize;
1145 let mut buf = Vec::with_capacity(len);
1146 for _ in 0..len {
1147 buf.push((xorshift64(&mut state) & 0xff) as u8);
1148 }
1149 match scan(&buf, Limits::DEFAULT) {
1150 Ok(p) => {
1151 p.validate(buf.len() as u64).unwrap();
1152 assert_eq!(p.total_len(), buf.len() as u64);
1153 }
1154 Err(e) => {
1155 let _ = e.class();
1157 }
1158 }
1159 }
1160 }
1161
1162 #[test]
1163 fn tiny_span_limit_is_resource_limit() {
1164 let pdf = canonical_pdf();
1165 let limits = Limits {
1166 max_pdf_spans: 1,
1167 ..Limits::DEFAULT
1168 };
1169 let err = scan(&pdf, limits).unwrap_err();
1170 assert_eq!(err.class(), ErrorClass::ResourceLimit);
1171 }
1172
1173 fn offset_of(hay: &[u8], needle: &[u8]) -> u64 {
1174 hay.windows(needle.len())
1175 .position(|w| w == needle)
1176 .expect("needle present") as u64
1177 }
1178
1179 fn slice_of(pdf: &[u8], s: PdfStreamSpan) -> &[u8] {
1180 &pdf[s.data_start as usize..(s.data_start + s.data_len) as usize]
1181 }
1182
1183 fn only_stream(p: &PdfPhysical) -> PdfStreamSpan {
1184 assert_eq!(p.streams.len(), 1, "expected exactly one stream");
1185 p.streams[0]
1186 }
1187
1188 #[test]
1189 fn direct_length_yields_exact_span() {
1190 let pdf =
1191 b"%PDF-1.5\n1 0 obj\n<< /Length 5 >>\nstream\nhello\nendstream\nendobj\n".to_vec();
1192 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1193
1194 let s = only_stream(&p);
1195 assert_eq!(s.object, 1);
1196 assert_eq!(s.generation, 0);
1197 assert_eq!(s.length_source, LengthSource::Direct);
1198 assert_eq!(s.data_start, offset_of(&pdf, b"hello"));
1199 assert_eq!(s.data_len, 5);
1200 assert_eq!(slice_of(&pdf, s), b"hello");
1201
1202 let ds = p
1203 .spans
1204 .iter()
1205 .find(|sp| sp.kind == PhysicalKind::StreamData)
1206 .unwrap();
1207 assert_eq!(ds.start, s.data_start);
1208 assert_eq!(ds.len, s.data_len);
1209 p.validate(pdf.len() as u64).unwrap();
1210 }
1211
1212 #[test]
1213 fn length_including_trailing_eol_is_accepted() {
1214 let pdf =
1215 b"%PDF-1.5\n1 0 obj\n<< /Length 6 >>\nstream\nhello\nendstream\nendobj\n".to_vec();
1216 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1217 let s = only_stream(&p);
1218 assert_eq!(s.length_source, LengthSource::Direct);
1219 assert_eq!(slice_of(&pdf, s), b"hello\n");
1220 p.validate(pdf.len() as u64).unwrap();
1221 }
1222
1223 #[test]
1224 fn crlf_and_lf_after_stream_are_both_handled() {
1225 let crlf =
1226 b"%PDF-1.5\n1 0 obj\n<< /Length 5 >>\r\nstream\r\nhello\r\nendstream\r\nendobj\n"
1227 .to_vec();
1228 let p = scan(&crlf, Limits::DEFAULT).unwrap();
1229 let s = only_stream(&p);
1230 assert_eq!(s.length_source, LengthSource::Direct);
1231 assert_eq!(slice_of(&crlf, s), b"hello");
1232 assert_eq!(s.data_start, offset_of(&crlf, b"hello"));
1233 p.validate(crlf.len() as u64).unwrap();
1234
1235 let lf = b"%PDF-1.5\n1 0 obj\n<< /Length 5 >>\nstream\nhello\nendstream\nendobj\n".to_vec();
1236 let p = scan(&lf, Limits::DEFAULT).unwrap();
1237 let s = only_stream(&p);
1238 assert_eq!(s.length_source, LengthSource::Direct);
1239 assert_eq!(slice_of(&lf, s), b"hello");
1240 p.validate(lf.len() as u64).unwrap();
1241 }
1242
1243 #[test]
1244 fn lone_cr_after_stream_falls_back() {
1245 let pdf =
1246 b"%PDF-1.5\n1 0 obj\n<< /Length 5 >>\nstream\rhello\r\nendstream\nendobj\n".to_vec();
1247 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1248 let s = only_stream(&p);
1249 assert_eq!(s.length_source, LengthSource::Fallback);
1250 assert!(slice_of(&pdf, s).windows(5).any(|w| w == b"hello"));
1251 p.validate(pdf.len() as u64).unwrap();
1252 }
1253
1254 #[test]
1255 fn indirect_length_is_resolved_forward_reference() {
1256 let pdf =
1258 b"%PDF-1.5\n1 0 obj\n<< /Length 5 0 R >>\nstream\nhello\nendstream\nendobj\n5 0 obj\n5\nendobj\n"
1259 .to_vec();
1260 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1261 let s = only_stream(&p);
1262 assert_eq!(s.length_source, LengthSource::Indirect);
1263 assert_eq!(s.data_start, offset_of(&pdf, b"hello"));
1264 assert_eq!(s.data_len, 5);
1265 assert_eq!(slice_of(&pdf, s), b"hello");
1266 p.validate(pdf.len() as u64).unwrap();
1267 }
1268
1269 #[test]
1270 fn missing_length_uses_keyword_fallback() {
1271 let pdf = b"%PDF-1.5\n1 0 obj\n<< /Type /X >>\nstream\nhello\nendstream\nendobj\n".to_vec();
1272 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1273 let s = only_stream(&p);
1274 assert_eq!(s.length_source, LengthSource::Missing);
1275 assert!(slice_of(&pdf, s).windows(5).any(|w| w == b"hello"));
1276 p.validate(pdf.len() as u64).unwrap();
1277 }
1278
1279 #[test]
1280 fn wrong_length_past_endstream_falls_back() {
1281 let pdf =
1282 b"%PDF-1.5\n1 0 obj\n<< /Length 100 >>\nstream\nhello\nendstream\nendobj\n".to_vec();
1283 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1284 let s = only_stream(&p);
1285 assert_eq!(s.length_source, LengthSource::Fallback);
1286 assert!(slice_of(&pdf, s).windows(5).any(|w| w == b"hello"));
1287 p.validate(pdf.len() as u64).unwrap();
1288 }
1289
1290 #[test]
1291 fn correct_length_beats_endstream_bytes_in_payload() {
1292 let payload = b"endstream\nfoo";
1295 let pdf =
1296 b"%PDF-1.5\n1 0 obj\n<< /Length 13 >>\nstream\nendstream\nfoo\nendstream\nendobj\n"
1297 .to_vec();
1298 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1299 let s = only_stream(&p);
1300 assert_eq!(s.length_source, LengthSource::Direct);
1301 assert_eq!(s.data_start, offset_of(&pdf, b"endstream\nfoo"));
1302 assert_eq!(s.data_len, payload.len() as u64);
1303 assert_eq!(slice_of(&pdf, s), payload);
1304 p.validate(pdf.len() as u64).unwrap();
1305 }
1306
1307 #[test]
1308 fn two_revisions_have_correct_boundaries() {
1309 let r1 = b"%PDF-1.4\n1 0 obj\n<< >>\nendobj\n%%EOF\n";
1310 let r2 = b"2 0 obj\n<< >>\nendobj\n%%EOF";
1311 let mut pdf = Vec::new();
1312 pdf.extend_from_slice(r1);
1313 pdf.extend_from_slice(r2);
1314
1315 let eof_positions: Vec<u64> = pdf
1316 .windows(5)
1317 .enumerate()
1318 .filter(|(_, w)| *w == b"%%EOF")
1319 .map(|(i, _)| i as u64)
1320 .collect();
1321 assert_eq!(eof_positions.len(), 2);
1322
1323 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1324 assert_eq!(p.eofs.len(), 2);
1325 assert_eq!(p.revisions.len(), 2);
1326
1327 let rev1_end = eof_positions[0] + 5;
1328 let rev2_end = eof_positions[1] + 5;
1329 assert_eq!(
1330 p.revisions,
1331 vec![
1332 RevisionInfo {
1333 index: 0,
1334 start: 0,
1335 end: rev1_end,
1336 startxref: None,
1337 prev: None,
1338 },
1339 RevisionInfo {
1340 index: 1,
1341 start: rev1_end + 1,
1342 end: rev2_end,
1343 startxref: None,
1344 prev: None,
1345 },
1346 ]
1347 );
1348 assert_eq!(rev2_end, pdf.len() as u64);
1349 assert_eq!(p.objects.len(), 2);
1350 p.validate(pdf.len() as u64).unwrap();
1351 }
1352
1353 #[test]
1354 fn classic_xref_revision_records_startxref_and_no_prev() {
1355 let pdf = canonical_pdf();
1356 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1357 assert_eq!(p.revisions.len(), 1);
1358 let r = p.revisions[0];
1359 assert_eq!(r.index, 0);
1360 assert_eq!(r.start, 0);
1361 assert_eq!(r.end, pdf.len() as u64);
1362 assert_eq!(r.startxref, Some(321));
1363 assert_eq!(r.prev, None);
1364 p.validate(pdf.len() as u64).unwrap();
1365 }
1366
1367 #[test]
1368 fn incremental_classic_trailer_prev_resolves_to_first_xref() {
1369 let rev1 = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\nxref\n0 2\n0000000000 65535 f \n0000000009 00000 n \ntrailer\n<< /Size 2 /Root 1 0 R >>\nstartxref\n9\n%%EOF\n";
1370 let x1 = rev1
1371 .windows(4)
1372 .position(|w| w == b"xref")
1373 .expect("xref present") as u64;
1374 let rev2 = format!(
1375 "2 0 obj\n<< /Type /Pages >>\nendobj\nxref\n0 3\n0000000000 65535 f \n0000000009 00000 n \n0000000042 00000 n \ntrailer\n<< /Size 3 /Prev {x1} /Root 1 0 R >>\nstartxref\n777\n%%EOF"
1376 );
1377 let mut pdf = Vec::new();
1378 pdf.extend_from_slice(rev1);
1379 pdf.extend_from_slice(rev2.as_bytes());
1380
1381 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1382 assert_eq!(p.revisions.len(), 2);
1383 assert_eq!(p.revisions[0].index, 0);
1384 assert_eq!(p.revisions[0].startxref, Some(9));
1385 assert_eq!(p.revisions[0].prev, None);
1386 assert_eq!(p.revisions[1].index, 1);
1387 assert_eq!(p.revisions[1].startxref, Some(777));
1388 assert_eq!(p.revisions[1].prev, Some(x1));
1389 p.validate(pdf.len() as u64).unwrap();
1390 }
1391
1392 #[test]
1393 fn xref_stream_anchor_prev_reference_is_resolved() {
1394 let rev1 = b"%PDF-1.5\n1 0 obj\n<< >>\nendobj\nstartxref\n0\n%%EOF\n";
1395 let rev2 = b"2 0 obj\n<< /Type /XRef /Prev 3 0 R >>\nendobj\n3 0 obj\n<< >>\nendobj\nstartxref\n0\n%%EOF";
1396 let mut pdf = Vec::new();
1397 pdf.extend_from_slice(rev1);
1398 pdf.extend_from_slice(rev2);
1399 let obj3_start = pdf
1400 .windows(8)
1401 .position(|w| w == b"3 0 obj\n")
1402 .expect("object 3 present") as u64;
1403
1404 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1405 assert_eq!(p.revisions.len(), 2);
1406 assert_eq!(p.revisions[0].prev, None);
1407 assert_eq!(p.revisions[1].prev, Some(obj3_start));
1408 assert_eq!(
1409 p.objects.iter().find(|o| o.number == 2).unwrap().role,
1410 ObjRole::XRefStream
1411 );
1412 p.validate(pdf.len() as u64).unwrap();
1413 }
1414
1415 #[test]
1416 fn object_roles_are_classified_from_leading_dict() {
1417 let pdf = b"%PDF-1.5\n1 0 obj\n<< /Type /Catalog >>\nendobj\n2 0 obj\n<< /Type /XRef >>\nendobj\n3 0 obj\n<< /Type /ObjStm /N 0 >>\nendobj\n4 0 obj\n<< /Foo /Bar >>\nendobj\n5 0 obj\n<< /Type 5 >>\nendobj\n6 0 obj\n42\nendobj\n7 0 obj\n<< /Foo ( /Type /XRef ) >>\nendobj\n".to_vec();
1418 let p = scan(&pdf, Limits::DEFAULT).unwrap();
1419 let role = |n: u64| p.objects.iter().find(|o| o.number == n).unwrap().role;
1420 assert_eq!(role(1), ObjRole::Generic);
1421 assert_eq!(role(2), ObjRole::XRefStream);
1422 assert_eq!(role(3), ObjRole::ObjectStream);
1423 assert_eq!(role(4), ObjRole::Generic);
1424 assert_eq!(role(5), ObjRole::Generic);
1425 assert_eq!(role(6), ObjRole::Generic);
1426 assert_eq!(role(7), ObjRole::Generic);
1427 p.validate(pdf.len() as u64).unwrap();
1428 }
1429
1430 #[test]
1431 fn random_inputs_keep_streams_and_revisions_consistent() {
1432 let mut state: u64 = 0xdead_beef_cafe_f00d;
1433 for _ in 0..500 {
1434 let len = (xorshift64(&mut state) % 128) as usize;
1435 let mut buf = Vec::with_capacity(len);
1436 for _ in 0..len {
1437 buf.push((xorshift64(&mut state) & 0xff) as u8);
1438 }
1439 let p = scan(&buf, Limits::DEFAULT).unwrap();
1440 p.validate(buf.len() as u64).unwrap();
1441 for s in &p.streams {
1442 assert!(s.data_start + s.data_len <= buf.len() as u64);
1443 }
1444 for r in &p.revisions {
1445 assert!(r.start <= r.end && r.end <= buf.len() as u64);
1446 }
1447 }
1448 }
1449}