libmagic_rs/lib.rs
1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Rust Libmagic - A pure-Rust implementation of libmagic
5//!
6//! This library provides safe, efficient file type identification through magic rule evaluation.
7//! It parses magic files into an Abstract Syntax Tree (AST) and evaluates them against file
8//! buffers using memory-mapped I/O for optimal performance.
9//!
10//! # Security Features
11//!
12//! This implementation prioritizes security through:
13//! - **Memory Safety**: Pure Rust with no unsafe code (except in vetted dependencies)
14//! - **Bounds Checking**: Comprehensive validation of all buffer accesses
15//! - **Resource Limits**: Configurable limits to prevent resource exhaustion attacks
16//! - **Input Validation**: Strict validation of magic files and configuration
17//! - **Error Handling**: Secure error messages that don't leak sensitive information
18//! - **Timeout Protection**: Configurable timeouts to prevent denial of service
19//!
20//! # Examples
21//!
22//! ## Complete Workflow: Load → Evaluate → Output
23//!
24//! ```rust,no_run
25//! use libmagic_rs::MagicDatabase;
26//!
27//! // Load magic rules from a text file
28//! let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
29//!
30//! // Evaluate a file to determine its type
31//! let result = db.evaluate_file("sample.bin")?;
32//! println!("File type: {}", result.description);
33//!
34//! // Access metadata about loaded rules
35//! if let Some(path) = db.source_path() {
36//! println!("Rules loaded from: {}", path.display());
37//! }
38//! # Ok::<(), Box<dyn std::error::Error>>(())
39//! ```
40//!
41//! ## Loading from a Directory
42//!
43//! ```rust,no_run
44//! use libmagic_rs::MagicDatabase;
45//!
46//! // Load all magic files from a directory (Magdir pattern)
47//! let db = MagicDatabase::load_from_file("/usr/share/misc/magic.d")?;
48//!
49//! // Evaluate multiple files
50//! for file in &["file1.bin", "file2.bin", "file3.bin"] {
51//! let result = db.evaluate_file(file)?;
52//! println!("{}: {}", file, result.description);
53//! }
54//! # Ok::<(), Box<dyn std::error::Error>>(())
55//! ```
56//!
57//! ## Error Handling for Binary Files
58//!
59//! ```rust,no_run
60//! use libmagic_rs::MagicDatabase;
61//!
62//! // Attempt to load a binary .mgc file
63//! match MagicDatabase::load_from_file("/usr/share/misc/magic.mgc") {
64//! Ok(db) => {
65//! let result = db.evaluate_file("sample.bin")?;
66//! println!("File type: {}", result.description);
67//! }
68//! Err(e) => {
69//! eprintln!("Error loading magic file: {}", e);
70//! eprintln!("Hint: Binary .mgc files are not supported.");
71//! eprintln!("Use --use-builtin option to use built-in rules,");
72//! eprintln!("or provide a text-based magic file or directory.");
73//! }
74//! }
75//! # Ok::<(), Box<dyn std::error::Error>>(())
76//! ```
77//!
78//! ## Debugging with Source Path Metadata
79//!
80//! ```rust,no_run
81//! use libmagic_rs::MagicDatabase;
82//!
83//! let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
84//!
85//! // Use source_path() for debugging and logging
86//! if let Some(source) = db.source_path() {
87//! println!("Loaded {} from {}",
88//! "magic rules",
89//! source.display());
90//! }
91//!
92//! // Evaluate files with source tracking
93//! let result = db.evaluate_file("sample.bin")?;
94//! println!("Detection result: {}", result.description);
95//! # Ok::<(), Box<dyn std::error::Error>>(())
96//! ```
97
98#![deny(missing_docs)]
99#![deny(unsafe_code)]
100#![deny(clippy::all)]
101#![warn(clippy::pedantic)]
102
103use std::path::{Path, PathBuf};
104
105use serde::Serialize;
106
107// Re-export modules
108pub mod builtin_rules;
109mod config;
110pub mod error;
111pub mod evaluator;
112pub mod io;
113pub mod mime;
114pub mod output;
115pub mod parser;
116pub mod tags;
117
118pub use config::EvaluationConfig;
119
120/// Build-time helpers for compiling magic rules.
121///
122/// This module contains functionality used by the build script to parse magic files
123/// and generate Rust code for built-in rules. It is only available during tests and
124/// documentation builds to enable comprehensive testing of the build process.
125#[cfg(any(test, doc))]
126pub mod build_helpers;
127
128// Re-export core AST types for convenience
129pub use parser::ast::{
130 Endianness, MagicRule, OffsetSpec, Operator, PStringLengthWidth, StrengthModifier, TypeKind,
131 Value,
132};
133
134// Re-export evaluator types for convenience
135pub use evaluator::{EvaluationContext, RuleMatch};
136
137// Re-export error types for convenience
138pub use error::{EvaluationError, LibmagicError, ParseError};
139
140/// Result type for library operations
141pub type Result<T> = std::result::Result<T, LibmagicError>;
142
143impl From<crate::io::IoError> for LibmagicError {
144 fn from(err: crate::io::IoError) -> Self {
145 // Preserve the structured error message (includes path and operation context)
146 LibmagicError::FileError(err.to_string())
147 }
148}
149
150/// Main interface for magic rule database
151#[derive(Debug)]
152#[non_exhaustive]
153pub struct MagicDatabase {
154 /// Named subroutine definitions extracted from magic file `name` rules,
155 /// keyed by identifier. The evaluator consults this table when a rule of
156 /// type `TypeKind::Meta(MetaType::Use(name))` is reached.
157 name_table: std::sync::Arc<crate::parser::name_table::NameTable>,
158 /// Top-level rules as a shared immutable slice. This is the primary rule
159 /// storage for the database. Passed through the evaluation context as part
160 /// of the rule environment so whole-database operations (e.g. `indirect`)
161 /// can re-enter at the root without re-sorting or cloning the rule tree.
162 root_rules: std::sync::Arc<[MagicRule]>,
163 config: EvaluationConfig,
164 /// Optional path to the source magic file or directory from which rules were loaded.
165 /// This is used for debugging and logging purposes.
166 source_path: Option<PathBuf>,
167 /// Cached MIME type mapper to avoid rebuilding the lookup table on every evaluation
168 mime_mapper: mime::MimeMapper,
169}
170
171impl MagicDatabase {
172 /// Create a database using built-in magic rules.
173 ///
174 /// Loads magic rules that are compiled into the library binary at build time
175 /// from `src/builtin_rules.magic`. These rules provide high-confidence detection
176 /// for common file types including executables (ELF, PE/DOS), archives (ZIP, TAR,
177 /// GZIP), images (JPEG, PNG, GIF, BMP), and documents (PDF).
178 ///
179 /// # Security
180 ///
181 /// This constructor uses [`EvaluationConfig::default()`], which leaves
182 /// `timeout_ms` unset (unbounded). When processing untrusted input
183 /// (adversarial file buffers, large uploads, etc.), prefer
184 /// [`MagicDatabase::with_builtin_rules_and_config`] with
185 /// [`EvaluationConfig::performance()`] (which sets a 1-second timeout)
186 /// or construct a config explicitly with a non-`None` timeout sized
187 /// for your workload. The `Default` impl intentionally targets CLI
188 /// one-shot usage rather than long-running services.
189 ///
190 /// # Thread safety
191 ///
192 /// `MagicDatabase` is `Send + Sync` and holds no interior mutability,
193 /// so an `Arc<MagicDatabase>` can be shared across threads for
194 /// parallel file scanning. A fresh evaluation context is constructed
195 /// per `evaluate_buffer` / `evaluate_file` call, so concurrent calls
196 /// do not interfere.
197 ///
198 /// # Errors
199 ///
200 /// Currently always returns `Ok`. In future implementations, this may return
201 /// an error if the built-in rules fail to load or validate.
202 ///
203 /// # Examples
204 ///
205 /// ```rust,no_run
206 /// use libmagic_rs::MagicDatabase;
207 ///
208 /// let db = MagicDatabase::with_builtin_rules()?;
209 /// let result = db.evaluate_buffer(b"\x7fELF")?;
210 /// // Returns actual file type detection (e.g., "ELF")
211 /// # Ok::<(), Box<dyn std::error::Error>>(())
212 /// ```
213 pub fn with_builtin_rules() -> Result<Self> {
214 Self::with_builtin_rules_and_config(EvaluationConfig::default())
215 }
216
217 /// Create database with built-in rules and custom configuration.
218 ///
219 /// Loads built-in magic rules compiled at build time and applies the specified
220 /// evaluation configuration (e.g., custom timeout settings).
221 ///
222 /// # Security
223 ///
224 /// For untrusted input (adversarial file buffers, web uploads, mail
225 /// scanning), pass a config with an explicit timeout such as
226 /// [`EvaluationConfig::performance()`]. The default config has
227 /// `timeout_ms = None` which leaves evaluation unbounded; see the
228 /// rationale on [`EvaluationConfig::default`].
229 ///
230 /// # Arguments
231 ///
232 /// * `config` - Custom evaluation configuration to use with the built-in rules
233 ///
234 /// # Errors
235 ///
236 /// Returns `LibmagicError` if the configuration is invalid (e.g., timeout is zero).
237 ///
238 /// # Examples
239 ///
240 /// ```rust,no_run
241 /// use libmagic_rs::{MagicDatabase, EvaluationConfig};
242 ///
243 /// // Prefer the performance() preset over default() when processing
244 /// // untrusted input. default() has no timeout by design.
245 /// let config = EvaluationConfig::performance();
246 /// let db = MagicDatabase::with_builtin_rules_and_config(config)?;
247 /// # Ok::<(), Box<dyn std::error::Error>>(())
248 /// ```
249 pub fn with_builtin_rules_and_config(config: EvaluationConfig) -> Result<Self> {
250 config.validate()?;
251 let mut rules = crate::builtin_rules::get_builtin_rules();
252 crate::evaluator::strength::sort_rules_by_strength_recursive(&mut rules);
253 let root_rules: std::sync::Arc<[MagicRule]> =
254 std::sync::Arc::from(rules.into_boxed_slice());
255 Ok(Self {
256 name_table: std::sync::Arc::new(crate::parser::name_table::NameTable::empty()),
257 root_rules,
258 config,
259 source_path: None,
260 mime_mapper: mime::MimeMapper::new(),
261 })
262 }
263
264 /// Load magic rules from a file
265 ///
266 /// # Security
267 ///
268 /// This constructor uses [`EvaluationConfig::default()`], which
269 /// leaves `timeout_ms` unset. See the security note on
270 /// [`Self::with_builtin_rules`] for the implications and prefer
271 /// [`Self::load_from_file_with_config`] with an explicit timeout
272 /// when processing untrusted input.
273 ///
274 /// # Arguments
275 ///
276 /// * `path` - Path to the magic file to load
277 ///
278 /// # Errors
279 ///
280 /// Returns `LibmagicError::IoError` if the file cannot be read.
281 /// Returns `LibmagicError::ParseError` if the magic file format is invalid.
282 ///
283 /// # Examples
284 ///
285 /// ```rust,no_run
286 /// use libmagic_rs::MagicDatabase;
287 ///
288 /// let db = MagicDatabase::load_from_file("magic.db")?;
289 /// # Ok::<(), Box<dyn std::error::Error>>(())
290 /// ```
291 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
292 Self::load_from_file_with_config(path, EvaluationConfig::default())
293 }
294
295 /// Load from file with custom config (e.g., timeout).
296 ///
297 /// # Security
298 ///
299 /// For untrusted input, pass [`EvaluationConfig::performance()`] or
300 /// a config with an explicit non-`None` `timeout_ms`. See
301 /// [`Self::with_builtin_rules`] for the full rationale.
302 ///
303 /// # Errors
304 ///
305 /// Returns error if file cannot be read, parsed, or config is invalid
306 pub fn load_from_file_with_config<P: AsRef<Path>>(
307 path: P,
308 config: EvaluationConfig,
309 ) -> Result<Self> {
310 config.validate()?;
311 let parsed = parser::load_magic_file(path.as_ref()).map_err(|e| match e {
312 ParseError::IoError(io_err) => LibmagicError::IoError(io_err),
313 other => LibmagicError::ParseError(other),
314 })?;
315 let parser::ParsedMagic {
316 mut rules,
317 mut name_table,
318 } = parsed;
319 crate::evaluator::strength::sort_rules_by_strength_recursive(&mut rules);
320 // Each named subroutine body must be sorted by the same strength
321 // ordering so evaluation of a `use` site is deterministic and
322 // matches the ordering applied to top-level rules.
323 name_table.sort_subroutines(|rules| {
324 crate::evaluator::strength::sort_rules_by_strength_recursive(rules);
325 });
326
327 let root_rules: std::sync::Arc<[MagicRule]> =
328 std::sync::Arc::from(rules.into_boxed_slice());
329 Ok(Self {
330 name_table: std::sync::Arc::new(name_table),
331 root_rules,
332 config,
333 source_path: Some(path.as_ref().to_path_buf()),
334 mime_mapper: mime::MimeMapper::new(),
335 })
336 }
337
338 /// Evaluate magic rules against a file
339 ///
340 /// # Arguments
341 ///
342 /// * `path` - Path to the file to evaluate
343 ///
344 /// # Errors
345 ///
346 /// Returns `LibmagicError::IoError` if the file cannot be accessed.
347 /// Returns `LibmagicError::EvaluationError` if rule evaluation fails.
348 ///
349 /// # Security
350 ///
351 /// This method has a time-of-check/time-of-use (TOCTOU) window between
352 /// resolving the path and memory-mapping the file
353 /// ([CWE-367](https://cwe.mitre.org/data/definitions/367.html)).
354 /// Internally, `evaluate_file` first calls `std::fs::metadata(path)` to
355 /// detect the empty-file case, then opens and memory-maps the file via
356 /// [`io::FileBuffer::new`], which itself re-validates file metadata
357 /// (regular file, size bounds) before calling `create_memory_mapping`.
358 /// Between these validation steps and the final `mmap` call, the path
359 /// may be swapped (for example, via a symlink replacement or rename)
360 /// by another process. The content that gets mapped may therefore
361 /// differ from the file that passed validation.
362 ///
363 /// The I/O layer mitigates the common shapes of this attack by
364 /// canonicalizing the path and rejecting special file types, and the
365 /// mapping itself is read-only, so a successful exploit cannot corrupt
366 /// the victim file. The residual risk is that `evaluate_file` may
367 /// classify a different file than the caller intended.
368 ///
369 /// **For adversarial or untrusted environments, prefer
370 /// [`MagicDatabase::evaluate_buffer`]**: load the bytes yourself using
371 /// whatever resource-bounded, TOCTOU-aware I/O strategy your
372 /// application requires (e.g., `openat` with `O_NOFOLLOW`, holding an
373 /// open file descriptor across validation and read), then pass the
374 /// in-memory slice directly to `evaluate_buffer`. See
375 /// [the security assurance case](https://evilbit-labs.github.io/libmagic-rs/security-assurance.html)
376 /// for the residual-risk discussion.
377 ///
378 /// # Examples
379 ///
380 /// ```rust,no_run
381 /// use libmagic_rs::MagicDatabase;
382 ///
383 /// let db = MagicDatabase::load_from_file("magic.db")?;
384 /// let result = db.evaluate_file("sample.bin")?;
385 /// println!("File type: {}", result.description);
386 /// # Ok::<(), Box<dyn std::error::Error>>(())
387 /// ```
388 pub fn evaluate_file<P: AsRef<Path>>(&self, path: P) -> Result<EvaluationResult> {
389 use crate::io::FileBuffer;
390 use std::fs;
391 use std::time::Instant;
392
393 let start_time = Instant::now();
394 let path = path.as_ref();
395
396 // Check if file is empty - if so, evaluate as empty buffer
397 // This allows empty files to be processed like any other file
398 let file_metadata = fs::metadata(path)?;
399 let file_size = file_metadata.len();
400
401 if file_size == 0 {
402 // Empty file - evaluate as empty buffer but preserve file metadata
403 let mut result = self.evaluate_buffer_internal(b"", start_time)?;
404 result.metadata.file_size = 0;
405 result.metadata.magic_file.clone_from(&self.source_path);
406 return Ok(result);
407 }
408
409 // Load the file into memory. Reuse the metadata we just read instead
410 // of having FileBuffer::new call canonicalize+metadata again.
411 let file_buffer = FileBuffer::from_path_and_metadata(path, &file_metadata)?;
412 let buffer = file_buffer.as_slice();
413
414 // Route the evaluation through `evaluate_buffer_internal` so the
415 // rule environment (name table + root rules) is attached to the
416 // context identically for in-memory and on-disk paths.
417 let mut result = self.evaluate_buffer_internal(buffer, start_time)?;
418 result.metadata.file_size = file_size;
419 Ok(result)
420 }
421
422 /// Evaluate magic rules against an in-memory buffer
423 ///
424 /// This method evaluates a byte buffer directly without reading from disk,
425 /// which is useful for stdin input or pre-loaded data.
426 ///
427 /// # Arguments
428 ///
429 /// * `buffer` - Byte buffer to evaluate
430 ///
431 /// # Errors
432 ///
433 /// Returns `LibmagicError::EvaluationError` if rule evaluation fails.
434 ///
435 /// # Examples
436 ///
437 /// ```rust,no_run
438 /// use libmagic_rs::MagicDatabase;
439 ///
440 /// let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
441 /// let buffer = b"test data";
442 /// let result = db.evaluate_buffer(buffer)?;
443 /// println!("Buffer type: {}", result.description);
444 /// # Ok::<(), Box<dyn std::error::Error>>(())
445 /// ```
446 pub fn evaluate_buffer(&self, buffer: &[u8]) -> Result<EvaluationResult> {
447 use std::time::Instant;
448 self.evaluate_buffer_internal(buffer, Instant::now())
449 }
450
451 /// Internal buffer evaluation with externally provided start time
452 fn evaluate_buffer_internal(
453 &self,
454 buffer: &[u8],
455 start_time: std::time::Instant,
456 ) -> Result<EvaluationResult> {
457 use crate::evaluator::{EvaluationContext, RuleEnvironment, evaluate_rules};
458
459 let file_size = buffer.len() as u64;
460
461 // Validate config once at the entry point to match the previous
462 // behavior of `evaluate_rules_with_config`.
463 self.config.validate()?;
464
465 // Reset the thread-local regex compile cache so it is bounded to
466 // the lifetime of a single top-level evaluation call.
467 crate::evaluator::types::regex::reset_regex_cache();
468
469 let env = std::sync::Arc::new(RuleEnvironment {
470 name_table: self.name_table.clone(),
471 root_rules: self.root_rules.clone(),
472 });
473
474 let mut context = EvaluationContext::new(self.config.clone()).with_rule_env(env);
475
476 // `evaluate_rules` returns `Ok(vec![])` for an empty rule list,
477 // so no `is_empty()` guard is needed here.
478 let matches = evaluate_rules(&self.root_rules, buffer, &mut context)?;
479
480 Ok(self.build_result(matches, file_size, start_time))
481 }
482
483 /// Build an `EvaluationResult` from match results, file size, and start time.
484 ///
485 /// This is shared between `evaluate_file` and `evaluate_buffer_internal` to
486 /// avoid duplicating the result-construction logic.
487 fn build_result(
488 &self,
489 matches: Vec<evaluator::RuleMatch>,
490 file_size: u64,
491 start_time: std::time::Instant,
492 ) -> EvaluationResult {
493 let (description, confidence) = if matches.is_empty() {
494 ("data".to_string(), 0.0)
495 } else {
496 (
497 Self::concatenate_messages(&matches),
498 matches.first().map_or(0.0, |m| m.confidence),
499 )
500 };
501
502 let mime_type = if self.config.enable_mime_types {
503 self.mime_mapper
504 .get_mime_type(&description)
505 .map(String::from)
506 } else {
507 None
508 };
509
510 EvaluationResult {
511 description,
512 mime_type,
513 confidence,
514 matches,
515 metadata: EvaluationMetadata {
516 file_size,
517 evaluation_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
518 rules_evaluated: self.root_rules.len(),
519 magic_file: self.source_path.clone(),
520 timed_out: false,
521 },
522 }
523 }
524
525 /// Concatenate match messages following libmagic behavior
526 ///
527 /// Each match's `message` is first run through
528 /// [`crate::output::format::format_magic_message`], which substitutes
529 /// printf-style specifiers (`%lld`, `%02x`, `%s`, etc.) with the
530 /// rule's read value. The resulting rendered strings are then joined
531 /// with spaces, except when a rendered string starts with the
532 /// backspace character (`\b`, U+0008) which suppresses both the
533 /// separating space and the backspace itself (GOTCHAS.md S14.1).
534 ///
535 /// The backspace check runs on the *post-substitution* text so rules
536 /// like `\b, version %s` compose correctly once the specifier has been
537 /// rendered.
538 fn concatenate_messages(matches: &[evaluator::RuleMatch]) -> String {
539 use crate::output::format::format_magic_message;
540
541 let capacity: usize = matches.iter().map(|m| m.message.len() + 1).sum();
542 let mut result = String::with_capacity(capacity);
543 for m in matches {
544 let rendered = format_magic_message(&m.message, &m.value, &m.type_kind);
545 if let Some(rest) = rendered.strip_prefix('\u{0008}') {
546 // Backspace suppresses the space and the character itself
547 result.push_str(rest);
548 } else if !result.is_empty() {
549 result.push(' ');
550 result.push_str(&rendered);
551 } else {
552 result.push_str(&rendered);
553 }
554 }
555 result
556 }
557
558 /// Returns the evaluation configuration used by this database.
559 ///
560 /// This provides read-only access to the evaluation configuration for
561 /// callers that need to inspect resource limits or evaluation options.
562 #[must_use]
563 pub fn config(&self) -> &EvaluationConfig {
564 &self.config
565 }
566
567 /// Returns the path from which magic rules were loaded.
568 ///
569 /// This method returns the source path that was used to load the magic rules
570 /// into this database. It is useful for debugging, logging, and tracking the
571 /// origin of magic rules.
572 ///
573 /// # Returns
574 ///
575 /// - `Some(&Path)` - If the database was loaded from a file or directory using
576 /// [`load_from_file()`](Self::load_from_file)
577 /// - `None` - If the database was constructed programmatically or the source
578 /// path was not recorded
579 ///
580 /// # Examples
581 ///
582 /// ```rust,no_run
583 /// use libmagic_rs::MagicDatabase;
584 ///
585 /// let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
586 /// if let Some(path) = db.source_path() {
587 /// println!("Rules loaded from: {}", path.display());
588 /// }
589 /// # Ok::<(), Box<dyn std::error::Error>>(())
590 /// ```
591 #[must_use]
592 pub fn source_path(&self) -> Option<&Path> {
593 self.source_path.as_deref()
594 }
595}
596
597/// Metadata about the evaluation process
598///
599/// Contains diagnostic information about how the evaluation was performed,
600/// including performance metrics and statistics about rule processing.
601///
602/// # Examples
603///
604/// ```
605/// use libmagic_rs::EvaluationMetadata;
606/// use std::path::PathBuf;
607///
608/// let metadata = EvaluationMetadata::new(
609/// 8192,
610/// 2.5,
611/// 42,
612/// Some(PathBuf::from("/usr/share/misc/magic")),
613/// false,
614/// );
615///
616/// assert_eq!(metadata.file_size, 8192);
617/// assert!(!metadata.timed_out);
618/// ```
619#[derive(Debug, Clone, Serialize)]
620#[non_exhaustive]
621pub struct EvaluationMetadata {
622 /// Size of the analyzed file or buffer in bytes
623 pub file_size: u64,
624 /// Time taken to evaluate rules in milliseconds
625 pub evaluation_time_ms: f64,
626 /// Number of top-level rules that were evaluated
627 pub rules_evaluated: usize,
628 /// Path to the magic file used, or None for built-in rules
629 #[serde(skip_serializing_if = "Option::is_none", default)]
630 pub magic_file: Option<PathBuf>,
631 /// Whether evaluation was stopped due to timeout
632 pub timed_out: bool,
633}
634
635impl Default for EvaluationMetadata {
636 fn default() -> Self {
637 Self {
638 file_size: 0,
639 evaluation_time_ms: 0.0,
640 rules_evaluated: 0,
641 magic_file: None,
642 timed_out: false,
643 }
644 }
645}
646
647/// Result of magic rule evaluation
648///
649/// Contains the file type description, optional MIME type, confidence score,
650/// individual match details, and evaluation metadata.
651///
652/// # Relationship to [`crate::output::EvaluationResult`]
653///
654/// This is the **library-facing** result type returned by [`MagicDatabase::evaluate_file`]
655/// and [`MagicDatabase::evaluate_buffer`].
656/// It carries a rolled-up description, MIME type, and confidence score along with
657/// raw [`evaluator::RuleMatch`] values. It intentionally does **not** carry the
658/// analyzed filename or a surface-level error string, because those are caller
659/// concerns (a caller may evaluate an in-memory buffer that has no filename).
660///
661/// The parallel type [`crate::output::EvaluationResult`] is the **output-facing**
662/// result used by the CLI and JSON/text formatters. It adds `filename` and
663/// `error`, carries enriched [`crate::output::MatchResult`] values (with
664/// extracted tags), and uses `u32` counters in its metadata to match the JSON
665/// output schema.
666///
667/// The two types are **intentionally distinct** — do not try to unify them.
668/// Convert library → output explicitly via
669/// [`crate::output::EvaluationResult::from_library_result`], which is the single
670/// named conversion point. Any drift between the two hierarchies should be
671/// resolved there, not by back-channel field copying in call sites.
672///
673/// # Examples
674///
675/// ```
676/// use libmagic_rs::{EvaluationResult, EvaluationMetadata};
677///
678/// let result = EvaluationResult::new(
679/// "ELF 64-bit executable".to_string(),
680/// Some("application/x-executable".to_string()),
681/// 0.9,
682/// vec![],
683/// EvaluationMetadata::default(),
684/// );
685///
686/// assert_eq!(result.description, "ELF 64-bit executable");
687/// assert!(result.confidence > 0.5);
688/// ```
689#[derive(Debug, Clone, Serialize)]
690#[non_exhaustive]
691pub struct EvaluationResult {
692 /// Human-readable file type description
693 ///
694 /// This is the concatenated message from all matching rules,
695 /// following libmagic behavior where hierarchical matches
696 /// are joined with spaces (unless backspace character is used).
697 pub description: String,
698 /// Optional MIME type for the detected file type
699 ///
700 /// Only populated when `enable_mime_types` is set in the configuration.
701 /// Omitted from the serialized form when unset (rather than emitted
702 /// as `"mime_type": null`) so downstream JSON consumers can treat
703 /// presence as the "MIME type is known" indicator.
704 #[serde(skip_serializing_if = "Option::is_none", default)]
705 pub mime_type: Option<String>,
706 /// Confidence score (0.0 to 1.0)
707 ///
708 /// Based on the depth of the match in the rule hierarchy.
709 /// Deeper matches indicate more specific identification.
710 pub confidence: f64,
711 /// Individual match results from rule evaluation
712 ///
713 /// Contains details about each rule that matched, including
714 /// offset, matched value, and per-match confidence.
715 pub matches: Vec<evaluator::RuleMatch>,
716 /// Metadata about the evaluation process
717 pub metadata: EvaluationMetadata,
718}
719
720impl EvaluationResult {
721 /// Construct a new library-side `EvaluationResult`.
722 ///
723 /// This is the outbound type returned by [`MagicDatabase::evaluate_file`]
724 /// and [`MagicDatabase::evaluate_buffer`]. For the output-facing
725 /// type used by the CLI and JSON/text formatters, see
726 /// [`crate::output::EvaluationResult::from_library_result`].
727 #[must_use]
728 pub fn new(
729 description: String,
730 mime_type: Option<String>,
731 confidence: f64,
732 matches: Vec<evaluator::RuleMatch>,
733 metadata: EvaluationMetadata,
734 ) -> Self {
735 Self {
736 description,
737 mime_type,
738 confidence,
739 matches,
740 metadata,
741 }
742 }
743}
744
745impl EvaluationMetadata {
746 /// Construct a new library-side `EvaluationMetadata` from the four
747 /// always-set fields. `magic_file` and `timed_out` default to `None`
748 /// / `false`; use struct-update syntax with [`EvaluationMetadata::default()`]
749 /// to set them explicitly.
750 #[must_use]
751 pub fn new(
752 file_size: u64,
753 evaluation_time_ms: f64,
754 rules_evaluated: usize,
755 magic_file: Option<PathBuf>,
756 timed_out: bool,
757 ) -> Self {
758 Self {
759 file_size,
760 evaluation_time_ms,
761 rules_evaluated,
762 magic_file,
763 timed_out,
764 }
765 }
766}
767
768#[cfg(test)]
769mod tests;