pub mod json;
pub mod text;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::LazyLock;
use crate::parser::ast::Value;
static DEFAULT_TAG_EXTRACTOR: LazyLock<crate::tags::TagExtractor> =
LazyLock::new(crate::tags::TagExtractor::new);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MatchResult {
pub message: String,
pub offset: usize,
pub length: usize,
pub value: Value,
pub rule_path: Vec<String>,
pub confidence: u8,
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluationResult {
pub filename: PathBuf,
pub matches: Vec<MatchResult>,
pub metadata: EvaluationMetadata,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluationMetadata {
pub file_size: u64,
pub evaluation_time_ms: f64,
pub rules_evaluated: u32,
pub rules_matched: u32,
}
impl MatchResult {
#[must_use]
pub fn new(message: String, offset: usize, value: Value) -> Self {
Self {
message,
offset,
length: match &value {
Value::Bytes(bytes) => bytes.len(),
Value::String(s) => s.len(),
Value::Uint(_) | Value::Int(_) => std::mem::size_of::<u64>(),
Value::Float(_) => std::mem::size_of::<f64>(),
},
value,
rule_path: Vec::new(),
confidence: 50, mime_type: None,
}
}
#[must_use]
pub fn with_metadata(
message: String,
offset: usize,
length: usize,
value: Value,
rule_path: Vec<String>,
confidence: u8,
mime_type: Option<String>,
) -> Self {
Self {
message,
offset,
length,
value,
rule_path,
confidence: confidence.min(100), mime_type,
}
}
#[must_use]
pub fn from_evaluator_match(m: &crate::evaluator::RuleMatch, mime_type: Option<&str>) -> Self {
let rule_path =
DEFAULT_TAG_EXTRACTOR.extract_rule_path(std::iter::once(m.message.as_str()));
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let confidence = (m.confidence * 100.0).min(100.0) as u8;
let length = match &m.value {
Value::Bytes(b) => b.len(),
Value::String(s) => s.len(),
Value::Uint(_) | Value::Int(_) | Value::Float(_) => m
.type_kind
.bit_width()
.map_or(0, |bits| (bits / 8) as usize),
};
Self::with_metadata(
m.message.clone(),
m.offset,
length,
m.value.clone(),
rule_path,
confidence,
mime_type.map(String::from),
)
}
pub fn set_confidence(&mut self, confidence: u8) {
self.confidence = confidence.min(100);
}
pub fn add_rule_path(&mut self, rule_name: String) {
self.rule_path.push(rule_name);
}
pub fn set_mime_type(&mut self, mime_type: Option<String>) {
self.mime_type = mime_type;
}
}
impl EvaluationResult {
#[must_use]
pub fn new(filename: PathBuf, matches: Vec<MatchResult>, metadata: EvaluationMetadata) -> Self {
Self {
filename,
matches,
metadata,
error: None,
}
}
#[must_use]
pub fn from_library_result(
result: &crate::EvaluationResult,
filename: &std::path::Path,
) -> Self {
let mut output_matches: Vec<MatchResult> = result
.matches
.iter()
.map(|m| MatchResult::from_evaluator_match(m, result.mime_type.as_deref()))
.collect();
if let Some(first) = output_matches.first_mut()
&& first.rule_path.is_empty()
{
first.rule_path = DEFAULT_TAG_EXTRACTOR.extract_tags(&result.description);
}
#[allow(clippy::cast_possible_truncation)]
let rules_evaluated = result.metadata.rules_evaluated as u32;
#[allow(clippy::cast_possible_truncation)]
let rules_matched = output_matches.len() as u32;
Self::new(
filename.to_path_buf(),
output_matches,
EvaluationMetadata::new(
result.metadata.file_size,
result.metadata.evaluation_time_ms,
rules_evaluated,
rules_matched,
),
)
}
#[must_use]
pub fn with_error(filename: PathBuf, error: String, metadata: EvaluationMetadata) -> Self {
Self {
filename,
matches: Vec::new(),
metadata,
error: Some(error),
}
}
pub fn add_match(&mut self, match_result: MatchResult) {
#[cfg(debug_assertions)]
Self::validate_match_result(&match_result);
self.matches.push(match_result);
}
#[cfg(debug_assertions)]
fn validate_match_result(match_result: &MatchResult) {
if match_result.confidence > 100 {
eprintln!(
"Warning: Match result has confidence score > 100: {}",
match_result.confidence
);
}
}
#[must_use]
pub fn primary_match(&self) -> Option<&MatchResult> {
self.matches
.iter()
.max_by_key(|match_result| match_result.confidence)
}
#[must_use]
pub fn is_success(&self) -> bool {
self.error.is_none()
}
}
impl EvaluationMetadata {
#[must_use]
pub fn new(
file_size: u64,
evaluation_time_ms: f64,
rules_evaluated: u32,
rules_matched: u32,
) -> Self {
Self {
file_size,
evaluation_time_ms,
rules_evaluated,
rules_matched,
}
}
#[must_use]
pub fn match_rate(&self) -> f64 {
if self.rules_evaluated == 0 {
0.0
} else {
(f64::from(self.rules_matched) / f64::from(self.rules_evaluated)) * 100.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_match_result_new() {
let result = MatchResult::new(
"Test file".to_string(),
42,
Value::String("test".to_string()),
);
assert_eq!(result.message, "Test file");
assert_eq!(result.offset, 42);
assert_eq!(result.length, 4); assert_eq!(result.value, Value::String("test".to_string()));
assert!(result.rule_path.is_empty());
assert_eq!(result.confidence, 50);
assert!(result.mime_type.is_none());
}
#[test]
fn test_match_result_with_metadata() {
let result = MatchResult::with_metadata(
"ELF executable".to_string(),
0,
4,
Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46]),
vec!["elf".to_string()],
95,
Some("application/x-executable".to_string()),
);
assert_eq!(result.message, "ELF executable");
assert_eq!(result.offset, 0);
assert_eq!(result.length, 4);
assert_eq!(result.rule_path, vec!["elf"]);
assert_eq!(result.confidence, 95);
assert_eq!(
result.mime_type,
Some("application/x-executable".to_string())
);
}
#[test]
fn test_match_result_length_calculation() {
let bytes_result = MatchResult::new("Bytes".to_string(), 0, Value::Bytes(vec![1, 2, 3]));
assert_eq!(bytes_result.length, 3);
let string_result =
MatchResult::new("String".to_string(), 0, Value::String("hello".to_string()));
assert_eq!(string_result.length, 5);
let uint_result = MatchResult::new("Uint".to_string(), 0, Value::Uint(42));
assert_eq!(uint_result.length, 8);
let int_result = MatchResult::new("Int".to_string(), 0, Value::Int(-42));
assert_eq!(int_result.length, 8); }
#[test]
fn test_match_result_set_confidence() {
let mut result = MatchResult::new("Test".to_string(), 0, Value::Uint(0));
result.set_confidence(75);
assert_eq!(result.confidence, 75);
result.set_confidence(150);
assert_eq!(result.confidence, 100);
result.set_confidence(0);
assert_eq!(result.confidence, 0);
}
#[test]
fn test_match_result_confidence_clamping_in_constructor() {
let result = MatchResult::with_metadata(
"Test".to_string(),
0,
1,
Value::Uint(0),
vec![],
200, None,
);
assert_eq!(result.confidence, 100);
}
#[test]
fn test_match_result_add_rule_path() {
let mut result = MatchResult::new("Test".to_string(), 0, Value::Uint(0));
result.add_rule_path("root".to_string());
result.add_rule_path("child".to_string());
result.add_rule_path("grandchild".to_string());
assert_eq!(result.rule_path, vec!["root", "child", "grandchild"]);
}
#[test]
fn test_match_result_set_mime_type() {
let mut result = MatchResult::new("Test".to_string(), 0, Value::Uint(0));
result.set_mime_type(Some("text/plain".to_string()));
assert_eq!(result.mime_type, Some("text/plain".to_string()));
result.set_mime_type(None);
assert!(result.mime_type.is_none());
}
#[test]
fn test_match_result_serialization() {
let result = MatchResult::with_metadata(
"PNG image".to_string(),
0,
8,
Value::Bytes(vec![0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
vec!["image".to_string(), "png".to_string()],
90,
Some("image/png".to_string()),
);
let json = serde_json::to_string(&result).expect("Failed to serialize MatchResult");
let deserialized: MatchResult =
serde_json::from_str(&json).expect("Failed to deserialize MatchResult");
assert_eq!(result, deserialized);
}
#[test]
fn test_evaluation_result_new() {
let metadata = EvaluationMetadata::new(1024, 2.5, 10, 2);
let result = EvaluationResult::new(PathBuf::from("test.bin"), vec![], metadata);
assert_eq!(result.filename, PathBuf::from("test.bin"));
assert!(result.matches.is_empty());
assert!(result.error.is_none());
assert_eq!(result.metadata.file_size, 1024);
}
#[test]
fn test_evaluation_result_with_error() {
let metadata = EvaluationMetadata::new(0, 0.0, 0, 0);
let result = EvaluationResult::with_error(
PathBuf::from("missing.txt"),
"File not found".to_string(),
metadata,
);
assert_eq!(result.error, Some("File not found".to_string()));
assert!(result.matches.is_empty());
assert!(!result.is_success());
}
#[test]
fn test_evaluation_result_add_match() {
let metadata = EvaluationMetadata::new(512, 1.0, 5, 0);
let mut result = EvaluationResult::new(PathBuf::from("data.bin"), vec![], metadata);
let match_result =
MatchResult::new("Binary data".to_string(), 0, Value::Bytes(vec![0x00, 0x01]));
result.add_match(match_result);
assert_eq!(result.matches.len(), 1);
assert_eq!(result.matches[0].message, "Binary data");
}
#[test]
fn test_evaluation_result_primary_match() {
let metadata = EvaluationMetadata::new(2048, 3.0, 20, 3);
let matches = vec![
MatchResult::with_metadata(
"Low confidence".to_string(),
0,
2,
Value::String("AB".to_string()),
vec![],
30,
None,
),
MatchResult::with_metadata(
"High confidence".to_string(),
10,
4,
Value::String("TEST".to_string()),
vec![],
95,
None,
),
MatchResult::with_metadata(
"Medium confidence".to_string(),
20,
3,
Value::String("XYZ".to_string()),
vec![],
60,
None,
),
];
let result = EvaluationResult::new(PathBuf::from("test.dat"), matches, metadata);
let primary = result.primary_match();
assert!(primary.is_some());
assert_eq!(primary.unwrap().message, "High confidence");
assert_eq!(primary.unwrap().confidence, 95);
}
#[test]
fn test_evaluation_result_primary_match_empty() {
let metadata = EvaluationMetadata::new(0, 0.0, 0, 0);
let result = EvaluationResult::new(PathBuf::from("empty.txt"), vec![], metadata);
assert!(result.primary_match().is_none());
}
#[test]
fn test_evaluation_result_is_success() {
let metadata = EvaluationMetadata::new(100, 0.5, 3, 1);
let success = EvaluationResult::new(PathBuf::from("good.txt"), vec![], metadata.clone());
let failure = EvaluationResult::with_error(
PathBuf::from("bad.txt"),
"Error occurred".to_string(),
metadata,
);
assert!(success.is_success());
assert!(!failure.is_success());
}
#[test]
fn test_evaluation_result_serialization() {
let match_result = MatchResult::new(
"Text file".to_string(),
0,
Value::String("Hello".to_string()),
);
let metadata = EvaluationMetadata::new(1024, 1.5, 8, 1);
let result =
EvaluationResult::new(PathBuf::from("hello.txt"), vec![match_result], metadata);
let json = serde_json::to_string(&result).expect("Failed to serialize EvaluationResult");
let deserialized: EvaluationResult =
serde_json::from_str(&json).expect("Failed to deserialize EvaluationResult");
assert_eq!(result.filename, deserialized.filename);
assert_eq!(result.matches.len(), deserialized.matches.len());
assert_eq!(result.metadata.file_size, deserialized.metadata.file_size);
}
#[test]
fn test_evaluation_metadata_new() {
let metadata = EvaluationMetadata::new(4096, 5.2, 50, 8);
assert_eq!(metadata.file_size, 4096);
assert!((metadata.evaluation_time_ms - 5.2).abs() < f64::EPSILON);
assert_eq!(metadata.rules_evaluated, 50);
assert_eq!(metadata.rules_matched, 8);
}
#[test]
fn test_evaluation_metadata_match_rate() {
let metadata = EvaluationMetadata::new(1024, 1.0, 20, 5);
assert!((metadata.match_rate() - 25.0).abs() < f64::EPSILON);
let perfect_match = EvaluationMetadata::new(1024, 1.0, 10, 10);
assert!((perfect_match.match_rate() - 100.0).abs() < f64::EPSILON);
let no_matches = EvaluationMetadata::new(1024, 1.0, 15, 0);
assert!((no_matches.match_rate() - 0.0).abs() < f64::EPSILON);
let no_rules = EvaluationMetadata::new(1024, 1.0, 0, 0);
assert!((no_rules.match_rate() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_evaluation_metadata_serialization() {
let metadata = EvaluationMetadata::new(2048, 3.7, 25, 4);
let json =
serde_json::to_string(&metadata).expect("Failed to serialize EvaluationMetadata");
let deserialized: EvaluationMetadata =
serde_json::from_str(&json).expect("Failed to deserialize EvaluationMetadata");
assert_eq!(metadata.file_size, deserialized.file_size);
assert!(
(metadata.evaluation_time_ms - deserialized.evaluation_time_ms).abs() < f64::EPSILON
);
assert_eq!(metadata.rules_evaluated, deserialized.rules_evaluated);
assert_eq!(metadata.rules_matched, deserialized.rules_matched);
}
#[test]
fn test_match_result_equality() {
let result1 = MatchResult::new("Test".to_string(), 0, Value::Uint(42));
let result2 = MatchResult::new("Test".to_string(), 0, Value::Uint(42));
let result3 = MatchResult::new("Different".to_string(), 0, Value::Uint(42));
assert_eq!(result1, result2);
assert_ne!(result1, result3);
}
#[test]
fn test_complex_evaluation_result() {
let matches = vec![
MatchResult::with_metadata(
"ELF 64-bit LSB executable".to_string(),
0,
4,
Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46]),
vec!["elf".to_string(), "elf64".to_string()],
95,
Some("application/x-executable".to_string()),
),
MatchResult::with_metadata(
"x86-64 architecture".to_string(),
18,
2,
Value::Uint(0x3e),
vec!["elf".to_string(), "elf64".to_string(), "x86_64".to_string()],
85,
None,
),
MatchResult::with_metadata(
"dynamically linked".to_string(),
16,
2,
Value::Uint(0x02),
vec![
"elf".to_string(),
"elf64".to_string(),
"dynamic".to_string(),
],
80,
None,
),
];
let metadata = EvaluationMetadata::new(8192, 4.2, 35, 3);
let result = EvaluationResult::new(PathBuf::from("/usr/bin/ls"), matches, metadata);
assert_eq!(result.matches.len(), 3);
let expected_rate = (3.0 / 35.0) * 100.0;
assert!((result.metadata.match_rate() - expected_rate).abs() < f64::EPSILON);
let primary = result.primary_match().unwrap();
assert_eq!(primary.message, "ELF 64-bit LSB executable");
assert_eq!(primary.confidence, 95);
assert_eq!(
primary.mime_type,
Some("application/x-executable".to_string())
);
for match_result in &result.matches {
assert!(!match_result.rule_path.is_empty());
assert!(match_result.rule_path[0] == "elf");
}
}
}