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;
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)]
152pub struct MagicDatabase {
153    /// Named subroutine definitions extracted from magic file `name` rules,
154    /// keyed by identifier. The evaluator consults this table when a rule of
155    /// type `TypeKind::Meta(MetaType::Use(name))` is reached.
156    name_table: std::sync::Arc<crate::parser::name_table::NameTable>,
157    /// Top-level rules as a shared immutable slice. This is the primary rule
158    /// storage for the database. Passed through the evaluation context as part
159    /// of the rule environment so whole-database operations (e.g. `indirect`)
160    /// can re-enter at the root without re-sorting or cloning the rule tree.
161    root_rules: std::sync::Arc<[MagicRule]>,
162    config: EvaluationConfig,
163    /// Optional path to the source magic file or directory from which rules were loaded.
164    /// This is used for debugging and logging purposes.
165    source_path: Option<PathBuf>,
166    /// Cached MIME type mapper to avoid rebuilding the lookup table on every evaluation
167    mime_mapper: mime::MimeMapper,
168}
169
170impl MagicDatabase {
171    /// Create a database using built-in magic rules.
172    ///
173    /// Loads magic rules that are compiled into the library binary at build time
174    /// from `src/builtin_rules.magic`. These rules provide high-confidence detection
175    /// for common file types including executables (ELF, PE/DOS), archives (ZIP, TAR,
176    /// GZIP), images (JPEG, PNG, GIF, BMP), and documents (PDF).
177    ///
178    /// # Security
179    ///
180    /// This constructor uses [`EvaluationConfig::default()`], which leaves
181    /// `timeout_ms` unset (unbounded). When processing untrusted input
182    /// (adversarial file buffers, large uploads, etc.), prefer
183    /// [`MagicDatabase::with_builtin_rules_and_config`] with
184    /// [`EvaluationConfig::performance()`] (which sets a 1-second timeout)
185    /// or construct a config explicitly with a non-`None` timeout sized
186    /// for your workload. The `Default` impl intentionally targets CLI
187    /// one-shot usage rather than long-running services.
188    ///
189    /// # Thread safety
190    ///
191    /// `MagicDatabase` is `Send + Sync` and holds no interior mutability,
192    /// so an `Arc<MagicDatabase>` can be shared across threads for
193    /// parallel file scanning. A fresh evaluation context is constructed
194    /// per `evaluate_buffer` / `evaluate_file` call, so concurrent calls
195    /// do not interfere.
196    ///
197    /// # Errors
198    ///
199    /// Currently always returns `Ok`. In future implementations, this may return
200    /// an error if the built-in rules fail to load or validate.
201    ///
202    /// # Examples
203    ///
204    /// ```rust,no_run
205    /// use libmagic_rs::MagicDatabase;
206    ///
207    /// let db = MagicDatabase::with_builtin_rules()?;
208    /// let result = db.evaluate_buffer(b"\x7fELF")?;
209    /// // Returns actual file type detection (e.g., "ELF")
210    /// # Ok::<(), Box<dyn std::error::Error>>(())
211    /// ```
212    pub fn with_builtin_rules() -> Result<Self> {
213        Self::with_builtin_rules_and_config(EvaluationConfig::default())
214    }
215
216    /// Create database with built-in rules and custom configuration.
217    ///
218    /// Loads built-in magic rules compiled at build time and applies the specified
219    /// evaluation configuration (e.g., custom timeout settings).
220    ///
221    /// # Security
222    ///
223    /// For untrusted input (adversarial file buffers, web uploads, mail
224    /// scanning), pass a config with an explicit timeout such as
225    /// [`EvaluationConfig::performance()`]. The default config has
226    /// `timeout_ms = None` which leaves evaluation unbounded; see the
227    /// rationale on [`EvaluationConfig::default`].
228    ///
229    /// # Arguments
230    ///
231    /// * `config` - Custom evaluation configuration to use with the built-in rules
232    ///
233    /// # Errors
234    ///
235    /// Returns `LibmagicError` if the configuration is invalid (e.g., timeout is zero).
236    ///
237    /// # Examples
238    ///
239    /// ```rust,no_run
240    /// use libmagic_rs::{MagicDatabase, EvaluationConfig};
241    ///
242    /// // Prefer the performance() preset over default() when processing
243    /// // untrusted input. default() has no timeout by design.
244    /// let config = EvaluationConfig::performance();
245    /// let db = MagicDatabase::with_builtin_rules_and_config(config)?;
246    /// # Ok::<(), Box<dyn std::error::Error>>(())
247    /// ```
248    pub fn with_builtin_rules_and_config(config: EvaluationConfig) -> Result<Self> {
249        config.validate()?;
250        let mut rules = crate::builtin_rules::get_builtin_rules();
251        crate::evaluator::strength::sort_rules_by_strength_recursive(&mut rules);
252        let root_rules: std::sync::Arc<[MagicRule]> =
253            std::sync::Arc::from(rules.into_boxed_slice());
254        Ok(Self {
255            name_table: std::sync::Arc::new(crate::parser::name_table::NameTable::empty()),
256            root_rules,
257            config,
258            source_path: None,
259            mime_mapper: mime::MimeMapper::new(),
260        })
261    }
262
263    /// Load magic rules from a file
264    ///
265    /// # Security
266    ///
267    /// This constructor uses [`EvaluationConfig::default()`], which
268    /// leaves `timeout_ms` unset. See the security note on
269    /// [`Self::with_builtin_rules`] for the implications and prefer
270    /// [`Self::load_from_file_with_config`] with an explicit timeout
271    /// when processing untrusted input.
272    ///
273    /// # Arguments
274    ///
275    /// * `path` - Path to the magic file to load
276    ///
277    /// # Errors
278    ///
279    /// Returns `LibmagicError::IoError` if the file cannot be read.
280    /// Returns `LibmagicError::ParseError` if the magic file format is invalid.
281    ///
282    /// # Examples
283    ///
284    /// ```rust,no_run
285    /// use libmagic_rs::MagicDatabase;
286    ///
287    /// let db = MagicDatabase::load_from_file("magic.db")?;
288    /// # Ok::<(), Box<dyn std::error::Error>>(())
289    /// ```
290    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
291        Self::load_from_file_with_config(path, EvaluationConfig::default())
292    }
293
294    /// Load from file with custom config (e.g., timeout).
295    ///
296    /// # Security
297    ///
298    /// For untrusted input, pass [`EvaluationConfig::performance()`] or
299    /// a config with an explicit non-`None` `timeout_ms`. See
300    /// [`Self::with_builtin_rules`] for the full rationale.
301    ///
302    /// # Errors
303    ///
304    /// Returns error if file cannot be read, parsed, or config is invalid
305    pub fn load_from_file_with_config<P: AsRef<Path>>(
306        path: P,
307        config: EvaluationConfig,
308    ) -> Result<Self> {
309        config.validate()?;
310        let parsed = parser::load_magic_file(path.as_ref()).map_err(|e| match e {
311            ParseError::IoError(io_err) => LibmagicError::IoError(io_err),
312            other => LibmagicError::ParseError(other),
313        })?;
314        let parser::ParsedMagic {
315            mut rules,
316            mut name_table,
317        } = parsed;
318        crate::evaluator::strength::sort_rules_by_strength_recursive(&mut rules);
319        // Each named subroutine body must be sorted by the same strength
320        // ordering so evaluation of a `use` site is deterministic and
321        // matches the ordering applied to top-level rules.
322        name_table.sort_subroutines(|rules| {
323            crate::evaluator::strength::sort_rules_by_strength_recursive(rules);
324        });
325
326        let root_rules: std::sync::Arc<[MagicRule]> =
327            std::sync::Arc::from(rules.into_boxed_slice());
328        Ok(Self {
329            name_table: std::sync::Arc::new(name_table),
330            root_rules,
331            config,
332            source_path: Some(path.as_ref().to_path_buf()),
333            mime_mapper: mime::MimeMapper::new(),
334        })
335    }
336
337    /// Evaluate magic rules against a file
338    ///
339    /// # Arguments
340    ///
341    /// * `path` - Path to the file to evaluate
342    ///
343    /// # Errors
344    ///
345    /// Returns `LibmagicError::IoError` if the file cannot be accessed.
346    /// Returns `LibmagicError::EvaluationError` if rule evaluation fails.
347    ///
348    /// # Security
349    ///
350    /// This method has a time-of-check/time-of-use (TOCTOU) window between
351    /// resolving the path and memory-mapping the file
352    /// ([CWE-367](https://cwe.mitre.org/data/definitions/367.html)).
353    /// Internally, `evaluate_file` first calls `std::fs::metadata(path)` to
354    /// detect the empty-file case, then opens and memory-maps the file via
355    /// [`io::FileBuffer::new`], which itself re-validates file metadata
356    /// (regular file, size bounds) before calling `create_memory_mapping`.
357    /// Between these validation steps and the final `mmap` call, the path
358    /// may be swapped (for example, via a symlink replacement or rename)
359    /// by another process. The content that gets mapped may therefore
360    /// differ from the file that passed validation.
361    ///
362    /// The I/O layer mitigates the common shapes of this attack by
363    /// canonicalizing the path and rejecting special file types, and the
364    /// mapping itself is read-only, so a successful exploit cannot corrupt
365    /// the victim file. The residual risk is that `evaluate_file` may
366    /// classify a different file than the caller intended.
367    ///
368    /// **For adversarial or untrusted environments, prefer
369    /// [`MagicDatabase::evaluate_buffer`]**: load the bytes yourself using
370    /// whatever resource-bounded, TOCTOU-aware I/O strategy your
371    /// application requires (e.g., `openat` with `O_NOFOLLOW`, holding an
372    /// open file descriptor across validation and read), then pass the
373    /// in-memory slice directly to `evaluate_buffer`. See
374    /// [the security assurance case](https://evilbit-labs.github.io/libmagic-rs/security-assurance.html)
375    /// for the residual-risk discussion.
376    ///
377    /// # Examples
378    ///
379    /// ```rust,no_run
380    /// use libmagic_rs::MagicDatabase;
381    ///
382    /// let db = MagicDatabase::load_from_file("magic.db")?;
383    /// let result = db.evaluate_file("sample.bin")?;
384    /// println!("File type: {}", result.description);
385    /// # Ok::<(), Box<dyn std::error::Error>>(())
386    /// ```
387    pub fn evaluate_file<P: AsRef<Path>>(&self, path: P) -> Result<EvaluationResult> {
388        use crate::io::FileBuffer;
389        use std::fs;
390        use std::time::Instant;
391
392        let start_time = Instant::now();
393        let path = path.as_ref();
394
395        // Check if file is empty - if so, evaluate as empty buffer
396        // This allows empty files to be processed like any other file
397        let file_metadata = fs::metadata(path)?;
398        let file_size = file_metadata.len();
399
400        if file_size == 0 {
401            // Empty file - evaluate as empty buffer but preserve file metadata
402            let mut result = self.evaluate_buffer_internal(b"", start_time)?;
403            result.metadata.file_size = 0;
404            result.metadata.magic_file.clone_from(&self.source_path);
405            return Ok(result);
406        }
407
408        // Load the file into memory. Reuse the metadata we just read instead
409        // of having FileBuffer::new call canonicalize+metadata again.
410        let file_buffer = FileBuffer::from_path_and_metadata(path, &file_metadata)?;
411        let buffer = file_buffer.as_slice();
412
413        // Route the evaluation through `evaluate_buffer_internal` so the
414        // rule environment (name table + root rules) is attached to the
415        // context identically for in-memory and on-disk paths.
416        let mut result = self.evaluate_buffer_internal(buffer, start_time)?;
417        result.metadata.file_size = file_size;
418        Ok(result)
419    }
420
421    /// Evaluate magic rules against an in-memory buffer
422    ///
423    /// This method evaluates a byte buffer directly without reading from disk,
424    /// which is useful for stdin input or pre-loaded data.
425    ///
426    /// # Arguments
427    ///
428    /// * `buffer` - Byte buffer to evaluate
429    ///
430    /// # Errors
431    ///
432    /// Returns `LibmagicError::EvaluationError` if rule evaluation fails.
433    ///
434    /// # Examples
435    ///
436    /// ```rust,no_run
437    /// use libmagic_rs::MagicDatabase;
438    ///
439    /// let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
440    /// let buffer = b"test data";
441    /// let result = db.evaluate_buffer(buffer)?;
442    /// println!("Buffer type: {}", result.description);
443    /// # Ok::<(), Box<dyn std::error::Error>>(())
444    /// ```
445    pub fn evaluate_buffer(&self, buffer: &[u8]) -> Result<EvaluationResult> {
446        use std::time::Instant;
447        self.evaluate_buffer_internal(buffer, Instant::now())
448    }
449
450    /// Internal buffer evaluation with externally provided start time
451    fn evaluate_buffer_internal(
452        &self,
453        buffer: &[u8],
454        start_time: std::time::Instant,
455    ) -> Result<EvaluationResult> {
456        use crate::evaluator::{EvaluationContext, RuleEnvironment, evaluate_rules};
457
458        let file_size = buffer.len() as u64;
459
460        // Validate config once at the entry point to match the previous
461        // behavior of `evaluate_rules_with_config`.
462        self.config.validate()?;
463
464        // Reset the thread-local regex compile cache so it is bounded to
465        // the lifetime of a single top-level evaluation call.
466        crate::evaluator::types::regex::reset_regex_cache();
467
468        let env = std::sync::Arc::new(RuleEnvironment {
469            name_table: self.name_table.clone(),
470            root_rules: self.root_rules.clone(),
471        });
472
473        let mut context = EvaluationContext::new(self.config.clone()).with_rule_env(env);
474
475        // `evaluate_rules` returns `Ok(vec![])` for an empty rule list,
476        // so no `is_empty()` guard is needed here.
477        let matches = evaluate_rules(&self.root_rules, buffer, &mut context)?;
478
479        Ok(self.build_result(matches, file_size, start_time))
480    }
481
482    /// Build an `EvaluationResult` from match results, file size, and start time.
483    ///
484    /// This is shared between `evaluate_file` and `evaluate_buffer_internal` to
485    /// avoid duplicating the result-construction logic.
486    fn build_result(
487        &self,
488        matches: Vec<evaluator::RuleMatch>,
489        file_size: u64,
490        start_time: std::time::Instant,
491    ) -> EvaluationResult {
492        let (description, confidence) = if matches.is_empty() {
493            ("data".to_string(), 0.0)
494        } else {
495            (
496                Self::concatenate_messages(&matches),
497                matches.first().map_or(0.0, |m| m.confidence),
498            )
499        };
500
501        let mime_type = if self.config.enable_mime_types {
502            self.mime_mapper
503                .get_mime_type(&description)
504                .map(String::from)
505        } else {
506            None
507        };
508
509        EvaluationResult {
510            description,
511            mime_type,
512            confidence,
513            matches,
514            metadata: EvaluationMetadata {
515                file_size,
516                evaluation_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
517                rules_evaluated: self.root_rules.len(),
518                magic_file: self.source_path.clone(),
519                timed_out: false,
520            },
521        }
522    }
523
524    /// Concatenate match messages following libmagic behavior
525    ///
526    /// Each match's `message` is first run through
527    /// [`crate::output::format::format_magic_message`], which substitutes
528    /// printf-style specifiers (`%lld`, `%02x`, `%s`, etc.) with the
529    /// rule's read value. The resulting rendered strings are then joined
530    /// with spaces, except when a rendered string starts with the
531    /// backspace character (`\b`, U+0008) which suppresses both the
532    /// separating space and the backspace itself (GOTCHAS.md S14.1).
533    ///
534    /// The backspace check runs on the *post-substitution* text so rules
535    /// like `\b, version %s` compose correctly once the specifier has been
536    /// rendered.
537    fn concatenate_messages(matches: &[evaluator::RuleMatch]) -> String {
538        use crate::output::format::format_magic_message;
539
540        let capacity: usize = matches.iter().map(|m| m.message.len() + 1).sum();
541        let mut result = String::with_capacity(capacity);
542        for m in matches {
543            let rendered = format_magic_message(&m.message, &m.value, &m.type_kind);
544            if let Some(rest) = rendered.strip_prefix('\u{0008}') {
545                // Backspace suppresses the space and the character itself
546                result.push_str(rest);
547            } else if !result.is_empty() {
548                result.push(' ');
549                result.push_str(&rendered);
550            } else {
551                result.push_str(&rendered);
552            }
553        }
554        result
555    }
556
557    /// Returns the evaluation configuration used by this database.
558    ///
559    /// This provides read-only access to the evaluation configuration for
560    /// callers that need to inspect resource limits or evaluation options.
561    #[must_use]
562    pub fn config(&self) -> &EvaluationConfig {
563        &self.config
564    }
565
566    /// Returns the path from which magic rules were loaded.
567    ///
568    /// This method returns the source path that was used to load the magic rules
569    /// into this database. It is useful for debugging, logging, and tracking the
570    /// origin of magic rules.
571    ///
572    /// # Returns
573    ///
574    /// - `Some(&Path)` - If the database was loaded from a file or directory using
575    ///   [`load_from_file()`](Self::load_from_file)
576    /// - `None` - If the database was constructed programmatically or the source
577    ///   path was not recorded
578    ///
579    /// # Examples
580    ///
581    /// ```rust,no_run
582    /// use libmagic_rs::MagicDatabase;
583    ///
584    /// let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
585    /// if let Some(path) = db.source_path() {
586    ///     println!("Rules loaded from: {}", path.display());
587    /// }
588    /// # Ok::<(), Box<dyn std::error::Error>>(())
589    /// ```
590    #[must_use]
591    pub fn source_path(&self) -> Option<&Path> {
592        self.source_path.as_deref()
593    }
594}
595
596/// Metadata about the evaluation process
597///
598/// Contains diagnostic information about how the evaluation was performed,
599/// including performance metrics and statistics about rule processing.
600///
601/// # Examples
602///
603/// ```
604/// use libmagic_rs::EvaluationMetadata;
605/// use std::path::PathBuf;
606///
607/// let metadata = EvaluationMetadata {
608///     file_size: 8192,
609///     evaluation_time_ms: 2.5,
610///     rules_evaluated: 42,
611///     magic_file: Some(PathBuf::from("/usr/share/misc/magic")),
612///     timed_out: false,
613/// };
614///
615/// assert_eq!(metadata.file_size, 8192);
616/// assert!(!metadata.timed_out);
617/// ```
618#[derive(Debug, Clone, Serialize, Deserialize)]
619pub struct EvaluationMetadata {
620    /// Size of the analyzed file or buffer in bytes
621    pub file_size: u64,
622    /// Time taken to evaluate rules in milliseconds
623    pub evaluation_time_ms: f64,
624    /// Number of top-level rules that were evaluated
625    pub rules_evaluated: usize,
626    /// Path to the magic file used, or None for built-in rules
627    #[serde(skip_serializing_if = "Option::is_none", default)]
628    pub magic_file: Option<PathBuf>,
629    /// Whether evaluation was stopped due to timeout
630    pub timed_out: bool,
631}
632
633impl Default for EvaluationMetadata {
634    fn default() -> Self {
635        Self {
636            file_size: 0,
637            evaluation_time_ms: 0.0,
638            rules_evaluated: 0,
639            magic_file: None,
640            timed_out: false,
641        }
642    }
643}
644
645/// Result of magic rule evaluation
646///
647/// Contains the file type description, optional MIME type, confidence score,
648/// individual match details, and evaluation metadata.
649///
650/// # Relationship to [`crate::output::EvaluationResult`]
651///
652/// This is the **library-facing** result type returned by [`MagicDatabase::evaluate_file`]
653/// and [`MagicDatabase::evaluate_buffer`].
654/// It carries a rolled-up description, MIME type, and confidence score along with
655/// raw [`evaluator::RuleMatch`] values. It intentionally does **not** carry the
656/// analyzed filename or a surface-level error string, because those are caller
657/// concerns (a caller may evaluate an in-memory buffer that has no filename).
658///
659/// The parallel type [`crate::output::EvaluationResult`] is the **output-facing**
660/// result used by the CLI and JSON/text formatters. It adds `filename` and
661/// `error`, carries enriched [`crate::output::MatchResult`] values (with
662/// extracted tags), and uses `u32` counters in its metadata to match the JSON
663/// output schema.
664///
665/// The two types are **intentionally distinct** — do not try to unify them.
666/// Convert library → output explicitly via
667/// [`crate::output::EvaluationResult::from_library_result`], which is the single
668/// named conversion point. Any drift between the two hierarchies should be
669/// resolved there, not by back-channel field copying in call sites.
670///
671/// # Examples
672///
673/// ```
674/// use libmagic_rs::{EvaluationResult, EvaluationMetadata};
675///
676/// let result = EvaluationResult {
677///     description: "ELF 64-bit executable".to_string(),
678///     mime_type: Some("application/x-executable".to_string()),
679///     confidence: 0.9,
680///     matches: vec![],
681///     metadata: EvaluationMetadata::default(),
682/// };
683///
684/// assert_eq!(result.description, "ELF 64-bit executable");
685/// assert!(result.confidence > 0.5);
686/// ```
687#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct EvaluationResult {
689    /// Human-readable file type description
690    ///
691    /// This is the concatenated message from all matching rules,
692    /// following libmagic behavior where hierarchical matches
693    /// are joined with spaces (unless backspace character is used).
694    pub description: String,
695    /// Optional MIME type for the detected file type
696    ///
697    /// Only populated when `enable_mime_types` is set in the configuration.
698    /// Omitted from the serialized form when unset (rather than emitted
699    /// as `"mime_type": null`) so downstream JSON consumers can treat
700    /// presence as the "MIME type is known" indicator.
701    #[serde(skip_serializing_if = "Option::is_none", default)]
702    pub mime_type: Option<String>,
703    /// Confidence score (0.0 to 1.0)
704    ///
705    /// Based on the depth of the match in the rule hierarchy.
706    /// Deeper matches indicate more specific identification.
707    pub confidence: f64,
708    /// Individual match results from rule evaluation
709    ///
710    /// Contains details about each rule that matched, including
711    /// offset, matched value, and per-match confidence.
712    pub matches: Vec<evaluator::RuleMatch>,
713    /// Metadata about the evaluation process
714    pub metadata: EvaluationMetadata,
715}
716
717#[cfg(test)]
718mod tests;