Skip to main content

stet_pdf_reader/
lexer.rs

1// stet-pdf-reader
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! PDF tokenizer.
6
7use crate::error::PdfError;
8use crate::objects::{PdfDict, PdfObj};
9
10/// PDF token types.
11#[derive(Debug, Clone, PartialEq)]
12pub enum Token {
13    Bool(bool),
14    Int(i64),
15    Real(f64),
16    /// Name without leading `/`.
17    Name(Vec<u8>),
18    /// Literal string `(...)`, decoded.
19    LitString(Vec<u8>),
20    /// Hex string `<...>`, decoded.
21    HexString(Vec<u8>),
22    /// `[`
23    ArrayBegin,
24    /// `]`
25    ArrayEnd,
26    /// `<<`
27    DictBegin,
28    /// `>>`
29    DictEnd,
30    /// Keywords: `obj`, `endobj`, `stream`, `endstream`, `R`, `null`, `xref`, `trailer`, etc.
31    Keyword(Vec<u8>),
32    Eof,
33}
34
35/// PDF lexer operating on a byte slice with a cursor.
36pub struct Lexer<'a> {
37    data: &'a [u8],
38    pos: usize,
39}
40
41impl<'a> Lexer<'a> {
42    pub fn new(data: &'a [u8]) -> Self {
43        Self { data, pos: 0 }
44    }
45
46    /// Create a lexer starting at a given offset.
47    pub fn at(data: &'a [u8], pos: usize) -> Self {
48        Self { data, pos }
49    }
50
51    /// Current byte offset.
52    pub fn pos(&self) -> usize {
53        self.pos
54    }
55
56    /// Set the cursor position.
57    pub fn set_pos(&mut self, pos: usize) {
58        self.pos = pos;
59    }
60
61    /// Underlying data slice.
62    pub fn data(&self) -> &'a [u8] {
63        self.data
64    }
65
66    /// Read the next token, advancing the cursor.
67    pub fn next_token(&mut self) -> Result<Token, PdfError> {
68        self.skip_whitespace_and_comments();
69
70        if self.pos >= self.data.len() {
71            return Ok(Token::Eof);
72        }
73
74        let b = self.data[self.pos];
75        match b {
76            b'/' => self.read_name(),
77            b'(' => self.read_literal_string(),
78            b'<' => {
79                if self.pos + 1 < self.data.len() && self.data[self.pos + 1] == b'<' {
80                    self.pos += 2;
81                    Ok(Token::DictBegin)
82                } else {
83                    self.read_hex_string()
84                }
85            }
86            b'>' => {
87                if self.pos + 1 < self.data.len() && self.data[self.pos + 1] == b'>' {
88                    self.pos += 2;
89                    Ok(Token::DictEnd)
90                } else {
91                    self.pos += 1;
92                    Err(PdfError::UnexpectedToken {
93                        expected: ">>".into(),
94                        got: ">".into(),
95                    })
96                }
97            }
98            b'[' => {
99                self.pos += 1;
100                Ok(Token::ArrayBegin)
101            }
102            b']' => {
103                self.pos += 1;
104                Ok(Token::ArrayEnd)
105            }
106            b'+' | b'-' | b'.' | b'0'..=b'9' => self.read_number(),
107            b'\'' | b'"' => {
108                // PDF text operators: ' (move to next line and show) and " (set spacing and show)
109                self.pos += 1;
110                Ok(Token::Keyword(vec![b]))
111            }
112            _ if b.is_ascii_alphabetic() => self.read_keyword(),
113            _ => {
114                let ch = b as char;
115                self.pos += 1;
116                Err(PdfError::UnexpectedToken {
117                    expected: "token".into(),
118                    got: format!("byte 0x{b:02x} '{ch}'"),
119                })
120            }
121        }
122    }
123
124    /// Peek at the next token without advancing.
125    pub fn peek_token(&mut self) -> Result<Token, PdfError> {
126        let saved = self.pos;
127        let tok = self.next_token();
128        self.pos = saved;
129        tok
130    }
131
132    /// Skip whitespace (space, tab, CR, LF, FF, NUL) and comments (% to EOL).
133    fn skip_whitespace_and_comments(&mut self) {
134        loop {
135            // Skip whitespace
136            while self.pos < self.data.len() && is_whitespace(self.data[self.pos]) {
137                self.pos += 1;
138            }
139            // Skip comments
140            if self.pos < self.data.len() && self.data[self.pos] == b'%' {
141                while self.pos < self.data.len()
142                    && self.data[self.pos] != b'\n'
143                    && self.data[self.pos] != b'\r'
144                {
145                    self.pos += 1;
146                }
147            } else {
148                break;
149            }
150        }
151    }
152
153    /// Read a number token (integer or real).
154    fn read_number(&mut self) -> Result<Token, PdfError> {
155        let start = self.pos;
156        let mut has_dot = false;
157
158        // Optional sign
159        if self.pos < self.data.len()
160            && (self.data[self.pos] == b'+' || self.data[self.pos] == b'-')
161        {
162            self.pos += 1;
163        }
164
165        // Digits and optional decimal point
166        while self.pos < self.data.len() {
167            let b = self.data[self.pos];
168            if b == b'.' && !has_dot {
169                has_dot = true;
170                self.pos += 1;
171            } else if b.is_ascii_digit() {
172                self.pos += 1;
173            } else {
174                break;
175            }
176        }
177
178        // Handle implicit exponent: "0.00-50" means "0.00e-50".
179        // Some PDF writers omit the 'e', producing a sign+digits suffix
180        // immediately after a real number.
181        let mut implicit_exp = false;
182        if has_dot
183            && self.pos < self.data.len()
184            && (self.data[self.pos] == b'+' || self.data[self.pos] == b'-')
185        {
186            // Peek ahead to check for digits after the sign
187            let sign_pos = self.pos;
188            let mut peek = sign_pos + 1;
189            while peek < self.data.len() && self.data[peek].is_ascii_digit() {
190                peek += 1;
191            }
192            if peek > sign_pos + 1 {
193                // Consume sign + exponent digits
194                self.pos = peek;
195                implicit_exp = true;
196            }
197        }
198
199        let s = &self.data[start..self.pos];
200        if s == b"+" || s == b"-" || s == b"." || s == b"+." || s == b"-." {
201            // Bare sign or dot without digits.  Treat as zero to match pdf.js
202            // behavior — some malformed PDFs use `--2.5` meaning `0 -2.5`, and
203            // returning a keyword would desynchronize the operand stack.
204            return Ok(Token::Int(0));
205        }
206
207        if has_dot {
208            let f: f64 = if implicit_exp {
209                // Insert 'e' before the exponent sign: "0.00-50" → "0.00e-50"
210                let s_str =
211                    std::str::from_utf8(s).map_err(|_| PdfError::Other("invalid number".into()))?;
212                let sign_idx = s_str.rfind(['+', '-']).unwrap();
213                let mut with_e = String::from(&s_str[..sign_idx]);
214                with_e.push('e');
215                with_e.push_str(&s_str[sign_idx..]);
216                with_e
217                    .parse()
218                    .map_err(|_| PdfError::Other(format!("invalid real: {s_str}")))?
219            } else {
220                let s_str =
221                    std::str::from_utf8(s).map_err(|_| PdfError::Other("invalid number".into()))?;
222                s_str
223                    .parse()
224                    .map_err(|_| PdfError::Other(format!("invalid real: {s_str}")))?
225            };
226            Ok(Token::Real(f))
227        } else {
228            let s_str =
229                std::str::from_utf8(s).map_err(|_| PdfError::Other("invalid number".into()))?;
230            let n: i64 = s_str
231                .parse()
232                .map_err(|_| PdfError::Other(format!("invalid integer: {s_str}")))?;
233            Ok(Token::Int(n))
234        }
235    }
236
237    /// Read a name token (after consuming `/`).
238    fn read_name(&mut self) -> Result<Token, PdfError> {
239        self.pos += 1; // skip '/'
240        let mut name = Vec::new();
241
242        while self.pos < self.data.len() {
243            let b = self.data[self.pos];
244            if is_whitespace(b) || is_delimiter(b) {
245                break;
246            }
247            if b == b'#' && self.pos + 2 < self.data.len() {
248                // Hex escape
249                let hi = hex_digit(self.data[self.pos + 1]);
250                let lo = hex_digit(self.data[self.pos + 2]);
251                if let (Some(h), Some(l)) = (hi, lo) {
252                    name.push(h << 4 | l);
253                    self.pos += 3;
254                    continue;
255                }
256            }
257            name.push(b);
258            self.pos += 1;
259        }
260
261        Ok(Token::Name(name))
262    }
263
264    /// Read a literal string `(...)` with escapes and nested parens.
265    fn read_literal_string(&mut self) -> Result<Token, PdfError> {
266        self.pos += 1; // skip '('
267        let mut result = Vec::new();
268        let mut depth = 1u32;
269
270        while self.pos < self.data.len() {
271            let b = self.data[self.pos];
272            match b {
273                b'(' => {
274                    depth += 1;
275                    result.push(b);
276                    self.pos += 1;
277                }
278                b')' => {
279                    depth -= 1;
280                    if depth == 0 {
281                        self.pos += 1;
282                        return Ok(Token::LitString(result));
283                    }
284                    result.push(b);
285                    self.pos += 1;
286                }
287                b'\\' => {
288                    self.pos += 1;
289                    if self.pos >= self.data.len() {
290                        break;
291                    }
292                    let esc = self.data[self.pos];
293                    match esc {
294                        b'n' => {
295                            result.push(b'\n');
296                            self.pos += 1;
297                        }
298                        b'r' => {
299                            result.push(b'\r');
300                            self.pos += 1;
301                        }
302                        b't' => {
303                            result.push(b'\t');
304                            self.pos += 1;
305                        }
306                        b'b' => {
307                            result.push(0x08);
308                            self.pos += 1;
309                        }
310                        b'f' => {
311                            result.push(0x0C);
312                            self.pos += 1;
313                        }
314                        b'(' | b')' | b'\\' => {
315                            result.push(esc);
316                            self.pos += 1;
317                        }
318                        b'\r' => {
319                            // Line continuation
320                            self.pos += 1;
321                            if self.pos < self.data.len() && self.data[self.pos] == b'\n' {
322                                self.pos += 1;
323                            }
324                        }
325                        b'\n' => {
326                            // Line continuation
327                            self.pos += 1;
328                        }
329                        b'0'..=b'7' => {
330                            // Octal escape (1-3 digits).
331                            //
332                            // Accumulated in u32, not u8: three octal digits
333                            // reach 0o777 = 511, so `val * 8` overflows a u8
334                            // on the third digit. PDF 32000-1 7.3.4.2 says the
335                            // high-order overflow "shall be ignored", which is
336                            // exactly the truncating cast below — so the
337                            // release build's silent wrap was already the
338                            // correct byte. Only the arithmetic was wrong, and
339                            // it panicked under overflow checks (reached by
340                            // `pdf_samples/142.pdf`).
341                            let mut val: u32 = u32::from(esc - b'0');
342                            self.pos += 1;
343                            if self.pos < self.data.len()
344                                && self.data[self.pos] >= b'0'
345                                && self.data[self.pos] <= b'7'
346                            {
347                                val = val * 8 + u32::from(self.data[self.pos] - b'0');
348                                self.pos += 1;
349                                if self.pos < self.data.len()
350                                    && self.data[self.pos] >= b'0'
351                                    && self.data[self.pos] <= b'7'
352                                {
353                                    val = val * 8 + u32::from(self.data[self.pos] - b'0');
354                                    self.pos += 1;
355                                }
356                            }
357                            result.push(val as u8);
358                        }
359                        _ => {
360                            // Unknown escape — just include the character
361                            result.push(esc);
362                            self.pos += 1;
363                        }
364                    }
365                }
366                _ => {
367                    result.push(b);
368                    self.pos += 1;
369                }
370            }
371        }
372
373        Err(PdfError::Unterminated("string"))
374    }
375
376    /// Read a hex string `<...>`.
377    fn read_hex_string(&mut self) -> Result<Token, PdfError> {
378        self.pos += 1; // skip '<'
379        let mut result = Vec::new();
380        let mut high_nibble: Option<u8> = None;
381
382        while self.pos < self.data.len() {
383            let b = self.data[self.pos];
384            if b == b'>' {
385                self.pos += 1;
386                // Odd number of hex digits: implicit trailing 0
387                if let Some(h) = high_nibble {
388                    result.push(h << 4);
389                }
390                return Ok(Token::HexString(result));
391            }
392            if is_whitespace(b) {
393                self.pos += 1;
394                continue;
395            }
396            if let Some(nibble) = hex_digit(b) {
397                match high_nibble {
398                    None => high_nibble = Some(nibble),
399                    Some(h) => {
400                        result.push(h << 4 | nibble);
401                        high_nibble = None;
402                    }
403                }
404                self.pos += 1;
405            } else {
406                self.pos += 1;
407                return Err(PdfError::UnexpectedToken {
408                    expected: "hex digit".into(),
409                    got: format!("byte 0x{b:02x}"),
410                });
411            }
412        }
413
414        Err(PdfError::Unterminated("hex string"))
415    }
416
417    /// Read a keyword (alphabetic sequence).
418    fn read_keyword(&mut self) -> Result<Token, PdfError> {
419        let start = self.pos;
420        while self.pos < self.data.len() && self.data[self.pos].is_ascii_alphabetic() {
421            self.pos += 1;
422        }
423        let word = &self.data[start..self.pos];
424        match word {
425            b"true" => Ok(Token::Bool(true)),
426            b"false" => Ok(Token::Bool(false)),
427            _ => Ok(Token::Keyword(word.to_vec())),
428        }
429    }
430}
431
432/// Maximum nesting depth for arrays and dictionaries within a single object.
433///
434/// `parse_object_from_token` is a recursive-descent parser: every `[` and `<<`
435/// costs a native stack frame. Without a cap, a small crafted file containing
436/// `[[[[…` aborts the process with a stack overflow, which is not a panic and
437/// so cannot be contained by `catch_unwind`. Real PDFs nest a handful of
438/// levels deep (the deepest common shape is a shading dictionary inside a
439/// pattern inside a resource dictionary); 256 is far beyond any legitimate
440/// document while keeping worst-case stack use to a few tens of kilobytes.
441pub const MAX_OBJECT_DEPTH: u32 = 256;
442
443/// Parse a PDF object from the lexer (recursive descent).
444///
445/// This handles arrays, dicts, and indirect references (`N G R`).
446///
447/// Container nesting is capped at [`MAX_OBJECT_DEPTH`]; beyond that the parse
448/// returns [`PdfError::NestingTooDeep`] rather than exhausting the stack.
449pub fn parse_object(lexer: &mut Lexer) -> Result<PdfObj, PdfError> {
450    parse_object_at_depth(lexer, 0)
451}
452
453/// Parse a PDF object given an already-consumed first token.
454///
455/// Container nesting is capped at [`MAX_OBJECT_DEPTH`].
456pub fn parse_object_from_token(lexer: &mut Lexer, tok: Token) -> Result<PdfObj, PdfError> {
457    parse_object_from_token_at_depth(lexer, tok, 0)
458}
459
460/// [`parse_object`], entered at an explicit container nesting depth.
461pub fn parse_object_at_depth(lexer: &mut Lexer, depth: u32) -> Result<PdfObj, PdfError> {
462    let tok = lexer.next_token()?;
463    parse_object_from_token_at_depth(lexer, tok, depth)
464}
465
466/// [`parse_object_from_token`], entered at an explicit container nesting depth.
467///
468/// `depth` counts the arrays and dictionaries already open around this object.
469pub fn parse_object_from_token_at_depth(
470    lexer: &mut Lexer,
471    tok: Token,
472    depth: u32,
473) -> Result<PdfObj, PdfError> {
474    // Refuse to open another container once the cap is reached. The token has
475    // already been consumed, so the caller's loop resumes on the container's
476    // body; every token inside it is then parsed by a loop at or below the
477    // cap, which terminates without recursing further.
478    if depth >= MAX_OBJECT_DEPTH && matches!(tok, Token::ArrayBegin | Token::DictBegin) {
479        return Err(PdfError::NestingTooDeep {
480            context: "array/dictionary",
481            limit: MAX_OBJECT_DEPTH,
482        });
483    }
484    match tok {
485        Token::Bool(b) => Ok(PdfObj::Bool(b)),
486        Token::Real(f) => Ok(PdfObj::Real(f)),
487        Token::Int(n) => {
488            // Could be start of indirect reference: N G R
489            let saved = lexer.pos();
490            match lexer.next_token() {
491                Ok(Token::Int(g)) => match lexer.next_token() {
492                    Ok(Token::Keyword(ref kw)) if kw == b"R" => Ok(PdfObj::Ref(n as u32, g as u16)),
493                    _ => {
494                        lexer.set_pos(saved);
495                        Ok(PdfObj::Int(n))
496                    }
497                },
498                _ => {
499                    lexer.set_pos(saved);
500                    Ok(PdfObj::Int(n))
501                }
502            }
503        }
504        Token::Name(n) => Ok(PdfObj::Name(n)),
505        Token::LitString(s) => Ok(PdfObj::Str(s)),
506        Token::HexString(s) => Ok(PdfObj::Str(s)),
507        Token::Keyword(ref kw) if kw == b"null" => Ok(PdfObj::Null),
508        Token::ArrayBegin => {
509            let mut elems = Vec::new();
510            loop {
511                let t = lexer.next_token()?;
512                if t == Token::ArrayEnd || t == Token::Eof {
513                    break;
514                }
515                // Skip unparseable tokens in arrays (corrupt PDF), and skip a
516                // container that would exceed the depth cap.
517                if let Ok(obj) = parse_object_from_token_at_depth(lexer, t, depth + 1) {
518                    elems.push(obj);
519                }
520            }
521            Ok(PdfObj::Array(elems))
522        }
523        Token::DictBegin => {
524            let dict = parse_dict_body_at_depth(lexer, depth + 1)?;
525            Ok(PdfObj::Dict(dict))
526        }
527        _ => Err(PdfError::UnexpectedToken {
528            expected: "object".into(),
529            got: format!("{tok:?}"),
530        }),
531    }
532}
533
534/// Parse dictionary entries until `>>`, returning a PdfDict.
535///
536/// Container nesting is capped at [`MAX_OBJECT_DEPTH`].
537pub fn parse_dict_body(lexer: &mut Lexer) -> Result<PdfDict, PdfError> {
538    parse_dict_body_at_depth(lexer, 0)
539}
540
541/// [`parse_dict_body`], entered at an explicit container nesting depth.
542///
543/// `depth` counts this dictionary itself, so it is one greater than the depth
544/// passed to the [`parse_object_from_token_at_depth`] call that opened it.
545pub fn parse_dict_body_at_depth(lexer: &mut Lexer, depth: u32) -> Result<PdfDict, PdfError> {
546    let mut dict = PdfDict::new();
547    loop {
548        // Tolerate garbage bytes between entries: a lexer error here just means
549        // next_token hit a byte that isn't a valid PDF token start (e.g. a
550        // stray backtick in a malformed dict like `/Encoding 30 0`R`). The
551        // lexer has already advanced past the bad byte, so we can retry.
552        let t = match lexer.next_token() {
553            Ok(t) => t,
554            Err(_) => continue,
555        };
556        match t {
557            Token::DictEnd | Token::Eof => break,
558            Token::Name(key) => {
559                // Parse the value. On a value-level parse error (e.g. a garbage
560                // byte inside the value slot), insert /Null and resync on the
561                // next token rather than discarding the whole dict. Keeping
562                // already-parsed entries is what lets the Times-Roman /BaseFont
563                // survive a later /Encoding parse failure.
564                match parse_object_at_depth(lexer, depth) {
565                    Ok(val) => {
566                        dict.insert(key, val);
567                    }
568                    Err(_) => {
569                        dict.insert(key, PdfObj::Null);
570                    }
571                }
572            }
573            _ => {
574                // Tolerate unexpected tokens in dict (skip and continue)
575                continue;
576            }
577        }
578    }
579    Ok(dict)
580}
581
582/// PDF whitespace characters (PDF spec 7.2.2).
583fn is_whitespace(b: u8) -> bool {
584    matches!(b, b' ' | b'\t' | b'\r' | b'\n' | 0x0C | 0x00)
585}
586
587/// PDF delimiter characters.
588fn is_delimiter(b: u8) -> bool {
589    matches!(
590        b,
591        b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
592    )
593}
594
595/// Convert a hex digit to its value (0-15).
596fn hex_digit(b: u8) -> Option<u8> {
597    match b {
598        b'0'..=b'9' => Some(b - b'0'),
599        b'a'..=b'f' => Some(b - b'a' + 10),
600        b'A'..=b'F' => Some(b - b'A' + 10),
601        _ => None,
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608
609    fn tokenize(input: &[u8]) -> Vec<Token> {
610        let mut lexer = Lexer::new(input);
611        let mut tokens = Vec::new();
612        loop {
613            let tok = lexer.next_token().unwrap();
614            if tok == Token::Eof {
615                break;
616            }
617            tokens.push(tok);
618        }
619        tokens
620    }
621
622    #[test]
623    fn integers() {
624        assert_eq!(tokenize(b"42"), vec![Token::Int(42)]);
625        assert_eq!(tokenize(b"-7"), vec![Token::Int(-7)]);
626        assert_eq!(tokenize(b"+5"), vec![Token::Int(5)]);
627        assert_eq!(tokenize(b"0"), vec![Token::Int(0)]);
628    }
629
630    #[test]
631    fn reals() {
632        assert_eq!(tokenize(b"2.5"), vec![Token::Real(2.5)]);
633        assert_eq!(tokenize(b".5"), vec![Token::Real(0.5)]);
634        assert_eq!(tokenize(b"-2.0"), vec![Token::Real(-2.0)]);
635    }
636
637    #[test]
638    fn names() {
639        assert_eq!(tokenize(b"/Type"), vec![Token::Name(b"Type".to_vec())]);
640        assert_eq!(tokenize(b"/"), vec![Token::Name(b"".to_vec())]); // empty name
641        assert_eq!(tokenize(b"/A#20B"), vec![Token::Name(b"A B".to_vec())]); // hex escape
642    }
643
644    #[test]
645    fn strings() {
646        assert_eq!(
647            tokenize(b"(hello)"),
648            vec![Token::LitString(b"hello".to_vec())]
649        );
650        assert_eq!(
651            tokenize(b"(nested (parens))"),
652            vec![Token::LitString(b"nested (parens)".to_vec())]
653        );
654        assert_eq!(
655            tokenize(b"(line\\nfeed)"),
656            vec![Token::LitString(b"line\nfeed".to_vec())]
657        );
658        assert_eq!(
659            tokenize(b"(octal\\101)"),
660            vec![Token::LitString(b"octalA".to_vec())]
661        );
662    }
663
664    #[test]
665    fn hex_strings() {
666        assert_eq!(
667            tokenize(b"<48656C6C6F>"),
668            vec![Token::HexString(b"Hello".to_vec())]
669        );
670        // Odd digits: trailing 0
671        assert_eq!(tokenize(b"<ABC>"), vec![Token::HexString(vec![0xAB, 0xC0])]);
672        // Whitespace inside
673        assert_eq!(
674            tokenize(b"<48 65 6C>"),
675            vec![Token::HexString(b"Hel".to_vec())]
676        );
677    }
678
679    #[test]
680    fn booleans_and_null() {
681        assert_eq!(tokenize(b"true"), vec![Token::Bool(true)]);
682        assert_eq!(tokenize(b"false"), vec![Token::Bool(false)]);
683        let obj = parse_object(&mut Lexer::new(b"null")).unwrap();
684        assert_eq!(obj, PdfObj::Null);
685    }
686
687    #[test]
688    fn delimiters() {
689        let toks = tokenize(b"<< >> [ ]");
690        assert_eq!(
691            toks,
692            vec![
693                Token::DictBegin,
694                Token::DictEnd,
695                Token::ArrayBegin,
696                Token::ArrayEnd,
697            ]
698        );
699    }
700
701    #[test]
702    fn comments_skipped() {
703        assert_eq!(tokenize(b"% comment\n42"), vec![Token::Int(42)]);
704    }
705
706    #[test]
707    fn keywords() {
708        assert_eq!(
709            tokenize(b"obj endobj stream"),
710            vec![
711                Token::Keyword(b"obj".to_vec()),
712                Token::Keyword(b"endobj".to_vec()),
713                Token::Keyword(b"stream".to_vec()),
714            ]
715        );
716    }
717
718    #[test]
719    fn parse_array() {
720        let obj = parse_object(&mut Lexer::new(b"[1 2 /Name]")).unwrap();
721        assert_eq!(
722            obj,
723            PdfObj::Array(vec![
724                PdfObj::Int(1),
725                PdfObj::Int(2),
726                PdfObj::Name(b"Name".to_vec()),
727            ])
728        );
729    }
730
731    #[test]
732    fn parse_dict() {
733        let obj = parse_object(&mut Lexer::new(b"<< /Type /Page /Count 5 >>")).unwrap();
734        let dict = obj.as_dict().unwrap();
735        assert_eq!(dict.get_name(b"Type"), Some(b"Page".as_slice()));
736        assert_eq!(dict.get_int(b"Count"), Some(5));
737    }
738
739    #[test]
740    fn parse_indirect_ref() {
741        let obj = parse_object(&mut Lexer::new(b"10 0 R")).unwrap();
742        assert_eq!(obj, PdfObj::Ref(10, 0));
743    }
744
745    #[test]
746    fn parse_nested_dict() {
747        let obj = parse_object(&mut Lexer::new(
748            b"<< /Resources << /Font << /F1 5 0 R >> >> >>",
749        ))
750        .unwrap();
751        let dict = obj.as_dict().unwrap();
752        let res = dict.get_dict(b"Resources").unwrap();
753        let font = res.get_dict(b"Font").unwrap();
754        assert_eq!(font.get(b"F1"), Some(&PdfObj::Ref(5, 0)));
755    }
756
757    #[test]
758    fn int_not_ref_at_eof() {
759        // A lone integer should not be confused with a ref
760        let obj = parse_object(&mut Lexer::new(b"42")).unwrap();
761        assert_eq!(obj, PdfObj::Int(42));
762    }
763}