Skip to main content

pdfrum_parser/
syntax.rs

1//! The object grammar (ISO 32000-1 §7.3): tokens in, [`Object`]s out.
2//!
3//! # Two strictnesses, one grammar
4//!
5//! PDF files disagree with the specification constantly, and a reader that
6//! insists on the grammar opens fewer of them. So the grammar runs in two
7//! modes. [`Strictness::Loose`] keeps whatever it managed to read — a partial
8//! array, a dictionary missing its `>>` — while [`Strictness::Strict`] fails
9//! the whole object instead. The choice is not stylistic: the recovery scan
10//! parses **strictly**, because there a body that only half-parses is
11//! evidence the offset was wrong, while ordinary fetches parse **loosely**,
12//! because there the file is all the reader has.
13//!
14//! Nested values are always parsed loosely regardless of the caller's mode,
15//! which is why a strict parse of `<< /A [1 2 >>` still yields a dictionary
16//! whose `/A` is the partial array `[1 2]`.
17//!
18//! # Where `/Length` is a suggestion
19//!
20//! A stream's declared length is checked, never trusted: the bytes it points
21//! at must be followed by `endstream`, and when they are not the reader
22//! throws the number away and searches for the keyword instead. That single
23//! repair is why so many damaged files still render, and [`read_stream`]
24//! implements it in full — including the case where `/Length` is an indirect
25//! reference to an object whose own parse needs this stream, which the
26//! store's cycle guard turns into a missing length rather than a hang.
27
28use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
29use pdfrum_object::{
30    Array, ByteSpan, Dict, Name, ObjRef, Object, PdfString, Resolve, Stream, name_decode, names,
31};
32
33use crate::error::Error;
34use crate::lexer::{
35    Delim, Lexer, Token, WordBoundary, atoui, find_word, is_line_ending, is_whitespace,
36};
37
38/// How much malformed syntax an object parse tolerates.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Strictness {
41    /// Keep what parsed: a partial array, a dictionary that skipped a bad
42    /// key. This is how the document's own objects are read.
43    Loose,
44    /// Fail the object when anything about it is malformed. The recovery
45    /// scan uses this, because there a half-parsed body means the offset was
46    /// wrong rather than the file being damaged.
47    Strict,
48}
49
50/// Everything the grammar needs from the rest of the reader.
51///
52/// Bundled into one record because it threads through every recursive call,
53/// and because the store is genuinely optional: parsing an object out of an
54/// object stream has no store to chase a `/Length` reference through, and
55/// must not pretend otherwise.
56pub struct Context<'r, R: Resolve + ?Sized> {
57    /// Caps on nesting and word length.
58    pub limits: &'r Limits,
59    /// Where repairs are recorded.
60    pub diags: &'r mut Diagnostics,
61    /// The whole file, for cutting stream payloads out of without copying.
62    pub file: Option<&'r ByteSpan>,
63    /// The object store, used only to resolve an indirect `/Length`.
64    pub store: Option<&'r R>,
65}
66
67impl<R: Resolve + ?Sized> Context<'_, R> {
68    /// Record a repair at `at`.
69    fn note(&mut self, severity: Severity, what: DiagKind, at: usize) {
70        self.diags.record(severity, what, Some(at as u64));
71    }
72}
73
74/// Parse one object body at the lexer's position.
75///
76/// This is the entry point every other layer uses; `depth` is the nesting
77/// already spent, which the caller threads so that an object stream's members
78/// share the file parse's budget rather than getting a fresh one.
79///
80/// # Errors
81///
82/// [`Error::NoObject`] when the bytes are not an object at all — which is
83/// also how composite parses learn they have reached their closing bracket —
84/// and [`Error::TooDeep`] when this object is itself nested past
85/// `limits.max_object_nesting`. A composite *containing* something too deep
86/// does not fail: it drops what it could not read.
87///
88/// ```
89/// use pdfrum_common::{Diagnostics, Limits};
90/// use pdfrum_object::{NoResolve, Object};
91/// use pdfrum_parser::{Lexer, Strictness, parse_object};
92///
93/// let limits = Limits::default();
94/// let mut diags = Diagnostics::default();
95/// let mut lx = Lexer::new(b"<< /Type /Page /Count 3 >>");
96/// let obj = parse_object(&mut lx, &limits, &mut diags, Strictness::Loose, &NoResolve)?;
97/// let dict = obj.as_dict().expect("a dictionary");
98/// assert_eq!(dict.int(pdfrum_object::names::COUNT, &NoResolve), Some(3));
99/// # Ok::<(), pdfrum_parser::Error>(())
100/// ```
101pub fn parse_object<R: Resolve + ?Sized>(
102    lx: &mut Lexer<'_>,
103    limits: &Limits,
104    diags: &mut Diagnostics,
105    strictness: Strictness,
106    store: &R,
107) -> Result<Object, Error> {
108    let mut ctx = Context {
109        limits,
110        diags,
111        file: None,
112        store: Some(store),
113    };
114    body(lx, &mut ctx, strictness, 0)
115}
116
117/// Parse one object body, carrying the reader's full context.
118///
119/// The grammar proper. `depth` counts composite nesting already entered.
120pub(crate) fn body<R: Resolve + ?Sized>(
121    lx: &mut Lexer<'_>,
122    ctx: &mut Context<'_, R>,
123    strictness: Strictness,
124    depth: u32,
125) -> Result<Object, Error> {
126    // `depth` counts the composites already entered, so the outermost body
127    // arrives at zero and a budget of 64 admits exactly 64 nested composites.
128    // Note where the refusal lands: a composite too deep to enter reports
129    // failure to its *parent*, which treats it as an element that would not
130    // parse and closes normally. So a file nested past the cap loses its
131    // innermost contents and keeps everything above them.
132    if depth >= ctx.limits.max_object_nesting {
133        return Err(Error::TooDeep(ctx.limits.max_object_nesting));
134    }
135    let start = lx.pos();
136    let token = lx.next_word(ctx.limits);
137
138    match token {
139        Token::Number(word) => number_or_reference(lx, ctx, word),
140        Token::Name(payload) => Ok(Object::Name(Name::decode(payload))),
141        Token::Keyword(b"true") => Ok(Object::Bool(true)),
142        Token::Keyword(b"false") => Ok(Object::Bool(false)),
143        Token::Keyword(b"null") => Ok(Object::Null),
144        Token::Delim(Delim::StringOpen) => Ok(Object::Str(PdfString::literal(
145            lx.read_literal_string().as_ref(),
146        ))),
147        Token::Delim(Delim::HexOpen) => Ok(Object::Str(PdfString::hex(lx.read_hex_string()))),
148        Token::Delim(Delim::ArrayOpen) => array(lx, ctx, strictness, depth),
149        Token::Delim(Delim::DictOpen) => dictionary(lx, ctx, strictness, depth),
150        // A `>>` belongs to an enclosing dictionary: give it back and fail,
151        // which is exactly how a dictionary's value loop learns it is done.
152        Token::Delim(Delim::DictClose) => {
153            lx.seek(start);
154            Err(Error::NoObject(start as u64))
155        }
156        // The end of the file, and `]`, `endobj`, `}`, junk — for the latter
157        // the position stays past the word so the caller can see what
158        // stopped it.
159        _ => Err(Error::NoObject(start as u64)),
160    }
161}
162
163/// A number token, which may turn out to be the head of `N G R`.
164fn number_or_reference<R: Resolve + ?Sized>(
165    lx: &mut Lexer<'_>,
166    ctx: &mut Context<'_, R>,
167    word: &[u8],
168) -> Result<Object, Error> {
169    let after_number = lx.pos();
170    let second = lx.next_word(ctx.limits);
171    if second.is_number() {
172        let generation = second.bytes();
173        if lx.next_word(ctx.limits).is_keyword(b"R") {
174            let num = atoui(word);
175            let generation = u16::try_from(atoui(generation)).unwrap_or(u16::MAX);
176            let reference = ObjRef::new(num, generation);
177            // The all-ones object number is the "no object" marker, so a
178            // reference spelling it is not an object at all.
179            if reference.is_invalid() {
180                return Err(Error::NoObject(after_number as u64));
181            }
182            return Ok(Object::Ref(reference));
183        }
184    }
185    // Not a reference after all: give back everything but the first number.
186    lx.seek(after_number);
187    Ok(parse_number(word))
188}
189
190/// Turn a number-shaped word into an `Int` or a `Real`.
191///
192/// The token layer already established every byte is in the numeric class,
193/// which admits spellings like `--37` and `1.2.3`. A word containing `.`
194/// reads as a real, everything else as an integer, and a spelling neither
195/// can represent reads as zero rather than failing.
196fn parse_number(word: &[u8]) -> Object {
197    let text = String::from_utf8_lossy(word);
198    if word.contains(&b'.') {
199        Object::Real(parse_real(&text))
200    } else {
201        Object::Int(parse_int(&text))
202    }
203}
204
205/// Split off at most **one** leading sign.
206///
207/// Exactly one: a second sign is not a sign but the end of the digits, so
208/// `--37` has no digits at all and is worth zero. The token layer admits such
209/// spellings because every byte is in the numeric class, and this is where
210/// they stop meaning anything.
211fn split_sign(text: &str) -> (bool, &str) {
212    match text.as_bytes().first() {
213        Some(b'-') => (true, text.get(1..).unwrap_or_default()),
214        Some(b'+') => (false, text.get(1..).unwrap_or_default()),
215        _ => (false, text),
216    }
217}
218
219/// Read a decimal integer, folding anything unrepresentable to zero.
220///
221/// The digits accumulate in a **`u32`**, not a wider type, and that width is
222/// load-bearing rather than an implementation detail: it is what makes
223/// `/P -1` and `/P 4294967295` name the same permission bits, and it is the
224/// range [`INT_RANGE`](pdfrum_object::INT_RANGE) promises every
225/// [`Object::Int`] a parse can produce will lie in. Two separate ceilings
226/// follow from it, and a spelling past either is worth **zero** — not the
227/// nearest representable value, which would silently turn an absurd
228/// `/Columns 99999999999999999999` into a plausible one:
229///
230/// - An **unsigned** spelling may reach `u32::MAX`; overflowing the
231///   accumulator itself folds to zero.
232/// - A **signed** spelling — one that led with `+` or `-` — may only reach
233///   `i32::MAX`, or `i32::MAX + 1` when negative so that `-2147483648`
234///   spells itself. Past that it folds to zero too, even though the
235///   accumulator held the value fine.
236fn parse_int(text: &str) -> i64 {
237    let signed = matches!(text.as_bytes().first(), Some(b'-' | b'+'));
238    let (negative, digits) = split_sign(text);
239
240    // Overflow here is not saturation: a token wider than the accumulator
241    // stops meaning anything at all.
242    let mut magnitude: Option<u32> = Some(0);
243    for byte in digits.bytes().take_while(u8::is_ascii_digit) {
244        magnitude = magnitude
245            .and_then(|acc| acc.checked_mul(10))
246            .and_then(|acc| acc.checked_add(u32::from(byte - b'0')));
247    }
248    let magnitude = magnitude.unwrap_or(0);
249
250    if !signed {
251        return i64::from(magnitude);
252    }
253
254    let limit = i64::from(i32::MAX) + i64::from(negative);
255    let magnitude = i64::from(magnitude);
256    if magnitude > limit {
257        return 0;
258    }
259    if negative { -magnitude } else { magnitude }
260}
261
262/// Read a real, keeping only the first decimal point so `1.2.3` is 1.2.
263fn parse_real(text: &str) -> f32 {
264    let (negative, digits) = split_sign(text);
265    let mut cleaned = String::with_capacity(digits.len());
266    let mut seen_dot = false;
267    for c in digits.chars() {
268        match c {
269            '.' if seen_dot => break,
270            '.' => {
271                seen_dot = true;
272                cleaned.push('.');
273            }
274            '0'..='9' => cleaned.push(c),
275            _ => break,
276        }
277    }
278    let magnitude = cleaned.parse::<f32>().unwrap_or(0.0);
279    if negative { -magnitude } else { magnitude }
280}
281
282/// Read array elements until one fails to parse (ISO 32000-1 §7.3.6).
283///
284/// Termination is by failure, not by looking for `]`: the element parse falls
285/// through every grammar case on a `]` and errors, and *that* is the signal.
286/// A strict parse then insists the token that stopped it really was `]`;
287/// a loose one keeps the elements it has, so an array left open by a
288/// truncated file still yields its contents.
289fn array<R: Resolve + ?Sized>(
290    lx: &mut Lexer<'_>,
291    ctx: &mut Context<'_, R>,
292    strictness: Strictness,
293    depth: u32,
294) -> Result<Object, Error> {
295    let mut out = Array::new();
296    loop {
297        let before = lx.pos();
298        match body(lx, ctx, Strictness::Loose, depth + 1) {
299            Ok(Object::Stream(_)) => {
300                // A stream may not be an array element; it is dropped and
301                // the array carries on (ISO 32000-1 §7.3.8.1).
302                ctx.note(
303                    Severity::Recovered,
304                    DiagKind::StreamInCompositeDropped,
305                    before,
306                );
307            }
308            Ok(value) => {
309                if out.len() >= ctx.limits.max_array_len {
310                    return Ok(Object::Array(out));
311                }
312                out.push(value);
313            }
314            // Running out of nesting budget is not special: like any element
315            // that will not parse, it ends the array, which then closes with
316            // what it has. So a file nested past the cap loses its innermost
317            // contents rather than its outermost object.
318            Err(_) => {
319                // What stopped us? Re-read the token the failed parse
320                // consumed, from where it started.
321                let mut probe = Lexer::at(lx.bytes(), before);
322                let stopper = probe.next_word(ctx.limits);
323                let closed = matches!(stopper, Token::Delim(Delim::ArrayClose));
324                if !closed {
325                    ctx.note(Severity::Suspicious, DiagKind::MalformedArray, before);
326                    if strictness == Strictness::Strict {
327                        return Err(Error::NoObject(before as u64));
328                    }
329                }
330                return Ok(Object::Array(out));
331            }
332        }
333    }
334}
335
336/// Read dictionary entries until `>>`, and then decide whether a stream
337/// follows (ISO 32000-1 §7.3.7).
338///
339/// Three repairs live here, all of them things real files do: an `endobj`
340/// where the `>>` should be ends the dictionary rather than eating the rest
341/// of the file; a key that is not a name is skipped along with nothing else,
342/// so the value that follows becomes the *next* key; and a value that will
343/// not parse drops its pair.
344fn dictionary<R: Resolve + ?Sized>(
345    lx: &mut Lexer<'_>,
346    ctx: &mut Context<'_, R>,
347    strictness: Strictness,
348    depth: u32,
349) -> Result<Object, Error> {
350    let mut dict = Dict::new();
351    loop {
352        let key_start = lx.pos();
353        let token = lx.next_word(ctx.limits);
354        match token {
355            // End of file inside a dictionary loses the whole thing.
356            Token::Eof => return Err(Error::NoObject(key_start as u64)),
357            Token::Delim(Delim::DictClose) => break,
358            Token::Keyword(b"endobj") => {
359                // The `>>` is missing. Hand `endobj` back so the indirect
360                // frame above sees it and take what we have.
361                ctx.note(Severity::Suspicious, DiagKind::MalformedDict, key_start);
362                lx.seek(key_start);
363                break;
364            }
365            Token::Name(payload) => {
366                let key = name_decode(payload);
367                let value_start = lx.pos();
368                match body(lx, ctx, Strictness::Loose, depth + 1) {
369                    Ok(Object::Stream(_)) => {
370                        ctx.note(
371                            Severity::Recovered,
372                            DiagKind::StreamInCompositeDropped,
373                            value_start,
374                        );
375                    }
376                    Ok(value) => {
377                        // A key that decodes to nothing is dropped: the file
378                        // said `/` and meant nothing nameable.
379                        if key.is_empty() {
380                            ctx.note(Severity::Suspicious, DiagKind::MalformedDict, key_start);
381                        } else {
382                            dict.push(Name::new(key), value);
383                        }
384                    }
385                    // As in an array, exhausting the nesting budget is just
386                    // a value that would not parse: the pair is dropped and
387                    // the dictionary carries on.
388                    Err(_) => {
389                        ctx.note(Severity::Suspicious, DiagKind::MalformedDict, value_start);
390                        if strictness == Strictness::Strict {
391                            lx.to_next_line();
392                            return Err(Error::NoObject(value_start as u64));
393                        }
394                    }
395                }
396            }
397            // Junk between entries: skipped in both modes. A file with a
398            // stray keyword in a dictionary is common enough that failing
399            // here would cost more than it saves.
400            _ => {
401                ctx.note(Severity::Suspicious, DiagKind::MalformedDict, key_start);
402            }
403        }
404    }
405
406    // A `stream` keyword now means this dictionary describes one.
407    let after_dict = lx.pos();
408    if lx.next_word(ctx.limits).is_keyword(b"stream") {
409        return read_stream(lx, ctx, dict);
410    }
411    lx.seek(after_dict);
412    Ok(Object::Dict(dict))
413}
414
415/// Read a stream's payload, the `stream` keyword already consumed
416/// (ISO 32000-1 §7.3.8).
417///
418/// The declared `/Length` is a hypothesis, tested by looking for `endstream`
419/// where it says the data ends. When the test fails — or there was no usable
420/// length in the first place — the reader scans forward for the keyword and
421/// believes the file's layout over its arithmetic.
422pub(crate) fn read_stream<R: Resolve + ?Sized>(
423    lx: &mut Lexer<'_>,
424    ctx: &mut Context<'_, R>,
425    dict: Dict,
426) -> Result<Object, Error> {
427    // `/Length` is one of the few keys read *through* a reference: files
428    // routinely put it in a separate object.
429    let declared = declared_length(&dict, ctx);
430
431    lx.to_next_line();
432    let data_start = lx.pos();
433    let file_len = lx.bytes().len();
434
435    let mut length = declared.filter(|&n| {
436        // A length running to or past the end of the file is not a length.
437        n == 0 || data_start.checked_add(n).is_some_and(|end| end < file_len)
438    });
439
440    if let Some(n) = length {
441        lx.seek(data_start.saturating_add(n));
442        lx.skip_eol_marker();
443        let word = lx.next_word(ctx.limits);
444        // A prefix compare, not equality: files write `endstreamXYZ`, and
445        // PDFium accepts them.
446        if !word.bytes().starts_with(b"endstream") {
447            ctx.note(Severity::Recovered, DiagKind::LengthMismatch, data_start);
448            length = None;
449            lx.seek(data_start);
450        }
451    } else if declared.is_some() {
452        ctx.note(Severity::Recovered, DiagKind::LengthMismatch, data_start);
453    }
454
455    let length = match length {
456        Some(n) => n,
457        None => match scan_for_end(lx.bytes(), data_start) {
458            Some(end) => {
459                ctx.note(Severity::Recovered, DiagKind::KeywordResync, data_start);
460                end.saturating_sub(data_start)
461            }
462            None => return Err(Error::NoObject(data_start as u64)),
463        },
464    };
465
466    let data_end = data_start.saturating_add(length).min(file_len);
467    let data = match ctx.file {
468        // A window into the file the caller already holds: a refcount bump,
469        // never an allocation. The bounds are still checked — `data_start`
470        // and `data_end` derive from an untrusted `/Length`.
471        Some(file) => file
472            .subspan(data_start..data_end)
473            .unwrap_or_else(|_| ByteSpan::empty()),
474        // Parsing out of a detached buffer (an object stream's decoded
475        // bytes) — copy, since there is no shared file to point into.
476        None => ByteSpan::from(
477            lx.bytes()
478                .get(data_start..data_end)
479                .unwrap_or_default()
480                .to_vec(),
481        ),
482    };
483
484    lx.seek(data_end);
485    resync_after_stream(lx, ctx);
486    Ok(Object::Stream(Box::new(Stream::new(dict, data))))
487}
488
489/// Read `/Length`, following one reference if that is what it holds.
490fn declared_length<R: Resolve + ?Sized>(dict: &Dict, ctx: &mut Context<'_, R>) -> Option<usize> {
491    let raw = dict.raw(names::LENGTH)?;
492    let value = match raw {
493        Object::Ref(r) => {
494            // The store's in-progress guard turns a self-referential
495            // `/Length` into a miss rather than a hang.
496            let store = ctx.store?;
497            store.fetch(*r).ok()?.as_int()?
498        }
499        other => other.as_number()?.as_int()?,
500    };
501    usize::try_from(value).ok()
502}
503
504/// Find where the payload ends by looking for the keywords that follow it.
505///
506/// Whichever of `endstream` and `endobj` comes first wins, and the end-of-line
507/// bytes immediately before it belong to the file's formatting rather than to
508/// the stream, so they are given back.
509fn scan_for_end(bytes: &[u8], start: usize) -> Option<usize> {
510    let endstream = find_word(bytes, b"endstream", start, WordBoundary::WhitespaceOnly);
511    let endobj = find_word(bytes, b"endobj", start, WordBoundary::WhitespaceOnly);
512    let keyword = match (endstream, endobj) {
513        (Some(a), Some(b)) => a.min(b),
514        (Some(a), None) => a,
515        (None, Some(b)) => b,
516        (None, None) => return None,
517    };
518    let end = trim_trailing_eol(bytes, keyword);
519    (end >= start).then_some(end)
520}
521
522/// Step back over the one end-of-line sequence before `pos`.
523fn trim_trailing_eol(bytes: &[u8], pos: usize) -> usize {
524    match bytes.get(pos.wrapping_sub(1)) {
525        Some(b'\n') => {
526            if bytes.get(pos.wrapping_sub(2)) == Some(&b'\r') {
527                pos.saturating_sub(2)
528            } else {
529                pos.saturating_sub(1)
530            }
531        }
532        Some(b'\r') => pos.saturating_sub(1),
533        _ => pos,
534    }
535}
536
537/// Consume the keyword that follows the payload, unless it turns out to be
538/// the `endobj` belonging to the enclosing object.
539///
540/// A file missing its `endstream` writes `endobj` there instead. Swallowing
541/// it would leave the indirect frame above looking for one that is gone, so
542/// it is put back.
543fn resync_after_stream<R: Resolve + ?Sized>(lx: &mut Lexer<'_>, ctx: &mut Context<'_, R>) {
544    let before_keyword = lx.pos();
545    let word = lx.next_word(ctx.limits);
546    // Exactly `endobj`, not a word starting with it: this is the one place a
547    // whole-word match matters, since resyncing on `endobjects` would hand
548    // the frame above a keyword that is not there.
549    if word.bytes() != b"endobj" {
550        return;
551    }
552
553    // Spaces and tabs may sit between the keyword and the line ending, and a
554    // file that writes them still means the object ended here.
555    let mut probe = Lexer::at(lx.bytes(), lx.pos());
556    while probe
557        .peek_byte()
558        .is_some_and(|b| is_whitespace(b) && !is_line_ending(b))
559    {
560        probe.seek(probe.pos() + 1);
561    }
562    // A line ending has to follow. At the end of the file there is none, and
563    // the keyword stays consumed.
564    if probe.skip_eol_marker() > 0 {
565        ctx.note(Severity::Recovered, DiagKind::KeywordResync, before_keyword);
566        lx.seek(before_keyword);
567    }
568}
569
570/// One indirect object: its number, generation, and body.
571#[derive(Debug, Clone, PartialEq)]
572pub struct Indirect {
573    /// The object number from the `N G obj` header.
574    pub num: u32,
575    /// The generation number from the header.
576    pub generation: u16,
577    /// The parsed body.
578    pub object: Object,
579}
580
581/// Parse `N G obj … endobj` at the lexer's position.
582///
583/// The header must be exactly two number tokens and the keyword `obj`;
584/// anything else rewinds to where the parse started and fails, which is what
585/// lets a caller probe an offset without losing its place.
586///
587/// # Errors
588///
589/// [`Error::NoObject`] when the header or body will not parse.
590pub fn parse_indirect_object<R: Resolve + ?Sized>(
591    lx: &mut Lexer<'_>,
592    limits: &Limits,
593    diags: &mut Diagnostics,
594    strictness: Strictness,
595    store: &R,
596) -> Result<Indirect, Error> {
597    let mut ctx = Context {
598        limits,
599        diags,
600        file: None,
601        store: Some(store),
602    };
603    indirect(lx, &mut ctx, strictness, 0)
604}
605
606/// Parse an indirect object with the reader's full context.
607pub(crate) fn indirect<R: Resolve + ?Sized>(
608    lx: &mut Lexer<'_>,
609    ctx: &mut Context<'_, R>,
610    strictness: Strictness,
611    depth: u32,
612) -> Result<Indirect, Error> {
613    let start = lx.pos();
614    let fail = |lx: &mut Lexer<'_>| {
615        lx.seek(start);
616        Err(Error::NoObject(start as u64))
617    };
618
619    let Token::Number(num_word) = lx.next_word(ctx.limits) else {
620        return fail(lx);
621    };
622    let Token::Number(gen_word) = lx.next_word(ctx.limits) else {
623        return fail(lx);
624    };
625    if !lx.next_word(ctx.limits).is_keyword(b"obj") {
626        return fail(lx);
627    }
628
629    let object = match body(lx, ctx, strictness, depth) {
630        Ok(o) => o,
631        Err(_) if strictness == Strictness::Loose => Object::Null,
632        Err(e) => return Err(e),
633    };
634
635    Ok(Indirect {
636        num: atoui(num_word),
637        generation: u16::try_from(atoui(gen_word)).unwrap_or(u16::MAX),
638        object,
639    })
640}
641
642#[cfg(test)]
643mod tests {
644    use super::{Context, Strictness, body, indirect, parse_object};
645    use crate::error::Error;
646    use crate::lexer::Lexer;
647    use pdfrum_common::{DiagKind, Diagnostics, Limits};
648    use pdfrum_object::ByteSpan;
649    use pdfrum_object::{NoResolve, ObjRef, Object, Resolve, names};
650    use std::sync::Arc;
651
652    fn parse(input: &[u8]) -> Result<Object, Error> {
653        let mut diags = Diagnostics::default();
654        parse_object(
655            &mut Lexer::new(input),
656            &Limits::default(),
657            &mut diags,
658            Strictness::Loose,
659            &NoResolve,
660        )
661    }
662
663    fn parse_strict(input: &[u8]) -> Result<Object, Error> {
664        let mut diags = Diagnostics::default();
665        parse_object(
666            &mut Lexer::new(input),
667            &Limits::default(),
668            &mut diags,
669            Strictness::Strict,
670            &NoResolve,
671        )
672    }
673
674    /// A map-backed store, for the one behavior that needs a resolver here:
675    /// a `/Length` that lives in another object.
676    struct Store(std::collections::HashMap<u32, Arc<Object>>);
677
678    impl Resolve for Store {
679        fn fetch(&self, r: ObjRef) -> Result<Arc<Object>, pdfrum_object::Error> {
680            self.0
681                .get(&r.num)
682                .cloned()
683                .ok_or(pdfrum_object::Error::UnresolvedRef(r))
684        }
685    }
686
687    /// Parse against a file-backed context so streams get real spans.
688    fn parse_in_file<R: Resolve + ?Sized>(
689        input: &[u8],
690        store: &R,
691        diags: &mut Diagnostics,
692    ) -> Result<Object, Error> {
693        let file = ByteSpan::from(input.to_vec());
694        let limits = Limits::default();
695        let mut ctx = Context {
696            limits: &limits,
697            diags,
698            file: Some(&file),
699            store: Some(store),
700        };
701        let mut lx = Lexer::new(&file);
702        body(&mut lx, &mut ctx, Strictness::Loose, 0)
703    }
704
705    #[test]
706    fn scalars() {
707        assert_eq!(parse(b"true"), Ok(Object::Bool(true)));
708        assert_eq!(parse(b"false"), Ok(Object::Bool(false)));
709        assert_eq!(parse(b"null"), Ok(Object::Null));
710        assert_eq!(parse(b"42"), Ok(Object::Int(42)));
711        assert_eq!(parse(b"-17"), Ok(Object::Int(-17)));
712        assert_eq!(parse(b"3.5"), Ok(Object::Real(3.5)));
713        assert_eq!(parse(b"-0.25"), Ok(Object::Real(-0.25)));
714        // Token-level numbers the value parse has to make sense of.
715        assert_eq!(parse(b"1.2.3"), Ok(Object::Real(1.2)));
716        assert_eq!(parse(b"--37"), Ok(Object::Int(0)));
717    }
718
719    /// Every integer a parse can produce lies in `pdfrum_object::INT_RANGE`,
720    /// because the accumulator is a `u32` and a spelling that does not fit
721    /// one is worth zero.
722    ///
723    /// Found by the `filters_chain` and `crypt_encrypt_dict` fuzz targets: a
724    /// `/Columns 999999999999999999999999` reached `narrow_to_signed32`, whose
725    /// `debug_assert!` on that range is the contract this pins. The bug was
726    /// an `i64` accumulator that *saturated* — turning an absurd token into
727    /// `i64::MAX` rather than into nothing.
728    #[test]
729    fn integers_outside_the_c_int_range_are_zero() {
730        use pdfrum_object::INT_RANGE;
731
732        // An unsigned spelling reaches u32::MAX and stops.
733        assert_eq!(parse(b"4294967295"), Ok(Object::Int(4_294_967_295)));
734        assert_eq!(parse(b"4294967296"), Ok(Object::Int(0)));
735        assert_eq!(parse(b"99999999999999999999999999"), Ok(Object::Int(0)));
736
737        // A signed spelling only reaches i32::MAX...
738        assert_eq!(parse(b"+2147483647"), Ok(Object::Int(2_147_483_647)));
739        assert_eq!(parse(b"+2147483648"), Ok(Object::Int(0)));
740        assert_eq!(parse(b"+4294967295"), Ok(Object::Int(0)));
741        // ...except negatively, where i32::MIN must be spellable.
742        assert_eq!(parse(b"-2147483648"), Ok(Object::Int(-2_147_483_648)));
743        assert_eq!(parse(b"-2147483649"), Ok(Object::Int(0)));
744        assert_eq!(parse(b"-99999999999999999999"), Ok(Object::Int(0)));
745
746        // The invariant itself, over every boundary spelling.
747        for spelling in [
748            &b"0"[..],
749            b"-0",
750            b"+0",
751            b"2147483647",
752            b"2147483648",
753            b"4294967295",
754            b"4294967296",
755            b"-2147483648",
756            b"-2147483649",
757            b"18446744073709551616",
758            b"999999999999999999999999999999",
759            b"-999999999999999999999999999999",
760        ] {
761            let Ok(Object::Int(v)) = parse(spelling) else {
762                panic!(
763                    "{} did not parse as an integer",
764                    String::from_utf8_lossy(spelling)
765                );
766            };
767            assert!(
768                INT_RANGE.contains(&v),
769                "{} parsed to {v}, outside INT_RANGE",
770                String::from_utf8_lossy(spelling)
771            );
772            // The accessor whose debug_assert the fuzzer tripped.
773            let _ = pdfrum_object::narrow_to_signed32(v);
774        }
775    }
776
777    #[test]
778    fn names_decode_escapes() {
779        assert_eq!(
780            parse(b"/A#20B"),
781            Ok(Object::Name(pdfrum_object::Name::from("A B")))
782        );
783        assert_eq!(parse(b"/"), Ok(Object::Name(pdfrum_object::Name::from(""))));
784    }
785
786    #[test]
787    fn references_and_the_invalid_one() {
788        assert_eq!(parse(b"12 0 R"), Ok(Object::Ref(ObjRef::new(12, 0))));
789        assert_eq!(parse(b"3 7 R"), Ok(Object::Ref(ObjRef::new(3, 7))));
790        // Object number zero is a legal spelling; the fetch is what fails.
791        assert_eq!(parse(b"0 0 R"), Ok(Object::Ref(ObjRef::new(0, 0))));
792        // The all-ones object number is not an object.
793        assert!(parse(b"4294967295 0 R").is_err());
794        // Not a reference: only the first number is consumed.
795        let mut lx = Lexer::new(b"12 0 X");
796        let mut diags = Diagnostics::default();
797        let obj = parse_object(
798            &mut lx,
799            &Limits::default(),
800            &mut diags,
801            Strictness::Loose,
802            &NoResolve,
803        );
804        assert_eq!(obj, Ok(Object::Int(12)));
805        assert_eq!(lx.pos(), 2);
806    }
807
808    #[test]
809    fn arrays() {
810        let obj = parse(b"[1 2 3]").expect("array");
811        let array = obj.as_array().expect("array");
812        assert_eq!(array.len(), 3);
813        assert_eq!(array.int_at(2), Some(3));
814        assert!(parse(b"[]").expect("array").as_array().is_some());
815    }
816
817    #[test]
818    fn a_loose_array_keeps_what_it_read() {
819        let mut diags = Diagnostics::default();
820        let obj = parse_object(
821            &mut Lexer::new(b"[1 2 endobj"),
822            &Limits::default(),
823            &mut diags,
824            Strictness::Loose,
825            &NoResolve,
826        )
827        .expect("partial array");
828        assert_eq!(obj.as_array().expect("array").len(), 2);
829        assert!(diags.contains(&DiagKind::MalformedArray));
830    }
831
832    #[test]
833    fn a_strict_array_needs_its_bracket() {
834        assert!(parse_strict(b"[1 2 endobj").is_err());
835        assert!(parse_strict(b"[1 2]").is_ok());
836    }
837
838    #[test]
839    fn dictionaries() {
840        let obj = parse(b"<< /Type /Page /Count 3 >>").expect("dict");
841        let dict = obj.as_dict().expect("dict");
842        assert_eq!(dict.name(names::TYPE), Some(names::PAGE));
843        assert_eq!(dict.direct_int(names::COUNT), Some(3));
844    }
845
846    #[test]
847    fn a_later_duplicate_key_wins() {
848        let obj = parse(b"<< /Size 3 /Size -1 >>").expect("dict");
849        assert_eq!(
850            obj.as_dict().expect("dict").direct_int(names::SIZE),
851            Some(-1)
852        );
853    }
854
855    #[test]
856    fn endobj_closes_an_unterminated_dictionary() {
857        let mut diags = Diagnostics::default();
858        let mut lx = Lexer::new(b"<< /A 1 endobj");
859        let obj = parse_object(
860            &mut lx,
861            &Limits::default(),
862            &mut diags,
863            Strictness::Loose,
864            &NoResolve,
865        )
866        .expect("dict");
867        assert_eq!(obj.as_dict().expect("dict").len(), 1);
868        assert!(diags.contains(&DiagKind::MalformedDict));
869        // `endobj` was put back for the frame above.
870        assert_eq!(lx.next_word(&Limits::default()).bytes(), b"endobj");
871    }
872
873    #[test]
874    fn junk_keys_are_skipped_and_shift_the_pairs() {
875        let obj = parse(b"<< junk /A 1 >>").expect("dict");
876        let dict = obj.as_dict().expect("dict");
877        assert_eq!(dict.len(), 1);
878        assert_eq!(dict.direct_int(&pdfrum_object::Name::from("A")), Some(1));
879    }
880
881    #[test]
882    fn a_bare_slash_key_is_dropped() {
883        let obj = parse(b"<< / 1 /A 2 >>").expect("dict");
884        assert_eq!(obj.as_dict().expect("dict").len(), 1);
885    }
886
887    #[test]
888    fn nesting_past_the_budget_loses_the_middle_not_the_object() {
889        // Depth of the parsed result, which is what the cap actually bounds.
890        fn depth_of(o: &Object) -> usize {
891            match o {
892                Object::Array(a) => 1 + a.iter().map(depth_of).max().unwrap_or(0),
893                _ => 0,
894            }
895        }
896        let nested = |n: usize| -> Vec<u8> {
897            let mut v: Vec<u8> = std::iter::repeat_n(b'[', n).collect();
898            v.extend(std::iter::repeat_n(b']', n));
899            v
900        };
901
902        // Everything up to the budget survives intact.
903        assert_eq!(depth_of(&parse(&nested(63)).expect("array")), 63);
904        assert_eq!(depth_of(&parse(&nested(64)).expect("array")), 64);
905        // Past it the object still parses — the arrays too deep to enter
906        // simply come back empty, so the damage is innermost, not outermost.
907        assert_eq!(depth_of(&parse(&nested(65)).expect("array")), 64);
908        assert_eq!(depth_of(&parse(&nested(500)).expect("array")), 64);
909    }
910
911    #[test]
912    fn a_stream_reads_its_declared_length() {
913        let mut diags = Diagnostics::default();
914        let obj = parse_in_file(
915            b"<< /Length 5 >>\nstream\nHELLO\nendstream\nendobj\n",
916            &NoResolve,
917            &mut diags,
918        )
919        .expect("stream");
920        assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
921        assert!(!diags.contains(&DiagKind::LengthMismatch));
922    }
923
924    #[test]
925    fn a_wrong_length_falls_back_to_the_keyword() {
926        let mut diags = Diagnostics::default();
927        let obj = parse_in_file(
928            b"<< /Length 2 >>\nstream\nHELLO\nendstream\nendobj\n",
929            &NoResolve,
930            &mut diags,
931        )
932        .expect("stream");
933        assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
934        assert!(diags.contains(&DiagKind::LengthMismatch));
935        assert!(diags.contains(&DiagKind::KeywordResync));
936    }
937
938    #[test]
939    fn a_length_past_the_file_falls_back() {
940        let mut diags = Diagnostics::default();
941        let obj = parse_in_file(
942            b"<< /Length 9999 >>\nstream\nHELLO\nendstream\nendobj\n",
943            &NoResolve,
944            &mut diags,
945        )
946        .expect("stream");
947        assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
948    }
949
950    #[test]
951    fn endstream_is_matched_by_prefix() {
952        let mut diags = Diagnostics::default();
953        let obj = parse_in_file(
954            b"<< /Length 5 >>\nstream\nHELLO\nendstreamXYZ\nendobj\n",
955            &NoResolve,
956            &mut diags,
957        )
958        .expect("stream");
959        assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
960        assert!(!diags.contains(&DiagKind::LengthMismatch));
961    }
962
963    #[test]
964    fn a_space_before_the_newline_still_resyncs() {
965        // The keyword standing in for `endstream` may be followed by spaces
966        // before its line ending, and it is still the object's end.
967        let mut diags = Diagnostics::default();
968        let obj = parse_in_file(
969            b"<< /Length 99 >>\nstream\nHELLO\nendobj  \n",
970            &NoResolve,
971            &mut diags,
972        )
973        .expect("stream");
974        assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
975        assert!(diags.contains(&DiagKind::KeywordResync));
976    }
977
978    #[test]
979    fn a_missing_endstream_resyncs_on_endobj() {
980        let mut diags = Diagnostics::default();
981        let obj = parse_in_file(
982            b"<< /Length 99 >>\nstream\nHELLO\nendobj\n",
983            &NoResolve,
984            &mut diags,
985        )
986        .expect("stream");
987        assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
988        assert!(diags.contains(&DiagKind::KeywordResync));
989    }
990
991    #[test]
992    fn a_delimiter_disqualifies_an_endstream_match() {
993        // `>>endstream` is not a whole-word match under the keyword rule, so
994        // the scan keeps going to the real one.
995        let mut diags = Diagnostics::default();
996        let obj = parse_in_file(
997            b"<< /Length 999 >>\nstream\nA>>endstream\nB\nendstream\nendobj\n",
998            &NoResolve,
999            &mut diags,
1000        )
1001        .expect("stream");
1002        assert_eq!(&*obj.as_stream().expect("stream").data, b"A>>endstream\nB");
1003    }
1004
1005    #[test]
1006    fn a_zero_length_stream_is_legal() {
1007        let mut diags = Diagnostics::default();
1008        let obj = parse_in_file(
1009            b"<< /Length 0 >>\nstream\nendstream\nendobj\n",
1010            &NoResolve,
1011            &mut diags,
1012        )
1013        .expect("stream");
1014        assert!(obj.as_stream().expect("stream").data.is_empty());
1015    }
1016
1017    #[test]
1018    fn an_indirect_length_is_chased() {
1019        let store = Store([(9u32, Arc::new(Object::Int(5)))].into_iter().collect());
1020        let mut diags = Diagnostics::default();
1021        let obj = parse_in_file(
1022            b"<< /Length 9 0 R >>\nstream\nHELLO\nendstream\nendobj\n",
1023            &store,
1024            &mut diags,
1025        )
1026        .expect("stream");
1027        assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
1028        assert!(!diags.contains(&DiagKind::LengthMismatch));
1029    }
1030
1031    #[test]
1032    fn an_unresolvable_length_falls_back_to_the_scan() {
1033        let mut diags = Diagnostics::default();
1034        let obj = parse_in_file(
1035            b"<< /Length 9 0 R >>\nstream\nHELLO\nendstream\nendobj\n",
1036            &NoResolve,
1037            &mut diags,
1038        )
1039        .expect("stream");
1040        assert_eq!(&*obj.as_stream().expect("stream").data, b"HELLO");
1041        assert!(diags.contains(&DiagKind::KeywordResync));
1042    }
1043
1044    #[test]
1045    fn streams_are_dropped_from_composites() {
1046        let mut diags = Diagnostics::default();
1047        let obj = parse_in_file(
1048            b"[ 1 << /Length 5 >>\nstream\nHELLO\nendstream\n 2 ]",
1049            &NoResolve,
1050            &mut diags,
1051        )
1052        .expect("array");
1053        let array = obj.as_array().expect("array");
1054        assert_eq!(array.len(), 2);
1055        assert_eq!(array.int_at(0), Some(1));
1056        assert_eq!(array.int_at(1), Some(2));
1057        assert!(diags.contains(&DiagKind::StreamInCompositeDropped));
1058    }
1059
1060    #[test]
1061    fn indirect_frames_need_their_header() {
1062        let file = ByteSpan::from(b"7 0 obj << /A 1 >> endobj".to_vec());
1063        let limits = Limits::default();
1064        let mut diags = Diagnostics::default();
1065        let mut ctx = Context {
1066            limits: &limits,
1067            diags: &mut diags,
1068            file: Some(&file),
1069            store: Some(&NoResolve),
1070        };
1071        let mut lx = Lexer::new(&file);
1072        let parsed = indirect(&mut lx, &mut ctx, Strictness::Loose, 0).expect("indirect");
1073        assert_eq!(parsed.num, 7);
1074        assert_eq!(parsed.generation, 0);
1075        assert!(parsed.object.as_dict().is_some());
1076
1077        // A missing `obj` keyword rewinds.
1078        let mut lx = Lexer::new(b"7 0 <<>>");
1079        assert!(
1080            indirect(
1081                &mut lx,
1082                &mut Context {
1083                    limits: &limits,
1084                    diags: &mut Diagnostics::default(),
1085                    file: None,
1086                    store: Some(&NoResolve),
1087                },
1088                Strictness::Loose,
1089                0,
1090            )
1091            .is_err()
1092        );
1093        assert_eq!(lx.pos(), 0);
1094    }
1095
1096    #[test]
1097    fn never_panics_on_arbitrary_bytes() {
1098        let seeds: &[&[u8]] = &[
1099            b"<<<<<<<<",
1100            b">>>>>>>>",
1101            b"[[[[[[[[",
1102            b"((((((((",
1103            b"<<<</Length -1>>stream",
1104            b"0 0 obj<</Length 99999999999999999999>>stream\n",
1105            b"<</A",
1106            b"/",
1107            b"\xff\xfe\x00\x80",
1108            b"1 0 R 2 0 R",
1109            b"<</Length 1 0 R>>stream\nx",
1110        ];
1111        for seed in seeds {
1112            let _ = parse(seed);
1113            let _ = parse_strict(seed);
1114            let _ = parse_in_file(seed, &NoResolve, &mut Diagnostics::default());
1115        }
1116    }
1117}