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::Serialize;
106
107// Re-export modules
108pub mod builtin_rules;
109mod config;
110pub mod error;
111pub mod evaluator;
112pub mod io;
113pub mod mime;
114pub mod output;
115pub mod parser;
116pub mod tags;
117
118pub use config::EvaluationConfig;
119
120/// Build-time helpers for compiling magic rules.
121///
122/// This module contains functionality used by the build script to parse magic files
123/// and generate Rust code for built-in rules. It is only available during tests and
124/// documentation builds to enable comprehensive testing of the build process.
125#[cfg(any(test, doc))]
126pub mod build_helpers;
127
128// Re-export core AST types for convenience
129pub use parser::ast::{
130    Endianness, MagicRule, OffsetSpec, Operator, PStringLengthWidth, StrengthModifier, TypeKind,
131    Value,
132};
133
134// Re-export evaluator types for convenience
135pub use evaluator::{EvaluationContext, RuleMatch};
136
137// Re-export error types for convenience
138pub use error::{EvaluationError, LibmagicError, ParseError};
139
140/// Result type for library operations
141pub type Result<T> = std::result::Result<T, LibmagicError>;
142
143impl From<crate::io::IoError> for LibmagicError {
144    fn from(err: crate::io::IoError) -> Self {
145        // Preserve the structured error message (includes path and operation context)
146        LibmagicError::FileError(err.to_string())
147    }
148}
149
150/// Main interface for magic rule database
151#[derive(Debug)]
152#[non_exhaustive]
153pub struct MagicDatabase {
154    /// Named subroutine definitions extracted from magic file `name` rules,
155    /// keyed by identifier. The evaluator consults this table when a rule of
156    /// type `TypeKind::Meta(MetaType::Use { name, .. })` is reached.
157    name_table: std::sync::Arc<crate::parser::name_table::NameTable>,
158    /// Top-level rules as a shared immutable slice. This is the primary rule
159    /// storage for the database. Passed through the evaluation context as part
160    /// of the rule environment so whole-database operations (e.g. `indirect`)
161    /// can re-enter at the root without re-sorting or cloning the rule tree.
162    root_rules: std::sync::Arc<[MagicRule]>,
163    config: EvaluationConfig,
164    /// Optional path to the source magic file or directory from which rules were loaded.
165    /// This is used for debugging and logging purposes.
166    source_path: Option<PathBuf>,
167    /// Cached MIME type mapper to avoid rebuilding the lookup table on every evaluation
168    mime_mapper: mime::MimeMapper,
169}
170
171impl MagicDatabase {
172    /// Create a database using built-in magic rules.
173    ///
174    /// Loads magic rules that are compiled into the library binary at build time
175    /// from `src/builtin_rules.magic`. These rules provide high-confidence detection
176    /// for common file types including executables (ELF, PE/DOS), archives (ZIP, TAR,
177    /// GZIP), images (JPEG, PNG, GIF, BMP), and documents (PDF).
178    ///
179    /// # Security
180    ///
181    /// This constructor uses [`EvaluationConfig::default()`], which leaves
182    /// `timeout_ms` unset (unbounded). When processing untrusted input
183    /// (adversarial file buffers, large uploads, etc.), prefer
184    /// [`MagicDatabase::with_builtin_rules_and_config`] with
185    /// [`EvaluationConfig::performance()`] (which sets a 1-second timeout)
186    /// or construct a config explicitly with a non-`None` timeout sized
187    /// for your workload. The `Default` impl intentionally targets CLI
188    /// one-shot usage rather than long-running services.
189    ///
190    /// # Thread safety
191    ///
192    /// `MagicDatabase` is `Send + Sync` and holds no interior mutability,
193    /// so an `Arc<MagicDatabase>` can be shared across threads for
194    /// parallel file scanning. A fresh evaluation context is constructed
195    /// per `evaluate_buffer` / `evaluate_file` call, so concurrent calls
196    /// do not interfere.
197    ///
198    /// # Errors
199    ///
200    /// Currently always returns `Ok`. In future implementations, this may return
201    /// an error if the built-in rules fail to load or validate.
202    ///
203    /// # Examples
204    ///
205    /// ```rust,no_run
206    /// use libmagic_rs::MagicDatabase;
207    ///
208    /// let db = MagicDatabase::with_builtin_rules()?;
209    /// let result = db.evaluate_buffer(b"\x7fELF")?;
210    /// // Returns actual file type detection (e.g., "ELF")
211    /// # Ok::<(), Box<dyn std::error::Error>>(())
212    /// ```
213    pub fn with_builtin_rules() -> Result<Self> {
214        Self::with_builtin_rules_and_config(EvaluationConfig::default())
215    }
216
217    /// Create database with built-in rules and custom configuration.
218    ///
219    /// Loads built-in magic rules compiled at build time and applies the specified
220    /// evaluation configuration (e.g., custom timeout settings).
221    ///
222    /// # Security
223    ///
224    /// For untrusted input (adversarial file buffers, web uploads, mail
225    /// scanning), pass a config with an explicit timeout such as
226    /// [`EvaluationConfig::performance()`]. The default config has
227    /// `timeout_ms = None` which leaves evaluation unbounded; see the
228    /// rationale on [`EvaluationConfig::default`].
229    ///
230    /// # Arguments
231    ///
232    /// * `config` - Custom evaluation configuration to use with the built-in rules
233    ///
234    /// # Errors
235    ///
236    /// Returns `LibmagicError` if the configuration is invalid (e.g., timeout is zero).
237    ///
238    /// # Examples
239    ///
240    /// ```rust,no_run
241    /// use libmagic_rs::{MagicDatabase, EvaluationConfig};
242    ///
243    /// // Prefer the performance() preset over default() when processing
244    /// // untrusted input. default() has no timeout by design.
245    /// let config = EvaluationConfig::performance();
246    /// let db = MagicDatabase::with_builtin_rules_and_config(config)?;
247    /// # Ok::<(), Box<dyn std::error::Error>>(())
248    /// ```
249    pub fn with_builtin_rules_and_config(config: EvaluationConfig) -> Result<Self> {
250        config.validate()?;
251        let mut rules = crate::builtin_rules::get_builtin_rules();
252        // Sort only the TOP-LEVEL rules by strength (libmagic's
253        // `apprentice_sort` orders whole magic entries by their first line's
254        // strength). Continuation/child rules are NOT reordered -- they run
255        // in file order, which is load-bearing for order-sensitive directives
256        // like `default`/`clear` and for multi-fragment descriptions whose
257        // pieces must render in source order (e.g. gzip's "last modified,
258        // max compression, from Unix"). See the non-recursive contract note
259        // on `sort_rules_by_strength`.
260        crate::evaluator::strength::sort_rules_by_strength(&mut rules);
261        let root_rules: std::sync::Arc<[MagicRule]> =
262            std::sync::Arc::from(rules.into_boxed_slice());
263        Ok(Self {
264            name_table: std::sync::Arc::new(crate::parser::name_table::NameTable::empty()),
265            root_rules,
266            config,
267            source_path: None,
268            mime_mapper: mime::MimeMapper::new(),
269        })
270    }
271
272    /// Load magic rules from a file
273    ///
274    /// # Security
275    ///
276    /// This constructor uses [`EvaluationConfig::default()`], which
277    /// leaves `timeout_ms` unset. See the security note on
278    /// [`Self::with_builtin_rules`] for the implications and prefer
279    /// [`Self::load_from_file_with_config`] with an explicit timeout
280    /// when processing untrusted input.
281    ///
282    /// # Arguments
283    ///
284    /// * `path` - Path to the magic file to load
285    ///
286    /// # Errors
287    ///
288    /// Returns `LibmagicError::IoError` if the file cannot be read.
289    /// Returns `LibmagicError::ParseError` if the magic file format is invalid.
290    ///
291    /// # Examples
292    ///
293    /// ```rust,no_run
294    /// use libmagic_rs::MagicDatabase;
295    ///
296    /// let db = MagicDatabase::load_from_file("magic.db")?;
297    /// # Ok::<(), Box<dyn std::error::Error>>(())
298    /// ```
299    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
300        Self::load_from_file_with_config(path, EvaluationConfig::default())
301    }
302
303    /// Load from file with custom config (e.g., timeout).
304    ///
305    /// # Security
306    ///
307    /// For untrusted input, pass [`EvaluationConfig::performance()`] or
308    /// a config with an explicit non-`None` `timeout_ms`. See
309    /// [`Self::with_builtin_rules`] for the full rationale.
310    ///
311    /// # Errors
312    ///
313    /// Returns error if file cannot be read, parsed, or config is invalid
314    pub fn load_from_file_with_config<P: AsRef<Path>>(
315        path: P,
316        config: EvaluationConfig,
317    ) -> Result<Self> {
318        config.validate()?;
319        let parsed = parser::load_magic_file(path.as_ref()).map_err(|e| match e {
320            ParseError::IoError(io_err) => LibmagicError::IoError(io_err),
321            other => LibmagicError::ParseError(other),
322        })?;
323        let parser::ParsedMagic {
324            mut rules,
325            name_table,
326        } = parsed;
327        // Sort only the TOP-LEVEL rules by strength, mirroring libmagic's
328        // `apprentice_sort` (which orders whole magic entries by their first
329        // line's strength and never reorders continuation lines). Child rules
330        // and `name`-block subroutine bodies stay in file order: they are
331        // continuation-level rules, and their order is load-bearing for
332        // `default`/`clear` firing and for multi-fragment descriptions that
333        // must render in source order (e.g. the `gzip-info` subroutine's
334        // "last modified, max compression, from Unix"). Strength-sorting them
335        // reorders a comparison-bearing sibling ahead of a low-strength
336        // `default`, wrongly suppressing the `default` message. See the
337        // non-recursive contract note on `sort_rules_by_strength`.
338        crate::evaluator::strength::sort_rules_by_strength(&mut rules);
339
340        let root_rules: std::sync::Arc<[MagicRule]> =
341            std::sync::Arc::from(rules.into_boxed_slice());
342        Ok(Self {
343            name_table: std::sync::Arc::new(name_table),
344            root_rules,
345            config,
346            source_path: Some(path.as_ref().to_path_buf()),
347            mime_mapper: mime::MimeMapper::new(),
348        })
349    }
350
351    /// Evaluate magic rules against a file
352    ///
353    /// # Arguments
354    ///
355    /// * `path` - Path to the file to evaluate
356    ///
357    /// # Errors
358    ///
359    /// Returns `LibmagicError::IoError` if the file cannot be accessed.
360    /// Returns `LibmagicError::EvaluationError` if rule evaluation fails.
361    ///
362    /// # Security
363    ///
364    /// This method has a time-of-check/time-of-use (TOCTOU) window between
365    /// resolving the path and memory-mapping the file
366    /// ([CWE-367](https://cwe.mitre.org/data/definitions/367.html)).
367    /// Internally, `evaluate_file` first calls `std::fs::metadata(path)` to
368    /// detect the empty-file case, then opens and memory-maps the file via
369    /// [`io::FileBuffer::new`], which itself re-validates file metadata
370    /// (regular file, size bounds) before calling `create_memory_mapping`.
371    /// Between these validation steps and the final `mmap` call, the path
372    /// may be swapped (for example, via a symlink replacement or rename)
373    /// by another process. The content that gets mapped may therefore
374    /// differ from the file that passed validation.
375    ///
376    /// The I/O layer mitigates the common shapes of this attack by
377    /// canonicalizing the path and rejecting special file types, and the
378    /// mapping itself is read-only, so a successful exploit cannot corrupt
379    /// the victim file. The residual risk is that `evaluate_file` may
380    /// classify a different file than the caller intended.
381    ///
382    /// **For adversarial or untrusted environments, prefer
383    /// [`MagicDatabase::evaluate_buffer`]**: load the bytes yourself using
384    /// whatever resource-bounded, TOCTOU-aware I/O strategy your
385    /// application requires (e.g., `openat` with `O_NOFOLLOW`, holding an
386    /// open file descriptor across validation and read), then pass the
387    /// in-memory slice directly to `evaluate_buffer`. See
388    /// [the security assurance case](https://evilbit-labs.github.io/libmagic-rs/security-assurance.html)
389    /// for the residual-risk discussion.
390    ///
391    /// # Examples
392    ///
393    /// ```rust,no_run
394    /// use libmagic_rs::MagicDatabase;
395    ///
396    /// let db = MagicDatabase::load_from_file("magic.db")?;
397    /// let result = db.evaluate_file("sample.bin")?;
398    /// println!("File type: {}", result.description);
399    /// # Ok::<(), Box<dyn std::error::Error>>(())
400    /// ```
401    pub fn evaluate_file<P: AsRef<Path>>(&self, path: P) -> Result<EvaluationResult> {
402        use crate::io::FileBuffer;
403        use std::fs;
404        use std::time::Instant;
405
406        let start_time = Instant::now();
407        let path = path.as_ref();
408
409        // Check if file is empty - if so, evaluate as empty buffer
410        // This allows empty files to be processed like any other file
411        let file_metadata = fs::metadata(path)?;
412        let file_size = file_metadata.len();
413
414        if file_size == 0 {
415            // Empty file - evaluate as empty buffer but preserve file metadata
416            let mut result = self.evaluate_buffer_internal(b"", start_time)?;
417            result.metadata.file_size = 0;
418            result.metadata.magic_file.clone_from(&self.source_path);
419            return Ok(result);
420        }
421
422        // Load the file into memory. Reuse the metadata we just read instead
423        // of having FileBuffer::new call canonicalize+metadata again.
424        let file_buffer = FileBuffer::from_path_and_metadata(path, &file_metadata)?;
425        let buffer = file_buffer.as_slice();
426
427        // Route the evaluation through `evaluate_buffer_internal` so the
428        // rule environment (name table + root rules) is attached to the
429        // context identically for in-memory and on-disk paths.
430        let mut result = self.evaluate_buffer_internal(buffer, start_time)?;
431        result.metadata.file_size = file_size;
432        Ok(result)
433    }
434
435    /// Evaluate magic rules against an in-memory buffer
436    ///
437    /// This method evaluates a byte buffer directly without reading from disk,
438    /// which is useful for stdin input or pre-loaded data.
439    ///
440    /// # Arguments
441    ///
442    /// * `buffer` - Byte buffer to evaluate
443    ///
444    /// # Errors
445    ///
446    /// Returns `LibmagicError::EvaluationError` if rule evaluation fails.
447    ///
448    /// # Examples
449    ///
450    /// ```rust,no_run
451    /// use libmagic_rs::MagicDatabase;
452    ///
453    /// let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
454    /// let buffer = b"test data";
455    /// let result = db.evaluate_buffer(buffer)?;
456    /// println!("Buffer type: {}", result.description);
457    /// # Ok::<(), Box<dyn std::error::Error>>(())
458    /// ```
459    pub fn evaluate_buffer(&self, buffer: &[u8]) -> Result<EvaluationResult> {
460        use std::time::Instant;
461        self.evaluate_buffer_internal(buffer, Instant::now())
462    }
463
464    /// Internal buffer evaluation with externally provided start time
465    fn evaluate_buffer_internal(
466        &self,
467        buffer: &[u8],
468        start_time: std::time::Instant,
469    ) -> Result<EvaluationResult> {
470        use crate::evaluator::{EvaluationContext, RuleEnvironment, evaluate_rules};
471
472        let file_size = buffer.len() as u64;
473
474        // Validate config once at the entry point to match the previous
475        // behavior of `evaluate_rules_with_config`.
476        self.config.validate()?;
477
478        // Reset the thread-local regex compile cache so it is bounded to
479        // the lifetime of a single top-level evaluation call.
480        crate::evaluator::types::regex::reset_regex_cache();
481
482        let env = std::sync::Arc::new(RuleEnvironment {
483            name_table: std::sync::Arc::clone(&self.name_table),
484            root_rules: std::sync::Arc::clone(&self.root_rules),
485        });
486
487        let mut context = EvaluationContext::new(self.config.clone()).with_rule_env(env);
488
489        // `evaluate_rules` returns `Ok(vec![])` for an empty rule list,
490        // so no `is_empty()` guard is needed here.
491        let matches = evaluate_rules(&self.root_rules, buffer, &mut context)?;
492
493        Ok(self.build_result(matches, buffer, file_size, start_time))
494    }
495
496    /// Build an `EvaluationResult` from match results, file size, and start time.
497    ///
498    /// This is shared between `evaluate_file` and `evaluate_buffer_internal` to
499    /// avoid duplicating the result-construction logic.
500    ///
501    /// # Text/data fallback
502    ///
503    /// When rule evaluation produces no usable description -- either
504    /// because no rule matched at all, or because every match that did
505    /// occur carries no description text (a message-less gating rule
506    /// that was allowed to proceed past `stop_at_first_match` without a
507    /// message-bearing rule ever firing behind it, GOTCHAS S13.2) -- the
508    /// description falls back to [`crate::output::ascmagic::classify_fallback`]
509    /// against the original buffer, mirroring GNU `file`'s `file_ascmagic`
510    /// basic text/data classification. This is what keeps the CLI from
511    /// ever printing a blank description for a readable file.
512    fn build_result(
513        &self,
514        matches: Vec<evaluator::RuleMatch>,
515        buffer: &[u8],
516        file_size: u64,
517        start_time: std::time::Instant,
518    ) -> EvaluationResult {
519        let rule_description = if matches.is_empty() {
520            String::new()
521        } else {
522            Self::concatenate_messages(&matches)
523        };
524
525        let (description, confidence) = if rule_description.trim().is_empty() {
526            (
527                crate::output::ascmagic::classify_fallback(buffer).to_string(),
528                0.0,
529            )
530        } else {
531            (
532                rule_description,
533                matches.first().map_or(0.0, |m| m.confidence),
534            )
535        };
536
537        let mime_type = if self.config.enable_mime_types {
538            self.mime_mapper
539                .get_mime_type(&description)
540                .map(String::from)
541        } else {
542            None
543        };
544
545        EvaluationResult {
546            description,
547            mime_type,
548            confidence,
549            matches,
550            metadata: EvaluationMetadata {
551                file_size,
552                evaluation_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
553                rules_evaluated: self.root_rules.len(),
554                magic_file: self.source_path.clone(),
555                timed_out: false,
556            },
557        }
558    }
559
560    /// Concatenate match messages following libmagic behavior
561    ///
562    /// Each match's `message` is first run through
563    /// [`crate::output::format::format_magic_message`], which substitutes
564    /// printf-style specifiers (`%lld`, `%02x`, `%s`, etc.) with the
565    /// rule's read value. Matches that render to an empty string (a
566    /// message-less gating rule -- see GOTCHAS S13.2 -- or a
567    /// `default`/`indirect`/`offset`/`use` directive with no message)
568    /// contribute nothing and are skipped entirely, so they cannot
569    /// introduce a stray separating space between the descriptions on
570    /// either side of them. The remaining rendered strings are joined
571    /// with spaces, except when a rendered string starts with the
572    /// backspace character (`\b`, U+0008) which suppresses both the
573    /// separating space and the backspace itself (GOTCHAS.md S14.1).
574    ///
575    /// The backspace check runs on the *post-substitution* text so rules
576    /// like `\b, version %s` compose correctly once the specifier has been
577    /// rendered.
578    fn concatenate_messages(matches: &[evaluator::RuleMatch]) -> String {
579        use crate::output::format::format_magic_message;
580
581        let capacity: usize = matches.iter().map(|m| m.message.len() + 1).sum();
582        let mut result = String::with_capacity(capacity);
583        for m in matches {
584            let rendered = format_magic_message(&m.message, &m.value, &m.type_kind);
585            if rendered.is_empty() {
586                // No text to contribute -- skip so this match cannot
587                // introduce a stray separating space.
588                continue;
589            }
590            // GNU `file`'s no-separator convention: a description beginning
591            // with a backspace suppresses both the separating space and the
592            // marker itself (GOTCHAS.md S14.1). The marker most often reaches
593            // us as the literal two-character sequence `\b` (backslash + 'b'),
594            // because the message parser preserves description text verbatim --
595            // matching GNU `file`, which keeps the desc literal and
596            // special-cases a leading `\b` at print time (e.g. the msdos
597            // `\b, for MS Windows` and Mach-O universal `\b]` rules). We also
598            // accept a bare U+0008 for programmatically-constructed messages.
599            let without_marker = crate::evaluator::strip_no_separator_marker(&rendered);
600            if let Some(rest) = without_marker {
601                result.push_str(rest);
602            } else if !result.is_empty() {
603                result.push(' ');
604                result.push_str(&rendered);
605            } else {
606                result.push_str(&rendered);
607            }
608        }
609        result
610    }
611
612    /// Returns the evaluation configuration used by this database.
613    ///
614    /// This provides read-only access to the evaluation configuration for
615    /// callers that need to inspect resource limits or evaluation options.
616    #[must_use]
617    pub fn config(&self) -> &EvaluationConfig {
618        &self.config
619    }
620
621    /// Returns the path from which magic rules were loaded.
622    ///
623    /// This method returns the source path that was used to load the magic rules
624    /// into this database. It is useful for debugging, logging, and tracking the
625    /// origin of magic rules.
626    ///
627    /// # Returns
628    ///
629    /// - `Some(&Path)` - If the database was loaded from a file or directory using
630    ///   [`load_from_file()`](Self::load_from_file)
631    /// - `None` - If the database was constructed programmatically or the source
632    ///   path was not recorded
633    ///
634    /// # Examples
635    ///
636    /// ```rust,no_run
637    /// use libmagic_rs::MagicDatabase;
638    ///
639    /// let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
640    /// if let Some(path) = db.source_path() {
641    ///     println!("Rules loaded from: {}", path.display());
642    /// }
643    /// # Ok::<(), Box<dyn std::error::Error>>(())
644    /// ```
645    #[must_use]
646    pub fn source_path(&self) -> Option<&Path> {
647        self.source_path.as_deref()
648    }
649}
650
651/// Metadata about the evaluation process
652///
653/// Contains diagnostic information about how the evaluation was performed,
654/// including performance metrics and statistics about rule processing.
655///
656/// # Examples
657///
658/// ```
659/// use libmagic_rs::EvaluationMetadata;
660/// use std::path::PathBuf;
661///
662/// let metadata = EvaluationMetadata::new(
663///     8192,
664///     2.5,
665///     42,
666///     Some(PathBuf::from("/usr/share/misc/magic")),
667///     false,
668/// );
669///
670/// assert_eq!(metadata.file_size, 8192);
671/// assert!(!metadata.timed_out);
672/// ```
673#[derive(Debug, Clone, Serialize)]
674#[non_exhaustive]
675pub struct EvaluationMetadata {
676    /// Size of the analyzed file or buffer in bytes
677    pub file_size: u64,
678    /// Time taken to evaluate rules in milliseconds
679    pub evaluation_time_ms: f64,
680    /// Number of top-level rules that were evaluated
681    pub rules_evaluated: usize,
682    /// Path to the magic file used, or None for built-in rules
683    #[serde(skip_serializing_if = "Option::is_none", default)]
684    pub magic_file: Option<PathBuf>,
685    /// Whether evaluation was stopped due to timeout
686    pub timed_out: bool,
687}
688
689impl Default for EvaluationMetadata {
690    fn default() -> Self {
691        Self {
692            file_size: 0,
693            evaluation_time_ms: 0.0,
694            rules_evaluated: 0,
695            magic_file: None,
696            timed_out: false,
697        }
698    }
699}
700
701/// Result of magic rule evaluation
702///
703/// Contains the file type description, optional MIME type, confidence score,
704/// individual match details, and evaluation metadata.
705///
706/// # Relationship to [`crate::output::EvaluationResult`]
707///
708/// This is the **library-facing** result type returned by [`MagicDatabase::evaluate_file`]
709/// and [`MagicDatabase::evaluate_buffer`].
710/// It carries a rolled-up description, MIME type, and confidence score along with
711/// raw [`evaluator::RuleMatch`] values. It intentionally does **not** carry the
712/// analyzed filename or a surface-level error string, because those are caller
713/// concerns (a caller may evaluate an in-memory buffer that has no filename).
714///
715/// The parallel type [`crate::output::EvaluationResult`] is the **output-facing**
716/// result used by the CLI and JSON/text formatters. It adds `filename` and
717/// `error`, carries enriched [`crate::output::MatchResult`] values (with
718/// extracted tags), and uses `u32` counters in its metadata to match the JSON
719/// output schema.
720///
721/// The two types are **intentionally distinct** — do not try to unify them.
722/// Convert library → output explicitly via
723/// [`crate::output::EvaluationResult::from_library_result`], which is the single
724/// named conversion point. Any drift between the two hierarchies should be
725/// resolved there, not by back-channel field copying in call sites.
726///
727/// # Examples
728///
729/// ```
730/// use libmagic_rs::{EvaluationResult, EvaluationMetadata};
731///
732/// let result = EvaluationResult::new(
733///     "ELF 64-bit executable".to_string(),
734///     Some("application/x-executable".to_string()),
735///     0.9,
736///     vec![],
737///     EvaluationMetadata::default(),
738/// );
739///
740/// assert_eq!(result.description, "ELF 64-bit executable");
741/// assert!(result.confidence > 0.5);
742/// ```
743#[derive(Debug, Clone, Serialize)]
744#[non_exhaustive]
745pub struct EvaluationResult {
746    /// Human-readable file type description
747    ///
748    /// This is the concatenated message from all matching rules,
749    /// following libmagic behavior where hierarchical matches
750    /// are joined with spaces (unless backspace character is used).
751    pub description: String,
752    /// Optional MIME type for the detected file type
753    ///
754    /// Only populated when `enable_mime_types` is set in the configuration.
755    /// Omitted from the serialized form when unset (rather than emitted
756    /// as `"mime_type": null`) so downstream JSON consumers can treat
757    /// presence as the "MIME type is known" indicator.
758    #[serde(skip_serializing_if = "Option::is_none", default)]
759    pub mime_type: Option<String>,
760    /// Confidence score (0.0 to 1.0)
761    ///
762    /// Based on the depth of the match in the rule hierarchy.
763    /// Deeper matches indicate more specific identification.
764    pub confidence: f64,
765    /// Individual match results from rule evaluation
766    ///
767    /// Contains details about each rule that matched, including
768    /// offset, matched value, and per-match confidence.
769    pub matches: Vec<evaluator::RuleMatch>,
770    /// Metadata about the evaluation process
771    pub metadata: EvaluationMetadata,
772}
773
774impl EvaluationResult {
775    /// Construct a new library-side `EvaluationResult`.
776    ///
777    /// This is the outbound type returned by [`MagicDatabase::evaluate_file`]
778    /// and [`MagicDatabase::evaluate_buffer`]. For the output-facing
779    /// type used by the CLI and JSON/text formatters, see
780    /// [`crate::output::EvaluationResult::from_library_result`].
781    #[must_use]
782    pub fn new(
783        description: String,
784        mime_type: Option<String>,
785        confidence: f64,
786        matches: Vec<evaluator::RuleMatch>,
787        metadata: EvaluationMetadata,
788    ) -> Self {
789        Self {
790            description,
791            mime_type,
792            confidence,
793            matches,
794            metadata,
795        }
796    }
797}
798
799impl EvaluationMetadata {
800    /// Construct a new library-side `EvaluationMetadata` from the four
801    /// always-set fields. `magic_file` and `timed_out` default to `None`
802    /// / `false`; use struct-update syntax with [`EvaluationMetadata::default()`]
803    /// to set them explicitly.
804    #[must_use]
805    pub fn new(
806        file_size: u64,
807        evaluation_time_ms: f64,
808        rules_evaluated: usize,
809        magic_file: Option<PathBuf>,
810        timed_out: bool,
811    ) -> Self {
812        Self {
813            file_size,
814            evaluation_time_ms,
815            rules_evaluated,
816            magic_file,
817            timed_out,
818        }
819    }
820}
821
822#[cfg(test)]
823mod tests;