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