libmagic_rs/lib.rs
1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Rust Libmagic - A pure-Rust implementation of libmagic
5//!
6//! This library provides safe, efficient file type identification through magic rule evaluation.
7//! It parses magic files into an Abstract Syntax Tree (AST) and evaluates them against file
8//! buffers using memory-mapped I/O for optimal performance.
9//!
10//! # Security Features
11//!
12//! This implementation prioritizes security through:
13//! - **Memory Safety**: Pure Rust with no unsafe code (except in vetted dependencies)
14//! - **Bounds Checking**: Comprehensive validation of all buffer accesses
15//! - **Resource Limits**: Configurable limits to prevent resource exhaustion attacks
16//! - **Input Validation**: Strict validation of magic files and configuration
17//! - **Error Handling**: Secure error messages that don't leak sensitive information
18//! - **Timeout Protection**: Configurable timeouts to prevent denial of service
19//!
20//! # Examples
21//!
22//! ## Complete Workflow: Load → Evaluate → Output
23//!
24//! ```rust,no_run
25//! use libmagic_rs::MagicDatabase;
26//!
27//! // Load magic rules from a text file
28//! let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
29//!
30//! // Evaluate a file to determine its type
31//! let result = db.evaluate_file("sample.bin")?;
32//! println!("File type: {}", result.description);
33//!
34//! // Access metadata about loaded rules
35//! if let Some(path) = db.source_path() {
36//! println!("Rules loaded from: {}", path.display());
37//! }
38//! # Ok::<(), Box<dyn std::error::Error>>(())
39//! ```
40//!
41//! ## Loading from a Directory
42//!
43//! ```rust,no_run
44//! use libmagic_rs::MagicDatabase;
45//!
46//! // Load all magic files from a directory (Magdir pattern)
47//! let db = MagicDatabase::load_from_file("/usr/share/misc/magic.d")?;
48//!
49//! // Evaluate multiple files
50//! for file in &["file1.bin", "file2.bin", "file3.bin"] {
51//! let result = db.evaluate_file(file)?;
52//! println!("{}: {}", file, result.description);
53//! }
54//! # Ok::<(), Box<dyn std::error::Error>>(())
55//! ```
56//!
57//! ## Error Handling for Binary Files
58//!
59//! ```rust,no_run
60//! use libmagic_rs::MagicDatabase;
61//!
62//! // Attempt to load a binary .mgc file
63//! match MagicDatabase::load_from_file("/usr/share/misc/magic.mgc") {
64//! Ok(db) => {
65//! let result = db.evaluate_file("sample.bin")?;
66//! println!("File type: {}", result.description);
67//! }
68//! Err(e) => {
69//! eprintln!("Error loading magic file: {}", e);
70//! eprintln!("Hint: Binary .mgc files are not supported.");
71//! eprintln!("Use --use-builtin option to use built-in rules,");
72//! eprintln!("or provide a text-based magic file or directory.");
73//! }
74//! }
75//! # Ok::<(), Box<dyn std::error::Error>>(())
76//! ```
77//!
78//! ## Debugging with Source Path Metadata
79//!
80//! ```rust,no_run
81//! use libmagic_rs::MagicDatabase;
82//!
83//! let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
84//!
85//! // Use source_path() for debugging and logging
86//! if let Some(source) = db.source_path() {
87//! println!("Loaded {} from {}",
88//! "magic rules",
89//! source.display());
90//! }
91//!
92//! // Evaluate files with source tracking
93//! let result = db.evaluate_file("sample.bin")?;
94//! println!("Detection result: {}", result.description);
95//! # Ok::<(), Box<dyn std::error::Error>>(())
96//! ```
97
98#![deny(missing_docs)]
99#![deny(unsafe_code)]
100#![deny(clippy::all)]
101#![warn(clippy::pedantic)]
102
103use std::path::{Path, PathBuf};
104
105use serde::{Deserialize, Serialize};
106
107// Re-export modules
108pub mod builtin_rules;
109pub mod error;
110pub mod evaluator;
111pub mod io;
112pub mod mime;
113pub mod output;
114pub mod parser;
115pub mod tags;
116
117/// Build-time helpers for compiling magic rules.
118///
119/// This module contains functionality used by the build script to parse magic files
120/// and generate Rust code for built-in rules. It is only available during tests and
121/// documentation builds to enable comprehensive testing of the build process.
122#[cfg(any(test, doc))]
123pub mod build_helpers;
124
125// Re-export core AST types for convenience
126pub use parser::ast::{
127 Endianness, MagicRule, OffsetSpec, Operator, StrengthModifier, TypeKind, Value,
128};
129
130// Re-export evaluator types for convenience
131pub use evaluator::{EvaluationContext, RuleMatch};
132
133// Re-export error types for convenience
134pub use error::{EvaluationError, LibmagicError, ParseError};
135
136/// Result type for library operations
137pub type Result<T> = std::result::Result<T, LibmagicError>;
138
139impl From<crate::io::IoError> for LibmagicError {
140 fn from(err: crate::io::IoError) -> Self {
141 // Preserve the structured error message (includes path and operation context)
142 LibmagicError::FileError(err.to_string())
143 }
144}
145
146/// Configuration for rule evaluation
147///
148/// This struct controls various aspects of magic rule evaluation behavior,
149/// including performance limits, output options, and matching strategies.
150///
151/// # Examples
152///
153/// ```rust
154/// use libmagic_rs::EvaluationConfig;
155///
156/// // Use default configuration
157/// let config = EvaluationConfig::default();
158///
159/// // Create custom configuration
160/// let custom_config = EvaluationConfig {
161/// max_recursion_depth: 10,
162/// max_string_length: 4096,
163/// stop_at_first_match: false, // Get all matches
164/// enable_mime_types: true,
165/// timeout_ms: Some(5000), // 5 second timeout
166/// };
167/// ```
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct EvaluationConfig {
170 /// Maximum recursion depth for nested rules
171 ///
172 /// This prevents infinite recursion in malformed magic files and limits
173 /// the depth of rule hierarchy traversal. Default is 20.
174 pub max_recursion_depth: u32,
175
176 /// Maximum string length to read
177 ///
178 /// This limits the amount of data read for string types to prevent
179 /// excessive memory usage. Default is 8192 bytes.
180 pub max_string_length: usize,
181
182 /// Stop at first match or continue for all matches
183 ///
184 /// When `true`, evaluation stops after the first matching rule.
185 /// When `false`, all rules are evaluated to find all matches.
186 /// Default is `true` for performance.
187 pub stop_at_first_match: bool,
188
189 /// Enable MIME type mapping in results
190 ///
191 /// When `true`, the evaluator will attempt to map file type descriptions
192 /// to standard MIME types. Default is `false`.
193 pub enable_mime_types: bool,
194
195 /// Timeout for evaluation in milliseconds
196 ///
197 /// If set, evaluation will be aborted if it takes longer than this duration.
198 /// `None` means no timeout. Default is `None`.
199 pub timeout_ms: Option<u64>,
200}
201
202impl Default for EvaluationConfig {
203 fn default() -> Self {
204 Self {
205 max_recursion_depth: 20,
206 max_string_length: 8192,
207 stop_at_first_match: true,
208 enable_mime_types: false,
209 timeout_ms: None,
210 }
211 }
212}
213
214impl EvaluationConfig {
215 /// Create a new configuration with default values
216 ///
217 /// # Examples
218 ///
219 /// ```rust
220 /// use libmagic_rs::EvaluationConfig;
221 ///
222 /// let config = EvaluationConfig::new();
223 /// assert_eq!(config.max_recursion_depth, 20);
224 /// assert_eq!(config.max_string_length, 8192);
225 /// assert!(config.stop_at_first_match);
226 /// assert!(!config.enable_mime_types);
227 /// assert_eq!(config.timeout_ms, None);
228 /// ```
229 #[must_use]
230 pub fn new() -> Self {
231 Self::default()
232 }
233
234 /// Create a configuration optimized for performance
235 ///
236 /// This configuration prioritizes speed over completeness:
237 /// - Lower recursion depth limit
238 /// - Smaller string length limit
239 /// - Stop at first match
240 /// - No MIME type mapping
241 /// - Short timeout
242 ///
243 /// # Examples
244 ///
245 /// ```rust
246 /// use libmagic_rs::EvaluationConfig;
247 ///
248 /// let config = EvaluationConfig::performance();
249 /// assert_eq!(config.max_recursion_depth, 10);
250 /// assert_eq!(config.max_string_length, 1024);
251 /// assert!(config.stop_at_first_match);
252 /// assert!(!config.enable_mime_types);
253 /// assert_eq!(config.timeout_ms, Some(1000));
254 /// ```
255 #[must_use]
256 pub const fn performance() -> Self {
257 Self {
258 max_recursion_depth: 10,
259 max_string_length: 1024,
260 stop_at_first_match: true,
261 enable_mime_types: false,
262 timeout_ms: Some(1000), // 1 second
263 }
264 }
265
266 /// Create a configuration optimized for completeness
267 ///
268 /// This configuration prioritizes finding all matches over speed:
269 /// - Higher recursion depth limit
270 /// - Larger string length limit
271 /// - Find all matches
272 /// - Enable MIME type mapping
273 /// - Longer timeout
274 ///
275 /// # Examples
276 ///
277 /// ```rust
278 /// use libmagic_rs::EvaluationConfig;
279 ///
280 /// let config = EvaluationConfig::comprehensive();
281 /// assert_eq!(config.max_recursion_depth, 50);
282 /// assert_eq!(config.max_string_length, 32768);
283 /// assert!(!config.stop_at_first_match);
284 /// assert!(config.enable_mime_types);
285 /// assert_eq!(config.timeout_ms, Some(30000));
286 /// ```
287 #[must_use]
288 pub const fn comprehensive() -> Self {
289 Self {
290 max_recursion_depth: 50,
291 max_string_length: 32768,
292 stop_at_first_match: false,
293 enable_mime_types: true,
294 timeout_ms: Some(30000), // 30 seconds
295 }
296 }
297
298 /// Validate the configuration settings
299 ///
300 /// Performs comprehensive security validation of all configuration values
301 /// to prevent malicious configurations that could lead to resource exhaustion,
302 /// denial of service, or other security issues.
303 ///
304 /// # Security
305 ///
306 /// This validation prevents:
307 /// - Stack overflow attacks through excessive recursion depth
308 /// - Memory exhaustion through oversized string limits
309 /// - Denial of service through excessive timeouts
310 /// - Integer overflow in configuration calculations
311 ///
312 /// # Errors
313 ///
314 /// Returns `LibmagicError::InvalidFormat` if any configuration values
315 /// are invalid or out of reasonable bounds.
316 ///
317 /// # Examples
318 ///
319 /// ```rust
320 /// use libmagic_rs::EvaluationConfig;
321 ///
322 /// let config = EvaluationConfig::default();
323 /// assert!(config.validate().is_ok());
324 ///
325 /// let invalid_config = EvaluationConfig {
326 /// max_recursion_depth: 0, // Invalid: must be > 0
327 /// ..Default::default()
328 /// };
329 /// assert!(invalid_config.validate().is_err());
330 /// ```
331 pub fn validate(&self) -> Result<()> {
332 self.validate_recursion_depth()?;
333 self.validate_string_length()?;
334 self.validate_timeout()?;
335 self.validate_resource_combination()?;
336 Ok(())
337 }
338
339 /// Validate recursion depth to prevent stack overflow attacks
340 fn validate_recursion_depth(&self) -> Result<()> {
341 const MAX_SAFE_RECURSION_DEPTH: u32 = 1000;
342
343 if self.max_recursion_depth == 0 {
344 return Err(LibmagicError::ConfigError {
345 reason: "max_recursion_depth must be greater than 0".to_string(),
346 });
347 }
348
349 if self.max_recursion_depth > MAX_SAFE_RECURSION_DEPTH {
350 return Err(LibmagicError::ConfigError {
351 reason: format!(
352 "max_recursion_depth must not exceed {MAX_SAFE_RECURSION_DEPTH} to prevent stack overflow"
353 ),
354 });
355 }
356
357 Ok(())
358 }
359
360 /// Validate string length to prevent memory exhaustion
361 fn validate_string_length(&self) -> Result<()> {
362 const MAX_SAFE_STRING_LENGTH: usize = 1_048_576; // 1MB
363
364 if self.max_string_length == 0 {
365 return Err(LibmagicError::ConfigError {
366 reason: "max_string_length must be greater than 0".to_string(),
367 });
368 }
369
370 if self.max_string_length > MAX_SAFE_STRING_LENGTH {
371 return Err(LibmagicError::ConfigError {
372 reason: format!(
373 "max_string_length must not exceed {MAX_SAFE_STRING_LENGTH} bytes to prevent memory exhaustion"
374 ),
375 });
376 }
377
378 Ok(())
379 }
380
381 /// Validate timeout to prevent denial of service
382 fn validate_timeout(&self) -> Result<()> {
383 const MAX_SAFE_TIMEOUT_MS: u64 = 300_000; // 5 minutes
384
385 if let Some(timeout) = self.timeout_ms {
386 if timeout == 0 {
387 return Err(LibmagicError::ConfigError {
388 reason: "timeout_ms must be greater than 0 if specified".to_string(),
389 });
390 }
391
392 if timeout > MAX_SAFE_TIMEOUT_MS {
393 return Err(LibmagicError::ConfigError {
394 reason: format!(
395 "timeout_ms must not exceed {MAX_SAFE_TIMEOUT_MS} (5 minutes) to prevent denial of service"
396 ),
397 });
398 }
399 }
400
401 Ok(())
402 }
403
404 /// Validate resource combination to prevent resource exhaustion
405 fn validate_resource_combination(&self) -> Result<()> {
406 const HIGH_RECURSION_THRESHOLD: u32 = 100;
407 const LARGE_STRING_THRESHOLD: usize = 65536;
408
409 if self.max_recursion_depth > HIGH_RECURSION_THRESHOLD
410 && self.max_string_length > LARGE_STRING_THRESHOLD
411 {
412 return Err(LibmagicError::ConfigError {
413 reason: format!(
414 "High recursion depth (>{HIGH_RECURSION_THRESHOLD}) combined with large string length (>{LARGE_STRING_THRESHOLD}) may cause resource exhaustion"
415 ),
416 });
417 }
418
419 Ok(())
420 }
421}
422
423/// Main interface for magic rule database
424#[derive(Debug)]
425pub struct MagicDatabase {
426 rules: Vec<MagicRule>,
427 config: EvaluationConfig,
428 /// Optional path to the source magic file or directory from which rules were loaded.
429 /// This is used for debugging and logging purposes.
430 source_path: Option<PathBuf>,
431 /// Cached MIME type mapper to avoid rebuilding the lookup table on every evaluation
432 mime_mapper: mime::MimeMapper,
433}
434
435impl MagicDatabase {
436 /// Create a database using built-in magic rules.
437 ///
438 /// Loads magic rules that are compiled into the library binary at build time
439 /// from `src/builtin_rules.magic`. These rules provide high-confidence detection
440 /// for common file types including executables (ELF, PE/DOS), archives (ZIP, TAR,
441 /// GZIP), images (JPEG, PNG, GIF, BMP), and documents (PDF).
442 ///
443 /// # Errors
444 ///
445 /// Currently always returns `Ok`. In future implementations, this may return
446 /// an error if the built-in rules fail to load or validate.
447 ///
448 /// # Examples
449 ///
450 /// ```rust,no_run
451 /// use libmagic_rs::MagicDatabase;
452 ///
453 /// let db = MagicDatabase::with_builtin_rules()?;
454 /// let result = db.evaluate_buffer(b"\x7fELF")?;
455 /// // Returns actual file type detection (e.g., "ELF")
456 /// # Ok::<(), Box<dyn std::error::Error>>(())
457 /// ```
458 pub fn with_builtin_rules() -> Result<Self> {
459 Self::with_builtin_rules_and_config(EvaluationConfig::default())
460 }
461
462 /// Create database with built-in rules and custom configuration.
463 ///
464 /// Loads built-in magic rules compiled at build time and applies the specified
465 /// evaluation configuration (e.g., custom timeout settings).
466 ///
467 /// # Arguments
468 ///
469 /// * `config` - Custom evaluation configuration to use with the built-in rules
470 ///
471 /// # Errors
472 ///
473 /// Returns `LibmagicError` if the configuration is invalid (e.g., timeout is zero).
474 ///
475 /// # Examples
476 ///
477 /// ```rust,no_run
478 /// use libmagic_rs::{MagicDatabase, EvaluationConfig};
479 ///
480 /// let config = EvaluationConfig {
481 /// timeout_ms: Some(5000), // 5 second timeout
482 /// ..EvaluationConfig::default()
483 /// };
484 /// let db = MagicDatabase::with_builtin_rules_and_config(config)?;
485 /// # Ok::<(), Box<dyn std::error::Error>>(())
486 /// ```
487 pub fn with_builtin_rules_and_config(config: EvaluationConfig) -> Result<Self> {
488 config.validate()?;
489 Ok(Self {
490 rules: crate::builtin_rules::get_builtin_rules(),
491 config,
492 source_path: None,
493 mime_mapper: mime::MimeMapper::new(),
494 })
495 }
496
497 /// Load magic rules from a file
498 ///
499 /// # Arguments
500 ///
501 /// * `path` - Path to the magic file to load
502 ///
503 /// # Errors
504 ///
505 /// Returns `LibmagicError::IoError` if the file cannot be read.
506 /// Returns `LibmagicError::ParseError` if the magic file format is invalid.
507 ///
508 /// # Examples
509 ///
510 /// ```rust,no_run
511 /// use libmagic_rs::MagicDatabase;
512 ///
513 /// let db = MagicDatabase::load_from_file("magic.db")?;
514 /// # Ok::<(), Box<dyn std::error::Error>>(())
515 /// ```
516 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
517 Self::load_from_file_with_config(path, EvaluationConfig::default())
518 }
519
520 /// Load from file with custom config (e.g., timeout)
521 ///
522 /// # Errors
523 ///
524 /// Returns error if file cannot be read, parsed, or config is invalid
525 pub fn load_from_file_with_config<P: AsRef<Path>>(
526 path: P,
527 config: EvaluationConfig,
528 ) -> Result<Self> {
529 config.validate()?;
530 let rules = parser::load_magic_file(path.as_ref()).map_err(|e| match e {
531 ParseError::IoError(io_err) => LibmagicError::IoError(io_err),
532 other => LibmagicError::ParseError(other),
533 })?;
534
535 Ok(Self {
536 rules,
537 config,
538 source_path: Some(path.as_ref().to_path_buf()),
539 mime_mapper: mime::MimeMapper::new(),
540 })
541 }
542
543 /// Evaluate magic rules against a file
544 ///
545 /// # Arguments
546 ///
547 /// * `path` - Path to the file to evaluate
548 ///
549 /// # Errors
550 ///
551 /// Returns `LibmagicError::IoError` if the file cannot be accessed.
552 /// Returns `LibmagicError::EvaluationError` if rule evaluation fails.
553 ///
554 /// # Examples
555 ///
556 /// ```rust,no_run
557 /// use libmagic_rs::MagicDatabase;
558 ///
559 /// let db = MagicDatabase::load_from_file("magic.db")?;
560 /// let result = db.evaluate_file("sample.bin")?;
561 /// println!("File type: {}", result.description);
562 /// # Ok::<(), Box<dyn std::error::Error>>(())
563 /// ```
564 pub fn evaluate_file<P: AsRef<Path>>(&self, path: P) -> Result<EvaluationResult> {
565 use crate::evaluator::evaluate_rules_with_config;
566 use crate::io::FileBuffer;
567 use std::fs;
568 use std::time::Instant;
569
570 let start_time = Instant::now();
571 let path = path.as_ref();
572
573 // Check if file is empty - if so, evaluate as empty buffer
574 // This allows empty files to be processed like any other file
575 let file_metadata = fs::metadata(path)?;
576 let file_size = file_metadata.len();
577
578 if file_size == 0 {
579 // Empty file - evaluate as empty buffer but preserve file metadata
580 let mut result = self.evaluate_buffer_internal(b"", start_time)?;
581 result.metadata.file_size = 0;
582 result.metadata.magic_file.clone_from(&self.source_path);
583 return Ok(result);
584 }
585
586 // Load the file into memory
587 let file_buffer = FileBuffer::new(path)?;
588 let buffer = file_buffer.as_slice();
589
590 // Evaluate rules against the file buffer (build_result handles empty rules/matches)
591 let matches = if self.rules.is_empty() {
592 vec![]
593 } else {
594 evaluate_rules_with_config(&self.rules, buffer, &self.config)?
595 };
596
597 Ok(self.build_result(matches, file_size, start_time))
598 }
599
600 /// Evaluate magic rules against an in-memory buffer
601 ///
602 /// This method evaluates a byte buffer directly without reading from disk,
603 /// which is useful for stdin input or pre-loaded data.
604 ///
605 /// # Arguments
606 ///
607 /// * `buffer` - Byte buffer to evaluate
608 ///
609 /// # Errors
610 ///
611 /// Returns `LibmagicError::EvaluationError` if rule evaluation fails.
612 ///
613 /// # Examples
614 ///
615 /// ```rust,no_run
616 /// use libmagic_rs::MagicDatabase;
617 ///
618 /// let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
619 /// let buffer = b"test data";
620 /// let result = db.evaluate_buffer(buffer)?;
621 /// println!("Buffer type: {}", result.description);
622 /// # Ok::<(), Box<dyn std::error::Error>>(())
623 /// ```
624 pub fn evaluate_buffer(&self, buffer: &[u8]) -> Result<EvaluationResult> {
625 use std::time::Instant;
626 self.evaluate_buffer_internal(buffer, Instant::now())
627 }
628
629 /// Internal buffer evaluation with externally provided start time
630 fn evaluate_buffer_internal(
631 &self,
632 buffer: &[u8],
633 start_time: std::time::Instant,
634 ) -> Result<EvaluationResult> {
635 use crate::evaluator::evaluate_rules_with_config;
636
637 let file_size = buffer.len() as u64;
638
639 let matches = if self.rules.is_empty() {
640 vec![]
641 } else {
642 evaluate_rules_with_config(&self.rules, buffer, &self.config)?
643 };
644
645 Ok(self.build_result(matches, file_size, start_time))
646 }
647
648 /// Build an `EvaluationResult` from match results, file size, and start time.
649 ///
650 /// This is shared between `evaluate_file` and `evaluate_buffer_internal` to
651 /// avoid duplicating the result-construction logic.
652 fn build_result(
653 &self,
654 matches: Vec<evaluator::RuleMatch>,
655 file_size: u64,
656 start_time: std::time::Instant,
657 ) -> EvaluationResult {
658 let (description, confidence) = if matches.is_empty() {
659 ("data".to_string(), 0.0)
660 } else {
661 (
662 Self::concatenate_messages(&matches),
663 matches.first().map_or(0.0, |m| m.confidence),
664 )
665 };
666
667 let mime_type = if self.config.enable_mime_types {
668 self.mime_mapper.get_mime_type(&description)
669 } else {
670 None
671 };
672
673 EvaluationResult {
674 description,
675 mime_type,
676 confidence,
677 matches,
678 metadata: EvaluationMetadata {
679 file_size,
680 evaluation_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
681 rules_evaluated: self.rules.len(),
682 magic_file: self.source_path.clone(),
683 timed_out: false,
684 },
685 }
686 }
687
688 /// Concatenate match messages following libmagic behavior
689 ///
690 /// Messages are joined with spaces, except when a message starts with
691 /// backspace character (\\b) which suppresses the space.
692 fn concatenate_messages(matches: &[evaluator::RuleMatch]) -> String {
693 let capacity: usize = matches.iter().map(|m| m.message.len() + 1).sum();
694 let mut result = String::with_capacity(capacity);
695 for m in matches {
696 if let Some(rest) = m.message.strip_prefix('\u{0008}') {
697 // Backspace suppresses the space and the character itself
698 result.push_str(rest);
699 } else if !result.is_empty() {
700 result.push(' ');
701 result.push_str(&m.message);
702 } else {
703 result.push_str(&m.message);
704 }
705 }
706 result
707 }
708
709 /// Returns the evaluation configuration used by this database.
710 ///
711 /// This provides read-only access to the evaluation configuration for
712 /// callers that need to inspect resource limits or evaluation options.
713 #[must_use]
714 pub fn config(&self) -> &EvaluationConfig {
715 &self.config
716 }
717
718 /// Returns the path from which magic rules were loaded.
719 ///
720 /// This method returns the source path that was used to load the magic rules
721 /// into this database. It is useful for debugging, logging, and tracking the
722 /// origin of magic rules.
723 ///
724 /// # Returns
725 ///
726 /// - `Some(&Path)` - If the database was loaded from a file or directory using
727 /// [`load_from_file()`](Self::load_from_file)
728 /// - `None` - If the database was constructed programmatically or the source
729 /// path was not recorded
730 ///
731 /// # Examples
732 ///
733 /// ```rust,no_run
734 /// use libmagic_rs::MagicDatabase;
735 ///
736 /// let db = MagicDatabase::load_from_file("/usr/share/misc/magic")?;
737 /// if let Some(path) = db.source_path() {
738 /// println!("Rules loaded from: {}", path.display());
739 /// }
740 /// # Ok::<(), Box<dyn std::error::Error>>(())
741 /// ```
742 #[must_use]
743 pub fn source_path(&self) -> Option<&Path> {
744 self.source_path.as_deref()
745 }
746}
747
748/// Metadata about the evaluation process
749///
750/// Contains diagnostic information about how the evaluation was performed,
751/// including performance metrics and statistics about rule processing.
752///
753/// # Examples
754///
755/// ```
756/// use libmagic_rs::EvaluationMetadata;
757/// use std::path::PathBuf;
758///
759/// let metadata = EvaluationMetadata {
760/// file_size: 8192,
761/// evaluation_time_ms: 2.5,
762/// rules_evaluated: 42,
763/// magic_file: Some(PathBuf::from("/usr/share/misc/magic")),
764/// timed_out: false,
765/// };
766///
767/// assert_eq!(metadata.file_size, 8192);
768/// assert!(!metadata.timed_out);
769/// ```
770#[derive(Debug, Clone, Serialize, Deserialize)]
771pub struct EvaluationMetadata {
772 /// Size of the analyzed file or buffer in bytes
773 pub file_size: u64,
774 /// Time taken to evaluate rules in milliseconds
775 pub evaluation_time_ms: f64,
776 /// Number of top-level rules that were evaluated
777 pub rules_evaluated: usize,
778 /// Path to the magic file used, or None for built-in rules
779 pub magic_file: Option<PathBuf>,
780 /// Whether evaluation was stopped due to timeout
781 pub timed_out: bool,
782}
783
784impl Default for EvaluationMetadata {
785 fn default() -> Self {
786 Self {
787 file_size: 0,
788 evaluation_time_ms: 0.0,
789 rules_evaluated: 0,
790 magic_file: None,
791 timed_out: false,
792 }
793 }
794}
795
796/// Result of magic rule evaluation
797///
798/// Contains the file type description, optional MIME type, confidence score,
799/// individual match details, and evaluation metadata.
800///
801/// # Examples
802///
803/// ```
804/// use libmagic_rs::{EvaluationResult, EvaluationMetadata};
805///
806/// let result = EvaluationResult {
807/// description: "ELF 64-bit executable".to_string(),
808/// mime_type: Some("application/x-executable".to_string()),
809/// confidence: 0.9,
810/// matches: vec![],
811/// metadata: EvaluationMetadata::default(),
812/// };
813///
814/// assert_eq!(result.description, "ELF 64-bit executable");
815/// assert!(result.confidence > 0.5);
816/// ```
817#[derive(Debug, Clone, Serialize, Deserialize)]
818pub struct EvaluationResult {
819 /// Human-readable file type description
820 ///
821 /// This is the concatenated message from all matching rules,
822 /// following libmagic behavior where hierarchical matches
823 /// are joined with spaces (unless backspace character is used).
824 pub description: String,
825 /// Optional MIME type for the detected file type
826 ///
827 /// Only populated when `enable_mime_types` is set in the configuration.
828 pub mime_type: Option<String>,
829 /// Confidence score (0.0 to 1.0)
830 ///
831 /// Based on the depth of the match in the rule hierarchy.
832 /// Deeper matches indicate more specific identification.
833 pub confidence: f64,
834 /// Individual match results from rule evaluation
835 ///
836 /// Contains details about each rule that matched, including
837 /// offset, matched value, and per-match confidence.
838 pub matches: Vec<evaluator::RuleMatch>,
839 /// Metadata about the evaluation process
840 pub metadata: EvaluationMetadata,
841}
842
843#[cfg(test)]
844mod tests;