Skip to main content

xberg_native_pdf/
error.rs

1//! Error types for the PDF library.
2//!
3//! This module defines all error types that can occur during PDF parsing and processing.
4
5#![forbid(unsafe_code)]
6
7/// Result type alias for PDF library operations.
8pub type Result<T> = std::result::Result<T, Error>;
9
10/// Error types that can occur during PDF processing.
11#[derive(Debug, thiserror::Error)]
12#[allow(clippy::enum_variant_names)]
13pub enum Error {
14    /// Invalid PDF header (expected '%PDF-')
15    #[error("Invalid PDF header: expected '%PDF-', found '{0}'")]
16    InvalidHeader(String),
17
18    /// Unsupported PDF version
19    #[error("Unsupported PDF version: {0}")]
20    UnsupportedVersion(String),
21
22    /// Parse error at specific byte offset
23    #[error("Failed to parse object at byte {offset}: {reason}")]
24    ParseError {
25        /// Byte offset where error occurred
26        offset: usize,
27        /// Reason for parse failure
28        reason: String,
29    },
30
31    /// Parse warning (non-fatal)
32    #[error("Parse warning at byte {offset}: {message}")]
33    ParseWarning {
34        /// Byte offset where warning occurred
35        offset: usize,
36        /// Warning message
37        message: String,
38    },
39
40    /// Invalid cross-reference table
41    #[error("Invalid cross-reference table")]
42    InvalidXref,
43
44    /// Referenced object not found in cross-reference table
45    #[error("Object not found: {0} {1} R")]
46    ObjectNotFound(u32, u16),
47
48    /// Object has wrong type
49    #[error("Invalid object type: expected {expected}, found {found}")]
50    InvalidObjectType {
51        /// Expected object type
52        expected: String,
53        /// Actual object type found
54        found: String,
55    },
56
57    /// Unexpected end of file
58    #[error("End of file reached unexpectedly")]
59    UnexpectedEof,
60
61    /// IO error
62    #[error("IO error: {0}")]
63    Io(#[from] std::io::Error),
64
65    /// UTF-8 decoding error
66    #[error("UTF-8 decoding error: {0}")]
67    Utf8Error(#[from] std::str::Utf8Error),
68
69    /// Unsupported feature
70    #[error("Unsupported feature: {0}")]
71    Unsupported(String),
72
73    /// Invalid PDF structure (generic)
74    #[error("Invalid PDF: {0}")]
75    InvalidPdf(String),
76
77    /// Stream decoding error
78    #[error("Stream decoding error: {0}")]
79    Decode(String),
80
81    /// Encoding error (e.g., image encoding)
82    #[error("Encoding error: {0}")]
83    Encode(String),
84
85    /// Unsupported stream filter
86    #[error("Unsupported filter: {0}")]
87    UnsupportedFilter(String),
88
89    /// Font error
90    #[error("Font error: {0}")]
91    Font(String),
92
93    /// Image error
94    #[error("Image error: {0}")]
95    Image(String),
96
97    /// Circular reference detected in object graph
98    #[error("Circular reference detected: object {0}")]
99    CircularReference(crate::object::ObjectRef),
100
101    /// Recursion depth limit exceeded
102    #[error("Recursion depth limit exceeded (max: {0})")]
103    RecursionLimitExceeded(u32),
104
105    /// Invalid operation (e.g., calling methods on uninitialized document)
106    #[error("Invalid operation: {0}")]
107    InvalidOperation(String),
108
109    /// Layout analysis error
110    #[error("Layout analysis error: {0}")]
111    LayoutAnalysis(String),
112
113    /// PDF is encrypted and has not been authenticated with the correct password.
114    ///
115    /// This error is returned when attempting to extract content from a PDF that
116    /// requires a password. Call `authenticate(password)` or open with
117    /// `PdfDocument::open_with_password()` before extracting content.
118    #[error("PDF is encrypted and requires a password. Call authenticate(password) before extracting content.")]
119    EncryptedPdf,
120
121    /// Runtime error during PostScript Type 4 function evaluation
122    /// (stack underflow, typecheck, division by zero, overflow, undefined
123    /// operations).
124    ///
125    /// Distinct from [`Error::InvalidPdf`], which means a malformed parse;
126    /// `Type4Runtime` means a syntactically valid program failed at execution
127    /// time. Callers should typically fall back to a default gray rendering
128    /// rather than failing the entire page.
129    #[error("Type 4 runtime error: {0}")]
130    Type4Runtime(String),
131}
132
133impl Error {
134    pub(crate) const fn telemetry_code(&self) -> &'static str {
135        match self {
136            Self::InvalidHeader(_) => "invalid_header",
137            Self::UnsupportedVersion(_) => "unsupported_version",
138            Self::ParseError { .. } => "parse_error",
139            Self::ParseWarning { .. } => "parse_warning",
140            Self::InvalidXref => "invalid_xref",
141            Self::ObjectNotFound(_, _) => "object_not_found",
142            Self::InvalidObjectType { .. } => "invalid_object_type",
143            Self::UnexpectedEof => "unexpected_eof",
144            Self::Io(_) => "io_error",
145            Self::Utf8Error(_) => "utf8_error",
146            Self::Unsupported(_) => "unsupported",
147            Self::InvalidPdf(_) => "invalid_pdf",
148            Self::Decode(_) => "decode_error",
149            Self::Encode(_) => "encode_error",
150            Self::UnsupportedFilter(_) => "unsupported_filter",
151            Self::Font(_) => "font_error",
152            Self::Image(_) => "image_error",
153            Self::CircularReference(_) => "circular_reference",
154            Self::RecursionLimitExceeded(_) => "recursion_limit_exceeded",
155            Self::InvalidOperation(_) => "invalid_operation",
156            Self::LayoutAnalysis(_) => "layout_analysis_error",
157            Self::EncryptedPdf => "encrypted_pdf",
158            Self::Type4Runtime(_) => "type4_runtime_error",
159        }
160    }
161
162    pub(crate) const fn telemetry_offset(&self) -> Option<usize> {
163        match self {
164            Self::ParseError { offset, .. } | Self::ParseWarning { offset, .. } => Some(*offset),
165            _ => None,
166        }
167    }
168}
169
170pub(crate) fn trace_recovery(operation: &'static str, error: &Error) {
171    tracing::warn!(
172        target: crate::LOG_TARGET_ROOT,
173        operation,
174        error_code = error.telemetry_code(),
175        error_offset = ?error.telemetry_offset(),
176        "PDF operation degraded"
177    );
178}
179
180pub(crate) fn trace_failure(operation: &'static str, error: &Error) {
181    tracing::error!(
182        target: crate::LOG_TARGET_ROOT,
183        operation,
184        error_code = error.telemetry_code(),
185        error_offset = ?error.telemetry_offset(),
186        "PDF operation failed"
187    );
188}
189
190pub(crate) fn trace_recovery_count(operation: &'static str, error_code: &'static str, skipped_count: usize) {
191    tracing::warn!(
192        target: crate::LOG_TARGET_ROOT,
193        operation,
194        error_code,
195        skipped_count,
196        "PDF recovery skipped malformed input"
197    );
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_invalid_header_error() {
206        let err = Error::InvalidHeader("NotAPDF".to_string());
207        let msg = format!("{}", err);
208        assert!(msg.contains("Invalid PDF header"));
209        assert!(msg.contains("NotAPDF"));
210    }
211
212    #[test]
213    fn test_unsupported_version_error() {
214        let err = Error::UnsupportedVersion("3.0".to_string());
215        let msg = format!("{}", err);
216        assert!(msg.contains("Unsupported PDF version"));
217        assert!(msg.contains("3.0"));
218    }
219
220    #[test]
221    fn test_parse_error() {
222        let err = Error::ParseError {
223            offset: 1234,
224            reason: "invalid token".to_string(),
225        };
226        let msg = format!("{}", err);
227        assert!(msg.contains("1234"));
228        assert!(msg.contains("invalid token"));
229    }
230
231    #[test]
232    fn test_object_not_found_error() {
233        let err = Error::ObjectNotFound(10, 0);
234        let msg = format!("{}", err);
235        assert!(msg.contains("10 0 R"));
236    }
237
238    #[test]
239    fn test_invalid_object_type_error() {
240        let err = Error::InvalidObjectType {
241            expected: "Dictionary".to_string(),
242            found: "Array".to_string(),
243        };
244        let msg = format!("{}", err);
245        assert!(msg.contains("Dictionary"));
246        assert!(msg.contains("Array"));
247    }
248
249    #[test]
250    fn test_error_is_send_and_sync() {
251        fn assert_send_sync<T: Send + Sync>() {}
252        assert_send_sync::<Error>();
253    }
254
255    #[test]
256    fn test_encrypted_pdf_error() {
257        let err = Error::EncryptedPdf;
258        let msg = format!("{}", err);
259        assert!(msg.contains("encrypted"));
260        assert!(msg.contains("password"));
261    }
262}