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