Skip to main content

oxideav_pdf/reader/
parse.rs

1//! PDF object parser — [`Lexer`] tokens → [`Object`] tree.
2//!
3//! Round-3 Commit 2: builds on [`crate::reader::lex`] to recognise
4//! every variant the writer can emit (numbers, names, strings,
5//! arrays, dicts, indirect references, stream objects, null).
6//!
7//! Streams are matched by the trailing `stream`+EOL marker per ISO
8//! 32000-1 §7.3.8. The body is sliced verbatim from the input — no
9//! filter decoding here; the reader's stream object handler (round-3
10//! commit 4) re-runs FlateDecode if the dictionary's `/Filter` says
11//! so.
12//!
13//! Object streams (PDF 1.5+, `/Type /ObjStm`) are deliberately out of
14//! scope for round 3 — the writer doesn't emit them and parsing them
15//! requires a full /XRef stream walker. Round-4+.
16
17use crate::error::PdfError;
18use crate::objects::{Dict, Object, ObjectId, Stream};
19use crate::reader::lex::{Lexer, Token, TokenKind};
20
21/// Caller-supplied hook that resolves an indirect `/Length` reference
22/// on a stream dictionary to the underlying integer.
23///
24/// ISO 32000-1 §7.3.10 Example 3 makes indirect `/Length` normative on
25/// stream objects: a one-pass writer that doesn't know the encoded
26/// stream size up front emits `<< /Length 8 0 R >>` and writes the
27/// length-carrying integer object after the stream. A reader walking
28/// such a PDF needs the xref table to fetch the integer.
29///
30/// The trait receives the [`ObjectId`] from the reference and is
31/// expected to return the resolved integer (or a [`PdfError`] when the
32/// target is missing / not an integer / a cycle is detected). Using a
33/// trait (rather than a free `FnMut`) sidesteps the lifetime
34/// gymnastics needed to thread an `Option<&mut dyn FnMut>` through
35/// multiple recursive parse helpers — implementers just write a tiny
36/// struct holding the borrowed state they need.
37///
38/// When the resolver is the unit no-op [`NoLengthResolver`] (the
39/// [`Parser::parse_indirect`] / [`Parser::parse_object`] entry
40/// points), an indirect `/Length` is rejected — that path is reserved
41/// for xref-stream parsing, where the xref isn't built yet and the
42/// spec (§7.5.8) effectively requires the direct-integer form anyway.
43pub trait LengthResolver {
44    /// Resolve the integer carried by the indirect object at
45    /// `length_ref`. Returning `Err(_)` flows up the parse stack and
46    /// aborts the stream-object decode.
47    fn resolve_length(&mut self, length_ref: ObjectId) -> Result<i64, PdfError>;
48}
49
50/// No-op [`LengthResolver`] — every indirect `/Length` is rejected.
51/// Used by [`Parser::parse_indirect`] and [`Parser::parse_object`] on
52/// code paths where the xref table isn't built yet (the xref-stream
53/// parser, the `parse_xref_stream_at` helper).
54pub struct NoLengthResolver;
55
56impl LengthResolver for NoLengthResolver {
57    fn resolve_length(&mut self, _length_ref: ObjectId) -> Result<i64, PdfError> {
58        Err(PdfError::other(
59            "PDF parser: stream /Length is an indirect reference but no resolver \
60             was supplied (call parse_indirect_with_length_resolver)",
61        ))
62    }
63}
64
65/// Blanket impl so closures with the same signature work as
66/// [`LengthResolver`]s without writing a struct.
67impl<F> LengthResolver for F
68where
69    F: FnMut(ObjectId) -> Result<i64, PdfError>,
70{
71    fn resolve_length(&mut self, length_ref: ObjectId) -> Result<i64, PdfError> {
72        (self)(length_ref)
73    }
74}
75
76/// Hard ceiling on nested array / dict recursion. ISO 32000-1 places
77/// no explicit cap on object nesting, but real-world PDFs rarely
78/// exceed a depth of ~30 (page-tree fragments, ColorSpace arrays,
79/// AcroForm field trees). A hostile crafted file containing
80/// `[[[[[[[...]]]]]]]` thousands deep would overflow the parser's
81/// thread stack before reaching this guard at ~100; the guard turns
82/// the abort into a recoverable [`PdfError`]. Picked to be well
83/// above any legitimate PDF while staying low enough that a typical
84/// 8 MB Rust thread stack survives the recursion (each
85/// `parse_array` / `parse_dict_or_stream` frame is small —
86/// `Vec<Object>` setup + a token lookahead).
87const MAX_PARSE_DEPTH: u32 = 256;
88
89/// Recursive-descent parser over a [`Lexer`].
90pub struct Parser<'a> {
91    lex: Lexer<'a>,
92    /// Lookahead slot — when [`Self::peek`] reads a token, it stores
93    /// it here so the next [`Self::next`] returns it instead of
94    /// pulling from the lexer. Matches the conventional 1-token
95    /// lookahead recursive-descent shape.
96    peeked: Option<Token<'a>>,
97    /// Current array / dict nesting depth — incremented on entry to
98    /// [`Self::parse_array`] / [`Self::parse_dict_or_stream`] and
99    /// checked against [`MAX_PARSE_DEPTH`] to bound the thread-stack
100    /// footprint of a crafted PDF whose body is deeply nested
101    /// composite objects. See the constant's doc-comment for the
102    /// rationale.
103    depth: u32,
104}
105
106impl<'a> Parser<'a> {
107    pub fn new(input: &'a [u8]) -> Self {
108        Self {
109            lex: Lexer::new(input),
110            peeked: None,
111            depth: 0,
112        }
113    }
114
115    /// Construct from an existing [`Lexer`] — used by the xref /
116    /// trailer scanner so the same cursor state is shared.
117    pub fn from_lexer(lex: Lexer<'a>) -> Self {
118        Self {
119            lex,
120            peeked: None,
121            depth: 0,
122        }
123    }
124
125    /// Borrow the underlying lexer (for `seek` / `slice` / `position`).
126    pub fn lexer_mut(&mut self) -> &mut Lexer<'a> {
127        // Drop any peeked token — its position cache is stale once the
128        // caller moves the cursor.
129        self.peeked = None;
130        &mut self.lex
131    }
132
133    /// Current byte position. Returns the start of the peeked token
134    /// when one is buffered (so the position reflects "the next
135    /// token's start", not "wherever the lexer happens to be").
136    pub fn position(&self) -> usize {
137        if let Some(t) = &self.peeked {
138            t.start
139        } else {
140            self.lex.position()
141        }
142    }
143
144    fn next(&mut self) -> Result<Option<Token<'a>>, PdfError> {
145        if let Some(t) = self.peeked.take() {
146            return Ok(Some(t));
147        }
148        self.lex.next_token()
149    }
150
151    fn peek(&mut self) -> Result<Option<&Token<'a>>, PdfError> {
152        if self.peeked.is_none() {
153            self.peeked = self.lex.next_token()?;
154        }
155        Ok(self.peeked.as_ref())
156    }
157
158    /// Parse one [`Object`]. Returns `None` if the input is exhausted.
159    ///
160    /// Streams encountered with an indirect `/Length` (a `Reference`
161    /// value rather than a direct integer) are rejected — call
162    /// [`Self::parse_object_with_length_resolver`] from a context that
163    /// can resolve the reference (typically the top-level
164    /// [`crate::reader::document::DocumentReader::resolve`]).
165    pub fn parse_object(&mut self) -> Result<Option<Object>, PdfError> {
166        self.parse_object_with_length_resolver(&mut NoLengthResolver)
167    }
168
169    /// Variant of [`Self::parse_object`] that accepts a
170    /// [`LengthResolver`] to resolve indirect `/Length` references on
171    /// stream-object dictionaries per ISO 32000-1 §7.3.10 Example 3.
172    pub fn parse_object_with_length_resolver(
173        &mut self,
174        resolver: &mut dyn LengthResolver,
175    ) -> Result<Option<Object>, PdfError> {
176        let Some(tok) = self.next()? else {
177            return Ok(None);
178        };
179        Ok(Some(self.object_from_token(tok, resolver)?))
180    }
181
182    fn object_from_token(
183        &mut self,
184        tok: Token<'a>,
185        resolver: &mut dyn LengthResolver,
186    ) -> Result<Object, PdfError> {
187        match tok.kind {
188            TokenKind::Integer(n) => self.maybe_indirect_ref(n, tok.end),
189            TokenKind::Real(f) => Ok(Object::Real(f)),
190            TokenKind::Name(bytes) => Ok(Object::Name(String::from_utf8(bytes).map_err(|_| {
191                PdfError::other(format!("PDF parser: non-UTF-8 name at byte {}", tok.start))
192            })?)),
193            TokenKind::LiteralString(bytes) => Ok(Object::LiteralString(bytes)),
194            TokenKind::HexString(bytes) => Ok(Object::HexString(bytes)),
195            TokenKind::ArrayStart => self.parse_array(resolver),
196            TokenKind::DictStart => self.parse_dict_or_stream(tok.start, resolver),
197            TokenKind::Keyword(kw) => match kw {
198                b"true" => Ok(Object::Bool(true)),
199                b"false" => Ok(Object::Bool(false)),
200                b"null" => Ok(Object::Null),
201                other => Err(PdfError::other(format!(
202                    "PDF parser: unexpected keyword `{}` at byte {}",
203                    String::from_utf8_lossy(other),
204                    tok.start
205                ))),
206            },
207            TokenKind::ArrayEnd => Err(PdfError::other(format!(
208                "PDF parser: unexpected `]` at byte {}",
209                tok.start
210            ))),
211            TokenKind::DictEnd => Err(PdfError::other(format!(
212                "PDF parser: unexpected `>>` at byte {}",
213                tok.start
214            ))),
215        }
216    }
217
218    /// An integer at the start of an object can be the front of an
219    /// indirect reference (`<num> <gen> R`). Pull the next two tokens
220    /// non-destructively and only consume them if they form `int int
221    /// R`; otherwise fall back to a plain integer.
222    fn maybe_indirect_ref(&mut self, n: i64, _start_end: usize) -> Result<Object, PdfError> {
223        // We can't easily peek 2 ahead without nesting Option<Option<…>>.
224        // Save the lexer position so we can roll back if the
225        // lookahead doesn't match.
226        let saved_pos = self.lex.position();
227        let saved_peeked = self.peeked.clone();
228
229        let t2 = self.next()?;
230        let Some(t2) = t2 else {
231            // EOF after the int — not a ref.
232            return Ok(Object::Integer(n));
233        };
234        let TokenKind::Integer(gen) = t2.kind else {
235            // Not int int — restore and emit Integer.
236            self.peeked = Some(t2);
237            // (saved positions don't need restoring — peeked covers
238            // the next-token slot)
239            let _ = saved_pos;
240            let _ = saved_peeked;
241            return Ok(Object::Integer(n));
242        };
243        let t3 = self.next()?;
244        let Some(t3) = t3 else {
245            // We've consumed gen — can't put two tokens back. Restore
246            // via lexer rewind.
247            self.lex.seek(saved_pos);
248            self.peeked = saved_peeked;
249            return Ok(Object::Integer(n));
250        };
251        let TokenKind::Keyword(b"R") = t3.kind else {
252            // Not a ref — restore both consumed tokens by rewinding
253            // the lexer to where we were before we touched it.
254            self.lex.seek(saved_pos);
255            self.peeked = saved_peeked;
256            return Ok(Object::Integer(n));
257        };
258        if n < 1 || n > u32::MAX as i64 || gen < 0 || gen > u16::MAX as i64 {
259            return Err(PdfError::other(format!(
260                "PDF parser: indirect ref out of range `{n} {gen} R`"
261            )));
262        }
263        Ok(Object::Reference(ObjectId {
264            number: n as u32,
265            generation: gen as u16,
266        }))
267    }
268
269    fn parse_array(&mut self, resolver: &mut dyn LengthResolver) -> Result<Object, PdfError> {
270        if self.depth >= MAX_PARSE_DEPTH {
271            return Err(PdfError::other(format!(
272                "PDF parser: array nesting exceeds maximum depth ({MAX_PARSE_DEPTH})"
273            )));
274        }
275        self.depth += 1;
276        let result = (|| -> Result<Object, PdfError> {
277            let mut items = Vec::new();
278            loop {
279                let tok = self.next()?.ok_or_else(|| {
280                    PdfError::other("PDF parser: unterminated array (EOF before `]`)")
281                })?;
282                if let TokenKind::ArrayEnd = tok.kind {
283                    return Ok(Object::Array(items));
284                }
285                items.push(self.object_from_token(tok, resolver)?);
286            }
287        })();
288        self.depth -= 1;
289        result
290    }
291
292    fn parse_dict_or_stream(
293        &mut self,
294        start: usize,
295        resolver: &mut dyn LengthResolver,
296    ) -> Result<Object, PdfError> {
297        if self.depth >= MAX_PARSE_DEPTH {
298            return Err(PdfError::other(format!(
299                "PDF parser: dict nesting exceeds maximum depth ({MAX_PARSE_DEPTH})"
300            )));
301        }
302        self.depth += 1;
303        let result = self.parse_dict_or_stream_inner(start, resolver);
304        self.depth -= 1;
305        result
306    }
307
308    fn parse_dict_or_stream_inner(
309        &mut self,
310        start: usize,
311        resolver: &mut dyn LengthResolver,
312    ) -> Result<Object, PdfError> {
313        let mut dict = Dict::new();
314        loop {
315            let tok = self.next()?.ok_or_else(|| {
316                PdfError::other(format!(
317                    "PDF parser: unterminated dict starting at byte {start} (EOF before `>>`)"
318                ))
319            })?;
320            if let TokenKind::DictEnd = tok.kind {
321                break;
322            }
323            // Key must be a Name.
324            let TokenKind::Name(key_bytes) = tok.kind else {
325                return Err(PdfError::other(format!(
326                    "PDF parser: dict key must be a Name at byte {} (got {:?})",
327                    tok.start, tok.kind
328                )));
329            };
330            let key = String::from_utf8(key_bytes).map_err(|_| {
331                PdfError::other(format!(
332                    "PDF parser: non-UTF-8 dict key at byte {}",
333                    tok.start
334                ))
335            })?;
336            // Value. Note: nested dict / array values inside a stream
337            // dictionary are themselves direct objects (§7.3.8.1 — the
338            // stream dict shall be a direct object). The resolver only
339            // applies to the top-level /Length lookup, so nested calls
340            // pass `None`.
341            let val = self.parse_object()?.ok_or_else(|| {
342                PdfError::other(format!(
343                    "PDF parser: dict key `{key}` at byte {} has no value",
344                    tok.start
345                ))
346            })?;
347            dict.set(&key, val);
348        }
349        // Check whether this dict is a stream header — `stream` keyword
350        // immediately follows (after optional whitespace).
351        let after = self.peek()?.cloned();
352        if let Some(t) = after {
353            if let TokenKind::Keyword(b"stream") = t.kind {
354                // Consume the `stream` keyword.
355                let _ = self.next()?;
356                // Per §7.3.8.1, the stream data starts immediately
357                // after the EOL marker that follows the `stream`
358                // keyword. The marker is CRLF or just LF.
359                let raw = self.lex.input();
360                let mut data_start = t.end;
361                if data_start < raw.len() && raw[data_start] == b'\r' {
362                    data_start += 1;
363                }
364                if data_start < raw.len() && raw[data_start] == b'\n' {
365                    data_start += 1;
366                }
367                // /Length is required per §7.3.8.2 — use it to find
368                // the body's end. ISO 32000-1 §7.3.10 Example 3
369                // makes indirect /Length normative on stream objects
370                // (`<< /Length 8 0 R >>` for one-pass writers). When a
371                // resolver is supplied (the top-level
372                // `DocumentReader::resolve` path), call it to fetch the
373                // referenced integer; when no resolver is supplied
374                // (the xref-stream parsing path, where the xref isn't
375                // yet built) the indirect form is rejected.
376                let length_obj = dict.entries().iter().find_map(|(k, v)| {
377                    if k == "Length" {
378                        Some(v.clone())
379                    } else {
380                        None
381                    }
382                });
383                let len: usize = match length_obj {
384                    Some(Object::Integer(n)) if n >= 0 => n as usize,
385                    Some(Object::Reference(id)) => {
386                        let resolved = resolver.resolve_length(id)?;
387                        if resolved < 0 {
388                            return Err(PdfError::other(format!(
389                                "PDF parser: indirect /Length {id:?} resolved to \
390                                 negative integer {resolved}"
391                            )));
392                        }
393                        // Patch the dict so downstream consumers (e.g.
394                        // `decode_stream`, encryption length tracking)
395                        // see the resolved direct integer rather than
396                        // the now-stale `Reference`.
397                        dict.set("Length", Object::Integer(resolved));
398                        resolved as usize
399                    }
400                    Some(other) => {
401                        return Err(PdfError::other(format!(
402                            "PDF parser: stream /Length must be a non-negative integer (got {other:?})"
403                        )));
404                    }
405                    None => {
406                        return Err(PdfError::other(
407                            "PDF parser: stream object missing required /Length entry",
408                        ));
409                    }
410                };
411                let data_end = data_start.saturating_add(len).min(raw.len());
412                let data = self.lex.slice(data_start, data_end).to_vec();
413                // Skip the body + the EOL before `endstream` + the
414                // `endstream` keyword itself.
415                self.lex.seek(data_end);
416                self.peeked = None;
417                // Drop any whitespace + EOL between the data and
418                // `endstream`.
419                let endstream = self
420                    .next()?
421                    .ok_or_else(|| PdfError::other("PDF parser: stream missing `endstream`"))?;
422                let TokenKind::Keyword(b"endstream") = endstream.kind else {
423                    return Err(PdfError::other(format!(
424                        "PDF parser: expected `endstream` after stream body at byte {} (got {:?})",
425                        endstream.start, endstream.kind
426                    )));
427                };
428                return Ok(Object::Stream(Stream::new(dict, data)));
429            }
430        }
431        Ok(Object::Dict(dict))
432    }
433
434    /// Parse one indirect object (`<n> <gen> obj … endobj`). Returns
435    /// `(ObjectId, Object)`. The caller is responsible for positioning
436    /// the parser at the first byte of the indirect object header.
437    ///
438    /// Streams with an indirect `/Length` are rejected — use
439    /// [`Self::parse_indirect_with_length_resolver`] from a context
440    /// that has a built xref table.
441    pub fn parse_indirect(&mut self) -> Result<(ObjectId, Object), PdfError> {
442        self.parse_indirect_with_length_resolver(&mut NoLengthResolver)
443    }
444
445    /// Variant of [`Self::parse_indirect`] that accepts a
446    /// [`LengthResolver`] to resolve indirect `/Length` references on
447    /// stream-object dictionaries (ISO 32000-1 §7.3.10 Example 3).
448    pub fn parse_indirect_with_length_resolver(
449        &mut self,
450        resolver: &mut dyn LengthResolver,
451    ) -> Result<(ObjectId, Object), PdfError> {
452        let n = self.expect_integer("indirect-object number")?;
453        let gen = self.expect_integer("indirect-object generation")?;
454        let obj_kw = self
455            .next()?
456            .ok_or_else(|| PdfError::other("PDF parser: unexpected EOF before `obj` keyword"))?;
457        let TokenKind::Keyword(b"obj") = obj_kw.kind else {
458            return Err(PdfError::other(format!(
459                "PDF parser: expected `obj` keyword at byte {} (got {:?})",
460                obj_kw.start, obj_kw.kind
461            )));
462        };
463        let body = self
464            .parse_object_with_length_resolver(resolver)?
465            .ok_or_else(|| PdfError::other("PDF parser: indirect object missing body"))?;
466        let endobj = self
467            .next()?
468            .ok_or_else(|| PdfError::other("PDF parser: indirect object missing `endobj`"))?;
469        let TokenKind::Keyword(b"endobj") = endobj.kind else {
470            return Err(PdfError::other(format!(
471                "PDF parser: expected `endobj` at byte {} (got {:?})",
472                endobj.start, endobj.kind
473            )));
474        };
475        if n < 1 || n > u32::MAX as i64 || !(0..=u16::MAX as i64).contains(&gen) {
476            return Err(PdfError::other(format!(
477                "PDF parser: indirect-object id `{n} {gen}` out of range"
478            )));
479        }
480        Ok((
481            ObjectId {
482                number: n as u32,
483                generation: gen as u16,
484            },
485            body,
486        ))
487    }
488
489    fn expect_integer(&mut self, what: &str) -> Result<i64, PdfError> {
490        let tok = self
491            .next()?
492            .ok_or_else(|| PdfError::other(format!("PDF parser: expected {what}, got EOF")))?;
493        match tok.kind {
494            TokenKind::Integer(n) => Ok(n),
495            other => Err(PdfError::other(format!(
496                "PDF parser: expected {what} (integer), got {other:?} at byte {}",
497                tok.start
498            ))),
499        }
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    fn parse_one(input: &[u8]) -> Object {
508        Parser::new(input)
509            .parse_object()
510            .unwrap()
511            .expect("parsed object")
512    }
513
514    #[test]
515    fn primitives_parse_to_objects() {
516        assert!(matches!(parse_one(b"true"), Object::Bool(true)));
517        assert!(matches!(parse_one(b"false"), Object::Bool(false)));
518        assert!(matches!(parse_one(b"null"), Object::Null));
519        assert!(matches!(parse_one(b"42"), Object::Integer(42)));
520        match parse_one(b"2.5") {
521            Object::Real(f) => assert!((f - 2.5).abs() < 1e-9),
522            other => panic!("expected real, got {other:?}"),
523        }
524        match parse_one(b"(hello)") {
525            Object::LiteralString(b) => assert_eq!(b, b"hello".to_vec()),
526            other => panic!("expected literal string, got {other:?}"),
527        }
528        match parse_one(b"<48656C6C6F>") {
529            Object::HexString(b) => assert_eq!(b, b"Hello".to_vec()),
530            other => panic!("expected hex string, got {other:?}"),
531        }
532        match parse_one(b"/Pages") {
533            Object::Name(s) => assert_eq!(s, "Pages"),
534            other => panic!("expected name, got {other:?}"),
535        }
536    }
537
538    #[test]
539    fn array_parses_recursively() {
540        let arr = parse_one(b"[1 2.5 (a) /b [3 4]]");
541        let Object::Array(items) = arr else {
542            panic!("expected array")
543        };
544        assert_eq!(items.len(), 5);
545        assert!(matches!(items[0], Object::Integer(1)));
546        assert!(matches!(items[1], Object::Real(_)));
547        assert!(matches!(items[2], Object::LiteralString(_)));
548        assert!(matches!(items[3], Object::Name(_)));
549        assert!(matches!(items[4], Object::Array(_)));
550    }
551
552    #[test]
553    fn dict_parses_with_named_keys() {
554        let d = parse_one(b"<< /Type /Page /Count 3 /Kids [1 2 3] >>");
555        let Object::Dict(d) = d else {
556            panic!("expected dict")
557        };
558        let entries = d.entries();
559        assert_eq!(entries.len(), 3);
560        assert_eq!(entries[0].0, "Type");
561        assert_eq!(entries[1].0, "Count");
562        assert_eq!(entries[2].0, "Kids");
563    }
564
565    #[test]
566    fn indirect_reference_recognised() {
567        let r = parse_one(b"5 0 R");
568        let Object::Reference(id) = r else {
569            panic!("expected ref")
570        };
571        assert_eq!(id.number, 5);
572        assert_eq!(id.generation, 0);
573    }
574
575    #[test]
576    fn integer_followed_by_non_ref_does_not_become_ref() {
577        // `5 0` (without R) → array of two integers.
578        let arr = parse_one(b"[5 0]");
579        let Object::Array(items) = arr else {
580            panic!("expected array")
581        };
582        assert_eq!(items.len(), 2);
583        assert!(matches!(items[0], Object::Integer(5)));
584        assert!(matches!(items[1], Object::Integer(0)));
585    }
586
587    #[test]
588    fn indirect_object_round_trip() {
589        let input = b"3 0 obj\n<< /Type /Page >>\nendobj\n";
590        let mut p = Parser::new(input);
591        let (id, body) = p.parse_indirect().unwrap();
592        assert_eq!(id.number, 3);
593        assert!(matches!(body, Object::Dict(_)));
594    }
595
596    #[test]
597    fn stream_object_extracts_body_per_length() {
598        let input = b"4 0 obj\n<< /Length 5 >>\nstream\nABCDE\nendstream\nendobj\n";
599        let mut p = Parser::new(input);
600        let (_, body) = p.parse_indirect().unwrap();
601        let Object::Stream(s) = body else {
602            panic!("expected stream")
603        };
604        assert_eq!(s.data, b"ABCDE".to_vec());
605    }
606
607    #[test]
608    fn stream_with_crlf_eol_marker() {
609        // After `stream`, the EOL can be CRLF (or LF). The body
610        // begins immediately after the marker.
611        let mut input = Vec::new();
612        input.extend_from_slice(b"4 0 obj\n<< /Length 3 >>\nstream\r\nXYZ\nendstream\nendobj\n");
613        let mut p = Parser::new(&input);
614        let (_, body) = p.parse_indirect().unwrap();
615        let Object::Stream(s) = body else {
616            panic!("expected stream")
617        };
618        assert_eq!(s.data, b"XYZ".to_vec());
619    }
620
621    #[test]
622    fn stream_indirect_length_without_resolver_is_rejected() {
623        // /Length 7 0 R — an indirect reference; the resolver-less
624        // entry point (used by the xref-stream parser, before any
625        // xref table exists) must still reject this so a malformed
626        // xref-stream object doesn't silently read past EOF.
627        let input = b"4 0 obj\n<< /Length 7 0 R >>\nstream\nXYZ\nendstream\nendobj\n";
628        let mut p = Parser::new(input);
629        let err = p.parse_indirect().unwrap_err();
630        assert!(format!("{err}").contains("indirect reference"));
631    }
632
633    #[test]
634    fn stream_indirect_length_with_resolver_resolves() {
635        // Round-91: ISO 32000-1 §7.3.10 Example 3 — stream's /Length
636        // is an indirect reference; with a resolver, the parser
637        // fetches the target integer and slices the body accordingly.
638        let input = b"4 0 obj\n<< /Length 7 0 R >>\nstream\nXYZ\nendstream\nendobj\n";
639        let mut p = Parser::new(input);
640        let mut resolver = |id: ObjectId| -> Result<i64, PdfError> {
641            assert_eq!(id, ObjectId::new(7));
642            Ok(3)
643        };
644        let (id, body) = p
645            .parse_indirect_with_length_resolver(&mut resolver)
646            .unwrap();
647        assert_eq!(id, ObjectId::new(4));
648        let Object::Stream(s) = body else {
649            panic!("expected stream, got {body:?}")
650        };
651        assert_eq!(s.data, b"XYZ".to_vec());
652        // The dict should now carry the resolved direct integer so
653        // downstream consumers (decoders, encryption) don't see the
654        // stale Reference.
655        let length = s
656            .dict
657            .entries()
658            .iter()
659            .find(|(k, _)| k == "Length")
660            .map(|(_, v)| v.clone());
661        assert!(matches!(length, Some(Object::Integer(3))));
662    }
663
664    #[test]
665    fn stream_indirect_length_negative_resolution_errors() {
666        // A resolver that returns a negative integer must be flagged
667        // (a real PDF should never do this, but we don't trust input).
668        let input = b"4 0 obj\n<< /Length 7 0 R >>\nstream\nXYZ\nendstream\nendobj\n";
669        let mut p = Parser::new(input);
670        let mut resolver = |_id: ObjectId| -> Result<i64, PdfError> { Ok(-1) };
671        let err = p
672            .parse_indirect_with_length_resolver(&mut resolver)
673            .unwrap_err();
674        assert!(format!("{err}").contains("negative"));
675    }
676
677    #[test]
678    fn stream_indirect_length_resolver_error_propagates() {
679        // The resolver may legitimately fail (missing xref entry,
680        // wrong-typed target object). Errors flow up the parser.
681        let input = b"4 0 obj\n<< /Length 7 0 R >>\nstream\nXYZ\nendstream\nendobj\n";
682        let mut p = Parser::new(input);
683        let mut resolver =
684            |_id: ObjectId| -> Result<i64, PdfError> { Err(PdfError::other("test: not found")) };
685        let err = p
686            .parse_indirect_with_length_resolver(&mut resolver)
687            .unwrap_err();
688        assert!(format!("{err}").contains("not found"));
689    }
690
691    #[test]
692    fn stream_missing_length_is_rejected() {
693        let input = b"4 0 obj\n<< /Type /XObject >>\nstream\nXYZ\nendstream\nendobj\n";
694        let mut p = Parser::new(input);
695        let err = p.parse_indirect().unwrap_err();
696        assert!(format!("{err}").contains("/Length"));
697    }
698
699    #[test]
700    fn nested_dict_in_array() {
701        // The bracket-state tracking has to handle dicts nested inside
702        // arrays and vice versa.
703        let arr = parse_one(b"[<< /A 1 >> << /B 2 >>]");
704        let Object::Array(items) = arr else {
705            panic!("expected array")
706        };
707        assert_eq!(items.len(), 2);
708        assert!(matches!(items[0], Object::Dict(_)));
709        assert!(matches!(items[1], Object::Dict(_)));
710    }
711
712    #[test]
713    fn dict_with_indirect_reference_value() {
714        let d = parse_one(b"<< /Pages 2 0 R >>");
715        let Object::Dict(d) = d else {
716            panic!("expected dict")
717        };
718        let entries = d.entries();
719        assert_eq!(entries.len(), 1);
720        assert_eq!(entries[0].0, "Pages");
721        assert!(matches!(entries[0].1, Object::Reference(_)));
722    }
723
724    /// Deeply nested arrays must surface a clean `PdfError` from the
725    /// [`MAX_PARSE_DEPTH`] guard rather than blowing the thread stack
726    /// — round 191 hardening alongside the fuzz-discovered ObjStm
727    /// container-cycle fix in `document.rs`.
728    #[test]
729    fn deeply_nested_array_rejected_below_stack_overflow() {
730        let mut body = vec![b'['; MAX_PARSE_DEPTH as usize + 8];
731        body.extend(std::iter::repeat_n(b']', MAX_PARSE_DEPTH as usize + 8));
732        let err = Parser::new(&body)
733            .parse_object()
734            .expect_err("over-deep array must error");
735        let msg = format!("{err}");
736        assert!(
737            msg.contains("nesting") || msg.contains("depth"),
738            "unexpected error message: {msg}"
739        );
740    }
741
742    /// Same shape, but for dictionaries — deeply nested `<< /k <<
743    /// /k << ... >> >> >>` must surface a clean error instead of
744    /// recursing past the thread stack.
745    #[test]
746    fn deeply_nested_dict_rejected_below_stack_overflow() {
747        // Each level needs a `/k ` key + `<<` open + matching `>>`.
748        let levels = (MAX_PARSE_DEPTH as usize) + 8;
749        let mut body = Vec::new();
750        for _ in 0..levels {
751            body.extend_from_slice(b"<< /k ");
752        }
753        body.extend_from_slice(b"null");
754        for _ in 0..levels {
755            body.extend_from_slice(b" >>");
756        }
757        let err = Parser::new(&body)
758            .parse_object()
759            .expect_err("over-deep dict must error");
760        let msg = format!("{err}");
761        assert!(
762            msg.contains("nesting") || msg.contains("depth"),
763            "unexpected error message: {msg}"
764        );
765    }
766}