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)]
14#[non_exhaustive]
15pub enum LibmagicError {
16    /// Error that occurred during magic file parsing.
17    #[error("Parse error: {0}")]
18    ParseError(#[from] ParseError),
19
20    /// Error that occurred during rule evaluation.
21    #[error("Evaluation error: {0}")]
22    EvaluationError(#[from] EvaluationError),
23
24    /// I/O error that occurred during file operations.
25    #[error("I/O error: {0}")]
26    IoError(#[from] std::io::Error),
27
28    /// Evaluation timeout exceeded.
29    #[error("Evaluation timeout exceeded after {timeout_ms}ms")]
30    Timeout {
31        /// The timeout duration in milliseconds
32        timeout_ms: u64,
33    },
34
35    /// Invalid configuration parameter.
36    ///
37    /// # Examples
38    ///
39    /// ```
40    /// use libmagic_rs::LibmagicError;
41    ///
42    /// let error = LibmagicError::ConfigError {
43    ///     reason: "invalid timeout value".to_string(),
44    /// };
45    /// assert!(matches!(error, LibmagicError::ConfigError { .. }));
46    /// ```
47    #[error("Configuration error: {reason}")]
48    ConfigError {
49        /// Description of the configuration issue
50        reason: String,
51    },
52
53    /// File I/O error with structured context (path, operation).
54    ///
55    /// Unlike `IoError` which wraps a generic `std::io::Error`, this variant
56    /// preserves the structured error information from file I/O operations
57    /// (e.g., file path, operation type).
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// use libmagic_rs::LibmagicError;
63    ///
64    /// let error = LibmagicError::FileError("failed to read /path/to/file".to_string());
65    /// assert!(matches!(error, LibmagicError::FileError(_)));
66    /// ```
67    #[error("File error: {0}")]
68    FileError(String),
69}
70
71/// Errors that can occur during magic file parsing.
72#[derive(Debug, thiserror::Error)]
73#[non_exhaustive]
74pub enum ParseError {
75    /// Invalid syntax in magic file.
76    #[error("Invalid syntax at line {line}: {message}")]
77    InvalidSyntax {
78        /// The line number where the error occurred
79        line: usize,
80        /// The error message describing the syntax issue
81        message: String,
82    },
83
84    /// Unsupported magic file feature.
85    #[error("Unsupported feature at line {line}: {feature}")]
86    UnsupportedFeature {
87        /// The line number where the error occurred
88        line: usize,
89        /// The name of the unsupported feature
90        feature: String,
91    },
92
93    /// Invalid offset specification.
94    #[error("Invalid offset specification at line {line}: {offset}")]
95    InvalidOffset {
96        /// The line number where the error occurred
97        line: usize,
98        /// The invalid offset specification
99        offset: String,
100    },
101
102    /// Invalid type specification.
103    #[error("Invalid type specification at line {line}: {type_spec}")]
104    InvalidType {
105        /// The line number where the error occurred
106        line: usize,
107        /// The invalid type specification
108        type_spec: String,
109    },
110
111    /// Invalid operator specification.
112    #[error("Invalid operator at line {line}: {operator}")]
113    InvalidOperator {
114        /// The line number where the error occurred
115        line: usize,
116        /// The invalid operator
117        operator: String,
118    },
119
120    /// Invalid value specification.
121    #[error("Invalid value at line {line}: {value}")]
122    InvalidValue {
123        /// The line number where the error occurred
124        line: usize,
125        /// The invalid value specification
126        value: String,
127    },
128
129    /// Unsupported magic file format.
130    #[error("Unsupported format at line {line}: {format_type}\n{message}")]
131    UnsupportedFormat {
132        /// The line number where the error occurred
133        line: usize,
134        /// The type of unsupported format
135        format_type: String,
136        /// Detailed message with guidance
137        message: String,
138    },
139
140    /// I/O error occurred during file operations.
141    #[error("I/O error: {0}")]
142    IoError(#[from] std::io::Error),
143}
144
145/// Errors that can occur during rule evaluation.
146#[derive(Debug, thiserror::Error)]
147#[non_exhaustive]
148pub enum EvaluationError {
149    /// Buffer overrun during file reading.
150    #[error("Buffer overrun at offset {offset}")]
151    BufferOverrun {
152        /// The offset where the buffer overrun occurred
153        offset: usize,
154    },
155
156    /// Invalid offset calculation.
157    #[error("Invalid offset: {offset}")]
158    InvalidOffset {
159        /// The invalid offset value
160        offset: i64,
161    },
162
163    /// Unsupported type during evaluation.
164    #[error("Unsupported type: {type_name}")]
165    UnsupportedType {
166        /// The name of the unsupported type
167        type_name: String,
168    },
169
170    /// Recursion limit exceeded during evaluation.
171    #[error("Recursion limit exceeded (depth: {depth})")]
172    RecursionLimitExceeded {
173        /// The recursion depth that was exceeded
174        depth: u32,
175    },
176
177    /// String length limit exceeded.
178    #[error("String length limit exceeded: {length} > {max_length}")]
179    StringLengthExceeded {
180        /// The actual string length
181        length: usize,
182        /// The maximum allowed length
183        max_length: usize,
184    },
185
186    /// Invalid string encoding.
187    #[error("Invalid string encoding at offset {offset}")]
188    InvalidStringEncoding {
189        /// The offset where the invalid encoding was found
190        offset: usize,
191    },
192
193    /// Evaluation timeout exceeded.
194    #[error("Evaluation timeout exceeded after {timeout_ms}ms")]
195    Timeout {
196        /// The timeout duration in milliseconds
197        timeout_ms: u64,
198    },
199
200    /// Type reading error during evaluation.
201    #[error("Type reading error: {0}")]
202    TypeReadError(#[from] crate::evaluator::types::TypeReadError),
203
204    /// A pre-comparison `ValueTransform` (`type+N`, `type/N`, etc.) failed
205    /// to apply: division/modulo by zero, integer overflow, or any other
206    /// arithmetic failure produced by `apply_value_transform`. This is a
207    /// recoverable per-rule failure (the engine's graceful-skip path
208    /// drops the rule and continues), not a programming bug -- a
209    /// magic-file author who writes `lequad*1000000` against a buffer
210    /// where the read value overflows triggers this and the rest of the
211    /// rule set still runs.
212    ///
213    /// # Examples
214    ///
215    /// ```
216    /// use libmagic_rs::error::EvaluationError;
217    ///
218    /// let error = EvaluationError::InvalidValueTransform {
219    ///     reason: "Mul(2) overflow on 18446744073709551615".to_string(),
220    /// };
221    /// assert!(matches!(error, EvaluationError::InvalidValueTransform { .. }));
222    /// ```
223    #[error("invalid value transform: {reason}")]
224    InvalidValueTransform {
225        /// Free-form description of the operation, operand, and value
226        /// that triggered the failure.
227        reason: String,
228    },
229
230    /// A `use` directive referenced a name not present in the name table.
231    ///
232    /// The evaluator currently handles this condition with a `warn!` log
233    /// plus `Ok(vec![])` for backward compatibility with magic files that
234    /// invoke subroutines defined in another magic file. This variant is
235    /// reserved for consumers that opt in to strict-mode evaluation where
236    /// an unknown-name reference should abort the run.
237    ///
238    /// # Examples
239    ///
240    /// ```
241    /// use libmagic_rs::error::EvaluationError;
242    ///
243    /// let error = EvaluationError::UnknownName {
244    ///     name: "missing_sub".to_string(),
245    /// };
246    /// assert!(matches!(error, EvaluationError::UnknownName { .. }));
247    /// ```
248    #[error("use directive references unknown name: {name}")]
249    UnknownName {
250        /// The name that could not be resolved in the name table.
251        name: String,
252    },
253
254    /// Internal error indicating a bug in the evaluation logic.
255    #[error("Internal error: {message}")]
256    InternalError {
257        /// Description of the internal error
258        message: String,
259    },
260
261    /// An `indirect` directive was evaluated without a rule environment
262    /// attached to the [`crate::evaluator::EvaluationContext`].
263    ///
264    /// `MetaType::Indirect` re-evaluates the entire root rule list at the
265    /// resolved offset. Without a [`crate::evaluator::RuleEnvironment`] (the
266    /// shared root rules + name table) the engine has nothing to re-enter,
267    /// so the directive is a no-op. This variant is reserved for consumers
268    /// that opt in to strict-mode evaluation where the misconfiguration
269    /// should abort the run; the default engine path logs at `debug!` and
270    /// returns `Ok(vec![])` for backward compatibility with low-level
271    /// programmatic callers (tests, fuzz harnesses) that intentionally run
272    /// without a `MagicDatabase`-attached environment, mirroring the
273    /// `Use`-without-env contract.
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// use libmagic_rs::error::EvaluationError;
279    ///
280    /// let error = EvaluationError::indirect_without_environment();
281    /// assert!(matches!(error, EvaluationError::IndirectWithoutEnvironment));
282    /// ```
283    #[error("indirect directive evaluated without a rule environment")]
284    IndirectWithoutEnvironment,
285}
286
287impl ParseError {
288    /// Create a new `InvalidSyntax` error.
289    #[must_use]
290    pub fn invalid_syntax(line: usize, message: impl Into<String>) -> Self {
291        Self::InvalidSyntax {
292            line,
293            message: message.into(),
294        }
295    }
296
297    /// Create a new `UnsupportedFeature` error.
298    #[must_use]
299    pub fn unsupported_feature(line: usize, feature: impl Into<String>) -> Self {
300        Self::UnsupportedFeature {
301            line,
302            feature: feature.into(),
303        }
304    }
305
306    /// Create a new `InvalidOffset` error.
307    #[must_use]
308    pub fn invalid_offset(line: usize, offset: impl Into<String>) -> Self {
309        Self::InvalidOffset {
310            line,
311            offset: offset.into(),
312        }
313    }
314
315    /// Create a new `InvalidType` error.
316    #[must_use]
317    pub fn invalid_type(line: usize, type_spec: impl Into<String>) -> Self {
318        Self::InvalidType {
319            line,
320            type_spec: type_spec.into(),
321        }
322    }
323
324    /// Create a new `InvalidOperator` error.
325    #[must_use]
326    pub fn invalid_operator(line: usize, operator: impl Into<String>) -> Self {
327        Self::InvalidOperator {
328            line,
329            operator: operator.into(),
330        }
331    }
332
333    /// Create a new `InvalidValue` error.
334    #[must_use]
335    pub fn invalid_value(line: usize, value: impl Into<String>) -> Self {
336        Self::InvalidValue {
337            line,
338            value: value.into(),
339        }
340    }
341
342    /// Create a new `UnsupportedFormat` error.
343    #[must_use]
344    pub fn unsupported_format(
345        line: usize,
346        format_type: impl Into<String>,
347        message: impl Into<String>,
348    ) -> Self {
349        Self::UnsupportedFormat {
350            line,
351            format_type: format_type.into(),
352            message: message.into(),
353        }
354    }
355}
356
357impl EvaluationError {
358    /// Create a new `BufferOverrun` error.
359    #[must_use]
360    pub fn buffer_overrun(offset: usize) -> Self {
361        Self::BufferOverrun { offset }
362    }
363
364    /// Create a new `InvalidOffset` error.
365    #[must_use]
366    pub fn invalid_offset(offset: i64) -> Self {
367        Self::InvalidOffset { offset }
368    }
369
370    /// Create a new `UnsupportedType` error.
371    #[must_use]
372    pub fn unsupported_type(type_name: impl Into<String>) -> Self {
373        Self::UnsupportedType {
374            type_name: type_name.into(),
375        }
376    }
377
378    /// Create a new `RecursionLimitExceeded` error.
379    #[must_use]
380    pub fn recursion_limit_exceeded(depth: u32) -> Self {
381        Self::RecursionLimitExceeded { depth }
382    }
383
384    /// Create a new `StringLengthExceeded` error.
385    #[must_use]
386    pub fn string_length_exceeded(length: usize, max_length: usize) -> Self {
387        Self::StringLengthExceeded { length, max_length }
388    }
389
390    /// Create a new `InvalidStringEncoding` error.
391    #[must_use]
392    pub fn invalid_string_encoding(offset: usize) -> Self {
393        Self::InvalidStringEncoding { offset }
394    }
395
396    /// Create a new `Timeout` error.
397    #[must_use]
398    pub fn timeout(timeout_ms: u64) -> Self {
399        Self::Timeout { timeout_ms }
400    }
401
402    /// Create a new `InternalError` error.
403    #[must_use]
404    pub fn internal_error(message: impl Into<String>) -> Self {
405        Self::InternalError {
406            message: message.into(),
407        }
408    }
409
410    /// Create a new `IndirectWithoutEnvironment` error.
411    #[must_use]
412    pub const fn indirect_without_environment() -> Self {
413        Self::IndirectWithoutEnvironment
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420    use std::io;
421
422    #[test]
423    fn test_libmagic_error_from_parse_error() {
424        let parse_error = ParseError::invalid_syntax(10, "unexpected token");
425        let libmagic_error = LibmagicError::from(parse_error);
426
427        match libmagic_error {
428            LibmagicError::ParseError(_) => (),
429            _ => panic!("Expected ParseError variant"),
430        }
431    }
432
433    #[test]
434    fn test_libmagic_error_from_evaluation_error() {
435        let eval_error = EvaluationError::buffer_overrun(100);
436        let libmagic_error = LibmagicError::from(eval_error);
437
438        match libmagic_error {
439            LibmagicError::EvaluationError(_) => (),
440            _ => panic!("Expected EvaluationError variant"),
441        }
442    }
443
444    #[test]
445    fn test_libmagic_error_from_io_error() {
446        let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
447        let libmagic_error = LibmagicError::from(io_error);
448
449        match libmagic_error {
450            LibmagicError::IoError(_) => (),
451            _ => panic!("Expected IoError variant"),
452        }
453    }
454
455    #[test]
456    fn test_parse_error_display() {
457        let error = ParseError::invalid_syntax(5, "missing operator");
458        let display = format!("{error}");
459        assert_eq!(display, "Invalid syntax at line 5: missing operator");
460    }
461
462    #[test]
463    fn test_parse_error_unsupported_feature() {
464        let error = ParseError::unsupported_feature(12, "regex patterns");
465        let display = format!("{error}");
466        assert_eq!(display, "Unsupported feature at line 12: regex patterns");
467    }
468
469    #[test]
470    fn test_parse_error_invalid_offset() {
471        let error = ParseError::invalid_offset(8, "invalid_offset_spec");
472        let display = format!("{error}");
473        assert_eq!(
474            display,
475            "Invalid offset specification at line 8: invalid_offset_spec"
476        );
477    }
478
479    #[test]
480    fn test_parse_error_invalid_type() {
481        let error = ParseError::invalid_type(15, "unknown_type");
482        let display = format!("{error}");
483        assert_eq!(
484            display,
485            "Invalid type specification at line 15: unknown_type"
486        );
487    }
488
489    #[test]
490    fn test_parse_error_invalid_operator() {
491        let error = ParseError::invalid_operator(20, "??");
492        let display = format!("{error}");
493        assert_eq!(display, "Invalid operator at line 20: ??");
494    }
495
496    #[test]
497    fn test_parse_error_invalid_value() {
498        let error = ParseError::invalid_value(25, "malformed_hex");
499        let display = format!("{error}");
500        assert_eq!(display, "Invalid value at line 25: malformed_hex");
501    }
502
503    #[test]
504    fn test_evaluation_error_buffer_overrun() {
505        let error = EvaluationError::buffer_overrun(1024);
506        let display = format!("{error}");
507        assert_eq!(display, "Buffer overrun at offset 1024");
508    }
509
510    #[test]
511    fn test_evaluation_error_invalid_offset() {
512        let error = EvaluationError::invalid_offset(-50);
513        let display = format!("{error}");
514        assert_eq!(display, "Invalid offset: -50");
515    }
516
517    #[test]
518    fn test_evaluation_error_unsupported_type() {
519        let error = EvaluationError::unsupported_type("complex_type");
520        let display = format!("{error}");
521        assert_eq!(display, "Unsupported type: complex_type");
522    }
523
524    #[test]
525    fn test_evaluation_error_recursion_limit() {
526        let error = EvaluationError::recursion_limit_exceeded(100);
527        let display = format!("{error}");
528        assert_eq!(display, "Recursion limit exceeded (depth: 100)");
529    }
530
531    #[test]
532    fn test_evaluation_error_string_length_exceeded() {
533        let error = EvaluationError::string_length_exceeded(2048, 1024);
534        let display = format!("{error}");
535        assert_eq!(display, "String length limit exceeded: 2048 > 1024");
536    }
537
538    #[test]
539    fn test_evaluation_error_invalid_string_encoding() {
540        let error = EvaluationError::invalid_string_encoding(512);
541        let display = format!("{error}");
542        assert_eq!(display, "Invalid string encoding at offset 512");
543    }
544
545    #[test]
546    fn test_evaluation_error_internal_error() {
547        let error = EvaluationError::internal_error("recursion depth underflow");
548        let display = format!("{error}");
549        assert_eq!(display, "Internal error: recursion depth underflow");
550    }
551
552    #[test]
553    fn test_libmagic_error_display_parse() {
554        let parse_error = ParseError::invalid_syntax(10, "unexpected token");
555        let libmagic_error = LibmagicError::from(parse_error);
556        let display = format!("{libmagic_error}");
557        assert_eq!(
558            display,
559            "Parse error: Invalid syntax at line 10: unexpected token"
560        );
561    }
562
563    #[test]
564    fn test_libmagic_error_display_evaluation() {
565        let eval_error = EvaluationError::buffer_overrun(100);
566        let libmagic_error = LibmagicError::from(eval_error);
567        let display = format!("{libmagic_error}");
568        assert_eq!(display, "Evaluation error: Buffer overrun at offset 100");
569    }
570
571    #[test]
572    fn test_libmagic_error_display_io() {
573        let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
574        let libmagic_error = LibmagicError::from(io_error);
575        let display = format!("{libmagic_error}");
576        assert!(display.starts_with("I/O error:"));
577        assert!(display.contains("access denied"));
578    }
579
580    #[test]
581    fn test_error_debug_formatting() {
582        let error = LibmagicError::ParseError(ParseError::invalid_syntax(5, "test"));
583        let debug = format!("{error:?}");
584        assert!(debug.contains("ParseError"));
585        assert!(debug.contains("InvalidSyntax"));
586    }
587
588    #[test]
589    fn test_parse_error_constructors() {
590        let error1 = ParseError::invalid_syntax(1, "test");
591        let error2 = ParseError::unsupported_feature(2, "feature");
592        let error3 = ParseError::invalid_offset(3, "offset");
593        let error4 = ParseError::invalid_type(4, "type");
594        let error5 = ParseError::invalid_operator(5, "op");
595        let error6 = ParseError::invalid_value(6, "value");
596
597        // Test that all constructors work
598        assert!(matches!(error1, ParseError::InvalidSyntax { .. }));
599        assert!(matches!(error2, ParseError::UnsupportedFeature { .. }));
600        assert!(matches!(error3, ParseError::InvalidOffset { .. }));
601        assert!(matches!(error4, ParseError::InvalidType { .. }));
602        assert!(matches!(error5, ParseError::InvalidOperator { .. }));
603        assert!(matches!(error6, ParseError::InvalidValue { .. }));
604    }
605
606    #[test]
607    fn test_evaluation_error_constructors() {
608        let error1 = EvaluationError::buffer_overrun(100);
609        let error2 = EvaluationError::invalid_offset(-1);
610        let error3 = EvaluationError::unsupported_type("test");
611        let error4 = EvaluationError::recursion_limit_exceeded(50);
612        let error5 = EvaluationError::string_length_exceeded(100, 50);
613        let error6 = EvaluationError::invalid_string_encoding(200);
614
615        // Test that all constructors work
616        assert!(matches!(error1, EvaluationError::BufferOverrun { .. }));
617        assert!(matches!(error2, EvaluationError::InvalidOffset { .. }));
618        assert!(matches!(error3, EvaluationError::UnsupportedType { .. }));
619        assert!(matches!(
620            error4,
621            EvaluationError::RecursionLimitExceeded { .. }
622        ));
623        assert!(matches!(
624            error5,
625            EvaluationError::StringLengthExceeded { .. }
626        ));
627        assert!(matches!(
628            error6,
629            EvaluationError::InvalidStringEncoding { .. }
630        ));
631    }
632
633    #[test]
634    fn test_parse_error_unsupported_format() {
635        let error = ParseError::unsupported_format(
636            0,
637            "binary .mgc",
638            "Binary files not supported. Use --use-builtin option.",
639        );
640        let display = format!("{error}");
641        assert!(display.contains("Unsupported format"));
642        assert!(display.contains("binary .mgc"));
643        assert!(display.contains("--use-builtin"));
644    }
645
646    #[test]
647    fn test_parse_error_unsupported_format_constructor() {
648        let error = ParseError::unsupported_format(5, "test_format", "test message");
649
650        match error {
651            ParseError::UnsupportedFormat {
652                line,
653                format_type,
654                message,
655            } => {
656                assert_eq!(line, 5);
657                assert_eq!(format_type, "test_format");
658                assert_eq!(message, "test message");
659            }
660            _ => panic!("Expected UnsupportedFormat variant"),
661        }
662    }
663}