Skip to main content

pdfboss_core/
lexer.rs

1//! Byte-level tokenizer for PDF syntax (ISO 32000 §7.2/§7.3), zero-copy
2//! where possible.
3//!
4//! Rules: whitespace is NUL/HT/LF/FF/CR/SP; delimiters are `( ) < > [ ] { }
5//! / %`; comments run from `%` to end of line; numbers may lead with `+ - .`
6//! (lenient); names decode `#xx` (bad hex kept literal); literal strings
7//! balance nested unescaped parentheses and support the standard escapes,
8//! 1-3 digit octal, backslash-EOL continuation, and raw EOL normalized to
9//! `\n`; hex strings ignore whitespace and pad an odd digit count with `0`;
10//! every other regular-character run is a [`Token::Keyword`].
11
12use crate::error::Result;
13use crate::object::Name;
14
15/// A single token produced by the [`Lexer`].
16#[derive(Debug, Clone, PartialEq)]
17pub enum Token {
18    Int(i64),
19    Real(f64),
20    Name(Name),
21    /// Literal string `(...)`, escapes already processed.
22    LitString(Vec<u8>),
23    /// Hex string `<...>`, decoded to bytes.
24    HexString(Vec<u8>),
25    ArrayOpen,
26    ArrayClose,
27    DictOpen,
28    DictClose,
29    /// Any bare regular-character run, e.g. `obj`, `endobj`, `stream`,
30    /// `endstream`, `R`, `xref`, `trailer`, `startxref`, `true`, `false`,
31    /// `null`, `n`, `f`.
32    Keyword(Vec<u8>),
33    Eof,
34}
35
36/// Whether `b` is PDF whitespace (ISO 32000 §7.2.2, Table 1).
37pub(crate) fn is_whitespace(b: u8) -> bool {
38    matches!(b, b'\0' | b'\t' | b'\n' | b'\x0C' | b'\r' | b' ')
39}
40
41/// Whether `b` is a PDF delimiter character (ISO 32000 §7.2.2, Table 2).
42pub(crate) fn is_delimiter(b: u8) -> bool {
43    matches!(
44        b,
45        b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
46    )
47}
48
49/// Whether `b` is a regular character (neither whitespace nor delimiter).
50pub(crate) fn is_regular(b: u8) -> bool {
51    !is_whitespace(b) && !is_delimiter(b)
52}
53
54/// Value of an ASCII hex digit, if `b` is one.
55fn hex_val(b: u8) -> Option<u8> {
56    match b {
57        b'0'..=b'9' => Some(b - b'0'),
58        b'a'..=b'f' => Some(b - b'a' + 10),
59        b'A'..=b'F' => Some(b - b'A' + 10),
60        _ => None,
61    }
62}
63
64/// Tokenizer over a byte slice.
65pub struct Lexer<'a> {
66    data: &'a [u8],
67    pos: usize,
68}
69
70impl<'a> Lexer<'a> {
71    /// Creates a lexer at the start of `data`.
72    pub fn new(data: &'a [u8]) -> Self {
73        Lexer { data, pos: 0 }
74    }
75
76    /// Creates a lexer positioned at byte offset `pos`.
77    pub fn at(data: &'a [u8], pos: usize) -> Self {
78        Lexer { data, pos }
79    }
80
81    /// Current byte offset.
82    pub fn pos(&self) -> usize {
83        self.pos
84    }
85
86    /// Moves the cursor to byte offset `pos`.
87    pub fn seek(&mut self, pos: usize) {
88        self.pos = pos;
89    }
90
91    /// Consumes and returns the next token.
92    pub fn next_token(&mut self) -> Result<Token> {
93        self.skip_whitespace_and_comments();
94        let Some(&b) = self.data.get(self.pos) else {
95            return Ok(Token::Eof);
96        };
97        match b {
98            b'[' => {
99                self.pos += 1;
100                Ok(Token::ArrayOpen)
101            }
102            b']' => {
103                self.pos += 1;
104                Ok(Token::ArrayClose)
105            }
106            b'<' => {
107                if self.data.get(self.pos + 1) == Some(&b'<') {
108                    self.pos += 2;
109                    Ok(Token::DictOpen)
110                } else {
111                    self.pos += 1;
112                    Ok(self.lex_hex_string())
113                }
114            }
115            b'>' => {
116                if self.data.get(self.pos + 1) == Some(&b'>') {
117                    self.pos += 2;
118                    Ok(Token::DictClose)
119                } else {
120                    // Stray `>`: surfaced leniently as a one-byte keyword.
121                    self.pos += 1;
122                    Ok(Token::Keyword(vec![b'>']))
123                }
124            }
125            b'(' => {
126                self.pos += 1;
127                Ok(self.lex_literal_string())
128            }
129            b'/' => {
130                self.pos += 1;
131                Ok(self.lex_name())
132            }
133            // Stray delimiters with no token of their own: kept lenient.
134            b')' | b'{' | b'}' => {
135                self.pos += 1;
136                Ok(Token::Keyword(vec![b]))
137            }
138            b'0'..=b'9' | b'+' | b'-' | b'.' => Ok(self.lex_number_or_keyword()),
139            _ => Ok(self.lex_keyword()),
140        }
141    }
142
143    /// Returns the next token without consuming it.
144    pub fn peek_token(&mut self) -> Result<Token> {
145        let save = self.pos;
146        let token = self.next_token();
147        self.pos = save;
148        token
149    }
150
151    /// Advances past whitespace and `%` comments.
152    pub fn skip_whitespace_and_comments(&mut self) {
153        loop {
154            while self.data.get(self.pos).is_some_and(|&b| is_whitespace(b)) {
155                self.pos += 1;
156            }
157            if self.data.get(self.pos) == Some(&b'%') {
158                while self
159                    .data
160                    .get(self.pos)
161                    .is_some_and(|&b| b != b'\r' && b != b'\n')
162                {
163                    self.pos += 1;
164                }
165            } else {
166                return;
167            }
168        }
169    }
170
171    /// The underlying input.
172    pub fn data(&self) -> &'a [u8] {
173        self.data
174    }
175
176    /// Consumes the run of regular characters starting at the cursor.
177    fn take_regular_run(&mut self) -> &'a [u8] {
178        let start = self.pos;
179        while self.data.get(self.pos).is_some_and(|&b| is_regular(b)) {
180            self.pos += 1;
181        }
182        &self.data[start..self.pos]
183    }
184
185    /// Lexes a run starting with a digit, sign, or period: a number when the
186    /// run contains only numeric characters, otherwise a keyword (lenient).
187    fn lex_number_or_keyword(&mut self) -> Token {
188        let run = self.take_regular_run();
189        if !run
190            .iter()
191            .all(|&b| matches!(b, b'0'..=b'9' | b'+' | b'-' | b'.'))
192        {
193            return Token::Keyword(run.to_vec());
194        }
195        // Fast path for well-formed numbers (the overwhelming majority):
196        // parse directly off the borrowed slice with no intermediate String.
197        // Integers with no `.` go to `i64`; anything else (including overflow)
198        // to `f64`. Malformed runs (multiple signs/dots, bare sign) fall
199        // through to the lenient cleaner below, preserving its exact result.
200        if let Ok(s) = std::str::from_utf8(run) {
201            if !run.contains(&b'.') {
202                if let Ok(value) = s.parse::<i64>() {
203                    return Token::Int(value);
204                }
205            }
206            if let Ok(value) = s.parse::<f64>() {
207                return Token::Real(value);
208            }
209        }
210        // Lenient numeric parse: honor the first sign, then keep digits and
211        // the first period; any further signs or periods are ignored.
212        let mut bytes = run.iter().copied();
213        let negative = match run.first() {
214            Some(b'-') => {
215                bytes.next();
216                true
217            }
218            Some(b'+') => {
219                bytes.next();
220                false
221            }
222            _ => false,
223        };
224        let mut digits = String::new();
225        let mut seen_dot = false;
226        for b in bytes {
227            match b {
228                b'0'..=b'9' => digits.push(char::from(b)),
229                b'.' if !seen_dot => {
230                    seen_dot = true;
231                    digits.push('.');
232                }
233                _ => {}
234            }
235        }
236        if seen_dot {
237            let value = if digits == "." {
238                0.0
239            } else {
240                digits.parse::<f64>().unwrap_or(0.0)
241            };
242            Token::Real(if negative { -value } else { value })
243        } else if digits.is_empty() {
244            // A bare sign; degrade to zero rather than erroring.
245            Token::Int(0)
246        } else if let Ok(value) = digits.parse::<i64>() {
247            Token::Int(if negative { -value } else { value })
248        } else {
249            // Magnitude exceeds i64: degrade to a real.
250            let value = digits.parse::<f64>().unwrap_or(0.0);
251            Token::Real(if negative { -value } else { value })
252        }
253    }
254
255    /// Lexes a keyword: any other run of regular characters.
256    fn lex_keyword(&mut self) -> Token {
257        Token::Keyword(self.take_regular_run().to_vec())
258    }
259
260    /// Lexes a name after the leading `/`, decoding `#xx` escapes. A `#`
261    /// not followed by two hex digits is kept literally.
262    fn lex_name(&mut self) -> Token {
263        let start = self.pos;
264        while let Some(&b) = self.data.get(self.pos) {
265            if !is_regular(b) {
266                break;
267            }
268            self.pos += 1;
269        }
270        let run = &self.data[start..self.pos];
271        // Fast path: no `#` escapes, so the name bytes are exactly the run —
272        // convert the borrowed slice directly without a per-byte copy.
273        if !run.contains(&b'#') {
274            return Token::Name(Name(String::from_utf8_lossy(run).into_owned()));
275        }
276        // Escape path: decode `#xx`; a `#` not followed by two hex digits (or
277        // at the very end of the run) is kept literally. Hex digits are regular
278        // name characters, so any real `#xx` pair lies wholly within the run.
279        let mut out = Vec::with_capacity(run.len());
280        let mut i = 0;
281        while i < run.len() {
282            let b = run[i];
283            if b == b'#' && i + 2 < run.len() {
284                if let (Some(hi), Some(lo)) = (hex_val(run[i + 1]), hex_val(run[i + 2])) {
285                    out.push((hi << 4) | lo);
286                    i += 3;
287                    continue;
288                }
289            }
290            out.push(b);
291            i += 1;
292        }
293        Token::Name(Name(String::from_utf8_lossy(&out).into_owned()))
294    }
295
296    /// Lexes a literal string after the opening `(`: balanced unescaped
297    /// parentheses, all standard escapes, 1-3 digit octal, backslash-EOL
298    /// line continuation, and raw EOL normalized to `\n`. An unterminated
299    /// string yields whatever was accumulated (lenient).
300    fn lex_literal_string(&mut self) -> Token {
301        let mut out = Vec::new();
302        let mut depth = 1usize;
303        while let Some(&b) = self.data.get(self.pos) {
304            self.pos += 1;
305            match b {
306                b'\\' => {
307                    let Some(&esc) = self.data.get(self.pos) else {
308                        break; // trailing backslash at end of input
309                    };
310                    self.pos += 1;
311                    match esc {
312                        b'n' => out.push(b'\n'),
313                        b'r' => out.push(b'\r'),
314                        b't' => out.push(b'\t'),
315                        b'b' => out.push(0x08),
316                        b'f' => out.push(0x0C),
317                        b'(' => out.push(b'('),
318                        b')' => out.push(b')'),
319                        b'\\' => out.push(b'\\'),
320                        b'0'..=b'7' => {
321                            let mut value = u32::from(esc - b'0');
322                            for _ in 0..2 {
323                                match self.data.get(self.pos) {
324                                    Some(&d @ b'0'..=b'7') => {
325                                        value = value * 8 + u32::from(d - b'0');
326                                        self.pos += 1;
327                                    }
328                                    _ => break,
329                                }
330                            }
331                            out.push((value & 0xFF) as u8);
332                        }
333                        b'\r' => {
334                            // Line continuation; a following LF belongs to it.
335                            if self.data.get(self.pos) == Some(&b'\n') {
336                                self.pos += 1;
337                            }
338                        }
339                        b'\n' => {}               // line continuation
340                        other => out.push(other), // unknown escape: byte kept, backslash dropped
341                    }
342                }
343                b'(' => {
344                    depth += 1;
345                    out.push(b'(');
346                }
347                b')' => {
348                    depth -= 1;
349                    if depth == 0 {
350                        return Token::LitString(out);
351                    }
352                    out.push(b')');
353                }
354                b'\r' => {
355                    // Raw EOL (CR or CRLF) normalizes to a single LF.
356                    if self.data.get(self.pos) == Some(&b'\n') {
357                        self.pos += 1;
358                    }
359                    out.push(b'\n');
360                }
361                other => out.push(other),
362            }
363        }
364        Token::LitString(out)
365    }
366
367    /// Lexes a hex string after the opening `<`: whitespace is ignored, a
368    /// trailing odd digit is padded with `0`, non-hex bytes are skipped
369    /// (lenient), and a missing `>` terminates at end of input.
370    fn lex_hex_string(&mut self) -> Token {
371        let mut out = Vec::new();
372        let mut pending: Option<u8> = None;
373        while let Some(&b) = self.data.get(self.pos) {
374            self.pos += 1;
375            if b == b'>' {
376                break;
377            }
378            let Some(v) = hex_val(b) else {
379                continue; // whitespace and invalid bytes are skipped
380            };
381            match pending.take() {
382                Some(hi) => out.push((hi << 4) | v),
383                None => pending = Some(v),
384            }
385        }
386        if let Some(hi) = pending {
387            out.push(hi << 4);
388        }
389        Token::HexString(out)
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    /// Lexes `src` to completion, asserting no errors, dropping the `Eof`.
398    fn toks(src: &[u8]) -> Vec<Token> {
399        let mut lexer = Lexer::new(src);
400        let mut out = Vec::new();
401        loop {
402            let token = lexer.next_token().expect("lexing must not fail");
403            if token == Token::Eof {
404                return out;
405            }
406            out.push(token);
407        }
408    }
409
410    fn one(src: &[u8]) -> Token {
411        let mut all = toks(src);
412        assert_eq!(all.len(), 1, "expected exactly one token in {src:?}");
413        all.pop().unwrap()
414    }
415
416    #[test]
417    fn numeric_forms() {
418        assert_eq!(
419            toks(b"+17 -98 34.5 -3.62 .5 4. -.002"),
420            vec![
421                Token::Int(17),
422                Token::Int(-98),
423                Token::Real(34.5),
424                Token::Real(-3.62),
425                Token::Real(0.5),
426                Token::Real(4.0),
427                Token::Real(-0.002),
428            ]
429        );
430        assert_eq!(one(b"0"), Token::Int(0));
431        assert_eq!(one(b"123"), Token::Int(123));
432        assert_eq!(one(b"0.0"), Token::Real(0.0));
433    }
434
435    #[test]
436    fn lenient_numbers() {
437        // First sign wins; later signs are ignored.
438        assert_eq!(one(b"--5"), Token::Int(-5));
439        assert_eq!(one(b"+-3"), Token::Int(3));
440        // A second period is ignored.
441        assert_eq!(one(b"1.2.3"), Token::Real(1.23));
442        // A lone period is 0.0; a lone sign is 0.
443        assert_eq!(one(b"."), Token::Real(0.0));
444        assert_eq!(one(b"-"), Token::Int(0));
445        // i64 overflow degrades to a real.
446        assert_eq!(one(b"99999999999999999999"), Token::Real(1e20));
447        // A numeric-looking run with letters is a keyword.
448        assert_eq!(one(b"1e5"), Token::Keyword(b"1e5".to_vec()));
449    }
450
451    #[test]
452    fn structural_delimiters() {
453        assert_eq!(
454            toks(b"[]<<>>"),
455            vec![
456                Token::ArrayOpen,
457                Token::ArrayClose,
458                Token::DictOpen,
459                Token::DictClose,
460            ]
461        );
462        assert_eq!(
463            toks(b"<< /Type /Page >>"),
464            vec![
465                Token::DictOpen,
466                Token::Name(Name("Type".into())),
467                Token::Name(Name("Page".into())),
468                Token::DictClose,
469            ]
470        );
471    }
472
473    #[test]
474    fn stray_delimiters_are_lenient_keywords() {
475        assert_eq!(one(b")"), Token::Keyword(b")".to_vec()));
476        assert_eq!(one(b"{"), Token::Keyword(b"{".to_vec()));
477        assert_eq!(one(b"}"), Token::Keyword(b"}".to_vec()));
478    }
479
480    #[test]
481    fn names() {
482        assert_eq!(one(b"/Name1"), Token::Name(Name("Name1".into())));
483        assert_eq!(one(b"/A#42"), Token::Name(Name("AB".into())));
484        assert_eq!(one(b"/Bad#zz"), Token::Name(Name("Bad#zz".into())));
485        assert_eq!(
486            one(b"/Lime#20Green"),
487            Token::Name(Name("Lime Green".into()))
488        );
489        assert_eq!(
490            one(b"/paired#28#29parentheses"),
491            Token::Name(Name("paired()parentheses".into()))
492        );
493        // Lowercase hex digits decode too.
494        assert_eq!(one(b"/A#6f"), Token::Name(Name("Ao".into())));
495        // Truncated escape at end of input is kept literally.
496        assert_eq!(one(b"/A#4"), Token::Name(Name("A#4".into())));
497        // The empty name is valid.
498        assert_eq!(one(b"/"), Token::Name(Name(String::new())));
499        // Names end at delimiters; the escape check must not read past one.
500        assert_eq!(
501            toks(b"/A#4/B"),
502            vec![
503                Token::Name(Name("A#4".into())),
504                Token::Name(Name("B".into())),
505            ]
506        );
507    }
508
509    #[test]
510    fn literal_string_basics() {
511        assert_eq!(one(b"()"), Token::LitString(Vec::new()));
512        assert_eq!(one(b"(hello)"), Token::LitString(b"hello".to_vec()));
513        assert_eq!(one(b"(a(b)c)"), Token::LitString(b"a(b)c".to_vec()));
514        assert_eq!(
515            one(b"(deep(er(and(deeper))))"),
516            Token::LitString(b"deep(er(and(deeper)))".to_vec())
517        );
518    }
519
520    #[test]
521    fn literal_string_every_escape_form() {
522        assert_eq!(
523            one(b"(\\n\\r\\t\\b\\f\\(\\)\\\\)"),
524            Token::LitString(vec![b'\n', b'\r', b'\t', 0x08, 0x0C, b'(', b')', b'\\'])
525        );
526        // An unknown escape drops the backslash and keeps the byte.
527        assert_eq!(one(b"(\\q)"), Token::LitString(b"q".to_vec()));
528    }
529
530    #[test]
531    fn literal_string_octal_escapes() {
532        assert_eq!(one(b"(\\053)"), Token::LitString(b"+".to_vec()));
533        assert_eq!(one(b"(\\53)"), Token::LitString(b"+".to_vec()));
534        assert_eq!(one(b"(\\5)"), Token::LitString(vec![0x05]));
535        // Exactly three digits are consumed; the fourth is literal.
536        assert_eq!(one(b"(\\0053)"), Token::LitString(vec![0x05, b'3']));
537        // High octal values wrap to one byte.
538        assert_eq!(one(b"(\\400)"), Token::LitString(vec![0x00]));
539        assert_eq!(one(b"(\\777)"), Token::LitString(vec![0xFF]));
540        // An octal escape terminated by a non-octal byte.
541        assert_eq!(one(b"(\\1x)"), Token::LitString(vec![0x01, b'x']));
542    }
543
544    #[test]
545    fn literal_string_line_continuations() {
546        assert_eq!(one(b"(ab\\\ncd)"), Token::LitString(b"abcd".to_vec()));
547        assert_eq!(one(b"(ab\\\rcd)"), Token::LitString(b"abcd".to_vec()));
548        assert_eq!(one(b"(ab\\\r\ncd)"), Token::LitString(b"abcd".to_vec()));
549    }
550
551    #[test]
552    fn literal_string_eol_normalization() {
553        assert_eq!(one(b"(a\nb)"), Token::LitString(b"a\nb".to_vec()));
554        assert_eq!(one(b"(a\rb)"), Token::LitString(b"a\nb".to_vec()));
555        assert_eq!(one(b"(a\r\nb)"), Token::LitString(b"a\nb".to_vec()));
556    }
557
558    #[test]
559    fn literal_string_unterminated_is_lenient() {
560        assert_eq!(one(b"(abc"), Token::LitString(b"abc".to_vec()));
561        assert_eq!(one(b"(abc\\"), Token::LitString(b"abc".to_vec()));
562    }
563
564    #[test]
565    fn hex_strings() {
566        assert_eq!(one(b"<>"), Token::HexString(Vec::new()));
567        assert_eq!(one(b"<901FA3>"), Token::HexString(vec![0x90, 0x1F, 0xA3]));
568        // Odd digit count pads a trailing zero.
569        assert_eq!(one(b"<901FA>"), Token::HexString(vec![0x90, 0x1F, 0xA0]));
570        // Whitespace inside is ignored.
571        assert_eq!(
572            one(b"<48 65\n6C\t6C 6F>"),
573            Token::HexString(b"Hello".to_vec())
574        );
575        // Lowercase digits decode too.
576        assert_eq!(
577            one(b"<deadBEEF>"),
578            Token::HexString(vec![0xDE, 0xAD, 0xBE, 0xEF])
579        );
580        // Missing `>` terminates at end of input (lenient).
581        assert_eq!(one(b"<41"), Token::HexString(vec![0x41]));
582    }
583
584    #[test]
585    fn comments() {
586        assert_eq!(
587            toks(b"1 % comment ( with ) delimiters <</junk>>\n2"),
588            vec![Token::Int(1), Token::Int(2)]
589        );
590        assert_eq!(toks(b"%PDF-1.7\n42"), vec![Token::Int(42)]);
591        // CR also ends a comment.
592        assert_eq!(toks(b"% c\r7"), vec![Token::Int(7)]);
593        // A comment running to end of input leaves only Eof.
594        assert_eq!(toks(b"5 % trailing"), vec![Token::Int(5)]);
595        assert_eq!(toks(b"%%EOF"), Vec::new());
596    }
597
598    #[test]
599    fn keywords() {
600        let src = b"obj endobj stream endstream R xref trailer startxref true false null n f";
601        let expected: Vec<Token> = src
602            .split(|&b| b == b' ')
603            .map(|w| Token::Keyword(w.to_vec()))
604            .collect();
605        assert_eq!(toks(src), expected);
606    }
607
608    #[test]
609    fn indirect_reference_shape() {
610        assert_eq!(
611            toks(b"12 0 R"),
612            vec![Token::Int(12), Token::Int(0), Token::Keyword(b"R".to_vec()),]
613        );
614    }
615
616    #[test]
617    fn eof_behavior() {
618        let mut lexer = Lexer::new(b"");
619        assert_eq!(lexer.next_token().unwrap(), Token::Eof);
620        // Eof is sticky: repeated calls keep returning it.
621        assert_eq!(lexer.next_token().unwrap(), Token::Eof);
622
623        let mut lexer = Lexer::new(b"  \t\r\n \x00\x0C ");
624        assert_eq!(lexer.next_token().unwrap(), Token::Eof);
625
626        let mut lexer = Lexer::new(b"1");
627        assert_eq!(lexer.next_token().unwrap(), Token::Int(1));
628        assert_eq!(lexer.next_token().unwrap(), Token::Eof);
629        assert_eq!(lexer.next_token().unwrap(), Token::Eof);
630
631        // Seeking past the end is Eof, not a panic.
632        let mut lexer = Lexer::new(b"abc");
633        lexer.seek(100);
634        assert_eq!(lexer.next_token().unwrap(), Token::Eof);
635    }
636
637    #[test]
638    fn peek_does_not_consume() {
639        let mut lexer = Lexer::new(b"/A 1");
640        let before = lexer.pos();
641        assert_eq!(lexer.peek_token().unwrap(), Token::Name(Name("A".into())));
642        assert_eq!(lexer.pos(), before, "peek must not move the cursor");
643        assert_eq!(lexer.peek_token().unwrap(), Token::Name(Name("A".into())));
644        assert_eq!(lexer.next_token().unwrap(), Token::Name(Name("A".into())));
645        assert_eq!(lexer.peek_token().unwrap(), Token::Int(1));
646        assert_eq!(lexer.next_token().unwrap(), Token::Int(1));
647        assert_eq!(lexer.peek_token().unwrap(), Token::Eof);
648    }
649
650    #[test]
651    fn seek_round_trips() {
652        let src = b"[ /Key (val) 42 ]";
653        let mut lexer = Lexer::new(src);
654        assert_eq!(lexer.next_token().unwrap(), Token::ArrayOpen);
655        let mark = lexer.pos();
656        assert_eq!(lexer.next_token().unwrap(), Token::Name(Name("Key".into())));
657        assert_eq!(
658            lexer.next_token().unwrap(),
659            Token::LitString(b"val".to_vec())
660        );
661        // Rewind and replay the same tokens.
662        lexer.seek(mark);
663        assert_eq!(lexer.next_token().unwrap(), Token::Name(Name("Key".into())));
664        assert_eq!(
665            lexer.next_token().unwrap(),
666            Token::LitString(b"val".to_vec())
667        );
668        assert_eq!(lexer.next_token().unwrap(), Token::Int(42));
669        assert_eq!(lexer.next_token().unwrap(), Token::ArrayClose);
670
671        // `at` starts mid-buffer at the same place `seek` would reach.
672        let mut resumed = Lexer::at(src, mark);
673        assert_eq!(
674            resumed.next_token().unwrap(),
675            Token::Name(Name("Key".into()))
676        );
677        assert_eq!(resumed.data(), src);
678    }
679
680    #[test]
681    fn skip_whitespace_and_comments_stops_at_token() {
682        let mut lexer = Lexer::new(b"  % one\n % two\r\n  7");
683        lexer.skip_whitespace_and_comments();
684        assert_eq!(lexer.data()[lexer.pos()], b'7');
685        // Idempotent when already at a token.
686        let pos = lexer.pos();
687        lexer.skip_whitespace_and_comments();
688        assert_eq!(lexer.pos(), pos);
689    }
690
691    #[test]
692    fn mixed_stream_of_tokens() {
693        assert_eq!(
694            toks(b"<</N 3/Root 1 0 R>>[(a)<62>/c true]"),
695            vec![
696                Token::DictOpen,
697                Token::Name(Name("N".into())),
698                Token::Int(3),
699                Token::Name(Name("Root".into())),
700                Token::Int(1),
701                Token::Int(0),
702                Token::Keyword(b"R".to_vec()),
703                Token::DictClose,
704                Token::ArrayOpen,
705                Token::LitString(b"a".to_vec()),
706                Token::HexString(b"b".to_vec()),
707                Token::Name(Name("c".into())),
708                Token::Keyword(b"true".to_vec()),
709                Token::ArrayClose,
710            ]
711        );
712    }
713
714    #[test]
715    fn character_classes() {
716        for b in [0x00u8, b'\t', b'\n', 0x0C, b'\r', b' '] {
717            assert!(is_whitespace(b), "{b:#04x} should be whitespace");
718            assert!(!is_regular(b));
719        }
720        for b in *b"()<>[]{}/%" {
721            assert!(is_delimiter(b), "{} should be a delimiter", b as char);
722            assert!(!is_regular(b));
723        }
724        for b in *b"aZ09+-.#_*'\"" {
725            assert!(is_regular(b), "{} should be regular", b as char);
726        }
727    }
728}