Skip to main content

paperforge_pdf/
parser.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::path::Path;
4
5use crate::error::{PdfError, PdfResult};
6use crate::object::*;
7use crate::stream::{StreamDecoder, StreamFilter};
8
9#[derive(Debug, Clone, Copy, PartialEq, Default)]
10pub enum ParseMode {
11    Strict,
12    #[default]
13    Lenient,
14}
15
16/// Bounds applied while parsing untrusted input, so a hostile PDF cannot
17/// exhaust memory (huge strings, arrays, streams, decompression bombs).
18#[derive(Debug, Clone, Copy)]
19pub struct ParserLimits {
20    pub max_objects: usize,
21    pub max_string_length: usize,
22    pub max_array_length: usize,
23    pub max_dict_entries: usize,
24    pub max_recursion_depth: u32,
25    pub max_stream_size: usize,
26    pub max_decoded_stream_size: usize,
27}
28
29impl Default for ParserLimits {
30    fn default() -> Self {
31        Self {
32            max_objects: 1_000_000,
33            max_string_length: 1_048_576,
34            max_array_length: 100_000,
35            max_dict_entries: 10_000,
36            max_recursion_depth: 64,
37            max_stream_size: 100 * 1024 * 1024,
38            max_decoded_stream_size: 100 * 1024 * 1024,
39        }
40    }
41}
42
43#[derive(Debug, Clone)]
44pub struct Document {
45    version: String,
46    /// Object store keyed by `(number, generation)`. A `BTreeMap` gives
47    /// O(log n) lookups (previously a linear scan of a `Vec`) and iteration in
48    /// ascending object-number order, which makes serialized output
49    /// deterministic without an extra sort.
50    objects: BTreeMap<ObjectId, PdfObject>,
51    catalog: Option<ObjectId>,
52    info: Option<ObjectId>,
53}
54
55impl Document {
56    pub fn new() -> Self {
57        Self {
58            version: "1.7".to_string(),
59            objects: BTreeMap::new(),
60            catalog: None,
61            info: None,
62        }
63    }
64
65    pub fn version(&self) -> &str {
66        &self.version
67    }
68
69    fn set_version(&mut self, version: String) {
70        self.version = version;
71    }
72
73    pub fn objects(&self) -> &BTreeMap<ObjectId, PdfObject> {
74        &self.objects
75    }
76
77    /// Inserts an object, keeping the first definition if an id is seen twice
78    /// (matches the previous `Vec` semantics where the first entry won).
79    pub fn add_object(&mut self, id: ObjectId, obj: PdfObject) {
80        self.objects.entry(id).or_insert(obj);
81    }
82
83    pub fn get_object(&self, id: ObjectId) -> Option<&PdfObject> {
84        self.objects.get(&id)
85    }
86
87    pub fn get_object_mut(&mut self, id: ObjectId) -> Option<&mut PdfObject> {
88        self.objects.get_mut(&id)
89    }
90
91    pub fn set_catalog(&mut self, id: ObjectId) {
92        self.catalog = Some(id);
93    }
94
95    pub fn catalog(&self) -> Option<ObjectId> {
96        self.catalog
97    }
98
99    pub fn set_info(&mut self, id: ObjectId) {
100        self.info = Some(id);
101    }
102
103    pub fn info(&self) -> Option<ObjectId> {
104        self.info
105    }
106}
107
108impl Default for Document {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114#[derive(Debug, Clone, Copy)]
115#[allow(dead_code)] // generations are carried by the xref map; kept for future xref-rebuild support
116enum XrefEntry {
117    Free { generation: u16 },
118    Used { offset: u64, generation: u16 },
119    Compressed { stream_number: u32, index: u32 },
120}
121
122type XrefSection = (BTreeMap<u32, (u16, XrefEntry)>, Option<PdfDictionary>);
123
124/// Header version, xref table and trailer — everything needed to address
125/// objects by offset without parsing object bodies.
126struct XrefInfo {
127    version: String,
128    xref: BTreeMap<u32, (u16, XrefEntry)>,
129    catalog: Option<ObjectId>,
130    info: Option<ObjectId>,
131}
132
133pub struct Parser {
134    mode: ParseMode,
135    limits: ParserLimits,
136}
137
138impl Parser {
139    pub fn new() -> Self {
140        Self {
141            mode: ParseMode::default(),
142            limits: ParserLimits::default(),
143        }
144    }
145
146    pub fn with_mode(mode: ParseMode) -> Self {
147        Self {
148            mode,
149            limits: ParserLimits::default(),
150        }
151    }
152
153    pub fn with_limits(limits: ParserLimits) -> Self {
154        Self {
155            mode: ParseMode::default(),
156            limits,
157        }
158    }
159
160    pub fn parse(&self, input: &[u8]) -> PdfResult<Document> {
161        let mut doc = Document::new();
162        let info = self.build_xref(input)?;
163        doc.set_version(info.version);
164        if let Some(id) = info.catalog {
165            doc.set_catalog(id);
166        }
167        if let Some(id) = info.info {
168            doc.set_info(id);
169        }
170
171        let mut object_streams: Vec<(u32, u32)> = Vec::new();
172        for (&number, &(generation, entry)) in &info.xref {
173            match entry {
174                XrefEntry::Free { .. } => {}
175                XrefEntry::Used { offset, .. } => {
176                    let obj = self.parse_object_at(input, offset);
177                    match obj {
178                        Ok(obj) => doc.add_object(ObjectId::new(number, generation), obj),
179                        Err(e) => {
180                            if self.mode == ParseMode::Strict {
181                                return Err(e);
182                            }
183                        }
184                    }
185                }
186                XrefEntry::Compressed {
187                    stream_number,
188                    index,
189                } => {
190                    object_streams.push((stream_number, index));
191                }
192            }
193        }
194
195        // Resolve objects stored in /ObjStm streams (compressed objects).
196        let mut resolved = BTreeMap::new();
197        for &(stream_number, index) in &object_streams {
198            let obj = doc.get_object(ObjectId::new(stream_number, 0));
199            if let Some(PdfObject::Stream(stream)) = obj {
200                let data = self.decode_stream(stream)?;
201                let n = stream.dictionary.get_integer("N").unwrap_or(0) as usize;
202                let first = stream.dictionary.get_integer("First").unwrap_or(0) as usize;
203                if let Some((obj_number, body)) =
204                    parse_object_stream(&data, n, first, index, self.limits)
205                {
206                    if let Some(header) = info.xref.get(&obj_number).copied() {
207                        resolved.insert(obj_number, (header.0, body));
208                        if resolved.len() > self.limits.max_objects {
209                            return Err(PdfError::Parse {
210                                offset: 0,
211                                message: "compressed object count exceeds limit".to_string(),
212                            });
213                        }
214                    }
215                }
216            }
217        }
218        for (number, (generation, body)) in resolved {
219            doc.add_object(ObjectId::new(number, generation), body);
220        }
221
222        Ok(doc)
223    }
224
225    /// Walks the xref chain (startxref -> sections -> trailer) and returns the
226    /// object table, trailer and header metadata without parsing object bodies.
227    fn build_xref(&self, input: &[u8]) -> PdfResult<XrefInfo> {
228        let mut version = String::new();
229        if let Some(eol) = input.iter().position(|&b| b == b'\n' || b == b'\r') {
230            if input.starts_with(b"%PDF-") {
231                version = String::from_utf8_lossy(&input[5..eol]).trim().to_string();
232            }
233        }
234
235        let kw_pos = find_startxref(input)?;
236        let mut lx = Lexer::new(input, kw_pos as usize, ParseMode::Lenient, self.limits);
237        lx.skip_ws();
238        lx.match_kw(b"startxref");
239        let mut offset = lx.parse_unsigned()?;
240        let mut xref: BTreeMap<u32, (u16, XrefEntry)> = BTreeMap::new();
241        let mut trailer: Option<PdfDictionary> = None;
242
243        loop {
244            let (section, dict) = self.parse_xref_section(input, offset)?;
245            for (number, entry) in section {
246                xref.entry(number).or_insert(entry);
247            }
248            if xref.len() > self.limits.max_objects {
249                return Err(PdfError::Parse {
250                    offset,
251                    message: format!(
252                        "object count {} exceeds limit {}",
253                        xref.len(),
254                        self.limits.max_objects
255                    ),
256                });
257            }
258            if dict.is_some() {
259                trailer = dict;
260            }
261            let prev = trailer
262                .as_ref()
263                .and_then(|d| d.get_integer("Prev"))
264                .filter(|p| *p > 0 && (*p as u64) < offset);
265            match prev {
266                Some(p) => offset = p as u64,
267                None => break,
268            }
269        }
270
271        let (catalog, info) = if let Some(t) = &trailer {
272            (
273                t.get("Root").and_then(|o| o.as_reference()),
274                t.get("Info").and_then(|o| o.as_reference()),
275            )
276        } else {
277            (None, None)
278        };
279        Ok(XrefInfo {
280            version,
281            xref,
282            catalog,
283            info,
284        })
285    }
286
287    pub fn parse_file(&self, path: &Path) -> PdfResult<Document> {
288        let data = std::fs::read(path)?;
289        self.parse(&data)
290    }
291
292    fn decode_stream(&self, stream: &PdfStream) -> PdfResult<Vec<u8>> {
293        let filter = stream.dictionary.get_name("Filter");
294        match filter {
295            Some(f) if f.as_str() == "FlateDecode" || f.as_str() == "Fl" => {
296                let decoded = StreamDecoder::new().decode_with_limit(
297                    &stream.data,
298                    StreamFilter::Flate,
299                    self.limits.max_decoded_stream_size,
300                )?;
301                if decoded.len() > self.limits.max_decoded_stream_size {
302                    return Err(PdfError::Parse {
303                        offset: 0,
304                        message: "decoded stream exceeds size limit".to_string(),
305                    });
306                }
307                Ok(decoded)
308            }
309            Some(_) => Err(PdfError::NotImplemented(
310                "unsupported stream filter".to_string(),
311            )),
312            None => Ok(stream.data.clone()),
313        }
314    }
315
316    /// Parses one xref location: either a classic `xref` table or an /XRef stream.
317    fn parse_xref_section(&self, input: &[u8], offset: u64) -> PdfResult<XrefSection> {
318        let mut lx = Lexer::new(input, offset as usize, self.mode, self.limits);
319        lx.skip_ws();
320        if lx.match_kw(b"xref") {
321            self.parse_classic_xref(&mut lx)
322        } else {
323            let obj = self.parse_object_at(input, offset)?;
324            match obj {
325                PdfObject::Stream(stream) => {
326                    let dict = stream.dictionary.clone();
327                    if dict.get_name("Type").map(|n| n.as_str()) != Some("XRef") {
328                        return Err(PdfError::Parse {
329                            offset,
330                            message: "expected /Type /XRef in xref stream".to_string(),
331                        });
332                    }
333                    let data = self.decode_stream(&stream)?;
334                    let entries = parse_xref_stream_entries(&data, &dict, offset)?;
335                    Ok((entries, Some(dict)))
336                }
337                _ => Err(PdfError::Parse {
338                    offset,
339                    message: "expected xref keyword or XRef stream".to_string(),
340                }),
341            }
342        }
343    }
344
345    fn parse_classic_xref(&self, lx: &mut Lexer<'_>) -> PdfResult<XrefSection> {
346        let mut entries = BTreeMap::new();
347        let max_entries = self.limits.max_objects;
348        loop {
349            lx.skip_ws();
350            let start = lx.parse_unsigned()?;
351            let count = lx.parse_unsigned()?;
352            for i in 0..count {
353                if entries.len() >= max_entries {
354                    return Err(lx.err(format!(
355                        "xref entry count {} exceeds limit {}",
356                        entries.len(),
357                        max_entries
358                    )));
359                }
360                let n = (start + i) as u32;
361                lx.skip_ws();
362                let field1 = lx.parse_unsigned()?;
363                lx.skip_ws();
364                let field2 = lx.parse_unsigned()?;
365                lx.skip_ws();
366                match lx.bump() {
367                    Some(b'n') => {
368                        entries.insert(
369                            n,
370                            (
371                                field2 as u16,
372                                XrefEntry::Used {
373                                    offset: field1,
374                                    generation: field2 as u16,
375                                },
376                            ),
377                        );
378                    }
379                    Some(b'f') => {
380                        entries.insert(
381                            n,
382                            (
383                                field2 as u16,
384                                XrefEntry::Free {
385                                    generation: field2 as u16,
386                                },
387                            ),
388                        );
389                    }
390                    _ => return Err(lx.err("expected `n` or `f` in xref entry")),
391                }
392            }
393            lx.skip_ws();
394            if lx.match_kw(b"trailer") {
395                break;
396            }
397        }
398        lx.skip_ws();
399        let dict = if lx.peek() == Some(b'<') {
400            let mut lx2 = Lexer::new(lx.data, lx.pos, self.mode, self.limits);
401            let d = lx2.parse_dict()?;
402            lx.pos = lx2.pos;
403            Some(d)
404        } else {
405            None
406        };
407        Ok((entries, dict))
408    }
409
410    fn parse_object_at(&self, input: &[u8], offset: u64) -> PdfResult<PdfObject> {
411        let mut lx = Lexer::new(input, offset as usize, self.mode, self.limits);
412        lx.skip_ws();
413        let _obj_number = lx.parse_unsigned()?;
414        lx.skip_ws();
415        let _generation = lx.parse_unsigned()?;
416        lx.skip_ws();
417        if !lx.match_kw(b"obj") {
418            return Err(lx.err("expected `obj` after object header"));
419        }
420        lx.skip_ws();
421        let obj = lx.parse_object()?;
422        lx.skip_ws();
423        if self.mode == ParseMode::Strict && !lx.match_kw(b"endobj") {
424            return Err(lx.err("expected `endobj`"));
425        }
426        Ok(obj)
427    }
428}
429
430/// Lazy file parser: reads only the xref chain eagerly, then parses
431/// individual objects on demand with windowed file reads (offsets come from
432/// the xref table). Objects — including large stream bodies — that are never
433/// requested are never loaded into RAM, unlike [`Parser::parse`], which
434/// materializes the whole file. Each object is cached after its first read.
435pub struct StreamingParser {
436    parser: Parser,
437    file: std::fs::File,
438    file_len: u64,
439    xref: BTreeMap<u32, (u16, XrefEntry)>,
440    version: String,
441    catalog: Option<ObjectId>,
442    info: Option<ObjectId>,
443    cache: RefCell<BTreeMap<ObjectId, PdfObject>>,
444}
445
446impl StreamingParser {
447    /// Opens `path` and walks its xref chain. Object content is not read yet.
448    pub fn open(path: impl AsRef<Path>) -> PdfResult<Self> {
449        Self::with_parser(Parser::new(), path)
450    }
451
452    /// Opens with a configured [`Parser`], e.g. carrying custom
453    /// [`ParserLimits`].
454    pub fn with_parser(parser: Parser, path: impl AsRef<Path>) -> PdfResult<Self> {
455        let file = std::fs::File::open(path)?;
456        let file_len = file.metadata()?.len();
457        let info = Self::walk_xref(&parser, &file, file_len)?;
458        Ok(Self {
459            parser,
460            file,
461            file_len,
462            xref: info.xref,
463            version: info.version,
464            catalog: info.catalog,
465            info: info.info,
466            cache: RefCell::new(BTreeMap::new()),
467        })
468    }
469
470    /// PDF version from the header, e.g. `"1.7"`.
471    pub fn version(&self) -> &str {
472        &self.version
473    }
474
475    pub fn catalog(&self) -> Option<ObjectId> {
476        self.catalog
477    }
478
479    pub fn info(&self) -> Option<ObjectId> {
480        self.info
481    }
482
483    /// Number of entries in the xref table (includes free entries).
484    pub fn object_count(&self) -> usize {
485        self.xref.len()
486    }
487
488    /// Size of the backing file in bytes.
489    pub fn file_size(&self) -> u64 {
490        self.file_len
491    }
492
493    /// Parses object `id` on demand (cached after first request). Returns
494    /// `Ok(None)` for free or unknown entries.
495    pub fn get_object(&self, id: ObjectId) -> PdfResult<Option<PdfObject>> {
496        if let Some(obj) = self.cache.borrow().get(&id) {
497            return Ok(Some(obj.clone()));
498        }
499        let Some(&(_, entry)) = self.xref.get(&id.number) else {
500            return Ok(None);
501        };
502        let obj = self.resolve_entry(id, entry, 0)?;
503        if let Some(obj) = &obj {
504            self.cache.borrow_mut().insert(id, obj.clone());
505        }
506        Ok(obj)
507    }
508
509    /// Returns the decoded body of a stream object, or `None` if `id` is not
510    /// a stream (or missing).
511    pub fn get_stream_data(&self, id: ObjectId) -> PdfResult<Option<Vec<u8>>> {
512        match self.get_object(id)? {
513            Some(PdfObject::Stream(stream)) => self.parser.decode_stream(&stream).map(Some),
514            Some(_) | None => Ok(None),
515        }
516    }
517
518    fn resolve_entry(
519        &self,
520        id: ObjectId,
521        entry: XrefEntry,
522        depth: u32,
523    ) -> PdfResult<Option<PdfObject>> {
524        if depth > 8 {
525            return Err(PdfError::Parse {
526                offset: id.number as u64,
527                message: "object stream nesting too deep".to_string(),
528            });
529        }
530        match entry {
531            XrefEntry::Free { .. } => Ok(None),
532            XrefEntry::Used { offset, .. } => self.parse_object_window(offset).map(Some),
533            XrefEntry::Compressed {
534                stream_number,
535                index,
536            } => {
537                let Some((_, stream_entry)) = self.xref.get(&stream_number) else {
538                    return Err(PdfError::Parse {
539                        offset: stream_number as u64,
540                        message: "missing object stream".to_string(),
541                    });
542                };
543                let stream_obj =
544                    self.resolve_entry(ObjectId::new(stream_number, 0), *stream_entry, depth + 1)?;
545                let Some(PdfObject::Stream(stream)) = stream_obj else {
546                    return Ok(None);
547                };
548                let data = self.parser.decode_stream(&stream)?;
549                let n = stream.dictionary.get_integer("N").unwrap_or(0) as usize;
550                let first = stream.dictionary.get_integer("First").unwrap_or(0) as usize;
551                let (_, body) = parse_object_stream(&data, n, first, index, self.parser.limits)
552                    .ok_or_else(|| PdfError::Parse {
553                        offset: index as u64,
554                        message: "object not found in object stream".to_string(),
555                    })?;
556                Ok(Some(body))
557            }
558        }
559    }
560
561    /// Walks the xref chain the same way [`Parser::parse`] does, but with
562    /// windowed reads so only the xref/trailer bytes are pulled from disk.
563    fn walk_xref(parser: &Parser, file: &std::fs::File, file_len: u64) -> PdfResult<XrefInfo> {
564        let mut version = String::new();
565        {
566            let head = read_at(file, file_len, 0, 1024)?;
567            if let Some(eol) = head.iter().position(|&b| b == b'\n' || b == b'\r') {
568                if head.starts_with(b"%PDF-") {
569                    version = String::from_utf8_lossy(&head[5..eol]).trim().to_string();
570                }
571            }
572        }
573
574        let mut offset = find_startxref_windowed(file, file_len)?;
575        let mut xref: BTreeMap<u32, (u16, XrefEntry)> = BTreeMap::new();
576        let mut trailer: Option<PdfDictionary> = None;
577        loop {
578            let (section, dict) = parse_growing(file, file_len, offset, |buf| {
579                let mut lx = Lexer::new(buf, 0, parser.mode, parser.limits);
580                lx.skip_ws();
581                if lx.match_kw(b"xref") {
582                    return parser.parse_classic_xref(&mut lx);
583                }
584                let obj = parser.parse_object_at(buf, 0)?;
585                match obj {
586                    PdfObject::Stream(stream) => {
587                        let dict = stream.dictionary.clone();
588                        if dict.get_name("Type").map(|n| n.as_str()) != Some("XRef") {
589                            return Err(PdfError::Parse {
590                                offset: 0,
591                                message: "expected /Type /XRef in xref stream".to_string(),
592                            });
593                        }
594                        let data = parser.decode_stream(&stream)?;
595                        let entries = parse_xref_stream_entries(&data, &dict, 0)?;
596                        Ok((entries, Some(dict)))
597                    }
598                    _ => Err(PdfError::Parse {
599                        offset: 0,
600                        message: "expected xref keyword or XRef stream".to_string(),
601                    }),
602                }
603            })?;
604            for (number, entry) in section {
605                xref.entry(number).or_insert(entry);
606            }
607            if xref.len() > parser.limits.max_objects {
608                return Err(PdfError::Parse {
609                    offset,
610                    message: format!(
611                        "object count {} exceeds limit {}",
612                        xref.len(),
613                        parser.limits.max_objects
614                    ),
615                });
616            }
617            if dict.is_some() {
618                trailer = dict;
619            }
620            let prev = trailer
621                .as_ref()
622                .and_then(|d| d.get_integer("Prev"))
623                .filter(|p| *p > 0 && (*p as u64) < offset);
624            match prev {
625                Some(p) => offset = p as u64,
626                None => break,
627            }
628        }
629
630        let (catalog, info) = if let Some(t) = &trailer {
631            (
632                t.get("Root").and_then(|o| o.as_reference()),
633                t.get("Info").and_then(|o| o.as_reference()),
634            )
635        } else {
636            (None, None)
637        };
638        Ok(XrefInfo {
639            version,
640            xref,
641            catalog,
642            info,
643        })
644    }
645
646    /// Parses the object at `offset`, growing the read window until the
647    /// object parses or the file ends.
648    fn parse_object_window(&self, offset: u64) -> PdfResult<PdfObject> {
649        parse_growing(&self.file, self.file_len, offset, |buf| {
650            self.parser.parse_object_at(buf, 0)
651        })
652    }
653}
654
655/// Attempts `f` on progressively larger windows starting at `offset` (64KiB,
656/// doubling until the file end), passing each window as a slice. A window
657/// truncated mid-object makes the lexer error, which triggers the next grow.
658fn parse_growing<R>(
659    file: &std::fs::File,
660    file_len: u64,
661    offset: u64,
662    f: impl Fn(&[u8]) -> PdfResult<R>,
663) -> PdfResult<R> {
664    let mut len = 64 * 1024u64;
665    loop {
666        let buf = read_at(file, file_len, offset, len)?;
667        match f(&buf) {
668            Ok(r) => return Ok(r),
669            Err(e) if (buf.len() as u64) < file_len.saturating_sub(offset) => {
670                if len >= file_len {
671                    return Err(e);
672                }
673                len = len.saturating_mul(4);
674            }
675            Err(e) => return Err(e),
676        }
677    }
678}
679
680/// Reads `len` bytes at `offset`, clamped to the file end.
681fn read_at(mut file: &std::fs::File, file_len: u64, offset: u64, len: u64) -> PdfResult<Vec<u8>> {
682    use std::io::{Read, Seek, SeekFrom};
683    let end = offset.saturating_add(len).min(file_len);
684    if end <= offset {
685        return Ok(Vec::new());
686    }
687    let mut buf = vec![0u8; (end - offset) as usize];
688    file.seek(SeekFrom::Start(offset))?;
689    file.read_exact(&mut buf)?;
690    Ok(buf)
691}
692
693/// Finds the `startxref` keyword by scanning backwards from the file end in
694/// growing windows and returns the offset it points to. A candidate is
695/// accepted only if an offset number follows it (guards against `startxref`
696/// inside comments).
697fn find_startxref_windowed(file: &std::fs::File, file_len: u64) -> PdfResult<u64> {
698    let mut chunk = 64 * 1024u64;
699    loop {
700        let start = file_len.saturating_sub(chunk);
701        let buf = read_at(file, file_len, start, chunk)?;
702        if let Ok(i) = find_startxref(&buf) {
703            let abs = start + i;
704            let tail = read_at(file, file_len, abs, 16)?;
705            let mut lx = Lexer::new(&tail, 0, ParseMode::Lenient, ParserLimits::default());
706            lx.skip_ws();
707            lx.match_kw(b"startxref");
708            if let Ok(n) = lx.parse_unsigned() {
709                return Ok(n);
710            }
711        }
712        if start == 0 {
713            return Err(PdfError::Parse {
714                offset: 0,
715                message: "no startxref keyword found".to_string(),
716            });
717        }
718        chunk = chunk.saturating_mul(4);
719    }
720}
721
722impl Default for Parser {
723    fn default() -> Self {
724        Self::new()
725    }
726}
727
728/// Decodes an /ObjStm stream body and returns the object at `index`.
729fn parse_object_stream(
730    data: &[u8],
731    n: usize,
732    first: usize,
733    index: u32,
734    limits: ParserLimits,
735) -> Option<(u32, PdfObject)> {
736    let mut header_lexer = Lexer::new(data, 0, ParseMode::Lenient, limits);
737    let mut pairs = Vec::with_capacity(n.min(limits.max_objects));
738    for _ in 0..n.min(limits.max_objects) {
739        let num = header_lexer.parse_unsigned().ok()?;
740        let off = header_lexer.parse_unsigned().ok()?;
741        pairs.push((num as u32, off as usize));
742    }
743    let (num, off) = *pairs.get(index as usize)?;
744    let mut body = Lexer::new(data, first + off, ParseMode::Lenient, limits);
745    let obj = body.parse_object().ok()?;
746    Some((num, obj))
747}
748
749/// Parses an XRef stream's entry data given the `/W` widths and `/Index` pairs.
750fn parse_xref_stream_entries(
751    data: &[u8],
752    dict: &PdfDictionary,
753    offset: u64,
754) -> PdfResult<BTreeMap<u32, (u16, XrefEntry)>> {
755    let w = dict.get_array("W").ok_or_else(|| PdfError::Parse {
756        offset,
757        message: "xref stream missing /W".to_string(),
758    })?;
759    let mut widths = Vec::new();
760    for item in w.0.iter() {
761        let i = item.as_integer().ok_or_else(|| PdfError::Parse {
762            offset,
763            message: "invalid /W entry".to_string(),
764        })?;
765        widths.push(i as usize);
766    }
767    let first = match dict.get_array("Index") {
768        Some(arr) => {
769            let mut out = Vec::new();
770            let mut it = arr.0.iter();
771            while let Some(f) = it.next() {
772                let count = it.next().ok_or_else(|| PdfError::Parse {
773                    offset,
774                    message: "invalid /Index".to_string(),
775                })?;
776                out.push((
777                    f.as_integer().unwrap_or(0) as u32,
778                    count.as_integer().unwrap_or(0) as u32,
779                ));
780            }
781            out
782        }
783        None => vec![(0, dict.get_integer("Size").unwrap_or(0) as u32)],
784    };
785
786    let record = widths[0] + widths[1] + widths[2];
787    if record == 0 {
788        return Ok(BTreeMap::new());
789    }
790    let mut entries = BTreeMap::new();
791    let mut pos = 0usize;
792    let field = |offset: usize, width: usize| -> u64 {
793        if width == 0 || offset + width > data.len() {
794            return 0;
795        }
796        let mut v: u64 = 0;
797        for &b in &data[offset..offset + width] {
798            v = (v << 8) | b as u64;
799        }
800        v
801    };
802    for (first, count) in first {
803        for i in 0..count {
804            if pos + record > data.len() {
805                break;
806            }
807            let typ = field(pos, widths[0]);
808            let f2 = field(pos + widths[0], widths[1]);
809            let f3 = field(pos + widths[0] + widths[1], widths[2]);
810            pos += record;
811            let number = first + i;
812            match typ {
813                0 => {
814                    entries.insert(
815                        number,
816                        (
817                            f3 as u16,
818                            XrefEntry::Free {
819                                generation: f3 as u16,
820                            },
821                        ),
822                    );
823                }
824                1 => {
825                    entries.insert(
826                        number,
827                        (
828                            f3 as u16,
829                            XrefEntry::Used {
830                                offset: f2,
831                                generation: f3 as u16,
832                            },
833                        ),
834                    );
835                }
836                2 => {
837                    entries.insert(
838                        number,
839                        (
840                            0,
841                            XrefEntry::Compressed {
842                                stream_number: f2 as u32,
843                                index: f3 as u32,
844                            },
845                        ),
846                    );
847                }
848                _ => {}
849            }
850        }
851    }
852    Ok(entries)
853}
854
855fn find_startxref(input: &[u8]) -> PdfResult<u64> {
856    let needle = b"startxref";
857    let mut search = input.len();
858    loop {
859        let end = input[..search].len();
860        let idx = input[..end]
861            .windows(needle.len())
862            .rposition(|w| w == needle);
863        let i = match idx {
864            Some(i) => i,
865            None => {
866                return Err(PdfError::Parse {
867                    offset: 0,
868                    message: "no startxref keyword found".to_string(),
869                });
870            }
871        };
872        let prev_ok = i == 0 || matches!(input[i - 1], b' ' | b'\t' | b'\r' | b'\n' | b'\x0c' | 0);
873        let next = input.get(i + needle.len()).copied();
874        let next_ok = next
875            .map(|b| matches!(b, b' ' | b'\t' | b'\r' | b'\n' | b'\x0c' | 0))
876            .unwrap_or(true);
877        if prev_ok && next_ok {
878            return Ok(i as u64);
879        }
880        search = i;
881        if search == 0 {
882            break;
883        }
884    }
885    Err(PdfError::Parse {
886        offset: 0,
887        message: "no startxref keyword found".to_string(),
888    })
889}
890
891struct Lexer<'a> {
892    data: &'a [u8],
893    pos: usize,
894    strict: bool,
895    max_depth: u32,
896    depth: u32,
897    limits: ParserLimits,
898}
899
900impl<'a> Lexer<'a> {
901    fn new(data: &'a [u8], pos: usize, mode: ParseMode, limits: ParserLimits) -> Self {
902        Self {
903            data,
904            pos,
905            strict: mode == ParseMode::Strict,
906            max_depth: limits.max_recursion_depth,
907            depth: 0,
908            limits,
909        }
910    }
911
912    fn is_ws(b: u8) -> bool {
913        matches!(b, b' ' | b'\t' | b'\r' | b'\n' | 0x0c | 0)
914    }
915
916    fn is_delim(b: u8) -> bool {
917        matches!(
918            b,
919            b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
920        )
921    }
922
923    fn peek(&self) -> Option<u8> {
924        self.data.get(self.pos).copied()
925    }
926
927    fn bump(&mut self) -> Option<u8> {
928        let b = self.peek();
929        if b.is_some() {
930            self.pos += 1;
931        }
932        b
933    }
934
935    fn err(&self, message: impl Into<String>) -> PdfError {
936        PdfError::Parse {
937            offset: self.pos as u64,
938            message: message.into(),
939        }
940    }
941
942    fn skip_ws(&mut self) {
943        loop {
944            while self
945                .data
946                .get(self.pos)
947                .copied()
948                .map(Self::is_ws)
949                .unwrap_or(false)
950            {
951                self.pos += 1;
952            }
953            if self.data.get(self.pos) == Some(&b'%') {
954                while self.pos < self.data.len() && self.data[self.pos] != b'\n' {
955                    self.pos += 1;
956                }
957            } else {
958                break;
959            }
960        }
961    }
962
963    fn match_kw(&mut self, kw: &[u8]) -> bool {
964        // The lexer position can point past the end of the buffer when a
965        // corrupted xref points at a bogus offset; slicing must not panic.
966        if self
967            .data
968            .get(self.pos..)
969            .is_some_and(|rest| rest.starts_with(kw))
970        {
971            let after = self.data.get(self.pos + kw.len()).copied().unwrap_or(b' ');
972            if Self::is_ws(after) || Self::is_delim(after) {
973                self.pos += kw.len();
974                return true;
975            }
976        }
977        false
978    }
979
980    fn parse_unsigned(&mut self) -> PdfResult<u64> {
981        self.skip_ws();
982        let start = self.pos;
983        while self
984            .data
985            .get(self.pos)
986            .map(|b| b.is_ascii_digit())
987            .unwrap_or(false)
988        {
989            self.pos += 1;
990        }
991        if start == self.pos {
992            return Err(self.err("expected number"));
993        }
994        let mut value: u64 = 0;
995        for &b in &self.data[start..self.pos] {
996            value = match value
997                .checked_mul(10)
998                .and_then(|v| v.checked_add(u64::from(b - b'0')))
999            {
1000                Some(v) => v,
1001                None => return Err(self.err("number out of range")),
1002            };
1003        }
1004        Ok(value)
1005    }
1006
1007    fn parse_number_object(&mut self) -> PdfResult<PdfObject> {
1008        self.skip_ws();
1009        let start = self.pos;
1010        while self
1011            .data
1012            .get(self.pos)
1013            .is_some_and(|b| b.is_ascii_digit() || matches!(b, b'+' | b'-' | b'.' | b'e' | b'E'))
1014        {
1015            self.pos += 1;
1016        }
1017        if start == self.pos {
1018            return Err(self.err("expected number"));
1019        }
1020        let tok = &self.data[start..self.pos];
1021        if !tok.contains(&b'.') && !tok.contains(&b'e') && !tok.contains(&b'E') {
1022            // Integer fast path: hand-rolled decimal accumulation that avoids
1023            // UTF-8 validation and `str::parse` machinery.
1024            let (negative, digits) = match tok.first() {
1025                Some(b'-') => (true, &tok[1..]),
1026                Some(b'+') => (false, &tok[1..]),
1027                _ => (false, tok),
1028            };
1029            // Only take the fast path for well-formed digits; anything else
1030            // (e.g. a lone sign or `++5`) falls through to the f64 parser,
1031            // which reports an error just like `str::parse` did before.
1032            if !digits.is_empty() && digits.iter().all(u8::is_ascii_digit) {
1033                // Accumulate as a negative value so i64::MIN parses without
1034                // overflow.
1035                let mut acc: i64 = 0;
1036                let mut overflow = false;
1037                for &b in digits {
1038                    match acc
1039                        .checked_mul(10)
1040                        .and_then(|v| v.checked_sub(i64::from(b - b'0')))
1041                    {
1042                        Some(v) => acc = v,
1043                        None => {
1044                            overflow = true;
1045                            break;
1046                        }
1047                    }
1048                }
1049                if !overflow {
1050                    let value = if negative {
1051                        Some(acc)
1052                    } else {
1053                        acc.checked_neg()
1054                    };
1055                    if let Some(value) = value {
1056                        return Ok(PdfObject::Integer(value));
1057                    }
1058                }
1059                // Out-of-i64-range digit strings (e.g. a serialized 1e20) are
1060                // legal PDF numbers; fall through to the f64 parser instead of
1061                // failing, matching real-world tolerant readers.
1062            }
1063        }
1064        let s = std::str::from_utf8(tok).map_err(|_| self.err("invalid number bytes"))?;
1065        let f: f64 = s
1066            .parse()
1067            .map_err(|_| self.err(format!("invalid number `{s}`")))?;
1068        Ok(PdfObject::Real(f))
1069    }
1070
1071    fn parse_name(&mut self) -> PdfResult<PdfName> {
1072        if self.peek() != Some(b'/') {
1073            return Err(self.err("expected name"));
1074        }
1075        self.bump();
1076        let mut bytes = Vec::with_capacity(8);
1077        loop {
1078            match self.peek() {
1079                None => break,
1080                Some(b) if Self::is_ws(b) || Self::is_delim(b) => break,
1081                Some(b'#') => {
1082                    self.bump();
1083                    let hex = [
1084                        self.bump().ok_or_else(|| self.err("truncated #-escape"))?,
1085                        self.bump().ok_or_else(|| self.err("truncated #-escape"))?,
1086                    ];
1087                    let pair = std::str::from_utf8(&hex).map_err(|_| self.err("bad #-escape"))?;
1088                    let v = u8::from_str_radix(pair, 16).map_err(|_| self.err("bad #-escape"))?;
1089                    bytes.push(v);
1090                }
1091                Some(b) => {
1092                    self.bump();
1093                    bytes.push(b);
1094                }
1095            }
1096        }
1097        // Fast path: valid UTF-8 bytes convert directly; only malformed names
1098        // fall back to lossy conversion.
1099        let name = match String::from_utf8(bytes) {
1100            Ok(s) => s,
1101            Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
1102        };
1103        Ok(PdfName::new(&name))
1104    }
1105
1106    fn enter_nested(&mut self) -> PdfResult<()> {
1107        self.depth += 1;
1108        if self.depth > self.max_depth {
1109            return Err(self.err("recursion limit exceeded"));
1110        }
1111        Ok(())
1112    }
1113
1114    fn exit_nested(&mut self) {
1115        self.depth = self.depth.saturating_sub(1);
1116    }
1117
1118    fn parse_literal_string(&mut self) -> PdfResult<PdfString> {
1119        self.bump(); // '('
1120        let mut out = Vec::new();
1121        let mut depth = 1u32;
1122        loop {
1123            if out.len() >= self.limits.max_string_length {
1124                return Err(self.err("string length exceeds limit"));
1125            }
1126            match self.bump() {
1127                None => return Err(self.err("unterminated string")),
1128                Some(b'(') => {
1129                    depth += 1;
1130                    out.push(b'(');
1131                }
1132                Some(b')') => {
1133                    depth -= 1;
1134                    if depth == 0 {
1135                        break;
1136                    }
1137                    out.push(b')');
1138                }
1139                Some(b'\\') => match self.bump() {
1140                    None => return Err(self.err("truncated escape")),
1141                    Some(b'n') => out.push(b'\n'),
1142                    Some(b'r') => out.push(b'\r'),
1143                    Some(b't') => out.push(b'\t'),
1144                    Some(b'b') => out.push(8),
1145                    Some(b'f') => out.push(12),
1146                    Some(b'(') => out.push(b'('),
1147                    Some(b')') => out.push(b')'),
1148                    Some(b'\\') => out.push(b'\\'),
1149                    Some(d @ b'0'..=b'7') => {
1150                        let mut v = d - b'0';
1151                        for _ in 0..2 {
1152                            match self.peek() {
1153                                Some(e @ b'0'..=b'7') => {
1154                                    v = v * 8 + (e - b'0');
1155                                    self.bump();
1156                                }
1157                                _ => break,
1158                            }
1159                        }
1160                        out.push(v);
1161                    }
1162                    Some(b'\r') => {
1163                        if self.peek() == Some(b'\n') {
1164                            self.bump();
1165                        }
1166                    }
1167                    Some(b'\n') => {}
1168                    Some(other) => out.push(other),
1169                },
1170                Some(b'\r') => {
1171                    if self.peek() == Some(b'\n') {
1172                        self.bump();
1173                    }
1174                    out.push(b'\n');
1175                }
1176                Some(b) => out.push(b),
1177            }
1178        }
1179        Ok(PdfString(out))
1180    }
1181
1182    fn parse_hex_string(&mut self) -> PdfResult<PdfString> {
1183        self.bump(); // '<'
1184        let mut out = Vec::new();
1185        let mut hi: Option<u8> = None;
1186        loop {
1187            if out.len() >= self.limits.max_string_length {
1188                return Err(self.err("string length exceeds limit"));
1189            }
1190            match self.bump() {
1191                None => return Err(self.err("unterminated hex string")),
1192                Some(b'>') => {
1193                    if let Some(h) = hi {
1194                        out.push(h << 4);
1195                    }
1196                    break;
1197                }
1198                Some(b) if b.is_ascii_whitespace() => {}
1199                Some(b) => {
1200                    let v = match b {
1201                        b'0'..=b'9' => b - b'0',
1202                        b'a'..=b'f' => b - b'a' + 10,
1203                        b'A'..=b'F' => b - b'A' + 10,
1204                        _ => return Err(self.err("invalid hex digit")),
1205                    };
1206                    match hi {
1207                        None => hi = Some(v),
1208                        Some(h) => {
1209                            out.push((h << 4) | v);
1210                            hi = None;
1211                        }
1212                    }
1213                }
1214            }
1215        }
1216        Ok(PdfString(out))
1217    }
1218
1219    fn parse_array(&mut self) -> PdfResult<PdfArray> {
1220        self.bump(); // '['
1221        self.enter_nested()?;
1222        let mut arr = PdfArray::new();
1223        loop {
1224            self.skip_ws();
1225            match self.peek() {
1226                None => return Err(self.err("unterminated array")),
1227                Some(b']') => {
1228                    self.bump();
1229                    self.exit_nested();
1230                    break;
1231                }
1232                _ => {
1233                    if arr.len() >= self.limits.max_array_length {
1234                        return Err(self.err("array length exceeds limit"));
1235                    }
1236                    arr.push(self.parse_object()?);
1237                }
1238            }
1239        }
1240        Ok(arr)
1241    }
1242
1243    fn parse_dict(&mut self) -> PdfResult<PdfDictionary> {
1244        self.bump();
1245        self.bump(); // <<
1246        self.enter_nested()?;
1247        let mut dict = PdfDictionary::new();
1248        loop {
1249            self.skip_ws();
1250            match self.peek() {
1251                None => return Err(self.err("unterminated dictionary")),
1252                Some(b'>') => {
1253                    if self.data.get(self.pos + 1) == Some(&b'>') {
1254                        self.bump();
1255                        self.bump();
1256                        self.exit_nested();
1257                        break;
1258                    }
1259                    return Err(self.err("single `>` inside dictionary"));
1260                }
1261                Some(b'/') => {
1262                    if dict.len() >= self.limits.max_dict_entries {
1263                        return Err(self.err("dictionary entry count exceeds limit"));
1264                    }
1265                    let key = self.parse_name()?;
1266                    self.skip_ws();
1267                    let val = self.parse_object()?;
1268                    dict.insert(key.as_str(), val);
1269                }
1270                _ => return Err(self.err("expected /Name in dictionary")),
1271            }
1272        }
1273        Ok(dict)
1274    }
1275
1276    fn parse_object(&mut self) -> PdfResult<PdfObject> {
1277        self.skip_ws();
1278        match self.peek() {
1279            None => Err(self.err("expected object")),
1280            Some(b'[') => Ok(PdfObject::Array(self.parse_array()?)),
1281            Some(b'<') => {
1282                if self.data.get(self.pos + 1) == Some(&b'<') {
1283                    let dict = self.parse_dict()?;
1284                    self.skip_ws();
1285                    if self.match_kw(b"stream") {
1286                        if self.peek() == Some(b'\r') {
1287                            self.bump();
1288                        }
1289                        if self.peek() == Some(b'\n') {
1290                            self.bump();
1291                        }
1292                        let len = dict
1293                            .get_integer("Length")
1294                            .filter(|l| *l >= 0)
1295                            .ok_or_else(|| self.err("stream missing valid /Length"))?
1296                            as usize;
1297                        if len > self.limits.max_stream_size {
1298                            return Err(self.err("stream length exceeds limit"));
1299                        }
1300                        if self.pos + len > self.data.len() {
1301                            return Err(self.err("stream /Length exceeds file"));
1302                        }
1303                        let data = self.data[self.pos..self.pos + len].to_vec();
1304                        self.pos += len;
1305                        self.skip_ws();
1306                        if self.strict && !self.match_kw(b"endstream") {
1307                            return Err(self.err("expected `endstream`"));
1308                        }
1309                        Ok(PdfObject::Stream(PdfStream::with_dict(dict, data)))
1310                    } else {
1311                        Ok(PdfObject::Dictionary(dict))
1312                    }
1313                } else {
1314                    Ok(PdfObject::String(self.parse_hex_string()?))
1315                }
1316            }
1317            Some(b'(') => Ok(PdfObject::String(self.parse_literal_string()?)),
1318            Some(b'/') => Ok(PdfObject::Name(self.parse_name()?)),
1319            Some(b'+') | Some(b'-') | Some(b'.') | Some(b'0'..=b'9') => {
1320                let first = self.parse_number_object()?;
1321                self.skip_ws();
1322                let next_is_number = matches!(
1323                    self.peek(),
1324                    Some(b'+') | Some(b'-') | Some(b'.') | Some(b'0'..=b'9')
1325                );
1326                if next_is_number {
1327                    let before_second = self.pos;
1328                    let second = self.parse_number_object()?;
1329                    self.skip_ws();
1330                    if self.peek() == Some(b'R') {
1331                        self.bump();
1332                        let num = first
1333                            .as_integer()
1334                            .ok_or_else(|| self.err("non-integer object number in reference"))?
1335                            as u32;
1336                        let gen = second
1337                            .as_integer()
1338                            .ok_or_else(|| self.err("non-integer generation in reference"))?
1339                            as u16;
1340                        return Ok(PdfObject::Reference(ObjectId::new(num, gen)));
1341                    }
1342                    // Not a reference: leave the second number for the next call.
1343                    self.pos = before_second;
1344                }
1345                Ok(first)
1346            }
1347            Some(b'T' | b't' | b'F' | b'f' | b'N' | b'n') => {
1348                for kw in [b"true".as_slice(), b"True".as_slice(), b"TRUE".as_slice()] {
1349                    if self.match_kw(kw) {
1350                        return Ok(PdfObject::Boolean(true));
1351                    }
1352                }
1353                for kw in [
1354                    b"false".as_slice(),
1355                    b"False".as_slice(),
1356                    b"FALSE".as_slice(),
1357                ] {
1358                    if self.match_kw(kw) {
1359                        return Ok(PdfObject::Boolean(false));
1360                    }
1361                }
1362                for kw in [b"null".as_slice(), b"Null".as_slice(), b"NULL".as_slice()] {
1363                    if self.match_kw(kw) {
1364                        return Ok(PdfObject::Null);
1365                    }
1366                }
1367                Err(self.err("unknown keyword"))
1368            }
1369            Some(_) => Err(self.err("unexpected token")),
1370        }
1371    }
1372}
1373
1374#[cfg(test)]
1375mod tests {
1376    use super::*;
1377    use crate::serializer::Serializer;
1378
1379    #[test]
1380    fn roundtrip_via_serializer() {
1381        let mut doc = Document::new();
1382        doc.set_catalog(ObjectId::new(1, 0));
1383        doc.set_info(ObjectId::new(4, 0));
1384        let mut catalog = PdfDictionary::new();
1385        catalog.insert("Type", PdfObject::Name(PdfName::new("Catalog")));
1386        catalog.insert("Pages", PdfObject::Reference(ObjectId::new(2, 0)));
1387        doc.add_object(ObjectId::new(1, 0), PdfObject::Dictionary(catalog));
1388        let mut pages = PdfDictionary::new();
1389        pages.insert("Type", PdfObject::Name(PdfName::new("Pages")));
1390        pages.insert("Count", PdfObject::Integer(1));
1391        let mut kids = PdfArray::new();
1392        kids.push(PdfObject::Reference(ObjectId::new(3, 0)));
1393        pages.insert("Kids", PdfObject::Array(kids));
1394        doc.add_object(ObjectId::new(2, 0), PdfObject::Dictionary(pages));
1395        let mut page = PdfDictionary::new();
1396        page.insert("Type", PdfObject::Name(PdfName::new("Page")));
1397        let mut media = PdfArray::new();
1398        media.push(PdfObject::Integer(0));
1399        media.push(PdfObject::Integer(0));
1400        media.push(PdfObject::Integer(612));
1401        media.push(PdfObject::Integer(792));
1402        page.insert("MediaBox", PdfObject::Array(media));
1403        doc.add_object(ObjectId::new(3, 0), PdfObject::Dictionary(page));
1404        let mut info = PdfDictionary::new();
1405        info.insert("Title", PdfObject::String(PdfString::from_literal("t")));
1406        doc.add_object(ObjectId::new(4, 0), PdfObject::Dictionary(info));
1407
1408        let mut buf = std::io::Cursor::new(Vec::new());
1409        Serializer::new().serialize(&doc, &mut buf).unwrap();
1410        let parsed = Parser::new().parse(buf.get_ref()).unwrap();
1411
1412        assert_eq!(parsed.catalog(), Some(ObjectId::new(1, 0)));
1413        assert_eq!(parsed.info(), Some(ObjectId::new(4, 0)));
1414        assert_eq!(parsed.objects().len(), 4);
1415        let media_data = parsed
1416            .get_object(ObjectId::new(3, 0))
1417            .unwrap()
1418            .as_dict()
1419            .unwrap()
1420            .get_array("MediaBox")
1421            .unwrap()
1422            .clone();
1423        assert_eq!(
1424            media_data.0,
1425            vec![
1426                PdfObject::Integer(0),
1427                PdfObject::Integer(0),
1428                PdfObject::Integer(612),
1429                PdfObject::Integer(792),
1430            ]
1431        );
1432    }
1433
1434    #[test]
1435    fn string_escaping_survives_serialize_parse_roundtrip() {
1436        let cases: &[&[u8]] = &[
1437            b"plain",
1438            b"with (parens) and \\ backslash",
1439            b"line1\nline2\r\nline3\rline4",
1440            "café ☕ 漢字 𝄞".as_bytes(),
1441            &[0x01, 0x07, 0x0b, 0x1b, 0x7f], // control bytes, octal escapes
1442            b"tab\there\t",
1443            b"",
1444        ];
1445        for &input in cases {
1446            let mut doc = Document::new();
1447            doc.set_catalog(ObjectId::new(1, 0));
1448            let mut dict = PdfDictionary::new();
1449            dict.insert("S", PdfObject::String(PdfString::from_bytes(input)));
1450            dict.insert("N", PdfObject::Real(1.5));
1451            doc.add_object(ObjectId::new(1, 0), PdfObject::Dictionary(dict));
1452
1453            let mut buf = std::io::Cursor::new(Vec::new());
1454            Serializer::new().serialize(&doc, &mut buf).unwrap();
1455            let parsed = Parser::new().parse(buf.get_ref()).unwrap();
1456            let round = parsed
1457                .get_object(ObjectId::new(1, 0))
1458                .unwrap()
1459                .as_dict()
1460                .unwrap()
1461                .get_string_bytes("S")
1462                .unwrap();
1463            assert_eq!(round, input, "roundtrip failed for {input:?}");
1464        }
1465    }
1466
1467    #[test]
1468    fn text_operator_escaping_roundtrips_tj_string() {
1469        // Content-stream Tj strings hold the same escaping rules as object strings.
1470        let tricky = vec![
1471            b"(a)".to_vec(),
1472            b"line1\nline2".to_vec(),
1473            "café ☕".as_bytes().to_vec(),
1474            b"\\back\\slash".to_vec(),
1475        ];
1476        for text in tricky {
1477            let mut doc = Document::new();
1478            doc.set_catalog(ObjectId::new(1, 0));
1479            let mut dict = PdfDictionary::new();
1480            dict.insert("Length", PdfObject::Integer(text.len() as i64));
1481            let stream = PdfStream::with_dict(dict, text.clone());
1482            doc.add_object(ObjectId::new(1, 0), PdfObject::Stream(stream));
1483
1484            let mut buf = std::io::Cursor::new(Vec::new());
1485            Serializer::new().serialize(&doc, &mut buf).unwrap();
1486            let parsed = Parser::new().parse(buf.get_ref()).unwrap();
1487            let round = parsed
1488                .get_object(ObjectId::new(1, 0))
1489                .unwrap()
1490                .as_stream()
1491                .unwrap()
1492                .data
1493                .clone();
1494            assert_eq!(round, text, "stream roundtrip failed for {text:?}");
1495        }
1496    }
1497
1498    #[test]
1499    fn parses_xref_stream_entries() {
1500        let mut dict = PdfDictionary::new();
1501        dict.insert("Type", PdfObject::Name(PdfName::new("XRef")));
1502        dict.insert("Size", PdfObject::Integer(4));
1503        let mut w = PdfArray::new();
1504        w.push(PdfObject::Integer(1));
1505        w.push(PdfObject::Integer(4));
1506        w.push(PdfObject::Integer(2));
1507        dict.insert("W", PdfObject::Array(w));
1508
1509        let mut data = Vec::new();
1510        data.extend_from_slice(&[0, 0, 0, 0, 0, 0xff, 0xff]);
1511        data.extend_from_slice(&[1, 0, 0, 0, 15, 0, 0]);
1512        data.extend_from_slice(&[2, 0, 0, 0, 10, 0, 3]);
1513        data.extend_from_slice(&[1, 0, 0, 3, 232, 0, 0]);
1514
1515        let entries = parse_xref_stream_entries(&data, &dict, 0).unwrap();
1516        assert_eq!(entries.len(), 4);
1517        assert!(matches!(
1518            entries[&0].1,
1519            XrefEntry::Free { generation: 65535 }
1520        ));
1521        assert!(matches!(
1522            entries[&1].1,
1523            XrefEntry::Used {
1524                offset: 15,
1525                generation: 0
1526            }
1527        ));
1528        assert!(matches!(
1529            entries[&2].1,
1530            XrefEntry::Compressed {
1531                stream_number: 10,
1532                index: 3
1533            }
1534        ));
1535        assert!(matches!(
1536            entries[&3].1,
1537            XrefEntry::Used {
1538                offset: 1000,
1539                generation: 0
1540            }
1541        ));
1542    }
1543
1544    #[test]
1545    fn decodes_flate_streams() {
1546        let plain = b"BT /F1 12 Tf 10 20 Td (hi) Tj ET";
1547        let mut dict = PdfDictionary::new();
1548        dict.insert("Filter", PdfObject::Name(PdfName::new("FlateDecode")));
1549        let encoded = crate::stream::StreamEncoder::new()
1550            .encode(plain, crate::stream::StreamFilter::Flate)
1551            .unwrap();
1552        let stream = PdfStream::with_dict(dict, encoded);
1553        let decoded = Parser::new().decode_stream(&stream).unwrap();
1554        assert_eq!(decoded, plain);
1555    }
1556
1557    #[test]
1558    fn parses_object_stream_objects() {
1559        let mut data = Vec::new();
1560        data.extend_from_slice(b"7 0 8 6");
1561        while data.len() < 12 {
1562            data.push(b' ');
1563        }
1564        data.extend_from_slice(b"42 ");
1565        while data.len() < 18 {
1566            data.push(b' ');
1567        }
1568        data.extend_from_slice(b"<< /A (x) >>");
1569
1570        let (num, obj) = parse_object_stream(&data, 2, 12, 0, ParserLimits::default()).unwrap();
1571        assert_eq!(num, 7);
1572        assert_eq!(obj, PdfObject::Integer(42));
1573
1574        let (num, obj) = parse_object_stream(&data, 2, 12, 1, ParserLimits::default()).unwrap();
1575        assert_eq!(num, 8);
1576        assert_eq!(
1577            obj.as_dict().unwrap().get("A"),
1578            Some(&PdfObject::String(PdfString::from_literal("x")))
1579        );
1580        assert!(parse_object_stream(&data, 2, 12, 9, ParserLimits::default()).is_none());
1581    }
1582
1583    #[test]
1584    fn parses_strings_names_and_numbers() {
1585        let mut lx = Lexer::new(
1586            b"(a\\(b\\)c) /A#20B 42 -1.5e2 true null <48656c6c6f>",
1587            0,
1588            ParseMode::Strict,
1589            ParserLimits::default(),
1590        );
1591        assert_eq!(
1592            lx.parse_object().unwrap(),
1593            PdfObject::String(PdfString::from_bytes(&b"a(b)c"[..]))
1594        );
1595        lx.skip_ws();
1596        assert_eq!(
1597            lx.parse_object().unwrap(),
1598            PdfObject::Name(PdfName::new("A B"))
1599        );
1600        lx.skip_ws();
1601        assert_eq!(lx.parse_object().unwrap(), PdfObject::Integer(42));
1602        lx.skip_ws();
1603        match lx.parse_object().unwrap() {
1604            PdfObject::Real(f) => assert!((f - -150.0).abs() < 0.001),
1605            _ => panic!("expected real"),
1606        }
1607        lx.skip_ws();
1608        assert_eq!(lx.parse_object().unwrap(), PdfObject::Boolean(true));
1609        lx.skip_ws();
1610        assert_eq!(lx.parse_object().unwrap(), PdfObject::Null);
1611        lx.skip_ws();
1612        assert_eq!(
1613            lx.parse_object().unwrap(),
1614            PdfObject::String(PdfString::from_bytes(&b"Hello"[..]))
1615        );
1616    }
1617
1618    fn tight_limits() -> ParserLimits {
1619        ParserLimits {
1620            max_objects: 4,
1621            max_string_length: 8,
1622            max_array_length: 4,
1623            max_dict_entries: 4,
1624            max_recursion_depth: 3,
1625            max_stream_size: 16,
1626            max_decoded_stream_size: 16,
1627        }
1628    }
1629
1630    #[test]
1631    fn limits_reject_overlong_string() {
1632        let mut lx = Lexer::new(b"(0123456789)", 0, ParseMode::Strict, tight_limits());
1633        assert!(lx.parse_object().is_err());
1634    }
1635
1636    #[test]
1637    fn limits_reject_deep_nesting() {
1638        let deep = b"[[[[[[[[[0]]]]]]]]]";
1639        let mut lx = Lexer::new(deep, 0, ParseMode::Strict, tight_limits());
1640        assert!(lx.parse_object().is_err());
1641    }
1642
1643    #[test]
1644    fn limits_reject_huge_array() {
1645        let mut lx = Lexer::new(b"[1 2 3 4 5]", 0, ParseMode::Strict, tight_limits());
1646        assert!(lx.parse_object().is_err());
1647    }
1648
1649    #[test]
1650    fn limits_reject_huge_stream_length() {
1651        // /Length larger than max_stream_size must be rejected before any copy.
1652        let input = b"<< /Length 1000 >>\nstream\n0123456789\nendstream";
1653        let mut lx = Lexer::new(input, 0, ParseMode::Strict, tight_limits());
1654        assert!(lx.parse_object().is_err());
1655    }
1656
1657    #[test]
1658    fn limits_reject_too_many_objects() {
1659        // Build a real PDF with 6 objects; parser with max_objects=4 must refuse.
1660        let mut doc = Document::new();
1661        for i in 1..=6u32 {
1662            doc.add_object(ObjectId::new(i, 0), PdfObject::Integer(i as i64));
1663        }
1664        let mut buf = std::io::Cursor::new(Vec::new());
1665        Serializer::new().serialize(&doc, &mut buf).unwrap();
1666        let limits = ParserLimits {
1667            max_objects: 4,
1668            ..tight_limits()
1669        };
1670        assert!(Parser::with_limits(limits).parse(buf.get_ref()).is_err());
1671    }
1672
1673    #[test]
1674    fn limits_reject_too_many_xref_entries() {
1675        // A classic xref table advertised with more entries than max_objects
1676        // must be stopped while scanning the table, before objects parse.
1677        let mut pdf = Vec::new();
1678        pdf.extend_from_slice(b"%PDF-1.7\n");
1679        pdf.extend_from_slice(b"1 0 obj\n<<>>\nendobj\n");
1680        let xref_at = pdf.len() as u64;
1681        pdf.extend_from_slice(b"xref\n0 6\n");
1682        pdf.extend_from_slice(b"0000000000 65535 f \n");
1683        for i in 1..=5u64 {
1684            pdf.extend_from_slice(format!("{i:010} 00000 n \n").as_bytes());
1685        }
1686        pdf.extend_from_slice(b"trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n");
1687        pdf.extend_from_slice(format!("{xref_at}\n").as_bytes());
1688        pdf.extend_from_slice(b"%%EOF");
1689
1690        let limits = ParserLimits {
1691            max_objects: 4,
1692            ..tight_limits()
1693        };
1694        let err = Parser::with_limits(limits).parse(&pdf).unwrap_err();
1695        assert!(
1696            err.to_string().contains("xref entry count"),
1697            "unexpected error: {err}"
1698        );
1699    }
1700
1701    /// Deterministic xorshift64 PRNG so the test is reproducible.
1702    struct XorShift(u64);
1703
1704    impl XorShift {
1705        fn next(&mut self) -> u64 {
1706            let mut x = self.0;
1707            x ^= x << 13;
1708            x ^= x >> 7;
1709            x ^= x << 17;
1710            self.0 = x;
1711            x
1712        }
1713    }
1714
1715    #[test]
1716    fn random_bytes_never_panic() {
1717        // Stand-in for the libFuzzer target (see `fuzz/`): feed thousands of
1718        // pseudo-random byte strings to the parser and require a Result, never
1719        // a panic. Runs in CI without a nightly toolchain.
1720        let mut rng = XorShift(0x9E37_79B9_7F4A_7C15);
1721        for _ in 0..5000 {
1722            let len = (rng.next() % 512) as usize;
1723            let bytes: Vec<u8> = (0..len).map(|_| (rng.next() & 0xff) as u8).collect();
1724            let _ = Parser::new().parse(&bytes);
1725        }
1726    }
1727
1728    #[test]
1729    fn byte_flipped_pdf_never_panics() {
1730        // A valid PDF with every single byte flipped (plus random mutations)
1731        // exercises parser assumptions the way corpus fuzzing does.
1732        let mut doc = Document::new();
1733        doc.set_catalog(ObjectId::new(1, 0));
1734        for i in 1..=4u32 {
1735            let mut dict = PdfDictionary::new();
1736            dict.insert("Type", PdfObject::Name(PdfName::new(&format!("T{i}"))));
1737            dict.insert("N", PdfObject::Integer(i as i64));
1738            doc.add_object(ObjectId::new(i, 0), PdfObject::Dictionary(dict));
1739        }
1740        let mut buf = std::io::Cursor::new(Vec::new());
1741        Serializer::new().serialize(&doc, &mut buf).unwrap();
1742        let original = buf.into_inner();
1743        let mut rng = XorShift(0xD1B5_4A32_D192_ED03);
1744        for _ in 0..1000 {
1745            let mut mutated = original.clone();
1746            let flips = 1 + (rng.next() % 4) as usize;
1747            for _ in 0..flips {
1748                let idx = (rng.next() as usize) % mutated.len().max(1);
1749                mutated[idx] ^= 1 << (rng.next() % 8);
1750            }
1751            let _ = Parser::new().parse(&mutated);
1752        }
1753    }
1754
1755    #[test]
1756    fn malformed_inputs_error_without_panicking() {
1757        let cases: &[&[u8]] = &[
1758            b"",
1759            b"%PDF-1.7",
1760            b"garbage",
1761            b"%PDF-1.7\n1 0 obj<<>>endobj\nxref\n0 1\ntrailer\nstartxref\n0\n%%EOF",
1762            b"%PDF-1.7\n1 0 obj\n(abc",
1763            b"%PDF-1.7\n1 0 obj\n<< /Length -5 >>\nstream\nx",
1764            &b"%PDF-1.7\n".repeat(2),
1765            &[0xff, 0xfe, 0x00, 0x00, 0x41, 0x42],
1766        ];
1767        for input in cases {
1768            // Must return a Result, never panic.
1769            let _ = Parser::new().parse(input);
1770        }
1771    }
1772
1773    #[test]
1774    fn stream_roundtrip_with_small_and_large_data() {
1775        for size in [0, 1, 15, 16, 17, 1000] {
1776            let payload: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
1777            let mut dict = PdfDictionary::new();
1778            dict.insert("Filter", PdfObject::Name(PdfName::new("FlateDecode")));
1779            let encoded = crate::stream::StreamEncoder::new()
1780                .encode(&payload, crate::stream::StreamFilter::Flate)
1781                .unwrap();
1782            let stream = PdfStream::with_dict(dict, encoded);
1783            let decoded = Parser::new().decode_stream(&stream).unwrap();
1784            assert_eq!(decoded, payload, "size {size} mismatch");
1785        }
1786    }
1787
1788    #[test]
1789    fn decoded_stream_size_is_bounded() {
1790        // A zlib bomb: 1 MB of zeros compresses to ~1 KB, so the decoder cap
1791        // must stop the expansion either by capping or erroring.
1792        let payload = vec![0u8; 1_000_000];
1793        let mut dict = PdfDictionary::new();
1794        dict.insert("Filter", PdfObject::Name(PdfName::new("FlateDecode")));
1795        let encoded = crate::stream::StreamEncoder::new()
1796            .encode(&payload, crate::stream::StreamFilter::Flate)
1797            .unwrap();
1798        assert!(encoded.len() < 2000, "test premise: bomb must compress");
1799        let stream = PdfStream::with_dict(dict, encoded);
1800        let limits = ParserLimits {
1801            max_decoded_stream_size: 64 * 1024,
1802            ..ParserLimits::default()
1803        };
1804        if let Ok(d) = Parser::with_limits(limits).decode_stream(&stream) {
1805            assert!(d.len() <= 64 * 1024 + 1);
1806        }
1807    }
1808
1809    #[test]
1810    fn streaming_matches_full_parse_on_file() {
1811        let mut doc = Document::new();
1812        doc.set_version("1.7".to_string());
1813        let mut catalog = PdfDictionary::new();
1814        catalog.insert("Type", PdfObject::Name(PdfName::new("Catalog")));
1815        doc.add_object(ObjectId::new(1, 0), PdfObject::Dictionary(catalog));
1816        let mut info = PdfDictionary::new();
1817        info.insert("Title", PdfObject::String(PdfString::from_literal("t")));
1818        doc.add_object(ObjectId::new(2, 0), PdfObject::Dictionary(info));
1819        for i in 0..30 {
1820            doc.add_object(
1821                ObjectId::new(3 + i, 0),
1822                PdfObject::Array(PdfArray(
1823                    (0..i).map(|j| PdfObject::Integer(j as i64)).collect(),
1824                )),
1825            );
1826        }
1827        let payload: Vec<u8> = (0..70000).map(|i| (i % 251) as u8).collect();
1828        let mut dict = PdfDictionary::new();
1829        dict.insert("Filter", PdfObject::Name(PdfName::new("FlateDecode")));
1830        let encoded = crate::stream::StreamEncoder::new()
1831            .encode(&payload, crate::stream::StreamFilter::Flate)
1832            .unwrap();
1833        doc.add_object(
1834            ObjectId::new(40, 0),
1835            PdfObject::Stream(PdfStream::with_dict(dict, encoded)),
1836        );
1837
1838        let mut buf = std::io::Cursor::new(Vec::new());
1839        Serializer::new().serialize(&doc, &mut buf).unwrap();
1840        let path = std::env::temp_dir().join(format!("pf_stream_{}.pdf", std::process::id()));
1841        std::fs::write(&path, buf.get_ref()).unwrap();
1842
1843        let parsed = Parser::new().parse(buf.get_ref()).unwrap();
1844        let streaming = StreamingParser::open(&path).unwrap();
1845        assert_eq!(streaming.version(), parsed.version());
1846        assert_eq!(streaming.catalog(), parsed.catalog());
1847        assert_eq!(streaming.info(), parsed.info());
1848        assert!(streaming.object_count() >= parsed.objects().len());
1849        assert!(streaming.file_size() > 0);
1850
1851        for (id, obj) in parsed.objects() {
1852            assert_eq!(streaming.get_object(*id).unwrap().as_ref(), Some(obj));
1853        }
1854        assert!(streaming
1855            .get_object(ObjectId::new(999, 0))
1856            .unwrap()
1857            .is_none());
1858
1859        let data = streaming
1860            .get_stream_data(ObjectId::new(40, 0))
1861            .unwrap()
1862            .unwrap();
1863        assert_eq!(data, payload);
1864        let _ = std::fs::remove_file(&path);
1865    }
1866
1867    #[test]
1868    fn streaming_resolves_compressed_objects() {
1869        // Hand-built file whose xref stream lists objects 7 and 8 inside an
1870        // /ObjStm stream, so the streaming path must resolve type-2 entries.
1871        let mut pdf = Vec::new();
1872        pdf.extend_from_slice(b"%PDF-1.7\n");
1873        let p1 = pdf.len() as u32;
1874        pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1875        let p2 = pdf.len() as u32;
1876        pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
1877        let p3 = pdf.len() as u32;
1878        pdf.extend_from_slice(
1879            // /First 9 (the pair header), /Length 42 (the two object bodies).
1880            b"3 0 obj\n<< /Type /ObjStm /N 2 /First 9 /Length 42 >>\nstream\n\
18817 0 8 17\n<< /A (seven) >> << /B (eight) >>\nendstream\nendobj\n",
1882        );
1883
1884        let mut xref_data = Vec::new();
1885        xref_data.extend_from_slice(&[0, 0, 0, 0, 0, 0xff, 0xff]);
1886        for pos in [p1, p2, p3] {
1887            let b = pos.to_be_bytes();
1888            xref_data.extend_from_slice(&[1, b[0], b[1], b[2], b[3], 0, 0]);
1889        }
1890        xref_data.extend_from_slice(&[2, 0, 0, 0, 3, 0, 0]);
1891        xref_data.extend_from_slice(&[2, 0, 0, 0, 3, 0, 1]);
1892
1893        let p10 = pdf.len() as u32;
1894        let b = p10.to_be_bytes();
1895        xref_data.extend_from_slice(&[1, b[0], b[1], b[2], b[3], 0, 0]);
1896        let xref_obj = format!(
1897            "10 0 obj\n<< /Type /XRef /Size 11 /Root 1 0 R /W [1 4 2] /Index [0 4 7 2 10 1] /Length {} >>\nstream\n",
1898            xref_data.len()
1899        );
1900        pdf.extend_from_slice(xref_obj.as_bytes());
1901        pdf.extend_from_slice(&xref_data);
1902        pdf.extend_from_slice(b"\nendstream\nendobj\n");
1903        pdf.extend_from_slice(format!("startxref\n{p10}\n%%EOF\n").as_bytes());
1904
1905        let path = std::env::temp_dir().join(format!("pf_objstm_{}.pdf", std::process::id()));
1906        std::fs::write(&path, &pdf).unwrap();
1907
1908        let streaming = StreamingParser::open(&path).unwrap();
1909        assert_eq!(streaming.catalog(), Some(ObjectId::new(1, 0)));
1910        assert_eq!(streaming.object_count(), 7);
1911        let seven = streaming.get_object(ObjectId::new(7, 0)).unwrap().unwrap();
1912        assert_eq!(
1913            seven.as_dict().unwrap().get("A"),
1914            Some(&PdfObject::String(PdfString::from_literal("seven")))
1915        );
1916        let eight = streaming.get_object(ObjectId::new(8, 0)).unwrap().unwrap();
1917        assert_eq!(
1918            eight.as_dict().unwrap().get("B"),
1919            Some(&PdfObject::String(PdfString::from_literal("eight")))
1920        );
1921        let _ = std::fs::remove_file(&path);
1922    }
1923}