Skip to main content

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