1use std::collections::{HashMap, HashSet};
7use std::path::Path;
8use std::sync::Arc;
9
10use bytes::Bytes;
11use pdfboss_core::elements::{ElementOpts, Span, XrefKind};
12use pdfboss_core::lexer::{Lexer, Token};
13use pdfboss_core::object::decode_text_string;
14use pdfboss_core::parser::{NoResolve, Parser, Resolve};
15use pdfboss_core::xref::XrefEntry;
16use pdfboss_core::{AsyncObjectSource, Dict, Metadata, ObjRef, Object, Page, Stream};
17
18use crate::backend::{Backend, BoxFuture, FileBackend, MemBackend};
19use crate::cache::CachedBackend;
20use crate::error::{Error, Result};
21
22const TAIL_WINDOW: u64 = 4096;
24const MAX_TAIL_WINDOW: u64 = 64 * 1024;
26
27const MAX_GROWTH_WINDOW: u64 = 256 * 1024 * 1024;
50
51pub(crate) struct Fetcher {
53 pub(crate) backend: Arc<dyn Backend>,
54 pub(crate) len: u64,
55}
56
57impl Fetcher {
58 pub(crate) async fn read_range(&self, start: u64, end: u64) -> Result<Vec<u8>> {
62 let wanted = usize::try_from(end.saturating_sub(start)).map_err(|overflow| {
63 Error::Core(pdfboss_core::Error::Other(format!(
64 "range {start}..{end} does not fit this platform: {overflow}"
65 )))
66 })?;
67 let mut buf = vec![0u8; wanted];
68 let mut filled = 0;
69 while filled < wanted {
70 let got = self
71 .backend
72 .read_at(start + filled as u64, &mut buf[filled..])
73 .await
74 .map_err(Error::from)?;
75 if got == 0 {
76 return Err(Error::TruncatedRead {
77 offset: start,
78 wanted,
79 got: filled,
80 });
81 }
82 filled += got;
83 }
84 Ok(buf)
85 }
86
87 pub(crate) async fn window(&self, offset: u64, window: usize) -> Result<Vec<u8>> {
90 let end = self.len.min(offset.saturating_add(window as u64));
91 if offset >= end {
92 return Ok(Vec::new());
93 }
94 self.read_range(offset, end).await
95 }
96}
97
98pub(crate) fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
100 if needle.is_empty() || haystack.len() < needle.len() {
101 return None;
102 }
103 haystack.windows(needle.len()).position(|w| w == needle)
104}
105
106pub(crate) fn rfind_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
108 if needle.is_empty() || haystack.len() < needle.len() {
109 return None;
110 }
111 haystack.windows(needle.len()).rposition(|w| w == needle)
112}
113
114pub(crate) struct StartXrefRecord {
116 pub(crate) offset: u64,
118 pub(crate) span: Span,
122}
123
124pub(crate) async fn find_tail(fetcher: &Fetcher) -> Result<(StartXrefRecord, Option<Span>)> {
130 let mut window = TAIL_WINDOW;
131 loop {
132 let start = fetcher.len.saturating_sub(window);
133 let tail = fetcher.read_range(start, fetcher.len).await?;
134 if let Some(rel) = rfind_bytes(&tail, b"startxref") {
135 let mut lexer = Lexer::at(&tail, rel + b"startxref".len());
136 if let Ok(Token::Int(value)) = lexer.next_token() {
137 if value >= 0 && (value as u64) < fetcher.len {
138 let record = StartXrefRecord {
139 offset: value as u64,
140 span: Span {
141 start: start + rel as u64,
142 end: start + lexer.pos() as u64,
143 },
144 };
145 let eof = rfind_bytes(&tail, b"%%EOF").map(|pos| Span {
146 start: start + pos as u64,
147 end: start + pos as u64 + 5,
148 });
149 return Ok((record, eof));
150 }
151 }
152 }
153 if window >= fetcher.len || window >= MAX_TAIL_WINDOW {
154 return Err(Error::Core(pdfboss_core::Error::InvalidXref));
155 }
156 window *= 2;
157 }
158}
159
160pub(crate) fn parse_version(head: &[u8]) -> (u8, u8) {
164 try_parse_version(head).unwrap_or((1, 4))
165}
166
167fn try_parse_version(head: &[u8]) -> Option<(u8, u8)> {
168 let window = &head[..head.len().min(1024)];
169 let pos = find_bytes(window, b"%PDF-")?;
170 let rest = &window[pos + 5..];
171 let (major, used) = read_version_component(rest)?;
172 if rest.get(used) != Some(&b'.') {
173 return None;
174 }
175 let minor = read_version_component(&rest[used + 1..])?.0;
176 Some((major, minor))
177}
178
179fn read_version_component(bytes: &[u8]) -> Option<(u8, usize)> {
182 let end = bytes
183 .iter()
184 .position(|b| !b.is_ascii_digit())
185 .unwrap_or(bytes.len());
186 if end == 0 || end > 3 {
187 return None;
188 }
189 let value = std::str::from_utf8(&bytes[..end]).ok()?.parse().ok()?;
190 Some((value, end))
191}
192
193pub(crate) fn header_span_in(head: &[u8]) -> Option<Span> {
198 let window = &head[..head.len().min(1024)];
199 let pos = find_bytes(window, b"%PDF-")?;
200 let version_end = window[pos + 5..]
201 .iter()
202 .position(|&b| !(b.is_ascii_digit() || b == b'.'))
203 .map(|rel| pos + 5 + rel)
204 .unwrap_or(window.len());
205 Some(Span {
206 start: pos as u64,
207 end: version_end as u64,
208 })
209}
210
211pub(crate) const PARSE_SLACK: usize = 16;
215
216#[derive(Clone, Debug)]
220pub(crate) struct PageRecord {
221 pub(crate) r: Option<ObjRef>,
225 pub(crate) dict: Dict,
228 pub(crate) resources: Dict,
231 pub(crate) media_box: Option<pdfboss_core::Rect>,
236 pub(crate) crop_box: Option<pdfboss_core::Rect>,
238 pub(crate) bleed_box: Option<pdfboss_core::Rect>,
241 pub(crate) trim_box: Option<pdfboss_core::Rect>,
243 pub(crate) art_box: Option<pdfboss_core::Rect>,
245 pub(crate) rotate: Option<i32>,
247}
248
249#[derive(Clone, Default)]
252struct InheritedAttrs {
253 resources: Dict,
254 media_box: Option<pdfboss_core::Rect>,
255 crop_box: Option<pdfboss_core::Rect>,
256 rotate: Option<i32>,
257}
258
259const MAX_TREE_DEPTH: usize = 256;
261
262#[derive(Clone)]
264pub(crate) struct SectionRecord {
265 pub(crate) kind: XrefKind,
268 pub(crate) span: Span,
271 pub(crate) entries: usize,
273 pub(crate) trailer_dict: Dict,
276 pub(crate) trailer_span: Span,
279}
280
281pub(crate) struct ParsedSection {
283 pub(crate) record: SectionRecord,
284 pub(crate) entries: Vec<(u32, XrefEntry)>,
285 pub(crate) prev: Option<u64>,
286 pub(crate) xrefstm: Option<u64>,
287}
288
289pub(crate) fn parse_section_window(
295 buf: &[u8],
296 base: u64,
297 file_len: u64,
298 at_eof: bool,
299) -> Result<Option<ParsedSection>> {
300 let mut probe = Lexer::new(buf);
301 let classic = matches!(probe.peek_token(),
302 Ok(Token::Keyword(ref k)) if k.as_slice() == b"xref");
303 if classic {
304 parse_classic_window(buf, base, file_len, at_eof)
305 } else {
306 parse_stream_window(buf, base, at_eof)
307 }
308}
309
310fn parse_classic_window(
314 buf: &[u8],
315 base: u64,
316 file_len: u64,
317 at_eof: bool,
318) -> Result<Option<ParsedSection>> {
319 fn incomplete<T>(at_eof: bool) -> Result<Option<T>> {
322 if at_eof {
323 Err(Error::Core(pdfboss_core::Error::InvalidXref))
324 } else {
325 Ok(None)
326 }
327 }
328
329 let mut lexer = Lexer::new(buf);
330 match lexer.next_token() {
331 Ok(Token::Keyword(ref k)) if k.as_slice() == b"xref" => {}
332 _ => return Err(Error::Core(pdfboss_core::Error::InvalidXref)),
333 }
334 let mut entries: Vec<(u32, XrefEntry)> = Vec::new();
335 loop {
336 lexer.skip_whitespace_and_comments();
337 let keyword_start = lexer.pos();
338 let token = match lexer.next_token() {
339 Ok(t) => t,
340 Err(_) => return incomplete(at_eof),
341 };
342 match token {
343 Token::Int(start) if start >= 0 => {
344 let count = match lexer.next_token() {
345 Ok(Token::Int(c)) if c >= 0 => c as u64,
346 Ok(Token::Eof) => return incomplete(at_eof),
347 Ok(_) => return Err(Error::Core(pdfboss_core::Error::InvalidXref)),
348 Err(_) => return incomplete(at_eof),
349 };
350 if count > file_len / 11 + 1 {
354 return Err(Error::Core(pdfboss_core::Error::InvalidXref));
355 }
356 for i in 0..count {
357 let field1 = match lexer.next_token() {
358 Ok(Token::Int(v)) if v >= 0 => v as u64,
359 Ok(Token::Eof) => return incomplete(at_eof),
360 Ok(_) => return Err(Error::Core(pdfboss_core::Error::InvalidXref)),
361 Err(_) => return incomplete(at_eof),
362 };
363 let field2 = match lexer.next_token() {
364 Ok(Token::Int(v)) if v >= 0 => v,
365 Ok(Token::Eof) => return incomplete(at_eof),
366 Ok(_) => return Err(Error::Core(pdfboss_core::Error::InvalidXref)),
367 Err(_) => return incomplete(at_eof),
368 };
369 let entry = match lexer.next_token() {
370 Ok(Token::Keyword(ref k)) if k.as_slice() == b"n" => XrefEntry::InFile {
371 offset: field1,
372 gen: field2.min(65535) as u16,
373 },
374 Ok(Token::Keyword(ref k)) if k.as_slice() == b"f" => XrefEntry::Free,
375 Ok(Token::Eof) => return incomplete(at_eof),
376 Ok(_) => return Err(Error::Core(pdfboss_core::Error::InvalidXref)),
377 Err(_) => return incomplete(at_eof),
378 };
379 if let Ok(num) = u32::try_from(start as u64 + i) {
380 entries.push((num, entry));
381 }
382 }
383 if lexer.pos() + PARSE_SLACK > buf.len() && !at_eof {
386 return Ok(None);
387 }
388 }
389 Token::Keyword(ref k) if k.as_slice() == b"trailer" => {
390 let mut parser = Parser::at(buf, lexer.pos());
391 let trailer_dict = match parser.parse_object(&NoResolve) {
392 Ok(Object::Dict(d)) => d,
393 Ok(_) => return Err(Error::Core(pdfboss_core::Error::InvalidXref)),
394 Err(_) => return incomplete(at_eof),
395 };
396 if parser.pos() + PARSE_SLACK > buf.len() && !at_eof {
397 return Ok(None); }
399 let prev = trailer_dict.get_int("Prev").and_then(non_negative);
400 let xrefstm = trailer_dict.get_int("XRefStm").and_then(non_negative);
401 let entry_count = entries.len();
402 return Ok(Some(ParsedSection {
403 record: SectionRecord {
404 kind: XrefKind::Table,
405 span: Span {
406 start: base,
407 end: base + keyword_start as u64,
408 },
409 entries: entry_count,
410 trailer_dict,
411 trailer_span: Span {
412 start: base + keyword_start as u64,
413 end: base + parser.pos() as u64,
414 },
415 },
416 entries,
417 prev,
418 xrefstm,
419 }));
420 }
421 Token::Eof => return incomplete(at_eof),
422 _ => return Err(Error::Core(pdfboss_core::Error::InvalidXref)),
423 }
424 }
425}
426
427fn parse_stream_window(buf: &[u8], base: u64, at_eof: bool) -> Result<Option<ParsedSection>> {
432 let mut parser = Parser::at(buf, 0);
433 let stream = match parser.parse_indirect(&NoResolve) {
434 Ok((_, Object::Stream(s))) => s,
435 Ok(_) => {
441 if !at_eof && parser.pos() + PARSE_SLACK > buf.len() {
442 return Ok(None);
443 }
444 return Err(Error::Core(pdfboss_core::Error::InvalidXref));
445 }
446 Err(_) if !at_eof => return Ok(None),
447 Err(_) => return Err(Error::Core(pdfboss_core::Error::InvalidXref)),
448 };
449 if parser.pos() + PARSE_SLACK > buf.len() && !at_eof {
450 return Ok(None); }
452 match stream.dict.get_int("Length") {
458 Some(declared) if declared >= 0 => {
459 if stream.data.len() as u64 != declared as u64 && !at_eof {
460 return Ok(None);
461 }
462 }
463 _ => {
464 if !at_eof {
465 return Ok(None);
466 }
467 }
468 }
469 let decoded = pdfboss_core::filters::decode_stream(&stream, &NoResolve)
470 .map_err(|_| Error::Core(pdfboss_core::Error::InvalidXref))?;
471 let dict = stream.dict;
472 let widths: Vec<usize> = dict
473 .get_array("W")
474 .ok_or(Error::Core(pdfboss_core::Error::InvalidXref))?
475 .iter()
476 .map(|v| {
477 v.as_int()
478 .filter(|&n| (0..=8).contains(&n))
479 .map(|n| n as usize)
480 })
481 .collect::<Option<Vec<_>>>()
482 .ok_or(Error::Core(pdfboss_core::Error::InvalidXref))?;
483 let w1 = widths.first().copied().unwrap_or(0);
484 let w2 = widths.get(1).copied().unwrap_or(0);
485 let w3 = widths.get(2).copied().unwrap_or(0);
486 let entry_len = w1 + w2 + w3;
487 if entry_len == 0 {
488 return Err(Error::Core(pdfboss_core::Error::InvalidXref));
489 }
490 let size = dict.get_int("Size").unwrap_or(0).max(0) as u64;
491 let subsections: Vec<(u64, u64)> = match dict.get_array("Index") {
492 Some(index) => index
493 .chunks(2)
494 .filter_map(|pair| {
495 let start = pair.first()?.as_int()?;
496 let count = pair.get(1)?.as_int()?;
497 (start >= 0 && count >= 0).then_some((start as u64, count as u64))
498 })
499 .collect(),
500 None => vec![(0, size)],
501 };
502 let mut entries: Vec<(u32, XrefEntry)> = Vec::new();
503 let mut pos = 0usize;
504 'subsections: for (start, count) in subsections {
505 for i in 0..count {
506 if pos + entry_len > decoded.len() {
507 break 'subsections; }
509 let kind = if w1 == 0 {
510 1
511 } else {
512 read_be(&decoded[pos..pos + w1])
513 };
514 let field2 = read_be(&decoded[pos + w1..pos + w1 + w2]);
515 let field3 = read_be(&decoded[pos + w1 + w2..pos + entry_len]);
516 pos += entry_len;
517 let entry = match kind {
518 1 => XrefEntry::InFile {
519 offset: field2,
520 gen: field3.min(65535) as u16,
521 },
522 2 => match (u32::try_from(field2), u32::try_from(field3)) {
523 (Ok(stream_num), Ok(index)) => XrefEntry::InStream { stream_num, index },
524 _ => XrefEntry::Free,
525 },
526 _ => XrefEntry::Free,
529 };
530 if let Ok(num) = u32::try_from(start + i) {
531 entries.push((num, entry));
532 }
533 }
534 }
535 let prev = dict.get_int("Prev").and_then(non_negative);
536 let span = Span {
537 start: base,
538 end: base + parser.pos() as u64,
539 };
540 let entry_count = entries.len();
541 Ok(Some(ParsedSection {
542 record: SectionRecord {
543 kind: XrefKind::Stream,
544 span,
545 entries: entry_count,
546 trailer_dict: dict,
547 trailer_span: span,
548 },
549 entries,
550 prev,
551 xrefstm: None,
552 }))
553}
554
555fn read_be(bytes: &[u8]) -> u64 {
557 bytes.iter().fold(0, |acc, &b| (acc << 8) | u64::from(b))
558}
559
560fn non_negative(value: i64) -> Option<u64> {
562 u64::try_from(value).ok()
563}
564
565pub(crate) struct XrefIndex {
568 pub(crate) entries: HashMap<u32, XrefEntry>,
569 pub(crate) trailer: Dict,
573 pub(crate) trailer_span: Span,
577}
578
579#[derive(Clone)]
586pub struct AsyncDocument {
587 pub(crate) inner: Arc<DocumentInner>,
588}
589
590pub(crate) struct DocumentInner {
591 pub(crate) backend: Arc<dyn Backend>,
592 pub(crate) file_len: u64,
593 pub(crate) version: (u8, u8),
594 pub(crate) header_span: Option<Span>,
599 pub(crate) xref: XrefIndex,
600 pub(crate) sections: Vec<SectionRecord>,
604 pub(crate) startxref: StartXrefRecord,
607 pub(crate) eof_span: Option<Span>,
610 pub(crate) objects: std::sync::Mutex<HashMap<(u32, u16), Arc<Object>>>,
612 pub(crate) objstms: tokio::sync::Mutex<HashMap<u32, Arc<ObjStmCache>>>,
617 pub(crate) pages: std::sync::OnceLock<Vec<PageRecord>>,
620 pub(crate) decryptor: std::sync::OnceLock<pdfboss_core::Decryptor>,
626}
627
628impl AsyncDocument {
629 pub async fn open(path: impl AsRef<Path>) -> Result<AsyncDocument> {
637 AsyncDocument::open_with_password(path, "").await
638 }
639
640 pub async fn open_with_password(
644 path: impl AsRef<Path>,
645 password: &str,
646 ) -> Result<AsyncDocument> {
647 let backend = FileBackend::open(path).await.map_err(Error::from)?;
648 AsyncDocument::from_arc(Arc::new(CachedBackend::new(backend)), password).await
649 }
650
651 pub async fn from_bytes(bytes: impl Into<Bytes>) -> Result<AsyncDocument> {
657 AsyncDocument::from_bytes_with_password(bytes, "").await
658 }
659
660 pub async fn from_bytes_with_password(
663 bytes: impl Into<Bytes>,
664 password: &str,
665 ) -> Result<AsyncDocument> {
666 AsyncDocument::from_arc(Arc::new(MemBackend::from(bytes.into())), password).await
667 }
668
669 pub async fn with_backend(backend: impl Backend) -> Result<AsyncDocument> {
675 AsyncDocument::from_arc(Arc::new(backend), "").await
676 }
677
678 pub async fn with_backend_with_password(
681 backend: impl Backend,
682 password: &str,
683 ) -> Result<AsyncDocument> {
684 AsyncDocument::from_arc(Arc::new(backend), password).await
685 }
686
687 #[cfg(feature = "http")]
694 pub async fn open_url(url: impl reqwest::IntoUrl) -> Result<AsyncDocument> {
695 AsyncDocument::open_url_with_password(url, "").await
696 }
697
698 #[cfg(feature = "http")]
701 pub async fn open_url_with_password(
702 url: impl reqwest::IntoUrl,
703 password: &str,
704 ) -> Result<AsyncDocument> {
705 let backend = crate::backend::HttpBackend::new(url).await?;
706 AsyncDocument::from_arc(Arc::new(CachedBackend::new(backend)), password).await
707 }
708
709 async fn from_arc(backend: Arc<dyn Backend>, password: &str) -> Result<AsyncDocument> {
711 let file_len = backend.len().await.map_err(Error::from)?;
712 let fetcher = Fetcher {
713 backend: Arc::clone(&backend),
714 len: file_len,
715 };
716 let head = fetcher.window(0, 1024).await?;
717 let version = parse_version(&head);
718 let header_span = header_span_in(&head);
719 let (startxref, eof_span) = find_tail(&fetcher).await?;
720 let (xref, sections) = load_xref_chain(&fetcher, startxref.offset).await?;
721 let encrypted = xref.trailer.get("Encrypt").is_some_and(|o| !o.is_null());
722 let inner = DocumentInner {
723 backend,
724 file_len,
725 version,
726 header_span,
727 xref,
728 sections,
729 startxref,
730 eof_span,
731 objects: std::sync::Mutex::new(HashMap::new()),
732 objstms: tokio::sync::Mutex::new(HashMap::new()),
733 pages: std::sync::OnceLock::new(),
734 decryptor: std::sync::OnceLock::new(),
735 };
736 let doc = AsyncDocument {
737 inner: Arc::new(inner),
738 };
739 if encrypted {
740 doc.setup_decryption(password).await?;
741 }
742 let pages = doc.flatten_pages().await;
743 doc.inner
744 .pages
745 .set(pages)
746 .expect("page index is set exactly once at open");
747 Ok(doc)
748 }
749
750 async fn setup_decryption(&self, password: &str) -> Result<()> {
759 let enc_obj = self
760 .inner
761 .xref
762 .trailer
763 .get("Encrypt")
764 .cloned()
765 .unwrap_or(Object::Null);
766 let enc = self.resolve(&enc_obj).await?;
767 let enc_dict = enc
768 .as_dict()
769 .ok_or(Error::Core(pdfboss_core::Error::Encrypted))?;
770 let id0: Vec<u8> = self
771 .inner
772 .xref
773 .trailer
774 .get("ID")
775 .and_then(Object::as_array)
776 .and_then(<[Object]>::first)
777 .and_then(Object::as_str_bytes)
778 .unwrap_or(&[])
779 .to_vec();
780 match pdfboss_core::Decryptor::from_standard_with_password_str(enc_dict, &id0, password) {
781 Some(dec) => {
782 self.inner
783 .decryptor
784 .set(dec)
785 .map_err(|_| Error::Core(pdfboss_core::Error::Encrypted))?;
786 self.inner
791 .objects
792 .lock()
793 .expect("object cache mutex")
794 .clear();
795 self.inner.objstms.lock().await.clear();
796 Ok(())
797 }
798 None => Err(Error::Core(pdfboss_core::Error::Encrypted)),
799 }
800 }
801
802 pub fn version(&self) -> (u8, u8) {
804 self.inner.version
805 }
806
807 pub(crate) fn fetcher(&self) -> Fetcher {
809 Fetcher {
810 backend: Arc::clone(&self.inner.backend),
811 len: self.inner.file_len,
812 }
813 }
814}
815
816impl AsyncObjectSource for AsyncDocument {
828 fn get(&self, r: ObjRef) -> BoxFuture<'_, pdfboss_core::Result<Object>> {
829 Box::pin(async move { self.get_object(r).await.map_err(Into::into) })
830 }
831
832 fn stream_data<'a>(&'a self, s: &'a Stream) -> BoxFuture<'a, pdfboss_core::Result<Vec<u8>>> {
833 Box::pin(async move { self.decode_stream(s).await.map_err(Into::into) })
834 }
835
836 fn resolve<'a>(&'a self, o: &'a Object) -> BoxFuture<'a, pdfboss_core::Result<Object>> {
837 Box::pin(async move { self.resolve(o).await.map_err(Into::into) })
840 }
841}
842
843const OBJECT_WINDOW: usize = 2048;
845const MAX_RESOLVE_DEPTH: usize = 32;
847
848struct LengthProbe {
852 known: Option<(ObjRef, i64)>,
853 missing: std::cell::Cell<Option<ObjRef>>,
854}
855
856impl LengthProbe {
857 fn new(known: Option<(ObjRef, i64)>) -> LengthProbe {
858 LengthProbe {
859 known,
860 missing: std::cell::Cell::new(None),
861 }
862 }
863
864 fn missing(&self) -> Option<ObjRef> {
865 self.missing.get()
866 }
867}
868
869impl Resolve for LengthProbe {
870 fn resolve_ref(&self, r: ObjRef) -> Option<Object> {
871 match self.known {
872 Some((known_ref, value)) if known_ref == r => Some(Object::Int(value)),
873 _ => {
874 if self.missing.get().is_none() {
875 self.missing.set(Some(r));
876 }
877 None
878 }
879 }
880 }
881}
882
883fn stream_dishonors_length(object: &Object, known_length: Option<(ObjRef, i64)>) -> bool {
888 let Some(stream) = object.as_stream() else {
889 return false;
890 };
891 let declared = match stream.dict.get("Length") {
892 Some(Object::Int(n)) => Some(*n),
893 Some(Object::Ref(r)) => {
894 known_length.and_then(|(known_ref, value)| (known_ref == *r).then_some(value))
895 }
896 _ => None,
897 };
898 match declared {
899 Some(length) if length >= 0 => stream.data.len() as u64 != length as u64,
900 _ => false,
901 }
902}
903
904fn object_type_name(o: &Object) -> &'static str {
906 match o {
907 Object::Null => "null",
908 Object::Bool(_) => "boolean",
909 Object::Int(_) => "integer",
910 Object::Real(_) => "real",
911 Object::String(_) => "string",
912 Object::Name(_) => "name",
913 Object::Array(_) => "array",
914 Object::Dict(_) => "dictionary",
915 Object::Stream(_) => "stream",
916 Object::Ref(_) => "reference",
917 }
918}
919
920pub(crate) struct ObjStmCache {
923 pub(crate) container: ObjRef,
925 pub(crate) container_span: Span,
928 first: usize,
929 data: Vec<u8>,
930 pub(crate) members: Vec<(u32, usize)>,
933}
934
935impl ObjStmCache {
936 pub(crate) fn object(&self, index: u32) -> Result<Object> {
938 let start = self.member_start(index)?;
939 Parser::at(&self.data, start)
940 .parse_object(&NoResolve)
941 .map_err(Error::Core)
942 }
943
944 pub(crate) fn member_span(&self, index: u32) -> Result<Span> {
948 let start = self.member_start(index)?;
949 let mut parser = Parser::at(&self.data, start);
950 parser.parse_object(&NoResolve).map_err(Error::Core)?;
951 Ok(Span {
952 start: start as u64,
953 end: parser.pos() as u64,
954 })
955 }
956
957 fn member_start(&self, index: u32) -> Result<usize> {
959 let offset = self
960 .members
961 .get(index as usize)
962 .map(|entry| entry.1)
963 .ok_or_else(|| {
964 Error::Core(pdfboss_core::Error::Other(format!(
965 "object stream index {index} out of range (N = {})",
966 self.members.len()
967 )))
968 })?;
969 self.first
970 .checked_add(offset)
971 .filter(|&pos| pos <= self.data.len())
972 .ok_or_else(|| {
973 Error::Core(pdfboss_core::Error::Other(format!(
974 "object stream offset {offset} lies outside the stream"
975 )))
976 })
977 }
978}
979
980fn parse_objstm_header(data: &[u8], n: usize) -> Result<Vec<(u32, usize)>> {
983 let mut lexer = Lexer::new(data);
984 let mut members = Vec::with_capacity(n);
985 for _ in 0..n {
986 let num = expect_header_int(&mut lexer)?;
987 let offset = expect_header_int(&mut lexer)?;
988 members.push((u32::try_from(num).unwrap_or(u32::MAX), offset));
989 }
990 Ok(members)
991}
992
993fn expect_header_int(lexer: &mut Lexer) -> Result<usize> {
995 match lexer.next_token().map_err(Error::Core)? {
996 Token::Int(v) if v >= 0 => Ok(v as usize),
997 _ => Err(Error::Core(pdfboss_core::Error::Syntax {
998 offset: lexer.pos(),
999 msg: "malformed object stream header".to_string(),
1000 })),
1001 }
1002}
1003
1004struct MapResolve(HashMap<ObjRef, Object>);
1006
1007impl Resolve for MapResolve {
1008 fn resolve_ref(&self, r: ObjRef) -> Option<Object> {
1009 self.0.get(&r).cloned()
1010 }
1011}
1012
1013impl AsyncDocument {
1014 pub async fn get_object(&self, r: ObjRef) -> Result<Object> {
1018 let mut chain = Vec::new();
1019 self.fetch_object_cached(r, &mut chain).await
1020 }
1021
1022 pub async fn resolve(&self, o: &Object) -> Result<Object> {
1026 let mut chain = Vec::new();
1027 self.resolve_with_chain(o, &mut chain).await
1028 }
1029
1030 pub(crate) fn fetch_object_cached<'a>(
1035 &'a self,
1036 r: ObjRef,
1037 chain: &'a mut Vec<u32>,
1038 ) -> BoxFuture<'a, Result<Object>> {
1039 Box::pin(async move {
1040 if let Some(cached) = self
1041 .inner
1042 .objects
1043 .lock()
1044 .expect("object cache mutex")
1045 .get(&(r.num, r.gen))
1046 {
1047 return Ok((**cached).clone());
1048 }
1049 if chain.contains(&r.num) {
1050 return Err(Error::Core(pdfboss_core::Error::CircularReference(r.num)));
1051 }
1052 chain.push(r.num);
1053 let outcome = self.load_object(r, chain).await;
1054 chain.pop();
1055 let object = outcome?;
1056 self.inner
1057 .objects
1058 .lock()
1059 .expect("object cache mutex")
1060 .insert((r.num, r.gen), Arc::new(object.clone()));
1061 Ok(object)
1062 })
1063 }
1064
1065 async fn load_object(&self, r: ObjRef, chain: &mut Vec<u32>) -> Result<Object> {
1068 match self.inner.xref.entries.get(&r.num).copied() {
1069 None | Some(XrefEntry::Free) => Err(Error::Core(pdfboss_core::Error::ObjectNotFound(
1070 r.num, r.gen,
1071 ))),
1072 Some(XrefEntry::InFile { offset, .. }) => {
1073 let parsed = self.parse_in_file(offset, chain).await?;
1074 Ok(parsed.1)
1075 }
1076 Some(XrefEntry::InStream { stream_num, index }) => {
1077 let cache = self.objstm_cache_with_chain(stream_num, chain).await?;
1078 cache.object(index)
1079 }
1080 }
1081 }
1082
1083 pub(crate) async fn parse_in_file(
1092 &self,
1093 offset: u64,
1094 chain: &mut Vec<u32>,
1095 ) -> Result<(Span, Object)> {
1096 if offset >= self.inner.file_len {
1097 return Err(Error::Core(pdfboss_core::Error::Other(format!(
1098 "object offset {offset} lies outside the file"
1099 ))));
1100 }
1101 let fetcher = self.fetcher();
1102 let mut window = OBJECT_WINDOW;
1103 let mut known_length: Option<(ObjRef, i64)> = None;
1104 loop {
1105 let buf = fetcher.window(offset, window).await?;
1106 let at_eof = offset + buf.len() as u64 >= self.inner.file_len;
1107 let probe = LengthProbe::new(known_length);
1108 let mut parser = Parser::at(&buf, 0);
1109 match parser.parse_indirect(&probe) {
1110 Ok((parsed_ref, mut object)) => {
1111 let end = parser.pos();
1112 if end + PARSE_SLACK <= buf.len() || at_eof {
1113 if let Some(missing) = probe.missing() {
1114 if let Ok(length_object) =
1115 self.fetch_object_cached(missing, chain).await
1116 {
1117 if let Some(value) = length_object.as_int() {
1118 known_length = Some((missing, value));
1119 continue;
1120 }
1121 }
1122 }
1125 if at_eof || !stream_dishonors_length(&object, known_length) {
1126 if let Some(dec) = self.inner.decryptor.get() {
1133 dec.decrypt_object(&mut object, parsed_ref.num, parsed_ref.gen);
1134 }
1135 return Ok((
1136 Span {
1137 start: offset,
1138 end: offset + end as u64,
1139 },
1140 object,
1141 ));
1142 }
1143 }
1144 }
1145 Err(parse_error) => {
1146 if at_eof {
1147 return Err(Error::Core(parse_error));
1148 }
1149 }
1150 }
1151 if window as u64 >= MAX_GROWTH_WINDOW {
1152 return Err(Error::Core(pdfboss_core::Error::Other(format!(
1153 "invalid or unrecoverable cross-reference data: object at offset {offset} \
1154 exceeded the {MAX_GROWTH_WINDOW}-byte parse window without completing"
1155 ))));
1156 }
1157 window = window.saturating_mul(2);
1158 }
1159 }
1160
1161 pub(crate) async fn objstm_cache(&self, stream_num: u32) -> Result<Arc<ObjStmCache>> {
1165 let mut chain = Vec::new();
1166 self.objstm_cache_with_chain(stream_num, &mut chain).await
1167 }
1168
1169 async fn objstm_cache_with_chain(
1170 &self,
1171 stream_num: u32,
1172 chain: &mut Vec<u32>,
1173 ) -> Result<Arc<ObjStmCache>> {
1174 if chain.contains(&stream_num) {
1180 return Err(Error::Core(pdfboss_core::Error::CircularReference(
1181 stream_num,
1182 )));
1183 }
1184 if let Some(hit) = self.inner.objstms.lock().await.get(&stream_num) {
1185 return Ok(Arc::clone(hit));
1186 }
1187 let offset = match self.inner.xref.entries.get(&stream_num).copied() {
1188 Some(XrefEntry::InFile { offset, .. }) => offset,
1189 Some(XrefEntry::InStream { .. }) | Some(XrefEntry::Free) | None => {
1192 return Err(Error::Core(pdfboss_core::Error::ObjectNotFound(
1193 stream_num, 0,
1194 )))
1195 }
1196 };
1197 chain.push(stream_num);
1198 let outcome = self.build_objstm_cache(stream_num, offset, chain).await;
1199 chain.pop();
1200 let entry = outcome?;
1201 let mut cache = self.inner.objstms.lock().await;
1202 let stored = cache
1203 .entry(stream_num)
1204 .or_insert_with(|| Arc::clone(&entry));
1205 Ok(Arc::clone(stored))
1206 }
1207
1208 async fn build_objstm_cache(
1211 &self,
1212 stream_num: u32,
1213 offset: u64,
1214 chain: &mut Vec<u32>,
1215 ) -> Result<Arc<ObjStmCache>> {
1216 let (container_span, object) = self.parse_in_file(offset, chain).await?;
1217 let stream = object
1218 .as_stream()
1219 .ok_or(Error::Core(pdfboss_core::Error::TypeMismatch {
1220 expected: "stream",
1221 found: object_type_name(&object),
1222 }))?;
1223 let n = self
1224 .resolve_with_chain(stream.dict.get("N").unwrap_or(&Object::Null), chain)
1225 .await?
1226 .as_int()
1227 .and_then(|v| usize::try_from(v).ok())
1228 .ok_or(Error::Core(pdfboss_core::Error::MissingKey("N")))?;
1229 let first = self
1230 .resolve_with_chain(stream.dict.get("First").unwrap_or(&Object::Null), chain)
1231 .await?
1232 .as_int()
1233 .and_then(|v| usize::try_from(v).ok())
1234 .ok_or(Error::Core(pdfboss_core::Error::MissingKey("First")))?;
1235 let data = self.decode_stream_with_chain(stream, chain).await?;
1236 let members = parse_objstm_header(&data, n)?;
1237 Ok(Arc::new(ObjStmCache {
1238 container: ObjRef {
1239 num: stream_num,
1240 gen: 0,
1241 },
1242 container_span,
1243 first,
1244 data,
1245 members,
1246 }))
1247 }
1248
1249 pub(crate) async fn resolve_with_chain(
1251 &self,
1252 o: &Object,
1253 chain: &mut Vec<u32>,
1254 ) -> Result<Object> {
1255 let mut current = o.clone();
1256 let mut last_num = 0;
1257 for _ in 0..MAX_RESOLVE_DEPTH {
1258 match current {
1259 Object::Ref(r) => {
1260 last_num = r.num;
1261 current = match self.fetch_object_cached(r, chain).await {
1262 Ok(object) => object,
1263 Err(Error::Core(pdfboss_core::Error::CircularReference(n))) => {
1264 return Err(Error::Core(pdfboss_core::Error::CircularReference(n)))
1265 }
1266 Err(_) => return Ok(Object::Null),
1267 };
1268 }
1269 other => return Ok(other),
1270 }
1271 }
1272 Err(Error::Core(pdfboss_core::Error::CircularReference(
1273 last_num,
1274 )))
1275 }
1276
1277 pub(crate) async fn decode_stream_with_chain(
1282 &self,
1283 s: &Stream,
1284 chain: &mut Vec<u32>,
1285 ) -> Result<Vec<u8>> {
1286 let resolver = self.prefetch_filter_refs(&s.dict, chain).await;
1287 pdfboss_core::filters::decode_stream(s, &resolver).map_err(Error::Core)
1288 }
1289
1290 async fn prefetch_filter_refs(&self, dict: &Dict, chain: &mut Vec<u32>) -> MapResolve {
1294 const FILTER_KEYS: [&str; 5] = ["Length", "Filter", "DecodeParms", "DP", "F"];
1295 let mut map: HashMap<ObjRef, Object> = HashMap::new();
1296 let mut frontier: Vec<Object> = FILTER_KEYS
1297 .iter()
1298 .filter_map(|key| dict.get(key).cloned())
1299 .collect();
1300 for _ in 0..MAX_RESOLVE_DEPTH {
1301 let mut next = Vec::new();
1302 for value in frontier.drain(..) {
1303 match value {
1304 Object::Ref(r) => {
1305 if let std::collections::hash_map::Entry::Vacant(entry) = map.entry(r) {
1306 let resolved = match self.fetch_object_cached(r, chain).await {
1307 Ok(object) => object,
1308 Err(_) => Object::Null,
1309 };
1310 next.push(resolved.clone());
1311 entry.insert(resolved);
1312 }
1313 }
1314 Object::Array(items) => next.extend(items),
1315 Object::Dict(d) => next.extend(d.iter().map(|(_, v)| v.clone())),
1316 _ => {}
1317 }
1318 }
1319 if next.is_empty() {
1320 break;
1321 }
1322 frontier = next;
1323 }
1324 MapResolve(map)
1325 }
1326
1327 pub async fn decode_stream(&self, s: &Stream) -> Result<Vec<u8>> {
1330 let mut chain = Vec::new();
1331 self.decode_stream_with_chain(s, &mut chain).await
1332 }
1333
1334 pub async fn read_span(&self, span: Span) -> Result<Vec<u8>> {
1337 let start = span.start.min(self.inner.file_len);
1338 let end = span.end.min(self.inner.file_len);
1339 if start >= end {
1340 return Ok(Vec::new());
1341 }
1342 self.fetcher().read_range(start, end).await
1343 }
1344
1345 pub fn file_len(&self) -> u64 {
1347 self.inner.file_len
1348 }
1349
1350 pub async fn metadata(&self) -> Result<Metadata> {
1354 let mut meta = Metadata::default();
1355 let Some(info) = self.inner.xref.trailer.get("Info") else {
1356 return Ok(meta);
1357 };
1358 let Ok(info) = self.resolve(info).await else {
1359 return Ok(meta);
1360 };
1361 let Some(dict) = info.as_dict() else {
1362 return Ok(meta);
1363 };
1364 meta.title = self.meta_string(dict, "Title").await;
1365 meta.author = self.meta_string(dict, "Author").await;
1366 meta.subject = self.meta_string(dict, "Subject").await;
1367 meta.keywords = self.meta_string(dict, "Keywords").await;
1368 meta.creator = self.meta_string(dict, "Creator").await;
1369 meta.producer = self.meta_string(dict, "Producer").await;
1370 meta.creation_date = self.meta_string(dict, "CreationDate").await;
1371 meta.mod_date = self.meta_string(dict, "ModDate").await;
1372 Ok(meta)
1373 }
1374
1375 async fn meta_string(&self, dict: &Dict, key: &str) -> Option<String> {
1377 let value = self.resolve(dict.get(key)?).await.ok()?;
1378 Some(decode_text_string(value.as_str_bytes()?))
1379 }
1380
1381 pub async fn oc_state(&self) -> Option<pdfboss_core::OcState> {
1387 pdfboss_core::OcState::load_with(self, &self.inner.xref.trailer).await
1388 }
1389
1390 pub fn page_count(&self) -> usize {
1394 self.inner.pages.get().map_or(0, Vec::len)
1395 }
1396
1397 pub fn page(&self, index: usize) -> Result<Page> {
1409 let Some(record) = self.page_record(index) else {
1410 return Err(Error::Core(pdfboss_core::Error::PageNotFound(
1411 index,
1412 self.page_count(),
1413 )));
1414 };
1415 Ok(Page::from_tree_attrs(
1416 index,
1417 Some(record.resources),
1418 record.media_box,
1419 record.crop_box,
1420 record.bleed_box,
1421 record.trim_box,
1422 record.art_box,
1423 record.rotate,
1424 record.dict,
1425 record.r,
1426 ))
1427 }
1428
1429 pub(crate) fn page_record(&self, index: usize) -> Option<PageRecord> {
1432 self.inner
1433 .pages
1434 .get()
1435 .and_then(|pages| pages.get(index))
1436 .cloned()
1437 }
1438
1439 async fn flatten_pages(&self) -> Vec<PageRecord> {
1445 let mut chain = Vec::new();
1446 let mut pages = Vec::new();
1447 let Some(root) = self.inner.xref.trailer.get("Root") else {
1448 return pages;
1449 };
1450 let Ok(catalog) = self.resolve_with_chain(root, &mut chain).await else {
1451 return pages;
1452 };
1453 let Some(tree_root) = catalog.as_dict().and_then(|d| d.get("Pages")).cloned() else {
1454 return pages;
1455 };
1456 let mut visited: HashSet<ObjRef> = HashSet::new();
1457 let mut stack: Vec<(Object, InheritedAttrs, usize)> =
1458 vec![(tree_root, InheritedAttrs::default(), 0)];
1459 while let Some((node, mut inherited, depth)) = stack.pop() {
1460 if depth > MAX_TREE_DEPTH {
1461 continue;
1462 }
1463 let node_ref = node.as_ref();
1464 if let Some(r) = node_ref {
1465 if !visited.insert(r) {
1466 continue; }
1468 }
1469 let Ok(resolved) = self.resolve_with_chain(&node, &mut chain).await else {
1470 continue;
1471 };
1472 let Some(dict) = resolved.as_dict() else {
1473 continue;
1474 };
1475 if let Some(value) = dict.get("Resources") {
1476 if let Ok(res) = self.resolve_with_chain(value, &mut chain).await {
1477 if let Some(res) = res.as_dict() {
1478 inherited.resources = res.clone();
1479 }
1480 }
1481 }
1482 if let Some(mb) = self.rect_value(dict, "MediaBox", &mut chain).await {
1483 inherited.media_box = Some(mb);
1484 }
1485 if let Some(cb) = self.rect_value(dict, "CropBox", &mut chain).await {
1486 inherited.crop_box = Some(cb);
1487 }
1488 if let Some(rot) = self.int_value(dict, "Rotate", &mut chain).await {
1489 inherited.rotate = Some(rot);
1490 }
1491 let is_page = dict.get_name("Type").is_some_and(|n| n.0 == "Page");
1492 let kids = if is_page {
1493 None
1494 } else {
1495 self.array_value(dict, "Kids", &mut chain).await
1496 };
1497 match kids {
1498 Some(kids) => {
1499 for kid in kids.iter().rev() {
1501 stack.push((kid.clone(), inherited.clone(), depth + 1));
1502 }
1503 }
1504 None => {
1505 let bleed_box = self.rect_value(dict, "BleedBox", &mut chain).await;
1509 let trim_box = self.rect_value(dict, "TrimBox", &mut chain).await;
1510 let art_box = self.rect_value(dict, "ArtBox", &mut chain).await;
1511 pages.push(PageRecord {
1512 r: node_ref,
1513 dict: dict.clone(),
1514 resources: inherited.resources.clone(),
1515 media_box: inherited.media_box,
1516 crop_box: inherited.crop_box,
1517 bleed_box,
1518 trim_box,
1519 art_box,
1520 rotate: inherited.rotate,
1521 });
1522 }
1523 }
1524 }
1525 pages
1526 }
1527
1528 async fn rect_value(
1532 &self,
1533 dict: &Dict,
1534 key: &str,
1535 chain: &mut Vec<u32>,
1536 ) -> Option<pdfboss_core::Rect> {
1537 let items = self.array_value(dict, key, chain).await?;
1538 if items.len() != 4 {
1539 return None;
1540 }
1541 let mut coords = [0.0f32; 4];
1542 for (slot, item) in coords.iter_mut().zip(&items) {
1543 let n = self.resolve_with_chain(item, chain).await.ok()?.as_f64()?;
1544 if !n.is_finite() {
1545 return None;
1546 }
1547 *slot = n as f32;
1548 }
1549 Some(pdfboss_core::Rect::new(coords[0], coords[1], coords[2], coords[3]).normalize())
1550 }
1551
1552 async fn int_value(&self, dict: &Dict, key: &str, chain: &mut Vec<u32>) -> Option<i32> {
1555 let v = self
1556 .resolve_with_chain(dict.get(key)?, chain)
1557 .await
1558 .ok()?
1559 .as_f64()?;
1560 if v.is_finite() {
1561 Some(v as i32)
1562 } else {
1563 None
1564 }
1565 }
1566
1567 async fn array_value(
1569 &self,
1570 dict: &Dict,
1571 key: &str,
1572 chain: &mut Vec<u32>,
1573 ) -> Option<Vec<Object>> {
1574 match self.resolve_with_chain(dict.get(key)?, chain).await.ok()? {
1575 Object::Array(items) => Some(items),
1576 _ => None,
1577 }
1578 }
1579
1580 pub fn elements(&self, opts: ElementOpts) -> crate::stream::ElementStream {
1590 crate::stream::element_stream(self, opts)
1591 }
1592
1593 pub(crate) async fn physical_object(&self, r: ObjRef, offset: u64) -> Result<(Span, Object)> {
1596 let mut chain = vec![r.num];
1597 let (span, object) = self.parse_in_file(offset, &mut chain).await?;
1598 self.inner
1599 .objects
1600 .lock()
1601 .expect("object cache mutex")
1602 .insert((r.num, r.gen), Arc::new(object.clone()));
1603 Ok((span, object))
1604 }
1605
1606 pub(crate) fn header_span(&self) -> Option<Span> {
1609 self.inner.header_span
1610 }
1611
1612 pub(crate) fn xref_entries(&self) -> Vec<(u32, XrefEntry)> {
1614 self.inner
1615 .xref
1616 .entries
1617 .iter()
1618 .map(|(&num, &entry)| (num, entry))
1619 .collect()
1620 }
1621
1622 pub(crate) fn sections(&self) -> &[SectionRecord] {
1624 &self.inner.sections
1625 }
1626
1627 pub(crate) fn merged_trailer(&self) -> (Dict, Span) {
1630 (
1631 self.inner.xref.trailer.clone(),
1632 self.inner.xref.trailer_span,
1633 )
1634 }
1635
1636 pub(crate) fn startxref_record(&self) -> (u64, Span) {
1638 (self.inner.startxref.offset, self.inner.startxref.span)
1639 }
1640
1641 pub(crate) fn eof_span(&self) -> Option<Span> {
1643 self.inner.eof_span
1644 }
1645}
1646
1647const SECTION_WINDOW: usize = 4096;
1650
1651async fn parse_section_at(fetcher: &Fetcher, offset: u64) -> Result<ParsedSection> {
1653 let mut window = SECTION_WINDOW;
1654 loop {
1655 let buf = fetcher.window(offset, window).await?;
1656 let at_eof = offset + buf.len() as u64 >= fetcher.len;
1657 if let Some(parsed) = parse_section_window(&buf, offset, fetcher.len, at_eof)? {
1658 return Ok(parsed);
1659 }
1660 if window as u64 >= MAX_GROWTH_WINDOW {
1664 return Err(Error::Core(pdfboss_core::Error::Other(format!(
1665 "invalid or unrecoverable cross-reference data: section at offset {offset} \
1666 exceeded the {MAX_GROWTH_WINDOW}-byte parse window without completing"
1667 ))));
1668 }
1669 window = window.saturating_mul(2);
1670 }
1671}
1672
1673pub(crate) async fn load_xref_chain(
1689 fetcher: &Fetcher,
1690 start: u64,
1691) -> Result<(XrefIndex, Vec<SectionRecord>)> {
1692 let mut entries: HashMap<u32, XrefEntry> = HashMap::new();
1693 let mut trailer = Dict::new();
1694 let mut trailer_span: Option<Span> = None;
1695 let mut sections: Vec<SectionRecord> = Vec::new();
1696 let mut visited: HashSet<u64> = HashSet::new();
1697 let mut next = Some(start);
1698 while let Some(offset) = next {
1699 if !visited.insert(offset) {
1700 break;
1701 }
1702 let parsed = parse_section_at(fetcher, offset).await?;
1703 if trailer_span.is_none() {
1704 trailer_span = Some(parsed.record.trailer_span);
1705 }
1706 let mut hybrid_record = None;
1711 if let Some(hybrid_offset) = parsed.xrefstm.filter(|&v| v < fetcher.len) {
1712 if visited.insert(hybrid_offset) {
1713 if let Ok(hybrid) = parse_section_at(fetcher, hybrid_offset).await {
1715 merge_section(&mut entries, &mut trailer, &hybrid);
1716 hybrid_record = Some(hybrid.record);
1717 }
1718 }
1719 }
1720 next = parsed.prev.filter(|&v| v < fetcher.len);
1721 merge_section(&mut entries, &mut trailer, &parsed);
1722 sections.push(parsed.record);
1723 if let Some(record) = hybrid_record {
1724 sections.push(record);
1725 }
1726 }
1727 if entries.is_empty() {
1728 return Err(Error::Core(pdfboss_core::Error::InvalidXref));
1729 }
1730 let trailer_span = trailer_span.expect("set on the first parsed section");
1733 Ok((
1734 XrefIndex {
1735 entries,
1736 trailer,
1737 trailer_span,
1738 },
1739 sections,
1740 ))
1741}
1742
1743fn merge_section(
1746 entries: &mut HashMap<u32, XrefEntry>,
1747 trailer: &mut Dict,
1748 parsed: &ParsedSection,
1749) {
1750 for (num, entry) in &parsed.entries {
1751 entries.entry(*num).or_insert(*entry);
1752 }
1753 for (key, value) in parsed.record.trailer_dict.iter() {
1754 if trailer.get(&key.0).is_none() {
1755 trailer.insert(key.clone(), value.clone());
1756 }
1757 }
1758}
1759
1760#[allow(dead_code)]
1762fn assert_document_is_shareable()
1763where
1764 AsyncDocument: Send + Sync + Clone,
1765{
1766}
1767
1768#[cfg(test)]
1769mod tests {
1770 use super::*;
1771 use crate::backend::MemBackend;
1772 use pdfboss_testkit::{multi_page_doc, simple_doc};
1773
1774 fn fetcher_for(data: Vec<u8>) -> Fetcher {
1775 let len = data.len() as u64;
1776 Fetcher {
1777 backend: std::sync::Arc::new(MemBackend::from(data)),
1778 len,
1779 }
1780 }
1781
1782 fn pos_of(data: &[u8], needle: &[u8]) -> usize {
1784 data.windows(needle.len())
1785 .position(|w| w == needle)
1786 .expect("needle present")
1787 }
1788
1789 #[tokio::test]
1790 async fn read_range_returns_exact_bytes_and_detects_truncation() {
1791 let fetcher = fetcher_for(b"0123456789".to_vec());
1792 assert_eq!(fetcher.read_range(2, 6).await.unwrap(), b"2345");
1793 assert_eq!(fetcher.window(8, 100).await.unwrap(), b"89");
1794 assert!(fetcher.window(10, 100).await.unwrap().is_empty());
1795 let lying = Fetcher {
1798 backend: std::sync::Arc::new(MemBackend::from(b"0123456789".to_vec())),
1799 len: 20,
1800 };
1801 match lying.read_range(5, 15).await {
1802 Err(crate::Error::TruncatedRead {
1803 offset,
1804 wanted,
1805 got,
1806 }) => {
1807 assert_eq!(offset, 5);
1808 assert_eq!(wanted, 10);
1809 assert_eq!(got, 5);
1810 }
1811 other => panic!("expected TruncatedRead, got {other:?}"),
1812 }
1813 }
1814
1815 #[tokio::test]
1816 async fn tail_scan_finds_startxref_and_eof() {
1817 let data = simple_doc("tail scan");
1818 let xref_pos = pos_of(&data, b"xref\n0 ") as u64;
1819 let startxref_pos = pos_of(&data, b"startxref") as u64;
1820 let eof_pos = pos_of(&data, b"%%EOF") as u64;
1821 let fetcher = fetcher_for(data);
1822 let (record, eof) = find_tail(&fetcher).await.unwrap();
1823 assert_eq!(record.offset, xref_pos);
1824 assert_eq!(record.span.start, startxref_pos);
1825 assert!(record.span.end > startxref_pos + b"startxref".len() as u64);
1826 assert_eq!(
1827 eof,
1828 Some(Span {
1829 start: eof_pos,
1830 end: eof_pos + 5
1831 })
1832 );
1833 }
1834
1835 #[tokio::test]
1836 async fn tail_scan_grows_past_trailing_padding() {
1837 let mut data = simple_doc("padded");
1838 let xref_pos = pos_of(&data, b"xref\n0 ") as u64;
1839 data.extend_from_slice(&vec![b' '; 8192]);
1840 let fetcher = fetcher_for(data);
1841 let (record, eof) = find_tail(&fetcher).await.unwrap();
1842 assert_eq!(record.offset, xref_pos);
1843 assert!(eof.is_some());
1844 }
1845
1846 #[tokio::test]
1847 async fn tail_scan_without_startxref_is_invalid_xref() {
1848 let fetcher = fetcher_for(b"not a pdf at all".to_vec());
1849 assert!(matches!(
1850 find_tail(&fetcher).await,
1851 Err(crate::Error::Core(pdfboss_core::Error::InvalidXref))
1852 ));
1853 }
1854
1855 #[test]
1856 fn version_parse_matches_header_and_defaults() {
1857 assert_eq!(parse_version(b"%PDF-1.7\nrest"), (1, 7));
1858 assert_eq!(parse_version(b"junk\n%PDF-2.0\n"), (2, 0));
1859 assert_eq!(parse_version(b"%QQQ-1.7"), (1, 4));
1860 assert_eq!(parse_version(b""), (1, 4));
1861 assert_eq!(parse_version(b"%PDF-1."), (1, 4));
1862 }
1863
1864 #[test]
1865 fn header_span_covers_the_version_run() {
1866 assert_eq!(
1867 header_span_in(b"%PDF-1.7\nrest"),
1868 Some(Span { start: 0, end: 8 })
1869 );
1870 assert_eq!(
1871 header_span_in(b"junk\n%PDF-2.0\n"),
1872 Some(Span { start: 5, end: 13 })
1873 );
1874 assert_eq!(header_span_in(b"%QQQ-1.7"), None);
1875 assert_eq!(header_span_in(b""), None);
1876 }
1877
1878 use pdfboss_core::xref::XrefEntry;
1879
1880 fn classic_section(data: &[u8]) -> (Vec<u8>, u64) {
1883 let off = pos_of(data, b"xref\n0 ");
1884 (data[off..].to_vec(), off as u64)
1885 }
1886
1887 #[test]
1888 fn classic_section_window_parses_entries_and_trailer() {
1889 let data = pdfboss_testkit::simple_doc("sections");
1890 let (buf, base) = classic_section(&data);
1891 let file_len = data.len() as u64;
1892 let parsed = parse_section_window(&buf, base, file_len, true)
1893 .unwrap()
1894 .expect("complete section parses");
1895 assert_eq!(parsed.record.kind, pdfboss_core::elements::XrefKind::Table);
1896 assert_eq!(parsed.record.entries, 6); assert_eq!(parsed.entries.len(), 6);
1898 assert!(matches!(
1899 parsed.entries.iter().find(|(num, _)| *num == 0),
1900 Some((0, XrefEntry::Free))
1901 ));
1902 let obj1_off = pos_of(&data, b"1 0 obj") as u64;
1903 assert!(parsed.entries.iter().any(|(num, entry)| *num == 1
1904 && matches!(entry, XrefEntry::InFile { offset, gen: 0 } if *offset == obj1_off)));
1905 assert_eq!(
1906 parsed.record.trailer_dict.get_ref("Root").map(|r| r.num),
1907 Some(1)
1908 );
1909 assert_eq!(parsed.prev, None);
1910 assert_eq!(parsed.xrefstm, None);
1911 assert_eq!(parsed.record.span.start, base);
1914 let trailer_off = pos_of(&data, b"trailer") as u64;
1915 assert_eq!(parsed.record.span.end, trailer_off);
1916 assert_eq!(parsed.record.trailer_span.start, trailer_off);
1917 let dict_end = pos_of(&data, b"startxref") as u64;
1918 assert!(parsed.record.trailer_span.end > trailer_off);
1919 assert!(parsed.record.trailer_span.end <= dict_end);
1920 }
1921
1922 #[test]
1923 fn truncated_classic_section_asks_for_more_bytes() {
1924 let data = pdfboss_testkit::simple_doc("cut short");
1925 let (buf, base) = classic_section(&data);
1926 let file_len = data.len() as u64;
1927 let cut = &buf[..40];
1930 assert!(parse_section_window(cut, base, file_len, false)
1931 .unwrap()
1932 .is_none());
1933 assert!(parse_section_window(cut, base, base + 40, true).is_err());
1935 }
1936
1937 #[test]
1938 fn xref_stream_section_window_parses_entries() {
1939 let (dict, payload) = pdfboss_testkit::objstm_payload(&[
1940 (1, "<< /Type /Catalog /Pages 2 0 R >>"),
1941 (2, "<< /Type /Pages /Kids [] /Count 0 >>"),
1942 ]);
1943 let mut b = pdfboss_testkit::PdfBuilder::new();
1944 b.stream(6, &dict, &payload);
1945 let data = b.build_xref_stream(1);
1946 let off = pos_of(&data, b"7 0 obj") as u64; let buf = data[off as usize..].to_vec();
1948 let parsed = parse_section_window(&buf, off, data.len() as u64, true)
1949 .unwrap()
1950 .expect("complete stream section parses");
1951 assert_eq!(parsed.record.kind, pdfboss_core::elements::XrefKind::Stream);
1952 assert_eq!(parsed.record.span.start, off);
1953 assert_eq!(parsed.record.trailer_span, parsed.record.span);
1954 assert!(parsed.entries.iter().any(|(num, entry)| *num == 1
1955 && matches!(
1956 entry,
1957 XrefEntry::InStream {
1958 stream_num: 6,
1959 index: 0
1960 }
1961 )));
1962 assert!(parsed
1963 .entries
1964 .iter()
1965 .any(|(num, entry)| *num == 6 && matches!(entry, XrefEntry::InFile { .. })));
1966 assert_eq!(
1967 parsed
1968 .record
1969 .trailer_dict
1970 .get_name("Type")
1971 .map(|n| n.0.as_str()),
1972 Some("XRef")
1973 );
1974 }
1975
1976 #[test]
1977 fn implausible_subsection_count_is_a_hard_error() {
1978 let buf = b"xref\n0 999999999\n".to_vec();
1981 assert!(parse_section_window(&buf, 0, 4096, false).is_err());
1982 }
1983
1984 #[test]
1985 fn truncated_stream_section_asks_for_more_bytes() {
1986 let mid_dict = b"7 0 obj\n<< /Type /XRef /Length 10 ".to_vec();
1991 assert!(parse_section_window(&mid_dict, 0, 10_000, false)
1992 .unwrap()
1993 .is_none());
1994 assert!(parse_section_window(&mid_dict, 0, mid_dict.len() as u64, true).is_err());
1996
1997 let (dict, payload) = pdfboss_testkit::objstm_payload(&[
2000 (1, "<< /Type /Catalog /Pages 2 0 R >>"),
2001 (2, "<< /Type /Pages /Kids [] /Count 0 >>"),
2002 ]);
2003 let mut b = pdfboss_testkit::PdfBuilder::new();
2004 b.stream(6, &dict, &payload);
2005 let data = b.build_xref_stream(1);
2006 let off = pos_of(&data, b"7 0 obj");
2007 let buf = data[off..].to_vec();
2008 let base = off as u64;
2009 let file_len = data.len() as u64;
2010 let stream_kw = find_bytes(&buf, b"stream\n").expect("stream keyword present");
2011 let cut = &buf[..stream_kw + b"stream\n".len() + 4];
2014 assert!(parse_section_window(cut, base, file_len, false)
2015 .unwrap()
2016 .is_none());
2017 }
2018
2019 use pdfboss_core::xref::load_xref;
2020
2021 async fn assert_xref_parity(data: Vec<u8>) {
2024 let sync_xref = load_xref(&data).unwrap();
2025 let size = sync_xref.trailer.get_int("Size").unwrap_or(64).max(1) as u32;
2026 let doc = AsyncDocument::from_bytes(data).await.unwrap();
2027 for num in 0..size + 2 {
2028 assert_eq!(
2029 doc.inner.xref.entries.get(&num).copied(),
2030 sync_xref.get(num),
2031 "entry for object {num}"
2032 );
2033 }
2034 assert_eq!(
2035 doc.inner.xref.trailer.get_ref("Root"),
2036 sync_xref.trailer.get_ref("Root")
2037 );
2038 assert_eq!(
2039 doc.inner.xref.trailer.get_int("Size"),
2040 sync_xref.trailer.get_int("Size")
2041 );
2042 }
2043
2044 #[tokio::test]
2045 async fn classic_document_matches_sync_xref() {
2046 assert_xref_parity(simple_doc("chain walk")).await;
2047 let doc = AsyncDocument::from_bytes(simple_doc("chain walk"))
2048 .await
2049 .unwrap();
2050 assert_eq!(doc.version(), (1, 7));
2051 assert_eq!(doc.inner.sections.len(), 1);
2052 let clone = doc.clone();
2053 assert_eq!(clone.version(), (1, 7));
2054 }
2055
2056 #[tokio::test]
2057 async fn xref_stream_document_matches_sync_xref() {
2058 let (dict, payload) = pdfboss_testkit::objstm_payload(&[
2059 (1, "<< /Type /Catalog /Pages 2 0 R >>"),
2060 (2, "<< /Type /Pages /Kids [] /Count 0 >>"),
2061 ]);
2062 let mut b = pdfboss_testkit::PdfBuilder::new();
2063 b.stream(6, &dict, &payload);
2064 assert_xref_parity(b.build_xref_stream(1)).await;
2065 }
2066
2067 fn prev_chain_doc() -> Vec<u8> {
2070 let mut data = b"%PDF-1.5\n".to_vec();
2071 let obj1 = data.len();
2072 data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
2073 let obj2_old = data.len();
2074 data.extend_from_slice(b"2 0 obj\n(old)\nendobj\n");
2075 let classic_off = data.len();
2076 data.extend_from_slice(b"xref\n0 3\n0000000000 65535 f\r\n");
2077 data.extend_from_slice(format!("{obj1:010} 00000 n\r\n").as_bytes());
2078 data.extend_from_slice(format!("{obj2_old:010} 00000 n\r\n").as_bytes());
2079 data.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
2080 let obj2_new = data.len();
2081 data.extend_from_slice(b"2 0 obj\n(new)\nendobj\n");
2082 let stream_off = data.len();
2083 let mut fields = Vec::new();
2084 for offset in [obj2_new, stream_off] {
2085 fields.push(1u8);
2086 fields.extend_from_slice(&(offset as u32).to_be_bytes());
2087 fields.extend_from_slice(&0u16.to_be_bytes());
2088 }
2089 data.extend_from_slice(
2090 format!(
2091 "4 0 obj\n<< /Type /XRef /Size 5 /W [1 4 2] /Index [2 1 4 1] \
2092 /Prev {} /Root 1 0 R /Length {} >>\nstream\n",
2093 classic_off,
2094 fields.len()
2095 )
2096 .as_bytes(),
2097 );
2098 data.extend_from_slice(&fields);
2099 data.extend_from_slice(b"\nendstream\nendobj\n");
2100 data.extend_from_slice(format!("startxref\n{stream_off}\n%%EOF\n").as_bytes());
2101 data
2102 }
2103
2104 #[tokio::test]
2105 async fn prev_chain_merges_newest_wins() {
2106 let data = prev_chain_doc();
2107 let obj2_new = pos_of(&data, b"2 0 obj\n(new)") as u64;
2108 assert_xref_parity(data.clone()).await;
2109 let doc = AsyncDocument::from_bytes(data).await.unwrap();
2110 assert!(matches!(
2111 doc.inner.xref.entries.get(&2),
2112 Some(XrefEntry::InFile { offset, .. }) if *offset == obj2_new
2113 ));
2114 assert_eq!(doc.inner.sections.len(), 2);
2117 assert!(doc.inner.sections[0].span.start > doc.inner.sections[1].span.start);
2118 assert_eq!(
2119 doc.inner.sections[0].kind,
2120 pdfboss_core::elements::XrefKind::Stream
2121 );
2122 assert_eq!(
2123 doc.inner.sections[1].kind,
2124 pdfboss_core::elements::XrefKind::Table
2125 );
2126 assert_eq!(doc.inner.xref.trailer_span, doc.inner.sections[0].span);
2129 }
2130
2131 fn hybrid_doc() -> Vec<u8> {
2134 let mut data = b"%PDF-1.5\n".to_vec();
2135 let obj1 = data.len();
2136 data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
2137 let obj2 = data.len();
2138 data.extend_from_slice(b"2 0 obj\n(hidden)\nendobj\n");
2139 let stm_off = data.len();
2140 let mut fields = Vec::new();
2141 for offset in [obj2, stm_off] {
2142 fields.push(1u8);
2143 fields.extend_from_slice(&(offset as u32).to_be_bytes());
2144 fields.extend_from_slice(&0u16.to_be_bytes());
2145 }
2146 data.extend_from_slice(
2147 format!(
2148 "3 0 obj\n<< /Type /XRef /Size 4 /W [1 4 2] /Index [2 1 3 1] \
2149 /Root 1 0 R /Length {} >>\nstream\n",
2150 fields.len()
2151 )
2152 .as_bytes(),
2153 );
2154 data.extend_from_slice(&fields);
2155 data.extend_from_slice(b"\nendstream\nendobj\n");
2156 let classic_off = data.len();
2157 data.extend_from_slice(b"xref\n0 3\n0000000000 65535 f\r\n");
2158 data.extend_from_slice(format!("{obj1:010} 00000 n\r\n").as_bytes());
2159 data.extend_from_slice(b"0000000000 00001 f\r\n");
2160 data.extend_from_slice(
2161 format!("trailer\n<< /Size 4 /Root 1 0 R /XRefStm {stm_off} >>\n").as_bytes(),
2162 );
2163 data.extend_from_slice(format!("startxref\n{classic_off}\n%%EOF\n").as_bytes());
2164 data
2165 }
2166
2167 #[tokio::test]
2168 async fn hybrid_xrefstm_beats_the_tables_free_entry() {
2169 let data = hybrid_doc();
2170 let obj2 = pos_of(&data, b"2 0 obj\n(hidden)") as u64;
2171 assert_xref_parity(data.clone()).await;
2172 let doc = AsyncDocument::from_bytes(data).await.unwrap();
2173 assert!(matches!(
2174 doc.inner.xref.entries.get(&2),
2175 Some(XrefEntry::InFile { offset, .. }) if *offset == obj2
2176 ));
2177 let kinds: Vec<pdfboss_core::elements::XrefKind> =
2182 doc.inner.sections.iter().map(|s| s.kind).collect();
2183 assert_eq!(
2184 kinds,
2185 [
2186 pdfboss_core::elements::XrefKind::Table,
2187 pdfboss_core::elements::XrefKind::Stream
2188 ],
2189 "classic section first, then its hybrid /XRefStm section"
2190 );
2191 }
2192
2193 #[tokio::test]
2194 async fn open_reads_from_disk() {
2195 let path =
2196 std::env::temp_dir().join(format!("pdfboss-aio-doc-test-{}.pdf", std::process::id()));
2197 std::fs::write(&path, simple_doc("from disk")).unwrap();
2198 let doc = AsyncDocument::open(&path).await.unwrap();
2199 std::fs::remove_file(&path).ok();
2200 assert_eq!(doc.version(), (1, 7));
2201 }
2202
2203 use pdfboss_core::{ObjRef, Object};
2204
2205 #[tokio::test]
2206 async fn objects_match_the_sync_document() {
2207 for data in [simple_doc("objects"), multi_page_doc(&["a", "b"])] {
2208 let sync_doc = pdfboss_core::Document::load(data.clone()).unwrap();
2209 let doc = AsyncDocument::from_bytes(data).await.unwrap();
2210 for num in 1..=8u32 {
2211 let r = ObjRef { num, gen: 0 };
2212 match sync_doc.get(r) {
2213 Ok(expected) => {
2214 assert_eq!(doc.get_object(r).await.unwrap(), expected, "object {num}")
2215 }
2216 Err(_) => assert!(doc.get_object(r).await.is_err(), "object {num}"),
2217 }
2218 }
2219 }
2220 }
2221
2222 #[tokio::test]
2223 async fn compressed_objects_are_fetched_from_their_container() {
2224 let (dict, payload) = pdfboss_testkit::objstm_payload(&[
2225 (1, "<< /Type /Catalog /Pages 2 0 R >>"),
2226 (5, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"),
2227 ]);
2228 let mut b = pdfboss_testkit::PdfBuilder::new();
2229 b.stream(6, &dict, &payload);
2230 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
2231 b.object(
2232 3,
2233 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>",
2234 );
2235 b.stream(4, "", b"BT (compressed) Tj ET");
2236 let data = b.build_xref_stream(1);
2237 let sync_doc = pdfboss_core::Document::load(data.clone()).unwrap();
2238 let doc = AsyncDocument::from_bytes(data).await.unwrap();
2239 let font = doc.get_object(ObjRef { num: 5, gen: 0 }).await.unwrap();
2240 assert_eq!(font, sync_doc.get(ObjRef { num: 5, gen: 0 }).unwrap());
2241 assert_eq!(
2242 font.as_dict()
2243 .and_then(|d| d.get_name("BaseFont"))
2244 .map(|n| n.0.as_str()),
2245 Some("Helvetica")
2246 );
2247 let catalog = doc.get_object(ObjRef { num: 1, gen: 0 }).await.unwrap();
2248 assert_eq!(catalog, sync_doc.get(ObjRef { num: 1, gen: 0 }).unwrap());
2249 }
2250
2251 #[tokio::test]
2252 async fn indirect_stream_length_triggers_one_extra_fetch() {
2253 let mut b = pdfboss_testkit::PdfBuilder::new();
2254 b.object(1, "<< /Type /Catalog >>");
2255 b.object(4, "<< /Length 7 0 R >>\nstream\nBT ET\nendstream");
2256 b.object(7, "5");
2257 let data = b.build(1);
2258 let sync_doc = pdfboss_core::Document::load(data.clone()).unwrap();
2259 let doc = AsyncDocument::from_bytes(data).await.unwrap();
2260 let stream = doc.get_object(ObjRef { num: 4, gen: 0 }).await.unwrap();
2261 assert_eq!(stream, sync_doc.get(ObjRef { num: 4, gen: 0 }).unwrap());
2262 assert_eq!(stream.as_stream().unwrap().data, b"BT ET");
2263 }
2264
2265 #[tokio::test]
2266 async fn objects_larger_than_the_initial_window_grow_until_complete() {
2267 let mut b = pdfboss_testkit::PdfBuilder::new();
2268 b.object(1, "<< /Type /Catalog >>");
2269 let big = vec![b'q'; 5000];
2270 b.stream(4, "", &big);
2271 let doc = AsyncDocument::from_bytes(b.build(1)).await.unwrap();
2272 let object = doc.get_object(ObjRef { num: 4, gen: 0 }).await.unwrap();
2273 assert_eq!(object.as_stream().unwrap().data, big);
2274 }
2275
2276 struct RecordingBackend {
2280 inner: MemBackend,
2281 bytes_read: Arc<std::sync::atomic::AtomicU64>,
2282 }
2283
2284 impl Backend for RecordingBackend {
2285 fn len(&self) -> BoxFuture<'_, std::io::Result<u64>> {
2286 self.inner.len()
2287 }
2288
2289 fn read_at<'a>(
2290 &'a self,
2291 offset: u64,
2292 buf: &'a mut [u8],
2293 ) -> BoxFuture<'a, std::io::Result<usize>> {
2294 Box::pin(async move {
2295 let n = self.inner.read_at(offset, buf).await?;
2296 self.bytes_read
2297 .fetch_add(n as u64, std::sync::atomic::Ordering::SeqCst);
2298 Ok(n)
2299 })
2300 }
2301 }
2302
2303 #[tokio::test]
2317 async fn corrupt_offset_in_a_huge_file_errors_within_the_growth_cap_instead_of_reading_to_eof()
2318 {
2319 let header = b"%PDF-1.7\n".to_vec();
2320 let obj1 = b"1 0 obj\n<< /Type /Catalog >>\nendobj\n".to_vec();
2321 let obj1_offset = header.len() as u64;
2322
2323 let mut data = header;
2324 data.extend_from_slice(&obj1);
2325
2326 let garbage_offset = data.len() as u64;
2331 let garbage_len = MAX_GROWTH_WINDOW as usize + 32 * 1024 * 1024;
2332 data.resize(data.len() + garbage_len, b']');
2333
2334 let xref_offset = data.len();
2335 data.extend_from_slice(b"xref\n0 2\n");
2336 data.extend_from_slice(b"0000000000 65535 f\r\n");
2337 data.extend_from_slice(format!("{obj1_offset:010} {:05} n\r\n", 0).as_bytes());
2338 data.extend_from_slice(b"trailer\n<< /Size 2 /Root 1 0 R >>\n");
2339 data.extend_from_slice(format!("startxref\n{xref_offset}\n%%EOF\n").as_bytes());
2340
2341 let bytes_read = Arc::new(std::sync::atomic::AtomicU64::new(0));
2342 let backend = RecordingBackend {
2343 inner: MemBackend::from(data),
2344 bytes_read: Arc::clone(&bytes_read),
2345 };
2346 let doc = AsyncDocument::with_backend(backend)
2347 .await
2348 .expect("a valid one-object catalog with no /Pages still opens");
2349
2350 let mut chain = Vec::new();
2351 let err = doc
2352 .parse_in_file(garbage_offset, &mut chain)
2353 .await
2354 .expect_err("a run of `]` bytes must never parse as an object");
2355 assert!(
2356 err.to_string().contains(&garbage_offset.to_string()),
2357 "error does not name the offending offset: {err}"
2358 );
2359
2360 let total = bytes_read.load(std::sync::atomic::Ordering::SeqCst);
2366 assert!(
2367 total < 3 * MAX_GROWTH_WINDOW,
2368 "read {total} bytes chasing one bogus offset -- the growth cap did not bound it"
2369 );
2370 }
2371
2372 #[tokio::test]
2373 async fn resolve_mirrors_sync_lenient_semantics() {
2374 let mut b = pdfboss_testkit::PdfBuilder::new();
2375 b.object(1, "<< /Type /Catalog >>");
2376 b.object(6, "6 0 R");
2377 let doc = AsyncDocument::from_bytes(b.build(1)).await.unwrap();
2378 let missing = Object::Ref(ObjRef { num: 99, gen: 0 });
2379 assert_eq!(doc.resolve(&missing).await.unwrap(), Object::Null);
2380 let loops = Object::Ref(ObjRef { num: 6, gen: 0 });
2381 assert!(matches!(
2382 doc.resolve(&loops).await,
2383 Err(Error::Core(pdfboss_core::Error::CircularReference(6)))
2384 ));
2385 let catalog = doc.get_object(ObjRef { num: 1, gen: 7 }).await.unwrap();
2387 assert!(catalog.as_dict().is_some());
2388 }
2389
2390 #[tokio::test]
2391 async fn decode_stream_matches_sync_stream_data() {
2392 let data = simple_doc("stream parity");
2393 let sync_doc = pdfboss_core::Document::load(data.clone()).unwrap();
2394 let doc = AsyncDocument::from_bytes(data).await.unwrap();
2395 let object = doc.get_object(ObjRef { num: 4, gen: 0 }).await.unwrap();
2396 let stream = object.as_stream().unwrap();
2397 assert_eq!(
2398 doc.decode_stream(stream).await.unwrap(),
2399 sync_doc.stream_data(stream).unwrap()
2400 );
2401 }
2402
2403 #[tokio::test]
2404 async fn read_span_returns_raw_file_bytes() {
2405 let data = simple_doc("raw bytes");
2406 let doc = AsyncDocument::from_bytes(data.clone()).await.unwrap();
2407 let slice = doc.read_span(Span { start: 0, end: 8 }).await.unwrap();
2408 assert_eq!(slice, b"%PDF-1.7");
2409 let file_len = data.len() as u64;
2411 assert_eq!(doc.file_len(), file_len);
2412 let tail = doc
2413 .read_span(Span {
2414 start: file_len - 6,
2415 end: file_len + 50,
2416 })
2417 .await
2418 .unwrap();
2419 assert_eq!(tail, b"%%EOF\n");
2420 assert!(doc
2421 .read_span(Span {
2422 start: file_len + 1,
2423 end: file_len + 2
2424 })
2425 .await
2426 .unwrap()
2427 .is_empty());
2428 }
2429
2430 #[tokio::test]
2431 async fn metadata_matches_the_sync_document() {
2432 let mut b = pdfboss_testkit::PdfBuilder::new().trailer_extra("/Info 6 0 R");
2433 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2434 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
2435 b.object(6, "<< /Title <FEFF00480151> /Author (plain author) >>");
2437 let data = b.build(1);
2438 let sync_doc = pdfboss_core::Document::load(data.clone()).unwrap();
2439 let doc = AsyncDocument::from_bytes(data).await.unwrap();
2440 let meta = doc.metadata().await.unwrap();
2441 assert_eq!(meta, sync_doc.metadata());
2442 assert_eq!(meta.title.as_deref(), Some("H\u{151}"));
2443 assert_eq!(meta.author.as_deref(), Some("plain author"));
2444 assert_eq!(meta.subject, None);
2445 }
2446
2447 #[tokio::test]
2448 async fn metadata_without_info_is_all_none() {
2449 let doc = AsyncDocument::from_bytes(simple_doc("x")).await.unwrap();
2450 assert_eq!(
2451 doc.metadata().await.unwrap(),
2452 pdfboss_core::Metadata::default()
2453 );
2454 }
2455
2456 #[tokio::test]
2457 async fn page_count_matches_the_sync_document() {
2458 for (data, expected) in [
2459 (simple_doc("one"), 1usize),
2460 (multi_page_doc(&["a", "b", "c"]), 3usize),
2461 ] {
2462 let sync_doc = pdfboss_core::Document::load(data.clone()).unwrap();
2463 let doc = AsyncDocument::from_bytes(data).await.unwrap();
2464 assert_eq!(doc.page_count(), expected);
2465 assert_eq!(doc.page_count(), sync_doc.page_count());
2466 }
2467 }
2468
2469 #[tokio::test]
2470 async fn page_records_carry_inherited_resources_and_refs() {
2471 let mut b = pdfboss_testkit::PdfBuilder::new();
2472 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2473 b.object(
2474 2,
2475 "<< /Type /Pages /Kids [3 0 R] /Count 1 \
2476 /Resources << /Font << /F1 5 0 R >> >> >>",
2477 );
2478 b.object(3, "<< /Type /Page /Parent 2 0 R >>");
2479 b.object(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>");
2480 let doc = AsyncDocument::from_bytes(b.build(1)).await.unwrap();
2481 assert_eq!(doc.page_count(), 1);
2482 let record = doc.page_record(0).unwrap();
2483 assert_eq!(record.r, Some(ObjRef { num: 3, gen: 0 }));
2484 assert!(
2485 record.resources.get("Font").is_some(),
2486 "inherited resources"
2487 );
2488 assert!(doc.page_record(1).is_none());
2489 }
2490
2491 #[tokio::test]
2492 async fn kids_cycle_truncates_without_hanging() {
2493 let mut b = pdfboss_testkit::PdfBuilder::new();
2494 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2495 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
2497 b.object(3, "<< /Type /Pages /Kids [4 0 R 2 0 R] /Count 1 >>");
2498 b.object(4, "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 100] >>");
2499 let doc = AsyncDocument::from_bytes(b.build(1)).await.unwrap();
2500 assert_eq!(doc.page_count(), 1, "cycle back-edge yields no extra pages");
2501 }
2502
2503 #[tokio::test]
2504 async fn page_count_is_the_flattened_length() {
2505 let mut b = pdfboss_testkit::PdfBuilder::new();
2510 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2511 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 5 >>");
2512 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
2513 let doc = AsyncDocument::from_bytes(b.build(1)).await.unwrap();
2514 assert_eq!(doc.page_count(), 1);
2515 }
2516
2517 #[tokio::test]
2518 async fn encrypted_documents_are_rejected_at_open() {
2519 let mut b = pdfboss_testkit::PdfBuilder::new().trailer_extra("/Encrypt 9 0 R");
2523 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2524 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
2525 b.object(
2526 9,
2527 "<< /Filter /Standard /V 1 /R 2 /O (dummydummydummydummydummydummyd) \
2528 /U (dummydummydummydummydummydummyd) /P -3904 >>",
2529 );
2530 let data = b.build(1);
2531 assert!(
2532 matches!(
2533 AsyncDocument::from_bytes(data).await,
2534 Err(Error::Core(pdfboss_core::Error::Encrypted))
2535 ),
2536 "an encrypted document must be rejected at open, not opened with garbage reads"
2537 );
2538 }
2539}