#![deny(missing_docs)]
#![deny(unsafe_code)]
#![deny(clippy::all)]
#![warn(clippy::pedantic)]
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub mod builtin_rules;
mod config;
pub mod error;
pub mod evaluator;
pub mod io;
pub mod mime;
pub mod output;
pub mod parser;
pub mod tags;
pub use config::EvaluationConfig;
#[cfg(any(test, doc))]
pub mod build_helpers;
pub use parser::ast::{
Endianness, MagicRule, OffsetSpec, Operator, PStringLengthWidth, StrengthModifier, TypeKind,
Value,
};
pub use evaluator::{EvaluationContext, RuleMatch};
pub use error::{EvaluationError, LibmagicError, ParseError};
pub type Result<T> = std::result::Result<T, LibmagicError>;
impl From<crate::io::IoError> for LibmagicError {
fn from(err: crate::io::IoError) -> Self {
LibmagicError::FileError(err.to_string())
}
}
#[derive(Debug)]
pub struct MagicDatabase {
name_table: std::sync::Arc<crate::parser::name_table::NameTable>,
root_rules: std::sync::Arc<[MagicRule]>,
config: EvaluationConfig,
source_path: Option<PathBuf>,
mime_mapper: mime::MimeMapper,
}
impl MagicDatabase {
pub fn with_builtin_rules() -> Result<Self> {
Self::with_builtin_rules_and_config(EvaluationConfig::default())
}
pub fn with_builtin_rules_and_config(config: EvaluationConfig) -> Result<Self> {
config.validate()?;
let mut rules = crate::builtin_rules::get_builtin_rules();
crate::evaluator::strength::sort_rules_by_strength_recursive(&mut rules);
let root_rules: std::sync::Arc<[MagicRule]> =
std::sync::Arc::from(rules.into_boxed_slice());
Ok(Self {
name_table: std::sync::Arc::new(crate::parser::name_table::NameTable::empty()),
root_rules,
config,
source_path: None,
mime_mapper: mime::MimeMapper::new(),
})
}
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::load_from_file_with_config(path, EvaluationConfig::default())
}
pub fn load_from_file_with_config<P: AsRef<Path>>(
path: P,
config: EvaluationConfig,
) -> Result<Self> {
config.validate()?;
let parsed = parser::load_magic_file(path.as_ref()).map_err(|e| match e {
ParseError::IoError(io_err) => LibmagicError::IoError(io_err),
other => LibmagicError::ParseError(other),
})?;
let parser::ParsedMagic {
mut rules,
mut name_table,
} = parsed;
crate::evaluator::strength::sort_rules_by_strength_recursive(&mut rules);
name_table.sort_subroutines(|rules| {
crate::evaluator::strength::sort_rules_by_strength_recursive(rules);
});
let root_rules: std::sync::Arc<[MagicRule]> =
std::sync::Arc::from(rules.into_boxed_slice());
Ok(Self {
name_table: std::sync::Arc::new(name_table),
root_rules,
config,
source_path: Some(path.as_ref().to_path_buf()),
mime_mapper: mime::MimeMapper::new(),
})
}
pub fn evaluate_file<P: AsRef<Path>>(&self, path: P) -> Result<EvaluationResult> {
use crate::io::FileBuffer;
use std::fs;
use std::time::Instant;
let start_time = Instant::now();
let path = path.as_ref();
let file_metadata = fs::metadata(path)?;
let file_size = file_metadata.len();
if file_size == 0 {
let mut result = self.evaluate_buffer_internal(b"", start_time)?;
result.metadata.file_size = 0;
result.metadata.magic_file.clone_from(&self.source_path);
return Ok(result);
}
let file_buffer = FileBuffer::from_path_and_metadata(path, &file_metadata)?;
let buffer = file_buffer.as_slice();
let mut result = self.evaluate_buffer_internal(buffer, start_time)?;
result.metadata.file_size = file_size;
Ok(result)
}
pub fn evaluate_buffer(&self, buffer: &[u8]) -> Result<EvaluationResult> {
use std::time::Instant;
self.evaluate_buffer_internal(buffer, Instant::now())
}
fn evaluate_buffer_internal(
&self,
buffer: &[u8],
start_time: std::time::Instant,
) -> Result<EvaluationResult> {
use crate::evaluator::{EvaluationContext, RuleEnvironment, evaluate_rules};
let file_size = buffer.len() as u64;
self.config.validate()?;
crate::evaluator::types::regex::reset_regex_cache();
let env = std::sync::Arc::new(RuleEnvironment {
name_table: self.name_table.clone(),
root_rules: self.root_rules.clone(),
});
let mut context = EvaluationContext::new(self.config.clone()).with_rule_env(env);
let matches = evaluate_rules(&self.root_rules, buffer, &mut context)?;
Ok(self.build_result(matches, file_size, start_time))
}
fn build_result(
&self,
matches: Vec<evaluator::RuleMatch>,
file_size: u64,
start_time: std::time::Instant,
) -> EvaluationResult {
let (description, confidence) = if matches.is_empty() {
("data".to_string(), 0.0)
} else {
(
Self::concatenate_messages(&matches),
matches.first().map_or(0.0, |m| m.confidence),
)
};
let mime_type = if self.config.enable_mime_types {
self.mime_mapper
.get_mime_type(&description)
.map(String::from)
} else {
None
};
EvaluationResult {
description,
mime_type,
confidence,
matches,
metadata: EvaluationMetadata {
file_size,
evaluation_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
rules_evaluated: self.root_rules.len(),
magic_file: self.source_path.clone(),
timed_out: false,
},
}
}
fn concatenate_messages(matches: &[evaluator::RuleMatch]) -> String {
use crate::output::format::format_magic_message;
let capacity: usize = matches.iter().map(|m| m.message.len() + 1).sum();
let mut result = String::with_capacity(capacity);
for m in matches {
let rendered = format_magic_message(&m.message, &m.value, &m.type_kind);
if let Some(rest) = rendered.strip_prefix('\u{0008}') {
result.push_str(rest);
} else if !result.is_empty() {
result.push(' ');
result.push_str(&rendered);
} else {
result.push_str(&rendered);
}
}
result
}
#[must_use]
pub fn config(&self) -> &EvaluationConfig {
&self.config
}
#[must_use]
pub fn source_path(&self) -> Option<&Path> {
self.source_path.as_deref()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluationMetadata {
pub file_size: u64,
pub evaluation_time_ms: f64,
pub rules_evaluated: usize,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub magic_file: Option<PathBuf>,
pub timed_out: bool,
}
impl Default for EvaluationMetadata {
fn default() -> Self {
Self {
file_size: 0,
evaluation_time_ms: 0.0,
rules_evaluated: 0,
magic_file: None,
timed_out: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluationResult {
pub description: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub mime_type: Option<String>,
pub confidence: f64,
pub matches: Vec<evaluator::RuleMatch>,
pub metadata: EvaluationMetadata,
}
#[cfg(test)]
mod tests;