Skip to main content

libmagic_rs/
lib.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Rust Libmagic - A pure-Rust implementation of libmagic
5//!
6//! This library provides safe, efficient file type identification through magic rule evaluation.
7//! It parses magic files into an Abstract Syntax Tree (AST) and evaluates them against file
8//! buffers using memory-mapped I/O for optimal performance.
9//!
10//! # Security Features
11//!
12//! This implementation prioritizes security through:
13//! - **Memory Safety**: Pure Rust with no unsafe code (except in vetted dependencies)
14//! - **Bounds Checking**: Comprehensive validation of all buffer accesses
15//! - **Resource Limits**: Configurable limits to prevent resource exhaustion attacks
16//! - **Input Validation**: Strict validation of magic files and configuration
17//! - **Error Handling**: Secure error messages that don't leak sensitive information
18//! - **Timeout Protection**: Configurable timeouts to prevent denial of service
19//!
20//! # Examples
21//!
22//! ## Complete Workflow: Load → Evaluate → Output
23//!
24//! ```rust,no_run
25//! use libmagic_rs::MagicDatabase;
26//!
27//! // Load magic rules from a text file
28//! let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
29//!
30//! // Evaluate a file to determine its type
31//! let result = db.evaluate_file("sample.bin")?;
32//! println!("File type: {}", result.description);
33//!
34//! // Access metadata about loaded rules
35//! if let Some(path) = db.source_path() {
36//!     println!("Rules loaded from: {}", path.display());
37//! }
38//! # Ok::<(), Box<dyn std::error::Error>>(())
39//! ```
40//!
41//! ## Loading from a Directory
42//!
43//! ```rust,no_run
44//! use libmagic_rs::MagicDatabase;
45//!
46//! // Load all magic files from a directory (Magdir pattern)
47//! let db = MagicDatabase::load_from_file("/usr/share/misc/magic.d")?;
48//!
49//! // Evaluate multiple files
50//! for file in &["file1.bin", "file2.bin", "file3.bin"] {
51//!     let result = db.evaluate_file(file)?;
52//!     println!("{}: {}", file, result.description);
53//! }
54//! # Ok::<(), Box<dyn std::error::Error>>(())
55//! ```
56//!
57//! ## Error Handling for Binary Files
58//!
59//! ```rust,no_run
60//! use libmagic_rs::MagicDatabase;
61//!
62//! // Attempt to load a binary .mgc file
63//! match MagicDatabase::load_from_file("/usr/share/misc/magic.mgc") {
64//!     Ok(db) => {
65//!         let result = db.evaluate_file("sample.bin")?;
66//!         println!("File type: {}", result.description);
67//!     }
68//!     Err(e) => {
69//!         eprintln!("Error loading magic file: {}", e);
70//!         eprintln!("Hint: Binary .mgc files are not supported.");
71//!         eprintln!("Use --use-builtin option to use built-in rules,");
72//!         eprintln!("or provide a text-based magic file or directory.");
73//!     }
74//! }
75//! # Ok::<(), Box<dyn std::error::Error>>(())
76//! ```
77//!
78//! ## Debugging with Source Path Metadata
79//!
80//! ```rust,no_run
81//! use libmagic_rs::MagicDatabase;
82//!
83//! let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
84//!
85//! // Use source_path() for debugging and logging
86//! if let Some(source) = db.source_path() {
87//!     println!("Loaded {} from {}",
88//!              "magic rules",
89//!              source.display());
90//! }
91//!
92//! // Evaluate files with source tracking
93//! let result = db.evaluate_file("sample.bin")?;
94//! println!("Detection result: {}", result.description);
95//! # Ok::<(), Box<dyn std::error::Error>>(())
96//! ```
97
98#![deny(missing_docs)]
99#![deny(unsafe_code)]
100#![deny(clippy::all)]
101#![warn(clippy::pedantic)]
102
103use std::path::{Path, PathBuf};
104
105use serde::{Deserialize, Serialize};
106
107// Re-export modules
108pub mod builtin_rules;
109pub mod error;
110pub mod evaluator;
111pub mod io;
112pub mod mime;
113pub mod output;
114pub mod parser;
115pub mod tags;
116
117/// Build-time helpers for compiling magic rules.
118///
119/// This module contains functionality used by the build script to parse magic files
120/// and generate Rust code for built-in rules. It is only available during tests and
121/// documentation builds to enable comprehensive testing of the build process.
122#[cfg(any(test, doc))]
123pub mod build_helpers;
124
125// Re-export core AST types for convenience
126pub use parser::ast::{
127    Endianness, MagicRule, OffsetSpec, Operator, StrengthModifier, TypeKind, Value,
128};
129
130// Re-export evaluator types for convenience
131pub use evaluator::{EvaluationContext, RuleMatch};
132
133// Re-export error types for convenience
134pub use error::{EvaluationError, LibmagicError, ParseError};
135
136/// Result type for library operations
137pub type Result<T> = std::result::Result<T, LibmagicError>;
138
139impl From<crate::io::IoError> for LibmagicError {
140    fn from(err: crate::io::IoError) -> Self {
141        // Preserve the structured error message (includes path and operation context)
142        LibmagicError::FileError(err.to_string())
143    }
144}
145
146/// Configuration for rule evaluation
147///
148/// This struct controls various aspects of magic rule evaluation behavior,
149/// including performance limits, output options, and matching strategies.
150///
151/// # Examples
152///
153/// ```rust
154/// use libmagic_rs::EvaluationConfig;
155///
156/// // Use default configuration
157/// let config = EvaluationConfig::default();
158///
159/// // Create custom configuration
160/// let custom_config = EvaluationConfig {
161///     max_recursion_depth: 10,
162///     max_string_length: 4096,
163///     stop_at_first_match: false, // Get all matches
164///     enable_mime_types: true,
165///     timeout_ms: Some(5000), // 5 second timeout
166/// };
167/// ```
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct EvaluationConfig {
170    /// Maximum recursion depth for nested rules
171    ///
172    /// This prevents infinite recursion in malformed magic files and limits
173    /// the depth of rule hierarchy traversal. Default is 20.
174    pub max_recursion_depth: u32,
175
176    /// Maximum string length to read
177    ///
178    /// This limits the amount of data read for string types to prevent
179    /// excessive memory usage. Default is 8192 bytes.
180    pub max_string_length: usize,
181
182    /// Stop at first match or continue for all matches
183    ///
184    /// When `true`, evaluation stops after the first matching rule.
185    /// When `false`, all rules are evaluated to find all matches.
186    /// Default is `true` for performance.
187    pub stop_at_first_match: bool,
188
189    /// Enable MIME type mapping in results
190    ///
191    /// When `true`, the evaluator will attempt to map file type descriptions
192    /// to standard MIME types. Default is `false`.
193    pub enable_mime_types: bool,
194
195    /// Timeout for evaluation in milliseconds
196    ///
197    /// If set, evaluation will be aborted if it takes longer than this duration.
198    /// `None` means no timeout. Default is `None`.
199    pub timeout_ms: Option<u64>,
200}
201
202impl Default for EvaluationConfig {
203    fn default() -> Self {
204        Self {
205            max_recursion_depth: 20,
206            max_string_length: 8192,
207            stop_at_first_match: true,
208            enable_mime_types: false,
209            timeout_ms: None,
210        }
211    }
212}
213
214impl EvaluationConfig {
215    /// Create a new configuration with default values
216    ///
217    /// # Examples
218    ///
219    /// ```rust
220    /// use libmagic_rs::EvaluationConfig;
221    ///
222    /// let config = EvaluationConfig::new();
223    /// assert_eq!(config.max_recursion_depth, 20);
224    /// assert_eq!(config.max_string_length, 8192);
225    /// assert!(config.stop_at_first_match);
226    /// assert!(!config.enable_mime_types);
227    /// assert_eq!(config.timeout_ms, None);
228    /// ```
229    #[must_use]
230    pub fn new() -> Self {
231        Self::default()
232    }
233
234    /// Create a configuration optimized for performance
235    ///
236    /// This configuration prioritizes speed over completeness:
237    /// - Lower recursion depth limit
238    /// - Smaller string length limit
239    /// - Stop at first match
240    /// - No MIME type mapping
241    /// - Short timeout
242    ///
243    /// # Examples
244    ///
245    /// ```rust
246    /// use libmagic_rs::EvaluationConfig;
247    ///
248    /// let config = EvaluationConfig::performance();
249    /// assert_eq!(config.max_recursion_depth, 10);
250    /// assert_eq!(config.max_string_length, 1024);
251    /// assert!(config.stop_at_first_match);
252    /// assert!(!config.enable_mime_types);
253    /// assert_eq!(config.timeout_ms, Some(1000));
254    /// ```
255    #[must_use]
256    pub const fn performance() -> Self {
257        Self {
258            max_recursion_depth: 10,
259            max_string_length: 1024,
260            stop_at_first_match: true,
261            enable_mime_types: false,
262            timeout_ms: Some(1000), // 1 second
263        }
264    }
265
266    /// Create a configuration optimized for completeness
267    ///
268    /// This configuration prioritizes finding all matches over speed:
269    /// - Higher recursion depth limit
270    /// - Larger string length limit
271    /// - Find all matches
272    /// - Enable MIME type mapping
273    /// - Longer timeout
274    ///
275    /// # Examples
276    ///
277    /// ```rust
278    /// use libmagic_rs::EvaluationConfig;
279    ///
280    /// let config = EvaluationConfig::comprehensive();
281    /// assert_eq!(config.max_recursion_depth, 50);
282    /// assert_eq!(config.max_string_length, 32768);
283    /// assert!(!config.stop_at_first_match);
284    /// assert!(config.enable_mime_types);
285    /// assert_eq!(config.timeout_ms, Some(30000));
286    /// ```
287    #[must_use]
288    pub const fn comprehensive() -> Self {
289        Self {
290            max_recursion_depth: 50,
291            max_string_length: 32768,
292            stop_at_first_match: false,
293            enable_mime_types: true,
294            timeout_ms: Some(30000), // 30 seconds
295        }
296    }
297
298    /// Validate the configuration settings
299    ///
300    /// Performs comprehensive security validation of all configuration values
301    /// to prevent malicious configurations that could lead to resource exhaustion,
302    /// denial of service, or other security issues.
303    ///
304    /// # Security
305    ///
306    /// This validation prevents:
307    /// - Stack overflow attacks through excessive recursion depth
308    /// - Memory exhaustion through oversized string limits
309    /// - Denial of service through excessive timeouts
310    /// - Integer overflow in configuration calculations
311    ///
312    /// # Errors
313    ///
314    /// Returns `LibmagicError::InvalidFormat` if any configuration values
315    /// are invalid or out of reasonable bounds.
316    ///
317    /// # Examples
318    ///
319    /// ```rust
320    /// use libmagic_rs::EvaluationConfig;
321    ///
322    /// let config = EvaluationConfig::default();
323    /// assert!(config.validate().is_ok());
324    ///
325    /// let invalid_config = EvaluationConfig {
326    ///     max_recursion_depth: 0, // Invalid: must be > 0
327    ///     ..Default::default()
328    /// };
329    /// assert!(invalid_config.validate().is_err());
330    /// ```
331    pub fn validate(&self) -> Result<()> {
332        self.validate_recursion_depth()?;
333        self.validate_string_length()?;
334        self.validate_timeout()?;
335        self.validate_resource_combination()?;
336        Ok(())
337    }
338
339    /// Validate recursion depth to prevent stack overflow attacks
340    fn validate_recursion_depth(&self) -> Result<()> {
341        const MAX_SAFE_RECURSION_DEPTH: u32 = 1000;
342
343        if self.max_recursion_depth == 0 {
344            return Err(LibmagicError::ConfigError {
345                reason: "max_recursion_depth must be greater than 0".to_string(),
346            });
347        }
348
349        if self.max_recursion_depth > MAX_SAFE_RECURSION_DEPTH {
350            return Err(LibmagicError::ConfigError {
351                reason: format!(
352                    "max_recursion_depth must not exceed {MAX_SAFE_RECURSION_DEPTH} to prevent stack overflow"
353                ),
354            });
355        }
356
357        Ok(())
358    }
359
360    /// Validate string length to prevent memory exhaustion
361    fn validate_string_length(&self) -> Result<()> {
362        const MAX_SAFE_STRING_LENGTH: usize = 1_048_576; // 1MB
363
364        if self.max_string_length == 0 {
365            return Err(LibmagicError::ConfigError {
366                reason: "max_string_length must be greater than 0".to_string(),
367            });
368        }
369
370        if self.max_string_length > MAX_SAFE_STRING_LENGTH {
371            return Err(LibmagicError::ConfigError {
372                reason: format!(
373                    "max_string_length must not exceed {MAX_SAFE_STRING_LENGTH} bytes to prevent memory exhaustion"
374                ),
375            });
376        }
377
378        Ok(())
379    }
380
381    /// Validate timeout to prevent denial of service
382    fn validate_timeout(&self) -> Result<()> {
383        const MAX_SAFE_TIMEOUT_MS: u64 = 300_000; // 5 minutes
384
385        if let Some(timeout) = self.timeout_ms {
386            if timeout == 0 {
387                return Err(LibmagicError::ConfigError {
388                    reason: "timeout_ms must be greater than 0 if specified".to_string(),
389                });
390            }
391
392            if timeout > MAX_SAFE_TIMEOUT_MS {
393                return Err(LibmagicError::ConfigError {
394                    reason: format!(
395                        "timeout_ms must not exceed {MAX_SAFE_TIMEOUT_MS} (5 minutes) to prevent denial of service"
396                    ),
397                });
398            }
399        }
400
401        Ok(())
402    }
403
404    /// Validate resource combination to prevent resource exhaustion
405    fn validate_resource_combination(&self) -> Result<()> {
406        const HIGH_RECURSION_THRESHOLD: u32 = 100;
407        const LARGE_STRING_THRESHOLD: usize = 65536;
408
409        if self.max_recursion_depth > HIGH_RECURSION_THRESHOLD
410            && self.max_string_length > LARGE_STRING_THRESHOLD
411        {
412            return Err(LibmagicError::ConfigError {
413                reason: format!(
414                    "High recursion depth (>{HIGH_RECURSION_THRESHOLD}) combined with large string length (>{LARGE_STRING_THRESHOLD}) may cause resource exhaustion"
415                ),
416            });
417        }
418
419        Ok(())
420    }
421}
422
423/// Main interface for magic rule database
424#[derive(Debug)]
425pub struct MagicDatabase {
426    rules: Vec<MagicRule>,
427    config: EvaluationConfig,
428    /// Optional path to the source magic file or directory from which rules were loaded.
429    /// This is used for debugging and logging purposes.
430    source_path: Option<PathBuf>,
431    /// Cached MIME type mapper to avoid rebuilding the lookup table on every evaluation
432    mime_mapper: mime::MimeMapper,
433}
434
435impl MagicDatabase {
436    /// Create a database using built-in magic rules.
437    ///
438    /// Loads magic rules that are compiled into the library binary at build time
439    /// from `src/builtin_rules.magic`. These rules provide high-confidence detection
440    /// for common file types including executables (ELF, PE/DOS), archives (ZIP, TAR,
441    /// GZIP), images (JPEG, PNG, GIF, BMP), and documents (PDF).
442    ///
443    /// # Errors
444    ///
445    /// Currently always returns `Ok`. In future implementations, this may return
446    /// an error if the built-in rules fail to load or validate.
447    ///
448    /// # Examples
449    ///
450    /// ```rust,no_run
451    /// use libmagic_rs::MagicDatabase;
452    ///
453    /// let db = MagicDatabase::with_builtin_rules()?;
454    /// let result = db.evaluate_buffer(b"\x7fELF")?;
455    /// // Returns actual file type detection (e.g., "ELF")
456    /// # Ok::<(), Box<dyn std::error::Error>>(())
457    /// ```
458    pub fn with_builtin_rules() -> Result<Self> {
459        Self::with_builtin_rules_and_config(EvaluationConfig::default())
460    }
461
462    /// Create database with built-in rules and custom configuration.
463    ///
464    /// Loads built-in magic rules compiled at build time and applies the specified
465    /// evaluation configuration (e.g., custom timeout settings).
466    ///
467    /// # Arguments
468    ///
469    /// * `config` - Custom evaluation configuration to use with the built-in rules
470    ///
471    /// # Errors
472    ///
473    /// Returns `LibmagicError` if the configuration is invalid (e.g., timeout is zero).
474    ///
475    /// # Examples
476    ///
477    /// ```rust,no_run
478    /// use libmagic_rs::{MagicDatabase, EvaluationConfig};
479    ///
480    /// let config = EvaluationConfig {
481    ///     timeout_ms: Some(5000), // 5 second timeout
482    ///     ..EvaluationConfig::default()
483    /// };
484    /// let db = MagicDatabase::with_builtin_rules_and_config(config)?;
485    /// # Ok::<(), Box<dyn std::error::Error>>(())
486    /// ```
487    pub fn with_builtin_rules_and_config(config: EvaluationConfig) -> Result<Self> {
488        config.validate()?;
489        Ok(Self {
490            rules: crate::builtin_rules::get_builtin_rules(),
491            config,
492            source_path: None,
493            mime_mapper: mime::MimeMapper::new(),
494        })
495    }
496
497    /// Load magic rules from a file
498    ///
499    /// # Arguments
500    ///
501    /// * `path` - Path to the magic file to load
502    ///
503    /// # Errors
504    ///
505    /// Returns `LibmagicError::IoError` if the file cannot be read.
506    /// Returns `LibmagicError::ParseError` if the magic file format is invalid.
507    ///
508    /// # Examples
509    ///
510    /// ```rust,no_run
511    /// use libmagic_rs::MagicDatabase;
512    ///
513    /// let db = MagicDatabase::load_from_file("magic.db")?;
514    /// # Ok::<(), Box<dyn std::error::Error>>(())
515    /// ```
516    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
517        Self::load_from_file_with_config(path, EvaluationConfig::default())
518    }
519
520    /// Load from file with custom config (e.g., timeout)
521    ///
522    /// # Errors
523    ///
524    /// Returns error if file cannot be read, parsed, or config is invalid
525    pub fn load_from_file_with_config<P: AsRef<Path>>(
526        path: P,
527        config: EvaluationConfig,
528    ) -> Result<Self> {
529        config.validate()?;
530        let rules = parser::load_magic_file(path.as_ref()).map_err(|e| match e {
531            ParseError::IoError(io_err) => LibmagicError::IoError(io_err),
532            other => LibmagicError::ParseError(other),
533        })?;
534
535        Ok(Self {
536            rules,
537            config,
538            source_path: Some(path.as_ref().to_path_buf()),
539            mime_mapper: mime::MimeMapper::new(),
540        })
541    }
542
543    /// Evaluate magic rules against a file
544    ///
545    /// # Arguments
546    ///
547    /// * `path` - Path to the file to evaluate
548    ///
549    /// # Errors
550    ///
551    /// Returns `LibmagicError::IoError` if the file cannot be accessed.
552    /// Returns `LibmagicError::EvaluationError` if rule evaluation fails.
553    ///
554    /// # Examples
555    ///
556    /// ```rust,no_run
557    /// use libmagic_rs::MagicDatabase;
558    ///
559    /// let db = MagicDatabase::load_from_file("magic.db")?;
560    /// let result = db.evaluate_file("sample.bin")?;
561    /// println!("File type: {}", result.description);
562    /// # Ok::<(), Box<dyn std::error::Error>>(())
563    /// ```
564    pub fn evaluate_file<P: AsRef<Path>>(&self, path: P) -> Result<EvaluationResult> {
565        use crate::evaluator::evaluate_rules_with_config;
566        use crate::io::FileBuffer;
567        use std::fs;
568        use std::time::Instant;
569
570        let start_time = Instant::now();
571        let path = path.as_ref();
572
573        // Check if file is empty - if so, evaluate as empty buffer
574        // This allows empty files to be processed like any other file
575        let file_metadata = fs::metadata(path)?;
576        let file_size = file_metadata.len();
577
578        if file_size == 0 {
579            // Empty file - evaluate as empty buffer but preserve file metadata
580            let mut result = self.evaluate_buffer_internal(b"", start_time)?;
581            result.metadata.file_size = 0;
582            result.metadata.magic_file.clone_from(&self.source_path);
583            return Ok(result);
584        }
585
586        // Load the file into memory
587        let file_buffer = FileBuffer::new(path)?;
588        let buffer = file_buffer.as_slice();
589
590        // Evaluate rules against the file buffer (build_result handles empty rules/matches)
591        let matches = if self.rules.is_empty() {
592            vec![]
593        } else {
594            evaluate_rules_with_config(&self.rules, buffer, &self.config)?
595        };
596
597        Ok(self.build_result(matches, file_size, start_time))
598    }
599
600    /// Evaluate magic rules against an in-memory buffer
601    ///
602    /// This method evaluates a byte buffer directly without reading from disk,
603    /// which is useful for stdin input or pre-loaded data.
604    ///
605    /// # Arguments
606    ///
607    /// * `buffer` - Byte buffer to evaluate
608    ///
609    /// # Errors
610    ///
611    /// Returns `LibmagicError::EvaluationError` if rule evaluation fails.
612    ///
613    /// # Examples
614    ///
615    /// ```rust,no_run
616    /// use libmagic_rs::MagicDatabase;
617    ///
618    /// let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
619    /// let buffer = b"test data";
620    /// let result = db.evaluate_buffer(buffer)?;
621    /// println!("Buffer type: {}", result.description);
622    /// # Ok::<(), Box<dyn std::error::Error>>(())
623    /// ```
624    pub fn evaluate_buffer(&self, buffer: &[u8]) -> Result<EvaluationResult> {
625        use std::time::Instant;
626        self.evaluate_buffer_internal(buffer, Instant::now())
627    }
628
629    /// Internal buffer evaluation with externally provided start time
630    fn evaluate_buffer_internal(
631        &self,
632        buffer: &[u8],
633        start_time: std::time::Instant,
634    ) -> Result<EvaluationResult> {
635        use crate::evaluator::evaluate_rules_with_config;
636
637        let file_size = buffer.len() as u64;
638
639        let matches = if self.rules.is_empty() {
640            vec![]
641        } else {
642            evaluate_rules_with_config(&self.rules, buffer, &self.config)?
643        };
644
645        Ok(self.build_result(matches, file_size, start_time))
646    }
647
648    /// Build an `EvaluationResult` from match results, file size, and start time.
649    ///
650    /// This is shared between `evaluate_file` and `evaluate_buffer_internal` to
651    /// avoid duplicating the result-construction logic.
652    fn build_result(
653        &self,
654        matches: Vec<evaluator::RuleMatch>,
655        file_size: u64,
656        start_time: std::time::Instant,
657    ) -> EvaluationResult {
658        let (description, confidence) = if matches.is_empty() {
659            ("data".to_string(), 0.0)
660        } else {
661            (
662                Self::concatenate_messages(&matches),
663                matches.first().map_or(0.0, |m| m.confidence),
664            )
665        };
666
667        let mime_type = if self.config.enable_mime_types {
668            self.mime_mapper.get_mime_type(&description)
669        } else {
670            None
671        };
672
673        EvaluationResult {
674            description,
675            mime_type,
676            confidence,
677            matches,
678            metadata: EvaluationMetadata {
679                file_size,
680                evaluation_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
681                rules_evaluated: self.rules.len(),
682                magic_file: self.source_path.clone(),
683                timed_out: false,
684            },
685        }
686    }
687
688    /// Concatenate match messages following libmagic behavior
689    ///
690    /// Messages are joined with spaces, except when a message starts with
691    /// backspace character (\\b) which suppresses the space.
692    fn concatenate_messages(matches: &[evaluator::RuleMatch]) -> String {
693        let capacity: usize = matches.iter().map(|m| m.message.len() + 1).sum();
694        let mut result = String::with_capacity(capacity);
695        for m in matches {
696            if let Some(rest) = m.message.strip_prefix('\u{0008}') {
697                // Backspace suppresses the space and the character itself
698                result.push_str(rest);
699            } else if !result.is_empty() {
700                result.push(' ');
701                result.push_str(&m.message);
702            } else {
703                result.push_str(&m.message);
704            }
705        }
706        result
707    }
708
709    /// Returns the evaluation configuration used by this database.
710    ///
711    /// This provides read-only access to the evaluation configuration for
712    /// callers that need to inspect resource limits or evaluation options.
713    #[must_use]
714    pub fn config(&self) -> &EvaluationConfig {
715        &self.config
716    }
717
718    /// Returns the path from which magic rules were loaded.
719    ///
720    /// This method returns the source path that was used to load the magic rules
721    /// into this database. It is useful for debugging, logging, and tracking the
722    /// origin of magic rules.
723    ///
724    /// # Returns
725    ///
726    /// - `Some(&Path)` - If the database was loaded from a file or directory using
727    ///   [`load_from_file()`](Self::load_from_file)
728    /// - `None` - If the database was constructed programmatically or the source
729    ///   path was not recorded
730    ///
731    /// # Examples
732    ///
733    /// ```rust,no_run
734    /// use libmagic_rs::MagicDatabase;
735    ///
736    /// let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
737    /// if let Some(path) = db.source_path() {
738    ///     println!("Rules loaded from: {}", path.display());
739    /// }
740    /// # Ok::<(), Box<dyn std::error::Error>>(())
741    /// ```
742    #[must_use]
743    pub fn source_path(&self) -> Option<&Path> {
744        self.source_path.as_deref()
745    }
746}
747
748/// Metadata about the evaluation process
749///
750/// Contains diagnostic information about how the evaluation was performed,
751/// including performance metrics and statistics about rule processing.
752///
753/// # Examples
754///
755/// ```
756/// use libmagic_rs::EvaluationMetadata;
757/// use std::path::PathBuf;
758///
759/// let metadata = EvaluationMetadata {
760///     file_size: 8192,
761///     evaluation_time_ms: 2.5,
762///     rules_evaluated: 42,
763///     magic_file: Some(PathBuf::from("/usr/share/misc/magic")),
764///     timed_out: false,
765/// };
766///
767/// assert_eq!(metadata.file_size, 8192);
768/// assert!(!metadata.timed_out);
769/// ```
770#[derive(Debug, Clone, Serialize, Deserialize)]
771pub struct EvaluationMetadata {
772    /// Size of the analyzed file or buffer in bytes
773    pub file_size: u64,
774    /// Time taken to evaluate rules in milliseconds
775    pub evaluation_time_ms: f64,
776    /// Number of top-level rules that were evaluated
777    pub rules_evaluated: usize,
778    /// Path to the magic file used, or None for built-in rules
779    pub magic_file: Option<PathBuf>,
780    /// Whether evaluation was stopped due to timeout
781    pub timed_out: bool,
782}
783
784impl Default for EvaluationMetadata {
785    fn default() -> Self {
786        Self {
787            file_size: 0,
788            evaluation_time_ms: 0.0,
789            rules_evaluated: 0,
790            magic_file: None,
791            timed_out: false,
792        }
793    }
794}
795
796/// Result of magic rule evaluation
797///
798/// Contains the file type description, optional MIME type, confidence score,
799/// individual match details, and evaluation metadata.
800///
801/// # Examples
802///
803/// ```
804/// use libmagic_rs::{EvaluationResult, EvaluationMetadata};
805///
806/// let result = EvaluationResult {
807///     description: "ELF 64-bit executable".to_string(),
808///     mime_type: Some("application/x-executable".to_string()),
809///     confidence: 0.9,
810///     matches: vec![],
811///     metadata: EvaluationMetadata::default(),
812/// };
813///
814/// assert_eq!(result.description, "ELF 64-bit executable");
815/// assert!(result.confidence > 0.5);
816/// ```
817#[derive(Debug, Clone, Serialize, Deserialize)]
818pub struct EvaluationResult {
819    /// Human-readable file type description
820    ///
821    /// This is the concatenated message from all matching rules,
822    /// following libmagic behavior where hierarchical matches
823    /// are joined with spaces (unless backspace character is used).
824    pub description: String,
825    /// Optional MIME type for the detected file type
826    ///
827    /// Only populated when `enable_mime_types` is set in the configuration.
828    pub mime_type: Option<String>,
829    /// Confidence score (0.0 to 1.0)
830    ///
831    /// Based on the depth of the match in the rule hierarchy.
832    /// Deeper matches indicate more specific identification.
833    pub confidence: f64,
834    /// Individual match results from rule evaluation
835    ///
836    /// Contains details about each rule that matched, including
837    /// offset, matched value, and per-match confidence.
838    pub matches: Vec<evaluator::RuleMatch>,
839    /// Metadata about the evaluation process
840    pub metadata: EvaluationMetadata,
841}
842
843#[cfg(test)]
844mod tests;