1use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
29use pdfrum_object::{
30 Array, ByteSpan, Dict, Name, ObjRef, Object, PdfString, Resolve, Stream, name_decode, names,
31};
32
33use crate::error::Error;
34use crate::lexer::{
35 Delim, Lexer, Token, WordBoundary, atoui, find_word, is_line_ending, is_whitespace,
36};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Strictness {
41 Loose,
44 Strict,
48}
49
50pub struct Context<'r, R: Resolve + ?Sized> {
57 pub limits: &'r Limits,
59 pub diags: &'r mut Diagnostics,
61 pub file: Option<&'r ByteSpan>,
63 pub store: Option<&'r R>,
65}
66
67impl<R: Resolve + ?Sized> Context<'_, R> {
68 fn note(&mut self, severity: Severity, what: DiagKind, at: usize) {
70 self.diags.record(severity, what, Some(at as u64));
71 }
72}
73
74pub fn parse_object<R: Resolve + ?Sized>(
102 lx: &mut Lexer<'_>,
103 limits: &Limits,
104 diags: &mut Diagnostics,
105 strictness: Strictness,
106 store: &R,
107) -> Result<Object, Error> {
108 let mut ctx = Context {
109 limits,
110 diags,
111 file: None,
112 store: Some(store),
113 };
114 body(lx, &mut ctx, strictness, 0)
115}
116
117pub(crate) fn body<R: Resolve + ?Sized>(
121 lx: &mut Lexer<'_>,
122 ctx: &mut Context<'_, R>,
123 strictness: Strictness,
124 depth: u32,
125) -> Result<Object, Error> {
126 if depth >= ctx.limits.max_object_nesting {
133 return Err(Error::TooDeep(ctx.limits.max_object_nesting));
134 }
135 let start = lx.pos();
136 let token = lx.next_word(ctx.limits);
137
138 match token {
139 Token::Number(word) => number_or_reference(lx, ctx, word),
140 Token::Name(payload) => Ok(Object::Name(Name::decode(payload))),
141 Token::Keyword(b"true") => Ok(Object::Bool(true)),
142 Token::Keyword(b"false") => Ok(Object::Bool(false)),
143 Token::Keyword(b"null") => Ok(Object::Null),
144 Token::Delim(Delim::StringOpen) => Ok(Object::Str(PdfString::literal(
145 lx.read_literal_string().as_ref(),
146 ))),
147 Token::Delim(Delim::HexOpen) => Ok(Object::Str(PdfString::hex(lx.read_hex_string()))),
148 Token::Delim(Delim::ArrayOpen) => array(lx, ctx, strictness, depth),
149 Token::Delim(Delim::DictOpen) => dictionary(lx, ctx, strictness, depth),
150 Token::Delim(Delim::DictClose) => {
153 lx.seek(start);
154 Err(Error::NoObject(start as u64))
155 }
156 _ => Err(Error::NoObject(start as u64)),
160 }
161}
162
163fn number_or_reference<R: Resolve + ?Sized>(
165 lx: &mut Lexer<'_>,
166 ctx: &mut Context<'_, R>,
167 word: &[u8],
168) -> Result<Object, Error> {
169 let after_number = lx.pos();
170 let second = lx.next_word(ctx.limits);
171 if second.is_number() {
172 let generation = second.bytes();
173 if lx.next_word(ctx.limits).is_keyword(b"R") {
174 let num = atoui(word);
175 let generation = u16::try_from(atoui(generation)).unwrap_or(u16::MAX);
176 let reference = ObjRef::new(num, generation);
177 if reference.is_invalid() {
180 return Err(Error::NoObject(after_number as u64));
181 }
182 return Ok(Object::Ref(reference));
183 }
184 }
185 lx.seek(after_number);
187 Ok(parse_number(word))
188}
189
190fn parse_number(word: &[u8]) -> Object {
197 let text = String::from_utf8_lossy(word);
198 if word.contains(&b'.') {
199 Object::Real(parse_real(&text))
200 } else {
201 Object::Int(parse_int(&text))
202 }
203}
204
205fn split_sign(text: &str) -> (bool, &str) {
212 match text.as_bytes().first() {
213 Some(b'-') => (true, text.get(1..).unwrap_or_default()),
214 Some(b'+') => (false, text.get(1..).unwrap_or_default()),
215 _ => (false, text),
216 }
217}
218
219fn parse_int(text: &str) -> i64 {
237 let signed = matches!(text.as_bytes().first(), Some(b'-' | b'+'));
238 let (negative, digits) = split_sign(text);
239
240 let mut magnitude: Option<u32> = Some(0);
243 for byte in digits.bytes().take_while(u8::is_ascii_digit) {
244 magnitude = magnitude
245 .and_then(|acc| acc.checked_mul(10))
246 .and_then(|acc| acc.checked_add(u32::from(byte - b'0')));
247 }
248 let magnitude = magnitude.unwrap_or(0);
249
250 if !signed {
251 return i64::from(magnitude);
252 }
253
254 let limit = i64::from(i32::MAX) + i64::from(negative);
255 let magnitude = i64::from(magnitude);
256 if magnitude > limit {
257 return 0;
258 }
259 if negative { -magnitude } else { magnitude }
260}
261
262fn parse_real(text: &str) -> f32 {
264 let (negative, digits) = split_sign(text);
265 let mut cleaned = String::with_capacity(digits.len());
266 let mut seen_dot = false;
267 for c in digits.chars() {
268 match c {
269 '.' if seen_dot => break,
270 '.' => {
271 seen_dot = true;
272 cleaned.push('.');
273 }
274 '0'..='9' => cleaned.push(c),
275 _ => break,
276 }
277 }
278 let magnitude = cleaned.parse::<f32>().unwrap_or(0.0);
279 if negative { -magnitude } else { magnitude }
280}
281
282fn array<R: Resolve + ?Sized>(
290 lx: &mut Lexer<'_>,
291 ctx: &mut Context<'_, R>,
292 strictness: Strictness,
293 depth: u32,
294) -> Result<Object, Error> {
295 let mut out = Array::new();
296 loop {
297 let before = lx.pos();
298 match body(lx, ctx, Strictness::Loose, depth + 1) {
299 Ok(Object::Stream(_)) => {
300 ctx.note(
303 Severity::Recovered,
304 DiagKind::StreamInCompositeDropped,
305 before,
306 );
307 }
308 Ok(value) => {
309 if out.len() >= ctx.limits.max_array_len {
310 return Ok(Object::Array(out));
311 }
312 out.push(value);
313 }
314 Err(_) => {
319 let mut probe = Lexer::at(lx.bytes(), before);
322 let stopper = probe.next_word(ctx.limits);
323 let closed = matches!(stopper, Token::Delim(Delim::ArrayClose));
324 if !closed {
325 ctx.note(Severity::Suspicious, DiagKind::MalformedArray, before);
326 if strictness == Strictness::Strict {
327 return Err(Error::NoObject(before as u64));
328 }
329 }
330 return Ok(Object::Array(out));
331 }
332 }
333 }
334}
335
336fn dictionary<R: Resolve + ?Sized>(
345 lx: &mut Lexer<'_>,
346 ctx: &mut Context<'_, R>,
347 strictness: Strictness,
348 depth: u32,
349) -> Result<Object, Error> {
350 let mut dict = Dict::new();
351 loop {
352 let key_start = lx.pos();
353 let token = lx.next_word(ctx.limits);
354 match token {
355 Token::Eof => return Err(Error::NoObject(key_start as u64)),
357 Token::Delim(Delim::DictClose) => break,
358 Token::Keyword(b"endobj") => {
359 ctx.note(Severity::Suspicious, DiagKind::MalformedDict, key_start);
362 lx.seek(key_start);
363 break;
364 }
365 Token::Name(payload) => {
366 let key = name_decode(payload);
367 let value_start = lx.pos();
368 match body(lx, ctx, Strictness::Loose, depth + 1) {
369 Ok(Object::Stream(_)) => {
370 ctx.note(
371 Severity::Recovered,
372 DiagKind::StreamInCompositeDropped,
373 value_start,
374 );
375 }
376 Ok(value) => {
377 if key.is_empty() {
380 ctx.note(Severity::Suspicious, DiagKind::MalformedDict, key_start);
381 } else {
382 dict.push(Name::new(key), value);
383 }
384 }
385 Err(_) => {
389 ctx.note(Severity::Suspicious, DiagKind::MalformedDict, value_start);
390 if strictness == Strictness::Strict {
391 lx.to_next_line();
392 return Err(Error::NoObject(value_start as u64));
393 }
394 }
395 }
396 }
397 _ => {
401 ctx.note(Severity::Suspicious, DiagKind::MalformedDict, key_start);
402 }
403 }
404 }
405
406 let after_dict = lx.pos();
408 if lx.next_word(ctx.limits).is_keyword(b"stream") {
409 return read_stream(lx, ctx, dict);
410 }
411 lx.seek(after_dict);
412 Ok(Object::Dict(dict))
413}
414
415pub(crate) fn read_stream<R: Resolve + ?Sized>(
423 lx: &mut Lexer<'_>,
424 ctx: &mut Context<'_, R>,
425 dict: Dict,
426) -> Result<Object, Error> {
427 let declared = declared_length(&dict, ctx);
430
431 lx.to_next_line();
432 let data_start = lx.pos();
433 let file_len = lx.bytes().len();
434
435 let mut length = declared.filter(|&n| {
436 n == 0 || data_start.checked_add(n).is_some_and(|end| end < file_len)
438 });
439
440 if let Some(n) = length {
441 lx.seek(data_start.saturating_add(n));
442 lx.skip_eol_marker();
443 let word = lx.next_word(ctx.limits);
444 if !word.bytes().starts_with(b"endstream") {
447 ctx.note(Severity::Recovered, DiagKind::LengthMismatch, data_start);
448 length = None;
449 lx.seek(data_start);
450 }
451 } else if declared.is_some() {
452 ctx.note(Severity::Recovered, DiagKind::LengthMismatch, data_start);
453 }
454
455 let length = match length {
456 Some(n) => n,
457 None => match scan_for_end(lx.bytes(), data_start) {
458 Some(end) => {
459 ctx.note(Severity::Recovered, DiagKind::KeywordResync, data_start);
460 end.saturating_sub(data_start)
461 }
462 None => return Err(Error::NoObject(data_start as u64)),
463 },
464 };
465
466 let data_end = data_start.saturating_add(length).min(file_len);
467 let data = match ctx.file {
468 Some(file) => file
472 .subspan(data_start..data_end)
473 .unwrap_or_else(|_| ByteSpan::empty()),
474 None => ByteSpan::from(
477 lx.bytes()
478 .get(data_start..data_end)
479 .unwrap_or_default()
480 .to_vec(),
481 ),
482 };
483
484 lx.seek(data_end);
485 resync_after_stream(lx, ctx);
486 Ok(Object::Stream(Box::new(Stream::new(dict, data))))
487}
488
489fn declared_length<R: Resolve + ?Sized>(dict: &Dict, ctx: &mut Context<'_, R>) -> Option<usize> {
491 let raw = dict.raw(names::LENGTH)?;
492 let value = match raw {
493 Object::Ref(r) => {
494 let store = ctx.store?;
497 store.fetch(*r).ok()?.as_int()?
498 }
499 other => other.as_number()?.as_int()?,
500 };
501 usize::try_from(value).ok()
502}
503
504fn scan_for_end(bytes: &[u8], start: usize) -> Option<usize> {
510 let endstream = find_word(bytes, b"endstream", start, WordBoundary::WhitespaceOnly);
511 let endobj = find_word(bytes, b"endobj", start, WordBoundary::WhitespaceOnly);
512 let keyword = match (endstream, endobj) {
513 (Some(a), Some(b)) => a.min(b),
514 (Some(a), None) => a,
515 (None, Some(b)) => b,
516 (None, None) => return None,
517 };
518 let end = trim_trailing_eol(bytes, keyword);
519 (end >= start).then_some(end)
520}
521
522fn trim_trailing_eol(bytes: &[u8], pos: usize) -> usize {
524 match bytes.get(pos.wrapping_sub(1)) {
525 Some(b'\n') => {
526 if bytes.get(pos.wrapping_sub(2)) == Some(&b'\r') {
527 pos.saturating_sub(2)
528 } else {
529 pos.saturating_sub(1)
530 }
531 }
532 Some(b'\r') => pos.saturating_sub(1),
533 _ => pos,
534 }
535}
536
537fn resync_after_stream<R: Resolve + ?Sized>(lx: &mut Lexer<'_>, ctx: &mut Context<'_, R>) {
544 let before_keyword = lx.pos();
545 let word = lx.next_word(ctx.limits);
546 if word.bytes() != b"endobj" {
550 return;
551 }
552
553 let mut probe = Lexer::at(lx.bytes(), lx.pos());
556 while probe
557 .peek_byte()
558 .is_some_and(|b| is_whitespace(b) && !is_line_ending(b))
559 {
560 probe.seek(probe.pos() + 1);
561 }
562 if probe.skip_eol_marker() > 0 {
565 ctx.note(Severity::Recovered, DiagKind::KeywordResync, before_keyword);
566 lx.seek(before_keyword);
567 }
568}
569
570#[derive(Debug, Clone, PartialEq)]
572pub struct Indirect {
573 pub num: u32,
575 pub generation: u16,
577 pub object: Object,
579}
580
581pub fn parse_indirect_object<R: Resolve + ?Sized>(
591 lx: &mut Lexer<'_>,
592 limits: &Limits,
593 diags: &mut Diagnostics,
594 strictness: Strictness,
595 store: &R,
596) -> Result<Indirect, Error> {
597 let mut ctx = Context {
598 limits,
599 diags,
600 file: None,
601 store: Some(store),
602 };
603 indirect(lx, &mut ctx, strictness, 0)
604}
605
606pub(crate) fn indirect<R: Resolve + ?Sized>(
608 lx: &mut Lexer<'_>,
609 ctx: &mut Context<'_, R>,
610 strictness: Strictness,
611 depth: u32,
612) -> Result<Indirect, Error> {
613 let start = lx.pos();
614 let fail = |lx: &mut Lexer<'_>| {
615 lx.seek(start);
616 Err(Error::NoObject(start as u64))
617 };
618
619 let Token::Number(num_word) = lx.next_word(ctx.limits) else {
620 return fail(lx);
621 };
622 let Token::Number(gen_word) = lx.next_word(ctx.limits) else {
623 return fail(lx);
624 };
625 if !lx.next_word(ctx.limits).is_keyword(b"obj") {
626 return fail(lx);
627 }
628
629 let object = match body(lx, ctx, strictness, depth) {
630 Ok(o) => o,
631 Err(_) if strictness == Strictness::Loose => Object::Null,
632 Err(e) => return Err(e),
633 };
634
635 Ok(Indirect {
636 num: atoui(num_word),
637 generation: u16::try_from(atoui(gen_word)).unwrap_or(u16::MAX),
638 object,
639 })
640}
641
642#[cfg(test)]
643mod tests {
644 use super::{Context, Strictness, body, indirect, parse_object};
645 use crate::error::Error;
646 use crate::lexer::Lexer;
647 use pdfrum_common::{DiagKind, Diagnostics, Limits};
648 use pdfrum_object::ByteSpan;
649 use pdfrum_object::{NoResolve, ObjRef, Object, Resolve, names};
650 use std::sync::Arc;
651
652 fn parse(input: &[u8]) -> Result<Object, Error> {
653 let mut diags = Diagnostics::default();
654 parse_object(
655 &mut Lexer::new(input),
656 &Limits::default(),
657 &mut diags,
658 Strictness::Loose,
659 &NoResolve,
660 )
661 }
662
663 fn parse_strict(input: &[u8]) -> Result<Object, Error> {
664 let mut diags = Diagnostics::default();
665 parse_object(
666 &mut Lexer::new(input),
667 &Limits::default(),
668 &mut diags,
669 Strictness::Strict,
670 &NoResolve,
671 )
672 }
673
674 struct Store(std::collections::HashMap<u32, Arc<Object>>);
677
678 impl Resolve for Store {
679 fn fetch(&self, r: ObjRef) -> Result<Arc<Object>, pdfrum_object::Error> {
680 self.0
681 .get(&r.num)
682 .cloned()
683 .ok_or(pdfrum_object::Error::UnresolvedRef(r))
684 }
685 }
686
687 fn parse_in_file<R: Resolve + ?Sized>(
689 input: &[u8],
690 store: &R,
691 diags: &mut Diagnostics,
692 ) -> Result<Object, Error> {
693 let file = ByteSpan::from(input.to_vec());
694 let limits = Limits::default();
695 let mut ctx = Context {
696 limits: &limits,
697 diags,
698 file: Some(&file),
699 store: Some(store),
700 };
701 let mut lx = Lexer::new(&file);
702 body(&mut lx, &mut ctx, Strictness::Loose, 0)
703 }
704
705 #[test]
706 fn scalars() {
707 assert_eq!(parse(b"true"), Ok(Object::Bool(true)));
708 assert_eq!(parse(b"false"), Ok(Object::Bool(false)));
709 assert_eq!(parse(b"null"), Ok(Object::Null));
710 assert_eq!(parse(b"42"), Ok(Object::Int(42)));
711 assert_eq!(parse(b"-17"), Ok(Object::Int(-17)));
712 assert_eq!(parse(b"3.5"), Ok(Object::Real(3.5)));
713 assert_eq!(parse(b"-0.25"), Ok(Object::Real(-0.25)));
714 assert_eq!(parse(b"1.2.3"), Ok(Object::Real(1.2)));
716 assert_eq!(parse(b"--37"), Ok(Object::Int(0)));
717 }
718
719 #[test]
729 fn integers_outside_the_c_int_range_are_zero() {
730 use pdfrum_object::INT_RANGE;
731
732 assert_eq!(parse(b"4294967295"), Ok(Object::Int(4_294_967_295)));
734 assert_eq!(parse(b"4294967296"), Ok(Object::Int(0)));
735 assert_eq!(parse(b"99999999999999999999999999"), Ok(Object::Int(0)));
736
737 assert_eq!(parse(b"+2147483647"), Ok(Object::Int(2_147_483_647)));
739 assert_eq!(parse(b"+2147483648"), Ok(Object::Int(0)));
740 assert_eq!(parse(b"+4294967295"), Ok(Object::Int(0)));
741 assert_eq!(parse(b"-2147483648"), Ok(Object::Int(-2_147_483_648)));
743 assert_eq!(parse(b"-2147483649"), Ok(Object::Int(0)));
744 assert_eq!(parse(b"-99999999999999999999"), Ok(Object::Int(0)));
745
746 for spelling in [
748 &b"0"[..],
749 b"-0",
750 b"+0",
751 b"2147483647",
752 b"2147483648",
753 b"4294967295",
754 b"4294967296",
755 b"-2147483648",
756 b"-2147483649",
757 b"18446744073709551616",
758 b"999999999999999999999999999999",
759 b"-999999999999999999999999999999",
760 ] {
761 let Ok(Object::Int(v)) = parse(spelling) else {
762 panic!(
763 "{} did not parse as an integer",
764 String::from_utf8_lossy(spelling)
765 );
766 };
767 assert!(
768 INT_RANGE.contains(&v),
769 "{} parsed to {v}, outside INT_RANGE",
770 String::from_utf8_lossy(spelling)
771 );
772 let _ = pdfrum_object::narrow_to_signed32(v);
774 }
775 }
776
777 #[test]
778 fn names_decode_escapes() {
779 assert_eq!(
780 parse(b"/A#20B"),
781 Ok(Object::Name(pdfrum_object::Name::from("A B")))
782 );
783 assert_eq!(parse(b"/"), Ok(Object::Name(pdfrum_object::Name::from(""))));
784 }
785
786 #[test]
787 fn references_and_the_invalid_one() {
788 assert_eq!(parse(b"12 0 R"), Ok(Object::Ref(ObjRef::new(12, 0))));
789 assert_eq!(parse(b"3 7 R"), Ok(Object::Ref(ObjRef::new(3, 7))));
790 assert_eq!(parse(b"0 0 R"), Ok(Object::Ref(ObjRef::new(0, 0))));
792 assert!(parse(b"4294967295 0 R").is_err());
794 let mut lx = Lexer::new(b"12 0 X");
796 let mut diags = Diagnostics::default();
797 let obj = parse_object(
798 &mut lx,
799 &Limits::default(),
800 &mut diags,
801 Strictness::Loose,
802 &NoResolve,
803 );
804 assert_eq!(obj, Ok(Object::Int(12)));
805 assert_eq!(lx.pos(), 2);
806 }
807
808 #[test]
809 fn arrays() {
810 let obj = parse(b"[1 2 3]").expect("array");
811 let array = obj.as_array().expect("array");
812 assert_eq!(array.len(), 3);
813 assert_eq!(array.int_at(2), Some(3));
814 assert!(parse(b"[]").expect("array").as_array().is_some());
815 }
816
817 #[test]
818 fn a_loose_array_keeps_what_it_read() {
819 let mut diags = Diagnostics::default();
820 let obj = parse_object(
821 &mut Lexer::new(b"[1 2 endobj"),
822 &Limits::default(),
823 &mut diags,
824 Strictness::Loose,
825 &NoResolve,
826 )
827 .expect("partial array");
828 assert_eq!(obj.as_array().expect("array").len(), 2);
829 assert!(diags.contains(&DiagKind::MalformedArray));
830 }
831
832 #[test]
833 fn a_strict_array_needs_its_bracket() {
834 assert!(parse_strict(b"[1 2 endobj").is_err());
835 assert!(parse_strict(b"[1 2]").is_ok());
836 }
837
838 #[test]
839 fn dictionaries() {
840 let obj = parse(b"<< /Type /Page /Count 3 >>").expect("dict");
841 let dict = obj.as_dict().expect("dict");
842 assert_eq!(dict.name(names::TYPE), Some(names::PAGE));
843 assert_eq!(dict.direct_int(names::COUNT), Some(3));
844 }
845
846 #[test]
847 fn a_later_duplicate_key_wins() {
848 let obj = parse(b"<< /Size 3 /Size -1 >>").expect("dict");
849 assert_eq!(
850 obj.as_dict().expect("dict").direct_int(names::SIZE),
851 Some(-1)
852 );
853 }
854
855 #[test]
856 fn endobj_closes_an_unterminated_dictionary() {
857 let mut diags = Diagnostics::default();
858 let mut lx = Lexer::new(b"<< /A 1 endobj");
859 let obj = parse_object(
860 &mut lx,
861 &Limits::default(),
862 &mut diags,
863 Strictness::Loose,
864 &NoResolve,
865 )
866 .expect("dict");
867 assert_eq!(obj.as_dict().expect("dict").len(), 1);
868 assert!(diags.contains(&DiagKind::MalformedDict));
869 assert_eq!(lx.next_word(&Limits::default()).bytes(), b"endobj");
871 }
872
873 #[test]
874 fn junk_keys_are_skipped_and_shift_the_pairs() {
875 let obj = parse(b"<< junk /A 1 >>").expect("dict");
876 let dict = obj.as_dict().expect("dict");
877 assert_eq!(dict.len(), 1);
878 assert_eq!(dict.direct_int(&pdfrum_object::Name::from("A")), Some(1));
879 }
880
881 #[test]
882 fn a_bare_slash_key_is_dropped() {
883 let obj = parse(b"<< / 1 /A 2 >>").expect("dict");
884 assert_eq!(obj.as_dict().expect("dict").len(), 1);
885 }
886
887 #[test]
888 fn nesting_past_the_budget_loses_the_middle_not_the_object() {
889 fn depth_of(o: &Object) -> usize {
891 match o {
892 Object::Array(a) => 1 + a.iter().map(depth_of).max().unwrap_or(0),
893 _ => 0,
894 }
895 }
896 let nested = |n: usize| -> Vec<u8> {
897 let mut v: Vec<u8> = std::iter::repeat_n(b'[', n).collect();
898 v.extend(std::iter::repeat_n(b']', n));
899 v
900 };
901
902 assert_eq!(depth_of(&parse(&nested(63)).expect("array")), 63);
904 assert_eq!(depth_of(&parse(&nested(64)).expect("array")), 64);
905 assert_eq!(depth_of(&parse(&nested(65)).expect("array")), 64);
908 assert_eq!(depth_of(&parse(&nested(500)).expect("array")), 64);
909 }
910
911 #[test]
912 fn a_stream_reads_its_declared_length() {
913 let mut diags = Diagnostics::default();
914 let obj = parse_in_file(
915 b"<< /Length 5 >>\nstream\nHELLO\nendstream\nendobj\n",
916 &NoResolve,
917 &mut diags,
918 )
919 .expect("stream");
920 assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
921 assert!(!diags.contains(&DiagKind::LengthMismatch));
922 }
923
924 #[test]
925 fn a_wrong_length_falls_back_to_the_keyword() {
926 let mut diags = Diagnostics::default();
927 let obj = parse_in_file(
928 b"<< /Length 2 >>\nstream\nHELLO\nendstream\nendobj\n",
929 &NoResolve,
930 &mut diags,
931 )
932 .expect("stream");
933 assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
934 assert!(diags.contains(&DiagKind::LengthMismatch));
935 assert!(diags.contains(&DiagKind::KeywordResync));
936 }
937
938 #[test]
939 fn a_length_past_the_file_falls_back() {
940 let mut diags = Diagnostics::default();
941 let obj = parse_in_file(
942 b"<< /Length 9999 >>\nstream\nHELLO\nendstream\nendobj\n",
943 &NoResolve,
944 &mut diags,
945 )
946 .expect("stream");
947 assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
948 }
949
950 #[test]
951 fn endstream_is_matched_by_prefix() {
952 let mut diags = Diagnostics::default();
953 let obj = parse_in_file(
954 b"<< /Length 5 >>\nstream\nHELLO\nendstreamXYZ\nendobj\n",
955 &NoResolve,
956 &mut diags,
957 )
958 .expect("stream");
959 assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
960 assert!(!diags.contains(&DiagKind::LengthMismatch));
961 }
962
963 #[test]
964 fn a_space_before_the_newline_still_resyncs() {
965 let mut diags = Diagnostics::default();
968 let obj = parse_in_file(
969 b"<< /Length 99 >>\nstream\nHELLO\nendobj \n",
970 &NoResolve,
971 &mut diags,
972 )
973 .expect("stream");
974 assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
975 assert!(diags.contains(&DiagKind::KeywordResync));
976 }
977
978 #[test]
979 fn a_missing_endstream_resyncs_on_endobj() {
980 let mut diags = Diagnostics::default();
981 let obj = parse_in_file(
982 b"<< /Length 99 >>\nstream\nHELLO\nendobj\n",
983 &NoResolve,
984 &mut diags,
985 )
986 .expect("stream");
987 assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
988 assert!(diags.contains(&DiagKind::KeywordResync));
989 }
990
991 #[test]
992 fn a_delimiter_disqualifies_an_endstream_match() {
993 let mut diags = Diagnostics::default();
996 let obj = parse_in_file(
997 b"<< /Length 999 >>\nstream\nA>>endstream\nB\nendstream\nendobj\n",
998 &NoResolve,
999 &mut diags,
1000 )
1001 .expect("stream");
1002 assert_eq!(&*obj.as_stream().expect("stream").data, b"A>>endstream\nB");
1003 }
1004
1005 #[test]
1006 fn a_zero_length_stream_is_legal() {
1007 let mut diags = Diagnostics::default();
1008 let obj = parse_in_file(
1009 b"<< /Length 0 >>\nstream\nendstream\nendobj\n",
1010 &NoResolve,
1011 &mut diags,
1012 )
1013 .expect("stream");
1014 assert!(obj.as_stream().expect("stream").data.is_empty());
1015 }
1016
1017 #[test]
1018 fn an_indirect_length_is_chased() {
1019 let store = Store([(9u32, Arc::new(Object::Int(5)))].into_iter().collect());
1020 let mut diags = Diagnostics::default();
1021 let obj = parse_in_file(
1022 b"<< /Length 9 0 R >>\nstream\nHELLO\nendstream\nendobj\n",
1023 &store,
1024 &mut diags,
1025 )
1026 .expect("stream");
1027 assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
1028 assert!(!diags.contains(&DiagKind::LengthMismatch));
1029 }
1030
1031 #[test]
1032 fn an_unresolvable_length_falls_back_to_the_scan() {
1033 let mut diags = Diagnostics::default();
1034 let obj = parse_in_file(
1035 b"<< /Length 9 0 R >>\nstream\nHELLO\nendstream\nendobj\n",
1036 &NoResolve,
1037 &mut diags,
1038 )
1039 .expect("stream");
1040 assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
1041 assert!(diags.contains(&DiagKind::KeywordResync));
1042 }
1043
1044 #[test]
1045 fn streams_are_dropped_from_composites() {
1046 let mut diags = Diagnostics::default();
1047 let obj = parse_in_file(
1048 b"[ 1 << /Length 5 >>\nstream\nHELLO\nendstream\n 2 ]",
1049 &NoResolve,
1050 &mut diags,
1051 )
1052 .expect("array");
1053 let array = obj.as_array().expect("array");
1054 assert_eq!(array.len(), 2);
1055 assert_eq!(array.int_at(0), Some(1));
1056 assert_eq!(array.int_at(1), Some(2));
1057 assert!(diags.contains(&DiagKind::StreamInCompositeDropped));
1058 }
1059
1060 #[test]
1061 fn indirect_frames_need_their_header() {
1062 let file = ByteSpan::from(b"7 0 obj << /A 1 >> endobj".to_vec());
1063 let limits = Limits::default();
1064 let mut diags = Diagnostics::default();
1065 let mut ctx = Context {
1066 limits: &limits,
1067 diags: &mut diags,
1068 file: Some(&file),
1069 store: Some(&NoResolve),
1070 };
1071 let mut lx = Lexer::new(&file);
1072 let parsed = indirect(&mut lx, &mut ctx, Strictness::Loose, 0).expect("indirect");
1073 assert_eq!(parsed.num, 7);
1074 assert_eq!(parsed.generation, 0);
1075 assert!(parsed.object.as_dict().is_some());
1076
1077 let mut lx = Lexer::new(b"7 0 <<>>");
1079 assert!(
1080 indirect(
1081 &mut lx,
1082 &mut Context {
1083 limits: &limits,
1084 diags: &mut Diagnostics::default(),
1085 file: None,
1086 store: Some(&NoResolve),
1087 },
1088 Strictness::Loose,
1089 0,
1090 )
1091 .is_err()
1092 );
1093 assert_eq!(lx.pos(), 0);
1094 }
1095
1096 #[test]
1097 fn never_panics_on_arbitrary_bytes() {
1098 let seeds: &[&[u8]] = &[
1099 b"<<<<<<<<",
1100 b">>>>>>>>",
1101 b"[[[[[[[[",
1102 b"((((((((",
1103 b"<<<</Length -1>>stream",
1104 b"0 0 obj<</Length 99999999999999999999>>stream\n",
1105 b"<</A",
1106 b"/",
1107 b"\xff\xfe\x00\x80",
1108 b"1 0 R 2 0 R",
1109 b"<</Length 1 0 R>>stream\nx",
1110 ];
1111 for seed in seeds {
1112 let _ = parse(seed);
1113 let _ = parse_strict(seed);
1114 let _ = parse_in_file(seed, &NoResolve, &mut Diagnostics::default());
1115 }
1116 }
1117}