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