Skip to main content

otf_pdf_syntax/
file.rs

1//! A PDF file opened for reading: header, xref chain, trailer, decryption
2//! and lazy, cached access to objects, with repair when the structure is
3//! broken (docs/PLAN.md ยง4, "Base layer" and "Lenient parsing").
4
5use std::collections::{BTreeMap, HashMap, HashSet};
6use std::sync::{Arc, Mutex, OnceLock};
7
8use crate::crypt::{CryptMethod, SecurityHandler};
9use crate::diag::{Diagnostic, DiagnosticKind, Diagnostics};
10use crate::error::{Error, Result};
11use crate::filters::{self, Decoded};
12use crate::header::{Header, find_header};
13use crate::lexer::{Lexer, Token, is_regular, is_whitespace};
14use crate::limits::{LimitKind, Limits, Strictness};
15use crate::object::{Dict, ObjId, Object, Stream};
16use crate::parser::{ObjectEnd, Parser};
17use crate::source::Source;
18use crate::xref::{self, XrefEntry, XrefMap};
19
20/// How to open a file.
21#[derive(Debug, Clone, Default)]
22pub struct OpenOptions {
23    /// Password for encrypted files, tried as user then owner password. The
24    /// empty password is always tried when this is empty.
25    pub password: Vec<u8>,
26    /// Whether repairs are allowed.
27    pub strictness: Strictness,
28    /// Resource limits.
29    pub limits: Limits,
30}
31
32/// A source that can be shared across threads.
33pub type SharedSource = Arc<dyn Source + Send + Sync>;
34
35/// An object stream, decoded once and cached.
36#[derive(Debug)]
37struct ObjStm {
38    data: Vec<u8>,
39    first: usize,
40    /// (object number, offset relative to `first`), in stream order.
41    index: Vec<(u32, usize)>,
42}
43
44/// What a whole-file scan found.
45#[derive(Debug, Default)]
46struct Scan {
47    entries: XrefMap,
48    trailer: Option<Dict>,
49}
50
51/// Initial window for parsing one object; grown on demand.
52const WINDOW: u64 = 4096;
53/// Chunk size for whole-file scans.
54const SCAN_CHUNK: u64 = 1 << 20;
55/// Nesting of loads (a `/Length` reference, an object stream) allowed.
56const MAX_LOAD_DEPTH: u32 = 8;
57
58/// A PDF file opened for reading. Objects are parsed on first access and
59/// cached; the file itself is never modified.
60pub struct File {
61    source: SharedSource,
62    len: u64,
63    header: Option<Header>,
64    /// Offset of the header: xref offsets in files with leading junk are
65    /// usually relative to it.
66    base: u64,
67    entries: XrefMap,
68    trailer: Dict,
69    /// Offsets of the xref sections read, newest first.
70    sections: Vec<u64>,
71    scan: OnceLock<Scan>,
72    cache: Mutex<HashMap<ObjId, Arc<Object>>>,
73    objstms: Mutex<HashMap<u32, Arc<ObjStm>>>,
74    security: Option<SecurityHandler>,
75    encrypt_ref: Option<ObjId>,
76    limits: Limits,
77    strictness: Strictness,
78    diagnostics: Diagnostics,
79}
80
81impl std::fmt::Debug for File {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        f.debug_struct("File")
84            .field("len", &self.len)
85            .field("header", &self.header)
86            .field("objects", &self.entries.len())
87            .field("encrypted", &self.security.is_some())
88            .finish_non_exhaustive()
89    }
90}
91
92impl File {
93    /// Opens `source`.
94    pub fn open(source: SharedSource, options: OpenOptions) -> Result<Self> {
95        let len = source.len();
96        let mut file = Self {
97            source,
98            len,
99            header: None,
100            base: 0,
101            entries: XrefMap::new(),
102            trailer: Dict::new(),
103            sections: Vec::new(),
104            scan: OnceLock::new(),
105            cache: Mutex::new(HashMap::new()),
106            objstms: Mutex::new(HashMap::new()),
107            security: None,
108            encrypt_ref: None,
109            limits: options.limits,
110            strictness: options.strictness,
111            diagnostics: Diagnostics::default(),
112        };
113
114        match find_header(&*file.source) {
115            Ok(h) => {
116                if h.has_leading_junk() {
117                    file.repair(DiagnosticKind::JunkBeforeHeader, Some(h.offset))?;
118                }
119                file.base = h.offset;
120                file.header = Some(h);
121            }
122            Err(crate::header::HeaderError::Source(e)) => return Err(Error::Source(e)),
123            Err(_) => file.repair(DiagnosticKind::MissingHeader, Some(0))?,
124        }
125
126        match file.load_xref_chain() {
127            Ok((entries, trailer, sections)) => {
128                file.entries = entries;
129                file.trailer = trailer;
130                file.sections = sections;
131            }
132            Err(e @ (Error::Source(_) | Error::Limit(LimitKind::Budget))) => return Err(e),
133            Err(e) => {
134                if file.strictness == Strictness::Strict {
135                    return Err(e);
136                }
137                file.adopt_scan()?;
138            }
139        }
140
141        // Decryption first: the catalog may sit in an encrypted object
142        // stream, and a wrong password must say so rather than "no catalog".
143        file.init_security(&options.password)?;
144
145        // A trailer without a usable catalog: the scan may find one.
146        if !file.root_is_usable() {
147            if file.strictness == Strictness::Strict {
148                return Err(Error::NoRoot);
149            }
150            file.adopt_scan()?;
151            if !file.root_is_usable() {
152                return Err(Error::NoRoot);
153            }
154        }
155        Ok(file)
156    }
157
158    /// Replaces the xref and trailer with what a whole-file scan finds.
159    fn adopt_scan(&mut self) -> Result<()> {
160        let scan = self.scan();
161        if scan.entries.is_empty() {
162            return Err(Error::NoXref);
163        }
164        let entries = scan.entries.clone();
165        let scanned_trailer = scan.trailer.clone();
166        self.diagnostics
167            .push(DiagnosticKind::XrefRebuilt, None, None);
168        // Keep what the xref said about objects the scan did not see (e.g.
169        // ones in object streams), but trust the scan for the rest.
170        for (num, entry) in entries {
171            self.entries.insert(num, entry);
172        }
173        if let Some(t) = scanned_trailer {
174            // Keep keys like /ID and /Encrypt that the found trailer lacks.
175            let mut merged = t;
176            for (k, v) in self.trailer.iter() {
177                if !merged.contains_key(k.as_bytes()) {
178                    merged.insert(k.clone(), v.clone());
179                }
180            }
181            self.trailer = merged;
182            self.diagnostics
183                .push(DiagnosticKind::TrailerRecovered, None, None);
184        }
185        if let Ok(mut cache) = self.cache.lock() {
186            cache.clear();
187        }
188        Ok(())
189    }
190
191    fn root_is_usable(&self) -> bool {
192        match self.trailer.get(b"Root") {
193            Some(Object::Ref(id)) => self
194                .load(*id, 0)
195                .is_ok_and(|o| o.as_dict().is_some_and(|d| !d.is_empty())),
196            Some(Object::Dict(_)) => true,
197            _ => false,
198        }
199    }
200
201    fn init_security(&mut self, password: &[u8]) -> Result<()> {
202        let encrypt = match self.trailer.get(b"Encrypt") {
203            None => return Ok(()),
204            Some(Object::Ref(id)) => {
205                self.encrypt_ref = Some(*id);
206                // Loaded before `security` is set, so it is not decrypted.
207                let obj = self.load(*id, 0)?;
208                obj.as_dict().cloned().ok_or(Error::WrongType {
209                    expected: "dictionary",
210                    found: obj.type_name(),
211                })?
212            }
213            Some(Object::Dict(d)) => d.clone(),
214            Some(other) => {
215                return Err(Error::WrongType {
216                    expected: "dictionary",
217                    found: other.type_name(),
218                });
219            }
220        };
221        let id0 = self
222            .trailer
223            .get_array(b"ID")
224            .and_then(|a| a.first())
225            .and_then(Object::as_string)
226            .unwrap_or_default()
227            .to_vec();
228        let handler = SecurityHandler::new(&encrypt, &id0, password)?;
229        self.security = Some(handler);
230        // Anything cached so far was read without decryption.
231        if let Ok(mut cache) = self.cache.lock() {
232            cache.clear();
233        }
234        if let Ok(mut objstms) = self.objstms.lock() {
235            objstms.clear();
236        }
237        Ok(())
238    }
239
240    /// Records a repair, or fails in strict mode.
241    fn repair(&self, kind: DiagnosticKind, offset: Option<u64>) -> Result<()> {
242        if self.strictness == Strictness::Strict {
243            return Err(Error::syntax(
244                offset.unwrap_or(0),
245                "repair needed in strict mode",
246            ));
247        }
248        self.diagnostics.push(kind, offset, None);
249        Ok(())
250    }
251
252    // ---- Accessors -------------------------------------------------------
253
254    /// The header, if one was found.
255    pub fn header(&self) -> Option<Header> {
256        self.header
257    }
258
259    /// The (merged) trailer dictionary.
260    pub fn trailer(&self) -> &Dict {
261        &self.trailer
262    }
263
264    /// The xref entries, by object number.
265    pub fn entries(&self) -> &XrefMap {
266        &self.entries
267    }
268
269    /// Offsets of the xref sections read, newest first. One per revision
270    /// for an incrementally updated file.
271    pub fn sections(&self) -> &[u64] {
272        &self.sections
273    }
274
275    /// The value written after the last `startxref`, as the file states it:
276    /// what an incremental update's `/Prev` must point to.
277    pub fn startxref(&self) -> Result<Option<u64>> {
278        self.find_startxref()
279    }
280
281    /// Whether the newest xref section is an xref stream (rather than a
282    /// classic table); `None` when no section was read.
283    pub fn newest_section_is_stream(&self) -> Result<Option<bool>> {
284        let Some(&at) = self.sections.first() else {
285            return Ok(None);
286        };
287        let head = self.read(at, 16)?;
288        let start = head.iter().position(|&b| !is_whitespace(b)).unwrap_or(0);
289        Ok(Some(
290            !head.get(start..).is_some_and(|h| h.starts_with(b"xref")),
291        ))
292    }
293
294    /// How far offsets written in this file are shifted from absolute byte
295    /// positions: the header offset when the file has junk before its header
296    /// and its xref offsets count from the header, else 0. New offsets
297    /// appended to the file must follow the same convention.
298    pub fn offset_shift(&self) -> Result<u64> {
299        let (Some(&at), Some(stated)) = (self.sections.first(), self.find_startxref()?) else {
300            return Ok(0);
301        };
302        Ok(if self.base > 0 && at == stated + self.base {
303            self.base
304        } else {
305            0
306        })
307    }
308
309    /// Whether the xref had to be rebuilt by scanning. Such a file has no
310    /// trustworthy revision chain to append to.
311    pub fn was_rebuilt(&self) -> bool {
312        self.diagnostics
313            .snapshot()
314            .iter()
315            .any(|d| d.kind == DiagnosticKind::XrefRebuilt)
316    }
317
318    /// The whole source, as an incremental save copies it.
319    pub fn read_all(&self) -> Result<Vec<u8>> {
320        let bytes = self.read(0, self.len)?;
321        if (bytes.len() as u64) < self.len {
322            return Err(Error::Source(crate::source::SourceError::new(
323                "source returned fewer bytes than its length",
324            )));
325        }
326        Ok(bytes)
327    }
328
329    /// The security handler, if the file is encrypted.
330    pub fn security(&self) -> Option<&SecurityHandler> {
331        self.security.as_ref()
332    }
333
334    /// Repairs made so far.
335    pub fn diagnostics(&self) -> Vec<Diagnostic> {
336        self.diagnostics.snapshot()
337    }
338
339    /// The diagnostics sink, for higher layers to add to.
340    pub fn diagnostics_sink(&self) -> &Diagnostics {
341        &self.diagnostics
342    }
343
344    /// The limits in force.
345    pub fn limits(&self) -> &Limits {
346        &self.limits
347    }
348
349    /// Whether repairs are allowed.
350    pub fn strictness(&self) -> Strictness {
351        self.strictness
352    }
353
354    /// File length in bytes.
355    pub fn len(&self) -> u64 {
356        self.len
357    }
358
359    /// Whether the file is empty.
360    pub fn is_empty(&self) -> bool {
361        self.len == 0
362    }
363
364    // ---- Objects ---------------------------------------------------------
365
366    /// The object `id`, decrypted. A missing object is an error here; use
367    /// [`File::resolve`] for the specification's "missing is null".
368    pub fn get(&self, id: ObjId) -> Result<Arc<Object>> {
369        if let Some(obj) = self.cache.lock().ok().and_then(|c| c.get(&id).cloned()) {
370            return Ok(obj);
371        }
372        self.limits.charge(1)?;
373        let obj = Arc::new(self.load_decrypted(id, 0)?);
374        if let Ok(mut cache) = self.cache.lock() {
375            cache.insert(id, Arc::clone(&obj));
376        }
377        Ok(obj)
378    }
379
380    /// Follows references until a direct object. Missing objects and
381    /// reference chains longer than the depth limit resolve to `null`.
382    pub fn resolve(&self, object: &Object) -> Object {
383        let mut current = object.clone();
384        for _ in 0..self.limits.max_depth {
385            match current {
386                Object::Ref(id) => match self.get(id) {
387                    Ok(o) => current = (*o).clone(),
388                    Err(_) => return Object::Null,
389                },
390                other => return other,
391            }
392        }
393        Object::Null
394    }
395
396    /// Resolves `object` as a dictionary (or a stream's dictionary).
397    pub fn resolve_dict(&self, object: &Object) -> Option<Dict> {
398        match self.resolve(object) {
399            Object::Dict(d) => Some(d),
400            Object::Stream(s) => Some(s.dict),
401            _ => None,
402        }
403    }
404
405    /// Decodes a stream's data through its filters. Image codecs are left
406    /// encoded and listed in [`Decoded::remaining`].
407    pub fn decode_stream(&self, stream: &Stream) -> Result<Decoded> {
408        let mut dict = stream.dict.clone();
409        for key in [&b"Filter"[..], b"DecodeParms"] {
410            if let Some(v) = dict.get(key).cloned() {
411                let resolved = self.resolve_shallow(&v);
412                dict.insert(key, resolved);
413            }
414        }
415        filters::decode(
416            &stream.raw,
417            &filters::filter_chain(&dict),
418            &self.limits,
419            self.strictness,
420            Some(&self.diagnostics),
421        )
422    }
423
424    /// Resolves `object` and any references directly inside it (one level).
425    fn resolve_shallow(&self, object: &Object) -> Object {
426        match self.resolve(object) {
427            Object::Array(items) => Object::Array(items.iter().map(|i| self.resolve(i)).collect()),
428            other => other,
429        }
430    }
431
432    fn load_decrypted(&self, id: ObjId, depth: u32) -> Result<Object> {
433        let (mut obj, direct) = self.load_with_origin(id, depth)?;
434        // Objects inside object streams were decrypted with their stream.
435        if direct
436            && Some(id) != self.encrypt_ref
437            && let Some(sec) = &self.security
438        {
439            self.decrypt(sec, id, &mut obj);
440        }
441        Ok(obj)
442    }
443
444    fn decrypt(&self, sec: &SecurityHandler, id: ObjId, obj: &mut Object) {
445        sec.decrypt_strings(id, obj);
446        if let Object::Stream(s) = obj {
447            let method = self.stream_method(sec, &s.dict);
448            s.raw = sec.decrypt(id, method, &s.raw).into();
449        }
450    }
451
452    fn stream_method(&self, sec: &SecurityHandler, dict: &Dict) -> CryptMethod {
453        if dict.is_type(b"XRef") {
454            return CryptMethod::Identity;
455        }
456        if dict.is_type(b"Metadata") && !sec.encrypt_metadata() {
457            return CryptMethod::Identity;
458        }
459        // A /Crypt filter names its own crypt filter (ยง7.4.10); only the
460        // Identity one is common, and it means "not encrypted".
461        let crypt = filters::filter_chain(dict)
462            .into_iter()
463            .find(|f| f.name.as_bytes() == b"Crypt");
464        if let Some(f) = crypt {
465            let name = f.params.as_ref().and_then(|p| p.get_name(b"Name"));
466            if name.is_none_or(|n| n == b"Identity") {
467                return CryptMethod::Identity;
468            }
469        }
470        if dict.is_type(b"EmbeddedFile") {
471            return sec.embedded_file_method();
472        }
473        sec.stream_method()
474    }
475
476    /// Loads an object without decryption (used directly for the `/Encrypt`
477    /// dictionary and while bootstrapping).
478    fn load(&self, id: ObjId, depth: u32) -> Result<Object> {
479        self.load_with_origin(id, depth).map(|(o, _)| o)
480    }
481
482    /// Loads an object; the flag says whether it was written directly (and
483    /// so needs decrypting) rather than inside an object stream.
484    fn load_with_origin(&self, id: ObjId, depth: u32) -> Result<(Object, bool)> {
485        if depth > MAX_LOAD_DEPTH {
486            return Err(Error::Limit(LimitKind::Depth));
487        }
488        let entry = self.entries.get(&id.num).copied();
489        let result = match entry {
490            Some(XrefEntry::InUse { offset, generation }) => {
491                if generation != id.generation {
492                    self.repair(DiagnosticKind::ObjectIdMismatch, Some(offset))?;
493                }
494                self.parse_at_xref_offset(id, offset, depth)
495                    .map(|o| (o, true))
496            }
497            Some(XrefEntry::Compressed { stream, index }) => self
498                .load_compressed(id, stream, index, depth)
499                .map(|o| (o, false)),
500            Some(XrefEntry::Free) | None => Err(Error::MissingObject(id)),
501        };
502        match result {
503            Ok(found) => Ok(found),
504            Err(e @ (Error::Limit(_) | Error::Source(_))) => Err(e),
505            Err(e) if self.strictness == Strictness::Strict => Err(e),
506            // Explicitly free (deleted in a later revision): the old bytes
507            // still in the file must not resurrect it.
508            Err(e) if entry == Some(XrefEntry::Free) => Err(e),
509            Err(e) => {
510                // The xref is wrong or incomplete: look where a scan found it.
511                let scanned = self.scan().entries.get(&id.num).copied();
512                match scanned {
513                    Some(XrefEntry::InUse { offset, .. }) if !matches!(entry, Some(XrefEntry::InUse { offset: o, .. }) if o == offset) =>
514                    {
515                        let obj = self.parse_at(offset, Some(id), depth)?;
516                        if entry.is_some() {
517                            self.diagnostics.push(
518                                DiagnosticKind::BadXrefEntry,
519                                Some(offset),
520                                Some(id),
521                            );
522                        }
523                        Ok((obj, true))
524                    }
525                    Some(XrefEntry::Compressed { stream, index })
526                        if !matches!(entry, Some(XrefEntry::Compressed { .. })) =>
527                    {
528                        Ok((self.load_compressed(id, stream, index, depth)?, false))
529                    }
530                    _ => Err(e),
531                }
532            }
533        }
534    }
535
536    fn parse_at_xref_offset(&self, id: ObjId, offset: u64, depth: u32) -> Result<Object> {
537        match self.parse_at(offset, Some(id), depth) {
538            Err(e) if self.base > 0 => self
539                .parse_at(offset + self.base, Some(id), depth)
540                .map_err(|_| e),
541            other => other,
542        }
543    }
544
545    /// Reads `len` bytes at `offset` (fewer at the end of the file).
546    fn read(&self, offset: u64, len: u64) -> Result<Vec<u8>> {
547        let len = len.min(self.len.saturating_sub(offset));
548        let mut buf =
549            vec![0u8; usize::try_from(len).map_err(|_| Error::Limit(LimitKind::ObjectSize))?];
550        let mut filled = 0;
551        while filled < buf.len() {
552            let Some(rest) = buf.get_mut(filled..) else {
553                break;
554            };
555            let n = self.source.read_at(offset + filled as u64, rest)?;
556            if n == 0 {
557                break;
558            }
559            filled += n;
560        }
561        buf.truncate(filled);
562        Ok(buf)
563    }
564
565    /// Runs `parse` over a window at `offset`, growing the window while the
566    /// parse runs out of input before the end of the file.
567    fn with_window<T>(&self, offset: u64, mut parse: impl FnMut(&[u8]) -> Result<T>) -> Result<T> {
568        let mut size = WINDOW;
569        loop {
570            let buf = self.read(offset, size)?;
571            match parse(&buf) {
572                Err(Error::UnexpectedEof) if offset + size < self.len => {
573                    if size >= self.limits.max_object_size {
574                        return Err(Error::Limit(LimitKind::ObjectSize));
575                    }
576                    size = (size * 4).min(self.limits.max_object_size);
577                }
578                other => return other,
579            }
580        }
581    }
582
583    /// Parses the indirect object at `offset`, including stream data.
584    fn parse_at(&self, offset: u64, expected: Option<ObjId>, depth: u32) -> Result<Object> {
585        if offset >= self.len {
586            return Err(Error::syntax(offset, "object offset past end of file"));
587        }
588        let (id, object, end) = self.with_window(offset, |buf| {
589            Parser::new(buf, offset, &self.limits, self.strictness)
590                .with_diagnostics(&self.diagnostics)
591                .parse_indirect(expected)
592        })?;
593        let ObjectEnd::Stream { data_start } = end else {
594            return Ok(object);
595        };
596        let Object::Dict(dict) = object else {
597            return Err(Error::syntax(offset, "stream without dictionary"));
598        };
599        let data_offset = offset + data_start as u64;
600        let declared = match dict.get(b"Length") {
601            Some(Object::Int(n)) => u64::try_from(*n).ok(),
602            Some(Object::Ref(r)) if *r != id => self
603                .load_decrypted(*r, depth + 1)
604                .ok()
605                .and_then(|o| o.as_int())
606                .and_then(|n| u64::try_from(n).ok()),
607            _ => None,
608        };
609        let length = match declared {
610            Some(n) if self.endstream_at(data_offset.saturating_add(n))? => n,
611            _ => {
612                self.repair(DiagnosticKind::BadStreamLength, Some(data_offset))?;
613                self.find_stream_end(data_offset)?
614            }
615        };
616        let raw = self.read(data_offset, length)?;
617        Ok(Object::Stream(Stream {
618            dict,
619            raw: raw.into(),
620        }))
621    }
622
623    /// Whether `endstream` follows `pos` (after optional whitespace).
624    fn endstream_at(&self, pos: u64) -> Result<bool> {
625        if pos > self.len {
626            return Ok(false);
627        }
628        let buf = self.read(pos, 32)?;
629        let start = buf
630            .iter()
631            .position(|&b| !is_whitespace(b))
632            .unwrap_or(buf.len());
633        Ok(buf
634            .get(start..)
635            .is_some_and(|r| r.starts_with(b"endstream")))
636    }
637
638    /// Finds the end of stream data starting at `start` by scanning for
639    /// `endstream` (or, failing that, `endobj`), dropping the end-of-line
640    /// marker before it.
641    fn find_stream_end(&self, start: u64) -> Result<u64> {
642        let found = self
643            .find_forward(start, b"endstream")?
644            .or(self.find_forward(start, b"endobj")?);
645        let Some(end) = found else {
646            return Ok(self.len.saturating_sub(start));
647        };
648        let tail = self.read(end.saturating_sub(2), 2.min(end))?;
649        let eol = match tail.as_slice() {
650            [b'\r', b'\n'] => 2,
651            [.., b'\n' | b'\r'] => 1,
652            _ => 0,
653        };
654        Ok(end.saturating_sub(eol).saturating_sub(start))
655    }
656
657    /// The offset of the first `needle` at or after `from`.
658    fn find_forward(&self, from: u64, needle: &[u8]) -> Result<Option<u64>> {
659        let overlap = needle.len() as u64;
660        let mut pos = from;
661        while pos < self.len {
662            let chunk = self.read(pos, SCAN_CHUNK)?;
663            if let Some(i) = chunk.windows(needle.len()).position(|w| w == needle) {
664                return Ok(Some(pos + i as u64));
665            }
666            if chunk.len() as u64 <= overlap {
667                break;
668            }
669            pos += chunk.len() as u64 - overlap;
670        }
671        Ok(None)
672    }
673
674    fn load_compressed(&self, id: ObjId, stream: u32, index: u32, depth: u32) -> Result<Object> {
675        if stream == id.num {
676            return Err(Error::syntax(0, "object stream contains itself"));
677        }
678        let objstm = self.objstm(stream, depth + 1)?;
679        let by_index = objstm
680            .index
681            .get(index as usize)
682            .filter(|(n, _)| *n == id.num);
683        // Lenient: the index may be wrong while the number is right.
684        let entry = match by_index {
685            Some(e) => Some(e),
686            None => objstm.index.iter().find(|(n, _)| *n == id.num),
687        };
688        let Some(&(_, offset)) = entry else {
689            return Err(Error::MissingObject(id));
690        };
691        let start = objstm.first.saturating_add(offset);
692        let buf = objstm.data.get(start..).unwrap_or_default();
693        Parser::new(buf, 0, &self.limits, self.strictness)
694            .with_diagnostics(&self.diagnostics)
695            .parse_object()
696    }
697
698    fn objstm(&self, num: u32, depth: u32) -> Result<Arc<ObjStm>> {
699        if let Some(s) = self.objstms.lock().ok().and_then(|m| m.get(&num).cloned()) {
700            return Ok(s);
701        }
702        let id = ObjId::new(num, 0);
703        let obj = self.load_decrypted(id, depth)?;
704        let Object::Stream(stream) = obj else {
705            return Err(Error::WrongType {
706                expected: "object stream",
707                found: obj.type_name(),
708            });
709        };
710        let decoded = self.decode_stream(&stream)?;
711        if !decoded.remaining.is_empty() {
712            return Err(Error::Filter("object stream uses an unsupported filter"));
713        }
714        let n = stream.dict.get_int(b"N").unwrap_or(0).max(0) as usize;
715        let first = stream.dict.get_int(b"First").unwrap_or(0).max(0) as usize;
716        let header = decoded
717            .data
718            .get(..first.min(decoded.data.len()))
719            .unwrap_or_default();
720        let mut lexer = Lexer::new(header);
721        let mut index = Vec::with_capacity(n.min(header.len() / 2 + 1));
722        for _ in 0..n {
723            match (lexer.next_token(), lexer.next_token()) {
724                (Ok(Token::Int(obj)), Ok(Token::Int(off))) => {
725                    if let (Ok(obj), Ok(off)) = (u32::try_from(obj), usize::try_from(off)) {
726                        index.push((obj, off));
727                    }
728                }
729                _ => break,
730            }
731        }
732        let objstm = Arc::new(ObjStm {
733            data: decoded.data,
734            first,
735            index,
736        });
737        if let Ok(mut m) = self.objstms.lock() {
738            m.insert(num, Arc::clone(&objstm));
739        }
740        Ok(objstm)
741    }
742
743    // ---- Xref chain ------------------------------------------------------
744
745    fn find_startxref(&self) -> Result<Option<u64>> {
746        for tail in [1024u64, SCAN_CHUNK] {
747            let start = self.len.saturating_sub(tail);
748            let buf = self.read(start, tail)?;
749            let Some(i) = buf.windows(9).rposition(|w| w == b"startxref") else {
750                continue;
751            };
752            let mut lexer = Lexer::new(buf.get(i + 9..).unwrap_or_default());
753            if let Ok(Token::Int(n)) = lexer.next_token() {
754                return Ok(u64::try_from(n).ok());
755            }
756            return Ok(None);
757        }
758        Ok(None)
759    }
760
761    #[allow(clippy::type_complexity)]
762    fn load_xref_chain(&self) -> Result<(XrefMap, Dict, Vec<u64>)> {
763        let Some(start) = self.find_startxref()? else {
764            self.repair(DiagnosticKind::BadStartxref, None)?;
765            return Err(Error::NoXref);
766        };
767        let mut entries = XrefMap::new();
768        let mut trailer = Dict::new();
769        let mut sections = Vec::new();
770        let mut seen = HashSet::new();
771        let mut next = Some(start);
772        while let Some(offset) = next.take() {
773            if !seen.insert(offset) || sections.len() as u32 >= self.limits.max_xref_sections {
774                self.repair(DiagnosticKind::XrefLoop, Some(offset))?;
775                break;
776            }
777            let (section, section_trailer, at) = match self.load_section_near(offset) {
778                Ok(s) => s,
779                Err(e @ (Error::Source(_) | Error::Limit(LimitKind::Budget))) => return Err(e),
780                Err(e) if sections.is_empty() => {
781                    if self.strictness == Strictness::Lenient {
782                        self.diagnostics
783                            .push(DiagnosticKind::BadStartxref, Some(offset), None);
784                    }
785                    return Err(e);
786                }
787                Err(_) => {
788                    // An older section is broken: keep the newer ones; the
789                    // scan covers objects only the broken one knew.
790                    self.repair(DiagnosticKind::BadXrefSection, Some(offset))?;
791                    break;
792                }
793            };
794            sections.push(at);
795            for (num, entry) in section {
796                entries.entry(num).or_insert(entry);
797            }
798            for (k, v) in section_trailer.iter() {
799                if !trailer.contains_key(k.as_bytes()) {
800                    trailer.insert(k.clone(), v.clone());
801                }
802            }
803            next = section_trailer
804                .get_int(b"Prev")
805                .and_then(|p| u64::try_from(p).ok());
806        }
807        trailer.remove(b"Prev");
808        trailer.remove(b"XRefStm");
809        Ok((entries, trailer, sections))
810    }
811
812    /// Loads the section at `offset`, or at `offset` relative to the header
813    /// for files with leading junk.
814    fn load_section_near(&self, offset: u64) -> Result<(XrefMap, Dict, u64)> {
815        match self.load_section(offset) {
816            Err(e) if self.base > 0 => self
817                .load_section(offset + self.base)
818                .map(|(m, t)| (m, t, offset + self.base))
819                .map_err(|_| e),
820            other => other.map(|(m, t)| (m, t, offset)),
821        }
822    }
823
824    fn load_section(&self, offset: u64) -> Result<(XrefMap, Dict)> {
825        if offset >= self.len {
826            return Err(Error::syntax(offset, "xref offset past end of file"));
827        }
828        let head = self.read(offset, 16)?;
829        let skip = head.iter().position(|&b| !is_whitespace(b)).unwrap_or(0) as u64;
830        if head
831            .get(skip as usize..)
832            .is_some_and(|h| h.starts_with(b"xref"))
833        {
834            let at = offset + skip;
835            let (mut entries, trailer) = self.with_window(at, |buf| {
836                xref::parse_table(buf, at, &self.limits, self.strictness, &self.diagnostics)
837            })?;
838            // Hybrid file: the xref stream adds objects the table hides.
839            if let Some(stm) = trailer
840                .get_int(b"XRefStm")
841                .and_then(|o| u64::try_from(o).ok())
842            {
843                match self.load_xref_stream(stm) {
844                    Ok((stream_entries, _)) => {
845                        for (num, entry) in stream_entries {
846                            let replace = matches!(entries.get(&num), None | Some(XrefEntry::Free));
847                            if replace {
848                                entries.insert(num, entry);
849                            }
850                        }
851                    }
852                    Err(e @ Error::Source(_)) => return Err(e),
853                    Err(_) => self.repair(DiagnosticKind::BadXrefSection, Some(stm))?,
854                }
855            }
856            return Ok((entries, trailer));
857        }
858        self.load_xref_stream(offset)
859    }
860
861    fn load_xref_stream(&self, offset: u64) -> Result<(XrefMap, Dict)> {
862        let obj = self.parse_at(offset, None, MAX_LOAD_DEPTH)?;
863        if !xref::is_xref_stream(&obj) {
864            return Err(Error::syntax(offset, "expected an xref stream"));
865        }
866        let Object::Stream(stream) = obj else {
867            return Err(Error::syntax(offset, "expected an xref stream"));
868        };
869        let decoded = filters::decode(
870            &stream.raw,
871            &filters::filter_chain(&stream.dict),
872            &self.limits,
873            self.strictness,
874            Some(&self.diagnostics),
875        )?;
876        let entries = xref::parse_stream(&stream.dict, &decoded.data, &self.limits)?;
877        let mut trailer = stream.dict;
878        for key in [
879            &b"Type"[..],
880            b"W",
881            b"Index",
882            b"Filter",
883            b"DecodeParms",
884            b"Length",
885        ] {
886            trailer.remove(key);
887        }
888        Ok((entries, trailer))
889    }
890
891    // ---- Repair ----------------------------------------------------------
892
893    /// Scans the whole file once for `num gen obj` and `trailer`.
894    fn scan(&self) -> &Scan {
895        self.scan
896            .get_or_init(|| self.scan_file().unwrap_or_default())
897    }
898
899    fn scan_file(&self) -> Result<Scan> {
900        let mut found: BTreeMap<u32, (u64, u16)> = BTreeMap::new();
901        let mut trailers = Vec::new();
902        let mut candidates: Vec<(u64, Flags)> = Vec::new();
903        let overlap = 64u64;
904        let mut pos = 0u64;
905        while pos < self.len {
906            self.limits.charge(SCAN_CHUNK)?;
907            let chunk = self.read(pos, SCAN_CHUNK)?;
908            let last = pos + chunk.len() as u64 >= self.len;
909            // Only report hits that start before the overlap, so none is
910            // seen twice; the next chunk sees the rest.
911            let limit = if last {
912                chunk.len()
913            } else {
914                chunk.len().saturating_sub(overlap as usize)
915            };
916            for i in 0..limit {
917                let Some(window) = chunk.get(i..) else { break };
918                if window.starts_with(b"obj") && !is_end_keyword(&chunk, i) {
919                    let after_ok = window.get(3).is_none_or(|&b| !is_regular(b));
920                    if after_ok
921                        && let Some((start, num, generation)) = object_header_before(&chunk, i)
922                    {
923                        let offset = pos + start as u64;
924                        if num <= self.limits.max_objects {
925                            // Later definitions (incremental updates) win.
926                            found.insert(num, (offset, generation));
927                            let body = chunk.get(i..(i + 600).min(chunk.len())).unwrap_or_default();
928                            candidates.push((offset, Flags::of(body)));
929                        }
930                    }
931                } else if window.starts_with(b"trailer") {
932                    trailers.push(pos + i as u64 + 7);
933                }
934            }
935            if last || chunk.len() as u64 <= overlap {
936                break;
937            }
938            pos += chunk.len() as u64 - overlap;
939        }
940
941        let mut scan = Scan::default();
942        for (&num, &(offset, generation)) in &found {
943            scan.entries
944                .insert(num, XrefEntry::InUse { offset, generation });
945        }
946
947        // Trailer: the last `trailer` dictionary with a /Root, else an xref
948        // stream's dictionary, else a synthesized one around a catalog.
949        let mut trailer: Option<Dict> = None;
950        for &at in trailers.iter().rev() {
951            let parsed = self.with_window(at, |buf| {
952                Parser::new(buf, at, &self.limits, Strictness::Lenient).parse_dict()
953            });
954            if let Ok(d) = parsed {
955                match &mut trailer {
956                    None => trailer = Some(d),
957                    Some(t) => {
958                        for (k, v) in d.iter() {
959                            if !t.contains_key(k.as_bytes()) {
960                                t.insert(k.clone(), v.clone());
961                            }
962                        }
963                    }
964                }
965            }
966        }
967        // Object streams found by the scan add their objects.
968        let live: HashSet<u64> = found.values().map(|&(o, _)| o).collect();
969        for &(offset, flags) in candidates.iter().filter(|(o, _)| live.contains(o)) {
970            if flags.objstm
971                && let Ok(Object::Stream(s)) = self.parse_at(offset, None, MAX_LOAD_DEPTH)
972                && let Ok(d) = filters::decode(
973                    &s.raw,
974                    &filters::filter_chain(&s.dict),
975                    &self.limits,
976                    Strictness::Lenient,
977                    None,
978                )
979            {
980                let n = s.dict.get_int(b"N").unwrap_or(0).max(0) as usize;
981                let first = s.dict.get_int(b"First").unwrap_or(0).max(0) as usize;
982                let mut lexer =
983                    Lexer::new(d.data.get(..first.min(d.data.len())).unwrap_or_default());
984                let stream_num = found
985                    .iter()
986                    .find(|(_, (o, _))| *o == offset)
987                    .map(|(&n, _)| n);
988                for index in 0..n {
989                    let (Ok(Token::Int(obj)), Ok(Token::Int(_))) =
990                        (lexer.next_token(), lexer.next_token())
991                    else {
992                        break;
993                    };
994                    if let (Ok(obj), Some(stream)) = (u32::try_from(obj), stream_num) {
995                        scan.entries.entry(obj).or_insert(XrefEntry::Compressed {
996                            stream,
997                            index: index as u32,
998                        });
999                    }
1000                }
1001            }
1002            if flags.xref
1003                && trailer.as_ref().is_none_or(|t| !t.contains_key(b"Root"))
1004                && let Ok(Object::Stream(s)) = self.parse_at(offset, None, MAX_LOAD_DEPTH)
1005                && s.dict.contains_key(b"Root")
1006            {
1007                let mut d = s.dict;
1008                for key in [
1009                    &b"Type"[..],
1010                    b"W",
1011                    b"Index",
1012                    b"Filter",
1013                    b"DecodeParms",
1014                    b"Length",
1015                    b"Prev",
1016                ] {
1017                    d.remove(key);
1018                }
1019                trailer = Some(d);
1020            }
1021        }
1022        if trailer.as_ref().is_none_or(|t| !t.contains_key(b"Root")) {
1023            // Prefer the last catalog in the file (the newest revision).
1024            for &(offset, flags) in candidates.iter().rev().filter(|(o, _)| live.contains(o)) {
1025                if !flags.catalog {
1026                    continue;
1027                }
1028                let Ok(obj) = self.with_window(offset, |buf| {
1029                    Parser::new(buf, offset, &self.limits, Strictness::Lenient).parse_indirect(None)
1030                }) else {
1031                    continue;
1032                };
1033                if obj.1.as_dict().is_some_and(|d| d.is_type(b"Catalog")) {
1034                    let t = trailer.get_or_insert_with(Dict::new);
1035                    t.insert("Root", obj.0);
1036                    break;
1037                }
1038            }
1039        }
1040        scan.trailer = trailer;
1041        Ok(scan)
1042    }
1043}
1044
1045/// What the start of a scanned object's body mentions.
1046#[derive(Debug, Clone, Copy, Default)]
1047struct Flags {
1048    objstm: bool,
1049    xref: bool,
1050    catalog: bool,
1051}
1052
1053impl Flags {
1054    fn of(body: &[u8]) -> Self {
1055        let has = |n: &[u8]| body.windows(n.len()).any(|w| w == n);
1056        Self {
1057            objstm: has(b"/ObjStm"),
1058            xref: has(b"/XRef"),
1059            catalog: has(b"/Catalog"),
1060        }
1061    }
1062}
1063
1064/// Whether the `obj` at `i` is the tail of `endobj`.
1065fn is_end_keyword(buf: &[u8], i: usize) -> bool {
1066    i >= 3 && buf.get(i - 3..i) == Some(b"end")
1067}
1068
1069/// Given `obj` at `i`, finds `num gen` before it: returns where `num`
1070/// starts and the two numbers.
1071fn object_header_before(buf: &[u8], i: usize) -> Option<(usize, u32, u16)> {
1072    let mut j = i;
1073    while j > 0 && buf.get(j - 1).copied().is_some_and(is_whitespace) {
1074        j -= 1;
1075    }
1076    let gen_end = j;
1077    while j > 0 && buf.get(j - 1).is_some_and(u8::is_ascii_digit) {
1078        j -= 1;
1079    }
1080    if j == gen_end || gen_end - j > 5 {
1081        return None;
1082    }
1083    let gen_start = j;
1084    while j > 0 && buf.get(j - 1).copied().is_some_and(is_whitespace) {
1085        j -= 1;
1086    }
1087    if j == gen_start {
1088        return None;
1089    }
1090    let num_end = j;
1091    while j > 0 && buf.get(j - 1).is_some_and(u8::is_ascii_digit) {
1092        j -= 1;
1093    }
1094    if j == num_end || num_end - j > 10 {
1095        return None;
1096    }
1097    if j > 0 && buf.get(j - 1).copied().is_some_and(is_regular) {
1098        return None;
1099    }
1100    let num: u32 = std::str::from_utf8(buf.get(j..num_end)?)
1101        .ok()?
1102        .parse()
1103        .ok()?;
1104    let generation: u16 = std::str::from_utf8(buf.get(gen_start..gen_end)?)
1105        .ok()?
1106        .parse()
1107        .ok()?;
1108    Some((j, num, generation))
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113    use super::*;
1114
1115    fn open(bytes: &[u8]) -> Result<File> {
1116        File::open(Arc::new(bytes.to_vec()), OpenOptions::default())
1117    }
1118
1119    fn open_strict(bytes: &[u8]) -> Result<File> {
1120        File::open(
1121            Arc::new(bytes.to_vec()),
1122            OpenOptions {
1123                strictness: Strictness::Strict,
1124                ..OpenOptions::default()
1125            },
1126        )
1127    }
1128
1129    /// A small valid file with correct offsets.
1130    fn build(objects: &[(u32, &str)], trailer_extra: &str) -> Vec<u8> {
1131        let mut out = b"%PDF-1.7\n".to_vec();
1132        let mut offsets = Vec::new();
1133        for (num, body) in objects {
1134            offsets.push((*num, out.len()));
1135            out.extend_from_slice(format!("{num} 0 obj\n{body}\nendobj\n").as_bytes());
1136        }
1137        let size = objects.iter().map(|(n, _)| n).max().copied().unwrap_or(0) + 1;
1138        let xref = out.len();
1139        out.extend_from_slice(format!("xref\n0 {size}\n0000000000 65535 f \n").as_bytes());
1140        for n in 1..size {
1141            match offsets.iter().find(|(m, _)| *m == n) {
1142                Some((_, o)) => out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()),
1143                None => out.extend_from_slice(b"0000000000 65535 f \n"),
1144            }
1145        }
1146        out.extend_from_slice(
1147            format!(
1148                "trailer\n<< /Size {size} /Root 1 0 R {trailer_extra}>>\nstartxref\n{xref}\n%%EOF\n"
1149            )
1150            .as_bytes(),
1151        );
1152        out
1153    }
1154
1155    const CATALOG: (u32, &str) = (1, "<< /Type /Catalog /Pages 2 0 R >>");
1156    const PAGES: (u32, &str) = (2, "<< /Type /Pages /Kids [] /Count 0 >>");
1157
1158    #[test]
1159    fn opens_a_valid_file() {
1160        let f = open(&build(&[CATALOG, PAGES, (3, "(hello)")], "")).unwrap();
1161        assert_eq!(f.header().map(|h| h.version.minor), Some(7));
1162        assert!(f.diagnostics().is_empty(), "{:?}", f.diagnostics());
1163        assert_eq!(f.sections().len(), 1);
1164        let root = f.trailer().get_ref(b"Root").unwrap();
1165        assert!(f.get(root).unwrap().as_dict().unwrap().is_type(b"Catalog"));
1166        assert_eq!(
1167            f.get(ObjId::new(3, 0)).unwrap().as_string(),
1168            Some(&b"hello"[..])
1169        );
1170        assert_eq!(
1171            f.resolve(&Object::Ref(ObjId::new(3, 0))).as_string(),
1172            Some(&b"hello"[..])
1173        );
1174        assert_eq!(f.resolve(&Object::Ref(ObjId::new(99, 0))), Object::Null);
1175        assert!(matches!(
1176            f.get(ObjId::new(99, 0)),
1177            Err(Error::MissingObject(_))
1178        ));
1179    }
1180
1181    #[test]
1182    fn stream_with_indirect_length() {
1183        let f = open(&build(
1184            &[
1185                CATALOG,
1186                PAGES,
1187                (3, "<< /Length 4 0 R >>\nstream\nabcdef\nendstream"),
1188                (4, "6"),
1189            ],
1190            "",
1191        ))
1192        .unwrap();
1193        let s = f.get(ObjId::new(3, 0)).unwrap();
1194        assert_eq!(&*s.as_stream().unwrap().raw, b"abcdef");
1195        assert!(f.diagnostics().is_empty());
1196    }
1197
1198    #[test]
1199    fn wrong_stream_length_is_repaired() {
1200        let bytes = build(
1201            &[
1202                CATALOG,
1203                PAGES,
1204                (3, "<< /Length 2 >>\nstream\nabcdef\r\nendstream"),
1205            ],
1206            "",
1207        );
1208        let f = open(&bytes).unwrap();
1209        let s = f.get(ObjId::new(3, 0)).unwrap();
1210        assert_eq!(&*s.as_stream().unwrap().raw, b"abcdef");
1211        assert_eq!(f.diagnostics()[0].kind, DiagnosticKind::BadStreamLength);
1212        let strict = open_strict(&bytes).unwrap();
1213        assert!(strict.get(ObjId::new(3, 0)).is_err());
1214    }
1215
1216    #[test]
1217    fn rebuilds_a_missing_xref() {
1218        let mut bytes = build(&[CATALOG, PAGES], "");
1219        let cut = bytes.windows(4).position(|w| w == b"xref").unwrap();
1220        bytes.truncate(cut);
1221        bytes.extend_from_slice(b"trailer\n<< /Root 1 0 R >>\n%%EOF\n");
1222        let f = open(&bytes).unwrap();
1223        assert!(
1224            f.diagnostics()
1225                .iter()
1226                .any(|d| d.kind == DiagnosticKind::XrefRebuilt)
1227        );
1228        assert!(
1229            f.get(ObjId::new(2, 0))
1230                .unwrap()
1231                .as_dict()
1232                .unwrap()
1233                .is_type(b"Pages")
1234        );
1235        assert!(open_strict(&bytes).is_err());
1236    }
1237
1238    #[test]
1239    fn finds_a_catalog_without_any_trailer() {
1240        let bytes = b"%PDF-1.4\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [] /Count 0 >> endobj\n";
1241        let f = open(bytes).unwrap();
1242        assert_eq!(f.trailer().get_ref(b"Root"), Some(ObjId::new(1, 0)));
1243    }
1244
1245    #[test]
1246    fn nothing_to_find() {
1247        assert!(open(b"%PDF-1.4\nnothing here\n").is_err());
1248        assert!(open(b"").is_err());
1249    }
1250
1251    #[test]
1252    fn wrong_xref_offsets_fall_back_to_scan() {
1253        let bytes = build(&[CATALOG, PAGES, (3, "42")], "");
1254        let text = String::from_utf8(bytes).unwrap();
1255        // Shift every in-use offset by 3.
1256        let fixed: String = text
1257            .lines()
1258            .map(|l| {
1259                if l.len() == 19 && l.ends_with(" 00000 n ") {
1260                    let n: u64 = l[..10].parse().unwrap();
1261                    format!("{:010} 00000 n \n", n - 3)
1262                } else {
1263                    format!("{l}\n")
1264                }
1265            })
1266            .collect();
1267        let f = open(fixed.as_bytes()).unwrap();
1268        assert_eq!(f.get(ObjId::new(3, 0)).unwrap().as_int(), Some(42));
1269        assert!(
1270            f.diagnostics()
1271                .iter()
1272                .any(|d| d.kind == DiagnosticKind::BadXrefEntry)
1273        );
1274    }
1275
1276    #[test]
1277    fn incremental_update_newest_wins() {
1278        let mut bytes = build(&[CATALOG, PAGES, (3, "(old)")], "");
1279        let first_xref = {
1280            let s = String::from_utf8_lossy(&bytes);
1281            let i = s.rfind("startxref\n").unwrap() + 10;
1282            s[i..].lines().next().unwrap().parse::<usize>().unwrap()
1283        };
1284        let obj_at = bytes.len();
1285        bytes.extend_from_slice(b"3 0 obj\n(new)\nendobj\n");
1286        let xref_at = bytes.len();
1287        bytes.extend_from_slice(
1288            format!("xref\n3 1\n{obj_at:010} 00000 n \ntrailer\n<< /Size 4 /Root 1 0 R /Prev {first_xref} >>\nstartxref\n{xref_at}\n%%EOF\n").as_bytes(),
1289        );
1290        let f = open(&bytes).unwrap();
1291        assert_eq!(f.sections().len(), 2);
1292        assert_eq!(
1293            f.get(ObjId::new(3, 0)).unwrap().as_string(),
1294            Some(&b"new"[..])
1295        );
1296        assert!(f.trailer().get(b"Prev").is_none());
1297    }
1298
1299    #[test]
1300    fn prev_loop_is_cut() {
1301        let bytes = build(&[CATALOG, PAGES], "/Prev 0 ");
1302        let xref = String::from_utf8_lossy(&bytes);
1303        let at = xref.rfind("startxref\n").unwrap() + 10;
1304        let offset: usize = xref[at..].lines().next().unwrap().parse().unwrap();
1305        let looped = String::from_utf8(bytes.clone())
1306            .unwrap()
1307            .replace("/Prev 0", &format!("/Prev {offset}"));
1308        let f = open(looped.as_bytes()).unwrap();
1309        assert!(
1310            f.diagnostics()
1311                .iter()
1312                .any(|d| d.kind == DiagnosticKind::XrefLoop)
1313        );
1314    }
1315
1316    #[test]
1317    fn revision_accessors() {
1318        let bytes = build(&[CATALOG, PAGES], "");
1319        let f = open(&bytes).unwrap();
1320        assert_eq!(f.newest_section_is_stream().unwrap(), Some(false));
1321        assert_eq!(f.startxref().unwrap(), Some(f.sections()[0]));
1322        assert!(!f.was_rebuilt());
1323        assert_eq!(f.read_all().unwrap(), bytes);
1324        let scanned =
1325            open(b"%PDF-1.4\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n").unwrap();
1326        assert!(scanned.was_rebuilt());
1327        assert_eq!(scanned.newest_section_is_stream().unwrap(), None);
1328    }
1329
1330    #[test]
1331    fn object_header_scanner() {
1332        assert_eq!(object_header_before(b"\n12 0 obj", 6), Some((1, 12, 0)));
1333        assert_eq!(object_header_before(b"12 3obj", 4), Some((0, 12, 3)));
1334        assert_eq!(object_header_before(b"x12 0 obj", 6), None);
1335        assert_eq!(object_header_before(b"0 obj", 2), None);
1336        assert_eq!(object_header_before(b"1 999999 obj", 9), None);
1337        assert!(is_end_keyword(b"endobj", 3));
1338    }
1339}