Skip to main content

pdfrum_cmap/
lexer.rs

1//! The word lexer that CMap programs are read with.
2//!
3//! A CMap stream is a PostScript program, but nothing here interprets
4//! PostScript: it is split into whitespace- and delimiter-separated *words*
5//! and the CMap operators are recognised among them. This is a different and
6//! much simpler tokenizer than the one a content stream or a PDF body needs —
7//! it has no concept of a number, a string value, or an escape — so it lives
8//! here rather than being shared with `pdfrum-parser`.
9//!
10//! `pdfrum-font`'s `ToUnicode` parser reads the same shape of program and
11//! re-uses [`Words`] directly; that is why [`Words`] is re-exported from the
12//! crate root, as `pdfrum_cmap::Words`. The module itself is private — the
13//! root re-export block is the crate's surface.
14//!
15//! The shape [`Words`] hands back is pinned by `a_cid_range_splits_into_five_words`
16//! below.
17
18/// PDF whitespace. Note `0x80` and `0xFF`, which the PDF specification does
19/// not list: PDFium's character table classifies them as whitespace and real
20/// files are tokenized that way, so a `0xFF` between two words separates them
21/// rather than joining them.
22fn is_whitespace(b: u8) -> bool {
23    matches!(b, 0x00 | 0x09 | 0x0A | 0x0C | 0x0D | 0x20 | 0x80 | 0xFF)
24}
25
26/// PDF delimiters (ISO 32000-1 §7.2.2).
27fn is_delimiter(b: u8) -> bool {
28    matches!(
29        b,
30        b'%' | b'(' | b')' | b'/' | b'<' | b'>' | b'[' | b']' | b'{' | b'}'
31    )
32}
33
34/// An iterator over the words of a CMap program.
35///
36/// Iteration ends at the first word the underlying scan reports as empty,
37/// which is not always the end of the data: a `/Name` that runs to the end of
38/// the stream with no separator after it produces an empty word and therefore
39/// silently truncates the program. That is the behavior the CMap parser is
40/// built on, so it is the iterator's contract too.
41#[derive(Debug, Clone)]
42pub struct Words<'a> {
43    data: &'a [u8],
44    at: usize,
45}
46
47impl<'a> Words<'a> {
48    /// Start reading words from the beginning of `bytes`.
49    #[must_use]
50    pub fn new(bytes: &'a [u8]) -> Self {
51        Self { data: bytes, at: 0 }
52    }
53
54    /// Skip whitespace and `%` comments; return the first significant byte,
55    /// leaving `at` just past it.
56    fn skip_spaces_and_comments(&mut self) -> Option<u8> {
57        loop {
58            let mut c = *self.data.get(self.at)?;
59            self.at += 1;
60            while is_whitespace(c) {
61                c = *self.data.get(self.at)?;
62                self.at += 1;
63            }
64            if c != b'%' {
65                return Some(c);
66            }
67            loop {
68                let c = *self.data.get(self.at)?;
69                self.at += 1;
70                if c == b'\r' || c == b'\n' {
71                    break;
72                }
73            }
74        }
75    }
76
77    fn since(&self, start: usize) -> &'a [u8] {
78        self.data.get(start..self.at).unwrap_or_default()
79    }
80
81    /// A run of non-delimiter, non-whitespace bytes: an operator such as
82    /// `begincidrange`, or a bare number.
83    fn regular(&mut self, start: usize) -> &'a [u8] {
84        while let Some(&c) = self.data.get(self.at) {
85            if is_delimiter(c) || is_whitespace(c) {
86                break;
87            }
88            self.at += 1;
89        }
90        self.since(start)
91    }
92
93    /// `/Name`. A name that reaches the end of the data without a separator
94    /// after it yields an **empty** word, which ends the program.
95    fn name(&mut self, start: usize) -> &'a [u8] {
96        while let Some(&c) = self.data.get(self.at) {
97            if is_whitespace(c) || is_delimiter(c) {
98                return self.since(start);
99            }
100            self.at += 1;
101        }
102        &[]
103    }
104
105    /// `<...>` — a hex string, returned **including** both brackets — or the
106    /// two-byte token `<<`.
107    fn angle_open(&mut self, start: usize) -> &'a [u8] {
108        let Some(&first) = self.data.get(self.at) else {
109            return self.since(start);
110        };
111        self.at += 1;
112        if first == b'<' {
113            return self.since(start);
114        }
115        let mut c = first;
116        while self.at < self.data.len() && c != b'>' {
117            c = self.data.get(self.at).copied().unwrap_or(b'>');
118            self.at += 1;
119        }
120        self.since(start)
121    }
122
123    /// `>` or `>>`.
124    fn angle_close(&mut self, start: usize) -> &'a [u8] {
125        if self.data.get(self.at) == Some(&b'>') {
126            self.at += 1;
127        }
128        self.since(start)
129    }
130
131    /// `(...)` with balanced nesting and **no escape handling**, so a `\(`
132    /// opens a level like any other `(`. The token includes both parentheses.
133    fn parens(&mut self, start: usize) -> &'a [u8] {
134        let mut level = 1i32;
135        while level > 0 {
136            let Some(&c) = self.data.get(self.at) else {
137                break;
138            };
139            self.at += 1;
140            match c {
141                b'(' => level += 1,
142                b')' => level -= 1,
143                _ => {}
144            }
145        }
146        self.since(start)
147    }
148}
149
150impl<'a> Iterator for Words<'a> {
151    type Item = &'a [u8];
152
153    fn next(&mut self) -> Option<Self::Item> {
154        let first = self.skip_spaces_and_comments()?;
155        let start = self.at.checked_sub(1)?;
156        let word = if is_delimiter(first) {
157            match first {
158                b'/' => self.name(start),
159                b'<' => self.angle_open(start),
160                b'>' => self.angle_close(start),
161                b'(' => self.parens(start),
162                // `)`, `[`, `]`, `{`, `}` and `%` are single-byte words.
163                _ => self.since(start),
164            }
165        } else {
166            self.regular(start)
167        };
168        (!word.is_empty()).then_some(word)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::Words;
175
176    fn words(bytes: &[u8]) -> Vec<&[u8]> {
177        Words::new(bytes).collect()
178    }
179
180    #[test]
181    fn splits_operators_and_hex_strings() {
182        assert_eq!(
183            words(b"1 begincidchar <20> <100> endcidchar"),
184            vec![&b"1"[..], b"begincidchar", b"<20>", b"<100>", b"endcidchar"]
185        );
186    }
187
188    #[test]
189    fn empty_input_yields_nothing() {
190        assert!(words(b"").is_empty());
191        assert!(words(b"   \n\t").is_empty());
192    }
193
194    #[test]
195    fn comments_run_to_the_end_of_the_line() {
196        assert_eq!(words(b"a % comment here\nb"), vec![&b"a"[..], b"b"]);
197        // A comment with no line ending swallows the rest of the program.
198        assert_eq!(words(b"a % comment here"), vec![&b"a"[..]]);
199        // Comments may be consecutive.
200        assert_eq!(words(b"%one\n%two\nz"), vec![&b"z"[..]]);
201    }
202
203    #[test]
204    fn double_angle_is_its_own_word() {
205        assert_eq!(words(b"<</X 1>>"), vec![&b"<<"[..], b"/X", b"1", b">>"]);
206        assert_eq!(words(b"<<"), vec![&b"<<"[..]]);
207        assert_eq!(words(b">"), vec![&b">"[..]]);
208        assert_eq!(words(b">>"), vec![&b">>"[..]]);
209    }
210
211    /// A `<` with nothing after it is still a word, and an unterminated hex
212    /// string is returned as the remainder.
213    #[test]
214    fn truncated_hex_strings_survive() {
215        assert_eq!(words(b"<"), vec![&b"<"[..]]);
216        assert_eq!(words(b"<a1"), vec![&b"<a1"[..]]);
217        assert_eq!(words(b"<a1>"), vec![&b"<a1>"[..]]);
218    }
219
220    /// Parentheses nest and are not escaped, so `\(` opens a level.
221    #[test]
222    fn parenthesised_strings_are_returned_whole() {
223        assert_eq!(words(b"(a(b)c) x"), vec![&b"(a(b)c)"[..], b"x"]);
224        assert_eq!(words(b"(unterminated"), vec![&b"(unterminated"[..]]);
225        assert_eq!(words(br"(a\(b) x"), vec![&br"(a\(b) x"[..]]);
226        assert_eq!(words(b"(Japan1)"), vec![&b"(Japan1)"[..]]);
227    }
228
229    /// The truncation that ends a program: a name at the very end with no
230    /// separator after it is dropped, and iteration stops there.
231    #[test]
232    fn a_name_at_end_of_data_ends_the_program() {
233        assert_eq!(words(b"a /Ordering"), vec![&b"a"[..]]);
234        assert_eq!(words(b"a /Ordering "), vec![&b"a"[..], b"/Ordering"]);
235        assert_eq!(words(b"a /Ordering("), vec![&b"a"[..], b"/Ordering", b"("]);
236    }
237
238    #[test]
239    fn bracket_delimiters_are_single_byte_words() {
240        assert_eq!(
241            words(b"[1]{2})"),
242            vec![&b"["[..], b"1", b"]", b"{", b"2", b"}", b")"]
243        );
244    }
245
246    /// 0x80 and 0xFF separate words, matching the character table the oracle
247    /// tokenizes with.
248    #[test]
249    fn high_whitespace_bytes_separate_words() {
250        assert_eq!(words(&[b'a', 0x80, b'b']), vec![&b"a"[..], b"b"]);
251        assert_eq!(words(&[b'a', 0xFF, b'b']), vec![&b"a"[..], b"b"]);
252        // Other high bytes do not.
253        assert_eq!(words(&[b'a', 0xFE, b'b']), vec![&[b'a', 0xFE, b'b'][..]]);
254    }
255
256    /// Whatever the input, the lexer terminates and never reports a word that
257    /// is not a subslice of the input.
258    #[test]
259    fn tokens_stay_inside_the_input() {
260        let inputs: &[&[u8]] = &[
261            b"<<<<<<",
262            b"((((",
263            b"))))",
264            b"/",
265            b"%",
266            b"<>",
267            b"><",
268            &[0x00, 0xFF, 0x80, 0x25, 0x28],
269            &[0x3C; 64],
270        ];
271        for input in inputs {
272            let mut total = 0usize;
273            for w in Words::new(input) {
274                assert!(!w.is_empty());
275                assert!(w.len() <= input.len());
276                total += w.len();
277                assert!(total <= input.len(), "words overlap in {input:?}");
278            }
279        }
280    }
281    /// Was this module's doctest, kept as a unit test now that the module is
282    /// private: a `begincidrange` line is five words, and a hex string keeps
283    /// its angle brackets.
284    #[test]
285    fn a_cid_range_splits_into_five_words() {
286        let words: Vec<&[u8]> = Words::new(b"begincidrange <20> <7e> 1 endcidrange").collect();
287        assert_eq!(words.first().copied(), Some(&b"begincidrange"[..]));
288        // The angle brackets are part of the word.
289        assert_eq!(words.get(1).copied(), Some(&b"<20>"[..]));
290        assert_eq!(words.len(), 5);
291    }
292}