Skip to main content

oxidize_pdf/parser/
reader.rs

1//! High-level PDF Reader API
2//!
3//! Provides a simple interface for reading PDF files
4
5use super::encryption_handler::EncryptionHandler;
6use super::header::PdfHeader;
7use super::object_stream::ObjectStream;
8use super::objects::{PdfArray, PdfDictionary, PdfObject, PdfString};
9use super::stack_safe::StackSafeContext;
10use super::trailer::PdfTrailer;
11use super::xref::{
12    find_byte_pattern, read_object_window, read_window_at, scan_page_object_refs, XRefTable,
13};
14use super::{ParseError, ParseResult};
15use crate::objects::ObjectId;
16use std::collections::HashMap;
17use std::fs::File;
18use std::io::{BufReader, Read, Seek, SeekFrom};
19use std::path::Path;
20
21/// Resolve a dictionary value that is expected to be an array into an owned
22/// [`PdfArray`], transparently following a single level of indirection.
23///
24/// ISO 32000-1 §7.3.10 permits any object — including `/Kids` — to be written
25/// as an indirect reference (`N G R`) instead of inline; iText 5.5.9 emits page
26/// trees this way. Returns `None` if the value is absent, is neither an array
27/// nor a reference to one, or cannot be resolved. Only one level is followed,
28/// which covers every real-world page tree observed (a reference-to-a-reference
29/// `/Kids` chain is not produced by any known writer).
30pub(crate) fn resolve_to_array<R: Read + Seek>(
31    reader: &mut PdfReader<R>,
32    value: Option<&PdfObject>,
33) -> Option<PdfArray> {
34    match value {
35        Some(PdfObject::Reference(num, gen)) => {
36            reader.get_object(*num, *gen).ok()?.as_array().cloned()
37        }
38        other => other.and_then(|o| o.as_array()).cloned(),
39    }
40}
41
42/// Bounded window for manual dictionary extraction (Issue #339). Object headers
43/// are located by the chunked scanner and only this many bytes are read at the
44/// object offset, instead of buffering the whole file. Large enough for any
45/// realistic catalog / pages dictionary (incl. multi-thousand-entry `/Kids`).
46const MANUAL_DICT_WINDOW: usize = 256 * 1024;
47
48/// Check if bytes start with "stream" after optional whitespace
49fn is_immediate_stream_start(data: &[u8]) -> bool {
50    let mut i = 0;
51
52    // Skip whitespace (spaces, tabs, newlines, carriage returns)
53    while i < data.len() && matches!(data[i], b' ' | b'\t' | b'\n' | b'\r') {
54        i += 1;
55    }
56
57    // Check if the rest starts with "stream"
58    data[i..].starts_with(b"stream")
59}
60
61/// Content between the first `open` byte and the first `close` byte that
62/// **follows** it, both exclusive. `None` when either delimiter is absent, or
63/// when `close` occurs only before `open`.
64///
65/// Searching the closer in the remainder after the opener is what makes this
66/// total: a naive `find(open)` + `find(close)` over the same haystack inverts
67/// the range on input like `/MediaBox ][` and panics. Both delimiters must be
68/// ASCII, so a match can never land inside a multi-byte UTF-8 sequence and the
69/// returned bounds are always char boundaries.
70fn slice_between(haystack: &str, open: u8, close: u8) -> Option<&str> {
71    debug_assert!(open.is_ascii() && close.is_ascii());
72    let open_idx = haystack.as_bytes().iter().position(|&b| b == open)?;
73    let rest = &haystack[open_idx + 1..];
74    let close_idx = rest.as_bytes().iter().position(|&b| b == close)?;
75    Some(&rest[..close_idx])
76}
77
78/// Byte offset, relative to `after`, of the `>>` that closes a dictionary whose
79/// opening `<<` has already been consumed. `None` if it never closes.
80///
81/// `after` is text recovered from arbitrary bytes via `from_utf8_lossy`, so it
82/// routinely holds multi-byte `U+FFFD`. The scan therefore stays in byte space
83/// throughout: `<` and `>` are ASCII and cannot occur inside a multi-byte
84/// sequence, which keeps the returned offset a valid slice bound. Mixing char
85/// indices with byte indices here splits a `U+FFFD` and panics.
86fn find_dict_end(after: &str) -> Option<usize> {
87    let bytes = after.as_bytes();
88    let mut depth = 1usize;
89    let mut i = 0usize;
90
91    while i + 1 < bytes.len() {
92        if bytes[i] == b'<' && bytes[i + 1] == b'<' {
93            depth += 1;
94            i += 2;
95        } else if bytes[i] == b'>' && bytes[i + 1] == b'>' {
96            depth -= 1;
97            if depth == 0 {
98                return Some(i);
99            }
100            i += 2;
101        } else {
102            i += 1;
103        }
104    }
105    None
106}
107
108/// Prefix of `s` of at most `max_bytes`, cut back to the nearest char boundary.
109fn truncate_on_char_boundary(s: &str, max_bytes: usize) -> &str {
110    if s.len() <= max_bytes {
111        return s;
112    }
113    let mut end = max_bytes;
114    while end > 0 && !s.is_char_boundary(end) {
115        end -= 1;
116    }
117    &s[..end]
118}
119
120/// High-level PDF reader
121pub struct PdfReader<R: Read + Seek> {
122    reader: BufReader<R>,
123    header: PdfHeader,
124    xref: XRefTable,
125    trailer: PdfTrailer,
126    /// Cache of loaded objects
127    object_cache: HashMap<(u32, u16), PdfObject>,
128    /// Cache of object streams
129    object_stream_cache: HashMap<u32, ObjectStream>,
130    /// Page tree navigator
131    page_tree: Option<super::page_tree::PageTree>,
132    /// Stack-safe parsing context
133    parse_context: StackSafeContext,
134    /// Parsing options
135    options: super::ParseOptions,
136    /// Encryption handler (if PDF is encrypted)
137    encryption_handler: Option<EncryptionHandler>,
138    /// Track objects currently being reconstructed (circular reference detection)
139    objects_being_reconstructed: std::sync::Mutex<std::collections::HashSet<u32>>,
140    /// Maximum reconstruction depth (prevents pathological cases)
141    max_reconstruction_depth: u32,
142}
143
144impl<R: Read + Seek> PdfReader<R> {
145    /// Get parsing options
146    pub fn options(&self) -> &super::ParseOptions {
147        &self.options
148    }
149
150    /// Check if the PDF is encrypted
151    pub fn is_encrypted(&self) -> bool {
152        self.encryption_handler.is_some()
153    }
154
155    /// Access the parsed document trailer.
156    ///
157    /// Exposes the already-parsed [`PdfTrailer`], which carries the base
158    /// `startxref` offset (`xref_offset`), and the `/Root`, `/Info`, `/ID`
159    /// and `/Size` entries. Required to build a conformant ISO 32000-1
160    /// §7.5.6 incremental update (the appended trailer must chain its
161    /// `/Prev` to this offset and reuse the base `/Root` and `/ID`).
162    pub fn trailer(&self) -> &PdfTrailer {
163        &self.trailer
164    }
165
166    /// Check if the PDF is unlocked (can read encrypted content)
167    pub fn is_unlocked(&self) -> bool {
168        match &self.encryption_handler {
169            Some(handler) => handler.is_unlocked(),
170            None => true, // Unencrypted PDFs are always "unlocked"
171        }
172    }
173
174    /// Get mutable access to encryption handler
175    pub fn encryption_handler_mut(&mut self) -> Option<&mut EncryptionHandler> {
176        self.encryption_handler.as_mut()
177    }
178
179    /// Get access to encryption handler
180    pub fn encryption_handler(&self) -> Option<&EncryptionHandler> {
181        self.encryption_handler.as_ref()
182    }
183
184    /// Try to unlock PDF with password
185    pub fn unlock_with_password(&mut self, password: &str) -> ParseResult<bool> {
186        match &mut self.encryption_handler {
187            Some(handler) => {
188                // A password that does not match returns Ok(false); an Err means
189                // the encryption dictionary itself could not be processed — a
190                // truncated /U, an unsupported revision. Both were collapsed into
191                // "wrong password", which is how the real cause of issue #459
192                // stayed invisible: the reporter was told to find a password for
193                // a document whose empty password was already correct.
194                //
195                // The owner password still gets its turn before any error is
196                // raised: a document whose /O is unusable can open on its /U.
197                let user = handler.unlock_with_user_password(password);
198                if matches!(user, Ok(true)) {
199                    return Ok(true);
200                }
201                let owner = handler.unlock_with_owner_password(password);
202                if matches!(owner, Ok(true)) {
203                    return Ok(true);
204                }
205                match (user, owner) {
206                    (Err(e), _) | (Ok(_), Err(e)) => Err(e),
207                    (Ok(_), Ok(_)) => Ok(false),
208                }
209            }
210            None => Ok(true), // Not encrypted
211        }
212    }
213
214    /// Try to unlock with empty password
215    pub fn try_empty_password(&mut self) -> ParseResult<bool> {
216        match &mut self.encryption_handler {
217            Some(handler) => Ok(handler.try_empty_password().unwrap_or(false)),
218            None => Ok(true), // Not encrypted
219        }
220    }
221
222    /// Unlock encrypted PDF with password
223    ///
224    /// Attempts to unlock the PDF using the provided password (tries both user
225    /// and owner passwords). If the PDF is not encrypted, this method returns
226    /// `Ok(())` immediately.
227    ///
228    /// # Arguments
229    ///
230    /// * `password` - User or owner password for the PDF
231    ///
232    /// # Errors
233    ///
234    /// Returns `ParseError::WrongPassword` if the password is incorrect.
235    ///
236    /// # Example
237    ///
238    /// ```no_run
239    /// use oxidize_pdf::parser::PdfReader;
240    ///
241    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
242    /// let mut reader = PdfReader::open("encrypted.pdf")?;
243    ///
244    /// if reader.is_encrypted() {
245    ///     reader.unlock("password")?;
246    /// }
247    ///
248    /// let catalog = reader.catalog()?;
249    /// # Ok(())
250    /// # }
251    /// ```
252    pub fn unlock(&mut self, password: &str) -> ParseResult<()> {
253        // If not encrypted, nothing to do
254        if !self.is_encrypted() {
255            return Ok(());
256        }
257
258        // Early return if already unlocked (idempotent)
259        if self.is_unlocked() {
260            return Ok(());
261        }
262
263        // Try to unlock with password (tries user and owner)
264        let success = self.unlock_with_password(password)?;
265
266        if success {
267            Ok(())
268        } else {
269            Err(ParseError::WrongPassword)
270        }
271    }
272
273    /// Check if PDF is locked and return error if so
274    fn ensure_unlocked(&self) -> ParseResult<()> {
275        if self.is_encrypted() && !self.is_unlocked() {
276            return Err(ParseError::PdfLocked);
277        }
278        Ok(())
279    }
280
281    /// Decrypt an object if encryption is active
282    ///
283    /// This method recursively decrypts strings and streams within the object.
284    /// Objects that don't contain encrypted data (numbers, names, booleans, null,
285    /// references) are returned unchanged.
286    fn decrypt_object_if_needed(
287        &self,
288        obj: PdfObject,
289        obj_num: u32,
290        gen_num: u16,
291    ) -> ParseResult<PdfObject> {
292        // Only decrypt if encryption is active and unlocked
293        let handler = match &self.encryption_handler {
294            Some(h) if h.is_unlocked() => h,
295            _ => return Ok(obj), // Not encrypted or not unlocked
296        };
297
298        let obj_id = ObjectId::new(obj_num, gen_num);
299
300        match obj {
301            PdfObject::String(ref s) => {
302                // Decrypt string
303                let decrypted_bytes = handler.decrypt_string(s.as_bytes(), &obj_id)?;
304                Ok(PdfObject::String(PdfString::new(decrypted_bytes)))
305            }
306            PdfObject::Stream(ref stream) => {
307                // Check if stream should be decrypted (Identity filter means no decryption)
308                let should_decrypt = stream
309                    .dict
310                    .get("StmF")
311                    .and_then(|o| o.as_name())
312                    .map(|n| n.0.as_str() != "Identity")
313                    .unwrap_or(true); // Default: decrypt if no /StmF
314
315                if should_decrypt {
316                    let decrypted_data = handler.decrypt_stream(&stream.data, &obj_id)?;
317
318                    // Create new stream with decrypted data
319                    let mut new_stream = stream.clone();
320                    new_stream.data = decrypted_data;
321                    Ok(PdfObject::Stream(new_stream))
322                } else {
323                    Ok(obj) // Don't decrypt /Identity streams
324                }
325            }
326            PdfObject::Dictionary(ref dict) => {
327                // Recursively decrypt dictionary values
328                let mut new_dict = PdfDictionary::new();
329                for (key, value) in dict.0.iter() {
330                    let decrypted_value =
331                        self.decrypt_object_if_needed(value.clone(), obj_num, gen_num)?;
332                    new_dict.insert(key.0.clone(), decrypted_value);
333                }
334                Ok(PdfObject::Dictionary(new_dict))
335            }
336            PdfObject::Array(ref arr) => {
337                // Recursively decrypt array elements
338                let mut new_arr = Vec::new();
339                for elem in arr.0.iter() {
340                    let decrypted_elem =
341                        self.decrypt_object_if_needed(elem.clone(), obj_num, gen_num)?;
342                    new_arr.push(decrypted_elem);
343                }
344                Ok(PdfObject::Array(PdfArray(new_arr)))
345            }
346            // Other types (Integer, Real, Boolean, Name, Null, Reference) don't get encrypted
347            _ => Ok(obj),
348        }
349    }
350}
351
352impl PdfReader<File> {
353    /// Open a PDF file from a path
354    pub fn open<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
355        #[cfg(feature = "verbose-debug")]
356        {
357            use std::io::Write;
358            if let Ok(mut f) = std::fs::File::create("/tmp/pdf_open_debug.log") {
359                writeln!(f, "Opening file: {:?}", path.as_ref()).ok();
360            }
361        }
362        let file = File::open(path)?;
363        // Use lenient options by default for maximum compatibility
364        let options = super::ParseOptions::lenient();
365        Self::new_with_options(file, options)
366    }
367
368    /// Open a PDF file from a path with strict parsing
369    pub fn open_strict<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
370        let file = File::open(path)?;
371        let options = super::ParseOptions::strict();
372        Self::new_with_options(file, options)
373    }
374
375    /// Open a PDF file from a path with custom parsing options
376    pub fn open_with_options<P: AsRef<Path>>(
377        path: P,
378        options: super::ParseOptions,
379    ) -> ParseResult<Self> {
380        let file = File::open(path)?;
381        Self::new_with_options(file, options)
382    }
383
384    /// Open a PDF file as a PdfDocument
385    pub fn open_document<P: AsRef<Path>>(
386        path: P,
387    ) -> ParseResult<super::document::PdfDocument<File>> {
388        let reader = Self::open(path)?;
389        Ok(reader.into_document())
390    }
391}
392
393impl<R: Read + Seek> PdfReader<R> {
394    /// Create a new PDF reader from a reader
395    ///
396    /// Uses default parsing options with `lenient_streams` enabled for
397    /// compatibility with real-world PDFs that use indirect references for
398    /// stream lengths. Use `new_with_options` with `ParseOptions::strict()`
399    /// if you need fully strict validation.
400    pub fn new(reader: R) -> ParseResult<Self> {
401        // Enable lenient_streams by default to handle indirect Length references
402        // This is consistent with PdfReader::open() behavior
403        let mut options = super::ParseOptions::default();
404        options.lenient_streams = true;
405        Self::new_with_options(reader, options)
406    }
407
408    /// Create a new PDF reader with custom parsing options
409    pub fn new_with_options(reader: R, options: super::ParseOptions) -> ParseResult<Self> {
410        let mut buf_reader = BufReader::new(reader);
411
412        // Check if file is empty
413        let start_pos = buf_reader.stream_position()?;
414        buf_reader.seek(SeekFrom::End(0))?;
415        let file_size = buf_reader.stream_position()?;
416        buf_reader.seek(SeekFrom::Start(start_pos))?;
417
418        if file_size == 0 {
419            return Err(ParseError::EmptyFile);
420        }
421
422        // Parse header
423        let header = PdfHeader::parse(&mut buf_reader)?;
424        #[cfg(feature = "verbose-debug")]
425        tracing::debug!("Header parsed: version {}", header.version);
426
427        // Parse xref table
428        let xref = XRefTable::parse_with_options(&mut buf_reader, &options)?;
429        #[cfg(feature = "verbose-debug")]
430        tracing::debug!("XRef table parsed with {} entries", xref.len());
431
432        // Get trailer
433        let trailer_dict = xref.trailer().ok_or(ParseError::InvalidTrailer)?.clone();
434
435        let xref_offset = xref.xref_offset();
436        let trailer = PdfTrailer::from_dict(trailer_dict, xref_offset)?;
437
438        // Validate trailer
439        trailer.validate()?;
440
441        // Check for encryption
442        let encryption_handler = if EncryptionHandler::detect_encryption(trailer.dict()) {
443            if let Ok(Some((encrypt_obj_num, encrypt_gen_num))) = trailer.encrypt() {
444                // We need to temporarily create the reader to load the encryption dictionary
445                let mut temp_reader = Self {
446                    reader: buf_reader,
447                    header: header.clone(),
448                    xref: xref.clone(),
449                    trailer: trailer.clone(),
450                    object_cache: HashMap::new(),
451                    object_stream_cache: HashMap::new(),
452                    page_tree: None,
453                    parse_context: StackSafeContext::new(),
454                    options: options.clone(),
455                    encryption_handler: None,
456                    objects_being_reconstructed: std::sync::Mutex::new(
457                        std::collections::HashSet::new(),
458                    ),
459                    max_reconstruction_depth: 100,
460                };
461
462                // Load encryption dictionary
463                let encrypt_obj = temp_reader.get_object(encrypt_obj_num, encrypt_gen_num)?;
464                if let Some(encrypt_dict) = encrypt_obj.as_dict() {
465                    // Get file ID from trailer
466                    let file_id = trailer.id().and_then(|id_obj| {
467                        if let PdfObject::Array(ref id_array) = id_obj {
468                            if let Some(PdfObject::String(ref id_bytes)) = id_array.get(0) {
469                                Some(id_bytes.as_bytes().to_vec())
470                            } else {
471                                None
472                            }
473                        } else {
474                            None
475                        }
476                    });
477
478                    match EncryptionHandler::new(encrypt_dict, file_id) {
479                        Ok(mut handler) => {
480                            // Auto-unlock with empty password (common for permission-restricted PDFs)
481                            let _ = handler.try_empty_password();
482                            // Move the reader back out
483                            buf_reader = temp_reader.reader;
484                            Some(handler)
485                        }
486                        Err(_) => {
487                            // Move reader back and continue without encryption
488                            let _ = temp_reader.reader;
489                            return Err(ParseError::EncryptionNotSupported);
490                        }
491                    }
492                } else {
493                    let _ = temp_reader.reader;
494                    return Err(ParseError::EncryptionNotSupported);
495                }
496            } else {
497                return Err(ParseError::EncryptionNotSupported);
498            }
499        } else {
500            None
501        };
502
503        Ok(Self {
504            reader: buf_reader,
505            header,
506            xref,
507            trailer,
508            object_cache: HashMap::new(),
509            object_stream_cache: HashMap::new(),
510            page_tree: None,
511            parse_context: StackSafeContext::new(),
512            options,
513            encryption_handler,
514            objects_being_reconstructed: std::sync::Mutex::new(std::collections::HashSet::new()),
515            max_reconstruction_depth: 100,
516        })
517    }
518
519    /// Get the PDF version
520    pub fn version(&self) -> &super::header::PdfVersion {
521        &self.header.version
522    }
523
524    /// Get the document catalog
525    pub fn catalog(&mut self) -> ParseResult<&PdfDictionary> {
526        // Try to get root from trailer
527        let (obj_num, gen_num) = match self.trailer.root() {
528            Ok(root) => {
529                // FIX for Issue #83: Validate that Root actually points to a Catalog
530                // In signed PDFs, Root might point to /Type/Sig instead of /Type/Catalog
531                if let Ok(obj) = self.get_object(root.0, root.1) {
532                    if let Some(dict) = obj.as_dict() {
533                        // Check if it's really a catalog
534                        if let Some(type_obj) = dict.get("Type") {
535                            if let Some(type_name) = type_obj.as_name() {
536                                if type_name.0 != "Catalog" {
537                                    tracing::warn!("Trailer /Root points to /Type/{} (not Catalog), scanning for real catalog", type_name.0);
538                                    // Root points to wrong object type, scan for real catalog
539                                    if let Ok(catalog_ref) = self.find_catalog_object() {
540                                        catalog_ref
541                                    } else {
542                                        root // Fallback to original if scan fails
543                                    }
544                                } else {
545                                    root // It's a valid catalog
546                                }
547                            } else {
548                                root // No type field, assume it's catalog
549                            }
550                        } else {
551                            root // No Type key, assume it's catalog
552                        }
553                    } else {
554                        root // Not a dict, will fail later but keep trying
555                    }
556                } else {
557                    root // Can't get object, will fail later
558                }
559            }
560            Err(_) => {
561                // If Root is missing, try fallback methods
562                #[cfg(debug_assertions)]
563                tracing::warn!("Trailer missing Root entry, attempting recovery");
564
565                // First try the fallback method
566                if let Some(root) = self.trailer.find_root_fallback() {
567                    root
568                } else {
569                    // Last resort: scan for Catalog object
570                    if let Ok(catalog_ref) = self.find_catalog_object() {
571                        catalog_ref
572                    } else {
573                        return Err(ParseError::MissingKey("Root".to_string()));
574                    }
575                }
576            }
577        };
578
579        // Check if we need to attempt reconstruction by examining the object type first
580        let key = (obj_num, gen_num);
581        let needs_reconstruction = {
582            match self.get_object(obj_num, gen_num) {
583                Ok(catalog) => {
584                    // Check if it's already a valid dictionary
585                    if catalog.as_dict().is_some() {
586                        // It's a valid dictionary, no reconstruction needed
587                        false
588                    } else {
589                        // Not a dictionary, needs reconstruction
590                        true
591                    }
592                }
593                Err(_) => {
594                    // Failed to get object, needs reconstruction
595                    true
596                }
597            }
598        };
599
600        if !needs_reconstruction {
601            // Object is valid, get it again to return the reference
602            let catalog = self.get_object(obj_num, gen_num)?;
603            return catalog.as_dict().ok_or_else(|| ParseError::SyntaxError {
604                position: 0,
605                message: format!("Catalog object {} {} is not a dictionary", obj_num, gen_num),
606            });
607        }
608
609        // If we reach here, reconstruction is needed
610
611        match self.extract_object_manually(obj_num) {
612            Ok(dict) => {
613                // Cache the reconstructed object
614                let obj = PdfObject::Dictionary(dict);
615                self.object_cache.insert(key, obj);
616
617                // Also add to XRef table so the object can be found later
618                use crate::parser::xref::XRefEntry;
619                let xref_entry = XRefEntry {
620                    offset: 0, // Dummy offset since object is cached
621                    generation: gen_num,
622                    in_use: true,
623                };
624                self.xref.add_entry(obj_num, xref_entry);
625
626                // Return reference to cached dictionary
627                if let Some(PdfObject::Dictionary(ref dict)) = self.object_cache.get(&key) {
628                    return Ok(dict);
629                }
630            }
631            Err(_e) => {}
632        }
633
634        // Return error if all reconstruction attempts failed
635        Err(ParseError::SyntaxError {
636            position: 0,
637            message: format!(
638                "Catalog object {} could not be parsed or reconstructed as a dictionary",
639                obj_num
640            ),
641        })
642    }
643
644    /// Get the document info dictionary
645    pub fn info(&mut self) -> ParseResult<Option<&PdfDictionary>> {
646        match self.trailer.info() {
647            Some((obj_num, gen_num)) => {
648                let info = self.get_object(obj_num, gen_num)?;
649                Ok(info.as_dict())
650            }
651            None => Ok(None),
652        }
653    }
654
655    /// Get an object by reference with circular reference protection
656    pub fn get_object(&mut self, obj_num: u32, gen_num: u16) -> ParseResult<&PdfObject> {
657        // Check if PDF is locked (encrypted but not unlocked)
658        self.ensure_unlocked()?;
659
660        let key = (obj_num, gen_num);
661
662        // Fast path: check cache first
663        if self.object_cache.contains_key(&key) {
664            return Ok(&self.object_cache[&key]);
665        }
666
667        // PROTECTION 1: Check for circular reference
668        {
669            let being_loaded =
670                self.objects_being_reconstructed
671                    .lock()
672                    .map_err(|_| ParseError::SyntaxError {
673                        position: 0,
674                        message: "Mutex poisoned during circular reference check".to_string(),
675                    })?;
676            if being_loaded.contains(&obj_num) {
677                drop(being_loaded);
678                if self.options.collect_warnings {}
679                self.object_cache.insert(key, PdfObject::Null);
680                return Ok(&self.object_cache[&key]);
681            }
682        }
683
684        // PROTECTION 2: Check depth limit
685        {
686            let being_loaded =
687                self.objects_being_reconstructed
688                    .lock()
689                    .map_err(|_| ParseError::SyntaxError {
690                        position: 0,
691                        message: "Mutex poisoned during depth limit check".to_string(),
692                    })?;
693            let depth = being_loaded.len() as u32;
694            if depth >= self.max_reconstruction_depth {
695                drop(being_loaded);
696                if self.options.collect_warnings {}
697                return Err(ParseError::SyntaxError {
698                    position: 0,
699                    message: format!(
700                        "Maximum object loading depth ({}) exceeded",
701                        self.max_reconstruction_depth
702                    ),
703                });
704            }
705        }
706
707        // Mark object as being loaded
708        self.objects_being_reconstructed
709            .lock()
710            .map_err(|_| ParseError::SyntaxError {
711                position: 0,
712                message: "Mutex poisoned while marking object as being loaded".to_string(),
713            })?
714            .insert(obj_num);
715
716        // Load object - if successful, it will be in cache
717        match self.load_object_from_disk(obj_num, gen_num) {
718            Ok(_) => {
719                // Object successfully loaded, now unmark and return from cache
720                self.objects_being_reconstructed
721                    .lock()
722                    .map_err(|_| ParseError::SyntaxError {
723                        position: 0,
724                        message: "Mutex poisoned while unmarking object after successful load"
725                            .to_string(),
726                    })?
727                    .remove(&obj_num);
728                // Object must be in cache now
729                Ok(&self.object_cache[&key])
730            }
731            Err(e) => {
732                // Loading failed, unmark and propagate error
733                // Note: If mutex is poisoned here, we prioritize the original error
734                if let Ok(mut guard) = self.objects_being_reconstructed.lock() {
735                    guard.remove(&obj_num);
736                }
737                Err(e)
738            }
739        }
740    }
741
742    /// Internal method to load an object from disk without stack management
743    fn load_object_from_disk(&mut self, obj_num: u32, gen_num: u16) -> ParseResult<&PdfObject> {
744        let key = (obj_num, gen_num);
745
746        // Check cache first
747        if self.object_cache.contains_key(&key) {
748            return Ok(&self.object_cache[&key]);
749        }
750
751        // Check if this is a compressed object
752        if let Some(ext_entry) = self.xref.get_extended_entry(obj_num) {
753            if let Some((stream_obj_num, index_in_stream)) = ext_entry.compressed_info {
754                // This is a compressed object - need to extract from object stream
755                return self.get_compressed_object(
756                    obj_num,
757                    gen_num,
758                    stream_obj_num,
759                    index_in_stream,
760                );
761            }
762        } else {
763        }
764
765        // Get xref entry and extract needed values
766        let (current_offset, _generation) = {
767            let entry = self.xref.get_entry(obj_num);
768
769            match entry {
770                Some(entry) => {
771                    if !entry.in_use {
772                        // Free object
773                        self.object_cache.insert(key, PdfObject::Null);
774                        return Ok(&self.object_cache[&key]);
775                    }
776
777                    if entry.generation != gen_num {
778                        if self.options.lenient_syntax {
779                            // In lenient mode, warn but use the available generation
780                            if self.options.collect_warnings {
781                                tracing::warn!("Object {} generation mismatch - expected {}, found {}, using available",
782                                    obj_num, gen_num, entry.generation);
783                            }
784                        } else {
785                            return Err(ParseError::InvalidReference(obj_num, gen_num));
786                        }
787                    }
788
789                    (entry.offset, entry.generation)
790                }
791                None => {
792                    // Object not found in XRef table
793                    if self.is_reconstructible_object(obj_num) {
794                        return self.attempt_manual_object_reconstruction(obj_num, gen_num, 0);
795                    } else {
796                        if self.options.lenient_syntax {
797                            // In lenient mode, return null object instead of failing completely
798                            if self.options.collect_warnings {
799                                tracing::warn!(
800                                    "Object {} {} R not found in XRef, returning null object",
801                                    obj_num,
802                                    gen_num
803                                );
804                            }
805                            self.object_cache.insert(key, PdfObject::Null);
806                            return Ok(&self.object_cache[&key]);
807                        } else {
808                            return Err(ParseError::InvalidReference(obj_num, gen_num));
809                        }
810                    }
811                }
812            }
813        };
814
815        // Try normal parsing first - only use manual reconstruction as fallback
816
817        // Seek to the (potentially corrected) object position
818        self.reader.seek(std::io::SeekFrom::Start(current_offset))?;
819
820        // Parse object header (obj_num gen_num obj) - but skip if we already positioned after it
821        let mut lexer =
822            super::lexer::Lexer::new_with_options(&mut self.reader, self.options.clone());
823
824        // Parse object header normally for all objects
825        {
826            // Read object number with recovery
827            let token = lexer.next_token()?;
828            let read_obj_num = match token {
829                super::lexer::Token::Integer(n) => n as u32,
830                _ => {
831                    // Try fallback recovery (simplified implementation)
832                    if self.options.lenient_syntax {
833                        // For now, use the expected object number and issue warning
834                        if self.options.collect_warnings {
835                            tracing::debug!(
836                                "Warning: Using expected object number {obj_num} instead of parsed token: {:?}",
837                                token
838                            );
839                        }
840                        obj_num
841                    } else {
842                        return Err(ParseError::SyntaxError {
843                            position: current_offset as usize,
844                            message: "Expected object number".to_string(),
845                        });
846                    }
847                }
848            };
849
850            if read_obj_num != obj_num && !self.options.lenient_syntax {
851                return Err(ParseError::SyntaxError {
852                    position: current_offset as usize,
853                    message: format!(
854                        "Object number mismatch: expected {obj_num}, found {read_obj_num}"
855                    ),
856                });
857            }
858
859            // Read generation number with recovery
860            let token = lexer.next_token()?;
861            let _read_gen_num = match token {
862                super::lexer::Token::Integer(n) => n as u16,
863                _ => {
864                    // Try fallback recovery
865                    if self.options.lenient_syntax {
866                        if self.options.collect_warnings {
867                            tracing::warn!(
868                                "Using generation 0 instead of parsed token for object {obj_num}"
869                            );
870                        }
871                        0
872                    } else {
873                        return Err(ParseError::SyntaxError {
874                            position: current_offset as usize,
875                            message: "Expected generation number".to_string(),
876                        });
877                    }
878                }
879            };
880
881            // Read 'obj' keyword
882            let token = lexer.next_token()?;
883            match token {
884                super::lexer::Token::Obj => {}
885                _ => {
886                    if self.options.lenient_syntax {
887                        // In lenient mode, warn but continue
888                        if self.options.collect_warnings {
889                            tracing::warn!("Expected 'obj' keyword for object {obj_num} {gen_num}, continuing anyway");
890                        }
891                    } else {
892                        return Err(ParseError::SyntaxError {
893                            position: current_offset as usize,
894                            message: "Expected 'obj' keyword".to_string(),
895                        });
896                    }
897                }
898            }
899        }
900
901        // Check recursion depth and parse object
902        self.parse_context.enter()?;
903
904        let obj = match PdfObject::parse_with_options(&mut lexer, &self.options) {
905            Ok(obj) => {
906                self.parse_context.exit();
907                // Debug: Print what object we actually parsed
908                if obj_num == 102 && self.options.collect_warnings {}
909                obj
910            }
911            Err(e) => {
912                self.parse_context.exit();
913
914                // Attempt manual reconstruction as fallback for known problematic objects
915                if self.is_reconstructible_object(obj_num)
916                    && self.can_attempt_manual_reconstruction(&e)
917                {
918                    match self.attempt_manual_object_reconstruction(
919                        obj_num,
920                        gen_num,
921                        current_offset,
922                    ) {
923                        Ok(reconstructed_obj) => {
924                            return Ok(reconstructed_obj);
925                        }
926                        Err(_reconstruction_error) => {}
927                    }
928                }
929
930                return Err(e);
931            }
932        };
933
934        // Read 'endobj' keyword
935        let token = lexer.next_token()?;
936        match token {
937            super::lexer::Token::EndObj => {}
938            _ => {
939                if self.options.lenient_syntax {
940                    // In lenient mode, warn but continue
941                    if self.options.collect_warnings {
942                        tracing::warn!("Expected 'endobj' keyword after object {obj_num} {gen_num}, continuing anyway");
943                    }
944                } else {
945                    return Err(ParseError::SyntaxError {
946                        position: current_offset as usize,
947                        message: "Expected 'endobj' keyword".to_string(),
948                    });
949                }
950            }
951        };
952
953        // Decrypt if encryption is active
954        let decrypted_obj = self.decrypt_object_if_needed(obj, obj_num, gen_num)?;
955
956        // Cache the decrypted object
957        self.object_cache.insert(key, decrypted_obj);
958
959        Ok(&self.object_cache[&key])
960    }
961
962    /// Resolve a reference to get the actual object
963    pub fn resolve<'a>(&'a mut self, obj: &'a PdfObject) -> ParseResult<&'a PdfObject> {
964        match obj {
965            PdfObject::Reference(obj_num, gen_num) => self.get_object(*obj_num, *gen_num),
966            _ => Ok(obj),
967        }
968    }
969
970    /// Resolve a stream length reference to get the actual length value
971    /// This is a specialized method for handling indirect references in stream Length fields
972    pub fn resolve_stream_length(&mut self, obj: &PdfObject) -> ParseResult<Option<usize>> {
973        match obj {
974            PdfObject::Integer(len) => {
975                if *len >= 0 {
976                    Ok(Some(*len as usize))
977                } else {
978                    // Negative lengths are invalid, treat as missing
979                    Ok(None)
980                }
981            }
982            PdfObject::Reference(obj_num, gen_num) => {
983                let resolved = self.get_object(*obj_num, *gen_num)?;
984                match resolved {
985                    PdfObject::Integer(len) => {
986                        if *len >= 0 {
987                            Ok(Some(*len as usize))
988                        } else {
989                            Ok(None)
990                        }
991                    }
992                    _ => {
993                        // Reference doesn't point to a valid integer
994                        Ok(None)
995                    }
996                }
997            }
998            _ => {
999                // Not a valid length type
1000                Ok(None)
1001            }
1002        }
1003    }
1004
1005    /// Get a compressed object from an object stream
1006    fn get_compressed_object(
1007        &mut self,
1008        obj_num: u32,
1009        gen_num: u16,
1010        stream_obj_num: u32,
1011        _index_in_stream: u32,
1012    ) -> ParseResult<&PdfObject> {
1013        let key = (obj_num, gen_num);
1014
1015        // Load the object stream if not cached
1016        if !self.object_stream_cache.contains_key(&stream_obj_num) {
1017            // Get the stream object using get_object (with circular ref protection)
1018            let stream_obj = self.get_object(stream_obj_num, 0)?;
1019
1020            if let Some(stream) = stream_obj.as_stream() {
1021                // Parse the object stream
1022                let obj_stream = ObjectStream::parse(stream.clone(), &self.options)?;
1023                self.object_stream_cache.insert(stream_obj_num, obj_stream);
1024            } else {
1025                return Err(ParseError::SyntaxError {
1026                    position: 0,
1027                    message: format!("Object {stream_obj_num} is not a stream"),
1028                });
1029            }
1030        }
1031
1032        // Get the object from the stream
1033        let obj_stream = &self.object_stream_cache[&stream_obj_num];
1034        let obj = obj_stream
1035            .get_object(obj_num)
1036            .ok_or_else(|| ParseError::SyntaxError {
1037                position: 0,
1038                message: format!("Object {obj_num} not found in object stream {stream_obj_num}"),
1039            })?;
1040
1041        // Decrypt if encryption is active (object stream contents may contain encrypted strings)
1042        let decrypted_obj = self.decrypt_object_if_needed(obj.clone(), obj_num, gen_num)?;
1043
1044        // Cache the decrypted object
1045        self.object_cache.insert(key, decrypted_obj);
1046        Ok(&self.object_cache[&key])
1047    }
1048
1049    /// Get the page tree root
1050    pub fn pages(&mut self) -> ParseResult<&PdfDictionary> {
1051        // Get the pages reference from catalog first
1052        let (pages_obj_num, pages_gen_num) = {
1053            let catalog = self.catalog()?;
1054
1055            // First try to get Pages reference
1056            if let Some(pages_ref) = catalog.get("Pages") {
1057                match pages_ref {
1058                    PdfObject::Reference(obj_num, gen_num) => (*obj_num, *gen_num),
1059                    _ => {
1060                        return Err(ParseError::SyntaxError {
1061                            position: 0,
1062                            message: "Pages must be a reference".to_string(),
1063                        })
1064                    }
1065                }
1066            } else {
1067                // If Pages is missing, try to find page objects by scanning
1068                #[cfg(debug_assertions)]
1069                tracing::warn!("Catalog missing Pages entry, attempting recovery");
1070
1071                // Look for objects that have Type = Page
1072                if let Ok(page_refs) = self.find_page_objects() {
1073                    if !page_refs.is_empty() {
1074                        // Create a synthetic Pages dictionary
1075                        return self.create_synthetic_pages_dict(&page_refs);
1076                    }
1077                }
1078
1079                // If Pages is missing and we have lenient parsing, try to find it
1080                if self.options.lenient_syntax {
1081                    if self.options.collect_warnings {
1082                        tracing::warn!("Missing Pages in catalog, searching for page tree");
1083                    }
1084                    // Search for a Pages object in the document
1085                    let mut found_pages = None;
1086                    for i in 1..self.xref.len() as u32 {
1087                        if let Ok(obj) = self.get_object(i, 0) {
1088                            if let Some(dict) = obj.as_dict() {
1089                                if let Some(obj_type) = dict.get("Type").and_then(|t| t.as_name()) {
1090                                    if obj_type.0 == "Pages" {
1091                                        found_pages = Some((i, 0));
1092                                        break;
1093                                    }
1094                                }
1095                            }
1096                        }
1097                    }
1098                    if let Some((obj_num, gen_num)) = found_pages {
1099                        (obj_num, gen_num)
1100                    } else {
1101                        return Err(ParseError::MissingKey("Pages".to_string()));
1102                    }
1103                } else {
1104                    return Err(ParseError::MissingKey("Pages".to_string()));
1105                }
1106            }
1107        };
1108
1109        // Now we can get the pages object without holding a reference to catalog
1110        // First, check if we need double indirection by peeking at the object
1111        let needs_double_resolve = {
1112            let pages_obj = self.get_object(pages_obj_num, pages_gen_num)?;
1113            pages_obj.as_reference()
1114        };
1115
1116        // If it's a reference, resolve the double indirection
1117        let (final_obj_num, final_gen_num) =
1118            if let Some((ref_obj_num, ref_gen_num)) = needs_double_resolve {
1119                (ref_obj_num, ref_gen_num)
1120            } else {
1121                (pages_obj_num, pages_gen_num)
1122            };
1123
1124        // Determine which object number to use for Pages (validate and potentially search)
1125        let actual_pages_num = {
1126            // Check if the referenced object is valid (in a scope to drop borrows)
1127            let is_valid_dict = {
1128                let pages_obj = self.get_object(final_obj_num, final_gen_num)?;
1129                pages_obj.as_dict().is_some()
1130            };
1131
1132            if is_valid_dict {
1133                // The referenced object is valid
1134                final_obj_num
1135            } else {
1136                // If Pages reference resolves to Null or non-dictionary, try to find Pages manually (corrupted PDF)
1137                #[cfg(debug_assertions)]
1138                tracing::warn!("Pages reference invalid, searching for valid Pages object");
1139
1140                if self.options.lenient_syntax {
1141                    // Search for a valid Pages object number
1142                    let xref_len = self.xref.len() as u32;
1143                    let mut found_pages_num = None;
1144
1145                    for i in 1..xref_len {
1146                        // Check in a scope to drop the borrow
1147                        let is_pages = {
1148                            if let Ok(obj) = self.get_object(i, 0) {
1149                                if let Some(dict) = obj.as_dict() {
1150                                    if let Some(obj_type) =
1151                                        dict.get("Type").and_then(|t| t.as_name())
1152                                    {
1153                                        obj_type.0 == "Pages"
1154                                    } else {
1155                                        false
1156                                    }
1157                                } else {
1158                                    false
1159                                }
1160                            } else {
1161                                false
1162                            }
1163                        };
1164
1165                        if is_pages {
1166                            found_pages_num = Some(i);
1167                            break;
1168                        }
1169                    }
1170
1171                    if let Some(obj_num) = found_pages_num {
1172                        #[cfg(debug_assertions)]
1173                        tracing::debug!("Found valid Pages object at {} 0 R", obj_num);
1174                        obj_num
1175                    } else {
1176                        // No valid Pages found
1177                        return Err(ParseError::SyntaxError {
1178                            position: 0,
1179                            message: "Pages is not a dictionary and no valid Pages object found"
1180                                .to_string(),
1181                        });
1182                    }
1183                } else {
1184                    // Lenient mode disabled, can't search
1185                    return Err(ParseError::SyntaxError {
1186                        position: 0,
1187                        message: "Pages is not a dictionary".to_string(),
1188                    });
1189                }
1190            }
1191        };
1192
1193        // Now get the final Pages object (all validation/search done above)
1194        let pages_obj = self.get_object(actual_pages_num, 0)?;
1195        pages_obj.as_dict().ok_or_else(|| ParseError::SyntaxError {
1196            position: 0,
1197            message: "Pages object is not a dictionary".to_string(),
1198        })
1199    }
1200
1201    /// Get the number of pages
1202    pub fn page_count(&mut self) -> ParseResult<u32> {
1203        /// Maximum page count accepted from the /Count entry.
1204        /// PDFs claiming more pages than this are likely malformed or malicious.
1205        const MAX_PAGE_COUNT: u32 = 100_000;
1206
1207        // Try standard method first
1208        match self.pages() {
1209            Ok(pages) => {
1210                // Read /Count and /Kids up front. Each may be inline or an
1211                // indirect reference (ISO 32000-1 §7.3.10); extract Copy values
1212                // now so the `pages` borrow ends before we resolve references.
1213                let count_inline = pages.get("Count").and_then(|o| o.as_integer());
1214                let count_ref = pages.get("Count").and_then(|o| o.as_reference());
1215                let kids_len_inline = pages
1216                    .get("Kids")
1217                    .and_then(|o| o.as_array())
1218                    .map(|a| a.0.len());
1219                let kids_ref = pages.get("Kids").and_then(|o| o.as_reference());
1220
1221                // Resolve /Count to an integer, whether inline or indirect.
1222                let count = count_inline.or_else(|| {
1223                    count_ref
1224                        .and_then(|(n, g)| self.get_object(n, g).ok().and_then(|o| o.as_integer()))
1225                });
1226                if let Some(count) = count {
1227                    let count = count as u32;
1228                    if count <= MAX_PAGE_COUNT {
1229                        return Ok(count);
1230                    }
1231                    tracing::warn!(
1232                        "PDF /Count {} exceeds limit {}, falling back to Kids array length",
1233                        count,
1234                        MAX_PAGE_COUNT
1235                    );
1236                    // Fall through to Kids counting
1237                }
1238
1239                // If Count is missing, invalid, or exceeds limit, count the
1240                // Kids array — inline or resolved from an indirect reference.
1241                if let Some(len) = kids_len_inline {
1242                    return Ok(len as u32);
1243                }
1244                if let Some((n, g)) = kids_ref {
1245                    if let Some(len) = self
1246                        .get_object(n, g)
1247                        .ok()
1248                        .and_then(|o| o.as_array())
1249                        .map(|a| a.0.len())
1250                    {
1251                        return Ok(len as u32);
1252                    }
1253                }
1254
1255                Ok(0)
1256            }
1257            Err(_) => {
1258                // If standard method fails, try fallback extraction
1259                tracing::debug!("Standard page extraction failed, trying direct extraction");
1260                self.page_count_fallback()
1261            }
1262        }
1263    }
1264
1265    /// Fallback method to extract page count directly from content for corrupted PDFs
1266    fn page_count_fallback(&mut self) -> ParseResult<u32> {
1267        // Try to extract from linearization info first (object 100 usually)
1268        if let Some(count) = self.extract_page_count_from_linearization() {
1269            tracing::debug!("Found page count {} from linearization", count);
1270            return Ok(count);
1271        }
1272
1273        // Fallback: count individual page objects
1274        if let Some(count) = self.count_page_objects_directly() {
1275            tracing::debug!("Found {} pages by counting page objects", count);
1276            return Ok(count);
1277        }
1278
1279        Ok(0)
1280    }
1281
1282    /// Extract page count from linearization info (object 100 usually)
1283    fn extract_page_count_from_linearization(&mut self) -> Option<u32> {
1284        // Try to get object 100 which often contains linearization info
1285        match self.get_object(100, 0) {
1286            Ok(obj) => {
1287                tracing::debug!("Found object 100: {:?}", obj);
1288                if let Some(dict) = obj.as_dict() {
1289                    tracing::debug!("Object 100 is a dictionary with {} keys", dict.0.len());
1290                    // Look for /N (number of pages) in linearization dictionary
1291                    if let Some(n_obj) = dict.get("N") {
1292                        tracing::debug!("Found /N field: {:?}", n_obj);
1293                        if let Some(count) = n_obj.as_integer() {
1294                            tracing::debug!("Extracted page count from linearization: {}", count);
1295                            return Some(count as u32);
1296                        }
1297                    } else {
1298                        tracing::debug!("No /N field found in object 100");
1299                        for (key, value) in &dict.0 {
1300                            tracing::debug!("  {:?}: {:?}", key, value);
1301                        }
1302                    }
1303                } else {
1304                    tracing::debug!("Object 100 is not a dictionary: {:?}", obj);
1305                }
1306            }
1307            Err(e) => {
1308                tracing::debug!("Failed to get object 100: {:?}", e);
1309                tracing::debug!("Attempting direct content extraction...");
1310                // If parser fails, try direct extraction from raw content
1311                return self.extract_n_value_from_raw_object_100();
1312            }
1313        }
1314
1315        None
1316    }
1317
1318    fn extract_n_value_from_raw_object_100(&mut self) -> Option<u32> {
1319        // Find object 100 in the XRef table
1320        if let Some(entry) = self.xref.get_entry(100) {
1321            // Seek to the object's position
1322            if self.reader.seek(SeekFrom::Start(entry.offset)).is_err() {
1323                return None;
1324            }
1325
1326            // Read a reasonable chunk of data around the object
1327            let mut buffer = vec![0u8; 1024];
1328            if let Ok(bytes_read) = self.reader.read(&mut buffer) {
1329                if bytes_read == 0 {
1330                    return None;
1331                }
1332
1333                // Convert to string for pattern matching
1334                let content = String::from_utf8_lossy(&buffer[..bytes_read]);
1335                tracing::debug!("Raw content around object 100:\n{}", content);
1336
1337                // Look for /N followed by a number
1338                if let Some(n_pos) = content.find("/N ") {
1339                    let after_n = &content[n_pos + 3..];
1340                    tracing::debug!(
1341                        "Content after /N: {}",
1342                        truncate_on_char_boundary(after_n, 50)
1343                    );
1344
1345                    // Extract the number that follows /N
1346                    let mut num_str = String::new();
1347                    for ch in after_n.chars() {
1348                        if ch.is_ascii_digit() {
1349                            num_str.push(ch);
1350                        } else if !num_str.is_empty() {
1351                            // Stop when we hit a non-digit after finding digits
1352                            break;
1353                        }
1354                        // Skip non-digits at the beginning
1355                    }
1356
1357                    if !num_str.is_empty() {
1358                        if let Ok(page_count) = num_str.parse::<u32>() {
1359                            tracing::debug!(
1360                                "Extracted page count from raw content: {}",
1361                                page_count
1362                            );
1363                            return Some(page_count);
1364                        }
1365                    }
1366                }
1367            }
1368        }
1369        None
1370    }
1371
1372    #[allow(dead_code)]
1373    fn find_object_pattern(&mut self, obj_num: u32, gen_num: u16) -> Option<u64> {
1374        let pattern = format!("{} {} obj", obj_num, gen_num);
1375
1376        // Save current position
1377        let original_pos = self.reader.stream_position().unwrap_or(0);
1378
1379        // Search from the beginning of the file
1380        if self.reader.seek(SeekFrom::Start(0)).is_err() {
1381            return None;
1382        }
1383
1384        // Read the entire file in chunks to search for the pattern
1385        let mut buffer = vec![0u8; 8192];
1386        let mut file_content = Vec::new();
1387
1388        loop {
1389            match self.reader.read(&mut buffer) {
1390                Ok(0) => break, // EOF
1391                Ok(bytes_read) => {
1392                    file_content.extend_from_slice(&buffer[..bytes_read]);
1393                }
1394                Err(_) => return None,
1395            }
1396        }
1397
1398        // Convert to string and search
1399        let content = String::from_utf8_lossy(&file_content);
1400        if let Some(pattern_pos) = content.find(&pattern) {
1401            // Now search for the << after the pattern
1402            let after_pattern = pattern_pos + pattern.len();
1403            let search_area = &content[after_pattern..];
1404
1405            if let Some(dict_start_offset) = search_area.find("<<") {
1406                let dict_start_pos = after_pattern + dict_start_offset;
1407
1408                // Restore original position
1409                self.reader.seek(SeekFrom::Start(original_pos)).ok();
1410                return Some(dict_start_pos as u64);
1411            } else {
1412            }
1413        }
1414
1415        // Restore original position
1416        self.reader.seek(SeekFrom::Start(original_pos)).ok();
1417        None
1418    }
1419
1420    /// Determine if we should attempt manual reconstruction for this error
1421    fn can_attempt_manual_reconstruction(&self, error: &ParseError) -> bool {
1422        match error {
1423            // These are the types of errors that might be fixable with manual reconstruction
1424            ParseError::SyntaxError { .. } => true,
1425            ParseError::UnexpectedToken { .. } => true,
1426            // Don't attempt reconstruction for other error types
1427            _ => false,
1428        }
1429    }
1430
1431    /// Check if an object can be manually reconstructed
1432    fn is_reconstructible_object(&self, obj_num: u32) -> bool {
1433        // Known problematic objects for corrupted PDF reconstruction
1434        if obj_num == 102 || obj_num == 113 || obj_num == 114 {
1435            return true;
1436        }
1437
1438        // Page objects that we found in find_page_objects scan
1439        // These are the 44 page objects from the corrupted PDF
1440        let page_objects = [
1441            1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 30, 34, 37, 39, 42, 44, 46, 49, 52,
1442            54, 56, 58, 60, 62, 64, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 104,
1443        ];
1444
1445        // Content stream objects and other critical objects
1446        // These are referenced by page objects for content streams
1447        let content_objects = [
1448            2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 29, 31, 32, 33, 35, 36, 38, 40, 41,
1449            43, 45, 47, 48, 50, 51, 53, 55, 57, 59, 61, 63, 65, 66, 68, 70, 72, 74, 76, 78, 80, 82,
1450            84, 86, 88, 90, 92, 94, 95, 96, 97, 98, 99, 100, 101, 105, 106, 107, 108, 109, 110,
1451            111,
1452        ];
1453
1454        page_objects.contains(&obj_num) || content_objects.contains(&obj_num)
1455    }
1456
1457    /// Check if an object number is a page object
1458    fn is_page_object(&self, obj_num: u32) -> bool {
1459        let page_objects = [
1460            1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 30, 34, 37, 39, 42, 44, 46, 49, 52,
1461            54, 56, 58, 60, 62, 64, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 104,
1462        ];
1463        page_objects.contains(&obj_num)
1464    }
1465
1466    /// Parse page dictionary content from raw string
1467    fn parse_page_dictionary_content(
1468        &self,
1469        dict_content: &str,
1470        result_dict: &mut std::collections::HashMap<
1471            crate::parser::objects::PdfName,
1472            crate::parser::objects::PdfObject,
1473        >,
1474        _obj_num: u32,
1475    ) -> ParseResult<()> {
1476        use crate::parser::objects::{PdfArray, PdfName, PdfObject};
1477        use std::collections::HashMap;
1478
1479        // Parse MediaBox: [ 0 0 612 792 ]
1480        if let Some(mediabox_start) = dict_content.find("/MediaBox") {
1481            let mediabox_area = &dict_content[mediabox_start..];
1482            if let Some(mediabox_content) = slice_between(mediabox_area, b'[', b']') {
1483                let values: Vec<f32> = mediabox_content
1484                    .split_whitespace()
1485                    .filter_map(|s| s.parse().ok())
1486                    .collect();
1487
1488                if values.len() == 4 {
1489                    let mediabox = PdfArray(vec![
1490                        PdfObject::Integer(values[0] as i64),
1491                        PdfObject::Integer(values[1] as i64),
1492                        PdfObject::Integer(values[2] as i64),
1493                        PdfObject::Integer(values[3] as i64),
1494                    ]);
1495                    result_dict.insert(PdfName("MediaBox".to_string()), PdfObject::Array(mediabox));
1496                }
1497            }
1498        }
1499
1500        // Parse Contents reference: /Contents 2 0 R
1501        if let Some(contents_match) = dict_content.find("/Contents") {
1502            let contents_area = &dict_content[contents_match..];
1503            // Look for pattern like "2 0 R"
1504            let parts: Vec<&str> = contents_area.split_whitespace().collect();
1505            if parts.len() >= 3 {
1506                if let (Ok(obj_ref), Ok(gen_ref)) =
1507                    (parts[1].parse::<u32>(), parts[2].parse::<u16>())
1508                {
1509                    if parts.len() > 3 && parts[3] == "R" {
1510                        result_dict.insert(
1511                            PdfName("Contents".to_string()),
1512                            PdfObject::Reference(obj_ref, gen_ref),
1513                        );
1514                    }
1515                }
1516            }
1517        }
1518
1519        // Parse Parent reference: /Parent 114 0 R -> change to 113 0 R (our reconstructed Pages object)
1520        if dict_content.contains("/Parent") {
1521            result_dict.insert(
1522                PdfName("Parent".to_string()),
1523                PdfObject::Reference(113, 0), // Always point to our reconstructed Pages object
1524            );
1525        }
1526
1527        // Parse Resources (improved implementation)
1528        if dict_content.contains("/Resources") {
1529            if let Ok(parsed_resources) = self.parse_resources_from_content(&dict_content) {
1530                result_dict.insert(PdfName("Resources".to_string()), parsed_resources);
1531            } else {
1532                // Fallback to empty Resources
1533                let resources = HashMap::new();
1534                result_dict.insert(
1535                    PdfName("Resources".to_string()),
1536                    PdfObject::Dictionary(crate::parser::objects::PdfDictionary(resources)),
1537                );
1538            }
1539        }
1540
1541        Ok(())
1542    }
1543
1544    /// Attempt to manually reconstruct an object as a fallback
1545    fn attempt_manual_object_reconstruction(
1546        &mut self,
1547        obj_num: u32,
1548        gen_num: u16,
1549        _current_offset: u64,
1550    ) -> ParseResult<&PdfObject> {
1551        // PROTECTION 1: Circular reference detection
1552        let is_circular = self
1553            .objects_being_reconstructed
1554            .lock()
1555            .map_err(|_| ParseError::SyntaxError {
1556                position: 0,
1557                message: "Mutex poisoned during circular reference check".to_string(),
1558            })?
1559            .contains(&obj_num);
1560
1561        if is_circular {
1562            tracing::debug!(
1563                "Warning: Circular reconstruction detected for object {} {} - attempting manual extraction",
1564                obj_num, gen_num
1565            );
1566
1567            // Instead of immediately returning Null, try to manually extract the object
1568            // This is particularly important for stream objects where /Length creates
1569            // a false circular dependency, but the stream data is actually available
1570            match self.extract_object_or_stream_manually(obj_num) {
1571                Ok(obj) => {
1572                    tracing::debug!(
1573                        "         Successfully extracted object {} {} manually despite circular reference",
1574                        obj_num, gen_num
1575                    );
1576                    self.object_cache.insert((obj_num, gen_num), obj);
1577                    return Ok(&self.object_cache[&(obj_num, gen_num)]);
1578                }
1579                Err(e) => {
1580                    tracing::debug!(
1581                        "         Manual extraction failed: {} - breaking cycle with null object",
1582                        e
1583                    );
1584                    // Only return Null if we truly can't reconstruct it
1585                    self.object_cache
1586                        .insert((obj_num, gen_num), PdfObject::Null);
1587                    return Ok(&self.object_cache[&(obj_num, gen_num)]);
1588                }
1589            }
1590        }
1591
1592        // PROTECTION 2: Depth limit check
1593        let current_depth = self
1594            .objects_being_reconstructed
1595            .lock()
1596            .map_err(|_| ParseError::SyntaxError {
1597                position: 0,
1598                message: "Mutex poisoned during depth check".to_string(),
1599            })?
1600            .len() as u32;
1601        if current_depth >= self.max_reconstruction_depth {
1602            return Err(ParseError::SyntaxError {
1603                position: 0,
1604                message: format!(
1605                    "Maximum reconstruction depth ({}) exceeded for object {} {}",
1606                    self.max_reconstruction_depth, obj_num, gen_num
1607                ),
1608            });
1609        }
1610
1611        // Mark as being reconstructed (prevents circular references)
1612        self.objects_being_reconstructed
1613            .lock()
1614            .map_err(|_| ParseError::SyntaxError {
1615                position: 0,
1616                message: "Mutex poisoned while marking object as being reconstructed".to_string(),
1617            })?
1618            .insert(obj_num);
1619
1620        // Try multiple reconstruction strategies
1621        let reconstructed_obj = match self.smart_object_reconstruction(obj_num, gen_num) {
1622            Ok(obj) => obj,
1623            Err(_) => {
1624                // Fallback to old method
1625                match self.extract_object_or_stream_manually(obj_num) {
1626                    Ok(obj) => obj,
1627                    Err(e) => {
1628                        // Last resort: create a null object
1629                        if self.options.lenient_syntax {
1630                            PdfObject::Null
1631                        } else {
1632                            // Unmark before returning error (best effort - ignore if mutex poisoned)
1633                            if let Ok(mut guard) = self.objects_being_reconstructed.lock() {
1634                                guard.remove(&obj_num);
1635                            }
1636                            return Err(e);
1637                        }
1638                    }
1639                }
1640            }
1641        };
1642
1643        // Unmark (reconstruction complete)
1644        self.objects_being_reconstructed
1645            .lock()
1646            .map_err(|_| ParseError::SyntaxError {
1647                position: 0,
1648                message: "Mutex poisoned while unmarking reconstructed object".to_string(),
1649            })?
1650            .remove(&obj_num);
1651
1652        self.object_cache
1653            .insert((obj_num, gen_num), reconstructed_obj);
1654
1655        // Also add to XRef table so the object can be found later
1656        use crate::parser::xref::XRefEntry;
1657        let xref_entry = XRefEntry {
1658            offset: 0, // Dummy offset since object is cached
1659            generation: gen_num,
1660            in_use: true,
1661        };
1662        self.xref.add_entry(obj_num, xref_entry);
1663
1664        self.object_cache
1665            .get(&(obj_num, gen_num))
1666            .ok_or_else(|| ParseError::SyntaxError {
1667                position: 0,
1668                message: format!(
1669                    "Object {} {} not in cache after reconstruction",
1670                    obj_num, gen_num
1671                ),
1672            })
1673    }
1674
1675    /// Smart object reconstruction using multiple heuristics
1676    fn smart_object_reconstruction(
1677        &mut self,
1678        obj_num: u32,
1679        gen_num: u16,
1680    ) -> ParseResult<PdfObject> {
1681        // Using objects from parent scope
1682
1683        // Strategy 1: Try to infer object type from context
1684        if let Ok(inferred_obj) = self.infer_object_from_context(obj_num) {
1685            return Ok(inferred_obj);
1686        }
1687
1688        // Strategy 2: Scan for object patterns in raw data
1689        if let Ok(scanned_obj) = self.scan_for_object_patterns(obj_num) {
1690            return Ok(scanned_obj);
1691        }
1692
1693        // Strategy 3: Create synthetic object based on common PDF structures
1694        if let Ok(synthetic_obj) = self.create_synthetic_object(obj_num) {
1695            return Ok(synthetic_obj);
1696        }
1697
1698        Err(ParseError::SyntaxError {
1699            position: 0,
1700            message: format!("Could not reconstruct object {} {}", obj_num, gen_num),
1701        })
1702    }
1703
1704    /// Infer object type from usage context in other objects
1705    fn infer_object_from_context(&mut self, obj_num: u32) -> ParseResult<PdfObject> {
1706        // Using objects from parent scope
1707
1708        // Scan existing objects to see how this object is referenced
1709        for (_key, obj) in self.object_cache.iter() {
1710            if let PdfObject::Dictionary(dict) = obj {
1711                for (key, value) in dict.0.iter() {
1712                    if let PdfObject::Reference(ref_num, _) = value {
1713                        if *ref_num == obj_num {
1714                            // This object is referenced as {key}, infer its type
1715                            match key.as_str() {
1716                                "Font" | "F1" | "F2" | "F3" => {
1717                                    return Ok(self.create_font_object(obj_num));
1718                                }
1719                                "XObject" | "Image" | "Im1" => {
1720                                    return Ok(self.create_xobject(obj_num));
1721                                }
1722                                "Contents" => {
1723                                    return Ok(self.create_content_stream(obj_num));
1724                                }
1725                                "Resources" => {
1726                                    return Ok(self.create_resources_dict(obj_num));
1727                                }
1728                                _ => continue,
1729                            }
1730                        }
1731                    }
1732                }
1733            }
1734        }
1735
1736        Err(ParseError::SyntaxError {
1737            position: 0,
1738            message: "Cannot infer object type from context".to_string(),
1739        })
1740    }
1741
1742    /// Scan raw PDF data for object patterns
1743    fn scan_for_object_patterns(&mut self, obj_num: u32) -> ParseResult<PdfObject> {
1744        // This would scan the raw PDF bytes for patterns like "obj_num 0 obj"
1745        // and try to extract whatever follows, with better error recovery
1746        self.extract_object_or_stream_manually(obj_num)
1747    }
1748
1749    /// Create synthetic objects for common PDF structures
1750    fn create_synthetic_object(&mut self, obj_num: u32) -> ParseResult<PdfObject> {
1751        use super::objects::{PdfDictionary, PdfName, PdfObject};
1752
1753        // Common object numbers and their likely types
1754        match obj_num {
1755            1..=10 => {
1756                // Usually structural objects (catalog, pages, etc.)
1757                let mut dict = PdfDictionary::new();
1758                dict.insert(
1759                    "Type".to_string(),
1760                    PdfObject::Name(PdfName("Null".to_string())),
1761                );
1762                Ok(PdfObject::Dictionary(dict))
1763            }
1764            _ => {
1765                // Generic null object
1766                Ok(PdfObject::Null)
1767            }
1768        }
1769    }
1770
1771    fn create_font_object(&self, _obj_num: u32) -> PdfObject {
1772        use super::objects::{PdfDictionary, PdfName, PdfObject};
1773        let mut font_dict = PdfDictionary::new();
1774        font_dict.insert(
1775            "Type".to_string(),
1776            PdfObject::Name(PdfName("Font".to_string())),
1777        );
1778        font_dict.insert(
1779            "Subtype".to_string(),
1780            PdfObject::Name(PdfName("Type1".to_string())),
1781        );
1782        font_dict.insert(
1783            "BaseFont".to_string(),
1784            PdfObject::Name(PdfName("Helvetica".to_string())),
1785        );
1786        PdfObject::Dictionary(font_dict)
1787    }
1788
1789    fn create_xobject(&self, _obj_num: u32) -> PdfObject {
1790        use super::objects::{PdfDictionary, PdfName, PdfObject};
1791        let mut xobj_dict = PdfDictionary::new();
1792        xobj_dict.insert(
1793            "Type".to_string(),
1794            PdfObject::Name(PdfName("XObject".to_string())),
1795        );
1796        xobj_dict.insert(
1797            "Subtype".to_string(),
1798            PdfObject::Name(PdfName("Form".to_string())),
1799        );
1800        PdfObject::Dictionary(xobj_dict)
1801    }
1802
1803    fn create_content_stream(&self, _obj_num: u32) -> PdfObject {
1804        use super::objects::{PdfDictionary, PdfObject, PdfStream};
1805        let mut stream_dict = PdfDictionary::new();
1806        stream_dict.insert("Length".to_string(), PdfObject::Integer(0));
1807
1808        let stream = PdfStream {
1809            dict: stream_dict,
1810            data: Vec::new(),
1811        };
1812        PdfObject::Stream(stream)
1813    }
1814
1815    fn create_resources_dict(&self, _obj_num: u32) -> PdfObject {
1816        use super::objects::{PdfArray, PdfDictionary, PdfObject};
1817        let mut res_dict = PdfDictionary::new();
1818        res_dict.insert("ProcSet".to_string(), PdfObject::Array(PdfArray::new()));
1819        PdfObject::Dictionary(res_dict)
1820    }
1821
1822    fn extract_object_manually(
1823        &mut self,
1824        obj_num: u32,
1825    ) -> ParseResult<crate::parser::objects::PdfDictionary> {
1826        use crate::parser::objects::{PdfArray, PdfDictionary, PdfName, PdfObject};
1827        use std::collections::HashMap;
1828
1829        // Save current position
1830        let original_pos = self.reader.stream_position().unwrap_or(0);
1831
1832        // Issue #339: locate the object header via the bounded chunked scanner and
1833        // read only a bounded window at its offset, instead of buffering the whole
1834        // file. Peak memory stays O(window) regardless of file size.
1835        let window = match read_object_window(&mut self.reader, obj_num, MANUAL_DICT_WINDOW) {
1836            Ok(Some((_, w))) => w,
1837            Ok(None) => {
1838                self.reader.seek(SeekFrom::Start(original_pos)).ok();
1839                return Err(ParseError::SyntaxError {
1840                    position: 0,
1841                    message: format!("Object {obj_num} not found in manual extraction"),
1842                });
1843            }
1844            Err(_) => {
1845                self.reader.seek(SeekFrom::Start(original_pos)).ok();
1846                return Err(ParseError::SyntaxError {
1847                    position: 0,
1848                    message: "Failed to read file for manual extraction".to_string(),
1849                });
1850            }
1851        };
1852
1853        let content = String::from_utf8_lossy(&window);
1854
1855        // Find the object content based on object number
1856        let pattern = format!("{} 0 obj", obj_num);
1857        if let Some(start) = content.find(&pattern) {
1858            let search_area = &content[start..];
1859            if let Some(dict_start) = search_area.find("<<") {
1860                let after_bracket = &search_area[dict_start + 2..];
1861
1862                if let Some(dict_end) = find_dict_end(after_bracket) {
1863                    let dict_content = &after_bracket[..dict_end];
1864
1865                    // Manually parse the object content based on object number
1866                    let mut result_dict = HashMap::new();
1867
1868                    // FIX for Issue #83: Generic catalog parsing for ANY object number
1869                    // Check if this is a Catalog object (regardless of object number)
1870                    if dict_content.contains("/Type/Catalog")
1871                        || dict_content.contains("/Type /Catalog")
1872                    {
1873                        result_dict.insert(
1874                            PdfName("Type".to_string()),
1875                            PdfObject::Name(PdfName("Catalog".to_string())),
1876                        );
1877
1878                        // Parse /Pages reference using regex-like pattern matching
1879                        // Pattern: /Pages <number> <gen> R
1880                        // Note: PDF can have compact format like "/Pages 13 0 R" or "/Pages13 0 R"
1881                        if let Some(pages_start) = dict_content.find("/Pages") {
1882                            let after_pages = &dict_content[pages_start + 6..]; // Skip "/Pages"
1883                                                                                // Trim any leading whitespace, then extract numbers
1884                            let trimmed = after_pages.trim_start();
1885                            // Split by whitespace to get object number, generation, and "R"
1886                            let parts: Vec<&str> = trimmed.split_whitespace().collect();
1887                            if parts.len() >= 3 {
1888                                // parts[0] should be the object number
1889                                // parts[1] should be the generation
1890                                // parts[2] should be "R" or "R/..." (compact format)
1891                                if let (Ok(obj), Ok(gen)) =
1892                                    (parts[0].parse::<u32>(), parts[1].parse::<u16>())
1893                                {
1894                                    if parts[2] == "R" || parts[2].starts_with('R') {
1895                                        result_dict.insert(
1896                                            PdfName("Pages".to_string()),
1897                                            PdfObject::Reference(obj, gen),
1898                                        );
1899                                    }
1900                                }
1901                            }
1902                        }
1903
1904                        // Parse other common catalog entries
1905                        // /Version
1906                        if let Some(ver_start) = dict_content.find("/Version") {
1907                            let after_ver = &dict_content[ver_start + 8..];
1908                            if let Some(ver_end) = after_ver.find(|c: char| c == '/' || c == '>') {
1909                                let version_str = after_ver[..ver_end].trim();
1910                                result_dict.insert(
1911                                    PdfName("Version".to_string()),
1912                                    PdfObject::Name(PdfName(
1913                                        version_str.trim_start_matches('/').to_string(),
1914                                    )),
1915                                );
1916                            }
1917                        }
1918
1919                        // /Metadata reference
1920                        if let Some(meta_start) = dict_content.find("/Metadata") {
1921                            let after_meta = &dict_content[meta_start + 9..];
1922                            let parts: Vec<&str> = after_meta.split_whitespace().collect();
1923                            if parts.len() >= 3 {
1924                                if let (Ok(obj), Ok(gen)) =
1925                                    (parts[0].parse::<u32>(), parts[1].parse::<u16>())
1926                                {
1927                                    if parts[2] == "R" {
1928                                        result_dict.insert(
1929                                            PdfName("Metadata".to_string()),
1930                                            PdfObject::Reference(obj, gen),
1931                                        );
1932                                    }
1933                                }
1934                            }
1935                        }
1936
1937                        // /AcroForm reference
1938                        if let Some(acro_start) = dict_content.find("/AcroForm") {
1939                            let after_acro = &dict_content[acro_start + 9..];
1940                            // Check if it's a reference or dictionary
1941                            if after_acro.trim_start().starts_with("<<") {
1942                                // It's an inline dictionary, skip for now (too complex)
1943                            } else {
1944                                let parts: Vec<&str> = after_acro.split_whitespace().collect();
1945                                if parts.len() >= 3 {
1946                                    if let (Ok(obj), Ok(gen)) =
1947                                        (parts[0].parse::<u32>(), parts[1].parse::<u16>())
1948                                    {
1949                                        if parts[2] == "R" {
1950                                            result_dict.insert(
1951                                                PdfName("AcroForm".to_string()),
1952                                                PdfObject::Reference(obj, gen),
1953                                            );
1954                                        }
1955                                    }
1956                                }
1957                            }
1958                        }
1959                    } else if obj_num == 102 {
1960                        // Verify this is actually a catalog before reconstructing
1961                        if dict_content.contains("/Type /Catalog") {
1962                            // Parse catalog object
1963                            result_dict.insert(
1964                                PdfName("Type".to_string()),
1965                                PdfObject::Name(PdfName("Catalog".to_string())),
1966                            );
1967
1968                            // Parse "/Dests 139 0 R"
1969                            if dict_content.contains("/Dests 139 0 R") {
1970                                result_dict.insert(
1971                                    PdfName("Dests".to_string()),
1972                                    PdfObject::Reference(139, 0),
1973                                );
1974                            }
1975
1976                            // Parse "/Pages 113 0 R"
1977                            if dict_content.contains("/Pages 113 0 R") {
1978                                result_dict.insert(
1979                                    PdfName("Pages".to_string()),
1980                                    PdfObject::Reference(113, 0),
1981                                );
1982                            }
1983                        } else {
1984                            // This object 102 is not a catalog, don't reconstruct it
1985                            // Restore original position
1986                            self.reader.seek(SeekFrom::Start(original_pos)).ok();
1987                            return Err(ParseError::SyntaxError {
1988                                position: 0,
1989                                message:
1990                                    "Object 102 is not a corrupted catalog, cannot reconstruct"
1991                                        .to_string(),
1992                            });
1993                        }
1994                    } else if obj_num == 113 {
1995                        // Object 113 is the main Pages object - need to find all Page objects
1996
1997                        result_dict.insert(
1998                            PdfName("Type".to_string()),
1999                            PdfObject::Name(PdfName("Pages".to_string())),
2000                        );
2001
2002                        // Find all Page objects in the PDF
2003                        let page_refs = match self.find_page_objects() {
2004                            Ok(refs) => refs,
2005                            Err(_e) => {
2006                                vec![]
2007                            }
2008                        };
2009
2010                        // Set count based on actual found pages
2011                        let page_count = if page_refs.is_empty() {
2012                            44
2013                        } else {
2014                            page_refs.len() as i64
2015                        };
2016                        result_dict
2017                            .insert(PdfName("Count".to_string()), PdfObject::Integer(page_count));
2018
2019                        // Create Kids array with real page object references
2020                        let kids_array: Vec<PdfObject> = page_refs
2021                            .into_iter()
2022                            .map(|(obj_num, gen_num)| PdfObject::Reference(obj_num, gen_num))
2023                            .collect();
2024
2025                        result_dict.insert(
2026                            PdfName("Kids".to_string()),
2027                            PdfObject::Array(PdfArray(kids_array)),
2028                        );
2029                    } else if obj_num == 114 {
2030                        // Parse object 114 - this should be a Pages object based on the string output
2031
2032                        result_dict.insert(
2033                            PdfName("Type".to_string()),
2034                            PdfObject::Name(PdfName("Pages".to_string())),
2035                        );
2036
2037                        // Find all Page objects in the PDF
2038                        let page_refs = match self.find_page_objects() {
2039                            Ok(refs) => refs,
2040                            Err(_e) => {
2041                                vec![]
2042                            }
2043                        };
2044
2045                        // Set count based on actual found pages
2046                        let page_count = if page_refs.is_empty() {
2047                            44
2048                        } else {
2049                            page_refs.len() as i64
2050                        };
2051                        result_dict
2052                            .insert(PdfName("Count".to_string()), PdfObject::Integer(page_count));
2053
2054                        // Create Kids array with real page object references
2055                        let kids_array: Vec<PdfObject> = page_refs
2056                            .into_iter()
2057                            .map(|(obj_num, gen_num)| PdfObject::Reference(obj_num, gen_num))
2058                            .collect();
2059
2060                        result_dict.insert(
2061                            PdfName("Kids".to_string()),
2062                            PdfObject::Array(PdfArray(kids_array)),
2063                        );
2064                    } else if self.is_page_object(obj_num) {
2065                        // This is a page object - parse the page dictionary
2066
2067                        result_dict.insert(
2068                            PdfName("Type".to_string()),
2069                            PdfObject::Name(PdfName("Page".to_string())),
2070                        );
2071
2072                        // Parse standard page entries from the found dictionary content
2073                        self.parse_page_dictionary_content(
2074                            &dict_content,
2075                            &mut result_dict,
2076                            obj_num,
2077                        )?;
2078                    }
2079
2080                    // Restore original position
2081                    self.reader.seek(SeekFrom::Start(original_pos)).ok();
2082
2083                    return Ok(PdfDictionary(result_dict));
2084                }
2085            }
2086        }
2087
2088        // Restore original position
2089        self.reader.seek(SeekFrom::Start(original_pos)).ok();
2090
2091        // Special case: if object 113 or 114 was not found in PDF, create fallback objects
2092        if obj_num == 113 {
2093            let mut result_dict = HashMap::new();
2094            result_dict.insert(
2095                PdfName("Type".to_string()),
2096                PdfObject::Name(PdfName("Pages".to_string())),
2097            );
2098
2099            // Find all Page objects in the PDF
2100            let page_refs = match self.find_page_objects() {
2101                Ok(refs) => refs,
2102                Err(_e) => {
2103                    vec![]
2104                }
2105            };
2106
2107            // Set count based on actual found pages
2108            let page_count = if page_refs.is_empty() {
2109                44
2110            } else {
2111                page_refs.len() as i64
2112            };
2113            result_dict.insert(PdfName("Count".to_string()), PdfObject::Integer(page_count));
2114
2115            // Create Kids array with real page object references
2116            let kids_array: Vec<PdfObject> = page_refs
2117                .into_iter()
2118                .map(|(obj_num, gen_num)| PdfObject::Reference(obj_num, gen_num))
2119                .collect();
2120
2121            result_dict.insert(
2122                PdfName("Kids".to_string()),
2123                PdfObject::Array(PdfArray(kids_array)),
2124            );
2125
2126            return Ok(PdfDictionary(result_dict));
2127        } else if obj_num == 114 {
2128            let mut result_dict = HashMap::new();
2129            result_dict.insert(
2130                PdfName("Type".to_string()),
2131                PdfObject::Name(PdfName("Pages".to_string())),
2132            );
2133
2134            // Find all Page objects in the PDF
2135            let page_refs = match self.find_page_objects() {
2136                Ok(refs) => refs,
2137                Err(_e) => {
2138                    vec![]
2139                }
2140            };
2141
2142            // Set count based on actual found pages
2143            let page_count = if page_refs.is_empty() {
2144                44
2145            } else {
2146                page_refs.len() as i64
2147            };
2148            result_dict.insert(PdfName("Count".to_string()), PdfObject::Integer(page_count));
2149
2150            // Create Kids array with real page object references
2151            let kids_array: Vec<PdfObject> = page_refs
2152                .into_iter()
2153                .map(|(obj_num, gen_num)| PdfObject::Reference(obj_num, gen_num))
2154                .collect();
2155
2156            result_dict.insert(
2157                PdfName("Kids".to_string()),
2158                PdfObject::Array(PdfArray(kids_array)),
2159            );
2160
2161            return Ok(PdfDictionary(result_dict));
2162        }
2163
2164        Err(ParseError::SyntaxError {
2165            position: 0,
2166            message: "Could not find catalog dictionary in manual extraction".to_string(),
2167        })
2168    }
2169
2170    /// Extract object manually, detecting whether it's a dictionary or stream
2171    fn extract_object_or_stream_manually(&mut self, obj_num: u32) -> ParseResult<PdfObject> {
2172        use crate::parser::objects::PdfObject;
2173
2174        // Save current position
2175        let original_pos = self.reader.stream_position().unwrap_or(0);
2176
2177        // Issue #339: locate the object via the bounded early-stopping scan and read
2178        // only a bounded window at its offset, instead of buffering the whole file.
2179        // The stream body (which can be large, e.g. XMP metadata) is read separately,
2180        // bounded by its /Length.
2181        let (obj_offset, window) =
2182            match read_object_window(&mut self.reader, obj_num, MANUAL_DICT_WINDOW) {
2183                Ok(Some(v)) => v,
2184                Ok(None) => {
2185                    self.reader.seek(SeekFrom::Start(original_pos)).ok();
2186                    return Err(ParseError::SyntaxError {
2187                        position: 0,
2188                        message: format!("Could not manually extract object {obj_num}"),
2189                    });
2190                }
2191                Err(_) => {
2192                    self.reader.seek(SeekFrom::Start(original_pos)).ok();
2193                    return Err(ParseError::SyntaxError {
2194                        position: 0,
2195                        message: "Failed to read file for manual extraction".to_string(),
2196                    });
2197                }
2198            };
2199
2200        // The window starts at the "N G obj" header; the object dictionary is the
2201        // first "<<" that follows.
2202        if let Some(dict_start) = find_byte_pattern(&window, b"<<") {
2203            // Handle nested dictionaries properly by counting brackets
2204            let mut bracket_count = 1;
2205            let mut pos = dict_start + 2;
2206            let mut dict_end = None;
2207
2208            while pos < window.len().saturating_sub(1) && bracket_count > 0 {
2209                if window[pos] == b'<' && window[pos + 1] == b'<' {
2210                    bracket_count += 1;
2211                    pos += 2;
2212                } else if window[pos] == b'>' && window[pos + 1] == b'>' {
2213                    bracket_count -= 1;
2214                    if bracket_count == 0 {
2215                        dict_end = Some(pos);
2216                        break;
2217                    }
2218                    pos += 2;
2219                } else {
2220                    pos += 1;
2221                }
2222            }
2223
2224            if let Some(dict_end_pos) = dict_end {
2225                let dict_content = String::from_utf8_lossy(&window[dict_start + 2..dict_end_pos]);
2226                // Full `<<...>>` slice, for generic re-parsing of the dictionary.
2227                let dict_bytes = &window[dict_start..dict_end_pos + 2];
2228
2229                // Is the dictionary immediately followed by stream data?
2230                let after_dict = &window[dict_end_pos + 2..];
2231                if is_immediate_stream_start(after_dict) {
2232                    // Absolute file offset of after_dict[0].
2233                    let after_dict_abs = obj_offset + (dict_end_pos + 2) as u64;
2234                    return self.reconstruct_stream_object_bounded(
2235                        obj_num,
2236                        dict_bytes,
2237                        &dict_content,
2238                        after_dict_abs,
2239                        after_dict,
2240                    );
2241                } else {
2242                    // Plain dictionary object - reuse the bounded dict extractor.
2243                    self.reader.seek(SeekFrom::Start(original_pos)).ok();
2244                    return self
2245                        .extract_object_manually(obj_num)
2246                        .map(PdfObject::Dictionary);
2247                }
2248            }
2249        }
2250
2251        // Restore original position
2252        self.reader.seek(SeekFrom::Start(original_pos)).ok();
2253
2254        Err(ParseError::SyntaxError {
2255            position: 0,
2256            message: format!("Could not manually extract object {obj_num}"),
2257        })
2258    }
2259
2260    /// Reconstruct a stream object using bounded reads (Issue #339).
2261    ///
2262    /// The dictionary was already parsed from a bounded window; the stream body is
2263    /// read directly at its absolute file offset, bounded by `/Length` (or, if
2264    /// `/Length` is indirect, by resolving that length object; or, if absent, by a
2265    /// bounded scan to `endstream`). `after_dict_abs` is the absolute file offset of
2266    /// `after_dict[0]` — the bytes immediately following the dictionary's `>>`.
2267    fn reconstruct_stream_object_bounded(
2268        &mut self,
2269        obj_num: u32,
2270        dict_bytes: &[u8],
2271        dict_content: &str,
2272        after_dict_abs: u64,
2273        after_dict: &[u8],
2274    ) -> ParseResult<PdfObject> {
2275        use crate::parser::objects::{PdfDictionary, PdfName, PdfObject, PdfStream};
2276        use std::collections::HashMap;
2277
2278        // Issue #351: reconstruct the FULL stream dictionary by re-parsing the
2279        // already-in-memory `<<...>>` bytes with the real object parser, rather than
2280        // recognizing only the single hardcoded `/Filter /FlateDecode` form. This
2281        // preserves every entry generically — non-Flate filters (`/DCTDecode`,
2282        // `/LZWDecode`), filter arrays, `/DecodeParms`, `/Subtype`, `/ColorSpace`,
2283        // etc. — which the manual fallback previously dropped, silently corrupting
2284        // downstream decoding. The stream body is still read separately and bounded
2285        // (Issue #339); only the dictionary construction changed.
2286        let opts = self.options.clone();
2287        let mut dict: HashMap<PdfName, PdfObject> = {
2288            let mut lexer = super::lexer::Lexer::new_with_options(
2289                std::io::Cursor::new(dict_bytes),
2290                opts.clone(),
2291            );
2292            match PdfObject::parse_with_options(&mut lexer, &opts) {
2293                Ok(PdfObject::Dictionary(d)) => d.0,
2294                // The slice ends at `>>`, so no stream body is available here; if the
2295                // parser still reports a stream, keep its dictionary.
2296                Ok(PdfObject::Stream(s)) => s.dict.0,
2297                // Parse failure: fall back to the legacy minimal behavior so this
2298                // repair path never regresses below what it preserved before.
2299                _ => {
2300                    let mut d = HashMap::new();
2301                    if dict_content.contains("/Filter /FlateDecode") {
2302                        d.insert(
2303                            PdfName("Filter".to_string()),
2304                            PdfObject::Name(PdfName("FlateDecode".to_string())),
2305                        );
2306                    }
2307                    d
2308                }
2309            }
2310        };
2311
2312        // Resolve the stream length: direct integer, indirect reference, or unknown.
2313        let length = self.parse_stream_length(dict_content)?;
2314
2315        // Locate "stream" within the bounded window and the first byte of the data.
2316        let Some(stream_kw) = find_byte_pattern(after_dict, b"stream") else {
2317            return Err(ParseError::SyntaxError {
2318                position: 0,
2319                message: format!("Could not reconstruct stream for object {obj_num}"),
2320            });
2321        };
2322        let after_kw = stream_kw + 6; // "stream".len()
2323        let data_rel = match after_dict.get(after_kw) {
2324            Some(b'\r') if after_dict.get(after_kw + 1) == Some(&b'\n') => after_kw + 2,
2325            Some(b'\r') | Some(b'\n') => after_kw + 1,
2326            _ => after_kw,
2327        };
2328        let data_abs = after_dict_abs + data_rel as u64;
2329
2330        // Read the stream body, bounded by its real size rather than the whole file.
2331        let data = match length {
2332            Some(len) => {
2333                // Trust /Length (ISO 32000 §7.3.8.1): read exactly `len` bytes. This
2334                // is correct for binary streams whose bytes may contain "endstream",
2335                // which an endstream-search would mistakenly truncate at. Accepted
2336                // tradeoff on this repair path: a stale-too-large /Length would read
2337                // past endstream, but that is rarer than binary data containing the
2338                // marker, and trusting /Length is the ISO-correct behavior.
2339                let body = read_window_at(&mut self.reader, data_abs, len)?;
2340                dict.insert(
2341                    PdfName("Length".to_string()),
2342                    PdfObject::Integer(len as i64),
2343                );
2344                body
2345            }
2346            None => {
2347                // Unknown length: scan forward in bounded windows to `endstream`,
2348                // retaining only the stream bytes (O(stream size), not O(file)).
2349                let body = self.read_stream_until_endstream(data_abs)?;
2350                // Pin /Length to the bytes actually read. The generic dict parse may
2351                // have carried an unresolvable indirect `/Length N G R`; replacing it
2352                // keeps the dictionary consistent with the body and avoids a dangling
2353                // reference downstream.
2354                dict.insert(
2355                    PdfName("Length".to_string()),
2356                    PdfObject::Integer(body.len() as i64),
2357                );
2358                body
2359            }
2360        };
2361
2362        Ok(PdfObject::Stream(PdfStream {
2363            dict: PdfDictionary(dict),
2364            data,
2365        }))
2366    }
2367
2368    /// Parse a stream's `/Length` from its dictionary text, resolving an indirect
2369    /// reference via a bounded lookup. Returns `None` if absent or unresolvable, in
2370    /// which case the caller scans to `endstream`.
2371    fn parse_stream_length(&mut self, dict_content: &str) -> ParseResult<Option<usize>> {
2372        let Some(idx) = dict_content.find("/Length") else {
2373            return Ok(None);
2374        };
2375        let rest = dict_content[idx + "/Length".len()..].trim_start();
2376        let tokens: Vec<&str> = rest.split_whitespace().collect();
2377
2378        // Indirect reference: "N G R" (e.g. "42 0 R" — object number, generation, keyword).
2379        if tokens.len() >= 3 && tokens[2] == "R" {
2380            if let Ok(len_obj) = tokens[0].parse::<u32>() {
2381                return self.resolve_length_object(len_obj);
2382            }
2383        }
2384
2385        // Direct integer.
2386        if let Some(tok) = tokens.first() {
2387            if let Ok(n) = tok.parse::<i64>() {
2388                if n >= 0 {
2389                    return Ok(Some(n as usize));
2390                }
2391            }
2392        }
2393        Ok(None)
2394    }
2395
2396    /// Resolve an indirect `/Length` object value via a small bounded window. The
2397    /// length object is a bare integer (`N G obj <int> endobj`), so a tiny read at
2398    /// its offset suffices — no recursion through `get_object`, avoiding the false
2399    /// circular dependency this manual path exists to break.
2400    fn resolve_length_object(&mut self, len_obj: u32) -> ParseResult<Option<usize>> {
2401        let Some((_, window)) = read_object_window(&mut self.reader, len_obj, 4096)? else {
2402            return Ok(None);
2403        };
2404        let text = String::from_utf8_lossy(&window);
2405        // Skip the "N G obj" header; the first "obj" is the header keyword.
2406        if let Some(obj_pos) = text.find("obj") {
2407            let after = text[obj_pos + 3..].trim_start();
2408            let digits: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
2409            if let Ok(n) = digits.parse::<usize>() {
2410                return Ok(Some(n));
2411            }
2412        }
2413        Ok(None)
2414    }
2415
2416    /// Read a stream body of unknown length by scanning forward in bounded windows
2417    /// from `data_abs` until `endstream`, accumulating only the stream bytes. Peak
2418    /// memory is O(window + stream size), never O(file size).
2419    fn read_stream_until_endstream(&mut self, data_abs: u64) -> ParseResult<Vec<u8>> {
2420        const WIN: usize = 64 * 1024;
2421        const MAX_STREAM: usize = 64 * 1024 * 1024; // runaway guard
2422
2423        let mut acc: Vec<u8> = Vec::new();
2424        let mut offset = data_abs;
2425        let mut searched = 0usize;
2426        loop {
2427            let chunk = read_window_at(&mut self.reader, offset, WIN)?;
2428            if chunk.is_empty() {
2429                break; // EOF without endstream
2430            }
2431            acc.extend_from_slice(&chunk);
2432
2433            // Search the freshly added region with an 8-byte overlap so an
2434            // "endstream" straddling a window boundary is not missed.
2435            let from = searched.saturating_sub(b"endstream".len() - 1);
2436            if let Some(rel) = find_byte_pattern(&acc[from..], b"endstream") {
2437                acc.truncate(from + rel);
2438                break;
2439            }
2440            searched = acc.len();
2441            offset += chunk.len() as u64;
2442
2443            if chunk.len() < WIN {
2444                break; // EOF before endstream
2445            }
2446            if acc.len() > MAX_STREAM {
2447                // Runaway guard: surface the truncation so a downstream decode
2448                // failure on the incomplete body is traceable to here rather than
2449                // appearing as a cryptic error.
2450                tracing::warn!(
2451                    "stream body exceeds {} MiB runaway guard without endstream; truncating",
2452                    MAX_STREAM / (1024 * 1024)
2453                );
2454                break;
2455            }
2456        }
2457
2458        // Trim a single trailing EOL before "endstream" (not part of the data).
2459        if acc.last() == Some(&b'\n') {
2460            acc.pop();
2461            if acc.last() == Some(&b'\r') {
2462                acc.pop();
2463            }
2464        } else if acc.last() == Some(&b'\r') {
2465            acc.pop();
2466        }
2467        Ok(acc)
2468    }
2469
2470    /// Parse Resources from PDF content string
2471    fn parse_resources_from_content(&self, dict_content: &str) -> ParseResult<PdfObject> {
2472        use crate::parser::objects::{PdfDictionary, PdfName, PdfObject};
2473        use std::collections::HashMap;
2474
2475        // Find the Resources section
2476        if let Some(resources_start) = dict_content.find("/Resources") {
2477            // Find the opening bracket
2478            if let Some(bracket_start) = dict_content[resources_start..].find("<<") {
2479                let abs_bracket_start = resources_start + bracket_start + 2;
2480
2481                // Find the matching `>>`, counting nesting. Offset is relative
2482                // to the text after the opening `<<`.
2483                let after_bracket = &dict_content[abs_bracket_start..];
2484                if let Some(end_rel) = find_dict_end(after_bracket) {
2485                    let resources_content = &after_bracket[..end_rel];
2486
2487                    // Parse basic Resources structure
2488                    let mut resources_dict = HashMap::new();
2489
2490                    // Look for Font dictionary
2491                    if let Some(font_start) = resources_content.find("/Font") {
2492                        if let Some(font_bracket) = resources_content[font_start..].find("<<") {
2493                            let abs_font_start = font_start + font_bracket + 2;
2494
2495                            // Simple font parsing - look for font references
2496                            let mut font_dict = HashMap::new();
2497
2498                            // Look for font entries like /F1 123 0 R
2499                            let font_section = &resources_content[abs_font_start..];
2500                            let mut pos = 0;
2501                            while let Some(f_pos) = font_section[pos..].find("/F") {
2502                                let abs_f_pos = pos + f_pos;
2503                                if let Some(space_pos) = font_section[abs_f_pos..].find(" ") {
2504                                    let font_name = &font_section[abs_f_pos..abs_f_pos + space_pos];
2505
2506                                    // Look for object reference after the font name
2507                                    let after_name = &font_section[abs_f_pos + space_pos..];
2508                                    if let Some(r_pos) = after_name.find(" R") {
2509                                        let ref_part = after_name[..r_pos].trim();
2510                                        if let Some(parts) = ref_part
2511                                            .split_whitespace()
2512                                            .collect::<Vec<&str>>()
2513                                            .get(0..2)
2514                                        {
2515                                            if let (Ok(obj_num), Ok(gen_num)) =
2516                                                (parts[0].parse::<u32>(), parts[1].parse::<u16>())
2517                                            {
2518                                                font_dict.insert(
2519                                                    PdfName(font_name[1..].to_string()), // Remove leading /
2520                                                    PdfObject::Reference(obj_num, gen_num),
2521                                                );
2522                                            }
2523                                        }
2524                                    }
2525                                }
2526                                pos = abs_f_pos + 1;
2527                            }
2528
2529                            if !font_dict.is_empty() {
2530                                resources_dict.insert(
2531                                    PdfName("Font".to_string()),
2532                                    PdfObject::Dictionary(PdfDictionary(font_dict)),
2533                                );
2534                            }
2535                        }
2536                    }
2537
2538                    return Ok(PdfObject::Dictionary(PdfDictionary(resources_dict)));
2539                }
2540            }
2541        }
2542
2543        Err(ParseError::SyntaxError {
2544            position: 0,
2545            message: "Could not parse Resources".to_string(),
2546        })
2547    }
2548
2549    #[allow(dead_code)]
2550    fn extract_catalog_directly(
2551        &mut self,
2552        obj_num: u32,
2553        gen_num: u16,
2554    ) -> ParseResult<&PdfDictionary> {
2555        // Find the catalog object in the XRef table
2556        if let Some(entry) = self.xref.get_entry(obj_num) {
2557            // Seek to the object's position
2558            if self.reader.seek(SeekFrom::Start(entry.offset)).is_err() {
2559                return Err(ParseError::SyntaxError {
2560                    position: 0,
2561                    message: "Failed to seek to catalog object".to_string(),
2562                });
2563            }
2564
2565            // Read content around the object
2566            let mut buffer = vec![0u8; 2048];
2567            if let Ok(bytes_read) = self.reader.read(&mut buffer) {
2568                let content = String::from_utf8_lossy(&buffer[..bytes_read]);
2569                tracing::debug!("Raw catalog content:\n{}", content);
2570
2571                // Look for the dictionary pattern << ... >>
2572                if let Some(dict_start) = content.find("<<") {
2573                    if let Some(dict_end) = content[dict_start..].find(">>") {
2574                        let dict_content = &content[dict_start..dict_start + dict_end + 2];
2575                        tracing::debug!("Found dictionary content: {}", dict_content);
2576
2577                        // Try to parse this directly as a dictionary
2578                        if let Ok(dict) = self.parse_dictionary_from_string(dict_content) {
2579                            // Cache the parsed dictionary
2580                            let key = (obj_num, gen_num);
2581                            self.object_cache.insert(key, PdfObject::Dictionary(dict));
2582
2583                            // Return reference to cached object
2584                            if let Some(PdfObject::Dictionary(ref dict)) =
2585                                self.object_cache.get(&key)
2586                            {
2587                                return Ok(dict);
2588                            }
2589                        }
2590                    }
2591                }
2592            }
2593        }
2594
2595        Err(ParseError::SyntaxError {
2596            position: 0,
2597            message: "Failed to extract catalog directly".to_string(),
2598        })
2599    }
2600
2601    #[allow(dead_code)]
2602    fn parse_dictionary_from_string(&self, dict_str: &str) -> ParseResult<PdfDictionary> {
2603        use crate::parser::lexer::{Lexer, Token};
2604
2605        // Create a lexer from the dictionary string
2606        let mut cursor = std::io::Cursor::new(dict_str.as_bytes());
2607        let mut lexer = Lexer::new_with_options(&mut cursor, self.options.clone());
2608
2609        // Parse the dictionary
2610        match lexer.next_token()? {
2611            Token::DictStart => {
2612                let mut dict = std::collections::HashMap::new();
2613
2614                loop {
2615                    let token = lexer.next_token()?;
2616                    match token {
2617                        Token::DictEnd => break,
2618                        Token::Name(key) => {
2619                            // Parse the value
2620                            let value = PdfObject::parse_with_options(&mut lexer, &self.options)?;
2621                            dict.insert(crate::parser::objects::PdfName(key), value);
2622                        }
2623                        _ => {
2624                            return Err(ParseError::SyntaxError {
2625                                position: 0,
2626                                message: "Invalid dictionary format".to_string(),
2627                            });
2628                        }
2629                    }
2630                }
2631
2632                Ok(PdfDictionary(dict))
2633            }
2634            _ => Err(ParseError::SyntaxError {
2635                position: 0,
2636                message: "Expected dictionary start".to_string(),
2637            }),
2638        }
2639    }
2640
2641    /// Count page objects directly by scanning for "/Type /Page"
2642    fn count_page_objects_directly(&mut self) -> Option<u32> {
2643        let mut page_count = 0;
2644
2645        // Iterate through all objects and count those with Type = Page
2646        for obj_num in 1..self.xref.len() as u32 {
2647            if let Ok(obj) = self.get_object(obj_num, 0) {
2648                if let Some(dict) = obj.as_dict() {
2649                    if let Some(obj_type) = dict.get("Type").and_then(|t| t.as_name()) {
2650                        if obj_type.0 == "Page" {
2651                            page_count += 1;
2652                        }
2653                    }
2654                }
2655            }
2656        }
2657
2658        if page_count > 0 {
2659            Some(page_count)
2660        } else {
2661            None
2662        }
2663    }
2664
2665    /// Get metadata from the document
2666    pub fn metadata(&mut self) -> ParseResult<DocumentMetadata> {
2667        let mut metadata = DocumentMetadata::default();
2668
2669        if let Some(info_dict) = self.info()? {
2670            if let Some(title) = info_dict.get("Title").and_then(|o| o.as_string()) {
2671                metadata.title = Some(title.to_text());
2672            }
2673            if let Some(author) = info_dict.get("Author").and_then(|o| o.as_string()) {
2674                metadata.author = Some(author.to_text());
2675            }
2676            if let Some(subject) = info_dict.get("Subject").and_then(|o| o.as_string()) {
2677                metadata.subject = Some(subject.to_text());
2678            }
2679            if let Some(keywords) = info_dict.get("Keywords").and_then(|o| o.as_string()) {
2680                metadata.keywords = Some(keywords.to_text());
2681            }
2682            if let Some(creator) = info_dict.get("Creator").and_then(|o| o.as_string()) {
2683                metadata.creator = Some(creator.to_text());
2684            }
2685            if let Some(producer) = info_dict.get("Producer").and_then(|o| o.as_string()) {
2686                metadata.producer = Some(producer.to_text());
2687            }
2688        }
2689
2690        metadata.version = self.version().to_string();
2691        metadata.page_count = self.page_count().ok();
2692
2693        Ok(metadata)
2694    }
2695
2696    /// Initialize the page tree navigator if not already done
2697    fn ensure_page_tree(&mut self) -> ParseResult<()> {
2698        if self.page_tree.is_none() {
2699            let page_count = self.page_count()?;
2700            self.page_tree = Some(super::page_tree::PageTree::new(page_count));
2701        }
2702        Ok(())
2703    }
2704
2705    /// Get a specific page by index (0-based)
2706    ///
2707    /// Note: This method is currently not implemented due to borrow checker constraints.
2708    /// The page_tree needs mutable access to both itself and the reader, which requires
2709    /// a redesign of the architecture. Use PdfDocument instead for page access.
2710    pub fn get_page(&mut self, _index: u32) -> ParseResult<&super::page_tree::ParsedPage> {
2711        self.ensure_page_tree()?;
2712
2713        // The page_tree needs mutable access to both itself and the reader
2714        // This requires a redesign of the architecture to avoid the borrow checker issue
2715        // For now, users should convert to PdfDocument using into_document() for page access
2716        Err(ParseError::SyntaxError {
2717            position: 0,
2718            message: "get_page not implemented due to borrow checker constraints. Use PdfDocument instead.".to_string(),
2719        })
2720    }
2721
2722    /// Get all pages
2723    pub fn get_all_pages(&mut self) -> ParseResult<Vec<super::page_tree::ParsedPage>> {
2724        let page_count = self.page_count()?;
2725        let mut pages = Vec::with_capacity(page_count as usize);
2726
2727        for i in 0..page_count {
2728            let page = self.get_page(i)?.clone();
2729            pages.push(page);
2730        }
2731
2732        Ok(pages)
2733    }
2734
2735    /// Convert this reader into a PdfDocument for easier page access
2736    pub fn into_document(self) -> super::document::PdfDocument<R> {
2737        super::document::PdfDocument::new(self)
2738    }
2739
2740    /// Clear the parse context (useful to avoid false circular references)
2741    pub fn clear_parse_context(&mut self) {
2742        self.parse_context = StackSafeContext::new();
2743    }
2744
2745    /// Get a mutable reference to the parse context
2746    pub fn parse_context_mut(&mut self) -> &mut StackSafeContext {
2747        &mut self.parse_context
2748    }
2749
2750    /// Find all page objects by scanning the entire PDF in bounded chunks.
2751    ///
2752    /// Issue #339: replaces a whole-file `read_to_end` with a chunked scan that
2753    /// probes each object header with a small bounded window, keeping peak memory
2754    /// O(chunk) regardless of file size. A scan error degrades to an empty list,
2755    /// matching the previous behavior.
2756    fn find_page_objects(&mut self) -> ParseResult<Vec<(u32, u16)>> {
2757        let original_pos = self.reader.stream_position().unwrap_or(0);
2758        let result = scan_page_object_refs(&mut self.reader);
2759        self.reader.seek(SeekFrom::Start(original_pos)).ok();
2760        Ok(result.unwrap_or_default())
2761    }
2762
2763    /// Find catalog object by scanning
2764    fn find_catalog_object(&mut self) -> ParseResult<(u32, u16)> {
2765        // FIX for Issue #83: Scan for actual catalog object, not just assume object 1
2766        // In signed PDFs, object 1 is often /Type/Sig (signature), not the catalog
2767
2768        // Get all object numbers from xref
2769        let obj_numbers: Vec<u32> = self.xref.entries().keys().copied().collect();
2770
2771        // Scan objects looking for /Type/Catalog
2772        for obj_num in obj_numbers {
2773            // Try to get object (generation 0 is most common)
2774            if let Ok(obj) = self.get_object(obj_num, 0) {
2775                if let Some(dict) = obj.as_dict() {
2776                    // Check if it's a catalog
2777                    if let Some(type_obj) = dict.get("Type") {
2778                        if let Some(type_name) = type_obj.as_name() {
2779                            if type_name.0 == "Catalog" {
2780                                return Ok((obj_num, 0));
2781                            }
2782                            // Skip known non-catalog types
2783                            if type_name.0 == "Sig"
2784                                || type_name.0 == "Pages"
2785                                || type_name.0 == "Page"
2786                            {
2787                                continue;
2788                            }
2789                        }
2790                    }
2791                }
2792            }
2793        }
2794
2795        // Fallback: try common object numbers if scan failed
2796        for obj_num in [1, 2, 3, 4, 5] {
2797            if let Ok(obj) = self.get_object(obj_num, 0) {
2798                if let Some(dict) = obj.as_dict() {
2799                    // Check if it has catalog-like properties (Pages key)
2800                    if dict.contains_key("Pages") {
2801                        return Ok((obj_num, 0));
2802                    }
2803                }
2804            }
2805        }
2806
2807        Err(ParseError::MissingKey(
2808            "Could not find Catalog object".to_string(),
2809        ))
2810    }
2811
2812    /// Create a synthetic Pages dictionary when the catalog is missing one
2813    fn create_synthetic_pages_dict(
2814        &mut self,
2815        page_refs: &[(u32, u16)],
2816    ) -> ParseResult<&PdfDictionary> {
2817        use super::objects::{PdfArray, PdfName};
2818
2819        // Validate and repair page objects first
2820        let mut valid_page_refs = Vec::new();
2821        for (obj_num, gen_num) in page_refs {
2822            if let Ok(page_obj) = self.get_object(*obj_num, *gen_num) {
2823                if let Some(page_dict) = page_obj.as_dict() {
2824                    // Ensure this is actually a page object
2825                    if let Some(obj_type) = page_dict.get("Type").and_then(|t| t.as_name()) {
2826                        if obj_type.0 == "Page" {
2827                            valid_page_refs.push((*obj_num, *gen_num));
2828                            continue;
2829                        }
2830                    }
2831
2832                    // If no Type but has page-like properties, treat as page
2833                    if page_dict.contains_key("MediaBox") || page_dict.contains_key("Contents") {
2834                        valid_page_refs.push((*obj_num, *gen_num));
2835                    }
2836                }
2837            }
2838        }
2839
2840        if valid_page_refs.is_empty() {
2841            return Err(ParseError::SyntaxError {
2842                position: 0,
2843                message: "No valid page objects found for synthetic Pages tree".to_string(),
2844            });
2845        }
2846
2847        // Create hierarchical tree for many pages (more than 10)
2848        if valid_page_refs.len() > 10 {
2849            return self.create_hierarchical_pages_tree(&valid_page_refs);
2850        }
2851
2852        // Create simple flat tree for few pages
2853        let mut kids = PdfArray::new();
2854        for (obj_num, gen_num) in &valid_page_refs {
2855            kids.push(PdfObject::Reference(*obj_num, *gen_num));
2856        }
2857
2858        // Create synthetic Pages dictionary
2859        let mut pages_dict = PdfDictionary::new();
2860        pages_dict.insert(
2861            "Type".to_string(),
2862            PdfObject::Name(PdfName("Pages".to_string())),
2863        );
2864        pages_dict.insert("Kids".to_string(), PdfObject::Array(kids));
2865        pages_dict.insert(
2866            "Count".to_string(),
2867            PdfObject::Integer(valid_page_refs.len() as i64),
2868        );
2869
2870        // Find a common MediaBox from the pages
2871        let mut media_box = None;
2872        for (obj_num, gen_num) in valid_page_refs.iter().take(3) {
2873            if let Ok(page_obj) = self.get_object(*obj_num, *gen_num) {
2874                if let Some(page_dict) = page_obj.as_dict() {
2875                    if let Some(mb) = page_dict.get("MediaBox") {
2876                        media_box = Some(mb.clone());
2877                    }
2878                }
2879            }
2880        }
2881
2882        // Use default Letter size if no MediaBox found
2883        if let Some(mb) = media_box {
2884            pages_dict.insert("MediaBox".to_string(), mb);
2885        } else {
2886            let mut mb_array = PdfArray::new();
2887            mb_array.push(PdfObject::Integer(0));
2888            mb_array.push(PdfObject::Integer(0));
2889            mb_array.push(PdfObject::Integer(612));
2890            mb_array.push(PdfObject::Integer(792));
2891            pages_dict.insert("MediaBox".to_string(), PdfObject::Array(mb_array));
2892        }
2893
2894        // Store in cache with a synthetic object number
2895        let synthetic_key = (u32::MAX - 1, 0);
2896        self.object_cache
2897            .insert(synthetic_key, PdfObject::Dictionary(pages_dict));
2898
2899        // Return reference to cached dictionary
2900        if let PdfObject::Dictionary(dict) = &self.object_cache[&synthetic_key] {
2901            Ok(dict)
2902        } else {
2903            unreachable!("Just inserted dictionary")
2904        }
2905    }
2906
2907    /// Create a hierarchical Pages tree for documents with many pages
2908    fn create_hierarchical_pages_tree(
2909        &mut self,
2910        page_refs: &[(u32, u16)],
2911    ) -> ParseResult<&PdfDictionary> {
2912        use super::objects::{PdfArray, PdfName};
2913
2914        const PAGES_PER_NODE: usize = 10; // Max pages per intermediate node
2915
2916        // Split pages into groups
2917        let chunks: Vec<&[(u32, u16)]> = page_refs.chunks(PAGES_PER_NODE).collect();
2918        let mut intermediate_nodes = Vec::new();
2919
2920        // Create intermediate Pages nodes for each chunk
2921        for (chunk_idx, chunk) in chunks.iter().enumerate() {
2922            let mut kids = PdfArray::new();
2923            for (obj_num, gen_num) in chunk.iter() {
2924                kids.push(PdfObject::Reference(*obj_num, *gen_num));
2925            }
2926
2927            let mut intermediate_dict = PdfDictionary::new();
2928            intermediate_dict.insert(
2929                "Type".to_string(),
2930                PdfObject::Name(PdfName("Pages".to_string())),
2931            );
2932            intermediate_dict.insert("Kids".to_string(), PdfObject::Array(kids));
2933            intermediate_dict.insert("Count".to_string(), PdfObject::Integer(chunk.len() as i64));
2934
2935            // Store intermediate node with synthetic object number
2936            let intermediate_key = (u32::MAX - 2 - chunk_idx as u32, 0);
2937            self.object_cache
2938                .insert(intermediate_key, PdfObject::Dictionary(intermediate_dict));
2939
2940            intermediate_nodes.push(intermediate_key);
2941        }
2942
2943        // Create root Pages node that references intermediate nodes
2944        let mut root_kids = PdfArray::new();
2945        for (obj_num, gen_num) in &intermediate_nodes {
2946            root_kids.push(PdfObject::Reference(*obj_num, *gen_num));
2947        }
2948
2949        let mut root_pages_dict = PdfDictionary::new();
2950        root_pages_dict.insert(
2951            "Type".to_string(),
2952            PdfObject::Name(PdfName("Pages".to_string())),
2953        );
2954        root_pages_dict.insert("Kids".to_string(), PdfObject::Array(root_kids));
2955        root_pages_dict.insert(
2956            "Count".to_string(),
2957            PdfObject::Integer(page_refs.len() as i64),
2958        );
2959
2960        // Add MediaBox if available
2961        if let Some((obj_num, gen_num)) = page_refs.first() {
2962            if let Ok(page_obj) = self.get_object(*obj_num, *gen_num) {
2963                if let Some(page_dict) = page_obj.as_dict() {
2964                    if let Some(mb) = page_dict.get("MediaBox") {
2965                        root_pages_dict.insert("MediaBox".to_string(), mb.clone());
2966                    }
2967                }
2968            }
2969        }
2970
2971        // Store root Pages dictionary
2972        let root_key = (u32::MAX - 1, 0);
2973        self.object_cache
2974            .insert(root_key, PdfObject::Dictionary(root_pages_dict));
2975
2976        // Return reference to cached dictionary
2977        if let PdfObject::Dictionary(dict) = &self.object_cache[&root_key] {
2978            Ok(dict)
2979        } else {
2980            unreachable!("Just inserted dictionary")
2981        }
2982    }
2983
2984    // =========================================================================
2985    // Digital Signatures API
2986    // =========================================================================
2987
2988    /// Detect all signature fields in the PDF
2989    ///
2990    /// Returns a list of signature fields found in the document's AcroForm.
2991    /// This method only detects signatures; use `verify_signatures()` for
2992    /// complete validation.
2993    ///
2994    /// # Example
2995    ///
2996    /// ```no_run
2997    /// use oxidize_pdf::parser::PdfReader;
2998    ///
2999    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3000    /// let mut reader = PdfReader::open("signed.pdf")?;
3001    /// let signatures = reader.signatures()?;
3002    ///
3003    /// println!("Found {} signature(s)", signatures.len());
3004    /// for sig in &signatures {
3005    ///     println!("  Filter: {}", sig.filter);
3006    ///     if sig.is_pades() {
3007    ///         println!("  Type: PAdES");
3008    ///     }
3009    /// }
3010    /// # Ok(())
3011    /// # }
3012    /// ```
3013    pub fn signatures(&mut self) -> ParseResult<Vec<crate::signatures::SignatureField>> {
3014        crate::signatures::detect_signature_fields(self).map_err(|e| ParseError::SyntaxError {
3015            position: 0,
3016            message: format!("Failed to detect signatures: {}", e),
3017        })
3018    }
3019
3020    /// Verify all signatures in the PDF using Mozilla's CA bundle
3021    ///
3022    /// This is a convenience method that uses the default trust store
3023    /// (Mozilla CA bundle). For custom trust stores, use
3024    /// `verify_signatures_with_trust_store()`.
3025    ///
3026    /// # Returns
3027    ///
3028    /// A vector of `FullSignatureValidationResult` for each signature found.
3029    /// Each result includes:
3030    /// - Hash verification status
3031    /// - Cryptographic signature verification status
3032    /// - Certificate validation status
3033    /// - Detection of modifications after signing
3034    ///
3035    /// # Example
3036    ///
3037    /// ```no_run
3038    /// use oxidize_pdf::parser::PdfReader;
3039    ///
3040    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3041    /// let mut reader = PdfReader::open("signed.pdf")?;
3042    /// let results = reader.verify_signatures()?;
3043    ///
3044    /// for result in &results {
3045    ///     if result.is_valid() {
3046    ///         println!("Valid signature from: {}", result.signer_name());
3047    ///     } else {
3048    ///         println!("Invalid: {:?}", result.validation_errors());
3049    ///     }
3050    /// }
3051    /// # Ok(())
3052    /// # }
3053    /// ```
3054    pub fn verify_signatures(
3055        &mut self,
3056    ) -> ParseResult<Vec<crate::signatures::FullSignatureValidationResult>> {
3057        self.verify_signatures_with_trust_store(crate::signatures::TrustStore::default())
3058    }
3059
3060    /// Verify all signatures in the PDF with a custom trust store
3061    ///
3062    /// Use this method when you need to validate certificates against a
3063    /// custom CA bundle instead of the Mozilla CA bundle.
3064    ///
3065    /// # Arguments
3066    ///
3067    /// * `trust_store` - The trust store containing root certificates
3068    ///
3069    /// # Example
3070    ///
3071    /// ```no_run
3072    /// use oxidize_pdf::parser::PdfReader;
3073    /// use oxidize_pdf::signatures::TrustStore;
3074    ///
3075    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
3076    /// let mut reader = PdfReader::open("signed.pdf")?;
3077    ///
3078    /// // Use empty trust store (no trusted CAs)
3079    /// let trust_store = TrustStore::empty();
3080    /// let results = reader.verify_signatures_with_trust_store(trust_store)?;
3081    ///
3082    /// for result in &results {
3083    ///     if !result.is_valid() {
3084    ///         // Expected: certificates won't be trusted
3085    ///         println!("Not trusted: {}", result.signer_name());
3086    ///     }
3087    /// }
3088    /// # Ok(())
3089    /// # }
3090    /// ```
3091    pub fn verify_signatures_with_trust_store(
3092        &mut self,
3093        trust_store: crate::signatures::TrustStore,
3094    ) -> ParseResult<Vec<crate::signatures::FullSignatureValidationResult>> {
3095        use crate::signatures::{
3096            has_incremental_update, parse_pkcs7_signature_detailed, verify_signature_detailed,
3097            FullSignatureValidationResult,
3098        };
3099
3100        // First, read the entire PDF bytes (needed for hash computation)
3101        let original_pos = self.reader.stream_position().unwrap_or(0);
3102        self.reader.seek(SeekFrom::Start(0))?;
3103
3104        let mut pdf_bytes = Vec::new();
3105        self.reader.read_to_end(&mut pdf_bytes)?;
3106
3107        // Restore original position
3108        self.reader.seek(SeekFrom::Start(original_pos)).ok();
3109
3110        // Detect all signature fields
3111        let signature_fields = self.signatures()?;
3112
3113        let mut results = Vec::new();
3114
3115        for field in signature_fields {
3116            let mut result = FullSignatureValidationResult {
3117                field: field.clone(),
3118                signer_name: None,
3119                signing_time: None,
3120                hash_valid: false,
3121                signature_valid: false,
3122                certificate_result: None,
3123                has_modifications_after_signing: false,
3124                errors: Vec::new(),
3125                warnings: Vec::new(),
3126            };
3127
3128            // Check for incremental updates
3129            result.has_modifications_after_signing =
3130                has_incremental_update(&pdf_bytes, &field.byte_range);
3131
3132            // Parse the PKCS#7/CMS signature
3133            let parsed_sig = match parse_pkcs7_signature_detailed(&field.contents) {
3134                Ok(sig) => sig,
3135                Err(e) => {
3136                    result
3137                        .errors
3138                        .push(format!("Failed to parse signature: {}", e));
3139                    results.push(result);
3140                    continue;
3141                }
3142            };
3143
3144            // Extract signer name and signing time
3145            result.signing_time = parsed_sig.signing_time.clone();
3146            result.signer_name = parsed_sig.signer_common_name().ok();
3147
3148            // Verify the cryptographic signature
3149            match verify_signature_detailed(&pdf_bytes, &parsed_sig, &field.byte_range) {
3150                Ok(verification) => {
3151                    result.hash_valid = verification.hash_valid;
3152                    result.signature_valid = verification.signature_valid;
3153                    if let Some(details) = verification.details {
3154                        result.warnings.push(details);
3155                    }
3156                }
3157                Err(e) => {
3158                    result
3159                        .errors
3160                        .push(format!("Signature verification failed: {}", e));
3161                }
3162            }
3163
3164            // Validate the certificate
3165            match crate::signatures::validate_certificate_chain(
3166                &parsed_sig.signer_certificate_der,
3167                &parsed_sig.certificates_der,
3168                &trust_store,
3169                None,
3170            ) {
3171                Ok(cert_result) => {
3172                    result.certificate_result =
3173                        Some(cert_result.into_certificate_result_fail_closed());
3174                }
3175                Err(e) => {
3176                    result
3177                        .errors
3178                        .push(format!("Certificate validation failed: {}", e));
3179                }
3180            }
3181
3182            results.push(result);
3183        }
3184
3185        Ok(results)
3186    }
3187}
3188
3189/// Document metadata
3190#[derive(Debug, Default, Clone)]
3191pub struct DocumentMetadata {
3192    pub title: Option<String>,
3193    pub author: Option<String>,
3194    pub subject: Option<String>,
3195    pub keywords: Option<String>,
3196    pub creator: Option<String>,
3197    pub producer: Option<String>,
3198    pub creation_date: Option<String>,
3199    pub modification_date: Option<String>,
3200    pub version: String,
3201    pub page_count: Option<u32>,
3202}
3203
3204pub struct EOLIter<'s> {
3205    remainder: &'s str,
3206}
3207impl<'s> Iterator for EOLIter<'s> {
3208    type Item = &'s str;
3209
3210    fn next(&mut self) -> Option<Self::Item> {
3211        if self.remainder.is_empty() {
3212            return None;
3213        }
3214
3215        if let Some((i, sep)) = ["\r\n", "\n", "\r"]
3216            .iter()
3217            .filter_map(|&sep| self.remainder.find(sep).map(|i| (i, sep)))
3218            .min_by_key(|(i, _)| *i)
3219        {
3220            let (line, rest) = self.remainder.split_at(i);
3221            self.remainder = &rest[sep.len()..];
3222            Some(line)
3223        } else {
3224            let line = self.remainder;
3225            self.remainder = "";
3226            Some(line)
3227        }
3228    }
3229}
3230pub trait PDFLines: AsRef<str> {
3231    fn pdf_lines(&self) -> EOLIter<'_> {
3232        EOLIter {
3233            remainder: self.as_ref(),
3234        }
3235    }
3236}
3237impl PDFLines for &str {}
3238impl<'a> PDFLines for std::borrow::Cow<'a, str> {}
3239impl PDFLines for String {}
3240
3241#[cfg(test)]
3242mod tests {
3243
3244    use super::*;
3245    use crate::parser::objects::{PdfName, PdfString};
3246    use crate::parser::test_helpers::*;
3247    use crate::parser::ParseOptions;
3248    use std::io::Cursor;
3249
3250    #[test]
3251    fn test_reader_construction() {
3252        let pdf_data = create_minimal_pdf();
3253        let cursor = Cursor::new(pdf_data);
3254        let result = PdfReader::new(cursor);
3255        assert!(result.is_ok());
3256    }
3257
3258    // --- Total string-slicing helpers used by the manual recovery path ---
3259    //
3260    // The recovery path builds these from `String::from_utf8_lossy` over
3261    // arbitrary bytes, so every input below is reachable from a malformed file.
3262    // The contract under test is totality: a wrong answer is acceptable on
3263    // garbage, a panic is not.
3264
3265    #[test]
3266    fn slice_between_extracts_delimited_content() {
3267        assert_eq!(
3268            slice_between("/MediaBox [0 0 612 792] /X", b'[', b']'),
3269            Some("0 0 612 792")
3270        );
3271        assert_eq!(slice_between("[]", b'[', b']'), Some(""));
3272    }
3273
3274    #[test]
3275    fn slice_between_searches_the_closer_after_the_opener() {
3276        // A closer that only appears BEFORE the opener must not produce an
3277        // inverted range. Mutated `/MediaBox ][` reached `&area[start+1..end]`
3278        // with start > end and panicked ("byte range starts at 26 but ends at
3279        // 22"); the closer is now searched in the remainder after the opener.
3280        assert_eq!(slice_between("/MediaBox ][", b'[', b']'), None);
3281        assert_eq!(slice_between("] a [ b ] c", b'[', b']'), Some(" b "));
3282    }
3283
3284    #[test]
3285    fn slice_between_returns_none_when_a_delimiter_is_missing() {
3286        assert_eq!(slice_between("/MediaBox 0 0 612 792", b'[', b']'), None);
3287        assert_eq!(slice_between("/MediaBox [0 0 612 792", b'[', b']'), None);
3288        assert_eq!(slice_between("", b'[', b']'), None);
3289    }
3290
3291    #[test]
3292    fn slice_between_never_splits_a_multibyte_char() {
3293        // U+FFFD is what from_utf8_lossy leaves behind for every invalid byte,
3294        // so multi-byte content is the norm here, not an edge case.
3295        assert_eq!(
3296            slice_between("[\u{FFFD}\u{20AC}]", b'[', b']'),
3297            Some("\u{FFFD}\u{20AC}")
3298        );
3299        // Delimiters are ASCII: they can never match a continuation byte of a
3300        // multi-byte sequence, so the bounds always land on char boundaries.
3301        assert_eq!(slice_between("\u{20AC}[x]\u{20AC}", b'[', b']'), Some("x"));
3302    }
3303
3304    #[test]
3305    fn find_dict_end_locates_the_closing_marker() {
3306        // Offset is relative to the text after the opening `<<`, and points at
3307        // the `>` of the matching `>>`.
3308        assert_eq!(find_dict_end(" /A 1 >>"), Some(6));
3309        assert_eq!(find_dict_end(">>"), Some(0));
3310    }
3311
3312    #[test]
3313    fn find_dict_end_matches_nesting() {
3314        assert_eq!(find_dict_end(" /A << /B 1 >> >> tail"), Some(15));
3315    }
3316
3317    #[test]
3318    fn find_dict_end_returns_none_when_unbalanced() {
3319        assert_eq!(find_dict_end(" /A 1"), None);
3320        assert_eq!(find_dict_end(" /A << /B 1 >>"), None);
3321        assert_eq!(find_dict_end(""), None);
3322        assert_eq!(find_dict_end(">"), None);
3323    }
3324
3325    #[test]
3326    fn find_dict_end_offset_is_a_char_boundary_with_multibyte_content() {
3327        // The scan is byte-level; `<`/`>` are ASCII and cannot occur inside a
3328        // multi-byte sequence, so the returned offset is always sliceable.
3329        let after = " /Font \u{FFFD} >> tail";
3330        let end = find_dict_end(after).expect("closes");
3331        assert_eq!(&after[..end], " /Font \u{FFFD} ");
3332    }
3333
3334    #[test]
3335    fn truncate_on_char_boundary_cuts_without_splitting() {
3336        assert_eq!(truncate_on_char_boundary("abc", 10), "abc");
3337        assert_eq!(truncate_on_char_boundary("abcdef", 3), "abc");
3338        // Cutting at 1 would land inside the 3-byte U+FFFD: back off to 0.
3339        assert_eq!(truncate_on_char_boundary("\u{FFFD}x", 1), "");
3340        assert_eq!(truncate_on_char_boundary("\u{FFFD}x", 3), "\u{FFFD}");
3341        assert_eq!(truncate_on_char_boundary("", 5), "");
3342    }
3343
3344    /// Reader instrumented with two shared counters so a test can reset them after
3345    /// `PdfReader::new` and observe only the reads done by the method under test:
3346    /// - `total_read`: total bytes consumed — distinguishes a bounded locate that
3347    ///   stops at a front object from an unbounded `read_to_end` of the whole file.
3348    /// - `max_read`: largest single read request — distinguishes a scan that only
3349    ///   ever asks for one chunk from a `read_to_end` that grows one huge buffer.
3350    struct CountingReader<R> {
3351        inner: R,
3352        total_read: std::sync::Arc<std::sync::atomic::AtomicUsize>,
3353        max_read: std::sync::Arc<std::sync::atomic::AtomicUsize>,
3354    }
3355    impl<R: Read> Read for CountingReader<R> {
3356        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
3357            self.max_read
3358                .fetch_max(buf.len(), std::sync::atomic::Ordering::SeqCst);
3359            let n = self.inner.read(buf)?;
3360            self.total_read
3361                .fetch_add(n, std::sync::atomic::Ordering::SeqCst);
3362            Ok(n)
3363        }
3364    }
3365    impl<R: Seek> Seek for CountingReader<R> {
3366        fn seek(&mut self, p: SeekFrom) -> std::io::Result<u64> {
3367            self.inner.seek(p)
3368        }
3369    }
3370
3371    #[test]
3372    fn test_extract_stream_manually_bounded_honors_length() {
3373        use crate::parser::objects::PdfObject;
3374        use std::sync::atomic::{AtomicUsize, Ordering};
3375        use std::sync::Arc;
3376
3377        // Issue #339: extract_object_or_stream_manually must locate the object
3378        // and read its stream body bounded by /Length, never buffering the whole
3379        // file. A large XMP-sized stream (> the 256 KiB dict window) followed by a
3380        // big unrelated filler object makes the bound observable and proves the
3381        // body is not truncated at any fixed window.
3382        const L: usize = 300 * 1024; // 307200, exceeds MANUAL_DICT_WINDOW
3383        const FILLER: usize = 4 * 1024 * 1024; // dwarfs the target object
3384
3385        // Deterministic +1 byte ramp: never produces the "endstream" byte run
3386        // (those bytes are not an arithmetic +1 sequence), so the marker search
3387        // is unambiguous and full-content equality is meaningful.
3388        let payload: Vec<u8> = (0..L).map(|i| (i % 251) as u8).collect();
3389        let filler: Vec<u8> = (0..FILLER).map(|i| ((i + 7) % 251) as u8).collect();
3390
3391        let header = b"%PDF-1.4\n";
3392        let obj1 = b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
3393        let obj2 = b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n";
3394
3395        let mut data = Vec::new();
3396        data.extend_from_slice(header);
3397        let obj1_start = data.len();
3398        data.extend_from_slice(obj1);
3399        let obj2_start = data.len();
3400        data.extend_from_slice(obj2);
3401
3402        // Object 4: the target stream with a DIRECT /Length. Not listed in xref,
3403        // so it is only reachable via the manual scan fallback.
3404        data.extend_from_slice(
3405            b"4 0 obj\n<< /Type /Metadata /Subtype /XML /Length 307200 >>\nstream\n",
3406        );
3407        data.extend_from_slice(&payload);
3408        data.extend_from_slice(b"\nendstream\nendobj\n");
3409
3410        // Object 5: large unrelated filler so file size >> target object size.
3411        data.extend_from_slice(b"5 0 obj\n<< /Length 4194304 >>\nstream\n");
3412        data.extend_from_slice(&filler);
3413        data.extend_from_slice(b"\nendstream\nendobj\n");
3414
3415        // xref lists only 0/1/2; startxref sits at the very end so new() parses it.
3416        let xref_start = data.len();
3417        let xref = format!(
3418            "xref\n0 3\n0000000000 65535 f \n{obj1_start:010} 00000 n \n{obj2_start:010} 00000 n \ntrailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{xref_start}\n%%EOF"
3419        );
3420        data.extend_from_slice(xref.as_bytes());
3421        let file_len = data.len();
3422
3423        let counter = Arc::new(AtomicUsize::new(0));
3424        let reader = CountingReader {
3425            inner: Cursor::new(data),
3426            total_read: counter.clone(),
3427            max_read: Arc::new(AtomicUsize::new(0)),
3428        };
3429        let mut pdf = PdfReader::new_with_options(reader, ParseOptions::tolerant())
3430            .expect("minimal PDF must parse");
3431
3432        // Observe only the reads performed by the method under test.
3433        counter.store(0, Ordering::SeqCst);
3434        let obj = pdf
3435            .extract_object_or_stream_manually(4)
3436            .expect("stream object 4 must be extracted");
3437
3438        // Content intact: full stream body, not truncated at any fixed window.
3439        let stream = match &obj {
3440            PdfObject::Stream(s) => s,
3441            other => panic!("expected a stream, got {other:?}"),
3442        };
3443        assert_eq!(
3444            stream.data.len(),
3445            L,
3446            "stream body must equal /Length (no truncation)"
3447        );
3448        assert_eq!(stream.data, payload, "stream body content must be intact");
3449
3450        // Bounded: the target object sits near the front, so a bounded locate that
3451        // stops at object 4 reads far less than the whole file (which holds the
3452        // 4 MiB filler). An unbounded read_to_end would consume the entire file.
3453        let total_read = counter.load(Ordering::SeqCst);
3454        assert!(
3455            total_read <= L + 2 * MANUAL_DICT_WINDOW,
3456            "manual stream extraction read {total_read} bytes total (file={file_len}); not bounded by object size"
3457        );
3458    }
3459
3460    /// Build a minimal PDF whose object 4 is a stream reachable only via the manual
3461    /// scan fallback (absent from xref), with the given dictionary body and payload.
3462    /// Returns the full file bytes. Used by the #351 reconstruction-dict tests.
3463    #[cfg(test)]
3464    fn build_manual_stream_pdf(obj4_dict_body: &str, payload: &[u8]) -> Vec<u8> {
3465        let header = b"%PDF-1.4\n";
3466        let obj1 = b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
3467        let obj2 = b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n";
3468
3469        let mut data = Vec::new();
3470        data.extend_from_slice(header);
3471        let obj1_start = data.len();
3472        data.extend_from_slice(obj1);
3473        let obj2_start = data.len();
3474        data.extend_from_slice(obj2);
3475
3476        // Object 4: stream reachable only via the manual scan (not listed in xref).
3477        data.extend_from_slice(format!("4 0 obj\n<<{obj4_dict_body}>>\nstream\n").as_bytes());
3478        data.extend_from_slice(payload);
3479        data.extend_from_slice(b"\nendstream\nendobj\n");
3480
3481        // xref lists only 0/1/2; startxref at the end so new() parses the file.
3482        let xref_start = data.len();
3483        let xref = format!(
3484            "xref\n0 3\n0000000000 65535 f \n{obj1_start:010} 00000 n \n{obj2_start:010} 00000 n \ntrailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{xref_start}\n%%EOF"
3485        );
3486        data.extend_from_slice(xref.as_bytes());
3487        data
3488    }
3489
3490    #[test]
3491    fn test_reconstruct_preserves_non_flate_filter() {
3492        use crate::parser::objects::PdfObject;
3493
3494        // Issue #351: the manual stream reconstruction fallback must preserve the
3495        // stream's /Filter generically, not only /FlateDecode. A DCTDecode stream
3496        // reached via the manual scan previously lost its /Filter (and /Subtype),
3497        // causing downstream to treat compressed bytes as raw — a silent wrong
3498        // result. Body content is irrelevant here; the dictionary is the contract.
3499        let payload = b"\xff\xd8\xff\xe0not-a-real-jpeg\xff\xd9";
3500        let len = payload.len();
3501        let dict_body =
3502            format!(" /Type /XObject /Subtype /Image /Filter /DCTDecode /Length {len} ");
3503        let data = build_manual_stream_pdf(&dict_body, payload);
3504
3505        let mut pdf = PdfReader::new_with_options(Cursor::new(data), ParseOptions::tolerant())
3506            .expect("minimal PDF must parse");
3507        let obj = pdf
3508            .extract_object_or_stream_manually(4)
3509            .expect("stream object 4 must be extracted");
3510        let stream = match &obj {
3511            PdfObject::Stream(s) => s,
3512            other => panic!("expected a stream, got {other:?}"),
3513        };
3514
3515        assert_eq!(
3516            stream
3517                .dict
3518                .get("Filter")
3519                .and_then(|o| o.as_name())
3520                .map(|n| n.0.as_str()),
3521            Some("DCTDecode"),
3522            "non-Flate /Filter must be preserved in reconstructed stream dict"
3523        );
3524        assert_eq!(
3525            stream
3526                .dict
3527                .get("Subtype")
3528                .and_then(|o| o.as_name())
3529                .map(|n| n.0.as_str()),
3530            Some("Image"),
3531            "/Subtype must be preserved, not dropped"
3532        );
3533        assert_eq!(stream.data, payload, "stream body must be intact");
3534    }
3535
3536    #[test]
3537    fn test_reconstruct_preserves_filter_array_and_decodeparms() {
3538        use crate::parser::objects::PdfObject;
3539
3540        // Issue #351: filter arrays and /DecodeParms must survive the manual
3541        // reconstruction, and the parser must tolerate the no-space `/Filter[`
3542        // spacing variant. A LZWDecode→FlateDecode chain with predictor parms is
3543        // a realistic case the old hardcoded `/Filter /FlateDecode` match dropped.
3544        let payload = b"compressed-bytes-placeholder";
3545        let len = payload.len();
3546        let dict_body = format!(
3547            " /Filter[/LZWDecode /FlateDecode] /DecodeParms[null<< /Predictor 12 /Columns 4 >>] /Length {len} "
3548        );
3549        let data = build_manual_stream_pdf(&dict_body, payload);
3550
3551        let mut pdf = PdfReader::new_with_options(Cursor::new(data), ParseOptions::tolerant())
3552            .expect("minimal PDF must parse");
3553        let obj = pdf
3554            .extract_object_or_stream_manually(4)
3555            .expect("stream object 4 must be extracted");
3556        let stream = match &obj {
3557            PdfObject::Stream(s) => s,
3558            other => panic!("expected a stream, got {other:?}"),
3559        };
3560
3561        let filter = stream
3562            .dict
3563            .get("Filter")
3564            .and_then(|o| o.as_array())
3565            .expect("/Filter array must be preserved");
3566        let names: Vec<&str> = filter
3567            .0
3568            .iter()
3569            .filter_map(|o| o.as_name())
3570            .map(|n| n.0.as_str())
3571            .collect();
3572        assert_eq!(
3573            names,
3574            vec!["LZWDecode", "FlateDecode"],
3575            "filter array entries must be preserved in order"
3576        );
3577        assert!(
3578            stream.dict.get("DecodeParms").is_some(),
3579            "/DecodeParms must be preserved alongside the filter chain"
3580        );
3581        assert_eq!(stream.data, payload, "stream body must be intact");
3582    }
3583
3584    #[test]
3585    fn test_reconstruct_malformed_dict_falls_back_without_panic() {
3586        use crate::parser::objects::PdfObject;
3587
3588        // Issue #351: when the generic dict parse fails (a dictionary the bracket
3589        // counter accepted as balanced `<<...>>` but the lexer rejects — here a
3590        // dangling key with no value), the reconstruction must not panic and must
3591        // still recover the stream via the legacy minimal path. /Length is honored
3592        // for the bounded body read; the body must come back intact.
3593        let payload = b"0123456789";
3594        let len = payload.len();
3595        // `/BadKey` has no value before `>>` → parse_with_options errors → fallback.
3596        let dict_body = format!(" /Length {len} /BadKey ");
3597        let data = build_manual_stream_pdf(&dict_body, payload);
3598
3599        let mut pdf = PdfReader::new_with_options(Cursor::new(data), ParseOptions::tolerant())
3600            .expect("minimal PDF must parse");
3601        let obj = pdf
3602            .extract_object_or_stream_manually(4)
3603            .expect("malformed-dict stream must still be reconstructed via fallback");
3604        let stream = match &obj {
3605            PdfObject::Stream(s) => s,
3606            other => panic!("expected a stream, got {other:?}"),
3607        };
3608
3609        assert_eq!(
3610            stream.data, payload,
3611            "fallback path must still read the bounded body intact"
3612        );
3613        // Legacy fallback dict carries no /Filter (none was the Flate literal form).
3614        assert!(
3615            stream.dict.get("Filter").is_none(),
3616            "fallback dict must not invent a /Filter"
3617        );
3618    }
3619
3620    #[test]
3621    fn test_find_page_objects_bounded() {
3622        use std::sync::atomic::{AtomicUsize, Ordering};
3623        use std::sync::Arc;
3624
3625        // Issue #339: find_page_objects scans the whole file to enumerate every
3626        // page object, so it must do so in bounded chunks — never holding the
3627        // entire file in memory. A multi-MiB filler makes an unbounded read_to_end
3628        // (one growing buffer) observable: the largest single read approaches the
3629        // file size, whereas the chunked scan never asks for more than one chunk.
3630        const FILLER: usize = 2 * 1024 * 1024;
3631        let filler: Vec<u8> = (0..FILLER).map(|i| (i % 251) as u8).collect();
3632
3633        let header = b"%PDF-1.4\n";
3634        let obj1 = b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n";
3635        let obj2 = b"2 0 obj\n<< /Type /Pages /Kids [4 0 R 5 0 R 6 0 R] /Count 3 >>\nendobj\n";
3636
3637        let mut data = Vec::new();
3638        data.extend_from_slice(header);
3639        let obj1_start = data.len();
3640        data.extend_from_slice(obj1);
3641        let obj2_start = data.len();
3642        data.extend_from_slice(obj2);
3643
3644        // Three page objects, not listed in xref, so only the scan finds them.
3645        for n in [4u32, 5, 6] {
3646            data.extend_from_slice(
3647                format!(
3648                    "{n} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"
3649                )
3650                .as_bytes(),
3651            );
3652        }
3653
3654        // Large unrelated filler stream object so the file dwarfs the chunk size.
3655        data.extend_from_slice(b"7 0 obj\n<< /Length 2097152 >>\nstream\n");
3656        data.extend_from_slice(&filler);
3657        data.extend_from_slice(b"\nendstream\nendobj\n");
3658
3659        let xref_start = data.len();
3660        let xref = format!(
3661            "xref\n0 3\n0000000000 65535 f \n{obj1_start:010} 00000 n \n{obj2_start:010} 00000 n \ntrailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{xref_start}\n%%EOF"
3662        );
3663        data.extend_from_slice(xref.as_bytes());
3664        let file_len = data.len();
3665
3666        let max_read = Arc::new(AtomicUsize::new(0));
3667        let reader = CountingReader {
3668            inner: Cursor::new(data),
3669            total_read: Arc::new(AtomicUsize::new(0)),
3670            max_read: max_read.clone(),
3671        };
3672        let mut pdf = PdfReader::new_with_options(reader, ParseOptions::tolerant())
3673            .expect("minimal PDF must parse");
3674
3675        // Observe only the reads performed by the method under test.
3676        max_read.store(0, Ordering::SeqCst);
3677        let pages = pdf.find_page_objects().expect("page scan must succeed");
3678
3679        // All three page objects discovered (and the /Pages node excluded).
3680        assert_eq!(
3681            pages,
3682            vec![(4, 0), (5, 0), (6, 0)],
3683            "must find exactly the three /Type /Page objects"
3684        );
3685
3686        // Bounded: the scan never requested more than one chunk in a single read.
3687        let peak = max_read.load(Ordering::SeqCst);
3688        assert!(
3689            peak <= 128 * 1024,
3690            "page scan requested {peak} bytes in one read (file={file_len}); not bounded"
3691        );
3692    }
3693
3694    #[test]
3695    fn test_signature_verification_reads_full_file_intentionally() {
3696        use std::sync::atomic::{AtomicUsize, Ordering};
3697        use std::sync::Arc;
3698
3699        // Issue #339 boundary: unlike the manual-extraction fallbacks, signature
3700        // verification MUST read the entire file — the signature digest is computed
3701        // over the full /ByteRange (ISO 32000-1 §12.8.3.3). This pins that intent so
3702        // the deliberate read_to_end is not mistakenly "optimized" into a bounded read.
3703        let data = create_minimal_pdf();
3704        let file_len = data.len();
3705
3706        let total = Arc::new(AtomicUsize::new(0));
3707        let reader = CountingReader {
3708            inner: Cursor::new(data),
3709            total_read: total.clone(),
3710            max_read: Arc::new(AtomicUsize::new(0)),
3711        };
3712        let mut pdf = PdfReader::new_with_options(reader, ParseOptions::tolerant())
3713            .expect("minimal PDF must parse");
3714
3715        total.store(0, Ordering::SeqCst);
3716        // No signatures present: the function still reads the whole file up front.
3717        let _ = pdf.verify_signatures();
3718        let read = total.load(Ordering::SeqCst);
3719        assert!(
3720            read >= file_len,
3721            "signature verification must read the whole file (read {read}, file {file_len})"
3722        );
3723    }
3724
3725    #[test]
3726    fn test_reader_version() {
3727        let pdf_data = create_minimal_pdf();
3728        let cursor = Cursor::new(pdf_data);
3729        let reader = PdfReader::new(cursor).unwrap();
3730        assert_eq!(reader.version().major, 1);
3731        assert_eq!(reader.version().minor, 4);
3732    }
3733
3734    #[test]
3735    fn test_reader_different_versions() {
3736        let versions = vec![
3737            "1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "2.0",
3738        ];
3739
3740        for version in versions {
3741            let pdf_data = create_pdf_with_version(version);
3742            let cursor = Cursor::new(pdf_data);
3743            let reader = PdfReader::new(cursor).unwrap();
3744
3745            let parts: Vec<&str> = version.split('.').collect();
3746            assert_eq!(reader.version().major, parts[0].parse::<u8>().unwrap());
3747            assert_eq!(reader.version().minor, parts[1].parse::<u8>().unwrap());
3748        }
3749    }
3750
3751    #[test]
3752    fn test_reader_catalog() {
3753        let pdf_data = create_minimal_pdf();
3754        let cursor = Cursor::new(pdf_data);
3755        let mut reader = PdfReader::new(cursor).unwrap();
3756
3757        let catalog = reader.catalog();
3758        assert!(catalog.is_ok());
3759
3760        let catalog_dict = catalog.unwrap();
3761        assert_eq!(
3762            catalog_dict.get("Type"),
3763            Some(&PdfObject::Name(PdfName("Catalog".to_string())))
3764        );
3765    }
3766
3767    #[test]
3768    fn test_reader_info_none() {
3769        let pdf_data = create_minimal_pdf();
3770        let cursor = Cursor::new(pdf_data);
3771        let mut reader = PdfReader::new(cursor).unwrap();
3772
3773        let info = reader.info().unwrap();
3774        assert!(info.is_none());
3775    }
3776
3777    #[test]
3778    fn test_reader_info_present() {
3779        let pdf_data = create_pdf_with_info();
3780        let cursor = Cursor::new(pdf_data);
3781        let mut reader = PdfReader::new(cursor).unwrap();
3782
3783        let info = reader.info().unwrap();
3784        assert!(info.is_some());
3785
3786        let info_dict = info.unwrap();
3787        assert_eq!(
3788            info_dict.get("Title"),
3789            Some(&PdfObject::String(PdfString(
3790                "Test PDF".to_string().into_bytes()
3791            )))
3792        );
3793        assert_eq!(
3794            info_dict.get("Author"),
3795            Some(&PdfObject::String(PdfString(
3796                "Test Author".to_string().into_bytes()
3797            )))
3798        );
3799    }
3800
3801    #[test]
3802    fn test_reader_get_object() {
3803        let pdf_data = create_minimal_pdf();
3804        let cursor = Cursor::new(pdf_data);
3805        let mut reader = PdfReader::new(cursor).unwrap();
3806
3807        // Get catalog object (1 0 obj)
3808        let obj = reader.get_object(1, 0);
3809        assert!(obj.is_ok());
3810
3811        let catalog = obj.unwrap();
3812        assert!(catalog.as_dict().is_some());
3813    }
3814
3815    #[test]
3816    fn test_reader_get_invalid_object() {
3817        let pdf_data = create_minimal_pdf();
3818        let cursor = Cursor::new(pdf_data);
3819        let mut reader = PdfReader::new(cursor).unwrap();
3820
3821        // Try to get non-existent object
3822        let obj = reader.get_object(999, 0);
3823        assert!(obj.is_err());
3824    }
3825
3826    #[test]
3827    fn test_reader_get_free_object() {
3828        let pdf_data = create_minimal_pdf();
3829        let cursor = Cursor::new(pdf_data);
3830        let mut reader = PdfReader::new(cursor).unwrap();
3831
3832        // Object 0 is always free (f flag in xref)
3833        let obj = reader.get_object(0, 65535);
3834        assert!(obj.is_ok());
3835        assert_eq!(obj.unwrap(), &PdfObject::Null);
3836    }
3837
3838    #[test]
3839    fn test_reader_resolve_reference() {
3840        let pdf_data = create_minimal_pdf();
3841        let cursor = Cursor::new(pdf_data);
3842        let mut reader = PdfReader::new(cursor).unwrap();
3843
3844        // Create a reference to catalog
3845        let ref_obj = PdfObject::Reference(1, 0);
3846        let resolved = reader.resolve(&ref_obj);
3847
3848        assert!(resolved.is_ok());
3849        assert!(resolved.unwrap().as_dict().is_some());
3850    }
3851
3852    #[test]
3853    fn test_reader_resolve_non_reference() {
3854        let pdf_data = create_minimal_pdf();
3855        let cursor = Cursor::new(pdf_data);
3856        let mut reader = PdfReader::new(cursor).unwrap();
3857
3858        // Resolve a non-reference object
3859        let int_obj = PdfObject::Integer(42);
3860        let resolved = reader.resolve(&int_obj).unwrap();
3861
3862        assert_eq!(resolved, &PdfObject::Integer(42));
3863    }
3864
3865    #[test]
3866    fn test_reader_cache_behavior() {
3867        let pdf_data = create_minimal_pdf();
3868        let cursor = Cursor::new(pdf_data);
3869        let mut reader = PdfReader::new(cursor).unwrap();
3870
3871        // Get object first time
3872        let obj1 = reader.get_object(1, 0).unwrap();
3873        assert!(obj1.as_dict().is_some());
3874
3875        // Get same object again - should use cache
3876        let obj2 = reader.get_object(1, 0).unwrap();
3877        assert!(obj2.as_dict().is_some());
3878    }
3879
3880    #[test]
3881    fn test_reader_wrong_generation() {
3882        let pdf_data = create_minimal_pdf();
3883        let cursor = Cursor::new(pdf_data);
3884        let mut reader = PdfReader::new(cursor).unwrap();
3885
3886        // Try to get object with wrong generation number
3887        let obj = reader.get_object(1, 99);
3888        assert!(obj.is_err());
3889    }
3890
3891    #[test]
3892    fn test_reader_invalid_pdf() {
3893        let invalid_data = b"This is not a PDF file";
3894        let cursor = Cursor::new(invalid_data.to_vec());
3895        let result = PdfReader::new(cursor);
3896
3897        assert!(result.is_err());
3898    }
3899
3900    #[test]
3901    fn test_reader_corrupt_xref() {
3902        let corrupt_pdf = b"%PDF-1.4
39031 0 obj
3904<< /Type /Catalog >>
3905endobj
3906xref
3907corrupted xref table
3908trailer
3909<< /Size 2 /Root 1 0 R >>
3910startxref
391124
3912%%EOF"
3913            .to_vec();
3914
3915        // Issue #374: a corrupt xref table is reconstructed by scanning object
3916        // headers (default options allow recovery). The scan finds object 1 and
3917        // resolves the catalog.
3918        let cursor = Cursor::new(corrupt_pdf.clone());
3919        let mut reader = PdfReader::new(cursor).expect("corrupt xref is reconstructed by scan");
3920        let catalog = reader
3921            .catalog()
3922            .expect("catalog resolves after reconstruction");
3923        assert_eq!(
3924            catalog.get("Type"),
3925            Some(&PdfObject::Name(PdfName("Catalog".to_string()))),
3926        );
3927
3928        // strict() disables recovery: the corrupt xref must still fail loudly.
3929        let cursor = Cursor::new(corrupt_pdf);
3930        assert!(PdfReader::new_with_options(cursor, ParseOptions::strict()).is_err());
3931    }
3932
3933    #[test]
3934    fn test_reader_missing_trailer() {
3935        let pdf_no_trailer = b"%PDF-1.4
39361 0 obj
3937<< /Type /Catalog >>
3938endobj
3939xref
39400 2
39410000000000 65535 f 
39420000000009 00000 n 
3943startxref
394424
3945%%EOF"
3946            .to_vec();
3947
3948        // Issue #374: without a trailer, recovery locates the catalog by
3949        // scanning objects for /Type /Catalog and synthesizes the trailer's
3950        // /Root, so default parsing succeeds.
3951        let cursor = Cursor::new(pdf_no_trailer.clone());
3952        let mut reader = PdfReader::new(cursor).expect("missing trailer recovered by object scan");
3953        let catalog = reader
3954            .catalog()
3955            .expect("catalog resolved from scanned /Root");
3956        assert_eq!(
3957            catalog.get("Type"),
3958            Some(&PdfObject::Name(PdfName("Catalog".to_string()))),
3959        );
3960
3961        // strict() disables recovery: a missing trailer must still fail.
3962        let cursor = Cursor::new(pdf_no_trailer);
3963        assert!(PdfReader::new_with_options(cursor, ParseOptions::strict()).is_err());
3964    }
3965
3966    #[test]
3967    fn test_reader_empty_pdf() {
3968        let cursor = Cursor::new(Vec::new());
3969        let result = PdfReader::new(cursor);
3970        assert!(result.is_err());
3971    }
3972
3973    #[test]
3974    fn test_reader_page_count() {
3975        let pdf_data = create_minimal_pdf();
3976        let cursor = Cursor::new(pdf_data);
3977        let mut reader = PdfReader::new(cursor).unwrap();
3978
3979        let count = reader.page_count();
3980        assert!(count.is_ok());
3981        assert_eq!(count.unwrap(), 0); // Minimal PDF has no pages
3982    }
3983
3984    #[test]
3985    fn test_reader_into_document() {
3986        let pdf_data = create_minimal_pdf();
3987        let cursor = Cursor::new(pdf_data);
3988        let reader = PdfReader::new(cursor).unwrap();
3989
3990        let document = reader.into_document();
3991        // Document should be valid
3992        let page_count = document.page_count();
3993        assert!(page_count.is_ok());
3994    }
3995
3996    #[test]
3997    fn test_reader_pages_dict() {
3998        let pdf_data = create_minimal_pdf();
3999        let cursor = Cursor::new(pdf_data);
4000        let mut reader = PdfReader::new(cursor).unwrap();
4001
4002        let pages = reader.pages();
4003        assert!(pages.is_ok());
4004        let pages_dict = pages.unwrap();
4005        assert_eq!(
4006            pages_dict.get("Type"),
4007            Some(&PdfObject::Name(PdfName("Pages".to_string())))
4008        );
4009    }
4010
4011    #[test]
4012    fn test_reader_pdf_with_binary_data() {
4013        let pdf_data = create_pdf_with_binary_marker();
4014
4015        let cursor = Cursor::new(pdf_data);
4016        let result = PdfReader::new(cursor);
4017        assert!(result.is_ok());
4018    }
4019
4020    #[test]
4021    fn test_reader_metadata() {
4022        let pdf_data = create_pdf_with_info();
4023        let cursor = Cursor::new(pdf_data);
4024        let mut reader = PdfReader::new(cursor).unwrap();
4025
4026        let metadata = reader.metadata().unwrap();
4027        assert_eq!(metadata.title, Some("Test PDF".to_string()));
4028        assert_eq!(metadata.author, Some("Test Author".to_string()));
4029        assert_eq!(metadata.subject, Some("Testing".to_string()));
4030        assert_eq!(metadata.version, "1.4".to_string());
4031    }
4032
4033    #[test]
4034    fn test_reader_metadata_empty() {
4035        let pdf_data = create_minimal_pdf();
4036        let cursor = Cursor::new(pdf_data);
4037        let mut reader = PdfReader::new(cursor).unwrap();
4038
4039        let metadata = reader.metadata().unwrap();
4040        assert!(metadata.title.is_none());
4041        assert!(metadata.author.is_none());
4042        assert_eq!(metadata.version, "1.4".to_string());
4043        assert_eq!(metadata.page_count, Some(0));
4044    }
4045
4046    #[test]
4047    fn test_reader_object_number_mismatch() {
4048        // This test validates that the reader properly handles
4049        // object number mismatches. We'll create a valid PDF
4050        // and then try to access an object with wrong generation number
4051        let pdf_data = create_minimal_pdf();
4052        let cursor = Cursor::new(pdf_data);
4053        let mut reader = PdfReader::new(cursor).unwrap();
4054
4055        // Object 1 exists with generation 0
4056        // Try to get it with wrong generation number
4057        let result = reader.get_object(1, 99);
4058        assert!(result.is_err());
4059
4060        // Also test with a non-existent object number
4061        let result2 = reader.get_object(999, 0);
4062        assert!(result2.is_err());
4063    }
4064
4065    #[test]
4066    fn test_document_metadata_struct() {
4067        let metadata = DocumentMetadata {
4068            title: Some("Title".to_string()),
4069            author: Some("Author".to_string()),
4070            subject: Some("Subject".to_string()),
4071            keywords: Some("Keywords".to_string()),
4072            creator: Some("Creator".to_string()),
4073            producer: Some("Producer".to_string()),
4074            creation_date: Some("D:20240101".to_string()),
4075            modification_date: Some("D:20240102".to_string()),
4076            version: "1.5".to_string(),
4077            page_count: Some(10),
4078        };
4079
4080        assert_eq!(metadata.title, Some("Title".to_string()));
4081        assert_eq!(metadata.page_count, Some(10));
4082    }
4083
4084    #[test]
4085    fn test_document_metadata_default() {
4086        let metadata = DocumentMetadata::default();
4087        assert!(metadata.title.is_none());
4088        assert!(metadata.author.is_none());
4089        assert!(metadata.subject.is_none());
4090        assert!(metadata.keywords.is_none());
4091        assert!(metadata.creator.is_none());
4092        assert!(metadata.producer.is_none());
4093        assert!(metadata.creation_date.is_none());
4094        assert!(metadata.modification_date.is_none());
4095        assert_eq!(metadata.version, "".to_string());
4096        assert!(metadata.page_count.is_none());
4097    }
4098
4099    #[test]
4100    fn test_document_metadata_clone() {
4101        let metadata = DocumentMetadata {
4102            title: Some("Test".to_string()),
4103            version: "1.4".to_string(),
4104            ..Default::default()
4105        };
4106
4107        let cloned = metadata;
4108        assert_eq!(cloned.title, Some("Test".to_string()));
4109        assert_eq!(cloned.version, "1.4".to_string());
4110    }
4111
4112    #[test]
4113    fn test_reader_trailer_validation_error() {
4114        // PDF with invalid trailer (missing required keys)
4115        let bad_pdf = b"%PDF-1.4
41161 0 obj
4117<< /Type /Catalog >>
4118endobj
4119xref
41200 2
41210000000000 65535 f 
41220000000009 00000 n 
4123trailer
4124<< /Size 2 >>
4125startxref
412646
4127%%EOF"
4128            .to_vec();
4129
4130        // Issue #374: a trailer without /Root triggers recovery, which locates
4131        // the catalog by content scan and synthesizes /Root, so default parsing
4132        // succeeds and resolves the catalog.
4133        let cursor = Cursor::new(bad_pdf.clone());
4134        let mut reader = PdfReader::new(cursor).expect("missing /Root recovered by object scan");
4135        let catalog = reader
4136            .catalog()
4137            .expect("catalog resolved from scanned /Root");
4138        assert_eq!(
4139            catalog.get("Type"),
4140            Some(&PdfObject::Name(PdfName("Catalog".to_string()))),
4141        );
4142
4143        // strict() disables recovery: a trailer without /Root must still fail.
4144        let cursor = Cursor::new(bad_pdf);
4145        assert!(PdfReader::new_with_options(cursor, ParseOptions::strict()).is_err());
4146    }
4147
4148    #[test]
4149    fn test_reader_with_options() {
4150        let pdf_data = create_minimal_pdf();
4151        let cursor = Cursor::new(pdf_data);
4152        let mut options = ParseOptions::default();
4153        options.lenient_streams = true;
4154        options.max_recovery_bytes = 2000;
4155        options.collect_warnings = true;
4156
4157        let reader = PdfReader::new_with_options(cursor, options);
4158        assert!(reader.is_ok());
4159    }
4160
4161    #[test]
4162    fn test_lenient_stream_parsing() {
4163        // Create a PDF with incorrect stream length
4164        let pdf_data = b"%PDF-1.4
41651 0 obj
4166<< /Type /Catalog /Pages 2 0 R >>
4167endobj
41682 0 obj
4169<< /Type /Pages /Kids [3 0 R] /Count 1 >>
4170endobj
41713 0 obj
4172<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>
4173endobj
41744 0 obj
4175<< /Length 10 >>
4176stream
4177This is a longer stream than 10 bytes
4178endstream
4179endobj
4180xref
41810 5
41820000000000 65535 f 
41830000000009 00000 n 
41840000000058 00000 n 
41850000000116 00000 n 
41860000000219 00000 n 
4187trailer
4188<< /Size 5 /Root 1 0 R >>
4189startxref
4190299
4191%%EOF"
4192            .to_vec();
4193
4194        // Test strict mode - using strict options since new() is now lenient
4195        let cursor = Cursor::new(pdf_data.clone());
4196        let strict_options = ParseOptions::strict();
4197        let strict_reader = PdfReader::new_with_options(cursor, strict_options);
4198        // The PDF is malformed (incomplete xref), so even basic parsing fails
4199        assert!(strict_reader.is_err());
4200
4201        // Issue #374: default options allow recovery, so a PDF with a mismatched
4202        // xref (caused here by the incorrect stream /Length) is reconstructed by
4203        // scanning objects, and the catalog resolves.
4204        let cursor = Cursor::new(pdf_data);
4205        let mut options = ParseOptions::default();
4206        options.lenient_streams = true;
4207        options.max_recovery_bytes = 1000;
4208        options.collect_warnings = false;
4209        let mut reader =
4210            PdfReader::new_with_options(cursor, options).expect("mismatched xref reconstructed");
4211        let catalog = reader
4212            .catalog()
4213            .expect("catalog resolves after reconstruction");
4214        assert_eq!(
4215            catalog.get("Type"),
4216            Some(&PdfObject::Name(PdfName("Catalog".to_string()))),
4217        );
4218    }
4219
4220    #[test]
4221    fn test_parse_options_default() {
4222        let options = ParseOptions::default();
4223        assert!(!options.lenient_streams);
4224        assert_eq!(options.max_recovery_bytes, 1000);
4225        assert!(!options.collect_warnings);
4226    }
4227
4228    #[test]
4229    fn test_parse_options_clone() {
4230        let mut options = ParseOptions::default();
4231        options.lenient_streams = true;
4232        options.max_recovery_bytes = 2000;
4233        options.collect_warnings = true;
4234        let cloned = options;
4235        assert!(cloned.lenient_streams);
4236        assert_eq!(cloned.max_recovery_bytes, 2000);
4237        assert!(cloned.collect_warnings);
4238    }
4239
4240    // ===== ENCRYPTION INTEGRATION TESTS =====
4241
4242    #[allow(dead_code)]
4243    fn create_encrypted_pdf_dict() -> PdfDictionary {
4244        let mut dict = PdfDictionary::new();
4245        dict.insert(
4246            "Filter".to_string(),
4247            PdfObject::Name(PdfName("Standard".to_string())),
4248        );
4249        dict.insert("V".to_string(), PdfObject::Integer(1));
4250        dict.insert("R".to_string(), PdfObject::Integer(2));
4251        dict.insert("O".to_string(), PdfObject::String(PdfString(vec![0u8; 32])));
4252        dict.insert("U".to_string(), PdfObject::String(PdfString(vec![0u8; 32])));
4253        dict.insert("P".to_string(), PdfObject::Integer(-4));
4254        dict
4255    }
4256
4257    fn create_pdf_with_encryption() -> Vec<u8> {
4258        // Create a minimal PDF with encryption dictionary
4259        b"%PDF-1.4
42601 0 obj
4261<< /Type /Catalog /Pages 2 0 R >>
4262endobj
42632 0 obj
4264<< /Type /Pages /Kids [3 0 R] /Count 1 >>
4265endobj
42663 0 obj
4267<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
4268endobj
42694 0 obj
4270<< /Filter /Standard /V 1 /R 2 /O (32 bytes of owner password hash data) /U (32 bytes of user password hash data) /P -4 >>
4271endobj
4272xref
42730 5
42740000000000 65535 f 
42750000000009 00000 n 
42760000000058 00000 n 
42770000000116 00000 n 
42780000000201 00000 n 
4279trailer
4280<< /Size 5 /Root 1 0 R /Encrypt 4 0 R /ID [(file id)] >>
4281startxref
4282295
4283%%EOF"
4284            .to_vec()
4285    }
4286
4287    #[test]
4288    fn test_reader_encryption_detection() {
4289        // Test unencrypted PDF
4290        let unencrypted_pdf = create_minimal_pdf();
4291        let cursor = Cursor::new(unencrypted_pdf);
4292        let reader = PdfReader::new(cursor).unwrap();
4293        assert!(!reader.is_encrypted());
4294        assert!(reader.is_unlocked()); // Unencrypted PDFs are always "unlocked"
4295
4296        // Test encrypted PDF. Its xref offsets don't match, so it goes through
4297        // reconstruction (Issue #374). The recovery must preserve /Encrypt so
4298        // the document opens as ENCRYPTED and LOCKED (fail-safe), never silently
4299        // as plaintext. It is not unlockable with the empty password here
4300        // because /O and /U are placeholders.
4301        let encrypted_pdf = create_pdf_with_encryption();
4302        let cursor = Cursor::new(encrypted_pdf);
4303        let reader = PdfReader::new(cursor).expect("encrypted PDF opens as locked, not rejected");
4304        assert!(
4305            reader.is_encrypted(),
4306            "must report encrypted, not plaintext"
4307        );
4308        assert!(
4309            !reader.is_unlocked(),
4310            "must stay locked without a valid password"
4311        );
4312    }
4313
4314    #[test]
4315    fn test_reader_encryption_methods_unencrypted() {
4316        let pdf_data = create_minimal_pdf();
4317        let cursor = Cursor::new(pdf_data);
4318        let mut reader = PdfReader::new(cursor).unwrap();
4319
4320        // For unencrypted PDFs, all encryption methods should work
4321        assert!(!reader.is_encrypted());
4322        assert!(reader.is_unlocked());
4323        assert!(reader.encryption_handler().is_none());
4324        assert!(reader.encryption_handler_mut().is_none());
4325
4326        // Password attempts should succeed (no encryption)
4327        assert!(reader.unlock_with_password("any_password").unwrap());
4328        assert!(reader.try_empty_password().unwrap());
4329    }
4330
4331    #[test]
4332    fn test_reader_encryption_handler_access() {
4333        let pdf_data = create_minimal_pdf();
4334        let cursor = Cursor::new(pdf_data);
4335        let mut reader = PdfReader::new(cursor).unwrap();
4336
4337        // Test handler access methods
4338        assert!(reader.encryption_handler().is_none());
4339        assert!(reader.encryption_handler_mut().is_none());
4340
4341        // Verify state consistency
4342        assert!(!reader.is_encrypted());
4343        assert!(reader.is_unlocked());
4344    }
4345
4346    #[test]
4347    fn test_reader_multiple_password_attempts() {
4348        let pdf_data = create_minimal_pdf();
4349        let cursor = Cursor::new(pdf_data);
4350        let mut reader = PdfReader::new(cursor).unwrap();
4351
4352        // Multiple attempts on unencrypted PDF should all succeed
4353        let passwords = vec!["test1", "test2", "admin", "", "password"];
4354        for password in passwords {
4355            assert!(reader.unlock_with_password(password).unwrap());
4356        }
4357
4358        // Empty password attempts
4359        for _ in 0..5 {
4360            assert!(reader.try_empty_password().unwrap());
4361        }
4362    }
4363
4364    #[test]
4365    fn test_reader_encryption_state_consistency() {
4366        let pdf_data = create_minimal_pdf();
4367        let cursor = Cursor::new(pdf_data);
4368        let mut reader = PdfReader::new(cursor).unwrap();
4369
4370        // Verify initial state
4371        assert!(!reader.is_encrypted());
4372        assert!(reader.is_unlocked());
4373        assert!(reader.encryption_handler().is_none());
4374
4375        // State should remain consistent after password attempts
4376        let _ = reader.unlock_with_password("test");
4377        assert!(!reader.is_encrypted());
4378        assert!(reader.is_unlocked());
4379        assert!(reader.encryption_handler().is_none());
4380
4381        let _ = reader.try_empty_password();
4382        assert!(!reader.is_encrypted());
4383        assert!(reader.is_unlocked());
4384        assert!(reader.encryption_handler().is_none());
4385    }
4386
4387    #[test]
4388    fn test_reader_encryption_error_handling() {
4389        // Fail-safe (Issue #374): an encrypted PDF whose xref must be rebuilt
4390        // must never be opened as plaintext. Two outcomes are safe: rejected
4391        // during construction, or opened while still reporting encrypted+locked.
4392        // Opening it as an unlocked/unencrypted document is a fail-open bug.
4393        let encrypted_pdf = create_pdf_with_encryption();
4394        let cursor = Cursor::new(encrypted_pdf);
4395
4396        match PdfReader::new(cursor) {
4397            // Rejecting the encrypted document is safe.
4398            Err(_) => {}
4399            // Opening it is only safe if encryption is still recognized and the
4400            // document remains locked (no valid password was supplied).
4401            Ok(reader) => {
4402                assert!(
4403                    reader.is_encrypted() && !reader.is_unlocked(),
4404                    "recovered encrypted PDF must stay encrypted+locked, not open as plaintext"
4405                );
4406            }
4407        }
4408    }
4409
4410    #[test]
4411    fn test_reader_encryption_with_options() {
4412        let pdf_data = create_minimal_pdf();
4413        let cursor = Cursor::new(pdf_data);
4414
4415        // Test with different parsing options
4416        let strict_options = ParseOptions::strict();
4417        let strict_reader = PdfReader::new_with_options(cursor, strict_options).unwrap();
4418        assert!(!strict_reader.is_encrypted());
4419        assert!(strict_reader.is_unlocked());
4420
4421        let pdf_data = create_minimal_pdf();
4422        let cursor = Cursor::new(pdf_data);
4423        let lenient_options = ParseOptions::lenient();
4424        let lenient_reader = PdfReader::new_with_options(cursor, lenient_options).unwrap();
4425        assert!(!lenient_reader.is_encrypted());
4426        assert!(lenient_reader.is_unlocked());
4427    }
4428
4429    #[test]
4430    fn test_reader_encryption_integration_edge_cases() {
4431        let pdf_data = create_minimal_pdf();
4432        let cursor = Cursor::new(pdf_data);
4433        let mut reader = PdfReader::new(cursor).unwrap();
4434
4435        // Test edge cases with empty/special passwords
4436        assert!(reader.unlock_with_password("").unwrap());
4437        assert!(reader.unlock_with_password("   ").unwrap()); // Spaces
4438        assert!(reader
4439            .unlock_with_password("very_long_password_that_exceeds_normal_length")
4440            .unwrap());
4441        assert!(reader.unlock_with_password("unicode_test_ñáéíóú").unwrap());
4442
4443        // Special characters that might cause issues
4444        assert!(reader.unlock_with_password("pass@#$%^&*()").unwrap());
4445        assert!(reader.unlock_with_password("pass\nwith\nnewlines").unwrap());
4446        assert!(reader.unlock_with_password("pass\twith\ttabs").unwrap());
4447    }
4448
4449    mod rigorous {
4450        use super::*;
4451
4452        // =============================================================================
4453        // RIGOROUS TESTS FOR ERROR HANDLING
4454        // =============================================================================
4455
4456        #[test]
4457        fn test_reader_invalid_pdf_header() {
4458            // Not a PDF at all
4459            let invalid_data = b"This is not a PDF file";
4460            let cursor = Cursor::new(invalid_data.to_vec());
4461            let result = PdfReader::new(cursor);
4462
4463            assert!(result.is_err(), "Should fail on invalid PDF header");
4464        }
4465
4466        #[test]
4467        fn test_reader_truncated_header() {
4468            // Truncated PDF header
4469            let truncated = b"%PDF";
4470            let cursor = Cursor::new(truncated.to_vec());
4471            let result = PdfReader::new(cursor);
4472
4473            assert!(result.is_err(), "Should fail on truncated header");
4474        }
4475
4476        #[test]
4477        fn test_reader_empty_file() {
4478            let empty = Vec::new();
4479            let cursor = Cursor::new(empty);
4480            let result = PdfReader::new(cursor);
4481
4482            assert!(result.is_err(), "Should fail on empty file");
4483        }
4484
4485        #[test]
4486        fn test_reader_malformed_version() {
4487            // PDF with invalid version number
4488            let malformed = b"%PDF-X.Y\n%%\xE2\xE3\xCF\xD3\n";
4489            let cursor = Cursor::new(malformed.to_vec());
4490            let result = PdfReader::new(cursor);
4491
4492            // Should either fail or handle gracefully
4493            if let Ok(reader) = result {
4494                // If it parsed, version should have some value
4495                let _version = reader.version();
4496            }
4497        }
4498
4499        #[test]
4500        fn test_reader_get_nonexistent_object() {
4501            let pdf_data = create_minimal_pdf();
4502            let cursor = Cursor::new(pdf_data);
4503            let mut reader = PdfReader::new(cursor).unwrap();
4504
4505            // Try to get object that doesn't exist (999 0 obj)
4506            let result = reader.get_object(999, 0);
4507
4508            assert!(result.is_err(), "Should fail when object doesn't exist");
4509        }
4510
4511        #[test]
4512        fn test_reader_get_object_wrong_generation() {
4513            let pdf_data = create_minimal_pdf();
4514            let cursor = Cursor::new(pdf_data);
4515            let mut reader = PdfReader::new(cursor).unwrap();
4516
4517            // Try to get existing object with wrong generation
4518            let result = reader.get_object(1, 99);
4519
4520            // Should either fail or return the object with gen 0
4521            if let Err(e) = result {
4522                // Expected - wrong generation
4523                let _ = e;
4524            }
4525        }
4526
4527        // =============================================================================
4528        // RIGOROUS TESTS FOR OBJECT RESOLUTION
4529        // =============================================================================
4530
4531        #[test]
4532        fn test_resolve_direct_object() {
4533            let pdf_data = create_minimal_pdf();
4534            let cursor = Cursor::new(pdf_data);
4535            let mut reader = PdfReader::new(cursor).unwrap();
4536
4537            // Create a direct object (not a reference)
4538            let direct_obj = PdfObject::Integer(42);
4539
4540            let resolved = reader.resolve(&direct_obj).unwrap();
4541
4542            // Should return the same object
4543            assert_eq!(resolved, &PdfObject::Integer(42));
4544        }
4545
4546        #[test]
4547        fn test_resolve_reference() {
4548            let pdf_data = create_minimal_pdf();
4549            let cursor = Cursor::new(pdf_data);
4550            let mut reader = PdfReader::new(cursor).unwrap();
4551
4552            // Get Pages reference from catalog (extract values before resolve)
4553            let pages_ref = {
4554                let catalog = reader.catalog().unwrap();
4555                if let Some(PdfObject::Reference(obj_num, gen_num)) = catalog.get("Pages") {
4556                    PdfObject::Reference(*obj_num, *gen_num)
4557                } else {
4558                    panic!("Catalog /Pages must be a Reference");
4559                }
4560            };
4561
4562            // Now resolve it
4563            let resolved = reader.resolve(&pages_ref).unwrap();
4564
4565            // Resolved object should be a dictionary with Type = Pages
4566            if let PdfObject::Dictionary(dict) = resolved {
4567                assert_eq!(
4568                    dict.get("Type"),
4569                    Some(&PdfObject::Name(PdfName("Pages".to_string())))
4570                );
4571            } else {
4572                panic!("Expected dictionary, got: {:?}", resolved);
4573            }
4574        }
4575
4576        // =============================================================================
4577        // RIGOROUS TESTS FOR ENCRYPTION
4578        // =============================================================================
4579
4580        #[test]
4581        fn test_is_encrypted_on_unencrypted() {
4582            let pdf_data = create_minimal_pdf();
4583            let cursor = Cursor::new(pdf_data);
4584            let reader = PdfReader::new(cursor).unwrap();
4585
4586            assert!(
4587                !reader.is_encrypted(),
4588                "Minimal PDF should not be encrypted"
4589            );
4590        }
4591
4592        #[test]
4593        fn test_is_unlocked_on_unencrypted() {
4594            let pdf_data = create_minimal_pdf();
4595            let cursor = Cursor::new(pdf_data);
4596            let reader = PdfReader::new(cursor).unwrap();
4597
4598            // Unencrypted PDFs are always "unlocked"
4599            assert!(reader.is_unlocked(), "Unencrypted PDF should be unlocked");
4600        }
4601
4602        #[test]
4603        fn test_try_empty_password_on_unencrypted() {
4604            let pdf_data = create_minimal_pdf();
4605            let cursor = Cursor::new(pdf_data);
4606            let mut reader = PdfReader::new(cursor).unwrap();
4607
4608            // Should succeed (no encryption)
4609            let result = reader.try_empty_password();
4610            assert!(result.is_ok());
4611        }
4612
4613        // =============================================================================
4614        // RIGOROUS TESTS FOR PARSE OPTIONS
4615        // =============================================================================
4616
4617        #[test]
4618        fn test_reader_with_strict_options() {
4619            let pdf_data = create_minimal_pdf();
4620            let cursor = Cursor::new(pdf_data);
4621
4622            let options = ParseOptions::strict();
4623            let result = PdfReader::new_with_options(cursor, options);
4624
4625            assert!(result.is_ok(), "Minimal PDF should parse in strict mode");
4626        }
4627
4628        #[test]
4629        fn test_reader_with_lenient_options() {
4630            let pdf_data = create_minimal_pdf();
4631            let cursor = Cursor::new(pdf_data);
4632
4633            let options = ParseOptions::lenient();
4634            let result = PdfReader::new_with_options(cursor, options);
4635
4636            assert!(result.is_ok(), "Minimal PDF should parse in lenient mode");
4637        }
4638
4639        #[test]
4640        fn test_reader_options_accessible() {
4641            let pdf_data = create_minimal_pdf();
4642            let cursor = Cursor::new(pdf_data);
4643
4644            let options = ParseOptions::lenient();
4645            let reader = PdfReader::new_with_options(cursor, options.clone()).unwrap();
4646
4647            // Options should be accessible
4648            let reader_options = reader.options();
4649            assert_eq!(reader_options.strict_mode, options.strict_mode);
4650        }
4651
4652        // =============================================================================
4653        // RIGOROUS TESTS FOR CATALOG AND INFO
4654        // =============================================================================
4655
4656        #[test]
4657        fn test_catalog_has_required_fields() {
4658            let pdf_data = create_minimal_pdf();
4659            let cursor = Cursor::new(pdf_data);
4660            let mut reader = PdfReader::new(cursor).unwrap();
4661
4662            let catalog = reader.catalog().unwrap();
4663
4664            // Catalog MUST have Type = Catalog
4665            assert_eq!(
4666                catalog.get("Type"),
4667                Some(&PdfObject::Name(PdfName("Catalog".to_string()))),
4668                "Catalog must have /Type /Catalog"
4669            );
4670
4671            // Catalog MUST have Pages
4672            assert!(
4673                catalog.contains_key("Pages"),
4674                "Catalog must have /Pages entry"
4675            );
4676        }
4677
4678        #[test]
4679        fn test_info_fields_when_present() {
4680            let pdf_data = create_pdf_with_info();
4681            let cursor = Cursor::new(pdf_data);
4682            let mut reader = PdfReader::new(cursor).unwrap();
4683
4684            let info = reader.info().unwrap();
4685            assert!(info.is_some(), "PDF should have Info dictionary");
4686
4687            let info_dict = info.unwrap();
4688
4689            // Verify specific fields exist
4690            assert!(info_dict.contains_key("Title"), "Info should have Title");
4691            assert!(info_dict.contains_key("Author"), "Info should have Author");
4692        }
4693
4694        #[test]
4695        fn test_info_none_when_absent() {
4696            let pdf_data = create_minimal_pdf();
4697            let cursor = Cursor::new(pdf_data);
4698            let mut reader = PdfReader::new(cursor).unwrap();
4699
4700            let info = reader.info().unwrap();
4701            assert!(info.is_none(), "Minimal PDF should not have Info");
4702        }
4703
4704        // =============================================================================
4705        // RIGOROUS TESTS FOR VERSION PARSING
4706        // =============================================================================
4707
4708        #[test]
4709        fn test_version_exact_values() {
4710            let pdf_data = create_pdf_with_version("1.7");
4711            let cursor = Cursor::new(pdf_data);
4712            let reader = PdfReader::new(cursor).unwrap();
4713
4714            let version = reader.version();
4715            assert_eq!(version.major, 1, "Major version must be exact");
4716            assert_eq!(version.minor, 7, "Minor version must be exact");
4717        }
4718
4719        #[test]
4720        fn test_version_pdf_20() {
4721            let pdf_data = create_pdf_with_version("2.0");
4722            let cursor = Cursor::new(pdf_data);
4723            let reader = PdfReader::new(cursor).unwrap();
4724
4725            let version = reader.version();
4726            assert_eq!(version.major, 2, "PDF 2.0 major version");
4727            assert_eq!(version.minor, 0, "PDF 2.0 minor version");
4728        }
4729
4730        // =============================================================================
4731        // RIGOROUS TESTS FOR PAGES AND PAGE_COUNT
4732        // =============================================================================
4733
4734        #[test]
4735        fn test_pages_returns_pages_dict() {
4736            let pdf_data = create_minimal_pdf();
4737            let cursor = Cursor::new(pdf_data);
4738            let mut reader = PdfReader::new(cursor).unwrap();
4739
4740            let pages_dict = reader
4741                .pages()
4742                .expect("pages() must return Pages dictionary");
4743
4744            assert_eq!(
4745                pages_dict.get("Type"),
4746                Some(&PdfObject::Name(PdfName("Pages".to_string()))),
4747                "Pages dict must have /Type /Pages"
4748            );
4749        }
4750
4751        #[test]
4752        fn test_page_count_minimal_pdf() {
4753            let pdf_data = create_minimal_pdf();
4754            let cursor = Cursor::new(pdf_data);
4755            let mut reader = PdfReader::new(cursor).unwrap();
4756
4757            let count = reader.page_count().expect("page_count() must succeed");
4758            assert_eq!(count, 0, "Minimal PDF has 0 pages");
4759        }
4760
4761        #[test]
4762        fn test_page_count_with_info_pdf() {
4763            let pdf_data = create_pdf_with_info();
4764            let cursor = Cursor::new(pdf_data);
4765            let mut reader = PdfReader::new(cursor).unwrap();
4766
4767            let count = reader.page_count().expect("page_count() must succeed");
4768            assert_eq!(count, 0, "create_pdf_with_info() has Count 0 in Pages dict");
4769        }
4770
4771        // =============================================================================
4772        // RIGOROUS TESTS FOR METADATA
4773        // =============================================================================
4774
4775        #[test]
4776        fn test_metadata_minimal_pdf() {
4777            let pdf_data = create_minimal_pdf();
4778            let cursor = Cursor::new(pdf_data);
4779            let mut reader = PdfReader::new(cursor).unwrap();
4780
4781            let meta = reader.metadata().expect("metadata() must succeed");
4782
4783            // Minimal PDF has no metadata fields
4784            assert!(meta.title.is_none(), "Minimal PDF has no title");
4785            assert!(meta.author.is_none(), "Minimal PDF has no author");
4786        }
4787
4788        #[test]
4789        fn test_metadata_with_info() {
4790            let pdf_data = create_pdf_with_info();
4791            let cursor = Cursor::new(pdf_data);
4792            let mut reader = PdfReader::new(cursor).unwrap();
4793
4794            let meta = reader.metadata().expect("metadata() must succeed");
4795
4796            assert!(meta.title.is_some(), "PDF with Info has title");
4797            assert_eq!(meta.title.unwrap(), "Test PDF", "Title must match");
4798            assert!(meta.author.is_some(), "PDF with Info has author");
4799            assert_eq!(meta.author.unwrap(), "Test Author", "Author must match");
4800        }
4801
4802        // =============================================================================
4803        // RIGOROUS TESTS FOR RESOLVE_STREAM_LENGTH
4804        // =============================================================================
4805
4806        #[test]
4807        fn test_resolve_stream_length_direct_integer() {
4808            let pdf_data = create_minimal_pdf();
4809            let cursor = Cursor::new(pdf_data);
4810            let mut reader = PdfReader::new(cursor).unwrap();
4811
4812            // Pass a direct integer (Length value)
4813            let length_obj = PdfObject::Integer(100);
4814
4815            let length = reader
4816                .resolve_stream_length(&length_obj)
4817                .expect("resolve_stream_length must succeed");
4818            assert_eq!(length, Some(100), "Direct integer must be resolved");
4819        }
4820
4821        #[test]
4822        fn test_resolve_stream_length_negative_integer() {
4823            let pdf_data = create_minimal_pdf();
4824            let cursor = Cursor::new(pdf_data);
4825            let mut reader = PdfReader::new(cursor).unwrap();
4826
4827            // Negative length is invalid
4828            let length_obj = PdfObject::Integer(-10);
4829
4830            let length = reader
4831                .resolve_stream_length(&length_obj)
4832                .expect("resolve_stream_length must succeed");
4833            assert_eq!(length, None, "Negative integer returns None");
4834        }
4835
4836        #[test]
4837        fn test_resolve_stream_length_non_integer() {
4838            let pdf_data = create_minimal_pdf();
4839            let cursor = Cursor::new(pdf_data);
4840            let mut reader = PdfReader::new(cursor).unwrap();
4841
4842            // Pass a non-integer object
4843            let name_obj = PdfObject::Name(PdfName("Test".to_string()));
4844
4845            let length = reader
4846                .resolve_stream_length(&name_obj)
4847                .expect("resolve_stream_length must succeed");
4848            assert_eq!(length, None, "Non-integer object returns None");
4849        }
4850
4851        // =============================================================================
4852        // RIGOROUS TESTS FOR GET_ALL_PAGES
4853        // =============================================================================
4854
4855        #[test]
4856        fn test_get_all_pages_empty_pdf() {
4857            let pdf_data = create_minimal_pdf();
4858            let cursor = Cursor::new(pdf_data);
4859            let mut reader = PdfReader::new(cursor).unwrap();
4860
4861            let pages = reader
4862                .get_all_pages()
4863                .expect("get_all_pages() must succeed");
4864            assert_eq!(pages.len(), 0, "Minimal PDF has 0 pages");
4865        }
4866
4867        #[test]
4868        fn test_get_all_pages_with_info() {
4869            let pdf_data = create_pdf_with_info();
4870            let cursor = Cursor::new(pdf_data);
4871            let mut reader = PdfReader::new(cursor).unwrap();
4872
4873            let pages = reader
4874                .get_all_pages()
4875                .expect("get_all_pages() must succeed");
4876            assert_eq!(
4877                pages.len(),
4878                0,
4879                "create_pdf_with_info() has 0 pages (Count 0)"
4880            );
4881        }
4882
4883        // =============================================================================
4884        // RIGOROUS TESTS FOR INTO_DOCUMENT
4885        // =============================================================================
4886
4887        #[test]
4888        fn test_into_document_consumes_reader() {
4889            let pdf_data = create_minimal_pdf();
4890            let cursor = Cursor::new(pdf_data);
4891            let reader = PdfReader::new(cursor).unwrap();
4892
4893            let document = reader.into_document();
4894
4895            // Verify document has valid version
4896            let version = document.version().expect("Document must have version");
4897            assert!(
4898                version.starts_with("1."),
4899                "Document must have PDF 1.x version, got: {}",
4900                version
4901            );
4902
4903            // Verify document can access page count
4904            let page_count = document
4905                .page_count()
4906                .expect("Document must allow page_count()");
4907            assert_eq!(
4908                page_count, 0,
4909                "Minimal PDF has 0 pages (Count 0 in test helper)"
4910            );
4911        }
4912
4913        // =============================================================================
4914        // RIGOROUS TESTS FOR PARSE_CONTEXT
4915        // =============================================================================
4916
4917        #[test]
4918        fn test_clear_parse_context() {
4919            let pdf_data = create_minimal_pdf();
4920            let cursor = Cursor::new(pdf_data);
4921            let mut reader = PdfReader::new(cursor).unwrap();
4922
4923            // Clear parse context (should not panic)
4924            reader.clear_parse_context();
4925
4926            // Verify reader still works after clearing
4927            let version = reader.version();
4928            assert_eq!(version.major, 1, "Reader must still work after clear");
4929        }
4930
4931        #[test]
4932        fn test_parse_context_mut_accessible() {
4933            let pdf_data = create_minimal_pdf();
4934            let cursor = Cursor::new(pdf_data);
4935            let mut reader = PdfReader::new(cursor).unwrap();
4936
4937            let context = reader.parse_context_mut();
4938
4939            // Verify context has expected structure
4940            let initial_depth = context.depth;
4941            assert_eq!(initial_depth, 0, "Parse context must start with depth 0");
4942
4943            // Verify max_depth is set to reasonable value
4944            assert!(
4945                context.max_depth > 0,
4946                "Parse context must have positive max_depth"
4947            );
4948        }
4949
4950        // =============================================================================
4951        // RIGOROUS TESTS FOR UTILITY FUNCTIONS
4952        // =============================================================================
4953
4954        #[test]
4955        fn test_find_byte_pattern_basic() {
4956            let haystack = b"Hello World";
4957            let needle = b"World";
4958            let pos = find_byte_pattern(haystack, needle);
4959            assert_eq!(pos, Some(6), "Must find 'World' at position 6");
4960        }
4961
4962        #[test]
4963        fn test_find_byte_pattern_not_found() {
4964            let haystack = b"Hello World";
4965            let needle = b"Rust";
4966            let pos = find_byte_pattern(haystack, needle);
4967            assert_eq!(pos, None, "Must return None when not found");
4968        }
4969
4970        #[test]
4971        fn test_find_byte_pattern_at_start() {
4972            let haystack = b"Hello World";
4973            let needle = b"Hello";
4974            let pos = find_byte_pattern(haystack, needle);
4975            assert_eq!(pos, Some(0), "Must find at position 0");
4976        }
4977
4978        #[test]
4979        fn test_is_immediate_stream_start_with_stream() {
4980            let data = b"stream\ndata";
4981            assert!(
4982                is_immediate_stream_start(data),
4983                "Must detect 'stream' at start"
4984            );
4985        }
4986
4987        #[test]
4988        fn test_is_immediate_stream_start_with_whitespace() {
4989            let data = b"  \n\tstream\ndata";
4990            assert!(
4991                is_immediate_stream_start(data),
4992                "Must detect 'stream' after whitespace"
4993            );
4994        }
4995
4996        #[test]
4997        fn test_is_immediate_stream_start_no_stream() {
4998            let data = b"endobj";
4999            assert!(
5000                !is_immediate_stream_start(data),
5001                "Must return false when 'stream' absent"
5002            );
5003        }
5004    }
5005}