Skip to main content

zpdf_parser/
lexer.rs

1use zpdf_core::{Error, ObjectId, ParseLimits, PdfName, PdfObject, PdfString, Result};
2
3pub struct Lexer<'a> {
4    data: &'a [u8],
5    pos: usize,
6    limits: &'a ParseLimits,
7    depth: u32,
8}
9
10impl<'a> Lexer<'a> {
11    pub fn new(data: &'a [u8], pos: usize, limits: &'a ParseLimits) -> Self {
12        Self {
13            data,
14            pos,
15            limits,
16            depth: 0,
17        }
18    }
19
20    /// Increment container-nesting depth, erroring if it exceeds the limit.
21    /// Call once on entry to `read_array`/`read_dict`.
22    fn enter_container(&mut self) -> Result<()> {
23        self.depth += 1;
24        if self.depth > self.limits.max_object_depth {
25            return Err(Error::RecursionLimit(self.limits.max_object_depth));
26        }
27        Ok(())
28    }
29
30    fn leave_container(&mut self) {
31        self.depth = self.depth.saturating_sub(1);
32    }
33
34    pub fn pos(&self) -> usize {
35        self.pos
36    }
37
38    pub fn set_pos(&mut self, pos: usize) {
39        self.pos = pos;
40    }
41
42    pub fn is_eof(&self) -> bool {
43        self.pos >= self.data.len()
44    }
45
46    fn peek(&self) -> Option<u8> {
47        self.data.get(self.pos).copied()
48    }
49
50    fn advance(&mut self) -> Option<u8> {
51        let b = self.data.get(self.pos).copied()?;
52        self.pos += 1;
53        Some(b)
54    }
55
56    pub fn skip_whitespace_and_comments(&mut self) {
57        loop {
58            match self.peek() {
59                Some(b' ' | b'\t' | b'\r' | b'\n' | b'\x00' | b'\x0c') => {
60                    self.pos += 1;
61                }
62                Some(b'%') => {
63                    self.pos += 1;
64                    while let Some(b) = self.peek() {
65                        self.pos += 1;
66                        if b == b'\r' || b == b'\n' {
67                            break;
68                        }
69                    }
70                }
71                _ => break,
72            }
73        }
74    }
75
76    pub fn next_token(&mut self) -> Result<PdfObject> {
77        self.skip_whitespace_and_comments();
78
79        if self.is_eof() {
80            return Err(Error::UnexpectedEof(self.pos as u64));
81        }
82
83        match self.peek().unwrap() {
84            b'/' => self.read_name(),
85            b'(' => self.read_literal_string(),
86            b'<' => {
87                if self.data.get(self.pos + 1) == Some(&b'<') {
88                    self.read_dict()
89                } else {
90                    self.read_hex_string()
91                }
92            }
93            b'[' => self.read_array(),
94            b'+' | b'-' | b'.' | b'0'..=b'9' => self.read_number(),
95            b't' | b'f' => self.read_bool_or_keyword(),
96            b'n' => self.read_null_or_keyword(),
97            _ => Err(Error::InvalidObject(
98                self.pos as u64,
99                format!("unexpected byte: 0x{:02x}", self.peek().unwrap()),
100            )),
101        }
102    }
103
104    fn read_name(&mut self) -> Result<PdfObject> {
105        self.advance(); // skip '/'
106        let start = self.pos;
107        while let Some(b) = self.peek() {
108            if is_delimiter(b) || is_whitespace(b) {
109                break;
110            }
111            self.pos += 1;
112        }
113        let raw = &self.data[start..self.pos];
114        if raw.len() > self.limits.max_string_length as usize {
115            return Err(Error::StringLengthLimit(self.limits.max_string_length));
116        }
117        let name = decode_name(raw);
118        Ok(PdfObject::Name(PdfName::new(name)))
119    }
120
121    fn read_literal_string(&mut self) -> Result<PdfObject> {
122        self.advance(); // skip '('
123        let mut buf = Vec::new();
124        let mut depth = 1u32;
125        let max = self.limits.max_string_length as usize;
126
127        while let Some(b) = self.advance() {
128            match b {
129                b'(' => {
130                    depth += 1;
131                    buf.push(b'(');
132                }
133                b')' => {
134                    depth -= 1;
135                    if depth == 0 {
136                        break;
137                    }
138                    buf.push(b')');
139                }
140                b'\\' => {
141                    if let Some(esc) = self.advance() {
142                        match esc {
143                            b'n' => buf.push(b'\n'),
144                            b'r' => buf.push(b'\r'),
145                            b't' => buf.push(b'\t'),
146                            b'b' => buf.push(0x08),
147                            b'f' => buf.push(0x0c),
148                            b'(' => buf.push(b'('),
149                            b')' => buf.push(b')'),
150                            b'\\' => buf.push(b'\\'),
151                            b'0'..=b'7' => {
152                                let mut octal = (esc - b'0') as u16;
153                                for _ in 0..2 {
154                                    match self.peek() {
155                                        Some(c @ b'0'..=b'7') => {
156                                            octal = octal * 8 + (c - b'0') as u16;
157                                            self.pos += 1;
158                                        }
159                                        _ => break,
160                                    }
161                                }
162                                buf.push(octal as u8);
163                            }
164                            b'\r' => {
165                                if self.peek() == Some(b'\n') {
166                                    self.pos += 1;
167                                }
168                            }
169                            b'\n' => {}
170                            _ => buf.push(esc),
171                        }
172                    }
173                }
174                _ => buf.push(b),
175            }
176            // Each iteration pushes at most one byte, so a single post-match
177            // check is sufficient to bound total string growth.
178            if buf.len() > max {
179                return Err(Error::StringLengthLimit(self.limits.max_string_length));
180            }
181        }
182
183        Ok(PdfObject::String(PdfString::new(buf)))
184    }
185
186    fn read_hex_string(&mut self) -> Result<PdfObject> {
187        self.advance(); // skip '<'
188        let mut buf = Vec::new();
189        let mut high: Option<u8> = None;
190        let max = self.limits.max_string_length as usize;
191
192        loop {
193            match self.advance() {
194                Some(b'>') => break,
195                Some(b) if is_whitespace(b) => continue,
196                Some(b) => {
197                    let nibble = hex_digit(b).ok_or_else(|| {
198                        Error::InvalidObject(self.pos as u64 - 1, "invalid hex digit".into())
199                    })?;
200                    match high {
201                        None => high = Some(nibble),
202                        Some(h) => {
203                            buf.push((h << 4) | nibble);
204                            high = None;
205                            if buf.len() > max {
206                                return Err(Error::StringLengthLimit(
207                                    self.limits.max_string_length,
208                                ));
209                            }
210                        }
211                    }
212                }
213                None => return Err(Error::UnexpectedEof(self.pos as u64)),
214            }
215        }
216
217        if let Some(h) = high {
218            buf.push(h << 4);
219        }
220
221        Ok(PdfObject::String(PdfString::new(buf)))
222    }
223
224    fn read_number(&mut self) -> Result<PdfObject> {
225        let start = self.pos;
226        let mut has_dot = false;
227
228        if matches!(self.peek(), Some(b'+' | b'-')) {
229            self.pos += 1;
230        }
231
232        while let Some(b) = self.peek() {
233            match b {
234                b'0'..=b'9' => self.pos += 1,
235                b'.' if !has_dot => {
236                    has_dot = true;
237                    self.pos += 1;
238                }
239                _ => break,
240            }
241        }
242
243        let s = std::str::from_utf8(&self.data[start..self.pos])
244            .map_err(|_| Error::InvalidObject(start as u64, "invalid number".into()))?;
245
246        if has_dot {
247            let n: f64 = s
248                .parse()
249                .map_err(|_| Error::InvalidObject(start as u64, format!("bad real: {s}")))?;
250            if !n.is_finite() {
251                return Err(Error::InvalidObject(
252                    start as u64,
253                    "non-finite real number".into(),
254                ));
255            }
256            Ok(PdfObject::Real(n))
257        } else {
258            let n: i64 = s
259                .parse()
260                .map_err(|_| Error::InvalidObject(start as u64, format!("bad integer: {s}")))?;
261            Ok(PdfObject::Integer(n))
262        }
263    }
264
265    fn read_bool_or_keyword(&mut self) -> Result<PdfObject> {
266        let start = self.pos;
267        while let Some(b) = self.peek() {
268            if is_delimiter(b) || is_whitespace(b) {
269                break;
270            }
271            self.pos += 1;
272        }
273        let word = &self.data[start..self.pos];
274        match word {
275            b"true" => Ok(PdfObject::Bool(true)),
276            b"false" => Ok(PdfObject::Bool(false)),
277            _ => Err(Error::InvalidObject(
278                start as u64,
279                format!("unexpected keyword: {}", String::from_utf8_lossy(word)),
280            )),
281        }
282    }
283
284    fn read_null_or_keyword(&mut self) -> Result<PdfObject> {
285        let start = self.pos;
286        while let Some(b) = self.peek() {
287            if is_delimiter(b) || is_whitespace(b) {
288                break;
289            }
290            self.pos += 1;
291        }
292        let word = &self.data[start..self.pos];
293        match word {
294            b"null" => Ok(PdfObject::Null),
295            _ => Err(Error::InvalidObject(
296                start as u64,
297                format!("unexpected keyword: {}", String::from_utf8_lossy(word)),
298            )),
299        }
300    }
301
302    fn read_array(&mut self) -> Result<PdfObject> {
303        self.enter_container()?;
304        self.advance(); // skip '['
305        let mut items = Vec::new();
306        loop {
307            self.skip_whitespace_and_comments();
308            if self.peek() == Some(b']') {
309                self.pos += 1;
310                break;
311            }
312            if self.is_eof() {
313                return Err(Error::UnexpectedEof(self.pos as u64));
314            }
315            let obj = self.next_token()?;
316            items.push(self.maybe_resolve_ref(obj)?);
317        }
318        self.leave_container();
319        Ok(PdfObject::Array(items))
320    }
321
322    fn read_dict(&mut self) -> Result<PdfObject> {
323        self.enter_container()?;
324        self.pos += 2; // skip '<<'
325        let mut dict = zpdf_core::PdfDict::new();
326        // Bound on malformed tokens skipped before we give up on this dict, so a
327        // pathological body can't make us churn. Well-formed dicts never trip it.
328        let mut bad = 0u32;
329        const MAX_BAD_TOKENS: u32 = 64;
330        loop {
331            self.skip_whitespace_and_comments();
332            if self.data.get(self.pos..self.pos + 2) == Some(b">>") {
333                self.pos += 2;
334                break;
335            }
336            if self.is_eof() {
337                // Tolerate a dict whose closing `>>` was truncated or overwritten
338                // (e.g. by the next `N 0 obj` header): return what parsed so far
339                // rather than failing the whole — often critical-path — object.
340                break;
341            }
342            // Read the key leniently: damaged files routinely corrupt one
343            // key/value while the rest of the dict is intact. A non-Name key or
344            // an untokenizable byte is skipped, not fatal — but a resource-limit
345            // error (depth/recursion) must still propagate so the guards hold.
346            let key = match self.next_token() {
347                Ok(PdfObject::Name(n)) => n,
348                Err(e @ Error::RecursionLimit(_)) => return Err(e),
349                Ok(_non_name) => {
350                    // next_token already advanced past the stray token.
351                    bad += 1;
352                    if bad > MAX_BAD_TOKENS {
353                        break;
354                    }
355                    continue;
356                }
357                Err(_) => {
358                    bad += 1;
359                    if bad > MAX_BAD_TOKENS {
360                        break;
361                    }
362                    self.pos += 1; // guarantee forward progress past the bad byte
363                    continue;
364                }
365            };
366            // A missing or garbled value ends the dict (best effort) instead of
367            // aborting the object; a recursion-limit error still propagates.
368            let value = match self.next_token() {
369                Ok(v) => v,
370                Err(e @ Error::RecursionLimit(_)) => return Err(e),
371                Err(_) => break,
372            };
373            let value = match self.maybe_resolve_ref(value) {
374                Ok(v) => v,
375                Err(e @ Error::RecursionLimit(_)) => return Err(e),
376                Err(_) => break,
377            };
378            dict.insert(key, value);
379        }
380        self.leave_container();
381        Ok(PdfObject::Dict(dict))
382    }
383
384    pub(crate) fn maybe_resolve_ref(&mut self, obj: PdfObject) -> Result<PdfObject> {
385        if let PdfObject::Integer(num) = obj {
386            let saved = self.pos;
387            self.skip_whitespace_and_comments();
388            if let Ok(PdfObject::Integer(gen)) = self.read_number_if_available() {
389                self.skip_whitespace_and_comments();
390                if self.peek() == Some(b'R') {
391                    self.pos += 1;
392                    if let (Ok(num), Ok(gen)) = (u32::try_from(num), u16::try_from(gen)) {
393                        return Ok(PdfObject::Ref(ObjectId(num, gen)));
394                    }
395                    return Ok(PdfObject::Null);
396                }
397            }
398            self.pos = saved;
399            Ok(PdfObject::Integer(num))
400        } else {
401            Ok(obj)
402        }
403    }
404
405    fn read_number_if_available(&mut self) -> Result<PdfObject> {
406        if matches!(self.peek(), Some(b'0'..=b'9' | b'+' | b'-' | b'.')) {
407            self.read_number()
408        } else {
409            Err(Error::InvalidObject(self.pos as u64, "not a number".into()))
410        }
411    }
412}
413
414fn is_whitespace(b: u8) -> bool {
415    matches!(b, b' ' | b'\t' | b'\r' | b'\n' | b'\x00' | b'\x0c')
416}
417
418fn is_delimiter(b: u8) -> bool {
419    matches!(
420        b,
421        b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
422    )
423}
424
425fn hex_digit(b: u8) -> Option<u8> {
426    match b {
427        b'0'..=b'9' => Some(b - b'0'),
428        b'a'..=b'f' => Some(b - b'a' + 10),
429        b'A'..=b'F' => Some(b - b'A' + 10),
430        _ => None,
431    }
432}
433
434fn decode_name(raw: &[u8]) -> String {
435    let mut result = Vec::with_capacity(raw.len());
436    let mut i = 0;
437    while i < raw.len() {
438        if raw[i] == b'#' && i + 2 < raw.len() {
439            if let (Some(h), Some(l)) = (hex_digit(raw[i + 1]), hex_digit(raw[i + 2])) {
440                result.push((h << 4) | l);
441                i += 3;
442                continue;
443            }
444        }
445        result.push(raw[i]);
446        i += 1;
447    }
448    String::from_utf8_lossy(&result).into_owned()
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    fn lim() -> ParseLimits {
456        ParseLimits::default()
457    }
458
459    #[test]
460    fn lex_name() {
461        let l = lim();
462        let mut lex = Lexer::new(b"/Type", 0, &l);
463        let obj = lex.next_token().unwrap();
464        assert_eq!(obj, PdfObject::Name(PdfName::new("Type")));
465    }
466
467    #[test]
468    fn lex_name_with_hex_escape() {
469        let l = lim();
470        let mut lex = Lexer::new(b"/A#20B", 0, &l);
471        let obj = lex.next_token().unwrap();
472        assert_eq!(obj, PdfObject::Name(PdfName::new("A B")));
473    }
474
475    #[test]
476    fn lex_integer() {
477        let l = lim();
478        let mut lex = Lexer::new(b"42 ", 0, &l);
479        assert_eq!(lex.next_token().unwrap(), PdfObject::Integer(42));
480    }
481
482    #[test]
483    fn lex_negative_real() {
484        let l = lim();
485        let mut lex = Lexer::new(b"-3.5 ", 0, &l);
486        match lex.next_token().unwrap() {
487            PdfObject::Real(n) => assert!((n - (-3.5)).abs() < 1e-10),
488            other => panic!("expected Real, got {other:?}"),
489        }
490    }
491
492    #[test]
493    fn reject_real_that_overflows_to_infinity() {
494        let l = lim();
495        let data = format!("{}.0", "9".repeat(400));
496        let mut lex = Lexer::new(data.as_bytes(), 0, &l);
497        assert!(matches!(lex.next_token(), Err(Error::InvalidObject(_, _))));
498    }
499
500    #[test]
501    fn lex_literal_string() {
502        let l = lim();
503        let mut lex = Lexer::new(b"(hello world)", 0, &l);
504        let obj = lex.next_token().unwrap();
505        assert_eq!(
506            obj,
507            PdfObject::String(PdfString::new(b"hello world".to_vec()))
508        );
509    }
510
511    #[test]
512    fn lex_literal_string_nested_parens() {
513        let l = lim();
514        let mut lex = Lexer::new(b"(a (b) c)", 0, &l);
515        let obj = lex.next_token().unwrap();
516        assert_eq!(obj, PdfObject::String(PdfString::new(b"a (b) c".to_vec())));
517    }
518
519    #[test]
520    fn lex_hex_string() {
521        let l = lim();
522        let mut lex = Lexer::new(b"<48656C6C6F>", 0, &l);
523        let obj = lex.next_token().unwrap();
524        assert_eq!(obj, PdfObject::String(PdfString::new(b"Hello".to_vec())));
525    }
526
527    #[test]
528    fn lex_array() {
529        let l = lim();
530        let mut lex = Lexer::new(b"[1 2 3]", 0, &l);
531        let obj = lex.next_token().unwrap();
532        assert_eq!(
533            obj,
534            PdfObject::Array(vec![
535                PdfObject::Integer(1),
536                PdfObject::Integer(2),
537                PdfObject::Integer(3),
538            ])
539        );
540    }
541
542    #[test]
543    fn lex_dict() {
544        let l = lim();
545        let mut lex = Lexer::new(b"<< /Type /Page /Count 5 >>", 0, &l);
546        let obj = lex.next_token().unwrap();
547        match obj {
548            PdfObject::Dict(d) => {
549                assert_eq!(d.get_name("Type").unwrap(), "Page");
550                assert_eq!(d.get_i64("Count").unwrap(), 5);
551            }
552            other => panic!("expected Dict, got {other:?}"),
553        }
554    }
555
556    #[test]
557    fn lex_bool_and_null() {
558        let l = lim();
559        let mut lex = Lexer::new(b"true", 0, &l);
560        assert_eq!(lex.next_token().unwrap(), PdfObject::Bool(true));
561
562        let mut lex = Lexer::new(b"false", 0, &l);
563        assert_eq!(lex.next_token().unwrap(), PdfObject::Bool(false));
564
565        let mut lex = Lexer::new(b"null", 0, &l);
566        assert_eq!(lex.next_token().unwrap(), PdfObject::Null);
567    }
568
569    #[test]
570    fn lex_indirect_ref_in_array() {
571        let l = lim();
572        let mut lex = Lexer::new(b"[12 0 R]", 0, &l);
573        let obj = lex.next_token().unwrap();
574        assert_eq!(obj, PdfObject::Array(vec![PdfObject::Ref(ObjectId(12, 0))]));
575    }
576
577    #[test]
578    fn skip_comments() {
579        let l = lim();
580        let mut lex = Lexer::new(b"% comment\n42 ", 0, &l);
581        assert_eq!(lex.next_token().unwrap(), PdfObject::Integer(42));
582    }
583
584    #[test]
585    fn reject_deeply_nested_array() {
586        let mut l = lim();
587        l.max_object_depth = 10;
588        let depth = 50usize;
589        let mut data = vec![b'['; depth];
590        data.extend(std::iter::repeat_n(b']', depth));
591        let mut lex = Lexer::new(&data, 0, &l);
592        let err = lex.next_token().unwrap_err();
593        assert!(matches!(err, Error::RecursionLimit(10)), "got {err:?}");
594    }
595
596    #[test]
597    fn reject_deeply_nested_dict() {
598        let mut l = lim();
599        l.max_object_depth = 5;
600        let n = 20usize;
601        let mut s = String::new();
602        for _ in 0..n {
603            s.push_str("<< /a ");
604        }
605        s.push('1');
606        for _ in 0..n {
607            s.push_str(" >>");
608        }
609        let data = s.into_bytes();
610        let mut lex = Lexer::new(&data, 0, &l);
611        let err = lex.next_token().unwrap_err();
612        assert!(matches!(err, Error::RecursionLimit(5)), "got {err:?}");
613    }
614
615    #[test]
616    fn nested_within_limit_ok() {
617        let l = lim(); // depth limit 100
618        let data = b"[[[[[1]]]]]"; // depth 5
619        let mut lex = Lexer::new(data, 0, &l);
620        assert!(lex.next_token().is_ok());
621    }
622
623    #[test]
624    fn reject_oversized_literal_string() {
625        let mut l = lim();
626        l.max_string_length = 8;
627        let mut data = vec![b'('];
628        data.extend(std::iter::repeat_n(b'a', 100));
629        data.push(b')');
630        let mut lex = Lexer::new(&data, 0, &l);
631        let err = lex.next_token().unwrap_err();
632        assert!(matches!(err, Error::StringLengthLimit(8)), "got {err:?}");
633    }
634
635    #[test]
636    fn reject_oversized_hex_string() {
637        let mut l = lim();
638        l.max_string_length = 4;
639        // 20 hex digits => 10 raw bytes > 4
640        let mut data = vec![b'<'];
641        data.extend(std::iter::repeat_n(b'4', 20));
642        data.push(b'>');
643        let mut lex = Lexer::new(&data, 0, &l);
644        let err = lex.next_token().unwrap_err();
645        assert!(matches!(err, Error::StringLengthLimit(4)), "got {err:?}");
646    }
647
648    #[test]
649    fn reject_oversized_name() {
650        let mut l = lim();
651        l.max_string_length = 4;
652        let mut lex = Lexer::new(b"/abcde", 0, &l);
653        assert!(matches!(lex.next_token(), Err(Error::StringLengthLimit(4))));
654    }
655
656    #[test]
657    fn out_of_range_reference_does_not_wrap() {
658        let l = lim();
659        let mut lex = Lexer::new(b"[4294967296 0 R -1 0 R 1 65536 R]", 0, &l);
660        let obj = lex.next_token().unwrap();
661        let arr = obj.as_array().unwrap();
662        assert!(arr.iter().all(|o| !matches!(o, PdfObject::Ref(_))));
663    }
664
665    #[test]
666    fn small_string_within_limit_ok() {
667        let l = lim(); // 65536
668        let mut lex = Lexer::new(b"(hello)", 0, &l);
669        assert_eq!(
670            lex.next_token().unwrap(),
671            PdfObject::String(PdfString::new(b"hello".to_vec()))
672        );
673    }
674}