Skip to main content

rpdfium_core/
error.rs

1//! Error types for rpdfium.
2//!
3//! This module defines the error hierarchy used throughout the rpdfium crates.
4//! All errors use [`thiserror`] for derivation and implement `Send + Sync`.
5
6use std::fmt;
7
8/// Unique identifier for a PDF indirect object.
9///
10/// PDF indirect objects are identified by a (number, generation) pair.
11/// The generation number is incremented when an object is updated in an
12/// incremental PDF update.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct ObjectId {
15    /// Object number (1-based in the PDF file).
16    pub number: u32,
17    /// Generation number (0 for newly created objects).
18    pub generation: u16,
19}
20
21impl ObjectId {
22    /// Create a new ObjectId.
23    pub fn new(number: u32, generation: u16) -> Self {
24        Self { number, generation }
25    }
26}
27
28impl fmt::Display for ObjectId {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(f, "{} {} R", self.number, self.generation)
31    }
32}
33
34/// Top-level error type for rpdfium operations.
35#[derive(Debug, thiserror::Error)]
36pub enum PdfError {
37    /// A syntax-level parse error occurred.
38    #[error("parse error: {0}")]
39    Parse(#[from] ParseError),
40
41    /// A circular reference was detected during object resolution.
42    #[error("circular reference detected at object {0}")]
43    CircularReference(ObjectId),
44
45    /// The recursion limit was exceeded during object resolution or traversal.
46    #[error("recursion limit exceeded")]
47    RecursionLimitExceeded,
48
49    /// The referenced object does not exist in the cross-reference table.
50    #[error("unknown object: {0}")]
51    UnknownObject(ObjectId),
52
53    /// The object is not a stream when a stream was expected.
54    #[error("expected a stream object")]
55    NotAStream,
56
57    /// The decode filter chain exceeds the maximum allowed length.
58    #[error("filter chain too long")]
59    FilterChainTooLong,
60
61    /// The image dimensions exceed the maximum allowed size.
62    #[error("image too large")]
63    ImageTooLarge,
64
65    /// The decompressed stream size exceeds the maximum allowed size.
66    #[error("stream too large")]
67    StreamTooLarge,
68
69    /// A decompression bomb was detected (compression ratio exceeds limit).
70    #[error("compression bomb detected")]
71    CompressionBombDetected,
72
73    /// The number of content stream operators exceeds the maximum allowed.
74    #[error("too many operators in content stream")]
75    TooManyOperators,
76
77    /// The `endstream` keyword could not be found within the scan distance.
78    #[error("endstream scan failed")]
79    EndstreamScanFailed,
80
81    /// The PDF file header is invalid.
82    #[error("invalid PDF header")]
83    InvalidHeader,
84
85    /// The cross-reference table is invalid.
86    #[error("invalid cross-reference table")]
87    InvalidXref,
88
89    /// The PDF trailer is invalid.
90    #[error("invalid trailer")]
91    InvalidTrailer,
92
93    /// A stream decode filter failed.
94    #[error("stream decode error: {0}")]
95    StreamDecodeError(String),
96
97    /// An object stream is malformed.
98    #[error("invalid object stream")]
99    InvalidObjectStream,
100
101    /// A PDF object is invalid or has an unexpected type.
102    #[error("invalid object: {0}")]
103    InvalidObject(String),
104
105    /// The provided password is incorrect.
106    #[error("invalid password")]
107    InvalidPassword,
108
109    /// The PDF uses an unsupported encryption scheme.
110    #[error("unsupported encryption")]
111    UnsupportedEncryption,
112
113    /// The requested operation is not supported by rpdfium (excluded by design).
114    #[error("not supported: {0}")]
115    NotSupported(String),
116
117    /// An I/O error occurred.
118    #[error("I/O error: {0}")]
119    Io(#[from] std::io::Error),
120}
121
122/// Syntax-level parse error for PDF objects.
123///
124/// These errors represent problems at the PDF syntax level (tokens,
125/// object structure, etc.) as opposed to higher-level semantic errors.
126#[derive(Debug, Clone, thiserror::Error)]
127pub enum ParseError {
128    /// An unexpected token was encountered.
129    #[error("unexpected token at offset {offset}: expected {expected}, found {found}")]
130    UnexpectedToken {
131        /// Byte offset in the source where the error occurred.
132        offset: u64,
133        /// Description of what was expected.
134        expected: String,
135        /// Description of what was found.
136        found: String,
137    },
138
139    /// An unexpected end of input was reached.
140    #[error("unexpected end of input at offset {offset}")]
141    UnexpectedEof {
142        /// Byte offset where the input ended.
143        offset: u64,
144    },
145
146    /// A numeric value is out of the representable range.
147    #[error("number out of range at offset {offset}")]
148    NumberOutOfRange {
149        /// Byte offset in the source where the error occurred.
150        offset: u64,
151    },
152
153    /// A string literal is malformed (unbalanced parentheses, invalid escape, etc.).
154    #[error("invalid string at offset {offset}: {detail}")]
155    InvalidString {
156        /// Byte offset in the source where the error occurred.
157        offset: u64,
158        /// Details about the problem.
159        detail: String,
160    },
161
162    /// A name object contains invalid data.
163    #[error("invalid name at offset {offset}: {detail}")]
164    InvalidName {
165        /// Byte offset in the source where the error occurred.
166        offset: u64,
167        /// Details about the problem.
168        detail: String,
169    },
170
171    /// An invalid indirect object header (`N G obj`).
172    #[error("invalid object header at offset {offset}")]
173    InvalidObjectHeader {
174        /// Byte offset in the source where the error occurred.
175        offset: u64,
176    },
177
178    /// The `endobj` keyword is missing after an indirect object.
179    #[error("missing endobj at offset {offset}")]
180    MissingEndobj {
181        /// Byte offset in the source where the error occurred.
182        offset: u64,
183    },
184
185    /// A duplicate key was found in a dictionary.
186    #[error("duplicate dictionary key: {key}")]
187    DuplicateKey {
188        /// The duplicated key name.
189        key: String,
190    },
191
192    /// The `endstream` keyword is missing after stream data.
193    #[error("missing endstream at offset {offset}")]
194    MissingEndstream {
195        /// Byte offset in the source where the error occurred.
196        offset: u64,
197    },
198
199    /// A numeric literal is invalid (not a valid integer or real).
200    #[error("invalid number at offset {offset}")]
201    InvalidNumber {
202        /// Byte offset in the source where the error occurred.
203        offset: u64,
204    },
205
206    /// A cross-reference entry is malformed.
207    #[error("invalid xref entry at offset {offset}")]
208    InvalidXrefEntry {
209        /// Byte offset in the source where the error occurred.
210        offset: u64,
211    },
212
213    /// A generic parse error with a descriptive message.
214    #[error("parse error at offset {offset}: {message}")]
215    Other {
216        /// Byte offset in the source where the error occurred.
217        offset: u64,
218        /// Description of the error.
219        message: String,
220    },
221}
222
223/// A specialized `Result` type for rpdfium operations.
224pub type PdfResult<T> = std::result::Result<T, PdfError>;
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_object_id_display() {
232        let id = ObjectId {
233            number: 42,
234            generation: 0,
235        };
236        assert_eq!(format!("{id}"), "42 0 R");
237    }
238
239    #[test]
240    fn test_object_id_equality() {
241        let a = ObjectId {
242            number: 1,
243            generation: 0,
244        };
245        let b = ObjectId {
246            number: 1,
247            generation: 0,
248        };
249        let c = ObjectId {
250            number: 1,
251            generation: 1,
252        };
253        assert_eq!(a, b);
254        assert_ne!(a, c);
255    }
256
257    #[test]
258    fn test_pdf_error_from_parse_error() {
259        let parse_err = ParseError::UnexpectedEof { offset: 100 };
260        let pdf_err: PdfError = parse_err.into();
261        assert!(matches!(pdf_err, PdfError::Parse(_)));
262    }
263
264    #[test]
265    fn test_pdf_error_from_io_error() {
266        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
267        let pdf_err: PdfError = io_err.into();
268        assert!(matches!(pdf_err, PdfError::Io(_)));
269    }
270
271    #[test]
272    fn test_pdf_error_display() {
273        let err = PdfError::CircularReference(ObjectId {
274            number: 5,
275            generation: 0,
276        });
277        assert_eq!(
278            format!("{err}"),
279            "circular reference detected at object 5 0 R"
280        );
281    }
282
283    #[test]
284    fn test_parse_error_display() {
285        let err = ParseError::UnexpectedToken {
286            offset: 42,
287            expected: "integer".to_string(),
288            found: "name".to_string(),
289        };
290        assert_eq!(
291            format!("{err}"),
292            "unexpected token at offset 42: expected integer, found name"
293        );
294    }
295
296    #[test]
297    fn test_pdf_error_is_send_sync() {
298        fn assert_send_sync<T: Send + Sync>() {}
299        assert_send_sync::<PdfError>();
300    }
301
302    #[test]
303    fn test_parse_error_is_send_sync() {
304        fn assert_send_sync<T: Send + Sync>() {}
305        assert_send_sync::<ParseError>();
306    }
307}