Skip to main content

libmagic_rs/
error.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Error types for the libmagic-rs library.
5//!
6//! This module defines the error types used throughout the library for
7//! consistent error handling and reporting.
8
9/// Main error type for the libmagic-rs library.
10///
11/// This enum represents all possible errors that can occur during
12/// magic file parsing, rule evaluation, and file I/O operations.
13#[derive(Debug, thiserror::Error)]
14pub enum LibmagicError {
15    /// Error that occurred during magic file parsing.
16    #[error("Parse error: {0}")]
17    ParseError(#[from] ParseError),
18
19    /// Error that occurred during rule evaluation.
20    #[error("Evaluation error: {0}")]
21    EvaluationError(#[from] EvaluationError),
22
23    /// I/O error that occurred during file operations.
24    #[error("I/O error: {0}")]
25    IoError(#[from] std::io::Error),
26
27    /// Evaluation timeout exceeded.
28    #[error("Evaluation timeout exceeded after {timeout_ms}ms")]
29    Timeout {
30        /// The timeout duration in milliseconds
31        timeout_ms: u64,
32    },
33
34    /// Invalid configuration parameter.
35    ///
36    /// # Examples
37    ///
38    /// ```
39    /// use libmagic_rs::LibmagicError;
40    ///
41    /// let error = LibmagicError::ConfigError {
42    ///     reason: "invalid timeout value".to_string(),
43    /// };
44    /// assert!(matches!(error, LibmagicError::ConfigError { .. }));
45    /// ```
46    #[error("Configuration error: {reason}")]
47    ConfigError {
48        /// Description of the configuration issue
49        reason: String,
50    },
51
52    /// File I/O error with structured context (path, operation).
53    ///
54    /// Unlike `IoError` which wraps a generic `std::io::Error`, this variant
55    /// preserves the structured error information from file I/O operations
56    /// (e.g., file path, operation type).
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use libmagic_rs::LibmagicError;
62    ///
63    /// let error = LibmagicError::FileError("failed to read /path/to/file".to_string());
64    /// assert!(matches!(error, LibmagicError::FileError(_)));
65    /// ```
66    #[error("File error: {0}")]
67    FileError(String),
68}
69
70/// Errors that can occur during magic file parsing.
71#[derive(Debug, thiserror::Error)]
72pub enum ParseError {
73    /// Invalid syntax in magic file.
74    #[error("Invalid syntax at line {line}: {message}")]
75    InvalidSyntax {
76        /// The line number where the error occurred
77        line: usize,
78        /// The error message describing the syntax issue
79        message: String,
80    },
81
82    /// Unsupported magic file feature.
83    #[error("Unsupported feature at line {line}: {feature}")]
84    UnsupportedFeature {
85        /// The line number where the error occurred
86        line: usize,
87        /// The name of the unsupported feature
88        feature: String,
89    },
90
91    /// Invalid offset specification.
92    #[error("Invalid offset specification at line {line}: {offset}")]
93    InvalidOffset {
94        /// The line number where the error occurred
95        line: usize,
96        /// The invalid offset specification
97        offset: String,
98    },
99
100    /// Invalid type specification.
101    #[error("Invalid type specification at line {line}: {type_spec}")]
102    InvalidType {
103        /// The line number where the error occurred
104        line: usize,
105        /// The invalid type specification
106        type_spec: String,
107    },
108
109    /// Invalid operator specification.
110    #[error("Invalid operator at line {line}: {operator}")]
111    InvalidOperator {
112        /// The line number where the error occurred
113        line: usize,
114        /// The invalid operator
115        operator: String,
116    },
117
118    /// Invalid value specification.
119    #[error("Invalid value at line {line}: {value}")]
120    InvalidValue {
121        /// The line number where the error occurred
122        line: usize,
123        /// The invalid value specification
124        value: String,
125    },
126
127    /// Unsupported magic file format.
128    #[error("Unsupported format at line {line}: {format_type}\n{message}")]
129    UnsupportedFormat {
130        /// The line number where the error occurred
131        line: usize,
132        /// The type of unsupported format
133        format_type: String,
134        /// Detailed message with guidance
135        message: String,
136    },
137
138    /// I/O error occurred during file operations.
139    #[error("I/O error: {0}")]
140    IoError(#[from] std::io::Error),
141}
142
143/// Errors that can occur during rule evaluation.
144#[derive(Debug, thiserror::Error)]
145pub enum EvaluationError {
146    /// Buffer overrun during file reading.
147    #[error("Buffer overrun at offset {offset}")]
148    BufferOverrun {
149        /// The offset where the buffer overrun occurred
150        offset: usize,
151    },
152
153    /// Invalid offset calculation.
154    #[error("Invalid offset: {offset}")]
155    InvalidOffset {
156        /// The invalid offset value
157        offset: i64,
158    },
159
160    /// Unsupported type during evaluation.
161    #[error("Unsupported type: {type_name}")]
162    UnsupportedType {
163        /// The name of the unsupported type
164        type_name: String,
165    },
166
167    /// Recursion limit exceeded during evaluation.
168    #[error("Recursion limit exceeded (depth: {depth})")]
169    RecursionLimitExceeded {
170        /// The recursion depth that was exceeded
171        depth: u32,
172    },
173
174    /// String length limit exceeded.
175    #[error("String length limit exceeded: {length} > {max_length}")]
176    StringLengthExceeded {
177        /// The actual string length
178        length: usize,
179        /// The maximum allowed length
180        max_length: usize,
181    },
182
183    /// Invalid string encoding.
184    #[error("Invalid string encoding at offset {offset}")]
185    InvalidStringEncoding {
186        /// The offset where the invalid encoding was found
187        offset: usize,
188    },
189
190    /// Evaluation timeout exceeded.
191    #[error("Evaluation timeout exceeded after {timeout_ms}ms")]
192    Timeout {
193        /// The timeout duration in milliseconds
194        timeout_ms: u64,
195    },
196
197    /// Type reading error during evaluation.
198    #[error("Type reading error: {0}")]
199    TypeReadError(#[from] crate::evaluator::types::TypeReadError),
200
201    /// Internal error indicating a bug in the evaluation logic.
202    #[error("Internal error: {message}")]
203    InternalError {
204        /// Description of the internal error
205        message: String,
206    },
207}
208
209impl ParseError {
210    /// Create a new `InvalidSyntax` error.
211    #[must_use]
212    pub fn invalid_syntax(line: usize, message: impl Into<String>) -> Self {
213        Self::InvalidSyntax {
214            line,
215            message: message.into(),
216        }
217    }
218
219    /// Create a new `UnsupportedFeature` error.
220    #[must_use]
221    pub fn unsupported_feature(line: usize, feature: impl Into<String>) -> Self {
222        Self::UnsupportedFeature {
223            line,
224            feature: feature.into(),
225        }
226    }
227
228    /// Create a new `InvalidOffset` error.
229    #[must_use]
230    pub fn invalid_offset(line: usize, offset: impl Into<String>) -> Self {
231        Self::InvalidOffset {
232            line,
233            offset: offset.into(),
234        }
235    }
236
237    /// Create a new `InvalidType` error.
238    #[must_use]
239    pub fn invalid_type(line: usize, type_spec: impl Into<String>) -> Self {
240        Self::InvalidType {
241            line,
242            type_spec: type_spec.into(),
243        }
244    }
245
246    /// Create a new `InvalidOperator` error.
247    #[must_use]
248    pub fn invalid_operator(line: usize, operator: impl Into<String>) -> Self {
249        Self::InvalidOperator {
250            line,
251            operator: operator.into(),
252        }
253    }
254
255    /// Create a new `InvalidValue` error.
256    #[must_use]
257    pub fn invalid_value(line: usize, value: impl Into<String>) -> Self {
258        Self::InvalidValue {
259            line,
260            value: value.into(),
261        }
262    }
263
264    /// Create a new `UnsupportedFormat` error.
265    #[must_use]
266    pub fn unsupported_format(
267        line: usize,
268        format_type: impl Into<String>,
269        message: impl Into<String>,
270    ) -> Self {
271        Self::UnsupportedFormat {
272            line,
273            format_type: format_type.into(),
274            message: message.into(),
275        }
276    }
277}
278
279impl EvaluationError {
280    /// Create a new `BufferOverrun` error.
281    #[must_use]
282    pub fn buffer_overrun(offset: usize) -> Self {
283        Self::BufferOverrun { offset }
284    }
285
286    /// Create a new `InvalidOffset` error.
287    #[must_use]
288    pub fn invalid_offset(offset: i64) -> Self {
289        Self::InvalidOffset { offset }
290    }
291
292    /// Create a new `UnsupportedType` error.
293    #[must_use]
294    pub fn unsupported_type(type_name: impl Into<String>) -> Self {
295        Self::UnsupportedType {
296            type_name: type_name.into(),
297        }
298    }
299
300    /// Create a new `RecursionLimitExceeded` error.
301    #[must_use]
302    pub fn recursion_limit_exceeded(depth: u32) -> Self {
303        Self::RecursionLimitExceeded { depth }
304    }
305
306    /// Create a new `StringLengthExceeded` error.
307    #[must_use]
308    pub fn string_length_exceeded(length: usize, max_length: usize) -> Self {
309        Self::StringLengthExceeded { length, max_length }
310    }
311
312    /// Create a new `InvalidStringEncoding` error.
313    #[must_use]
314    pub fn invalid_string_encoding(offset: usize) -> Self {
315        Self::InvalidStringEncoding { offset }
316    }
317
318    /// Create a new `Timeout` error.
319    #[must_use]
320    pub fn timeout(timeout_ms: u64) -> Self {
321        Self::Timeout { timeout_ms }
322    }
323
324    /// Create a new `InternalError` error.
325    #[must_use]
326    pub fn internal_error(message: impl Into<String>) -> Self {
327        Self::InternalError {
328            message: message.into(),
329        }
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use std::io;
337
338    #[test]
339    fn test_libmagic_error_from_parse_error() {
340        let parse_error = ParseError::invalid_syntax(10, "unexpected token");
341        let libmagic_error = LibmagicError::from(parse_error);
342
343        match libmagic_error {
344            LibmagicError::ParseError(_) => (),
345            _ => panic!("Expected ParseError variant"),
346        }
347    }
348
349    #[test]
350    fn test_libmagic_error_from_evaluation_error() {
351        let eval_error = EvaluationError::buffer_overrun(100);
352        let libmagic_error = LibmagicError::from(eval_error);
353
354        match libmagic_error {
355            LibmagicError::EvaluationError(_) => (),
356            _ => panic!("Expected EvaluationError variant"),
357        }
358    }
359
360    #[test]
361    fn test_libmagic_error_from_io_error() {
362        let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
363        let libmagic_error = LibmagicError::from(io_error);
364
365        match libmagic_error {
366            LibmagicError::IoError(_) => (),
367            _ => panic!("Expected IoError variant"),
368        }
369    }
370
371    #[test]
372    fn test_parse_error_display() {
373        let error = ParseError::invalid_syntax(5, "missing operator");
374        let display = format!("{error}");
375        assert_eq!(display, "Invalid syntax at line 5: missing operator");
376    }
377
378    #[test]
379    fn test_parse_error_unsupported_feature() {
380        let error = ParseError::unsupported_feature(12, "regex patterns");
381        let display = format!("{error}");
382        assert_eq!(display, "Unsupported feature at line 12: regex patterns");
383    }
384
385    #[test]
386    fn test_parse_error_invalid_offset() {
387        let error = ParseError::invalid_offset(8, "invalid_offset_spec");
388        let display = format!("{error}");
389        assert_eq!(
390            display,
391            "Invalid offset specification at line 8: invalid_offset_spec"
392        );
393    }
394
395    #[test]
396    fn test_parse_error_invalid_type() {
397        let error = ParseError::invalid_type(15, "unknown_type");
398        let display = format!("{error}");
399        assert_eq!(
400            display,
401            "Invalid type specification at line 15: unknown_type"
402        );
403    }
404
405    #[test]
406    fn test_parse_error_invalid_operator() {
407        let error = ParseError::invalid_operator(20, "??");
408        let display = format!("{error}");
409        assert_eq!(display, "Invalid operator at line 20: ??");
410    }
411
412    #[test]
413    fn test_parse_error_invalid_value() {
414        let error = ParseError::invalid_value(25, "malformed_hex");
415        let display = format!("{error}");
416        assert_eq!(display, "Invalid value at line 25: malformed_hex");
417    }
418
419    #[test]
420    fn test_evaluation_error_buffer_overrun() {
421        let error = EvaluationError::buffer_overrun(1024);
422        let display = format!("{error}");
423        assert_eq!(display, "Buffer overrun at offset 1024");
424    }
425
426    #[test]
427    fn test_evaluation_error_invalid_offset() {
428        let error = EvaluationError::invalid_offset(-50);
429        let display = format!("{error}");
430        assert_eq!(display, "Invalid offset: -50");
431    }
432
433    #[test]
434    fn test_evaluation_error_unsupported_type() {
435        let error = EvaluationError::unsupported_type("complex_type");
436        let display = format!("{error}");
437        assert_eq!(display, "Unsupported type: complex_type");
438    }
439
440    #[test]
441    fn test_evaluation_error_recursion_limit() {
442        let error = EvaluationError::recursion_limit_exceeded(100);
443        let display = format!("{error}");
444        assert_eq!(display, "Recursion limit exceeded (depth: 100)");
445    }
446
447    #[test]
448    fn test_evaluation_error_string_length_exceeded() {
449        let error = EvaluationError::string_length_exceeded(2048, 1024);
450        let display = format!("{error}");
451        assert_eq!(display, "String length limit exceeded: 2048 > 1024");
452    }
453
454    #[test]
455    fn test_evaluation_error_invalid_string_encoding() {
456        let error = EvaluationError::invalid_string_encoding(512);
457        let display = format!("{error}");
458        assert_eq!(display, "Invalid string encoding at offset 512");
459    }
460
461    #[test]
462    fn test_evaluation_error_internal_error() {
463        let error = EvaluationError::internal_error("recursion depth underflow");
464        let display = format!("{error}");
465        assert_eq!(display, "Internal error: recursion depth underflow");
466    }
467
468    #[test]
469    fn test_libmagic_error_display_parse() {
470        let parse_error = ParseError::invalid_syntax(10, "unexpected token");
471        let libmagic_error = LibmagicError::from(parse_error);
472        let display = format!("{libmagic_error}");
473        assert_eq!(
474            display,
475            "Parse error: Invalid syntax at line 10: unexpected token"
476        );
477    }
478
479    #[test]
480    fn test_libmagic_error_display_evaluation() {
481        let eval_error = EvaluationError::buffer_overrun(100);
482        let libmagic_error = LibmagicError::from(eval_error);
483        let display = format!("{libmagic_error}");
484        assert_eq!(display, "Evaluation error: Buffer overrun at offset 100");
485    }
486
487    #[test]
488    fn test_libmagic_error_display_io() {
489        let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
490        let libmagic_error = LibmagicError::from(io_error);
491        let display = format!("{libmagic_error}");
492        assert!(display.starts_with("I/O error:"));
493        assert!(display.contains("access denied"));
494    }
495
496    #[test]
497    fn test_error_debug_formatting() {
498        let error = LibmagicError::ParseError(ParseError::invalid_syntax(5, "test"));
499        let debug = format!("{error:?}");
500        assert!(debug.contains("ParseError"));
501        assert!(debug.contains("InvalidSyntax"));
502    }
503
504    #[test]
505    fn test_parse_error_constructors() {
506        let error1 = ParseError::invalid_syntax(1, "test");
507        let error2 = ParseError::unsupported_feature(2, "feature");
508        let error3 = ParseError::invalid_offset(3, "offset");
509        let error4 = ParseError::invalid_type(4, "type");
510        let error5 = ParseError::invalid_operator(5, "op");
511        let error6 = ParseError::invalid_value(6, "value");
512
513        // Test that all constructors work
514        assert!(matches!(error1, ParseError::InvalidSyntax { .. }));
515        assert!(matches!(error2, ParseError::UnsupportedFeature { .. }));
516        assert!(matches!(error3, ParseError::InvalidOffset { .. }));
517        assert!(matches!(error4, ParseError::InvalidType { .. }));
518        assert!(matches!(error5, ParseError::InvalidOperator { .. }));
519        assert!(matches!(error6, ParseError::InvalidValue { .. }));
520    }
521
522    #[test]
523    fn test_evaluation_error_constructors() {
524        let error1 = EvaluationError::buffer_overrun(100);
525        let error2 = EvaluationError::invalid_offset(-1);
526        let error3 = EvaluationError::unsupported_type("test");
527        let error4 = EvaluationError::recursion_limit_exceeded(50);
528        let error5 = EvaluationError::string_length_exceeded(100, 50);
529        let error6 = EvaluationError::invalid_string_encoding(200);
530
531        // Test that all constructors work
532        assert!(matches!(error1, EvaluationError::BufferOverrun { .. }));
533        assert!(matches!(error2, EvaluationError::InvalidOffset { .. }));
534        assert!(matches!(error3, EvaluationError::UnsupportedType { .. }));
535        assert!(matches!(
536            error4,
537            EvaluationError::RecursionLimitExceeded { .. }
538        ));
539        assert!(matches!(
540            error5,
541            EvaluationError::StringLengthExceeded { .. }
542        ));
543        assert!(matches!(
544            error6,
545            EvaluationError::InvalidStringEncoding { .. }
546        ));
547    }
548
549    #[test]
550    fn test_parse_error_unsupported_format() {
551        let error = ParseError::unsupported_format(
552            0,
553            "binary .mgc",
554            "Binary files not supported. Use --use-builtin option.",
555        );
556        let display = format!("{error}");
557        assert!(display.contains("Unsupported format"));
558        assert!(display.contains("binary .mgc"));
559        assert!(display.contains("--use-builtin"));
560    }
561
562    #[test]
563    fn test_parse_error_unsupported_format_constructor() {
564        let error = ParseError::unsupported_format(5, "test_format", "test message");
565
566        match error {
567            ParseError::UnsupportedFormat {
568                line,
569                format_type,
570                message,
571            } => {
572                assert_eq!(line, 5);
573                assert_eq!(format_type, "test_format");
574                assert_eq!(message, "test message");
575            }
576            _ => panic!("Expected UnsupportedFormat variant"),
577        }
578    }
579}