oxidize_pdf/
error.rs

1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum PdfError {
5    #[error("IO error: {0}")]
6    Io(#[from] std::io::Error),
7
8    #[error("Invalid PDF structure: {0}")]
9    InvalidStructure(String),
10
11    #[error("Invalid object reference: {0}")]
12    InvalidReference(String),
13
14    #[error("Encoding error: {0}")]
15    EncodingError(String),
16
17    #[error("Font error: {0}")]
18    FontError(String),
19
20    #[error("Compression error: {0}")]
21    CompressionError(String),
22
23    #[error("Invalid image: {0}")]
24    InvalidImage(String),
25
26    #[error("Invalid object reference: {0} {1} R")]
27    InvalidObjectReference(u32, u16),
28
29    #[error("Parse error: {0}")]
30    ParseError(String),
31
32    #[error("Invalid page number: {0}")]
33    InvalidPageNumber(u32),
34
35    #[error("Invalid format: {0}")]
36    InvalidFormat(String),
37
38    #[error("Invalid header")]
39    InvalidHeader,
40
41    #[error("Content stream too large: {0} bytes")]
42    ContentStreamTooLarge(usize),
43
44    #[error("Operation cancelled")]
45    OperationCancelled,
46
47    #[error("Encryption error: {0}")]
48    EncryptionError(String),
49
50    #[error("Permission denied: {0}")]
51    PermissionDenied(String),
52
53    #[error("Invalid operation: {0}")]
54    InvalidOperation(String),
55
56    #[error("Duplicate field: {0}")]
57    DuplicateField(String),
58
59    #[error("Field not found: {0}")]
60    FieldNotFound(String),
61
62    #[error("External validation error: {0}")]
63    ExternalValidationError(String),
64}
65
66pub type Result<T> = std::result::Result<T, PdfError>;
67
68// Convert AesError to PdfError
69impl From<crate::encryption::AesError> for PdfError {
70    fn from(err: crate::encryption::AesError) -> Self {
71        PdfError::EncryptionError(err.to_string())
72    }
73}
74
75impl From<crate::parser::ParseError> for PdfError {
76    fn from(err: crate::parser::ParseError) -> Self {
77        PdfError::ParseError(err.to_string())
78    }
79}
80
81// Separate error type for oxidize-pdf-core
82#[derive(Error, Debug)]
83pub enum OxidizePdfError {
84    #[error("IO error: {0}")]
85    Io(#[from] std::io::Error),
86
87    #[error("Parse error: {0}")]
88    ParseError(String),
89
90    #[error("Invalid PDF structure: {0}")]
91    InvalidStructure(String),
92
93    #[error("Encoding error: {0}")]
94    EncodingError(String),
95
96    #[error("Other error: {0}")]
97    Other(String),
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use std::io::{Error as IoError, ErrorKind};
104
105    #[test]
106    fn test_pdf_error_display() {
107        let error = PdfError::InvalidStructure("test message".to_string());
108        assert_eq!(error.to_string(), "Invalid PDF structure: test message");
109    }
110
111    #[test]
112    fn test_pdf_error_debug() {
113        let error = PdfError::InvalidReference("object 1 0".to_string());
114        let debug_str = format!("{error:?}");
115        assert!(debug_str.contains("InvalidReference"));
116        assert!(debug_str.contains("object 1 0"));
117    }
118
119    #[test]
120    fn test_pdf_error_from_io_error() {
121        let io_error = IoError::new(ErrorKind::NotFound, "file not found");
122        let pdf_error = PdfError::from(io_error);
123
124        match pdf_error {
125            PdfError::Io(ref err) => {
126                assert_eq!(err.kind(), ErrorKind::NotFound);
127            }
128            _ => panic!("Expected IO error variant"),
129        }
130    }
131
132    #[test]
133    fn test_all_pdf_error_variants() {
134        let errors = vec![
135            PdfError::InvalidStructure("structure error".to_string()),
136            PdfError::InvalidObjectReference(1, 0),
137            PdfError::EncodingError("encoding error".to_string()),
138            PdfError::FontError("font error".to_string()),
139            PdfError::CompressionError("compression error".to_string()),
140            PdfError::InvalidImage("image error".to_string()),
141            PdfError::ParseError("parse error".to_string()),
142            PdfError::InvalidPageNumber(999),
143            PdfError::InvalidFormat("format error".to_string()),
144            PdfError::InvalidHeader,
145            PdfError::ContentStreamTooLarge(1024 * 1024),
146        ];
147
148        // Test that all variants can be created and displayed
149        for error in errors {
150            let error_string = error.to_string();
151            assert!(!error_string.is_empty());
152        }
153    }
154
155    #[test]
156    fn test_oxidize_pdf_error_display() {
157        let error = OxidizePdfError::ParseError("parsing failed".to_string());
158        assert_eq!(error.to_string(), "Parse error: parsing failed");
159    }
160
161    #[test]
162    fn test_oxidize_pdf_error_debug() {
163        let error = OxidizePdfError::InvalidStructure("malformed PDF".to_string());
164        let debug_str = format!("{error:?}");
165        assert!(debug_str.contains("InvalidStructure"));
166        assert!(debug_str.contains("malformed PDF"));
167    }
168
169    #[test]
170    fn test_oxidize_pdf_error_from_io_error() {
171        let io_error = IoError::new(ErrorKind::PermissionDenied, "access denied");
172        let pdf_error = OxidizePdfError::from(io_error);
173
174        match pdf_error {
175            OxidizePdfError::Io(ref err) => {
176                assert_eq!(err.kind(), ErrorKind::PermissionDenied);
177            }
178            _ => panic!("Expected IO error variant"),
179        }
180    }
181
182    #[test]
183    fn test_all_oxidize_pdf_error_variants() {
184        let errors = vec![
185            OxidizePdfError::ParseError("parse error".to_string()),
186            OxidizePdfError::InvalidStructure("structure error".to_string()),
187            OxidizePdfError::EncodingError("encoding error".to_string()),
188            OxidizePdfError::Other("other error".to_string()),
189        ];
190
191        // Test that all variants can be created and displayed
192        for error in errors {
193            let error_string = error.to_string();
194            assert!(!error_string.is_empty());
195            assert!(error_string.contains("error"));
196        }
197    }
198
199    #[test]
200    fn test_result_type_ok() {
201        let result: Result<i32> = Ok(42);
202        assert!(result.is_ok());
203        assert_eq!(result.unwrap(), 42);
204    }
205
206    #[test]
207    fn test_result_type_err() {
208        let result: Result<i32> = Err(PdfError::InvalidStructure("test".to_string()));
209        assert!(result.is_err());
210
211        let error = result.unwrap_err();
212        match error {
213            PdfError::InvalidStructure(msg) => assert_eq!(msg, "test"),
214            _ => panic!("Expected InvalidStructure variant"),
215        }
216    }
217
218    #[test]
219    fn test_error_chain_display() {
220        // Test that error messages are properly formatted
221        let errors = [
222            (
223                "Invalid PDF structure: corrupted header",
224                PdfError::InvalidStructure("corrupted header".to_string()),
225            ),
226            (
227                "Invalid object reference: 999 0 R",
228                PdfError::InvalidObjectReference(999, 0),
229            ),
230            (
231                "Encoding error: unsupported encoding",
232                PdfError::EncodingError("unsupported encoding".to_string()),
233            ),
234            (
235                "Font error: missing font",
236                PdfError::FontError("missing font".to_string()),
237            ),
238            (
239                "Compression error: deflate failed",
240                PdfError::CompressionError("deflate failed".to_string()),
241            ),
242            (
243                "Invalid image: corrupt JPEG",
244                PdfError::InvalidImage("corrupt JPEG".to_string()),
245            ),
246        ];
247
248        for (expected, error) in errors {
249            assert_eq!(error.to_string(), expected);
250        }
251    }
252
253    #[test]
254    fn test_oxidize_pdf_error_chain_display() {
255        // Test that OxidizePdfError messages are properly formatted
256        let errors = [
257            (
258                "Parse error: unexpected token",
259                OxidizePdfError::ParseError("unexpected token".to_string()),
260            ),
261            (
262                "Invalid PDF structure: missing xref",
263                OxidizePdfError::InvalidStructure("missing xref".to_string()),
264            ),
265            (
266                "Encoding error: invalid UTF-8",
267                OxidizePdfError::EncodingError("invalid UTF-8".to_string()),
268            ),
269            (
270                "Other error: unknown issue",
271                OxidizePdfError::Other("unknown issue".to_string()),
272            ),
273        ];
274
275        for (expected, error) in errors {
276            assert_eq!(error.to_string(), expected);
277        }
278    }
279
280    #[test]
281    fn test_error_send_sync() {
282        // Ensure error types implement Send + Sync for thread safety
283        fn assert_send_sync<T: Send + Sync>() {}
284        assert_send_sync::<PdfError>();
285        assert_send_sync::<OxidizePdfError>();
286    }
287
288    #[test]
289    fn test_error_struct_creation() {
290        // Test creating errors with string messages
291        let errors = vec![
292            PdfError::InvalidStructure("test".to_string()),
293            PdfError::InvalidObjectReference(1, 0),
294            PdfError::EncodingError("encoding".to_string()),
295            PdfError::FontError("font".to_string()),
296            PdfError::CompressionError("compression".to_string()),
297            PdfError::InvalidImage("image".to_string()),
298            PdfError::ParseError("parse".to_string()),
299            PdfError::InvalidPageNumber(1),
300            PdfError::InvalidFormat("format".to_string()),
301            PdfError::InvalidHeader,
302            PdfError::ContentStreamTooLarge(1024),
303            PdfError::OperationCancelled,
304        ];
305
306        // Verify each error can be created and has the expected message structure
307        for error in errors {
308            let msg = error.to_string();
309            assert!(!msg.is_empty(), "Error message should not be empty");
310
311            // Check that the message makes sense for the error type
312            match &error {
313                PdfError::OperationCancelled => assert!(msg.contains("cancelled")),
314                PdfError::ContentStreamTooLarge(_) => assert!(msg.contains("too large")),
315                _ => assert!(msg.contains("error") || msg.contains("Invalid")),
316            }
317        }
318    }
319
320    #[test]
321    fn test_oxidize_pdf_error_struct_creation() {
322        // Test creating OxidizePdfError with string messages
323        let errors = vec![
324            OxidizePdfError::ParseError("test".to_string()),
325            OxidizePdfError::InvalidStructure("structure".to_string()),
326            OxidizePdfError::EncodingError("encoding".to_string()),
327            OxidizePdfError::Other("other".to_string()),
328        ];
329
330        // Verify each error can be created and has the expected message structure
331        for error in errors {
332            let msg = error.to_string();
333            assert!(msg.contains("error") || msg.contains("Invalid"));
334        }
335    }
336
337    #[test]
338    fn test_error_equality() {
339        let error1 = PdfError::InvalidStructure("test".to_string());
340        let error2 = PdfError::InvalidStructure("test".to_string());
341        let error3 = PdfError::InvalidStructure("different".to_string());
342
343        // Note: thiserror doesn't automatically derive PartialEq, so we test the display output
344        assert_eq!(error1.to_string(), error2.to_string());
345        assert_ne!(error1.to_string(), error3.to_string());
346    }
347
348    #[test]
349    fn test_io_error_preservation() {
350        // Test that IO error details are preserved through conversion
351        let original_io_error = IoError::new(ErrorKind::UnexpectedEof, "sudden EOF");
352        let pdf_error = PdfError::from(original_io_error);
353
354        if let PdfError::Io(io_err) = pdf_error {
355            assert_eq!(io_err.kind(), ErrorKind::UnexpectedEof);
356            assert_eq!(io_err.to_string(), "sudden EOF");
357        } else {
358            panic!("IO error should be preserved as PdfError::Io");
359        }
360    }
361
362    #[test]
363    fn test_oxidize_pdf_error_io_error_preservation() {
364        // Test that IO error details are preserved through conversion
365        let original_io_error = IoError::new(ErrorKind::InvalidData, "corrupted data");
366        let oxidize_error = OxidizePdfError::from(original_io_error);
367
368        if let OxidizePdfError::Io(io_err) = oxidize_error {
369            assert_eq!(io_err.kind(), ErrorKind::InvalidData);
370            assert_eq!(io_err.to_string(), "corrupted data");
371        } else {
372            panic!("IO error should be preserved as OxidizePdfError::Io");
373        }
374    }
375
376    #[test]
377    fn test_operation_cancelled_error() {
378        // Test the OperationCancelled variant (line 44-45)
379        let error = PdfError::OperationCancelled;
380        assert_eq!(error.to_string(), "Operation cancelled");
381
382        // Test in a Result context
383        let result: Result<()> = Err(PdfError::OperationCancelled);
384        assert!(result.is_err());
385        if let Err(PdfError::OperationCancelled) = result {
386            // Variant matched correctly
387        } else {
388            panic!("Expected OperationCancelled variant");
389        }
390    }
391
392    #[test]
393    fn test_encryption_error() {
394        // Test the EncryptionError variant (line 47-48)
395        let error = PdfError::EncryptionError("AES decryption failed".to_string());
396        assert_eq!(error.to_string(), "Encryption error: AES decryption failed");
397
398        // Test debug format
399        let debug_str = format!("{:?}", error);
400        assert!(debug_str.contains("EncryptionError"));
401        assert!(debug_str.contains("AES decryption failed"));
402    }
403
404    #[test]
405    fn test_permission_denied_error() {
406        // Test the PermissionDenied variant (line 50-51)
407        let error = PdfError::PermissionDenied("Cannot modify protected document".to_string());
408        assert_eq!(
409            error.to_string(),
410            "Permission denied: Cannot modify protected document"
411        );
412
413        // Test that it's different from InvalidOperation
414        let other_error = PdfError::InvalidOperation("Cannot modify".to_string());
415        assert_ne!(error.to_string(), other_error.to_string());
416    }
417
418    #[test]
419    fn test_invalid_operation_error() {
420        // Test the InvalidOperation variant (line 53-54)
421        let error =
422            PdfError::InvalidOperation("Cannot perform operation on encrypted PDF".to_string());
423        assert_eq!(
424            error.to_string(),
425            "Invalid operation: Cannot perform operation on encrypted PDF"
426        );
427
428        // Test in match expression
429        match error {
430            PdfError::InvalidOperation(msg) => {
431                assert!(msg.contains("encrypted"));
432            }
433            _ => panic!("Expected InvalidOperation variant"),
434        }
435    }
436
437    #[test]
438    fn test_duplicate_field_error() {
439        // Test the DuplicateField variant (line 56-57)
440        let field_name = "email_address";
441        let error = PdfError::DuplicateField(field_name.to_string());
442        assert_eq!(error.to_string(), "Duplicate field: email_address");
443
444        // Test that it handles empty field names
445        let empty_error = PdfError::DuplicateField(String::new());
446        assert_eq!(empty_error.to_string(), "Duplicate field: ");
447    }
448
449    #[test]
450    fn test_field_not_found_error() {
451        // Test the FieldNotFound variant (line 59-60)
452        let field_name = "signature_field";
453        let error = PdfError::FieldNotFound(field_name.to_string());
454        assert_eq!(error.to_string(), "Field not found: signature_field");
455
456        // Test with special characters
457        let special_field = "field[0].subfield";
458        let special_error = PdfError::FieldNotFound(special_field.to_string());
459        assert_eq!(
460            special_error.to_string(),
461            "Field not found: field[0].subfield"
462        );
463    }
464
465    #[test]
466    fn test_aes_error_conversion() {
467        // Test the From<AesError> conversion (line 66-70)
468        // We need to simulate an AesError
469        use crate::encryption::AesError;
470
471        let aes_error = AesError::InvalidKeyLength {
472            expected: 32,
473            actual: 16,
474        };
475        let pdf_error: PdfError = aes_error.into();
476
477        match pdf_error {
478            PdfError::EncryptionError(msg) => {
479                assert!(msg.contains("Invalid key length") || msg.contains("InvalidKeyLength"));
480            }
481            _ => panic!("Expected EncryptionError from AesError conversion"),
482        }
483    }
484
485    #[test]
486    fn test_parse_error_conversion() {
487        // Test the From<ParseError> conversion (line 72-76)
488        use crate::parser::ParseError;
489
490        let parse_error = ParseError::InvalidXRef;
491        let pdf_error: PdfError = parse_error.into();
492
493        match pdf_error {
494            PdfError::ParseError(msg) => {
495                assert!(msg.contains("XRef") || msg.contains("Invalid"));
496            }
497            _ => panic!("Expected ParseError from ParseError conversion"),
498        }
499    }
500}