Skip to main content

paperforge_pdf/
parser.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3
4use crate::error::{PdfError, PdfResult};
5use crate::object::*;
6use crate::stream::{StreamDecoder, StreamFilter};
7
8#[derive(Debug, Clone, Copy, PartialEq, Default)]
9pub enum ParseMode {
10    Strict,
11    #[default]
12    Lenient,
13}
14
15/// Bounds applied while parsing untrusted input, so a hostile PDF cannot
16/// exhaust memory (huge strings, arrays, streams, decompression bombs).
17#[derive(Debug, Clone, Copy)]
18pub struct ParserLimits {
19    pub max_objects: usize,
20    pub max_string_length: usize,
21    pub max_array_length: usize,
22    pub max_dict_entries: usize,
23    pub max_recursion_depth: u32,
24    pub max_stream_size: usize,
25    pub max_decoded_stream_size: usize,
26}
27
28impl Default for ParserLimits {
29    fn default() -> Self {
30        Self {
31            max_objects: 1_000_000,
32            max_string_length: 1_048_576,
33            max_array_length: 100_000,
34            max_dict_entries: 10_000,
35            max_recursion_depth: 64,
36            max_stream_size: 100 * 1024 * 1024,
37            max_decoded_stream_size: 100 * 1024 * 1024,
38        }
39    }
40}
41
42#[derive(Debug, Clone)]
43pub struct Document {
44    version: String,
45    /// Object store keyed by `(number, generation)`. A `BTreeMap` gives
46    /// O(log n) lookups (previously a linear scan of a `Vec`) and iteration in
47    /// ascending object-number order, which makes serialized output
48    /// deterministic without an extra sort.
49    objects: BTreeMap<ObjectId, PdfObject>,
50    catalog: Option<ObjectId>,
51    info: Option<ObjectId>,
52}
53
54impl Document {
55    pub fn new() -> Self {
56        Self {
57            version: "1.7".to_string(),
58            objects: BTreeMap::new(),
59            catalog: None,
60            info: None,
61        }
62    }
63
64    pub fn version(&self) -> &str {
65        &self.version
66    }
67
68    fn set_version(&mut self, version: String) {
69        self.version = version;
70    }
71
72    pub fn objects(&self) -> &BTreeMap<ObjectId, PdfObject> {
73        &self.objects
74    }
75
76    /// Inserts an object, keeping the first definition if an id is seen twice
77    /// (matches the previous `Vec` semantics where the first entry won).
78    pub fn add_object(&mut self, id: ObjectId, obj: PdfObject) {
79        self.objects.entry(id).or_insert(obj);
80    }
81
82    pub fn get_object(&self, id: ObjectId) -> Option<&PdfObject> {
83        self.objects.get(&id)
84    }
85
86    pub fn get_object_mut(&mut self, id: ObjectId) -> Option<&mut PdfObject> {
87        self.objects.get_mut(&id)
88    }
89
90    pub fn set_catalog(&mut self, id: ObjectId) {
91        self.catalog = Some(id);
92    }
93
94    pub fn catalog(&self) -> Option<ObjectId> {
95        self.catalog
96    }
97
98    pub fn set_info(&mut self, id: ObjectId) {
99        self.info = Some(id);
100    }
101
102    pub fn info(&self) -> Option<ObjectId> {
103        self.info
104    }
105}
106
107impl Default for Document {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113#[derive(Debug, Clone, Copy)]
114#[allow(dead_code)] // generations are carried by the xref map; kept for future xref-rebuild support
115enum XrefEntry {
116    Free { generation: u16 },
117    Used { offset: u64, generation: u16 },
118    Compressed { stream_number: u32, index: u32 },
119}
120
121type XrefSection = (BTreeMap<u32, (u16, XrefEntry)>, Option<PdfDictionary>);
122
123pub struct Parser {
124    mode: ParseMode,
125    limits: ParserLimits,
126}
127
128impl Parser {
129    pub fn new() -> Self {
130        Self {
131            mode: ParseMode::default(),
132            limits: ParserLimits::default(),
133        }
134    }
135
136    pub fn with_mode(mode: ParseMode) -> Self {
137        Self {
138            mode,
139            limits: ParserLimits::default(),
140        }
141    }
142
143    pub fn with_limits(limits: ParserLimits) -> Self {
144        Self {
145            mode: ParseMode::default(),
146            limits,
147        }
148    }
149
150    pub fn parse(&self, input: &[u8]) -> PdfResult<Document> {
151        let mut doc = Document::new();
152        if let Some(eol) = input.iter().position(|&b| b == b'\n' || b == b'\r') {
153            if input.starts_with(b"%PDF-") {
154                doc.set_version(String::from_utf8_lossy(&input[5..eol]).trim().to_string());
155            }
156        }
157
158        let kw_pos = find_startxref(input)?;
159        let mut lx = Lexer::new(input, kw_pos as usize, ParseMode::Lenient, self.limits);
160        lx.skip_ws();
161        lx.match_kw(b"startxref");
162        let mut offset = lx.parse_unsigned()?;
163        let mut xref: BTreeMap<u32, (u16, XrefEntry)> = BTreeMap::new();
164        let mut trailer: Option<PdfDictionary> = None;
165
166        loop {
167            let (section, dict) = self.parse_xref_section(input, offset)?;
168            for (number, entry) in section {
169                xref.entry(number).or_insert(entry);
170            }
171            if xref.len() > self.limits.max_objects {
172                return Err(PdfError::Parse {
173                    offset,
174                    message: format!(
175                        "object count {} exceeds limit {}",
176                        xref.len(),
177                        self.limits.max_objects
178                    ),
179                });
180            }
181            if dict.is_some() {
182                trailer = dict;
183            }
184            let prev = trailer
185                .as_ref()
186                .and_then(|d| d.get_integer("Prev"))
187                .filter(|p| *p > 0 && (*p as u64) < offset);
188            match prev {
189                Some(p) => offset = p as u64,
190                None => break,
191            }
192        }
193
194        if let Some(t) = &trailer {
195            if let Some(id) = t.get("Root").and_then(|o| o.as_reference()) {
196                doc.set_catalog(id);
197            }
198            if let Some(id) = t.get("Info").and_then(|o| o.as_reference()) {
199                doc.set_info(id);
200            }
201        }
202
203        let mut object_streams: Vec<(u32, u32)> = Vec::new();
204        for (&number, &(generation, entry)) in &xref {
205            match entry {
206                XrefEntry::Free { .. } => {}
207                XrefEntry::Used { offset, .. } => {
208                    let obj = self.parse_object_at(input, offset);
209                    match obj {
210                        Ok(obj) => doc.add_object(ObjectId::new(number, generation), obj),
211                        Err(e) => {
212                            if self.mode == ParseMode::Strict {
213                                return Err(e);
214                            }
215                        }
216                    }
217                }
218                XrefEntry::Compressed {
219                    stream_number,
220                    index,
221                } => {
222                    object_streams.push((stream_number, index));
223                }
224            }
225        }
226
227        // Resolve objects stored in /ObjStm streams (compressed objects).
228        let mut resolved = BTreeMap::new();
229        for &(stream_number, index) in &object_streams {
230            let obj = doc.get_object(ObjectId::new(stream_number, 0));
231            if let Some(PdfObject::Stream(stream)) = obj {
232                let data = self.decode_stream(stream)?;
233                let n = stream.dictionary.get_integer("N").unwrap_or(0) as usize;
234                let first = stream.dictionary.get_integer("First").unwrap_or(0) as usize;
235                if let Some((obj_number, body)) =
236                    parse_object_stream(&data, n, first, index, self.limits)
237                {
238                    if let Some(header) = xref.get(&obj_number).copied() {
239                        resolved.insert(obj_number, (header.0, body));
240                        if resolved.len() > self.limits.max_objects {
241                            return Err(PdfError::Parse {
242                                offset: 0,
243                                message: "compressed object count exceeds limit".to_string(),
244                            });
245                        }
246                    }
247                }
248            }
249        }
250        for (number, (generation, body)) in resolved {
251            doc.add_object(ObjectId::new(number, generation), body);
252        }
253
254        Ok(doc)
255    }
256
257    pub fn parse_file(&self, path: &Path) -> PdfResult<Document> {
258        let data = std::fs::read(path)?;
259        self.parse(&data)
260    }
261
262    fn decode_stream(&self, stream: &PdfStream) -> PdfResult<Vec<u8>> {
263        let filter = stream.dictionary.get_name("Filter");
264        match filter {
265            Some(f) if f.as_str() == "FlateDecode" || f.as_str() == "Fl" => {
266                let decoded = StreamDecoder::new().decode_with_limit(
267                    &stream.data,
268                    StreamFilter::Flate,
269                    self.limits.max_decoded_stream_size,
270                )?;
271                if decoded.len() > self.limits.max_decoded_stream_size {
272                    return Err(PdfError::Parse {
273                        offset: 0,
274                        message: "decoded stream exceeds size limit".to_string(),
275                    });
276                }
277                Ok(decoded)
278            }
279            Some(_) => Err(PdfError::NotImplemented(
280                "unsupported stream filter".to_string(),
281            )),
282            None => Ok(stream.data.clone()),
283        }
284    }
285
286    /// Parses one xref location: either a classic `xref` table or an /XRef stream.
287    fn parse_xref_section(&self, input: &[u8], offset: u64) -> PdfResult<XrefSection> {
288        let mut lx = Lexer::new(input, offset as usize, self.mode, self.limits);
289        lx.skip_ws();
290        if lx.match_kw(b"xref") {
291            self.parse_classic_xref(&mut lx)
292        } else {
293            let obj = self.parse_object_at(input, offset)?;
294            match obj {
295                PdfObject::Stream(stream) => {
296                    let dict = stream.dictionary.clone();
297                    if dict.get_name("Type").map(|n| n.as_str()) != Some("XRef") {
298                        return Err(PdfError::Parse {
299                            offset,
300                            message: "expected /Type /XRef in xref stream".to_string(),
301                        });
302                    }
303                    let data = self.decode_stream(&stream)?;
304                    let entries = parse_xref_stream_entries(&data, &dict, offset)?;
305                    Ok((entries, Some(dict)))
306                }
307                _ => Err(PdfError::Parse {
308                    offset,
309                    message: "expected xref keyword or XRef stream".to_string(),
310                }),
311            }
312        }
313    }
314
315    fn parse_classic_xref(&self, lx: &mut Lexer<'_>) -> PdfResult<XrefSection> {
316        let mut entries = BTreeMap::new();
317        let max_entries = self.limits.max_objects;
318        loop {
319            lx.skip_ws();
320            let start = lx.parse_unsigned()?;
321            let count = lx.parse_unsigned()?;
322            for i in 0..count {
323                if entries.len() >= max_entries {
324                    return Err(lx.err(format!(
325                        "xref entry count {} exceeds limit {}",
326                        entries.len(),
327                        max_entries
328                    )));
329                }
330                let n = (start + i) as u32;
331                lx.skip_ws();
332                let field1 = lx.parse_unsigned()?;
333                lx.skip_ws();
334                let field2 = lx.parse_unsigned()?;
335                lx.skip_ws();
336                match lx.bump() {
337                    Some(b'n') => {
338                        entries.insert(
339                            n,
340                            (
341                                field2 as u16,
342                                XrefEntry::Used {
343                                    offset: field1,
344                                    generation: field2 as u16,
345                                },
346                            ),
347                        );
348                    }
349                    Some(b'f') => {
350                        entries.insert(
351                            n,
352                            (
353                                field2 as u16,
354                                XrefEntry::Free {
355                                    generation: field2 as u16,
356                                },
357                            ),
358                        );
359                    }
360                    _ => return Err(lx.err("expected `n` or `f` in xref entry")),
361                }
362            }
363            lx.skip_ws();
364            if lx.match_kw(b"trailer") {
365                break;
366            }
367        }
368        lx.skip_ws();
369        let dict = if lx.peek() == Some(b'<') {
370            let mut lx2 = Lexer::new(lx.data, lx.pos, self.mode, self.limits);
371            let d = lx2.parse_dict()?;
372            lx.pos = lx2.pos;
373            Some(d)
374        } else {
375            None
376        };
377        Ok((entries, dict))
378    }
379
380    fn parse_object_at(&self, input: &[u8], offset: u64) -> PdfResult<PdfObject> {
381        let mut lx = Lexer::new(input, offset as usize, self.mode, self.limits);
382        lx.skip_ws();
383        let _obj_number = lx.parse_unsigned()?;
384        lx.skip_ws();
385        let _generation = lx.parse_unsigned()?;
386        lx.skip_ws();
387        if !lx.match_kw(b"obj") {
388            return Err(lx.err("expected `obj` after object header"));
389        }
390        lx.skip_ws();
391        let obj = lx.parse_object()?;
392        lx.skip_ws();
393        if self.mode == ParseMode::Strict && !lx.match_kw(b"endobj") {
394            return Err(lx.err("expected `endobj`"));
395        }
396        Ok(obj)
397    }
398}
399
400impl Default for Parser {
401    fn default() -> Self {
402        Self::new()
403    }
404}
405
406/// Decodes an /ObjStm stream body and returns the object at `index`.
407fn parse_object_stream(
408    data: &[u8],
409    n: usize,
410    first: usize,
411    index: u32,
412    limits: ParserLimits,
413) -> Option<(u32, PdfObject)> {
414    let mut header_lexer = Lexer::new(data, 0, ParseMode::Lenient, limits);
415    let mut pairs = Vec::with_capacity(n.min(limits.max_objects));
416    for _ in 0..n.min(limits.max_objects) {
417        let num = header_lexer.parse_unsigned().ok()?;
418        let off = header_lexer.parse_unsigned().ok()?;
419        pairs.push((num as u32, off as usize));
420    }
421    let (num, off) = *pairs.get(index as usize)?;
422    let mut body = Lexer::new(data, first + off, ParseMode::Lenient, limits);
423    let obj = body.parse_object().ok()?;
424    Some((num, obj))
425}
426
427/// Parses an XRef stream's entry data given the `/W` widths and `/Index` pairs.
428fn parse_xref_stream_entries(
429    data: &[u8],
430    dict: &PdfDictionary,
431    offset: u64,
432) -> PdfResult<BTreeMap<u32, (u16, XrefEntry)>> {
433    let w = dict.get_array("W").ok_or_else(|| PdfError::Parse {
434        offset,
435        message: "xref stream missing /W".to_string(),
436    })?;
437    let mut widths = Vec::new();
438    for item in w.0.iter() {
439        let i = item.as_integer().ok_or_else(|| PdfError::Parse {
440            offset,
441            message: "invalid /W entry".to_string(),
442        })?;
443        widths.push(i as usize);
444    }
445    let first = match dict.get_array("Index") {
446        Some(arr) => {
447            let mut out = Vec::new();
448            let mut it = arr.0.iter();
449            while let Some(f) = it.next() {
450                let count = it.next().ok_or_else(|| PdfError::Parse {
451                    offset,
452                    message: "invalid /Index".to_string(),
453                })?;
454                out.push((
455                    f.as_integer().unwrap_or(0) as u32,
456                    count.as_integer().unwrap_or(0) as u32,
457                ));
458            }
459            out
460        }
461        None => vec![(0, dict.get_integer("Size").unwrap_or(0) as u32)],
462    };
463
464    let record = widths[0] + widths[1] + widths[2];
465    if record == 0 {
466        return Ok(BTreeMap::new());
467    }
468    let mut entries = BTreeMap::new();
469    let mut pos = 0usize;
470    let field = |offset: usize, width: usize| -> u64 {
471        if width == 0 || offset + width > data.len() {
472            return 0;
473        }
474        let mut v: u64 = 0;
475        for &b in &data[offset..offset + width] {
476            v = (v << 8) | b as u64;
477        }
478        v
479    };
480    for (first, count) in first {
481        for i in 0..count {
482            if pos + record > data.len() {
483                break;
484            }
485            let typ = field(pos, widths[0]);
486            let f2 = field(pos + widths[0], widths[1]);
487            let f3 = field(pos + widths[0] + widths[1], widths[2]);
488            pos += record;
489            let number = first + i;
490            match typ {
491                0 => {
492                    entries.insert(
493                        number,
494                        (
495                            f3 as u16,
496                            XrefEntry::Free {
497                                generation: f3 as u16,
498                            },
499                        ),
500                    );
501                }
502                1 => {
503                    entries.insert(
504                        number,
505                        (
506                            f3 as u16,
507                            XrefEntry::Used {
508                                offset: f2,
509                                generation: f3 as u16,
510                            },
511                        ),
512                    );
513                }
514                2 => {
515                    entries.insert(
516                        number,
517                        (
518                            0,
519                            XrefEntry::Compressed {
520                                stream_number: f2 as u32,
521                                index: f3 as u32,
522                            },
523                        ),
524                    );
525                }
526                _ => {}
527            }
528        }
529    }
530    Ok(entries)
531}
532
533fn find_startxref(input: &[u8]) -> PdfResult<u64> {
534    let needle = b"startxref";
535    let mut search = input.len();
536    loop {
537        let end = input[..search].len();
538        let idx = input[..end]
539            .windows(needle.len())
540            .rposition(|w| w == needle);
541        let i = match idx {
542            Some(i) => i,
543            None => {
544                return Err(PdfError::Parse {
545                    offset: 0,
546                    message: "no startxref keyword found".to_string(),
547                });
548            }
549        };
550        let prev_ok = i == 0 || matches!(input[i - 1], b' ' | b'\t' | b'\r' | b'\n' | b'\x0c' | 0);
551        let next = input.get(i + needle.len()).copied();
552        let next_ok = next
553            .map(|b| matches!(b, b' ' | b'\t' | b'\r' | b'\n' | b'\x0c' | 0))
554            .unwrap_or(true);
555        if prev_ok && next_ok {
556            return Ok(i as u64);
557        }
558        search = i;
559        if search == 0 {
560            break;
561        }
562    }
563    Err(PdfError::Parse {
564        offset: 0,
565        message: "no startxref keyword found".to_string(),
566    })
567}
568
569struct Lexer<'a> {
570    data: &'a [u8],
571    pos: usize,
572    strict: bool,
573    max_depth: u32,
574    depth: u32,
575    limits: ParserLimits,
576}
577
578impl<'a> Lexer<'a> {
579    fn new(data: &'a [u8], pos: usize, mode: ParseMode, limits: ParserLimits) -> Self {
580        Self {
581            data,
582            pos,
583            strict: mode == ParseMode::Strict,
584            max_depth: limits.max_recursion_depth,
585            depth: 0,
586            limits,
587        }
588    }
589
590    fn is_ws(b: u8) -> bool {
591        matches!(b, b' ' | b'\t' | b'\r' | b'\n' | 0x0c | 0)
592    }
593
594    fn is_delim(b: u8) -> bool {
595        matches!(
596            b,
597            b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
598        )
599    }
600
601    fn peek(&self) -> Option<u8> {
602        self.data.get(self.pos).copied()
603    }
604
605    fn bump(&mut self) -> Option<u8> {
606        let b = self.peek();
607        if b.is_some() {
608            self.pos += 1;
609        }
610        b
611    }
612
613    fn err(&self, message: impl Into<String>) -> PdfError {
614        PdfError::Parse {
615            offset: self.pos as u64,
616            message: message.into(),
617        }
618    }
619
620    fn skip_ws(&mut self) {
621        loop {
622            while self
623                .data
624                .get(self.pos)
625                .copied()
626                .map(Self::is_ws)
627                .unwrap_or(false)
628            {
629                self.pos += 1;
630            }
631            if self.data.get(self.pos) == Some(&b'%') {
632                while self.pos < self.data.len() && self.data[self.pos] != b'\n' {
633                    self.pos += 1;
634                }
635            } else {
636                break;
637            }
638        }
639    }
640
641    fn match_kw(&mut self, kw: &[u8]) -> bool {
642        // The lexer position can point past the end of the buffer when a
643        // corrupted xref points at a bogus offset; slicing must not panic.
644        if self
645            .data
646            .get(self.pos..)
647            .is_some_and(|rest| rest.starts_with(kw))
648        {
649            let after = self.data.get(self.pos + kw.len()).copied().unwrap_or(b' ');
650            if Self::is_ws(after) || Self::is_delim(after) {
651                self.pos += kw.len();
652                return true;
653            }
654        }
655        false
656    }
657
658    fn parse_unsigned(&mut self) -> PdfResult<u64> {
659        self.skip_ws();
660        let start = self.pos;
661        while self
662            .data
663            .get(self.pos)
664            .map(|b| b.is_ascii_digit())
665            .unwrap_or(false)
666        {
667            self.pos += 1;
668        }
669        if start == self.pos {
670            return Err(self.err("expected number"));
671        }
672        let mut value: u64 = 0;
673        for &b in &self.data[start..self.pos] {
674            value = match value
675                .checked_mul(10)
676                .and_then(|v| v.checked_add(u64::from(b - b'0')))
677            {
678                Some(v) => v,
679                None => return Err(self.err("number out of range")),
680            };
681        }
682        Ok(value)
683    }
684
685    fn parse_number_object(&mut self) -> PdfResult<PdfObject> {
686        self.skip_ws();
687        let start = self.pos;
688        while self
689            .data
690            .get(self.pos)
691            .is_some_and(|b| b.is_ascii_digit() || matches!(b, b'+' | b'-' | b'.' | b'e' | b'E'))
692        {
693            self.pos += 1;
694        }
695        if start == self.pos {
696            return Err(self.err("expected number"));
697        }
698        let tok = &self.data[start..self.pos];
699        if !tok.contains(&b'.') && !tok.contains(&b'e') && !tok.contains(&b'E') {
700            // Integer fast path: hand-rolled decimal accumulation that avoids
701            // UTF-8 validation and `str::parse` machinery.
702            let (negative, digits) = match tok.first() {
703                Some(b'-') => (true, &tok[1..]),
704                Some(b'+') => (false, &tok[1..]),
705                _ => (false, tok),
706            };
707            // Only take the fast path for well-formed digits; anything else
708            // (e.g. a lone sign or `++5`) falls through to the f64 parser,
709            // which reports an error just like `str::parse` did before.
710            if !digits.is_empty() && digits.iter().all(u8::is_ascii_digit) {
711                // Accumulate as a negative value so i64::MIN parses without
712                // overflow.
713                let mut acc: i64 = 0;
714                let mut overflow = false;
715                for &b in digits {
716                    match acc
717                        .checked_mul(10)
718                        .and_then(|v| v.checked_sub(i64::from(b - b'0')))
719                    {
720                        Some(v) => acc = v,
721                        None => {
722                            overflow = true;
723                            break;
724                        }
725                    }
726                }
727                if !overflow {
728                    let value = if negative {
729                        Some(acc)
730                    } else {
731                        acc.checked_neg()
732                    };
733                    if let Some(value) = value {
734                        return Ok(PdfObject::Integer(value));
735                    }
736                }
737                // Out-of-i64-range digit strings (e.g. a serialized 1e20) are
738                // legal PDF numbers; fall through to the f64 parser instead of
739                // failing, matching real-world tolerant readers.
740            }
741        }
742        let s = std::str::from_utf8(tok).map_err(|_| self.err("invalid number bytes"))?;
743        let f: f64 = s
744            .parse()
745            .map_err(|_| self.err(format!("invalid number `{s}`")))?;
746        Ok(PdfObject::Real(f))
747    }
748
749    fn parse_name(&mut self) -> PdfResult<PdfName> {
750        if self.peek() != Some(b'/') {
751            return Err(self.err("expected name"));
752        }
753        self.bump();
754        let mut bytes = Vec::with_capacity(8);
755        loop {
756            match self.peek() {
757                None => break,
758                Some(b) if Self::is_ws(b) || Self::is_delim(b) => break,
759                Some(b'#') => {
760                    self.bump();
761                    let hex = [
762                        self.bump().ok_or_else(|| self.err("truncated #-escape"))?,
763                        self.bump().ok_or_else(|| self.err("truncated #-escape"))?,
764                    ];
765                    let pair = std::str::from_utf8(&hex).map_err(|_| self.err("bad #-escape"))?;
766                    let v = u8::from_str_radix(pair, 16).map_err(|_| self.err("bad #-escape"))?;
767                    bytes.push(v);
768                }
769                Some(b) => {
770                    self.bump();
771                    bytes.push(b);
772                }
773            }
774        }
775        // Fast path: valid UTF-8 bytes convert directly; only malformed names
776        // fall back to lossy conversion.
777        let name = match String::from_utf8(bytes) {
778            Ok(s) => s,
779            Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
780        };
781        Ok(PdfName(name))
782    }
783
784    fn enter_nested(&mut self) -> PdfResult<()> {
785        self.depth += 1;
786        if self.depth > self.max_depth {
787            return Err(self.err("recursion limit exceeded"));
788        }
789        Ok(())
790    }
791
792    fn exit_nested(&mut self) {
793        self.depth = self.depth.saturating_sub(1);
794    }
795
796    fn parse_literal_string(&mut self) -> PdfResult<PdfString> {
797        self.bump(); // '('
798        let mut out = Vec::new();
799        let mut depth = 1u32;
800        loop {
801            if out.len() >= self.limits.max_string_length {
802                return Err(self.err("string length exceeds limit"));
803            }
804            match self.bump() {
805                None => return Err(self.err("unterminated string")),
806                Some(b'(') => {
807                    depth += 1;
808                    out.push(b'(');
809                }
810                Some(b')') => {
811                    depth -= 1;
812                    if depth == 0 {
813                        break;
814                    }
815                    out.push(b')');
816                }
817                Some(b'\\') => match self.bump() {
818                    None => return Err(self.err("truncated escape")),
819                    Some(b'n') => out.push(b'\n'),
820                    Some(b'r') => out.push(b'\r'),
821                    Some(b't') => out.push(b'\t'),
822                    Some(b'b') => out.push(8),
823                    Some(b'f') => out.push(12),
824                    Some(b'(') => out.push(b'('),
825                    Some(b')') => out.push(b')'),
826                    Some(b'\\') => out.push(b'\\'),
827                    Some(d @ b'0'..=b'7') => {
828                        let mut v = d - b'0';
829                        for _ in 0..2 {
830                            match self.peek() {
831                                Some(e @ b'0'..=b'7') => {
832                                    v = v * 8 + (e - b'0');
833                                    self.bump();
834                                }
835                                _ => break,
836                            }
837                        }
838                        out.push(v);
839                    }
840                    Some(b'\r') => {
841                        if self.peek() == Some(b'\n') {
842                            self.bump();
843                        }
844                    }
845                    Some(b'\n') => {}
846                    Some(other) => out.push(other),
847                },
848                Some(b'\r') => {
849                    if self.peek() == Some(b'\n') {
850                        self.bump();
851                    }
852                    out.push(b'\n');
853                }
854                Some(b) => out.push(b),
855            }
856        }
857        Ok(PdfString(out))
858    }
859
860    fn parse_hex_string(&mut self) -> PdfResult<PdfString> {
861        self.bump(); // '<'
862        let mut out = Vec::new();
863        let mut hi: Option<u8> = None;
864        loop {
865            if out.len() >= self.limits.max_string_length {
866                return Err(self.err("string length exceeds limit"));
867            }
868            match self.bump() {
869                None => return Err(self.err("unterminated hex string")),
870                Some(b'>') => {
871                    if let Some(h) = hi {
872                        out.push(h << 4);
873                    }
874                    break;
875                }
876                Some(b) if b.is_ascii_whitespace() => {}
877                Some(b) => {
878                    let v = match b {
879                        b'0'..=b'9' => b - b'0',
880                        b'a'..=b'f' => b - b'a' + 10,
881                        b'A'..=b'F' => b - b'A' + 10,
882                        _ => return Err(self.err("invalid hex digit")),
883                    };
884                    match hi {
885                        None => hi = Some(v),
886                        Some(h) => {
887                            out.push((h << 4) | v);
888                            hi = None;
889                        }
890                    }
891                }
892            }
893        }
894        Ok(PdfString(out))
895    }
896
897    fn parse_array(&mut self) -> PdfResult<PdfArray> {
898        self.bump(); // '['
899        self.enter_nested()?;
900        let mut arr = PdfArray::new();
901        loop {
902            self.skip_ws();
903            match self.peek() {
904                None => return Err(self.err("unterminated array")),
905                Some(b']') => {
906                    self.bump();
907                    self.exit_nested();
908                    break;
909                }
910                _ => {
911                    if arr.len() >= self.limits.max_array_length {
912                        return Err(self.err("array length exceeds limit"));
913                    }
914                    arr.push(self.parse_object()?);
915                }
916            }
917        }
918        Ok(arr)
919    }
920
921    fn parse_dict(&mut self) -> PdfResult<PdfDictionary> {
922        self.bump();
923        self.bump(); // <<
924        self.enter_nested()?;
925        let mut dict = PdfDictionary::new();
926        loop {
927            self.skip_ws();
928            match self.peek() {
929                None => return Err(self.err("unterminated dictionary")),
930                Some(b'>') => {
931                    if self.data.get(self.pos + 1) == Some(&b'>') {
932                        self.bump();
933                        self.bump();
934                        self.exit_nested();
935                        break;
936                    }
937                    return Err(self.err("single `>` inside dictionary"));
938                }
939                Some(b'/') => {
940                    if dict.len() >= self.limits.max_dict_entries {
941                        return Err(self.err("dictionary entry count exceeds limit"));
942                    }
943                    let key = self.parse_name()?;
944                    self.skip_ws();
945                    let val = self.parse_object()?;
946                    dict.insert(&key.0, val);
947                }
948                _ => return Err(self.err("expected /Name in dictionary")),
949            }
950        }
951        Ok(dict)
952    }
953
954    fn parse_object(&mut self) -> PdfResult<PdfObject> {
955        self.skip_ws();
956        match self.peek() {
957            None => Err(self.err("expected object")),
958            Some(b'[') => Ok(PdfObject::Array(self.parse_array()?)),
959            Some(b'<') => {
960                if self.data.get(self.pos + 1) == Some(&b'<') {
961                    let dict = self.parse_dict()?;
962                    self.skip_ws();
963                    if self.match_kw(b"stream") {
964                        if self.peek() == Some(b'\r') {
965                            self.bump();
966                        }
967                        if self.peek() == Some(b'\n') {
968                            self.bump();
969                        }
970                        let len = dict
971                            .get_integer("Length")
972                            .filter(|l| *l >= 0)
973                            .ok_or_else(|| self.err("stream missing valid /Length"))?
974                            as usize;
975                        if len > self.limits.max_stream_size {
976                            return Err(self.err("stream length exceeds limit"));
977                        }
978                        if self.pos + len > self.data.len() {
979                            return Err(self.err("stream /Length exceeds file"));
980                        }
981                        let data = self.data[self.pos..self.pos + len].to_vec();
982                        self.pos += len;
983                        self.skip_ws();
984                        if self.strict && !self.match_kw(b"endstream") {
985                            return Err(self.err("expected `endstream`"));
986                        }
987                        Ok(PdfObject::Stream(PdfStream::with_dict(dict, data)))
988                    } else {
989                        Ok(PdfObject::Dictionary(dict))
990                    }
991                } else {
992                    Ok(PdfObject::String(self.parse_hex_string()?))
993                }
994            }
995            Some(b'(') => Ok(PdfObject::String(self.parse_literal_string()?)),
996            Some(b'/') => Ok(PdfObject::Name(self.parse_name()?)),
997            Some(b'+') | Some(b'-') | Some(b'.') | Some(b'0'..=b'9') => {
998                let first = self.parse_number_object()?;
999                self.skip_ws();
1000                let next_is_number = matches!(
1001                    self.peek(),
1002                    Some(b'+') | Some(b'-') | Some(b'.') | Some(b'0'..=b'9')
1003                );
1004                if next_is_number {
1005                    let before_second = self.pos;
1006                    let second = self.parse_number_object()?;
1007                    self.skip_ws();
1008                    if self.peek() == Some(b'R') {
1009                        self.bump();
1010                        let num = first
1011                            .as_integer()
1012                            .ok_or_else(|| self.err("non-integer object number in reference"))?
1013                            as u32;
1014                        let gen = second
1015                            .as_integer()
1016                            .ok_or_else(|| self.err("non-integer generation in reference"))?
1017                            as u16;
1018                        return Ok(PdfObject::Reference(ObjectId::new(num, gen)));
1019                    }
1020                    // Not a reference: leave the second number for the next call.
1021                    self.pos = before_second;
1022                }
1023                Ok(first)
1024            }
1025            Some(b'T' | b't' | b'F' | b'f' | b'N' | b'n') => {
1026                for kw in [b"true".as_slice(), b"True".as_slice(), b"TRUE".as_slice()] {
1027                    if self.match_kw(kw) {
1028                        return Ok(PdfObject::Boolean(true));
1029                    }
1030                }
1031                for kw in [
1032                    b"false".as_slice(),
1033                    b"False".as_slice(),
1034                    b"FALSE".as_slice(),
1035                ] {
1036                    if self.match_kw(kw) {
1037                        return Ok(PdfObject::Boolean(false));
1038                    }
1039                }
1040                for kw in [b"null".as_slice(), b"Null".as_slice(), b"NULL".as_slice()] {
1041                    if self.match_kw(kw) {
1042                        return Ok(PdfObject::Null);
1043                    }
1044                }
1045                Err(self.err("unknown keyword"))
1046            }
1047            Some(_) => Err(self.err("unexpected token")),
1048        }
1049    }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054    use super::*;
1055    use crate::serializer::Serializer;
1056
1057    #[test]
1058    fn roundtrip_via_serializer() {
1059        let mut doc = Document::new();
1060        doc.set_catalog(ObjectId::new(1, 0));
1061        doc.set_info(ObjectId::new(4, 0));
1062        let mut catalog = PdfDictionary::new();
1063        catalog.insert("Type", PdfObject::Name(PdfName::new("Catalog")));
1064        catalog.insert("Pages", PdfObject::Reference(ObjectId::new(2, 0)));
1065        doc.add_object(ObjectId::new(1, 0), PdfObject::Dictionary(catalog));
1066        let mut pages = PdfDictionary::new();
1067        pages.insert("Type", PdfObject::Name(PdfName::new("Pages")));
1068        pages.insert("Count", PdfObject::Integer(1));
1069        let mut kids = PdfArray::new();
1070        kids.push(PdfObject::Reference(ObjectId::new(3, 0)));
1071        pages.insert("Kids", PdfObject::Array(kids));
1072        doc.add_object(ObjectId::new(2, 0), PdfObject::Dictionary(pages));
1073        let mut page = PdfDictionary::new();
1074        page.insert("Type", PdfObject::Name(PdfName::new("Page")));
1075        let mut media = PdfArray::new();
1076        media.push(PdfObject::Integer(0));
1077        media.push(PdfObject::Integer(0));
1078        media.push(PdfObject::Integer(612));
1079        media.push(PdfObject::Integer(792));
1080        page.insert("MediaBox", PdfObject::Array(media));
1081        doc.add_object(ObjectId::new(3, 0), PdfObject::Dictionary(page));
1082        let mut info = PdfDictionary::new();
1083        info.insert("Title", PdfObject::String(PdfString::from_literal("t")));
1084        doc.add_object(ObjectId::new(4, 0), PdfObject::Dictionary(info));
1085
1086        let mut buf = std::io::Cursor::new(Vec::new());
1087        Serializer::new().serialize(&doc, &mut buf).unwrap();
1088        let parsed = Parser::new().parse(buf.get_ref()).unwrap();
1089
1090        assert_eq!(parsed.catalog(), Some(ObjectId::new(1, 0)));
1091        assert_eq!(parsed.info(), Some(ObjectId::new(4, 0)));
1092        assert_eq!(parsed.objects().len(), 4);
1093        let media_data = parsed
1094            .get_object(ObjectId::new(3, 0))
1095            .unwrap()
1096            .as_dict()
1097            .unwrap()
1098            .get_array("MediaBox")
1099            .unwrap()
1100            .clone();
1101        assert_eq!(
1102            media_data.0,
1103            vec![
1104                PdfObject::Integer(0),
1105                PdfObject::Integer(0),
1106                PdfObject::Integer(612),
1107                PdfObject::Integer(792),
1108            ]
1109        );
1110    }
1111
1112    #[test]
1113    fn string_escaping_survives_serialize_parse_roundtrip() {
1114        let cases: &[&[u8]] = &[
1115            b"plain",
1116            b"with (parens) and \\ backslash",
1117            b"line1\nline2\r\nline3\rline4",
1118            "café ☕ 漢字 𝄞".as_bytes(),
1119            &[0x01, 0x07, 0x0b, 0x1b, 0x7f], // control bytes, octal escapes
1120            b"tab\there\t",
1121            b"",
1122        ];
1123        for &input in cases {
1124            let mut doc = Document::new();
1125            doc.set_catalog(ObjectId::new(1, 0));
1126            let mut dict = PdfDictionary::new();
1127            dict.insert("S", PdfObject::String(PdfString::from_bytes(input)));
1128            dict.insert("N", PdfObject::Real(1.5));
1129            doc.add_object(ObjectId::new(1, 0), PdfObject::Dictionary(dict));
1130
1131            let mut buf = std::io::Cursor::new(Vec::new());
1132            Serializer::new().serialize(&doc, &mut buf).unwrap();
1133            let parsed = Parser::new().parse(buf.get_ref()).unwrap();
1134            let round = parsed
1135                .get_object(ObjectId::new(1, 0))
1136                .unwrap()
1137                .as_dict()
1138                .unwrap()
1139                .get_string_bytes("S")
1140                .unwrap();
1141            assert_eq!(round, input, "roundtrip failed for {input:?}");
1142        }
1143    }
1144
1145    #[test]
1146    fn text_operator_escaping_roundtrips_tj_string() {
1147        // Content-stream Tj strings hold the same escaping rules as object strings.
1148        let tricky = vec![
1149            b"(a)".to_vec(),
1150            b"line1\nline2".to_vec(),
1151            "café ☕".as_bytes().to_vec(),
1152            b"\\back\\slash".to_vec(),
1153        ];
1154        for text in tricky {
1155            let mut doc = Document::new();
1156            doc.set_catalog(ObjectId::new(1, 0));
1157            let mut dict = PdfDictionary::new();
1158            dict.insert("Length", PdfObject::Integer(text.len() as i64));
1159            let stream = PdfStream::with_dict(dict, text.clone());
1160            doc.add_object(ObjectId::new(1, 0), PdfObject::Stream(stream));
1161
1162            let mut buf = std::io::Cursor::new(Vec::new());
1163            Serializer::new().serialize(&doc, &mut buf).unwrap();
1164            let parsed = Parser::new().parse(buf.get_ref()).unwrap();
1165            let round = parsed
1166                .get_object(ObjectId::new(1, 0))
1167                .unwrap()
1168                .as_stream()
1169                .unwrap()
1170                .data
1171                .clone();
1172            assert_eq!(round, text, "stream roundtrip failed for {text:?}");
1173        }
1174    }
1175
1176    #[test]
1177    fn parses_xref_stream_entries() {
1178        let mut dict = PdfDictionary::new();
1179        dict.insert("Type", PdfObject::Name(PdfName::new("XRef")));
1180        dict.insert("Size", PdfObject::Integer(4));
1181        let mut w = PdfArray::new();
1182        w.push(PdfObject::Integer(1));
1183        w.push(PdfObject::Integer(4));
1184        w.push(PdfObject::Integer(2));
1185        dict.insert("W", PdfObject::Array(w));
1186
1187        let mut data = Vec::new();
1188        data.extend_from_slice(&[0, 0, 0, 0, 0, 0xff, 0xff]);
1189        data.extend_from_slice(&[1, 0, 0, 0, 15, 0, 0]);
1190        data.extend_from_slice(&[2, 0, 0, 0, 10, 0, 3]);
1191        data.extend_from_slice(&[1, 0, 0, 3, 232, 0, 0]);
1192
1193        let entries = parse_xref_stream_entries(&data, &dict, 0).unwrap();
1194        assert_eq!(entries.len(), 4);
1195        assert!(matches!(
1196            entries[&0].1,
1197            XrefEntry::Free { generation: 65535 }
1198        ));
1199        assert!(matches!(
1200            entries[&1].1,
1201            XrefEntry::Used {
1202                offset: 15,
1203                generation: 0
1204            }
1205        ));
1206        assert!(matches!(
1207            entries[&2].1,
1208            XrefEntry::Compressed {
1209                stream_number: 10,
1210                index: 3
1211            }
1212        ));
1213        assert!(matches!(
1214            entries[&3].1,
1215            XrefEntry::Used {
1216                offset: 1000,
1217                generation: 0
1218            }
1219        ));
1220    }
1221
1222    #[test]
1223    fn decodes_flate_streams() {
1224        let plain = b"BT /F1 12 Tf 10 20 Td (hi) Tj ET";
1225        let mut dict = PdfDictionary::new();
1226        dict.insert("Filter", PdfObject::Name(PdfName::new("FlateDecode")));
1227        let encoded = crate::stream::StreamEncoder::new()
1228            .encode(plain, crate::stream::StreamFilter::Flate)
1229            .unwrap();
1230        let stream = PdfStream::with_dict(dict, encoded);
1231        let decoded = Parser::new().decode_stream(&stream).unwrap();
1232        assert_eq!(decoded, plain);
1233    }
1234
1235    #[test]
1236    fn parses_object_stream_objects() {
1237        let mut data = Vec::new();
1238        data.extend_from_slice(b"7 0 8 6");
1239        while data.len() < 12 {
1240            data.push(b' ');
1241        }
1242        data.extend_from_slice(b"42 ");
1243        while data.len() < 18 {
1244            data.push(b' ');
1245        }
1246        data.extend_from_slice(b"<< /A (x) >>");
1247
1248        let (num, obj) = parse_object_stream(&data, 2, 12, 0, ParserLimits::default()).unwrap();
1249        assert_eq!(num, 7);
1250        assert_eq!(obj, PdfObject::Integer(42));
1251
1252        let (num, obj) = parse_object_stream(&data, 2, 12, 1, ParserLimits::default()).unwrap();
1253        assert_eq!(num, 8);
1254        assert_eq!(
1255            obj.as_dict().unwrap().get("A"),
1256            Some(&PdfObject::String(PdfString::from_literal("x")))
1257        );
1258        assert!(parse_object_stream(&data, 2, 12, 9, ParserLimits::default()).is_none());
1259    }
1260
1261    #[test]
1262    fn parses_strings_names_and_numbers() {
1263        let mut lx = Lexer::new(
1264            b"(a\\(b\\)c) /A#20B 42 -1.5e2 true null <48656c6c6f>",
1265            0,
1266            ParseMode::Strict,
1267            ParserLimits::default(),
1268        );
1269        assert_eq!(
1270            lx.parse_object().unwrap(),
1271            PdfObject::String(PdfString::from_bytes(&b"a(b)c"[..]))
1272        );
1273        lx.skip_ws();
1274        assert_eq!(
1275            lx.parse_object().unwrap(),
1276            PdfObject::Name(PdfName::new("A B"))
1277        );
1278        lx.skip_ws();
1279        assert_eq!(lx.parse_object().unwrap(), PdfObject::Integer(42));
1280        lx.skip_ws();
1281        match lx.parse_object().unwrap() {
1282            PdfObject::Real(f) => assert!((f - -150.0).abs() < 0.001),
1283            _ => panic!("expected real"),
1284        }
1285        lx.skip_ws();
1286        assert_eq!(lx.parse_object().unwrap(), PdfObject::Boolean(true));
1287        lx.skip_ws();
1288        assert_eq!(lx.parse_object().unwrap(), PdfObject::Null);
1289        lx.skip_ws();
1290        assert_eq!(
1291            lx.parse_object().unwrap(),
1292            PdfObject::String(PdfString::from_bytes(&b"Hello"[..]))
1293        );
1294    }
1295
1296    fn tight_limits() -> ParserLimits {
1297        ParserLimits {
1298            max_objects: 4,
1299            max_string_length: 8,
1300            max_array_length: 4,
1301            max_dict_entries: 4,
1302            max_recursion_depth: 3,
1303            max_stream_size: 16,
1304            max_decoded_stream_size: 16,
1305        }
1306    }
1307
1308    #[test]
1309    fn limits_reject_overlong_string() {
1310        let mut lx = Lexer::new(b"(0123456789)", 0, ParseMode::Strict, tight_limits());
1311        assert!(lx.parse_object().is_err());
1312    }
1313
1314    #[test]
1315    fn limits_reject_deep_nesting() {
1316        let deep = b"[[[[[[[[[0]]]]]]]]]";
1317        let mut lx = Lexer::new(deep, 0, ParseMode::Strict, tight_limits());
1318        assert!(lx.parse_object().is_err());
1319    }
1320
1321    #[test]
1322    fn limits_reject_huge_array() {
1323        let mut lx = Lexer::new(b"[1 2 3 4 5]", 0, ParseMode::Strict, tight_limits());
1324        assert!(lx.parse_object().is_err());
1325    }
1326
1327    #[test]
1328    fn limits_reject_huge_stream_length() {
1329        // /Length larger than max_stream_size must be rejected before any copy.
1330        let input = b"<< /Length 1000 >>\nstream\n0123456789\nendstream";
1331        let mut lx = Lexer::new(input, 0, ParseMode::Strict, tight_limits());
1332        assert!(lx.parse_object().is_err());
1333    }
1334
1335    #[test]
1336    fn limits_reject_too_many_objects() {
1337        // Build a real PDF with 6 objects; parser with max_objects=4 must refuse.
1338        let mut doc = Document::new();
1339        for i in 1..=6u32 {
1340            doc.add_object(ObjectId::new(i, 0), PdfObject::Integer(i as i64));
1341        }
1342        let mut buf = std::io::Cursor::new(Vec::new());
1343        Serializer::new().serialize(&doc, &mut buf).unwrap();
1344        let limits = ParserLimits {
1345            max_objects: 4,
1346            ..tight_limits()
1347        };
1348        assert!(Parser::with_limits(limits).parse(buf.get_ref()).is_err());
1349    }
1350
1351    #[test]
1352    fn limits_reject_too_many_xref_entries() {
1353        // A classic xref table advertised with more entries than max_objects
1354        // must be stopped while scanning the table, before objects parse.
1355        let mut pdf = Vec::new();
1356        pdf.extend_from_slice(b"%PDF-1.7\n");
1357        pdf.extend_from_slice(b"1 0 obj\n<<>>\nendobj\n");
1358        let xref_at = pdf.len() as u64;
1359        pdf.extend_from_slice(b"xref\n0 6\n");
1360        pdf.extend_from_slice(b"0000000000 65535 f \n");
1361        for i in 1..=5u64 {
1362            pdf.extend_from_slice(format!("{i:010} 00000 n \n").as_bytes());
1363        }
1364        pdf.extend_from_slice(b"trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n");
1365        pdf.extend_from_slice(format!("{xref_at}\n").as_bytes());
1366        pdf.extend_from_slice(b"%%EOF");
1367
1368        let limits = ParserLimits {
1369            max_objects: 4,
1370            ..tight_limits()
1371        };
1372        let err = Parser::with_limits(limits).parse(&pdf).unwrap_err();
1373        assert!(
1374            err.to_string().contains("xref entry count"),
1375            "unexpected error: {err}"
1376        );
1377    }
1378
1379    /// Deterministic xorshift64 PRNG so the test is reproducible.
1380    struct XorShift(u64);
1381
1382    impl XorShift {
1383        fn next(&mut self) -> u64 {
1384            let mut x = self.0;
1385            x ^= x << 13;
1386            x ^= x >> 7;
1387            x ^= x << 17;
1388            self.0 = x;
1389            x
1390        }
1391    }
1392
1393    #[test]
1394    fn random_bytes_never_panic() {
1395        // Stand-in for the libFuzzer target (see `fuzz/`): feed thousands of
1396        // pseudo-random byte strings to the parser and require a Result, never
1397        // a panic. Runs in CI without a nightly toolchain.
1398        let mut rng = XorShift(0x9E37_79B9_7F4A_7C15);
1399        for _ in 0..5000 {
1400            let len = (rng.next() % 512) as usize;
1401            let bytes: Vec<u8> = (0..len).map(|_| (rng.next() & 0xff) as u8).collect();
1402            let _ = Parser::new().parse(&bytes);
1403        }
1404    }
1405
1406    #[test]
1407    fn byte_flipped_pdf_never_panics() {
1408        // A valid PDF with every single byte flipped (plus random mutations)
1409        // exercises parser assumptions the way corpus fuzzing does.
1410        let mut doc = Document::new();
1411        doc.set_catalog(ObjectId::new(1, 0));
1412        for i in 1..=4u32 {
1413            let mut dict = PdfDictionary::new();
1414            dict.insert("Type", PdfObject::Name(PdfName::new(&format!("T{i}"))));
1415            dict.insert("N", PdfObject::Integer(i as i64));
1416            doc.add_object(ObjectId::new(i, 0), PdfObject::Dictionary(dict));
1417        }
1418        let mut buf = std::io::Cursor::new(Vec::new());
1419        Serializer::new().serialize(&doc, &mut buf).unwrap();
1420        let original = buf.into_inner();
1421        let mut rng = XorShift(0xD1B5_4A32_D192_ED03);
1422        for _ in 0..1000 {
1423            let mut mutated = original.clone();
1424            let flips = 1 + (rng.next() % 4) as usize;
1425            for _ in 0..flips {
1426                let idx = (rng.next() as usize) % mutated.len().max(1);
1427                mutated[idx] ^= 1 << (rng.next() % 8);
1428            }
1429            let _ = Parser::new().parse(&mutated);
1430        }
1431    }
1432
1433    #[test]
1434    fn malformed_inputs_error_without_panicking() {
1435        let cases: &[&[u8]] = &[
1436            b"",
1437            b"%PDF-1.7",
1438            b"garbage",
1439            b"%PDF-1.7\n1 0 obj<<>>endobj\nxref\n0 1\ntrailer\nstartxref\n0\n%%EOF",
1440            b"%PDF-1.7\n1 0 obj\n(abc",
1441            b"%PDF-1.7\n1 0 obj\n<< /Length -5 >>\nstream\nx",
1442            &b"%PDF-1.7\n".repeat(2),
1443            &[0xff, 0xfe, 0x00, 0x00, 0x41, 0x42],
1444        ];
1445        for input in cases {
1446            // Must return a Result, never panic.
1447            let _ = Parser::new().parse(input);
1448        }
1449    }
1450
1451    #[test]
1452    fn stream_roundtrip_with_small_and_large_data() {
1453        for size in [0, 1, 15, 16, 17, 1000] {
1454            let payload: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
1455            let mut dict = PdfDictionary::new();
1456            dict.insert("Filter", PdfObject::Name(PdfName::new("FlateDecode")));
1457            let encoded = crate::stream::StreamEncoder::new()
1458                .encode(&payload, crate::stream::StreamFilter::Flate)
1459                .unwrap();
1460            let stream = PdfStream::with_dict(dict, encoded);
1461            let decoded = Parser::new().decode_stream(&stream).unwrap();
1462            assert_eq!(decoded, payload, "size {size} mismatch");
1463        }
1464    }
1465
1466    #[test]
1467    fn decoded_stream_size_is_bounded() {
1468        // A zlib bomb: 1 MB of zeros compresses to ~1 KB, so the decoder cap
1469        // must stop the expansion either by capping or erroring.
1470        let payload = vec![0u8; 1_000_000];
1471        let mut dict = PdfDictionary::new();
1472        dict.insert("Filter", PdfObject::Name(PdfName::new("FlateDecode")));
1473        let encoded = crate::stream::StreamEncoder::new()
1474            .encode(&payload, crate::stream::StreamFilter::Flate)
1475            .unwrap();
1476        assert!(encoded.len() < 2000, "test premise: bomb must compress");
1477        let stream = PdfStream::with_dict(dict, encoded);
1478        let limits = ParserLimits {
1479            max_decoded_stream_size: 64 * 1024,
1480            ..ParserLimits::default()
1481        };
1482        if let Ok(d) = Parser::with_limits(limits).decode_stream(&stream) {
1483            assert!(d.len() <= 64 * 1024 + 1);
1484        }
1485    }
1486}