1#![forbid(unsafe_code)]
6
7pub type Result<T> = std::result::Result<T, Error>;
9
10#[derive(Debug, thiserror::Error)]
12#[allow(clippy::enum_variant_names)]
13pub enum Error {
14 #[error("Invalid PDF header: expected '%PDF-', found '{0}'")]
16 InvalidHeader(String),
17
18 #[error("Unsupported PDF version: {0}")]
20 UnsupportedVersion(String),
21
22 #[error("Failed to parse object at byte {offset}: {reason}")]
24 ParseError {
25 offset: usize,
27 reason: String,
29 },
30
31 #[error("Parse warning at byte {offset}: {message}")]
33 ParseWarning {
34 offset: usize,
36 message: String,
38 },
39
40 #[error("Invalid cross-reference table")]
42 InvalidXref,
43
44 #[error("Object not found: {0} {1} R")]
46 ObjectNotFound(u32, u16),
47
48 #[error("Invalid object type: expected {expected}, found {found}")]
50 InvalidObjectType {
51 expected: String,
53 found: String,
55 },
56
57 #[error("End of file reached unexpectedly")]
59 UnexpectedEof,
60
61 #[error("IO error: {0}")]
63 Io(#[from] std::io::Error),
64
65 #[error("UTF-8 decoding error: {0}")]
67 Utf8Error(#[from] std::str::Utf8Error),
68
69 #[error("Unsupported feature: {0}")]
71 Unsupported(String),
72
73 #[error("Invalid PDF: {0}")]
75 InvalidPdf(String),
76
77 #[error("Stream decoding error: {0}")]
79 Decode(String),
80
81 #[error("Encoding error: {0}")]
83 Encode(String),
84
85 #[error("Unsupported filter: {0}")]
87 UnsupportedFilter(String),
88
89 #[error("Font error: {0}")]
91 Font(String),
92
93 #[error("Image error: {0}")]
95 Image(String),
96
97 #[error("Circular reference detected: object {0}")]
99 CircularReference(crate::object::ObjectRef),
100
101 #[error("Recursion depth limit exceeded (max: {0})")]
103 RecursionLimitExceeded(u32),
104
105 #[error("Invalid operation: {0}")]
107 InvalidOperation(String),
108
109 #[error("Layout analysis error: {0}")]
111 LayoutAnalysis(String),
112
113 #[error("PDF is encrypted and requires a password. Call authenticate(password) before extracting content.")]
119 EncryptedPdf,
120
121 #[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}