Skip to main content

oxidize_pdf/parser/
xref_stream.rs

1//! Cross-reference stream support for PDF 1.5+
2//!
3//! This module implements cross-reference streams according to
4//! ISO 32000-1:2008 Section 7.5.8 (Cross-Reference Streams).
5//!
6//! Cross-reference streams are an alternative to traditional xref tables,
7//! providing more compact representation and supporting compressed object streams.
8
9use crate::parser::objects::{PdfArray, PdfDictionary, PdfName, PdfObject};
10use crate::parser::ParseOptions;
11use crate::parser::{ParseError, ParseResult};
12use std::io::{Read, Seek};
13
14/// Cross-reference entry
15#[derive(Debug, Clone, PartialEq)]
16pub enum XRefEntry {
17    /// Free object entry
18    Free {
19        /// Next free object number
20        next_free_object: u32,
21        /// Generation number
22        generation: u16,
23    },
24    /// In-use object entry
25    InUse {
26        /// Byte offset in the file
27        offset: u64,
28        /// Generation number
29        generation: u16,
30    },
31    /// Compressed object entry (PDF 1.5+)
32    Compressed {
33        /// Object number of the object stream containing this object
34        stream_object_number: u32,
35        /// Index of this object within the object stream
36        index_within_stream: u32,
37    },
38}
39
40/// Cross-reference stream parser
41pub struct XRefStream {
42    /// Stream dictionary
43    pub dict: PdfDictionary,
44    /// Decoded stream data
45    pub data: Vec<u8>,
46    /// Field widths from W array
47    pub widths: Vec<usize>,
48    /// Index array (pairs of [first_object_number, count])
49    pub index: Vec<(u32, u32)>,
50}
51
52impl XRefStream {
53    /// Parse a cross-reference stream from already-decoded entry bytes.
54    ///
55    /// `stream_data` must be the decoded (decompressed) content: the caller is
56    /// responsible for running `stream.decode()` before invoking this function.
57    /// The `stream_dict` may still carry `/Filter` and `/DecodeParms`, but this
58    /// function does NOT apply them. Passing raw compressed bytes here would
59    /// re-inflate already-inflated data and produce a bogus "Xref stream data
60    /// truncated" error (issue #341).
61    pub fn parse<R: Read + Seek>(
62        _reader: &mut R,
63        stream_dict: PdfDictionary,
64        stream_data: Vec<u8>,
65        _options: &ParseOptions,
66    ) -> ParseResult<Self> {
67        // Get the W (widths) array
68        let widths = stream_dict
69            .get("W")
70            .and_then(|obj| obj.as_array())
71            .ok_or_else(|| ParseError::MissingKey("W array in xref stream".to_string()))?
72            .0
73            .iter()
74            .map(|obj| {
75                obj.as_integer()
76                    .ok_or_else(|| ParseError::SyntaxError {
77                        position: 0,
78                        message: "Invalid width in W array".to_string(),
79                    })
80                    .map(|n| n as usize)
81            })
82            .collect::<ParseResult<Vec<_>>>()?;
83
84        if widths.len() != 3 {
85            return Err(ParseError::SyntaxError {
86                position: 0,
87                message: format!(
88                    "W array must have 3 elements, found {len}",
89                    len = widths.len()
90                ),
91            });
92        }
93
94        // Get the Index array if present
95        let index =
96            if let Some(index_array) = stream_dict.get("Index").and_then(|obj| obj.as_array()) {
97                let mut index_pairs = Vec::new();
98                let mut i = 0;
99                while i + 1 < index_array.len() {
100                    let first =
101                        index_array.0[i]
102                            .as_integer()
103                            .ok_or_else(|| ParseError::SyntaxError {
104                                position: 0,
105                                message: "Invalid first object number in Index".to_string(),
106                            })? as u32;
107                    let count = index_array.0[i + 1].as_integer().ok_or_else(|| {
108                        ParseError::SyntaxError {
109                            position: 0,
110                            message: "Invalid count in Index".to_string(),
111                        }
112                    })? as u32;
113                    index_pairs.push((first, count));
114                    i += 2;
115                }
116                index_pairs
117            } else {
118                // Default: start at 0, count is Size
119                let size = stream_dict
120                    .get("Size")
121                    .and_then(|obj| obj.as_integer())
122                    .ok_or_else(|| ParseError::MissingKey("Size in xref stream".to_string()))?
123                    as u32;
124                vec![(0, size)]
125            };
126
127        // The `stream_data` argument is ALREADY decoded: the only production
128        // caller (`xref.rs`) runs `stream.decode()` (FlateDecode + any predictor)
129        // before handing the buffer to `parse`. The stream dict still carries
130        // `/Filter` (and possibly `/DecodeParms`), but re-applying the filter
131        // here would inflate already-inflated bytes — yielding 0 bytes and a
132        // bogus "Xref stream data truncated" error (issue #341). The contract is
133        // therefore: `parse` does not decode; it consumes decoded entry bytes.
134        let decoded_data = stream_data;
135
136        Ok(XRefStream {
137            dict: stream_dict,
138            data: decoded_data,
139            widths,
140            index,
141        })
142    }
143
144    /// Convert the cross-reference stream to XRefTable entries
145    pub fn to_xref_entries(&self) -> ParseResult<Vec<(u32, XRefEntry)>> {
146        let mut entries = Vec::new();
147        let entry_size = self.widths.iter().sum::<usize>();
148
149        if entry_size == 0 {
150            return Err(ParseError::SyntaxError {
151                position: 0,
152                message: "Invalid entry size (0) in xref stream".to_string(),
153            });
154        }
155
156        let mut data_offset = 0;
157
158        for &(first_obj, count) in &self.index {
159            for i in 0..count {
160                if data_offset + entry_size > self.data.len() {
161                    return Err(ParseError::SyntaxError {
162                        position: data_offset,
163                        message: format!("Xref stream data truncated at obj {}", first_obj + i),
164                    });
165                }
166
167                // Read fields according to widths
168                let mut field_offset = data_offset;
169                let mut fields = Vec::new();
170
171                for &width in &self.widths {
172                    let field_value = if width == 0 {
173                        0 // Default value when width is 0
174                    } else {
175                        read_field(&self.data[field_offset..field_offset + width])
176                    };
177                    fields.push(field_value);
178                    field_offset += width;
179                }
180
181                // Interpret fields based on type
182                let entry_type = fields[0];
183                let obj_num = first_obj + i;
184
185                let entry = match entry_type {
186                    0 => {
187                        // Type 0: Free object
188                        XRefEntry::Free {
189                            next_free_object: fields[1] as u32,
190                            generation: fields[2] as u16,
191                        }
192                    }
193                    1 => {
194                        // Type 1: Uncompressed object
195                        XRefEntry::InUse {
196                            offset: fields[1],
197                            generation: fields[2] as u16,
198                        }
199                    }
200                    2 => {
201                        // Type 2: Compressed object
202                        XRefEntry::Compressed {
203                            stream_object_number: fields[1] as u32,
204                            index_within_stream: fields[2] as u32,
205                        }
206                    }
207                    _ => {
208                        return Err(ParseError::SyntaxError {
209                            position: data_offset,
210                            message: format!("Invalid xref entry type: {entry_type}"),
211                        });
212                    }
213                };
214
215                entries.push((obj_num, entry));
216                data_offset += entry_size;
217            }
218        }
219
220        Ok(entries)
221    }
222
223    /// Get the trailer dictionary from the xref stream
224    pub fn trailer_dict(&self) -> &PdfDictionary {
225        &self.dict
226    }
227
228    /// Check if this is a hybrid reference file
229    pub fn is_hybrid(&self) -> bool {
230        // A hybrid file has both xref stream and traditional xref table
231        self.dict.get("XRefStm").is_some()
232    }
233
234    /// Get the offset to additional xref stream (for hybrid files)
235    pub fn get_xref_stm_offset(&self) -> Option<u64> {
236        self.dict
237            .get("XRefStm")
238            .and_then(|obj| obj.as_integer())
239            .map(|n| n as u64)
240    }
241
242    /// Get the previous xref offset
243    pub fn get_prev_offset(&self) -> Option<u64> {
244        self.dict
245            .get("Prev")
246            .and_then(|obj| obj.as_integer())
247            .map(|n| n as u64)
248    }
249}
250
251/// Read a field from bytes (big-endian)
252fn read_field(bytes: &[u8]) -> u64 {
253    let mut value = 0u64;
254    for &byte in bytes {
255        value = (value << 8) | (byte as u64);
256    }
257    value
258}
259
260/// XRef stream builder for creating new xref streams
261pub struct XRefStreamBuilder {
262    /// Entries to include in the stream
263    entries: Vec<(u32, XRefEntry)>,
264    /// Additional trailer dictionary entries
265    trailer_entries: PdfDictionary,
266}
267
268impl Default for XRefStreamBuilder {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274impl XRefStreamBuilder {
275    /// Create a new XRef stream builder
276    pub fn new() -> Self {
277        Self {
278            entries: Vec::new(),
279            trailer_entries: PdfDictionary::new(),
280        }
281    }
282
283    /// Add an entry to the xref stream
284    pub fn add_entry(&mut self, obj_num: u32, entry: XRefEntry) {
285        self.entries.push((obj_num, entry));
286    }
287
288    /// Add a trailer dictionary entry
289    pub fn add_trailer_entry(&mut self, key: &str, value: PdfObject) {
290        self.trailer_entries.insert(key.to_string(), value);
291    }
292
293    /// Build the xref stream
294    pub fn build(mut self) -> ParseResult<(PdfDictionary, Vec<u8>)> {
295        // Sort entries by object number
296        self.entries.sort_by_key(|(num, _)| *num);
297
298        // Determine field widths
299        let mut max_offset = 0u64;
300        let mut max_obj_num = 0u32;
301        let mut max_gen = 0u16;
302        let mut _has_compressed = false;
303
304        for (obj_num, entry) in &self.entries {
305            max_obj_num = max_obj_num.max(*obj_num);
306            match entry {
307                XRefEntry::InUse { offset, generation } => {
308                    max_offset = max_offset.max(*offset);
309                    max_gen = max_gen.max(*generation);
310                }
311                XRefEntry::Free { generation, .. } => {
312                    max_gen = max_gen.max(*generation);
313                }
314                XRefEntry::Compressed {
315                    stream_object_number,
316                    index_within_stream,
317                } => {
318                    _has_compressed = true;
319                    max_obj_num = max_obj_num.max(*stream_object_number);
320                    max_offset = max_offset.max(*index_within_stream as u64);
321                }
322            }
323        }
324
325        // Calculate minimum bytes needed for each field
326        let w1 = 1; // Type field (0, 1, or 2)
327        let w2 = bytes_needed(max_offset.max(max_obj_num as u64));
328        let w3 = bytes_needed(max_gen as u64);
329
330        // Build the stream data
331        let mut stream_data = Vec::new();
332
333        for (_obj_num, entry) in &self.entries {
334            match entry {
335                XRefEntry::Free {
336                    next_free_object,
337                    generation,
338                } => {
339                    write_field(&mut stream_data, 0, w1); // Type 0
340                    write_field(&mut stream_data, *next_free_object as u64, w2);
341                    write_field(&mut stream_data, *generation as u64, w3);
342                }
343                XRefEntry::InUse { offset, generation } => {
344                    write_field(&mut stream_data, 1, w1); // Type 1
345                    write_field(&mut stream_data, *offset, w2);
346                    write_field(&mut stream_data, *generation as u64, w3);
347                }
348                XRefEntry::Compressed {
349                    stream_object_number,
350                    index_within_stream,
351                } => {
352                    write_field(&mut stream_data, 2, w1); // Type 2
353                    write_field(&mut stream_data, *stream_object_number as u64, w2);
354                    write_field(&mut stream_data, *index_within_stream as u64, w3);
355                }
356            }
357        }
358
359        // Build the stream dictionary
360        let mut dict = self.trailer_entries;
361        dict.insert(
362            "Type".to_string(),
363            PdfObject::Name(PdfName("XRef".to_string())),
364        );
365        dict.insert(
366            "W".to_string(),
367            PdfObject::Array(PdfArray(vec![
368                PdfObject::Integer(w1 as i64),
369                PdfObject::Integer(w2 as i64),
370                PdfObject::Integer(w3 as i64),
371            ])),
372        );
373
374        // Add Size
375        let size = self.entries.iter().map(|(n, _)| n + 1).max().unwrap_or(0);
376        dict.insert("Size".to_string(), PdfObject::Integer(size as i64));
377
378        // Add Index array if not starting from 0
379        if !self.entries.is_empty() {
380            let first = self.entries[0].0;
381            let count = self.entries.len() as u32;
382            if first != 0 {
383                dict.insert(
384                    "Index".to_string(),
385                    PdfObject::Array(PdfArray(vec![
386                        PdfObject::Integer(first as i64),
387                        PdfObject::Integer(count as i64),
388                    ])),
389                );
390            }
391        }
392
393        // Add Length
394        dict.insert(
395            "Length".to_string(),
396            PdfObject::Integer(stream_data.len() as i64),
397        );
398
399        // Apply compression (FlateDecode)
400        let compressed = compress_data(&stream_data)?;
401        dict.insert(
402            "Filter".to_string(),
403            PdfObject::Name(PdfName("FlateDecode".to_string())),
404        );
405
406        Ok((dict, compressed))
407    }
408}
409
410/// Calculate minimum bytes needed to represent a value
411fn bytes_needed(value: u64) -> usize {
412    if value == 0 {
413        1
414    } else {
415        ((64 - value.leading_zeros()).div_ceil(8)) as usize
416    }
417}
418
419/// Write a field value with specified width (big-endian)
420fn write_field(output: &mut Vec<u8>, value: u64, width: usize) {
421    for i in (0..width).rev() {
422        output.push((value >> (i * 8)) as u8);
423    }
424}
425
426/// Compress data using flate compression
427fn compress_data(data: &[u8]) -> ParseResult<Vec<u8>> {
428    use flate2::write::ZlibEncoder;
429    use flate2::Compression;
430    use std::io::Write;
431
432    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
433    encoder
434        .write_all(data)
435        .map_err(|e| ParseError::StreamDecodeError(format!("Compression failed: {e}")))?;
436    encoder
437        .finish()
438        .map_err(|e| ParseError::StreamDecodeError(format!("Compression failed: {e}")))
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use crate::parser::filters::{apply_filter, Filter};
445
446    #[test]
447    fn test_read_field() {
448        assert_eq!(read_field(&[0x00]), 0);
449        assert_eq!(read_field(&[0xFF]), 255);
450        assert_eq!(read_field(&[0x01, 0x23]), 0x0123);
451        assert_eq!(read_field(&[0x12, 0x34, 0x56]), 0x123456);
452    }
453
454    #[test]
455    fn test_write_field() {
456        let mut data = Vec::new();
457        write_field(&mut data, 0x1234, 2);
458        assert_eq!(data, vec![0x12, 0x34]);
459
460        data.clear();
461        write_field(&mut data, 0xFF, 1);
462        assert_eq!(data, vec![0xFF]);
463
464        data.clear();
465        write_field(&mut data, 0x123456, 3);
466        assert_eq!(data, vec![0x12, 0x34, 0x56]);
467    }
468
469    #[test]
470    fn test_bytes_needed() {
471        assert_eq!(bytes_needed(0), 1);
472        assert_eq!(bytes_needed(0xFF), 1);
473        assert_eq!(bytes_needed(0x100), 2);
474        assert_eq!(bytes_needed(0xFFFF), 2);
475        assert_eq!(bytes_needed(0x10000), 3);
476        assert_eq!(bytes_needed(0xFFFFFF), 3);
477        assert_eq!(bytes_needed(0x1000000), 4);
478    }
479
480    #[test]
481    fn test_xref_stream_builder() {
482        let mut builder = XRefStreamBuilder::new();
483
484        // Add some entries
485        builder.add_entry(
486            0,
487            XRefEntry::Free {
488                next_free_object: 0,
489                generation: 65535,
490            },
491        );
492
493        builder.add_entry(
494            1,
495            XRefEntry::InUse {
496                offset: 15,
497                generation: 0,
498            },
499        );
500
501        builder.add_entry(
502            2,
503            XRefEntry::Compressed {
504                stream_object_number: 5,
505                index_within_stream: 0,
506            },
507        );
508
509        let result = builder.build();
510        assert!(result.is_ok());
511
512        let (dict, _data) = result.unwrap();
513
514        // Check dictionary entries
515        assert_eq!(
516            dict.get("Type")
517                .and_then(|o| o.as_name())
518                .map(|n| n.0.as_str()),
519            Some("XRef")
520        );
521        assert!(dict.get("W").is_some());
522        assert!(dict.get("Size").is_some());
523        assert!(dict.get("Filter").is_some());
524    }
525
526    /// Build a representative xref stream and decode its data exactly the way
527    /// the production caller (`xref.rs`) does (`stream.decode()` first), then
528    /// return the dict (which still carries `/Filter`) together with the
529    /// already-decoded entry bytes. This is the precise calling convention that
530    /// `XRefStream::parse` receives in production.
531    fn decoded_xref_stream_inputs() -> (PdfDictionary, Vec<u8>) {
532        let mut builder = XRefStreamBuilder::new();
533        builder.add_entry(
534            0,
535            XRefEntry::Free {
536                next_free_object: 0,
537                generation: 65535,
538            },
539        );
540        builder.add_entry(
541            1,
542            XRefEntry::InUse {
543                offset: 15,
544                generation: 0,
545            },
546        );
547        builder.add_entry(
548            2,
549            XRefEntry::Compressed {
550                stream_object_number: 5,
551                index_within_stream: 0,
552            },
553        );
554
555        let (dict, compressed) = builder.build().unwrap();
556        // Mirror production: the caller decodes via `stream.decode()` before
557        // `parse`. The builder emits FlateDecode, so decoding with FlateDecode
558        // here reproduces exactly what `stream.decode()` would yield. If the
559        // builder ever switches filter (predictor/LZW), this line must follow it
560        // or the decoded-input precondition no longer matches production.
561        let decoded = apply_filter(&compressed, Filter::FlateDecode).unwrap();
562        (dict, decoded)
563    }
564
565    fn assert_three_entries(stream: &XRefStream) {
566        let entries = stream.to_xref_entries().unwrap();
567        assert_eq!(entries.len(), 3, "must recover all three xref entries");
568        assert!(matches!(entries[0].1, XRefEntry::Free { .. }));
569        assert!(matches!(
570            entries[1].1,
571            XRefEntry::InUse {
572                offset: 15,
573                generation: 0
574            }
575        ));
576        assert!(matches!(
577            entries[2].1,
578            XRefEntry::Compressed {
579                stream_object_number: 5,
580                index_within_stream: 0
581            }
582        ));
583    }
584
585    /// Issue #341: `parse` receives already-decoded data from the only caller.
586    /// It must NOT re-apply the `/Filter` still present in the dict. Before the
587    /// fix, re-inflating already-inflated bytes produced 0 bytes and
588    /// `to_xref_entries` reported "Xref stream data truncated".
589    #[test]
590    fn parse_treats_input_as_already_decoded_flate() {
591        let (dict, decoded) = decoded_xref_stream_inputs();
592        assert!(
593            dict.get("Filter").is_some(),
594            "precondition: dict still carries /Filter, as in production"
595        );
596
597        let mut reader = std::io::Cursor::new(Vec::<u8>::new());
598        let stream =
599            XRefStream::parse(&mut reader, dict, decoded, &ParseOptions::default()).unwrap();
600
601        assert_three_entries(&stream);
602    }
603
604    #[test]
605    fn test_xref_entry_parsing() {
606        // Test data for xref stream entries
607        // Type 1 entry: offset=1000, generation=0
608        let entry_data = vec![
609            1, // Type 1
610            0x03, 0xE8, // Offset = 1000 (0x03E8)
611            0,    // Generation = 0
612        ];
613
614        let xref_stream = XRefStream {
615            dict: PdfDictionary::new(),
616            data: entry_data,
617            widths: vec![1, 2, 1],
618            index: vec![(10, 1)],
619        };
620
621        let entries = xref_stream.to_xref_entries().unwrap();
622        assert_eq!(entries.len(), 1);
623
624        let (obj_num, entry) = &entries[0];
625        assert_eq!(*obj_num, 10);
626
627        match entry {
628            XRefEntry::InUse { offset, generation } => {
629                assert_eq!(*offset, 1000);
630                assert_eq!(*generation, 0);
631            }
632            _ => panic!("Expected InUse entry"),
633        }
634    }
635
636    #[test]
637    fn test_compressed_entry_parsing() {
638        // Test compressed object entry
639        let entry_data = vec![
640            2, // Type 2 (compressed)
641            0x00, 0x05, // Stream object number = 5
642            0x00, 0x03, // Index within stream = 3
643        ];
644
645        let xref_stream = XRefStream {
646            dict: PdfDictionary::new(),
647            data: entry_data,
648            widths: vec![1, 2, 2],
649            index: vec![(20, 1)],
650        };
651
652        let entries = xref_stream.to_xref_entries().unwrap();
653        assert_eq!(entries.len(), 1);
654
655        let (obj_num, entry) = &entries[0];
656        assert_eq!(*obj_num, 20);
657
658        match entry {
659            XRefEntry::Compressed {
660                stream_object_number,
661                index_within_stream,
662            } => {
663                assert_eq!(*stream_object_number, 5);
664                assert_eq!(*index_within_stream, 3);
665            }
666            _ => panic!("Expected Compressed entry"),
667        }
668    }
669
670    #[test]
671    fn test_multiple_index_ranges() {
672        // Test with multiple index ranges
673        let entry_data = vec![
674            // First range: objects 0-1 (each entry: 1+2+2=5 bytes)
675            0, 0, 0, 0xFF, 0xFF, // Free object 0
676            1, 0, 0x0A, 0, 0, // InUse object 1 at offset 10
677            // Second range: objects 10-11
678            1, 0, 0x14, 0, 0, // InUse object 10 at offset 20
679            1, 0, 0x1E, 0, 0, // InUse object 11 at offset 30
680        ];
681
682        let xref_stream = XRefStream {
683            dict: PdfDictionary::new(),
684            data: entry_data,
685            widths: vec![1, 2, 2],
686            index: vec![(0, 2), (10, 2)],
687        };
688
689        let entries = xref_stream.to_xref_entries().unwrap();
690        assert_eq!(entries.len(), 4);
691
692        // Check object numbers
693        assert_eq!(entries[0].0, 0);
694        assert_eq!(entries[1].0, 1);
695        assert_eq!(entries[2].0, 10);
696        assert_eq!(entries[3].0, 11);
697    }
698}