Skip to main content

oxideav_pdf/reader/
lex.rs

1//! PDF tokenizer (ISO 32000-1 §7.2).
2//!
3//! Converts a byte slice into a stream of [`Token`]s. The lexer is
4//! position-preserving — every token carries its `start` byte offset
5//! into the input, so the higher-level object parser can:
6//!
7//! - decode a `stream` payload by jumping straight to the byte after
8//!   the `stream` keyword's EOL (avoids re-scanning the body), and
9//! - report errors with byte-level positions for debug tooling.
10//!
11//! The lexer never copies bytes; [`TokenKind::Name`],
12//! [`TokenKind::LiteralString`], [`TokenKind::HexString`], and
13//! [`TokenKind::Keyword`] all borrow from the input slice. The
14//! object parser is in charge of escape decoding for literal strings
15//! (the bytes between `(` and `)` — the lexer only matches the
16//! delimiters and leaves the inner payload untouched).
17//!
18//! Round 3 only handles the surface our writer emits: numbers, names,
19//! literal strings, hex strings, names, booleans, null, the standard
20//! PDF keywords (`obj`, `endobj`, `stream`, `endstream`, `R`, `xref`,
21//! `trailer`, `startxref`, `f`, `n`, `true`, `false`, `null`),
22//! brackets `[`/`]` and dict markers `<<`/`>>`.
23
24use crate::error::PdfError;
25
26/// One token at a byte position.
27#[derive(Debug, Clone, PartialEq)]
28pub struct Token<'a> {
29    pub start: usize,
30    pub end: usize,
31    pub kind: TokenKind<'a>,
32}
33
34/// Token payload. References into the input slice are zero-copy.
35#[derive(Debug, Clone, PartialEq)]
36pub enum TokenKind<'a> {
37    /// Integer (no `.`, no `e`/`E`). The bytes are the original
38    /// digits + optional sign.
39    Integer(i64),
40    /// Real number (has `.`). Stored as the original string so the
41    /// parser can re-format with the same precision policy.
42    Real(f64),
43    /// Name `/foo` — the bytes (without the leading `/`), already
44    /// `#xx`-decoded.
45    Name(Vec<u8>),
46    /// Literal string `(...)`. Inner bytes only, with PDF escape
47    /// sequences (`\n`, `\r`, `\t`, `\\`, `\(`, `\)`, octal `\nnn`,
48    /// line-continuation `\<EOL>`) already decoded. Balanced parens
49    /// inside a literal string are preserved verbatim per §7.3.4.2.
50    LiteralString(Vec<u8>),
51    /// Hex string `<...>`. Bytes are the **decoded** payload (every
52    /// pair of hex digits produces one byte; an odd trailing nibble
53    /// is left-aligned per §7.3.4.3).
54    HexString(Vec<u8>),
55    /// Bare ASCII identifier — keywords like `obj`, `endobj`, `R`,
56    /// `true`, `false`, `null`, `xref`, `trailer`, `startxref`,
57    /// `stream`, `endstream`, plus content-stream operators (`m`,
58    /// `l`, `c`, `cm`, `q`, `Q`, `f`, `f*`, `S`, `B`, `B*`, `n`,
59    /// `re`, `RG`, `rg`, `w`, etc.). The parser dispatches on the
60    /// keyword text.
61    Keyword(&'a [u8]),
62    /// `[` — array start.
63    ArrayStart,
64    /// `]` — array end.
65    ArrayEnd,
66    /// `<<` — dictionary start.
67    DictStart,
68    /// `>>` — dictionary end.
69    DictEnd,
70}
71
72/// Streaming tokenizer over a byte slice. Holds a cursor into the
73/// input; advance with [`Lexer::next_token`].
74pub struct Lexer<'a> {
75    input: &'a [u8],
76    pos: usize,
77}
78
79impl<'a> Lexer<'a> {
80    pub fn new(input: &'a [u8]) -> Self {
81        Self { input, pos: 0 }
82    }
83
84    /// Current byte offset.
85    pub fn position(&self) -> usize {
86        self.pos
87    }
88
89    /// Move the cursor to an absolute byte offset. Used by the object
90    /// parser when it needs to re-anchor (e.g. after consuming the
91    /// `stream` keyword + its EOL marker, the parser jumps the cursor
92    /// past the binary payload directly).
93    pub fn seek(&mut self, pos: usize) {
94        self.pos = pos.min(self.input.len());
95    }
96
97    /// Peek at the byte at `offset` from the current position. Returns
98    /// `None` past EOF.
99    pub fn peek_byte(&self, offset: usize) -> Option<u8> {
100        self.input.get(self.pos + offset).copied()
101    }
102
103    /// Borrow a slice of the input from `start` to `end` (clamped to
104    /// the input length). Used by the object parser for stream-body
105    /// extraction.
106    pub fn slice(&self, start: usize, end: usize) -> &'a [u8] {
107        let s = start.min(self.input.len());
108        let e = end.min(self.input.len()).max(s);
109        &self.input[s..e]
110    }
111
112    /// Whole input. Useful for the trailer / startxref scanner that
113    /// works backward from EOF.
114    pub fn input(&self) -> &'a [u8] {
115        self.input
116    }
117
118    /// Read the next token. Returns `Ok(None)` at EOF.
119    pub fn next_token(&mut self) -> Result<Option<Token<'a>>, PdfError> {
120        self.skip_whitespace_and_comments();
121        if self.pos >= self.input.len() {
122            return Ok(None);
123        }
124        let start = self.pos;
125        let b = self.input[self.pos];
126        let tok = match b {
127            b'[' => {
128                self.pos += 1;
129                Token {
130                    start,
131                    end: self.pos,
132                    kind: TokenKind::ArrayStart,
133                }
134            }
135            b']' => {
136                self.pos += 1;
137                Token {
138                    start,
139                    end: self.pos,
140                    kind: TokenKind::ArrayEnd,
141                }
142            }
143            b'<' => {
144                if self.peek_byte(1) == Some(b'<') {
145                    self.pos += 2;
146                    Token {
147                        start,
148                        end: self.pos,
149                        kind: TokenKind::DictStart,
150                    }
151                } else {
152                    self.read_hex_string(start)?
153                }
154            }
155            b'>' => {
156                if self.peek_byte(1) == Some(b'>') {
157                    self.pos += 2;
158                    Token {
159                        start,
160                        end: self.pos,
161                        kind: TokenKind::DictEnd,
162                    }
163                } else {
164                    return Err(PdfError::other(format!(
165                        "PDF lexer: unexpected `>` at byte {start} (need `>>` for dict end)"
166                    )));
167                }
168            }
169            b'(' => self.read_literal_string(start)?,
170            b'/' => self.read_name(start)?,
171            b'+' | b'-' | b'.' | b'0'..=b'9' => self.read_number(start)?,
172            _ => self.read_keyword(start)?,
173        };
174        Ok(Some(tok))
175    }
176
177    /// Drop whitespace + `%`-line comments. The PDF spec treats both
178    /// as "whitespace separating tokens" (§7.2.3 / §7.2.4).
179    fn skip_whitespace_and_comments(&mut self) {
180        while self.pos < self.input.len() {
181            let b = self.input[self.pos];
182            if is_whitespace(b) {
183                self.pos += 1;
184            } else if b == b'%' {
185                while self.pos < self.input.len()
186                    && self.input[self.pos] != b'\n'
187                    && self.input[self.pos] != b'\r'
188                {
189                    self.pos += 1;
190                }
191            } else {
192                break;
193            }
194        }
195    }
196
197    fn read_number(&mut self, start: usize) -> Result<Token<'a>, PdfError> {
198        let mut end = start;
199        // Optional sign.
200        if self.input.get(end).is_some_and(|&b| b == b'+' || b == b'-') {
201            end += 1;
202        }
203        let mut saw_dot = false;
204        let mut saw_digit = false;
205        while end < self.input.len() {
206            let b = self.input[end];
207            if b.is_ascii_digit() {
208                saw_digit = true;
209                end += 1;
210            } else if b == b'.' && !saw_dot {
211                saw_dot = true;
212                end += 1;
213            } else {
214                break;
215            }
216        }
217        if !saw_digit {
218            // A bare sign or dot wasn't a number — fall back to the
219            // keyword path so e.g. ".n" or "+e" surface as keywords
220            // rather than a phantom 0.
221            return self.read_keyword(start);
222        }
223        self.pos = end;
224        let text = std::str::from_utf8(&self.input[start..end])
225            .map_err(|_| PdfError::other(format!("PDF lexer: non-UTF-8 number at byte {start}")))?;
226        let kind = if saw_dot {
227            let f = text.parse::<f64>().map_err(|_| {
228                PdfError::other(format!("PDF lexer: invalid real `{text}` at byte {start}"))
229            })?;
230            TokenKind::Real(f)
231        } else {
232            let n = text.parse::<i64>().map_err(|_| {
233                PdfError::other(format!(
234                    "PDF lexer: invalid integer `{text}` at byte {start}"
235                ))
236            })?;
237            TokenKind::Integer(n)
238        };
239        Ok(Token { start, end, kind })
240    }
241
242    fn read_name(&mut self, start: usize) -> Result<Token<'a>, PdfError> {
243        debug_assert_eq!(self.input[start], b'/');
244        let mut end = start + 1;
245        let mut decoded = Vec::with_capacity(16);
246        while end < self.input.len() {
247            let b = self.input[end];
248            if is_delimiter(b) || is_whitespace(b) {
249                break;
250            }
251            if b == b'#' {
252                // Two-hex-digit escape per §7.3.5. Anything malformed
253                // surfaces as an error rather than silently producing
254                // a `#` byte — it'd corrupt downstream key lookups.
255                let h1 = self.input.get(end + 1).copied().ok_or_else(|| {
256                    PdfError::other(format!("PDF lexer: truncated #xx escape at byte {end}"))
257                })?;
258                let h2 = self.input.get(end + 2).copied().ok_or_else(|| {
259                    PdfError::other(format!("PDF lexer: truncated #xx escape at byte {end}"))
260                })?;
261                let hi = hex_digit(h1).ok_or_else(|| {
262                    PdfError::other(format!(
263                        "PDF lexer: bad hex digit `{h1}` in name #xx at byte {end}"
264                    ))
265                })?;
266                let lo = hex_digit(h2).ok_or_else(|| {
267                    PdfError::other(format!(
268                        "PDF lexer: bad hex digit `{h2}` in name #xx at byte {end}"
269                    ))
270                })?;
271                decoded.push((hi << 4) | lo);
272                end += 3;
273            } else {
274                decoded.push(b);
275                end += 1;
276            }
277        }
278        self.pos = end;
279        Ok(Token {
280            start,
281            end,
282            kind: TokenKind::Name(decoded),
283        })
284    }
285
286    fn read_literal_string(&mut self, start: usize) -> Result<Token<'a>, PdfError> {
287        debug_assert_eq!(self.input[start], b'(');
288        let mut end = start + 1;
289        let mut depth = 1u32;
290        let mut decoded = Vec::with_capacity(32);
291        while end < self.input.len() {
292            let b = self.input[end];
293            if b == b'\\' {
294                end += 1;
295                if end >= self.input.len() {
296                    break;
297                }
298                let esc = self.input[end];
299                match esc {
300                    b'n' => {
301                        decoded.push(b'\n');
302                        end += 1;
303                    }
304                    b'r' => {
305                        decoded.push(b'\r');
306                        end += 1;
307                    }
308                    b't' => {
309                        decoded.push(b'\t');
310                        end += 1;
311                    }
312                    b'b' => {
313                        decoded.push(0x08);
314                        end += 1;
315                    }
316                    b'f' => {
317                        decoded.push(0x0C);
318                        end += 1;
319                    }
320                    b'\\' => {
321                        decoded.push(b'\\');
322                        end += 1;
323                    }
324                    b'(' => {
325                        decoded.push(b'(');
326                        end += 1;
327                    }
328                    b')' => {
329                        decoded.push(b')');
330                        end += 1;
331                    }
332                    b'\n' => {
333                        // Line continuation — drop the LF.
334                        end += 1;
335                    }
336                    b'\r' => {
337                        end += 1;
338                        if end < self.input.len() && self.input[end] == b'\n' {
339                            end += 1;
340                        }
341                    }
342                    b'0'..=b'7' => {
343                        // Up to 3 octal digits.
344                        let mut v = (esc - b'0') as u16;
345                        end += 1;
346                        for _ in 0..2 {
347                            if end < self.input.len() && (b'0'..=b'7').contains(&self.input[end]) {
348                                v = v * 8 + (self.input[end] - b'0') as u16;
349                                end += 1;
350                            } else {
351                                break;
352                            }
353                        }
354                        decoded.push((v & 0xFF) as u8);
355                    }
356                    other => {
357                        // Unknown escape — per §7.3.4.2, the `\` is
358                        // ignored and the next character passes
359                        // through verbatim.
360                        decoded.push(other);
361                        end += 1;
362                    }
363                }
364                continue;
365            }
366            if b == b'(' {
367                depth += 1;
368                decoded.push(b'(');
369                end += 1;
370                continue;
371            }
372            if b == b')' {
373                depth -= 1;
374                if depth == 0 {
375                    end += 1;
376                    self.pos = end;
377                    return Ok(Token {
378                        start,
379                        end,
380                        kind: TokenKind::LiteralString(decoded),
381                    });
382                }
383                decoded.push(b')');
384                end += 1;
385                continue;
386            }
387            // Per §7.3.4.2, a literal string normalises CR / CRLF /
388            // LF to a single LF inside the payload.
389            if b == b'\r' {
390                decoded.push(b'\n');
391                end += 1;
392                if end < self.input.len() && self.input[end] == b'\n' {
393                    end += 1;
394                }
395                continue;
396            }
397            decoded.push(b);
398            end += 1;
399        }
400        Err(PdfError::other(format!(
401            "PDF lexer: unterminated literal string starting at byte {start}"
402        )))
403    }
404
405    fn read_hex_string(&mut self, start: usize) -> Result<Token<'a>, PdfError> {
406        debug_assert_eq!(self.input[start], b'<');
407        let mut end = start + 1;
408        let mut nibble: Option<u8> = None;
409        let mut decoded = Vec::with_capacity(16);
410        while end < self.input.len() {
411            let b = self.input[end];
412            if b == b'>' {
413                if let Some(hi) = nibble {
414                    // Odd trailing nibble — left-align per §7.3.4.3.
415                    decoded.push(hi << 4);
416                }
417                end += 1;
418                self.pos = end;
419                return Ok(Token {
420                    start,
421                    end,
422                    kind: TokenKind::HexString(decoded),
423                });
424            }
425            if is_whitespace(b) {
426                end += 1;
427                continue;
428            }
429            let h = hex_digit(b).ok_or_else(|| {
430                PdfError::other(format!(
431                    "PDF lexer: bad hex digit `{b}` in hex string at byte {end}"
432                ))
433            })?;
434            match nibble {
435                None => nibble = Some(h),
436                Some(hi) => {
437                    decoded.push((hi << 4) | h);
438                    nibble = None;
439                }
440            }
441            end += 1;
442        }
443        Err(PdfError::other(format!(
444            "PDF lexer: unterminated hex string starting at byte {start}"
445        )))
446    }
447
448    fn read_keyword(&mut self, start: usize) -> Result<Token<'a>, PdfError> {
449        let mut end = start;
450        while end < self.input.len() {
451            let b = self.input[end];
452            if is_whitespace(b) || is_delimiter(b) {
453                break;
454            }
455            end += 1;
456        }
457        if end == start {
458            // Not a real keyword — single non-token byte. Skip and
459            // recurse so the lexer doesn't loop forever.
460            self.pos += 1;
461            return Err(PdfError::other(format!(
462                "PDF lexer: unrecognised byte `{}` at byte {start}",
463                self.input[start]
464            )));
465        }
466        self.pos = end;
467        Ok(Token {
468            start,
469            end,
470            kind: TokenKind::Keyword(&self.input[start..end]),
471        })
472    }
473}
474
475fn is_whitespace(b: u8) -> bool {
476    // §7.2.3 — NUL, HT, LF, FF, CR, SP.
477    matches!(b, 0x00 | b'\t' | b'\n' | 0x0C | b'\r' | b' ')
478}
479
480fn is_delimiter(b: u8) -> bool {
481    // §7.2.3 — `( ) < > [ ] { } / %`.
482    matches!(
483        b,
484        b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
485    )
486}
487
488fn hex_digit(b: u8) -> Option<u8> {
489    match b {
490        b'0'..=b'9' => Some(b - b'0'),
491        b'a'..=b'f' => Some(b - b'a' + 10),
492        b'A'..=b'F' => Some(b - b'A' + 10),
493        _ => None,
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500
501    fn tokenize(input: &[u8]) -> Vec<TokenKind<'_>> {
502        let mut lex = Lexer::new(input);
503        let mut out = Vec::new();
504        while let Some(t) = lex.next_token().unwrap() {
505            out.push(t.kind);
506        }
507        out
508    }
509
510    #[test]
511    fn integers_and_reals() {
512        assert_eq!(
513            tokenize(b"42 -7 0 +3"),
514            vec![
515                TokenKind::Integer(42),
516                TokenKind::Integer(-7),
517                TokenKind::Integer(0),
518                TokenKind::Integer(3),
519            ]
520        );
521        let toks = tokenize(b"0.5 -1.25 .75");
522        assert_eq!(toks.len(), 3);
523        match toks[0] {
524            TokenKind::Real(f) => assert!((f - 0.5).abs() < 1e-9),
525            _ => panic!("expected real"),
526        }
527        match toks[1] {
528            TokenKind::Real(f) => assert!((f + 1.25).abs() < 1e-9),
529            _ => panic!("expected real"),
530        }
531        match toks[2] {
532            TokenKind::Real(f) => assert!((f - 0.75).abs() < 1e-9),
533            _ => panic!("expected real"),
534        }
535    }
536
537    #[test]
538    fn names_decode_hex_escapes() {
539        let toks = tokenize(b"/Pages /a#20b /dc#3Arights");
540        assert_eq!(toks.len(), 3);
541        assert_eq!(toks[0], TokenKind::Name(b"Pages".to_vec()));
542        assert_eq!(toks[1], TokenKind::Name(b"a b".to_vec()));
543        assert_eq!(toks[2], TokenKind::Name(b"dc:rights".to_vec()));
544    }
545
546    #[test]
547    fn literal_string_with_escapes() {
548        let toks = tokenize(b"(hello\\nworld) (\\(c\\))");
549        assert_eq!(toks.len(), 2);
550        assert_eq!(toks[0], TokenKind::LiteralString(b"hello\nworld".to_vec()));
551        assert_eq!(toks[1], TokenKind::LiteralString(b"(c)".to_vec()));
552    }
553
554    #[test]
555    fn literal_string_balanced_parens() {
556        // Per §7.3.4.2, balanced parens inside a literal string need
557        // no escaping — the lexer has to track depth.
558        let toks = tokenize(b"(a (b (c) d) e)");
559        assert_eq!(toks.len(), 1);
560        assert_eq!(toks[0], TokenKind::LiteralString(b"a (b (c) d) e".to_vec()));
561    }
562
563    #[test]
564    fn literal_string_octal_escapes() {
565        // \101 = 'A' (decimal 65)
566        let toks = tokenize(b"(\\101BC)");
567        assert_eq!(toks, vec![TokenKind::LiteralString(b"ABC".to_vec())]);
568    }
569
570    #[test]
571    fn hex_string_decodes_pairs() {
572        let toks = tokenize(b"<48656C6C6F>");
573        assert_eq!(toks, vec![TokenKind::HexString(b"Hello".to_vec())]);
574    }
575
576    #[test]
577    fn hex_string_odd_nibble_left_aligned() {
578        // Per §7.3.4.3, an odd trailing nibble is left-aligned: <F> = 0xF0.
579        let toks = tokenize(b"<F>");
580        assert_eq!(toks, vec![TokenKind::HexString(vec![0xF0])]);
581    }
582
583    #[test]
584    fn hex_string_with_whitespace_between_digits() {
585        let toks = tokenize(b"<48 65 6C 6C 6F>");
586        assert_eq!(toks, vec![TokenKind::HexString(b"Hello".to_vec())]);
587    }
588
589    #[test]
590    fn dict_and_array_markers() {
591        let toks = tokenize(b"<< /Type /Page >> [1 2 3]");
592        assert_eq!(toks.len(), 9);
593        assert_eq!(toks[0], TokenKind::DictStart);
594        assert_eq!(toks[1], TokenKind::Name(b"Type".to_vec()));
595        assert_eq!(toks[2], TokenKind::Name(b"Page".to_vec()));
596        assert_eq!(toks[3], TokenKind::DictEnd);
597        assert_eq!(toks[4], TokenKind::ArrayStart);
598        assert_eq!(toks[5], TokenKind::Integer(1));
599        assert_eq!(toks[6], TokenKind::Integer(2));
600        assert_eq!(toks[7], TokenKind::Integer(3));
601        assert_eq!(toks[8], TokenKind::ArrayEnd);
602    }
603
604    #[test]
605    fn keywords_pass_through() {
606        let toks =
607            tokenize(b"obj endobj stream endstream R xref trailer startxref true false null");
608        assert_eq!(toks.len(), 11);
609        assert_eq!(toks[0], TokenKind::Keyword(b"obj"));
610        assert_eq!(toks[1], TokenKind::Keyword(b"endobj"));
611        assert_eq!(toks[2], TokenKind::Keyword(b"stream"));
612        assert_eq!(toks[3], TokenKind::Keyword(b"endstream"));
613        assert_eq!(toks[4], TokenKind::Keyword(b"R"));
614        assert_eq!(toks[5], TokenKind::Keyword(b"xref"));
615        assert_eq!(toks[6], TokenKind::Keyword(b"trailer"));
616        assert_eq!(toks[7], TokenKind::Keyword(b"startxref"));
617        assert_eq!(toks[8], TokenKind::Keyword(b"true"));
618        assert_eq!(toks[9], TokenKind::Keyword(b"false"));
619        assert_eq!(toks[10], TokenKind::Keyword(b"null"));
620    }
621
622    #[test]
623    fn comments_are_skipped_like_whitespace() {
624        let toks = tokenize(b"% header comment\n42 % trailing\n/Foo");
625        assert_eq!(
626            toks,
627            vec![TokenKind::Integer(42), TokenKind::Name(b"Foo".to_vec())]
628        );
629    }
630
631    #[test]
632    fn position_advances_after_each_token() {
633        let mut lex = Lexer::new(b"42 /foo");
634        let t1 = lex.next_token().unwrap().unwrap();
635        assert_eq!(t1.start, 0);
636        assert_eq!(t1.end, 2);
637        let t2 = lex.next_token().unwrap().unwrap();
638        assert_eq!(t2.start, 3);
639        assert_eq!(t2.end, 7);
640        assert!(lex.next_token().unwrap().is_none());
641    }
642
643    #[test]
644    fn seek_and_slice_helpers() {
645        let mut lex = Lexer::new(b"obj\nstream\nABCDEF\nendstream\nendobj\n");
646        // Skip past `obj`.
647        let _ = lex.next_token().unwrap();
648        // Skip past `stream`.
649        let stream_tok = lex.next_token().unwrap().unwrap();
650        assert!(matches!(stream_tok.kind, TokenKind::Keyword(b"stream")));
651        // Per §7.3.8.1, the data starts at the byte after the EOL
652        // marker (LF or CRLF). Seek there manually + slice.
653        let data_start = stream_tok.end + 1; // skip the LF after `stream`
654        let data_end = data_start + 6; // "ABCDEF"
655        assert_eq!(lex.slice(data_start, data_end), b"ABCDEF");
656        lex.seek(data_end);
657        // Next non-whitespace token is `endstream`.
658        let t = lex.next_token().unwrap().unwrap();
659        assert_eq!(t.kind, TokenKind::Keyword(b"endstream"));
660    }
661
662    #[test]
663    fn unterminated_literal_string_is_error() {
664        let mut lex = Lexer::new(b"(hello");
665        assert!(lex.next_token().is_err());
666    }
667
668    #[test]
669    fn unterminated_hex_string_is_error() {
670        let mut lex = Lexer::new(b"<48656C");
671        assert!(lex.next_token().is_err());
672    }
673}