Skip to main content

stet_core/
tokenizer.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! PostScript tokenizer.
6//!
7//! Converts a byte stream into a sequence of tokens following the PLRM
8//! tokenization rules: numbers, names, strings, hex strings, procedures.
9
10use crate::error::PsError;
11use crate::file_store::FileStore;
12use crate::object::EntityId;
13
14/// A single PostScript token.
15#[derive(Debug, Clone, PartialEq)]
16pub enum Token {
17    Int(i64),
18    Real(f64),
19    Name(Vec<u8>, bool),    // (bytes, is_executable)
20    LiteralName(Vec<u8>),   // /name
21    ImmediateName(Vec<u8>), // //name
22    String(Vec<u8>),        // (hello) or <hex>
23    ProcBegin,              // {
24    ProcEnd,                // }
25    ArrayBegin,             // [
26    ArrayEnd,               // ]
27    DictBegin,              // <<
28    DictEnd,                // >>
29    /// Binary token byte (128-159) — caller must invoke the binary token parser.
30    BinaryTokenByte(u8),
31    Eof,
32}
33
34/// PostScript tokenizer.
35pub struct Tokenizer<'a> {
36    input: &'a [u8],
37    pos: usize,
38}
39
40impl<'a> Tokenizer<'a> {
41    pub fn new(input: &'a [u8]) -> Self {
42        Self { input, pos: 0 }
43    }
44
45    /// Current byte position in the input.
46    pub fn position(&self) -> usize {
47        self.pos
48    }
49
50    /// Return the remaining input bytes starting at the given position.
51    pub fn remaining_from(&self, pos: usize) -> &[u8] {
52        &self.input[pos..]
53    }
54
55    /// Advance the position by the given number of bytes.
56    pub fn advance(&mut self, n: usize) {
57        self.pos += n;
58    }
59
60    /// Return the next token, or `None` at EOF.
61    pub fn next_token(&mut self) -> Result<Option<Token>, PsError> {
62        self.skip_whitespace_and_comments();
63
64        if self.pos >= self.input.len() {
65            return Ok(None);
66        }
67
68        let b = self.input[self.pos];
69        match b {
70            b'(' => self.scan_string().map(Some),
71            b'<' => {
72                if self.pos + 1 < self.input.len() && self.input[self.pos + 1] == b'<' {
73                    self.pos += 2;
74                    Ok(Some(Token::DictBegin))
75                } else if self.pos + 1 < self.input.len() && self.input[self.pos + 1] == b'~' {
76                    self.scan_ascii85_string().map(Some)
77                } else {
78                    self.scan_hex_string().map(Some)
79                }
80            }
81            b'>' => {
82                if self.pos + 1 < self.input.len() && self.input[self.pos + 1] == b'>' {
83                    self.pos += 2;
84                    Ok(Some(Token::DictEnd))
85                } else {
86                    Err(PsError::SyntaxError)
87                }
88            }
89            b'{' => {
90                self.pos += 1;
91                Ok(Some(Token::ProcBegin))
92            }
93            b'}' => {
94                self.pos += 1;
95                Ok(Some(Token::ProcEnd))
96            }
97            b'[' => {
98                self.pos += 1;
99                Ok(Some(Token::ArrayBegin))
100            }
101            b']' => {
102                self.pos += 1;
103                Ok(Some(Token::ArrayEnd))
104            }
105            b'/' => {
106                self.pos += 1;
107                if self.pos < self.input.len() && self.input[self.pos] == b'/' {
108                    self.pos += 1;
109                    Ok(Some(self.scan_immediate_name()))
110                } else {
111                    Ok(Some(self.scan_literal_name()))
112                }
113            }
114            // Binary token bytes
115            128..=159 => {
116                self.pos += 1;
117                Ok(Some(Token::BinaryTokenByte(b)))
118            }
119            _ => {
120                // Try number first, fall back to name. A number stet cannot
121                // represent propagates as an error rather than becoming a name.
122                match self.try_scan_number()? {
123                    Some(tok) => Ok(Some(tok)),
124                    None => Ok(Some(self.scan_name())),
125                }
126            }
127        }
128    }
129
130    fn skip_whitespace_and_comments(&mut self) {
131        while self.pos < self.input.len() {
132            let b = self.input[self.pos];
133            if Self::is_whitespace(b) {
134                self.pos += 1;
135            } else if b == b'%' {
136                // Skip to end of line
137                while self.pos < self.input.len()
138                    && self.input[self.pos] != b'\n'
139                    && self.input[self.pos] != b'\r'
140                {
141                    self.pos += 1;
142                }
143            } else {
144                break;
145            }
146        }
147    }
148
149    /// Try to scan a number.
150    ///
151    /// `Ok(None)` means the token at `pos` is not a number and should be
152    /// scanned as a name. `Err` means it is a number stet cannot represent.
153    fn try_scan_number(&mut self) -> Result<Option<Token>, PsError> {
154        let start = self.pos;
155        let bytes = self.input;
156        let len = bytes.len();
157
158        if start >= len {
159            return Ok(None);
160        }
161
162        // Collect the token (up to whitespace, delimiter, or binary token byte)
163        let mut end = start;
164        while end < len
165            && !Self::is_whitespace(bytes[end])
166            && !Self::is_delimiter(bytes[end])
167            && !is_binary_token_byte(bytes[end])
168        {
169            end += 1;
170        }
171
172        if end == start {
173            return Ok(None);
174        }
175
176        let token_bytes = &bytes[start..end];
177
178        match try_parse_number_token(token_bytes) {
179            Ok(Some(tok)) => {
180                self.pos = end;
181                Ok(Some(tok))
182            }
183            Ok(None) => Ok(None),
184            Err(e) => Err(e),
185        }
186    }
187
188    fn scan_name(&mut self) -> Token {
189        let start = self.pos;
190        while self.pos < self.input.len()
191            && !Self::is_whitespace(self.input[self.pos])
192            && !Self::is_delimiter(self.input[self.pos])
193            && !is_binary_token_byte(self.input[self.pos])
194        {
195            self.pos += 1;
196        }
197        let name = self.input[start..self.pos].to_vec();
198        Token::Name(name, true) // executable name
199    }
200
201    fn scan_literal_name(&mut self) -> Token {
202        let start = self.pos;
203        while self.pos < self.input.len()
204            && !Self::is_whitespace(self.input[self.pos])
205            && !Self::is_delimiter(self.input[self.pos])
206            && !is_binary_token_byte(self.input[self.pos])
207        {
208            self.pos += 1;
209        }
210        let name = self.input[start..self.pos].to_vec();
211        Token::LiteralName(name)
212    }
213
214    fn scan_immediate_name(&mut self) -> Token {
215        let start = self.pos;
216        while self.pos < self.input.len()
217            && !Self::is_whitespace(self.input[self.pos])
218            && !Self::is_delimiter(self.input[self.pos])
219            && !is_binary_token_byte(self.input[self.pos])
220        {
221            self.pos += 1;
222        }
223        let name = self.input[start..self.pos].to_vec();
224        Token::ImmediateName(name)
225    }
226
227    /// Scan a parenthesized string: `(...)` with escape handling and balanced parens.
228    fn scan_string(&mut self) -> Result<Token, PsError> {
229        self.pos += 1; // skip opening '('
230        let mut result = Vec::new();
231        let mut depth = 1;
232
233        while self.pos < self.input.len() && depth > 0 {
234            let b = self.input[self.pos];
235            match b {
236                b'(' => {
237                    depth += 1;
238                    result.push(b'(');
239                    self.pos += 1;
240                }
241                b')' => {
242                    depth -= 1;
243                    if depth > 0 {
244                        result.push(b')');
245                    }
246                    self.pos += 1;
247                }
248                b'\\' => {
249                    self.pos += 1;
250                    if self.pos >= self.input.len() {
251                        return Err(PsError::SyntaxError);
252                    }
253                    let esc = self.input[self.pos];
254                    match esc {
255                        b'n' => {
256                            result.push(b'\n');
257                            self.pos += 1;
258                        }
259                        b'r' => {
260                            result.push(b'\r');
261                            self.pos += 1;
262                        }
263                        b't' => {
264                            result.push(b'\t');
265                            self.pos += 1;
266                        }
267                        b'b' => {
268                            result.push(0x08);
269                            self.pos += 1;
270                        }
271                        b'f' => {
272                            result.push(0x0C);
273                            self.pos += 1;
274                        }
275                        b'\\' => {
276                            result.push(b'\\');
277                            self.pos += 1;
278                        }
279                        b'(' => {
280                            result.push(b'(');
281                            self.pos += 1;
282                        }
283                        b')' => {
284                            result.push(b')');
285                            self.pos += 1;
286                        }
287                        b'\n' => {
288                            // Line continuation — skip
289                            self.pos += 1;
290                        }
291                        b'\r' => {
292                            // Line continuation — skip (and skip \n if follows)
293                            self.pos += 1;
294                            if self.pos < self.input.len() && self.input[self.pos] == b'\n' {
295                                self.pos += 1;
296                            }
297                        }
298                        b'0'..=b'7' => {
299                            // Octal escape: 1-3 digits
300                            let mut val: u8 = esc - b'0';
301                            self.pos += 1;
302                            for _ in 0..2 {
303                                if self.pos < self.input.len()
304                                    && self.input[self.pos] >= b'0'
305                                    && self.input[self.pos] <= b'7'
306                                {
307                                    val = (val << 3) | (self.input[self.pos] - b'0');
308                                    self.pos += 1;
309                                } else {
310                                    break;
311                                }
312                            }
313                            result.push(val);
314                        }
315                        _ => {
316                            // Unrecognized escape: just the char itself
317                            result.push(esc);
318                            self.pos += 1;
319                        }
320                    }
321                }
322                _ => {
323                    result.push(b);
324                    self.pos += 1;
325                }
326            }
327        }
328
329        if depth != 0 {
330            return Err(PsError::SyntaxError);
331        }
332
333        Ok(Token::String(result))
334    }
335
336    /// Scan a hex string: `<...>`.
337    fn scan_hex_string(&mut self) -> Result<Token, PsError> {
338        self.pos += 1; // skip '<'
339        let mut result = Vec::new();
340        let mut nibble: Option<u8> = None;
341
342        while self.pos < self.input.len() {
343            let b = self.input[self.pos];
344            if b == b'>' {
345                self.pos += 1;
346                // If we have a pending nibble, pad with 0
347                if let Some(high) = nibble {
348                    result.push(high << 4);
349                }
350                return Ok(Token::String(result));
351            }
352
353            if Self::is_whitespace(b) {
354                self.pos += 1;
355                continue;
356            }
357
358            let digit = match b {
359                b'0'..=b'9' => b - b'0',
360                b'a'..=b'f' => b - b'a' + 10,
361                b'A'..=b'F' => b - b'A' + 10,
362                _ => return Err(PsError::SyntaxError),
363            };
364
365            match nibble {
366                None => nibble = Some(digit),
367                Some(high) => {
368                    result.push((high << 4) | digit);
369                    nibble = None;
370                }
371            }
372            self.pos += 1;
373        }
374
375        Err(PsError::SyntaxError) // unterminated hex string
376    }
377
378    /// Scan an ASCII85 string: `<~...~>`.
379    fn scan_ascii85_string(&mut self) -> Result<Token, PsError> {
380        self.pos += 2; // skip '<~'
381        let mut encoded = Vec::new();
382
383        while self.pos < self.input.len() {
384            let b = self.input[self.pos];
385            if b == b'~' {
386                self.pos += 1;
387                if self.pos < self.input.len() && self.input[self.pos] == b'>' {
388                    self.pos += 1;
389                    return Ok(Token::String(Self::decode_ascii85(&encoded)?));
390                }
391                return Err(PsError::SyntaxError);
392            }
393            if !Self::is_whitespace(b) {
394                encoded.push(b);
395            }
396            self.pos += 1;
397        }
398
399        Err(PsError::SyntaxError)
400    }
401
402    fn decode_ascii85(data: &[u8]) -> Result<Vec<u8>, PsError> {
403        decode_ascii85(data)
404    }
405
406    fn is_whitespace(b: u8) -> bool {
407        is_whitespace(b)
408    }
409
410    fn is_delimiter(b: u8) -> bool {
411        is_delimiter(b)
412    }
413}
414
415// ─── Standalone helpers (shared by slice-based and streaming tokenizers) ─────
416
417/// PostScript whitespace: all bytes ≤ 0x20.
418fn is_whitespace(b: u8) -> bool {
419    b <= b' '
420}
421
422/// Binary token byte (128-159) — terminates names and numbers.
423fn is_binary_token_byte(b: u8) -> bool {
424    (128..=159).contains(&b)
425}
426
427/// PostScript delimiter characters.
428fn is_delimiter(b: u8) -> bool {
429    matches!(
430        b,
431        b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
432    )
433}
434
435/// Decode an ASCII85-encoded byte sequence.
436fn decode_ascii85(data: &[u8]) -> Result<Vec<u8>, PsError> {
437    let mut result = Vec::new();
438    let mut i = 0;
439
440    while i < data.len() {
441        if data[i] == b'z' {
442            result.extend_from_slice(&[0, 0, 0, 0]);
443            i += 1;
444            continue;
445        }
446
447        let mut group = [0u8; 5];
448        let mut count = 0;
449        while count < 5 && i < data.len() && data[i] != b'z' {
450            if data[i] < b'!' || data[i] > b'u' {
451                return Err(PsError::SyntaxError);
452            }
453            group[count] = data[i] - b'!';
454            count += 1;
455            i += 1;
456        }
457
458        if count < 2 {
459            if count == 1 {
460                return Err(PsError::SyntaxError);
461            }
462            break;
463        }
464
465        // Pad remaining with 'u' (84)
466        for g in group.iter_mut().skip(count) {
467            *g = 84;
468        }
469
470        let mut value: u32 = 0;
471        for &g in &group {
472            value = value
473                .checked_mul(85)
474                .and_then(|v| v.checked_add(g as u32))
475                .ok_or(PsError::SyntaxError)?;
476        }
477
478        let bytes = value.to_be_bytes();
479        let output_count = count - 1;
480        result.extend_from_slice(&bytes[..output_count]);
481    }
482
483    Ok(result)
484}
485
486/// Try to parse a byte sequence as a PostScript number token.
487///
488/// `Ok(None)` means "not a number, treat it as a name". `Err` is reserved for
489/// a token that *is* syntactically a number but names a value stet cannot
490/// represent — see the `limitcheck` below.
491fn try_parse_number_token(token_bytes: &[u8]) -> Result<Option<Token>, PsError> {
492    if token_bytes.is_empty() {
493        return Ok(None);
494    }
495
496    // Try radix: base#digits
497    if let Some(result) = try_parse_radix(token_bytes) {
498        return Ok(Some(result));
499    }
500
501    let Ok(s) = std::str::from_utf8(token_bytes) else {
502        return Ok(None);
503    };
504
505    let first = token_bytes[0];
506    let looks_numeric = first.is_ascii_digit()
507        || ((first == b'+' || first == b'-')
508            && token_bytes.len() > 1
509            && (token_bytes[1].is_ascii_digit() || token_bytes[1] == b'.'))
510        || (first == b'.' && token_bytes.len() > 1 && token_bytes[1].is_ascii_digit());
511
512    if !looks_numeric {
513        return Ok(None);
514    }
515
516    let is_real = s.contains('.') || s.contains('e') || s.contains('E');
517
518    if is_real {
519        return match s.parse::<f64>() {
520            Ok(v) => finite_real_token(v),
521            Err(_) => Ok(None),
522        };
523    }
524
525    // A literal too large for i64 becomes a real, per PLRM: "an integer that
526    // would exceed this limit is automatically converted to a real value".
527    if let Ok(v) = s.parse::<i64>() {
528        return Ok(Some(Token::Int(v)));
529    }
530    match s.parse::<f64>() {
531        Ok(v) => finite_real_token(v),
532        Err(_) => Ok(None),
533    }
534}
535
536/// Accept a scanned real, or decline the token if it is not representable.
537///
538/// `"1e999".parse::<f64>()` succeeds and yields `inf`, so without this a
539/// program could introduce a non-finite number by writing one down — no
540/// arithmetic required — and it would go on to reach coordinates, matrices,
541/// and colour components, none of which have defined behaviour for one.
542///
543/// Declining (`Ok(None)`) hands the token to the name scanner, which is what
544/// already happens to `1e999x`. A real program that writes `1e999` as an
545/// operand therefore gets `undefined` rather than a value, so the ingress is
546/// closed either way.
547///
548/// **Ghostscript raises `limitcheck` here instead, and stet deliberately does
549/// not.** That was tried first and it broke a file that renders correctly:
550/// `ps_corpus/files/pdftops-level1/88/882e212be166.ps` carries megabytes of
551/// hex image data, and byte runs like `5657564e574` are syntactically reals
552/// with a 580-digit exponent. They are scanned and discarded harmlessly as
553/// names, but erroring on them killed a 35 MB file that had produced a
554/// correct 2 MB page. (Ghostscript fails that file too — matching it is not
555/// worth losing a page stet can render.) Being more permissive than
556/// Ghostscript where it is safe to be is the same call already documented for
557/// the `i64` integer width.
558fn finite_real_token(v: f64) -> Result<Option<Token>, PsError> {
559    if v.is_finite() {
560        Ok(Some(Token::Real(v)))
561    } else {
562        Ok(None)
563    }
564}
565
566/// Try to parse a radix number: `base#digits`.
567fn try_parse_radix(token: &[u8]) -> Option<Token> {
568    let s = std::str::from_utf8(token).ok()?;
569    let hash_pos = s.find('#')?;
570    let base_str = &s[..hash_pos];
571    let digits_str = &s[hash_pos + 1..];
572    if digits_str.is_empty() {
573        return None;
574    }
575    let base: u32 = base_str.parse().ok()?;
576    if !(2..=36).contains(&base) {
577        return None;
578    }
579    // Radix literals are unsigned in source form; `16#FFFFFFFF` is a positive
580    // value that fits i64, so the whole parsed range stays integral.
581    let value = i64::from_str_radix(digits_str, base).ok()?;
582    Some(Token::Int(value))
583}
584
585// ─── Streaming tokenizer (byte-at-a-time from FileStore) ────────────────────
586
587/// Read the next token from a file/filter stream, one byte at a time.
588///
589/// Returns the token and the number of newlines consumed in leading
590/// whitespace/comments. Returns `None` on EOF.
591pub fn stream_next_token(
592    files: &mut FileStore,
593    entity: EntityId,
594) -> Result<Option<(Token, u32)>, PsError> {
595    let mut newlines = 0u32;
596
597    // Skip whitespace and comments to find the first significant byte.
598    let first = loop {
599        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
600            None => return Ok(None),
601            Some(b) if is_whitespace(b) => {
602                if b == b'\n' || b == b'\r' {
603                    newlines += 1;
604                }
605                continue;
606            }
607            Some(b'%') => {
608                // Skip comment until end of line.
609                loop {
610                    match files.read_byte(entity).map_err(|_| PsError::IOError)? {
611                        None => return Ok(None),
612                        Some(b'\n') | Some(b'\r') => {
613                            newlines += 1;
614                            break;
615                        }
616                        Some(_) => {}
617                    }
618                }
619                continue;
620            }
621            Some(b) => break b,
622        }
623    };
624
625    let token = match first {
626        b'(' => stream_scan_string(files, entity)?,
627        b'<' => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
628            Some(b'<') => Token::DictBegin,
629            Some(b'~') => stream_scan_ascii85(files, entity)?,
630            other => {
631                if let Some(b) = other {
632                    files.putback_bytes(entity, &[b]);
633                }
634                stream_scan_hex_string(files, entity)?
635            }
636        },
637        b'>' => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
638            Some(b'>') => Token::DictEnd,
639            _ => return Err(PsError::SyntaxError),
640        },
641        b'{' => Token::ProcBegin,
642        b'}' => Token::ProcEnd,
643        b'[' => Token::ArrayBegin,
644        b']' => Token::ArrayEnd,
645        b'/' => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
646            Some(b'/') => {
647                let name = stream_read_name_bytes(files, entity)?;
648                Token::ImmediateName(name)
649            }
650            Some(b) if !is_whitespace(b) && !is_delimiter(b) => {
651                files.putback_bytes(entity, &[b]);
652                let name = stream_read_name_bytes(files, entity)?;
653                Token::LiteralName(name)
654            }
655            other => {
656                if let Some(b) = other {
657                    files.putback_bytes(entity, &[b]);
658                }
659                Token::LiteralName(Vec::new())
660            }
661        },
662        // Binary token bytes
663        128..=159 => Token::BinaryTokenByte(first),
664        _ => {
665            // Number or executable name — collect bytes until delimiter.
666            let mut token_bytes = vec![first];
667            loop {
668                match files.read_byte(entity).map_err(|_| PsError::IOError)? {
669                    None => break,
670                    Some(b) if is_whitespace(b) => {
671                        // PLRM: trailing whitespace consumed for numbers and
672                        // executable names.  Critical for `RD` followed by
673                        // binary charstring data.
674                        if b == b'\n' || b == b'\r' {
675                            newlines += 1;
676                        }
677                        break;
678                    }
679                    Some(b) if is_delimiter(b) => {
680                        files.putback_bytes(entity, &[b]);
681                        break;
682                    }
683                    // Binary token bytes terminate names/numbers
684                    Some(b @ 128..=159) => {
685                        files.putback_bytes(entity, &[b]);
686                        break;
687                    }
688                    Some(b) => token_bytes.push(b),
689                }
690            }
691            match try_parse_number_token(&token_bytes)? {
692                Some(tok) => tok,
693                None => Token::Name(token_bytes, true),
694            }
695        }
696    };
697
698    Ok(Some((token, newlines)))
699}
700
701/// Read name bytes from a stream until whitespace or delimiter.
702/// Always puts back the terminating byte (literal/immediate names
703/// do NOT consume trailing whitespace per PLRM).
704fn stream_read_name_bytes(files: &mut FileStore, entity: EntityId) -> Result<Vec<u8>, PsError> {
705    let mut name = Vec::new();
706    loop {
707        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
708            None => break,
709            Some(b) if is_whitespace(b) || is_delimiter(b) || is_binary_token_byte(b) => {
710                files.putback_bytes(entity, &[b]);
711                break;
712            }
713            Some(b) => name.push(b),
714        }
715    }
716    Ok(name)
717}
718
719/// Scan a parenthesised string from a stream: `(...)`.
720fn stream_scan_string(files: &mut FileStore, entity: EntityId) -> Result<Token, PsError> {
721    let mut result = Vec::new();
722    let mut depth: u32 = 1;
723
724    while depth > 0 {
725        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
726            None => return Err(PsError::SyntaxError),
727            Some(b'(') => {
728                depth += 1;
729                result.push(b'(');
730            }
731            Some(b')') => {
732                depth -= 1;
733                if depth > 0 {
734                    result.push(b')');
735                }
736            }
737            Some(b'\\') => {
738                let esc = files
739                    .read_byte(entity)
740                    .map_err(|_| PsError::IOError)?
741                    .ok_or(PsError::SyntaxError)?;
742                match esc {
743                    b'n' => result.push(b'\n'),
744                    b'r' => result.push(b'\r'),
745                    b't' => result.push(b'\t'),
746                    b'b' => result.push(0x08),
747                    b'f' => result.push(0x0C),
748                    b'\\' => result.push(b'\\'),
749                    b'(' => result.push(b'('),
750                    b')' => result.push(b')'),
751                    b'\n' => {} // line continuation
752                    b'\r' => {
753                        // \r\n is a single line continuation.
754                        if let Some(next) = files.read_byte(entity).map_err(|_| PsError::IOError)?
755                            && next != b'\n'
756                        {
757                            files.putback_bytes(entity, &[next]);
758                        }
759                    }
760                    b'0'..=b'7' => {
761                        let mut val = esc - b'0';
762                        for _ in 0..2 {
763                            match files.read_byte(entity).map_err(|_| PsError::IOError)? {
764                                Some(b @ b'0'..=b'7') => val = (val << 3) | (b - b'0'),
765                                Some(other) => {
766                                    files.putback_bytes(entity, &[other]);
767                                    break;
768                                }
769                                None => break,
770                            }
771                        }
772                        result.push(val);
773                    }
774                    _ => result.push(esc),
775                }
776            }
777            Some(b) => result.push(b),
778        }
779    }
780
781    Ok(Token::String(result))
782}
783
784/// Scan a hex string from a stream: `<...>`.
785fn stream_scan_hex_string(files: &mut FileStore, entity: EntityId) -> Result<Token, PsError> {
786    let mut result = Vec::new();
787    let mut nibble: Option<u8> = None;
788
789    loop {
790        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
791            None => return Err(PsError::SyntaxError),
792            Some(b'>') => {
793                if let Some(high) = nibble {
794                    result.push(high << 4);
795                }
796                return Ok(Token::String(result));
797            }
798            Some(b) if is_whitespace(b) => continue,
799            Some(b) => {
800                let digit = match b {
801                    b'0'..=b'9' => b - b'0',
802                    b'a'..=b'f' => b - b'a' + 10,
803                    b'A'..=b'F' => b - b'A' + 10,
804                    _ => return Err(PsError::SyntaxError),
805                };
806                match nibble {
807                    None => nibble = Some(digit),
808                    Some(high) => {
809                        result.push((high << 4) | digit);
810                        nibble = None;
811                    }
812                }
813            }
814        }
815    }
816}
817
818/// Scan an ASCII85 string from a stream: `<~...~>`.
819fn stream_scan_ascii85(files: &mut FileStore, entity: EntityId) -> Result<Token, PsError> {
820    let mut encoded = Vec::new();
821
822    loop {
823        match files.read_byte(entity).map_err(|_| PsError::IOError)? {
824            None => return Err(PsError::SyntaxError),
825            Some(b'~') => match files.read_byte(entity).map_err(|_| PsError::IOError)? {
826                Some(b'>') => return Ok(Token::String(decode_ascii85(&encoded)?)),
827                _ => return Err(PsError::SyntaxError),
828            },
829            Some(b) if is_whitespace(b) => continue,
830            Some(b) => encoded.push(b),
831        }
832    }
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838
839    fn tokenize_all(input: &[u8]) -> Vec<Token> {
840        let mut t = Tokenizer::new(input);
841        let mut tokens = Vec::new();
842        while let Ok(Some(tok)) = t.next_token() {
843            tokens.push(tok);
844        }
845        tokens
846    }
847
848    /// A literal past the `f64` range must not become `inf`. It declines to
849    /// be a number and falls through to the name scanner, so the value never
850    /// enters the number space — see `finite_real_token` for why this is a
851    /// name rather than Ghostscript's `limitcheck`.
852    #[test]
853    fn out_of_range_real_literal_does_not_become_infinity() {
854        for src in [
855            &b"1e999"[..],
856            &b"-1e999"[..],
857            &b"1.5e400"[..],
858            &b"1e309"[..],
859            &b"99999999999999999999999999e999"[..],
860            // The shape that appears in hex image data, which must stay
861            // harmless: erroring on it broke a 35 MB corpus file that renders.
862            &b"5657564e574"[..],
863        ] {
864            let toks = tokenize_all(src);
865            assert!(
866                matches!(toks.as_slice(), [Token::Name(..)]),
867                "expected a name from {:?}, got {toks:?}",
868                String::from_utf8_lossy(src)
869            );
870        }
871    }
872
873    /// The boundary is `f64`'s, not Ghostscript's narrower one: values it
874    /// refuses but stet represents exactly must keep scanning.
875    #[test]
876    fn representable_reals_still_scan() {
877        assert_eq!(tokenize_all(b"1e308"), vec![Token::Real(1e308)]);
878        assert_eq!(tokenize_all(b"-1e308"), vec![Token::Real(-1e308)]);
879        assert_eq!(tokenize_all(b"1e-308"), vec![Token::Real(1e-308)]);
880        assert_eq!(tokenize_all(b"1e38"), vec![Token::Real(1e38)]);
881    }
882
883    /// Underflow to zero is representable and is not an error — PLRM's
884    /// "underflow" wording notwithstanding, `1e-999` is exactly what every
885    /// interpreter yields 0.0 for, and rejecting it would break real files.
886    #[test]
887    fn underflowing_literals_scan_as_zero() {
888        assert_eq!(tokenize_all(b"1e-999"), vec![Token::Real(0.0)]);
889    }
890
891    /// A token that merely looks numeric and is not must still fall through
892    /// to being a name, rather than becoming an error.
893    #[test]
894    fn non_numeric_tokens_are_still_names() {
895        assert!(matches!(
896            tokenize_all(b"1e999x").as_slice(),
897            [Token::Name(..)]
898        ));
899        assert!(matches!(tokenize_all(b"foo").as_slice(), [Token::Name(..)]));
900    }
901
902    #[test]
903    fn test_integers() {
904        assert_eq!(tokenize_all(b"42"), vec![Token::Int(42)]);
905        assert_eq!(tokenize_all(b"-7"), vec![Token::Int(-7)]);
906        assert_eq!(tokenize_all(b"+3"), vec![Token::Int(3)]);
907        assert_eq!(tokenize_all(b"0"), vec![Token::Int(0)]);
908    }
909
910    #[test]
911    fn test_reals() {
912        assert_eq!(tokenize_all(b"2.5"), vec![Token::Real(2.5)]);
913        assert_eq!(tokenize_all(b"-0.5"), vec![Token::Real(-0.5)]);
914        assert_eq!(tokenize_all(b"1e10"), vec![Token::Real(1e10)]);
915        assert_eq!(tokenize_all(b"1.5E-3"), vec![Token::Real(1.5e-3)]);
916    }
917
918    #[test]
919    fn test_radix() {
920        assert_eq!(tokenize_all(b"16#FF"), vec![Token::Int(255)]);
921        assert_eq!(tokenize_all(b"2#1010"), vec![Token::Int(10)]);
922        assert_eq!(tokenize_all(b"8#77"), vec![Token::Int(63)]);
923    }
924
925    #[test]
926    fn test_names() {
927        assert_eq!(
928            tokenize_all(b"add"),
929            vec![Token::Name(b"add".to_vec(), true)]
930        );
931        assert_eq!(
932            tokenize_all(b"/foo"),
933            vec![Token::LiteralName(b"foo".to_vec())]
934        );
935        assert_eq!(
936            tokenize_all(b"//bar"),
937            vec![Token::ImmediateName(b"bar".to_vec())]
938        );
939    }
940
941    #[test]
942    fn test_string_basic() {
943        assert_eq!(
944            tokenize_all(b"(hello)"),
945            vec![Token::String(b"hello".to_vec())]
946        );
947    }
948
949    #[test]
950    fn test_string_escapes() {
951        assert_eq!(
952            tokenize_all(b"(a\\nb)"),
953            vec![Token::String(b"a\nb".to_vec())]
954        );
955        assert_eq!(
956            tokenize_all(b"(a\\\\b)"),
957            vec![Token::String(b"a\\b".to_vec())]
958        );
959        assert_eq!(
960            tokenize_all(b"(\\110\\145\\154\\154\\157)"),
961            vec![Token::String(b"Hello".to_vec())]
962        );
963    }
964
965    #[test]
966    fn test_string_balanced_parens() {
967        assert_eq!(
968            tokenize_all(b"(a(b)c)"),
969            vec![Token::String(b"a(b)c".to_vec())]
970        );
971    }
972
973    #[test]
974    fn test_hex_string() {
975        assert_eq!(
976            tokenize_all(b"<48656C6C6F>"),
977            vec![Token::String(b"Hello".to_vec())]
978        );
979        // Odd nibble padded
980        assert_eq!(tokenize_all(b"<0>"), vec![Token::String(vec![0x00])]);
981    }
982
983    #[test]
984    fn test_procedures() {
985        let tokens = tokenize_all(b"{ add }");
986        assert_eq!(
987            tokens,
988            vec![
989                Token::ProcBegin,
990                Token::Name(b"add".to_vec(), true),
991                Token::ProcEnd,
992            ]
993        );
994    }
995
996    #[test]
997    fn test_comments() {
998        let tokens = tokenize_all(b"3 % comment\n4 add");
999        assert_eq!(
1000            tokens,
1001            vec![
1002                Token::Int(3),
1003                Token::Int(4),
1004                Token::Name(b"add".to_vec(), true),
1005            ]
1006        );
1007    }
1008
1009    #[test]
1010    fn test_full_program() {
1011        let tokens = tokenize_all(b"3 4 add 7 eq { (YES\\n) print } { (NO\\n) print } ifelse");
1012        assert_eq!(tokens.len(), 14);
1013        assert_eq!(tokens[0], Token::Int(3));
1014        assert_eq!(tokens[1], Token::Int(4));
1015        assert_eq!(tokens[2], Token::Name(b"add".to_vec(), true));
1016        assert_eq!(tokens[3], Token::Int(7));
1017        assert_eq!(tokens[4], Token::Name(b"eq".to_vec(), true));
1018        assert_eq!(tokens[5], Token::ProcBegin);
1019        assert_eq!(tokens[6], Token::String(b"YES\n".to_vec()));
1020        assert_eq!(tokens[7], Token::Name(b"print".to_vec(), true));
1021        assert_eq!(tokens[8], Token::ProcEnd);
1022        assert_eq!(tokens[9], Token::ProcBegin);
1023        assert_eq!(tokens[10], Token::String(b"NO\n".to_vec()));
1024        assert_eq!(tokens[11], Token::Name(b"print".to_vec(), true));
1025        assert_eq!(tokens[12], Token::ProcEnd);
1026        assert_eq!(tokens[13], Token::Name(b"ifelse".to_vec(), true));
1027    }
1028
1029    #[test]
1030    fn test_dict_delimiters() {
1031        let tokens = tokenize_all(b"<< /foo 42 >>");
1032        assert_eq!(
1033            tokens,
1034            vec![
1035                Token::DictBegin,
1036                Token::LiteralName(b"foo".to_vec()),
1037                Token::Int(42),
1038                Token::DictEnd,
1039            ]
1040        );
1041    }
1042}