From d030821f926bb9f40b5d872d4042f5abe3a744fc Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Fri, 6 Mar 2026 12:49:38 -0500 Subject: [PATCH 01/12] feat(evaluator): introduce core evaluation engine for magic rules This commit adds a new module `engine.rs` that implements the core logic for evaluating magic rules against file buffers. Key functionalities include: - `evaluate_single_rule`: Evaluates an individual magic rule, resolving offsets and applying operators. - `evaluate_rules`: Handles hierarchical evaluation of rules, managing context and error handling gracefully. Additionally, the `mod.rs` file has been updated to expose these new functions, and the previous test suite has been removed as part of the refactor. This lays the groundwork for improved rule evaluation and extensibility in the magic file identification process. Signed-off-by: UncleSp1d3r --- src/evaluator/engine.rs | 2096 ++++++++++++++++++++++++++++++++++ src/evaluator/mod.rs | 720 +++++++----- src/evaluator/tests.rs | 2385 --------------------------------------- 3 files changed, 2509 insertions(+), 2692 deletions(-) create mode 100644 src/evaluator/engine.rs delete mode 100644 src/evaluator/tests.rs diff --git a/src/evaluator/engine.rs b/src/evaluator/engine.rs new file mode 100644 index 00000000..f84f5288 --- /dev/null +++ b/src/evaluator/engine.rs @@ -0,0 +1,2096 @@ +// Copyright (c) 2025-2026 the libmagic-rs contributors +// SPDX-License-Identifier: Apache-2.0 + +//! Core evaluation engine for magic rules. +//! +//! This module contains the core recursive evaluation logic for executing magic +//! rules against file buffers. It is responsible for: +//! - Evaluating individual rules (`evaluate_single_rule`) +//! - Evaluating hierarchical rule sets with context (`evaluate_rules`) +//! - Providing a convenience wrapper for evaluation with configuration +//! (`evaluate_rules_with_config`) + +use crate::parser::ast::MagicRule; +use crate::{EvaluationConfig, LibmagicError}; + +use super::{EvaluationContext, RuleMatch, offset, operators, types}; + +/// Evaluate a single magic rule against a file buffer +/// +/// This function performs the core rule evaluation by: +/// 1. Resolving the rule's offset specification to an absolute position +/// 2. Reading and interpreting bytes at that position according to the rule's type +/// 3. Applying the rule's operator to compare the read value with the expected value +/// +/// # Arguments +/// +/// * `rule` - The magic rule to evaluate +/// * `buffer` - The file buffer to evaluate against +/// +/// # Returns +/// +/// Returns `Ok(Some((offset, value)))` if the rule matches (with the resolved offset and +/// read value), `Ok(None)` if it doesn't match, or `Err(LibmagicError)` if evaluation +/// fails due to buffer access issues or other errors. +/// +/// # Errors +/// +/// * `LibmagicError::EvaluationError` - If offset resolution fails, buffer access is out of bounds, +/// or type interpretation fails +pub fn evaluate_single_rule( + rule: &MagicRule, + buffer: &[u8], +) -> Result, LibmagicError> { + // Step 1: Resolve the offset specification to an absolute position + let absolute_offset = offset::resolve_offset(&rule.offset, buffer)?; + + // Step 2: Read and interpret bytes at the resolved offset according to the rule's type + let read_value = types::read_typed_value(buffer, absolute_offset, &rule.typ) + .map_err(|e| LibmagicError::EvaluationError(e.into()))?; + + // Step 3: Coerce the rule's expected value to match the type's signedness/width + let expected_value = types::coerce_value_to_type(&rule.value, &rule.typ); + + // Step 4: Apply the operator to compare the read value with the expected value + // BitwiseNot needs type-aware bit-width masking so the complement is computed + // at the type's natural width (e.g., byte NOT of 0x00 = 0xFF, not u64::MAX). + let matched = match &rule.op { + crate::parser::ast::Operator::BitwiseNot => operators::apply_bitwise_not_with_width( + &read_value, + &expected_value, + rule.typ.bit_width(), + ), + op => operators::apply_operator(op, &read_value, &expected_value), + }; + Ok(matched.then_some((absolute_offset, read_value))) +} + +/// Evaluate a list of magic rules against a file buffer with hierarchical processing +/// +/// This function implements the core hierarchical rule evaluation algorithm with graceful +/// error handling: +/// 1. Evaluates each top-level rule in sequence +/// 2. If a parent rule matches, evaluates its child rules for refinement +/// 3. Collects all matches or stops at first match based on configuration +/// 4. Maintains evaluation context for recursion limits and state +/// 5. Implements graceful degradation by skipping problematic rules and continuing evaluation +/// +/// The hierarchical evaluation follows these principles: +/// - Parent rules must match before children are evaluated +/// - Child rules provide refinement and additional detail +/// - Evaluation can stop at first match or continue for all matches +/// - Recursion depth is limited to prevent infinite loops +/// - Problematic rules are skipped to allow evaluation to continue +/// +/// # Errors +/// +/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout +/// * `LibmagicError::EvaluationError` - For critical failures (e.g. recursion limit exceeded) +pub fn evaluate_rules( + rules: &[MagicRule], + buffer: &[u8], + context: &mut EvaluationContext, +) -> Result, LibmagicError> { + let mut matches = Vec::with_capacity(8); + let start_time = std::time::Instant::now(); + let mut rule_count = 0u32; + + for rule in rules { + // Check timeout periodically (every 16 rules) to reduce syscall overhead + rule_count = rule_count.wrapping_add(1); + if rule_count.trailing_zeros() >= 4 + && let Some(timeout_ms) = context.timeout_ms() + && start_time.elapsed().as_millis() > u128::from(timeout_ms) + { + return Err(LibmagicError::Timeout { timeout_ms }); + } + + // Evaluate the current rule with graceful error handling + let match_data = match evaluate_single_rule(rule, buffer) { + Ok(data) => data, + Err( + LibmagicError::EvaluationError( + crate::error::EvaluationError::BufferOverrun { .. } + | crate::error::EvaluationError::InvalidOffset { .. } + | crate::error::EvaluationError::TypeReadError(_), + ) + | LibmagicError::IoError(_), + ) => { + // Expected evaluation errors for individual rules -- skip gracefully + continue; + } + Err(e) => { + // Unexpected errors (InternalError, UnsupportedType, etc.) should propagate + return Err(e); + } + }; + + if let Some((absolute_offset, read_value)) = match_data { + let match_result = RuleMatch { + message: rule.message.clone(), + offset: absolute_offset, + level: rule.level, + value: read_value, + confidence: RuleMatch::calculate_confidence(rule.level), + }; + matches.push(match_result); + + // If this rule has children, evaluate them recursively + if !rule.children.is_empty() { + // Check recursion depth limit - this is a critical error that should stop evaluation + context.increment_recursion_depth()?; + + // Recursively evaluate child rules with graceful error handling + match evaluate_rules(&rule.children, buffer, context) { + Ok(child_matches) => { + matches.extend(child_matches); + } + Err(LibmagicError::Timeout { .. }) => { + // Timeout is critical, propagate it up + context.decrement_recursion_depth()?; + return Err(LibmagicError::Timeout { + timeout_ms: context.timeout_ms().unwrap_or(0), + }); + } + Err(LibmagicError::EvaluationError( + crate::error::EvaluationError::RecursionLimitExceeded { .. }, + )) => { + // Recursion limit is critical, propagate it up + context.decrement_recursion_depth()?; + return Err(LibmagicError::EvaluationError( + crate::error::EvaluationError::RecursionLimitExceeded { + depth: context.recursion_depth(), + }, + )); + } + Err( + LibmagicError::EvaluationError( + crate::error::EvaluationError::BufferOverrun { .. } + | crate::error::EvaluationError::InvalidOffset { .. } + | crate::error::EvaluationError::TypeReadError(_), + ) + | LibmagicError::IoError(_), + ) => { + // Expected child evaluation errors -- skip gracefully + } + Err(e) => { + // Unexpected errors in children should propagate + context.decrement_recursion_depth()?; + return Err(e); + } + } + + // Restore recursion depth + context.decrement_recursion_depth()?; + } + + // Stop at first match if configured to do so + if context.should_stop_at_first_match() { + break; + } + } + } + + Ok(matches) +} + +/// Evaluate magic rules with a fresh context +/// +/// This is a convenience function that creates a new evaluation context +/// and evaluates the rules. Useful for simple evaluation scenarios. +/// +/// # Errors +/// +/// * `LibmagicError::EvaluationError` - If rule evaluation fails +/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout +pub fn evaluate_rules_with_config( + rules: &[MagicRule], + buffer: &[u8], + config: &EvaluationConfig, +) -> Result, LibmagicError> { + let mut context = EvaluationContext::new(config.clone()); + evaluate_rules(rules, buffer, &mut context) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::ast::{Endianness, OffsetSpec, Operator, TypeKind, Value}; + + #[test] + fn test_evaluate_single_rule_byte_equal_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_byte_equal_no_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x50, 0x4b, 0x03, 0x04]; // ZIP magic bytes + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_none()); + } + + #[test] + fn test_evaluate_single_rule_byte_not_equal_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::NotEqual, + value: Value::Uint(0x00), + message: "Non-zero byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); // 0x7f != 0x00 + } + + #[test] + fn test_evaluate_single_rule_byte_not_equal_no_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::NotEqual, + value: Value::Uint(0x7f), + message: "Not ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_none()); // 0x7f == 0x7f, so NotEqual is false + } + + #[test] + fn test_evaluate_single_rule_byte_bitwise_and_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::BitwiseAnd, + value: Value::Uint(0x80), // Check if high bit is set + message: "High bit set".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0xff, 0x45, 0x4c, 0x46]; // 0xff has high bit set + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); // 0xff & 0x80 = 0x80 (non-zero) + } + + #[test] + fn test_evaluate_single_rule_byte_bitwise_and_no_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::BitwiseAnd, + value: Value::Uint(0x80), // Check if high bit is set + message: "High bit set".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // 0x7f has high bit clear + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_none()); // 0x7f & 0x80 = 0x00 (zero) + } + + #[test] + fn test_evaluate_single_rule_short_little_endian() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234), + message: "Little-endian short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x34, 0x12, 0x56, 0x78]; // 0x1234 in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_short_big_endian() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Big, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234), + message: "Big-endian short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x12, 0x34, 0x56, 0x78]; // 0x1234 in big-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_short_signed_positive() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: true, + }, + op: Operator::Equal, + value: Value::Int(32767), // 0x7fff + message: "Positive signed short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0xff, 0x7f, 0x00, 0x00]; // 0x7fff in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_short_signed_negative() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: true, + }, + op: Operator::Equal, + value: Value::Int(-1), // 0xffff as signed + message: "Negative signed short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0xff, 0xff, 0x00, 0x00]; // 0xffff in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_long_little_endian() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234_5678), + message: "Little-endian long".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x78, 0x56, 0x34, 0x12, 0x00]; // 0x12345678 in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_long_big_endian() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Big, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234_5678), + message: "Big-endian long".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x12, 0x34, 0x56, 0x78, 0x00]; // 0x12345678 in big-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_long_signed_positive() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: true, + }, + op: Operator::Equal, + value: Value::Int(2_147_483_647), // 0x7fffffff + message: "Positive signed long".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0xff, 0xff, 0xff, 0x7f, 0x00]; // 0x7fffffff in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_long_signed_negative() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: true, + }, + op: Operator::Equal, + value: Value::Int(-1), // 0xffffffff as signed + message: "Negative signed long".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0xff, 0xff, 0xff, 0xff, 0x00]; // 0xffffffff in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_different_offsets() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(2), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x4c), + message: "ELF class byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_negative_offset() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(-1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x46), + message: "Last byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_from_end_offset() { + let rule = MagicRule { + offset: OffsetSpec::FromEnd(-2), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x4c), + message: "Second to last byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_offset_out_of_bounds() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(10), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Out of bounds".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Buffer overrun")); + } + _ => panic!("Expected EvaluationError"), + } + } + + #[test] + fn test_evaluate_single_rule_short_insufficient_bytes() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(3), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234), + message: "Insufficient bytes".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Buffer overrun")); + } + _ => panic!("Expected EvaluationError"), + } + } + + #[test] + fn test_evaluate_single_rule_long_insufficient_bytes() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(2), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234_5678), + message: "Insufficient bytes".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Buffer overrun")); + } + _ => panic!("Expected EvaluationError"), + } + } + + #[test] + fn test_evaluate_single_rule_empty_buffer() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Empty buffer".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[]; + let result = evaluate_single_rule(&rule, buffer); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Buffer overrun")); + } + _ => panic!("Expected EvaluationError"), + } + } + + #[test] + fn test_evaluate_single_rule_string_type_supported() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::String { max_length: None }, + op: Operator::Equal, + value: Value::String("test".to_string()), + message: "String type".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = b"test\x00 data"; + let result = evaluate_single_rule(&rule, buffer); + assert!(result.is_ok()); + let matches = result.unwrap(); + assert!(matches.is_some()); + + let rule_no_match = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::String { max_length: None }, + op: Operator::Equal, + value: Value::String("hello".to_string()), + message: "String type".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let result = evaluate_single_rule(&rule_no_match, buffer); + assert!(result.is_ok()); + let matches = result.unwrap(); + assert!(matches.is_none()); + } + + #[test] + fn test_evaluate_single_rule_cross_type_comparison() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Int(42), + message: "Cross-type comparison".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[42]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_bitwise_and_with_shorts() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: false, + }, + op: Operator::BitwiseAnd, + value: Value::Uint(0xff00), + message: "High byte check".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x34, 0x12]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_bitwise_and_with_longs() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Big, + signed: false, + }, + op: Operator::BitwiseAnd, + value: Value::Uint(0xffff_0000), + message: "High word check".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x12, 0x34, 0x56, 0x78]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_comprehensive_elf_check() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x464c_457f), + message: "ELF executable".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let elf_buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let result = evaluate_single_rule(&rule, elf_buffer).unwrap(); + assert!(result.is_some()); + + let non_elf_buffer = &[0x50, 0x4b, 0x03, 0x04, 0x14, 0x00]; + let result = evaluate_single_rule(&rule, non_elf_buffer).unwrap(); + assert!(result.is_none()); + } + + #[test] + fn test_evaluate_single_rule_native_endianness() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Native, + signed: false, + }, + op: Operator::NotEqual, + value: Value::Uint(0), + message: "Non-zero native short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x01, 0x02]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_all_operators() { + let buffer = &[0x42, 0x00, 0xff, 0x80]; + + let equal_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x42), + message: "Equal test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!(evaluate_single_rule(&equal_rule, buffer).unwrap().is_some()); + + let not_equal_rule = MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::NotEqual, + value: Value::Uint(0x42), + message: "NotEqual test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(¬_equal_rule, buffer) + .unwrap() + .is_some() + ); + + let bitwise_and_rule = MagicRule { + offset: OffsetSpec::Absolute(3), + typ: TypeKind::Byte { signed: true }, + op: Operator::BitwiseAnd, + value: Value::Uint(0x80), + message: "BitwiseAnd test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&bitwise_and_rule, buffer) + .unwrap() + .is_some() + ); + } + + #[test] + fn test_evaluate_single_rule_comparison_operators() { + let buffer = &[0x42, 0x00, 0xff, 0x80]; + + let less_than_rule = MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: false }, + op: Operator::LessThan, + value: Value::Uint(0x42), + message: "LessThan test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&less_than_rule, buffer) + .unwrap() + .is_some() + ); + + let greater_than_rule = MagicRule { + offset: OffsetSpec::Absolute(2), + typ: TypeKind::Byte { signed: false }, + op: Operator::GreaterThan, + value: Value::Uint(0x42), + message: "GreaterThan test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&greater_than_rule, buffer) + .unwrap() + .is_some() + ); + + let less_equal_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: false }, + op: Operator::LessEqual, + value: Value::Uint(0x42), + message: "LessEqual test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&less_equal_rule, buffer) + .unwrap() + .is_some() + ); + + let greater_equal_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: false }, + op: Operator::GreaterEqual, + value: Value::Uint(0x42), + message: "GreaterEqual test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&greater_equal_rule, buffer) + .unwrap() + .is_some() + ); + } + + #[test] + fn test_evaluate_comparison_with_signed_byte() { + let buffer = &[0x80]; + + let signed_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::LessThan, + value: Value::Uint(0), + message: "signed less".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&signed_rule, buffer) + .unwrap() + .is_some() + ); + + let unsigned_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: false }, + op: Operator::LessThan, + value: Value::Uint(0), + message: "unsigned less".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&unsigned_rule, buffer) + .unwrap() + .is_none() + ); + } + + #[test] + fn test_evaluate_comparison_operators_negative_cases() { + let buffer = &[0x42]; + + let cases: Vec<(Operator, u64, bool)> = vec![ + (Operator::LessThan, 66, false), + (Operator::LessThan, 67, true), + (Operator::GreaterThan, 66, false), + (Operator::GreaterThan, 65, true), + (Operator::LessEqual, 65, false), + (Operator::LessEqual, 66, true), + (Operator::GreaterEqual, 67, false), + (Operator::GreaterEqual, 66, true), + ]; + + for (op, value, expected) in cases { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: false }, + op: op.clone(), + value: Value::Uint(value), + message: "test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert_eq!( + result.is_some(), + expected, + "{op:?} with value {value}: expected {expected}" + ); + } + } + + #[test] + fn test_evaluate_single_rule_edge_case_values() { + let max_uint_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0xffff_ffff), + message: "Max uint32".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let max_buffer = &[0xff, 0xff, 0xff, 0xff]; + let result = evaluate_single_rule(&max_uint_rule, max_buffer).unwrap(); + assert!(result.is_some()); + + let min_int_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: true, + }, + op: Operator::Equal, + value: Value::Int(-2_147_483_648), + message: "Min int32".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let min_buffer = &[0x00, 0x00, 0x00, 0x80]; + let result = evaluate_single_rule(&min_int_rule, min_buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_single_rule_various_buffer_sizes() { + let single_byte_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: false }, + op: Operator::Equal, + value: Value::Uint(0xaa), + message: "Single byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let single_buffer = &[0xaa]; + let result = evaluate_single_rule(&single_byte_rule, single_buffer).unwrap(); + assert!(result.is_some()); + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let large_buffer: Vec = (0..1024).map(|i| (i % 256) as u8).collect(); + let large_rule = MagicRule { + offset: OffsetSpec::Absolute(1000), + typ: TypeKind::Byte { signed: false }, + op: Operator::Equal, + value: Value::Uint((1000 % 256) as u64), + message: "Large buffer".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let result = evaluate_single_rule(&large_rule, &large_buffer).unwrap(); + assert!(result.is_some()); + } + + #[test] + fn test_evaluate_rules_empty_list() { + let rules = vec![]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert!(matches.is_empty()); + } + + #[test] + fn test_evaluate_rules_single_matching_rule() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].message, "ELF magic"); + assert_eq!(matches[0].offset, 0); + assert_eq!(matches[0].level, 0); + assert_eq!(matches[0].value, Value::Int(0x7f)); + } + + #[test] + fn test_evaluate_rules_single_non_matching_rule() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x50), + message: "ZIP magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert!(matches.is_empty()); + } + + #[test] + fn test_evaluate_rules_multiple_rules_stop_at_first() { + let rule1 = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "First match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule2 = MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Second match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule_list = vec![rule1, rule2]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + stop_at_first_match: true, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rule_list, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].message, "First match"); + } + + #[test] + fn test_evaluate_rules_multiple_rules_find_all() { + let rule1 = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "First match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule2 = MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Second match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule_set = vec![rule1, rule2]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + stop_at_first_match: false, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rule_set, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "First match"); + assert_eq!(matches[1].message, "Second match"); + } + + #[test] + fn test_evaluate_rules_hierarchical_parent_child() { + let child_rule = MagicRule { + offset: OffsetSpec::Absolute(4), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x02), + message: "64-bit".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }; + + let parent_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF".to_string(), + children: vec![child_rule], + level: 0, + strength_modifier: None, + }; + + let rules = vec![parent_rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "ELF"); + assert_eq!(matches[0].level, 0); + assert_eq!(matches[1].message, "64-bit"); + assert_eq!(matches[1].level, 1); + } + + #[test] + fn test_evaluate_rules_hierarchical_parent_no_match() { + let child_rule = MagicRule { + offset: OffsetSpec::Absolute(4), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x02), + message: "64-bit".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }; + + let parent_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x50), + message: "ZIP".to_string(), + children: vec![child_rule], + level: 0, + strength_modifier: None, + }; + + let rules = vec![parent_rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert!(matches.is_empty()); + } + + #[test] + fn test_evaluate_rules_hierarchical_parent_match_child_no_match() { + let child_rule = MagicRule { + offset: OffsetSpec::Absolute(4), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x01), + message: "32-bit".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }; + + let parent_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF".to_string(), + children: vec![child_rule], + level: 0, + strength_modifier: None, + }; + + let rules = vec![parent_rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].message, "ELF"); + assert_eq!(matches[0].level, 0); + } + + #[test] + fn test_evaluate_rules_deep_hierarchy() { + let grandchild_rule = MagicRule { + offset: OffsetSpec::Absolute(5), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x01), + message: "little-endian".to_string(), + children: vec![], + level: 2, + strength_modifier: None, + }; + + let child_rule = MagicRule { + offset: OffsetSpec::Absolute(4), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x02), + message: "64-bit".to_string(), + children: vec![grandchild_rule], + level: 1, + strength_modifier: None, + }; + + let parent_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF".to_string(), + children: vec![child_rule], + level: 0, + strength_modifier: None, + }; + + let rules = vec![parent_rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 3); + assert_eq!(matches[0].message, "ELF"); + assert_eq!(matches[0].level, 0); + assert_eq!(matches[1].message, "64-bit"); + assert_eq!(matches[1].level, 1); + assert_eq!(matches[2].message, "little-endian"); + assert_eq!(matches[2].level, 2); + } + + #[test] + fn test_evaluate_rules_multiple_children() { + let child1 = MagicRule { + offset: OffsetSpec::Absolute(4), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x02), + message: "64-bit".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }; + + let child2 = MagicRule { + offset: OffsetSpec::Absolute(5), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x01), + message: "little-endian".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }; + + let parent_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF".to_string(), + children: vec![child1, child2], + level: 0, + strength_modifier: None, + }; + + let rules = vec![parent_rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let config = EvaluationConfig { + stop_at_first_match: false, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 3); + assert_eq!(matches[0].message, "ELF"); + assert_eq!(matches[1].message, "64-bit"); + assert_eq!(matches[2].message, "little-endian"); + } + + #[test] + fn test_evaluate_rules_recursion_depth_limit() { + let mut current_rule = MagicRule { + offset: OffsetSpec::Absolute(10), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Deep level".to_string(), + children: vec![], + level: 10, + strength_modifier: None, + }; + + for i in (0u32..10u32).rev() { + current_rule = MagicRule { + offset: OffsetSpec::Absolute(i64::from(i)), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(u64::from(i)), + message: format!("Level {i}"), + children: vec![current_rule], + level: i, + strength_modifier: None, + }; + } + + let rules = vec![current_rule]; + let buffer = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + let config = EvaluationConfig { + max_recursion_depth: 5, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let result = evaluate_rules(&rules, buffer, &mut context); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Recursion limit exceeded")); + } + _ => panic!("Expected EvaluationError for recursion limit"), + } + } + + #[test] + fn test_evaluate_rules_with_config_convenience() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + + let matches = evaluate_rules_with_config(&rules, buffer, &config).unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].message, "ELF magic"); + } + + #[test] + fn test_evaluate_rules_timeout() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + timeout_ms: Some(0), + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let result = evaluate_rules(&rules, buffer, &mut context); + if let Err(LibmagicError::Timeout { timeout_ms }) = result { + assert_eq!(timeout_ms, 0); + } + } + + #[test] + fn test_evaluate_rules_empty_buffer() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Should not match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let result = evaluate_rules(&rules, buffer, &mut context); + assert!(result.is_ok()); + + let matches = result.unwrap(); + assert_eq!(matches.len(), 0); + } + + #[test] + fn test_evaluate_rules_mixed_matching_non_matching() { + let rule1 = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Matches".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule2 = MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x99), + message: "Doesn't match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule3 = MagicRule { + offset: OffsetSpec::Absolute(2), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x4c), + message: "Also matches".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule_collection = vec![rule1, rule2, rule3]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + stop_at_first_match: false, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rule_collection, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "Matches"); + assert_eq!(matches[1].message, "Also matches"); + } + + #[test] + fn test_evaluate_rules_context_state_preservation() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + context.set_current_offset(100); + let initial_offset = context.current_offset(); + let initial_depth = context.recursion_depth(); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1); + + assert_eq!(context.current_offset(), initial_offset); + assert_eq!(context.recursion_depth(), initial_depth); + } + + #[test] + fn test_error_recovery_skip_problematic_rules() { + let rules = vec![ + MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Valid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Invalid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Another valid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + ]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + max_recursion_depth: 20, + max_string_length: 8192, + stop_at_first_match: false, + enable_mime_types: false, + timeout_ms: None, + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "Valid rule"); + assert_eq!(matches[1].message, "Another valid rule"); + } + + #[test] + fn test_error_recovery_child_rule_failures() { + let rules = vec![MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Parent rule".to_string(), + children: vec![ + MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Valid child".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Invalid child".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }, + ], + level: 0, + strength_modifier: None, + }]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "Parent rule"); + assert_eq!(matches[1].message, "Valid child"); + } + + #[test] + fn test_error_recovery_mixed_rule_types() { + let rules = vec![ + MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Valid byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(3), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234), + message: "Invalid short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::String { + max_length: Some(3), + }, + op: Operator::Equal, + value: Value::String("ELF".to_string()), + message: "Valid string".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + ]; + + let buffer = &[0x7f, b'E', b'L', b'F']; + let config = EvaluationConfig { + max_recursion_depth: 20, + max_string_length: 8192, + stop_at_first_match: false, + enable_mime_types: false, + timeout_ms: None, + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "Valid byte"); + assert_eq!(matches[1].message, "Valid string"); + } + + #[test] + fn test_error_recovery_all_rules_fail() { + let rules = vec![ + MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Out of bounds".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(2), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234_5678), + message: "Insufficient bytes".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + ]; + + let buffer = &[0x7f, 0x45]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 0); + } + + #[test] + fn test_error_recovery_timeout_propagation() { + let rules = vec![MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Test rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + max_recursion_depth: 10, + max_string_length: 1024, + stop_at_first_match: false, + enable_mime_types: false, + timeout_ms: Some(0), + }; + let mut context = EvaluationContext::new(config); + + let result = evaluate_rules(&rules, buffer, &mut context); + + match result { + Ok(_) | Err(LibmagicError::Timeout { .. }) => {} + Err(e) => { + panic!("Unexpected error type: {e:?}"); + } + } + } + + #[test] + fn test_error_recovery_recursion_limit_propagation() { + let rules = vec![MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Parent".to_string(), + children: vec![MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Child".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }], + level: 0, + strength_modifier: None, + }]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + max_recursion_depth: 0, + max_string_length: 1024, + stop_at_first_match: false, + enable_mime_types: false, + timeout_ms: None, + }; + let mut context = EvaluationContext::new(config); + + let result = evaluate_rules(&rules, buffer, &mut context); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError( + crate::error::EvaluationError::RecursionLimitExceeded { .. }, + ) => {} + _ => panic!("Expected recursion limit error"), + } + } + + #[test] + fn test_error_recovery_preserves_context_state() { + let rules = vec![ + MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Valid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Invalid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + ]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + context.set_current_offset(42); + let initial_offset = context.current_offset(); + let initial_depth = context.recursion_depth(); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1); + + assert_eq!(context.current_offset(), initial_offset); + assert_eq!(context.recursion_depth(), initial_depth); + } + + #[test] + fn test_any_value_parse_and_evaluate_paren_message() { + use crate::parser::grammar::parse_magic_rule; + + let input = ">0 byte x (0)"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::AnyValue); + assert_eq!(rule.message, "(0)"); + + let buffer = &[0x00, 0x01, 0x02, 0x03]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_some(), + "AnyValue rule should match unconditionally" + ); + } + + #[test] + fn test_any_value_parse_and_evaluate_backslash_message() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 long x \\b, data"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::AnyValue); + assert_eq!(rule.message, "\\b, data"); + + let buffer = &[0xFF, 0xFE, 0xFD, 0xFC]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_some(), + "AnyValue rule should match unconditionally" + ); + } + + #[test] + fn test_any_value_parse_and_evaluate_no_message() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 byte x"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::AnyValue); + + let buffer = &[0x42]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_some(), + "AnyValue rule should match unconditionally" + ); + } + + #[test] + fn test_bitwise_xor_parse_and_evaluate_match() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 byte ^0x01 XOR match"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::BitwiseXor); + assert_eq!(rule.message, "XOR match"); + + let buffer = &[0x0F]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_some(), + "BitwiseXor should match when XOR is non-zero" + ); + } + + #[test] + fn test_bitwise_xor_parse_and_evaluate_no_match() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 byte ^0x42 XOR no match"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::BitwiseXor); + + let buffer = &[0x42]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_none(), + "BitwiseXor should not match when XOR is zero" + ); + } + + #[test] + fn test_bitwise_not_parse_and_evaluate_match() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 ubyte ~0xFF NOT match"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::BitwiseNot); + assert_eq!(rule.message, "NOT match"); + + let buffer = &[0x00]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_some(), + "BitwiseNot should match when NOT(value) equals operand at byte width" + ); + } + + #[test] + fn test_bitwise_not_parse_and_evaluate_no_match() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 ubyte ~0x01 NOT no match"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::BitwiseNot); + + let buffer = &[0x42]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_none(), + "BitwiseNot should not match when NOT(value) != operand" + ); + } + + #[test] + fn test_debug_error_recovery() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Out of bounds rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45]; + + let single_result = evaluate_single_rule(&rule, buffer); + assert!(single_result.is_err()); + + let rules = vec![rule]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 0); + } + + #[test] + fn test_debug_mixed_rules() { + let rules = vec![ + MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Valid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Invalid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Another valid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + ]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + + let config = EvaluationConfig { + stop_at_first_match: false, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 2); + } +} diff --git a/src/evaluator/mod.rs b/src/evaluator/mod.rs index 8fcfbf84..57bd43a2 100644 --- a/src/evaluator/mod.rs +++ b/src/evaluator/mod.rs @@ -6,15 +6,21 @@ //! This module contains the core evaluation logic for executing magic rules //! against file buffers to identify file types. -use crate::parser::ast::MagicRule; use crate::{EvaluationConfig, LibmagicError}; use serde::{Deserialize, Serialize}; +mod engine; pub mod offset; pub mod operators; pub mod strength; pub mod types; +pub use engine::{evaluate_rules, evaluate_rules_with_config, evaluate_single_rule}; +pub use offset::*; +pub use operators::*; +pub use strength::*; +pub use types::*; + /// Context for maintaining evaluation state during rule processing /// /// The `EvaluationContext` tracks the current state of rule evaluation, @@ -242,321 +248,421 @@ impl RuleMatch { (0.3 + (f64::from(level) * 0.2)).min(1.0) } } +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::ast::Value; + + #[test] + fn test_evaluation_context_new() { + let config = EvaluationConfig::default(); + let context = EvaluationContext::new(config.clone()); + + assert_eq!(context.current_offset(), 0); + assert_eq!(context.recursion_depth(), 0); + assert_eq!( + context.config().max_recursion_depth, + config.max_recursion_depth + ); + assert_eq!(context.config().max_string_length, config.max_string_length); + assert_eq!( + context.config().stop_at_first_match, + config.stop_at_first_match + ); + } -/// Evaluate a single magic rule against a file buffer -/// -/// This function performs the core rule evaluation by: -/// 1. Resolving the rule's offset specification to an absolute position -/// 2. Reading and interpreting bytes at that position according to the rule's type -/// 3. Applying the rule's operator to compare the read value with the expected value -/// -/// # Arguments -/// -/// * `rule` - The magic rule to evaluate -/// * `buffer` - The file buffer to evaluate against -/// -/// # Returns -/// -/// Returns `Ok(Some((offset, value)))` if the rule matches (with the resolved offset and -/// read value), `Ok(None)` if it doesn't match, or `Err(LibmagicError)` if evaluation -/// fails due to buffer access issues or other errors. -/// -/// # Examples -/// -/// ```rust -/// use libmagic_rs::evaluator::evaluate_single_rule; -/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; -/// -/// // Create a rule to check for ELF magic bytes at offset 0 -/// let rule = MagicRule { -/// offset: OffsetSpec::Absolute(0), -/// typ: TypeKind::Byte { signed: true }, -/// op: Operator::Equal, -/// value: Value::Uint(0x7f), -/// message: "ELF magic".to_string(), -/// children: vec![], -/// level: 0, -/// strength_modifier: None, -/// }; -/// -/// let elf_buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes -/// let result = evaluate_single_rule(&rule, elf_buffer).unwrap(); -/// assert!(result.is_some()); // Should match -/// -/// let non_elf_buffer = &[0x50, 0x4b, 0x03, 0x04]; // ZIP magic bytes -/// let result = evaluate_single_rule(&rule, non_elf_buffer).unwrap(); -/// assert!(result.is_none()); // Should not match -/// ``` -/// -/// # Errors -/// -/// * `LibmagicError::EvaluationError` - If offset resolution fails, buffer access is out of bounds, -/// or type interpretation fails -pub fn evaluate_single_rule( - rule: &MagicRule, - buffer: &[u8], -) -> Result, LibmagicError> { - // Step 1: Resolve the offset specification to an absolute position - let absolute_offset = offset::resolve_offset(&rule.offset, buffer)?; - - // Step 2: Read and interpret bytes at the resolved offset according to the rule's type - let read_value = types::read_typed_value(buffer, absolute_offset, &rule.typ) - .map_err(|e| LibmagicError::EvaluationError(e.into()))?; - - // Step 3: Coerce the rule's expected value to match the type's signedness/width - let expected_value = types::coerce_value_to_type(&rule.value, &rule.typ); - - // Step 4: Apply the operator to compare the read value with the expected value - // BitwiseNot needs type-aware bit-width masking so the complement is computed - // at the type's natural width (e.g., byte NOT of 0x00 = 0xFF, not u64::MAX). - let matched = match &rule.op { - crate::parser::ast::Operator::BitwiseNot => operators::apply_bitwise_not_with_width( - &read_value, - &expected_value, - rule.typ.bit_width(), - ), - op => operators::apply_operator(op, &read_value, &expected_value), - }; - Ok(matched.then_some((absolute_offset, read_value))) -} + #[test] + fn test_evaluation_context_offset_management() { + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); -/// Evaluate a list of magic rules against a file buffer with hierarchical processing -/// -/// This function implements the core hierarchical rule evaluation algorithm with graceful -/// error handling: -/// 1. Evaluates each top-level rule in sequence -/// 2. If a parent rule matches, evaluates its child rules for refinement -/// 3. Collects all matches or stops at first match based on configuration -/// 4. Maintains evaluation context for recursion limits and state -/// 5. Implements graceful degradation by skipping problematic rules and continuing evaluation -/// -/// The hierarchical evaluation follows these principles: -/// - Parent rules must match before children are evaluated -/// - Child rules provide refinement and additional detail -/// - Evaluation can stop at first match or continue for all matches -/// - Recursion depth is limited to prevent infinite loops -/// - Problematic rules are skipped to allow evaluation to continue -/// -/// # Arguments -/// -/// * `rules` - The list of magic rules to evaluate -/// * `buffer` - The file buffer to evaluate against -/// * `context` - Mutable evaluation context for state management -/// -/// # Returns -/// -/// Returns `Ok(Vec)` containing all matches found. Errors in individual rules -/// are logged and skipped to allow evaluation to continue. Only returns `Err(LibmagicError)` -/// for critical failures like timeout or recursion limit exceeded. -/// -/// # Examples -/// -/// ```rust -/// use libmagic_rs::evaluator::{evaluate_rules, EvaluationContext, RuleMatch}; -/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; -/// use libmagic_rs::EvaluationConfig; -/// -/// // Create a hierarchical rule set for ELF files -/// let parent_rule = MagicRule { -/// offset: OffsetSpec::Absolute(0), -/// typ: TypeKind::Byte { signed: true }, -/// op: Operator::Equal, -/// value: Value::Uint(0x7f), -/// message: "ELF".to_string(), -/// children: vec![ -/// MagicRule { -/// offset: OffsetSpec::Absolute(4), -/// typ: TypeKind::Byte { signed: true }, -/// op: Operator::Equal, -/// value: Value::Uint(2), -/// message: "64-bit".to_string(), -/// children: vec![], -/// level: 1, -/// strength_modifier: None, -/// } -/// ], -/// level: 0, -/// strength_modifier: None, -/// }; -/// -/// let rules = vec![parent_rule]; -/// let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; // ELF64 header -/// let config = EvaluationConfig::default(); -/// let mut context = EvaluationContext::new(config); -/// -/// let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); -/// assert_eq!(matches.len(), 2); // Parent and child should both match -/// ``` -/// -/// # Errors -/// -/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout -/// * `LibmagicError::EvaluationError` - Only for critical failures like recursion limit exceeded -/// -/// Individual rule evaluation errors are handled gracefully and do not stop the overall evaluation. -pub fn evaluate_rules( - rules: &[MagicRule], - buffer: &[u8], - context: &mut EvaluationContext, -) -> Result, LibmagicError> { - let mut matches = Vec::with_capacity(8); - let start_time = std::time::Instant::now(); - let mut rule_count = 0u32; - - for rule in rules { - // Check timeout periodically (every 16 rules) to reduce syscall overhead - rule_count = rule_count.wrapping_add(1); - if rule_count.trailing_zeros() >= 4 - && let Some(timeout_ms) = context.timeout_ms() - && start_time.elapsed().as_millis() > u128::from(timeout_ms) - { - return Err(LibmagicError::Timeout { timeout_ms }); - } + // Test initial offset + assert_eq!(context.current_offset(), 0); - // Evaluate the current rule with graceful error handling - let match_data = match evaluate_single_rule(rule, buffer) { - Ok(data) => data, - Err( - LibmagicError::EvaluationError( - crate::error::EvaluationError::BufferOverrun { .. } - | crate::error::EvaluationError::InvalidOffset { .. } - | crate::error::EvaluationError::TypeReadError(_), - ) - | LibmagicError::IoError(_), - ) => { - // Expected evaluation errors for individual rules -- skip gracefully - continue; - } - Err(e) => { - // Unexpected errors (InternalError, UnsupportedType, etc.) should propagate - return Err(e); - } + // Test setting offset + context.set_current_offset(42); + assert_eq!(context.current_offset(), 42); + + // Test setting different offset + context.set_current_offset(1024); + assert_eq!(context.current_offset(), 1024); + + // Test setting offset to 0 + context.set_current_offset(0); + assert_eq!(context.current_offset(), 0); + } + + #[test] + fn test_evaluation_context_recursion_depth_management() { + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + // Test initial recursion depth + assert_eq!(context.recursion_depth(), 0); + + // Test incrementing recursion depth + context.increment_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 1); + + context.increment_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 2); + + // Test decrementing recursion depth + context.decrement_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 1); + + context.decrement_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 0); + } + + #[test] + fn test_evaluation_context_recursion_depth_limit() { + let config = EvaluationConfig { + max_recursion_depth: 2, + ..Default::default() }; + let mut context = EvaluationContext::new(config); - if let Some((absolute_offset, read_value)) = match_data { - let match_result = RuleMatch { - message: rule.message.clone(), - offset: absolute_offset, - level: rule.level, - value: read_value, - confidence: RuleMatch::calculate_confidence(rule.level), - }; - matches.push(match_result); - - // If this rule has children, evaluate them recursively - if !rule.children.is_empty() { - // Check recursion depth limit - this is a critical error that should stop evaluation - context.increment_recursion_depth()?; - - // Recursively evaluate child rules with graceful error handling - match evaluate_rules(&rule.children, buffer, context) { - Ok(child_matches) => { - matches.extend(child_matches); - } - Err(LibmagicError::Timeout { .. }) => { - // Timeout is critical, propagate it up - context.decrement_recursion_depth()?; - return Err(LibmagicError::Timeout { - timeout_ms: context.timeout_ms().unwrap_or(0), - }); - } - Err(LibmagicError::EvaluationError( - crate::error::EvaluationError::RecursionLimitExceeded { .. }, - )) => { - // Recursion limit is critical, propagate it up - context.decrement_recursion_depth()?; - return Err(LibmagicError::EvaluationError( - crate::error::EvaluationError::RecursionLimitExceeded { - depth: context.recursion_depth(), - }, - )); - } - Err( - LibmagicError::EvaluationError( - crate::error::EvaluationError::BufferOverrun { .. } - | crate::error::EvaluationError::InvalidOffset { .. } - | crate::error::EvaluationError::TypeReadError(_), - ) - | LibmagicError::IoError(_), - ) => { - // Expected child evaluation errors -- skip gracefully - } - Err(e) => { - // Unexpected errors in children should propagate - context.decrement_recursion_depth()?; - return Err(e); - } - } - - // Restore recursion depth - context.decrement_recursion_depth()?; - } + // Should be able to increment up to the limit + assert!(context.increment_recursion_depth().is_ok()); + assert_eq!(context.recursion_depth(), 1); - // Stop at first match if configured to do so - if context.should_stop_at_first_match() { - break; + assert!(context.increment_recursion_depth().is_ok()); + assert_eq!(context.recursion_depth(), 2); + + // Should fail when exceeding the limit + let result = context.increment_recursion_depth(); + assert!(result.is_err()); + assert_eq!(context.recursion_depth(), 2); // Should not have changed + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Recursion limit exceeded")); } + _ => panic!("Expected EvaluationError"), } } - Ok(matches) -} + #[test] + fn test_evaluation_context_recursion_depth_underflow() { + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + // Should return an error when trying to decrement below 0 + let result = context.decrement_recursion_depth(); + assert!(result.is_err()); + + let err = result.unwrap_err(); + let err_msg = err.to_string(); + assert!( + err_msg.contains("decrement recursion depth below 0"), + "Expected error about decrementing below 0, got: {err_msg}" + ); + } -/// Evaluate magic rules with a fresh context -/// -/// This is a convenience function that creates a new evaluation context -/// and evaluates the rules. Useful for simple evaluation scenarios. -/// -/// # Arguments -/// -/// * `rules` - The list of magic rules to evaluate -/// * `buffer` - The file buffer to evaluate against -/// * `config` - Configuration for evaluation behavior -/// -/// # Returns -/// -/// Returns `Ok(Vec)` containing all matches found, or `Err(LibmagicError)` -/// if evaluation fails. -/// -/// # Examples -/// -/// ```rust -/// use libmagic_rs::evaluator::{evaluate_rules_with_config, RuleMatch}; -/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; -/// use libmagic_rs::EvaluationConfig; -/// -/// let rule = MagicRule { -/// offset: OffsetSpec::Absolute(0), -/// typ: TypeKind::Byte { signed: true }, -/// op: Operator::Equal, -/// value: Value::Uint(0x7f), -/// message: "ELF magic".to_string(), -/// children: vec![], -/// level: 0, -/// strength_modifier: None, -/// }; -/// -/// let rules = vec![rule]; -/// let buffer = &[0x7f, 0x45, 0x4c, 0x46]; -/// let config = EvaluationConfig::default(); -/// -/// let matches = evaluate_rules_with_config(&rules, buffer, &config).unwrap(); -/// assert_eq!(matches.len(), 1); -/// assert_eq!(matches[0].message, "ELF magic"); -/// ``` -/// -/// # Errors -/// -/// * `LibmagicError::EvaluationError` - If rule evaluation fails -/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout -pub fn evaluate_rules_with_config( - rules: &[MagicRule], - buffer: &[u8], - config: &EvaluationConfig, -) -> Result, LibmagicError> { - let mut context = EvaluationContext::new(config.clone()); - evaluate_rules(rules, buffer, &mut context) -} + #[test] + fn test_evaluation_context_config_access() { + let config = EvaluationConfig { + max_recursion_depth: 10, + max_string_length: 4096, + stop_at_first_match: false, + enable_mime_types: true, + timeout_ms: Some(2000), + }; -#[cfg(test)] -mod tests; + let context = EvaluationContext::new(config); + + // Test config access + assert_eq!(context.config().max_recursion_depth, 10); + assert_eq!(context.config().max_string_length, 4096); + assert!(!context.config().stop_at_first_match); + + // Test convenience methods + assert!(!context.should_stop_at_first_match()); + assert_eq!(context.max_string_length(), 4096); + } + + #[test] + fn test_evaluation_context_reset() { + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config.clone()); + + // Modify the context state + context.set_current_offset(100); + context.increment_recursion_depth().unwrap(); + context.increment_recursion_depth().unwrap(); + + assert_eq!(context.current_offset(), 100); + assert_eq!(context.recursion_depth(), 2); + + // Reset should restore initial state but keep config + context.reset(); + + assert_eq!(context.current_offset(), 0); + assert_eq!(context.recursion_depth(), 0); + assert_eq!( + context.config().max_recursion_depth, + config.max_recursion_depth + ); + } + + #[test] + fn test_evaluation_context_clone() { + let config = EvaluationConfig { + max_recursion_depth: 5, + max_string_length: 2048, + ..Default::default() + }; + + let mut context = EvaluationContext::new(config); + context.set_current_offset(50); + context.increment_recursion_depth().unwrap(); + + // Clone the context + let cloned_context = context.clone(); + + // Both should have the same state + assert_eq!(context.current_offset(), cloned_context.current_offset()); + assert_eq!(context.recursion_depth(), cloned_context.recursion_depth()); + assert_eq!( + context.config().max_recursion_depth, + cloned_context.config().max_recursion_depth + ); + assert_eq!( + context.config().max_string_length, + cloned_context.config().max_string_length + ); + + // Modifying one should not affect the other + context.set_current_offset(75); + assert_eq!(context.current_offset(), 75); + assert_eq!(cloned_context.current_offset(), 50); + } + + #[test] + fn test_evaluation_context_with_custom_config() { + let config = EvaluationConfig { + max_recursion_depth: 15, + max_string_length: 16384, + stop_at_first_match: false, + enable_mime_types: true, + timeout_ms: Some(5000), + }; + + let context = EvaluationContext::new(config); + + assert_eq!(context.config().max_recursion_depth, 15); + assert_eq!(context.max_string_length(), 16384); + assert!(!context.should_stop_at_first_match()); + + // Test that we can increment up to the custom limit + let mut mutable_context = context; + for i in 1..=15 { + assert!(mutable_context.increment_recursion_depth().is_ok()); + assert_eq!(mutable_context.recursion_depth(), i); + } + + // Should fail on the 16th increment + let result = mutable_context.increment_recursion_depth(); + assert!(result.is_err()); + } + + #[test] + fn test_evaluation_context_mime_types_access() { + let config_with_mime = EvaluationConfig { + enable_mime_types: true, + ..Default::default() + }; + let context_with_mime = EvaluationContext::new(config_with_mime); + assert!(context_with_mime.enable_mime_types()); + + let config_without_mime = EvaluationConfig { + enable_mime_types: false, + ..Default::default() + }; + let context_without_mime = EvaluationContext::new(config_without_mime); + assert!(!context_without_mime.enable_mime_types()); + } + + #[test] + fn test_evaluation_context_timeout_access() { + let config_with_timeout = EvaluationConfig { + timeout_ms: Some(5000), + ..Default::default() + }; + let context_with_timeout = EvaluationContext::new(config_with_timeout); + assert_eq!(context_with_timeout.timeout_ms(), Some(5000)); + + let config_without_timeout = EvaluationConfig { + timeout_ms: None, + ..Default::default() + }; + let context_without_timeout = EvaluationContext::new(config_without_timeout); + assert_eq!(context_without_timeout.timeout_ms(), None); + } + + #[test] + fn test_evaluation_context_comprehensive_config() { + let config = EvaluationConfig { + max_recursion_depth: 30, + max_string_length: 16384, + stop_at_first_match: false, + enable_mime_types: true, + timeout_ms: Some(10000), + }; + let context = EvaluationContext::new(config); + + assert_eq!(context.config().max_recursion_depth, 30); + assert_eq!(context.config().max_string_length, 16384); + assert!(!context.should_stop_at_first_match()); + assert!(context.enable_mime_types()); + assert_eq!(context.timeout_ms(), Some(10000)); + assert_eq!(context.max_string_length(), 16384); + } + + #[test] + fn test_evaluation_context_performance_config() { + let config = EvaluationConfig { + max_recursion_depth: 5, + max_string_length: 512, + stop_at_first_match: true, + enable_mime_types: false, + timeout_ms: Some(1000), + }; + let context = EvaluationContext::new(config); + + assert_eq!(context.config().max_recursion_depth, 5); + assert_eq!(context.max_string_length(), 512); + assert!(context.should_stop_at_first_match()); + assert!(!context.enable_mime_types()); + assert_eq!(context.timeout_ms(), Some(1000)); + } + + #[test] + fn test_evaluation_context_state_management_sequence() { + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + // Simulate a sequence of evaluation operations + assert_eq!(context.current_offset(), 0); + assert_eq!(context.recursion_depth(), 0); + + // Start evaluation at offset 10 + context.set_current_offset(10); + assert_eq!(context.current_offset(), 10); + + // Enter nested rule evaluation + context.increment_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 1); + + // Move to different offset during nested evaluation + context.set_current_offset(25); + assert_eq!(context.current_offset(), 25); + + // Enter deeper nesting + context.increment_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 2); + + // Exit nested evaluation + context.decrement_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 1); + + // Continue evaluation at different offset + context.set_current_offset(50); + assert_eq!(context.current_offset(), 50); + + // Exit all nesting + context.decrement_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 0); + + // Final state check + assert_eq!(context.current_offset(), 50); + assert_eq!(context.recursion_depth(), 0); + } + + #[test] + fn test_rule_match_creation() { + let match_result = RuleMatch { + message: "ELF executable".to_string(), + offset: 0, + level: 0, + value: Value::Uint(0x7f), + confidence: RuleMatch::calculate_confidence(0), + }; + + assert_eq!(match_result.message, "ELF executable"); + assert_eq!(match_result.offset, 0); + assert_eq!(match_result.level, 0); + assert_eq!(match_result.value, Value::Uint(0x7f)); + assert!((match_result.confidence - 0.3).abs() < 0.001); + } + + #[test] + fn test_rule_match_clone() { + let original = RuleMatch { + message: "Test message".to_string(), + offset: 42, + level: 1, + value: Value::String("test".to_string()), + confidence: RuleMatch::calculate_confidence(1), + }; + + let cloned = original.clone(); + assert_eq!(original, cloned); + } + + #[test] + fn test_rule_match_debug() { + let match_result = RuleMatch { + message: "Debug test".to_string(), + offset: 10, + level: 2, + value: Value::Bytes(vec![0x01, 0x02]), + confidence: RuleMatch::calculate_confidence(2), + }; + + let debug_str = format!("{match_result:?}"); + assert!(debug_str.contains("RuleMatch")); + assert!(debug_str.contains("Debug test")); + assert!(debug_str.contains("10")); + assert!(debug_str.contains('2')); + } + + #[test] + fn test_confidence_calculation_depth_0() { + let confidence = RuleMatch::calculate_confidence(0); + assert!((confidence - 0.3).abs() < 0.001); + } + + #[test] + fn test_confidence_calculation_depth_1() { + let confidence = RuleMatch::calculate_confidence(1); + assert!((confidence - 0.5).abs() < 0.001); + } + + #[test] + fn test_confidence_calculation_depth_2() { + let confidence = RuleMatch::calculate_confidence(2); + assert!((confidence - 0.7).abs() < 0.001); + } + + #[test] + fn test_confidence_calculation_depth_3() { + let confidence = RuleMatch::calculate_confidence(3); + assert!((confidence - 0.9).abs() < 0.001); + } + + #[test] + fn test_confidence_calculation_capped_at_1() { + // Level 4+ should cap at 1.0 + let confidence_4 = RuleMatch::calculate_confidence(4); + assert!((confidence_4 - 1.0).abs() < 0.001); + + let confidence_10 = RuleMatch::calculate_confidence(10); + assert!((confidence_10 - 1.0).abs() < 0.001); + + let confidence_100 = RuleMatch::calculate_confidence(100); + assert!((confidence_100 - 1.0).abs() < 0.001); + } +} diff --git a/src/evaluator/tests.rs b/src/evaluator/tests.rs deleted file mode 100644 index 7dbabaab..00000000 --- a/src/evaluator/tests.rs +++ /dev/null @@ -1,2385 +0,0 @@ -// Copyright (c) 2025-2026 the libmagic-rs contributors -// SPDX-License-Identifier: Apache-2.0 - -use super::*; -use crate::parser::ast::{Endianness, OffsetSpec, Operator, TypeKind, Value}; - -#[test] -fn test_evaluate_single_rule_byte_equal_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); -} - -#[test] -fn test_evaluate_single_rule_byte_equal_no_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x50, 0x4b, 0x03, 0x04]; // ZIP magic bytes - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_none()); -} - -#[test] -fn test_evaluate_single_rule_byte_not_equal_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::NotEqual, - value: Value::Uint(0x00), - message: "Non-zero byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // 0x7f != 0x00 -} - -#[test] -fn test_evaluate_single_rule_byte_not_equal_no_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::NotEqual, - value: Value::Uint(0x7f), - message: "Not ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_none()); // 0x7f == 0x7f, so NotEqual is false -} - -#[test] -fn test_evaluate_single_rule_byte_bitwise_and_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::BitwiseAnd, - value: Value::Uint(0x80), // Check if high bit is set - message: "High bit set".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0xff, 0x45, 0x4c, 0x46]; // 0xff has high bit set - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // 0xff & 0x80 = 0x80 (non-zero) -} - -#[test] -fn test_evaluate_single_rule_byte_bitwise_and_no_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::BitwiseAnd, - value: Value::Uint(0x80), // Check if high bit is set - message: "High bit set".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // 0x7f has high bit clear - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_none()); // 0x7f & 0x80 = 0x00 (zero) -} - -#[test] -fn test_evaluate_single_rule_short_little_endian() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234), - message: "Little-endian short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x34, 0x12, 0x56, 0x78]; // 0x1234 in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); -} - -#[test] -fn test_evaluate_single_rule_short_big_endian() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Big, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234), - message: "Big-endian short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x12, 0x34, 0x56, 0x78]; // 0x1234 in big-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); -} - -#[test] -fn test_evaluate_single_rule_short_signed_positive() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Little, - signed: true, - }, - op: Operator::Equal, - value: Value::Int(32767), // 0x7fff - message: "Positive signed short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0xff, 0x7f, 0x00, 0x00]; // 0x7fff in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); -} - -#[test] -fn test_evaluate_single_rule_short_signed_negative() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Little, - signed: true, - }, - op: Operator::Equal, - value: Value::Int(-1), // 0xffff as signed - message: "Negative signed short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0xff, 0xff, 0x00, 0x00]; // 0xffff in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); -} - -#[test] -fn test_evaluate_single_rule_long_little_endian() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234_5678), - message: "Little-endian long".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x78, 0x56, 0x34, 0x12, 0x00]; // 0x12345678 in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); -} - -#[test] -fn test_evaluate_single_rule_long_big_endian() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Big, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234_5678), - message: "Big-endian long".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x12, 0x34, 0x56, 0x78, 0x00]; // 0x12345678 in big-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); -} - -#[test] -fn test_evaluate_single_rule_long_signed_positive() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: true, - }, - op: Operator::Equal, - value: Value::Int(2_147_483_647), // 0x7fffffff - message: "Positive signed long".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0xff, 0xff, 0xff, 0x7f, 0x00]; // 0x7fffffff in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); -} - -#[test] -fn test_evaluate_single_rule_long_signed_negative() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: true, - }, - op: Operator::Equal, - value: Value::Int(-1), // 0xffffffff as signed - message: "Negative signed long".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0xff, 0xff, 0xff, 0xff, 0x00]; // 0xffffffff in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); -} - -#[test] -fn test_evaluate_single_rule_different_offsets() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(2), // Read from offset 2 - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x4c), - message: "ELF class byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // buffer[2] == 0x4c -} - -#[test] -fn test_evaluate_single_rule_negative_offset() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(-1), // Last byte - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x46), - message: "Last byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // Last byte is 0x46 -} - -#[test] -fn test_evaluate_single_rule_from_end_offset() { - let rule = MagicRule { - offset: OffsetSpec::FromEnd(-2), // Second to last byte - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x4c), - message: "Second to last byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // buffer[2] == 0x4c (second to last) -} - -#[test] -fn test_evaluate_single_rule_offset_out_of_bounds() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(10), // Beyond buffer - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Out of bounds".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // Only 4 bytes - let result = evaluate_single_rule(&rule, buffer); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Buffer overrun")); - } - _ => panic!("Expected EvaluationError"), - } -} - -#[test] -fn test_evaluate_single_rule_short_insufficient_bytes() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(3), // Only 1 byte left - typ: TypeKind::Short { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234), - message: "Insufficient bytes".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // 4 bytes total - let result = evaluate_single_rule(&rule, buffer); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Buffer overrun")); - } - _ => panic!("Expected EvaluationError"), - } -} - -#[test] -fn test_evaluate_single_rule_long_insufficient_bytes() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(2), // Only 2 bytes left - typ: TypeKind::Long { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234_5678), - message: "Insufficient bytes".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // 4 bytes total - let result = evaluate_single_rule(&rule, buffer); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Buffer overrun")); - } - _ => panic!("Expected EvaluationError"), - } -} - -#[test] -fn test_evaluate_single_rule_empty_buffer() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Empty buffer".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[]; // Empty buffer - let result = evaluate_single_rule(&rule, buffer); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Buffer overrun")); - } - _ => panic!("Expected EvaluationError"), - } -} - -#[test] -fn test_evaluate_single_rule_string_type_supported() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::String { max_length: None }, - op: Operator::Equal, - value: Value::String("test".to_string()), - message: "String type".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - // Test matching string - let buffer = b"test\x00 data"; - let result = evaluate_single_rule(&rule, buffer); - assert!(result.is_ok()); - let matches = result.unwrap(); - assert!(matches.is_some()); // Should match - - // Test non-matching string - let rule_no_match = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::String { max_length: None }, - op: Operator::Equal, - value: Value::String("hello".to_string()), - message: "String type".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let result = evaluate_single_rule(&rule_no_match, buffer); - assert!(result.is_ok()); - let matches = result.unwrap(); - assert!(matches.is_none()); // Should not match -} - -#[test] -fn test_evaluate_single_rule_cross_type_comparison() { - // Test that cross-type integer comparisons use coercion (Uint(42) == Int(42)) - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Int(42), // Int value vs Uint from byte read - message: "Cross-type comparison".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[42]; // Byte value 42 - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // Should match via cross-type integer coercion -} - -#[test] -fn test_evaluate_single_rule_bitwise_and_with_shorts() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Little, - signed: false, - }, - op: Operator::BitwiseAnd, - value: Value::Uint(0xff00), // Check high byte - message: "High byte check".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x34, 0x12]; // 0x1234 in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // 0x1234 & 0xff00 = 0x1200 (non-zero) -} - -#[test] -fn test_evaluate_single_rule_bitwise_and_with_longs() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Big, - signed: false, - }, - op: Operator::BitwiseAnd, - value: Value::Uint(0xffff_0000), // Check high word - message: "High word check".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x12, 0x34, 0x56, 0x78]; // 0x12345678 in big-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // 0x12345678 & 0xffff0000 = 0x12340000 (non-zero) -} - -#[test] -fn test_evaluate_single_rule_comprehensive_elf_check() { - // Test a comprehensive ELF magic check - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x464c_457f), // ELF magic as 32-bit little-endian - message: "ELF executable".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let elf_buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; // ELF64 header start - let result = evaluate_single_rule(&rule, elf_buffer).unwrap(); - assert!(result.is_some()); - - let non_elf_buffer = &[0x50, 0x4b, 0x03, 0x04, 0x14, 0x00]; // ZIP header - let result = evaluate_single_rule(&rule, non_elf_buffer).unwrap(); - assert!(result.is_none()); -} - -#[test] -fn test_evaluate_single_rule_native_endianness() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Native, - signed: false, - }, - op: Operator::NotEqual, - value: Value::Uint(0), - message: "Non-zero native short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x01, 0x02]; // Non-zero bytes - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // Should be non-zero regardless of endianness -} - -#[test] -fn test_evaluate_single_rule_all_operators() { - let buffer = &[0x42, 0x00, 0xff, 0x80]; - - // Test Equal operator - let equal_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x42), - message: "Equal test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!(evaluate_single_rule(&equal_rule, buffer).unwrap().is_some()); - - // Test NotEqual operator - let not_equal_rule = MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::NotEqual, - value: Value::Uint(0x42), - message: "NotEqual test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(¬_equal_rule, buffer) - .unwrap() - .is_some() - ); // 0x00 != 0x42 - - // Test BitwiseAnd operator - let bitwise_and_rule = MagicRule { - offset: OffsetSpec::Absolute(3), - typ: TypeKind::Byte { signed: true }, - op: Operator::BitwiseAnd, - value: Value::Uint(0x80), - message: "BitwiseAnd test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&bitwise_and_rule, buffer) - .unwrap() - .is_some() - ); // 0x80 & 0x80 = 0x80 -} - -#[test] -fn test_evaluate_single_rule_comparison_operators() { - let buffer = &[0x42, 0x00, 0xff, 0x80]; - - // LessThan: byte at offset 1 is 0x00, 0x00 < 0x42 = true - let less_than_rule = MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: false }, - op: Operator::LessThan, - value: Value::Uint(0x42), - message: "LessThan test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&less_than_rule, buffer) - .unwrap() - .is_some() - ); - - // GreaterThan: byte at offset 2 is 0xff, 0xff > 0x42 = true - let greater_than_rule = MagicRule { - offset: OffsetSpec::Absolute(2), - typ: TypeKind::Byte { signed: false }, - op: Operator::GreaterThan, - value: Value::Uint(0x42), - message: "GreaterThan test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&greater_than_rule, buffer) - .unwrap() - .is_some() - ); - - // LessEqual: byte at offset 0 is 0x42, 0x42 <= 0x42 = true - let less_equal_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: false }, - op: Operator::LessEqual, - value: Value::Uint(0x42), - message: "LessEqual test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&less_equal_rule, buffer) - .unwrap() - .is_some() - ); - - // GreaterEqual: byte at offset 0 is 0x42, 0x42 >= 0x42 = true - let greater_equal_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: false }, - op: Operator::GreaterEqual, - value: Value::Uint(0x42), - message: "GreaterEqual test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&greater_equal_rule, buffer) - .unwrap() - .is_some() - ); -} - -#[test] -fn test_evaluate_comparison_with_signed_byte() { - // 0x80 = -128 as signed byte, 128 as unsigned byte - let buffer = &[0x80]; - - // Signed byte: reads as Int(-128), which IS less than Uint(0) - let signed_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::LessThan, - value: Value::Uint(0), - message: "signed less".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&signed_rule, buffer) - .unwrap() - .is_some() - ); - - // Unsigned byte: reads as Uint(128), which is NOT less than Uint(0) - let unsigned_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: false }, - op: Operator::LessThan, - value: Value::Uint(0), - message: "unsigned less".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&unsigned_rule, buffer) - .unwrap() - .is_none() - ); -} - -#[test] -fn test_evaluate_comparison_operators_negative_cases() { - let buffer = &[0x42]; // 66 - - let cases: Vec<(Operator, u64, bool)> = vec![ - // LessThan: 66 < 66 = false, 66 < 67 = true - (Operator::LessThan, 66, false), - (Operator::LessThan, 67, true), - // GreaterThan: 66 > 66 = false, 66 > 65 = true - (Operator::GreaterThan, 66, false), - (Operator::GreaterThan, 65, true), - // LessEqual: 66 <= 65 = false, 66 <= 66 = true - (Operator::LessEqual, 65, false), - (Operator::LessEqual, 66, true), - // GreaterEqual: 66 >= 67 = false, 66 >= 66 = true - (Operator::GreaterEqual, 67, false), - (Operator::GreaterEqual, 66, true), - ]; - - for (op, value, expected) in cases { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: false }, - op: op.clone(), - value: Value::Uint(value), - message: "test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert_eq!( - result.is_some(), - expected, - "{op:?} with value {value}: expected {expected}" - ); - } -} - -#[test] -fn test_evaluate_single_rule_edge_case_values() { - // Test with maximum values - let max_uint_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0xffff_ffff), - message: "Max uint32".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let max_buffer = &[0xff, 0xff, 0xff, 0xff]; - let result = evaluate_single_rule(&max_uint_rule, max_buffer).unwrap(); - assert!(result.is_some()); - - // Test with minimum signed value - let min_int_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: true, - }, - op: Operator::Equal, - value: Value::Int(-2_147_483_648), // i32::MIN - message: "Min int32".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let min_buffer = &[0x00, 0x00, 0x00, 0x80]; // 0x80000000 in little-endian - let result = evaluate_single_rule(&min_int_rule, min_buffer).unwrap(); - assert!(result.is_some()); -} - -#[test] -fn test_evaluate_single_rule_various_buffer_sizes() { - // Test with single byte buffer (unsigned for values > 127) - let single_byte_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: false }, - op: Operator::Equal, - value: Value::Uint(0xaa), - message: "Single byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let single_buffer = &[0xaa]; - let result = evaluate_single_rule(&single_byte_rule, single_buffer).unwrap(); - assert!(result.is_some()); - - // Test with large buffer - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let large_buffer: Vec = (0..1024).map(|i| (i % 256) as u8).collect(); - let large_rule = MagicRule { - offset: OffsetSpec::Absolute(1000), - typ: TypeKind::Byte { signed: false }, - op: Operator::Equal, - value: Value::Uint((1000 % 256) as u64), - message: "Large buffer".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let result = evaluate_single_rule(&large_rule, &large_buffer).unwrap(); - assert!(result.is_some()); -} - -// Tests for EvaluationContext -#[test] -fn test_evaluation_context_new() { - let config = EvaluationConfig::default(); - let context = EvaluationContext::new(config.clone()); - - assert_eq!(context.current_offset(), 0); - assert_eq!(context.recursion_depth(), 0); - assert_eq!( - context.config().max_recursion_depth, - config.max_recursion_depth - ); - assert_eq!(context.config().max_string_length, config.max_string_length); - assert_eq!( - context.config().stop_at_first_match, - config.stop_at_first_match - ); -} - -#[test] -fn test_evaluation_context_offset_management() { - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Test initial offset - assert_eq!(context.current_offset(), 0); - - // Test setting offset - context.set_current_offset(42); - assert_eq!(context.current_offset(), 42); - - // Test setting different offset - context.set_current_offset(1024); - assert_eq!(context.current_offset(), 1024); - - // Test setting offset to 0 - context.set_current_offset(0); - assert_eq!(context.current_offset(), 0); -} - -#[test] -fn test_evaluation_context_recursion_depth_management() { - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Test initial recursion depth - assert_eq!(context.recursion_depth(), 0); - - // Test incrementing recursion depth - context.increment_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 1); - - context.increment_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 2); - - // Test decrementing recursion depth - context.decrement_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 1); - - context.decrement_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 0); -} - -#[test] -fn test_evaluation_context_recursion_depth_limit() { - let config = EvaluationConfig { - max_recursion_depth: 2, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - // Should be able to increment up to the limit - assert!(context.increment_recursion_depth().is_ok()); - assert_eq!(context.recursion_depth(), 1); - - assert!(context.increment_recursion_depth().is_ok()); - assert_eq!(context.recursion_depth(), 2); - - // Should fail when exceeding the limit - let result = context.increment_recursion_depth(); - assert!(result.is_err()); - assert_eq!(context.recursion_depth(), 2); // Should not have changed - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Recursion limit exceeded")); - } - _ => panic!("Expected EvaluationError"), - } -} - -#[test] -fn test_evaluation_context_recursion_depth_underflow() { - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Should return an error when trying to decrement below 0 - let result = context.decrement_recursion_depth(); - assert!(result.is_err()); - - let err = result.unwrap_err(); - let err_msg = err.to_string(); - assert!( - err_msg.contains("decrement recursion depth below 0"), - "Expected error about decrementing below 0, got: {err_msg}" - ); -} - -#[test] -fn test_evaluation_context_config_access() { - let config = EvaluationConfig { - max_recursion_depth: 10, - max_string_length: 4096, - stop_at_first_match: false, - enable_mime_types: true, - timeout_ms: Some(2000), - }; - - let context = EvaluationContext::new(config); - - // Test config access - assert_eq!(context.config().max_recursion_depth, 10); - assert_eq!(context.config().max_string_length, 4096); - assert!(!context.config().stop_at_first_match); - - // Test convenience methods - assert!(!context.should_stop_at_first_match()); - assert_eq!(context.max_string_length(), 4096); -} - -#[test] -fn test_evaluation_context_reset() { - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config.clone()); - - // Modify the context state - context.set_current_offset(100); - context.increment_recursion_depth().unwrap(); - context.increment_recursion_depth().unwrap(); - - assert_eq!(context.current_offset(), 100); - assert_eq!(context.recursion_depth(), 2); - - // Reset should restore initial state but keep config - context.reset(); - - assert_eq!(context.current_offset(), 0); - assert_eq!(context.recursion_depth(), 0); - assert_eq!( - context.config().max_recursion_depth, - config.max_recursion_depth - ); -} - -#[test] -fn test_evaluation_context_clone() { - let config = EvaluationConfig { - max_recursion_depth: 5, - max_string_length: 2048, - ..Default::default() - }; - - let mut context = EvaluationContext::new(config); - context.set_current_offset(50); - context.increment_recursion_depth().unwrap(); - - // Clone the context - let cloned_context = context.clone(); - - // Both should have the same state - assert_eq!(context.current_offset(), cloned_context.current_offset()); - assert_eq!(context.recursion_depth(), cloned_context.recursion_depth()); - assert_eq!( - context.config().max_recursion_depth, - cloned_context.config().max_recursion_depth - ); - assert_eq!( - context.config().max_string_length, - cloned_context.config().max_string_length - ); - - // Modifying one should not affect the other - context.set_current_offset(75); - assert_eq!(context.current_offset(), 75); - assert_eq!(cloned_context.current_offset(), 50); -} - -#[test] -fn test_evaluation_context_with_custom_config() { - let config = EvaluationConfig { - max_recursion_depth: 15, - max_string_length: 16384, - stop_at_first_match: false, - enable_mime_types: true, - timeout_ms: Some(5000), - }; - - let context = EvaluationContext::new(config); - - assert_eq!(context.config().max_recursion_depth, 15); - assert_eq!(context.max_string_length(), 16384); - assert!(!context.should_stop_at_first_match()); - - // Test that we can increment up to the custom limit - let mut mutable_context = context; - for i in 1..=15 { - assert!(mutable_context.increment_recursion_depth().is_ok()); - assert_eq!(mutable_context.recursion_depth(), i); - } - - // Should fail on the 16th increment - let result = mutable_context.increment_recursion_depth(); - assert!(result.is_err()); -} - -#[test] -fn test_evaluation_context_mime_types_access() { - let config_with_mime = EvaluationConfig { - enable_mime_types: true, - ..Default::default() - }; - let context_with_mime = EvaluationContext::new(config_with_mime); - assert!(context_with_mime.enable_mime_types()); - - let config_without_mime = EvaluationConfig { - enable_mime_types: false, - ..Default::default() - }; - let context_without_mime = EvaluationContext::new(config_without_mime); - assert!(!context_without_mime.enable_mime_types()); -} - -#[test] -fn test_evaluation_context_timeout_access() { - let config_with_timeout = EvaluationConfig { - timeout_ms: Some(5000), - ..Default::default() - }; - let context_with_timeout = EvaluationContext::new(config_with_timeout); - assert_eq!(context_with_timeout.timeout_ms(), Some(5000)); - - let config_without_timeout = EvaluationConfig { - timeout_ms: None, - ..Default::default() - }; - let context_without_timeout = EvaluationContext::new(config_without_timeout); - assert_eq!(context_without_timeout.timeout_ms(), None); -} - -#[test] -fn test_evaluation_context_comprehensive_config() { - let config = EvaluationConfig { - max_recursion_depth: 30, - max_string_length: 16384, - stop_at_first_match: false, - enable_mime_types: true, - timeout_ms: Some(10000), - }; - let context = EvaluationContext::new(config); - - assert_eq!(context.config().max_recursion_depth, 30); - assert_eq!(context.config().max_string_length, 16384); - assert!(!context.should_stop_at_first_match()); - assert!(context.enable_mime_types()); - assert_eq!(context.timeout_ms(), Some(10000)); - assert_eq!(context.max_string_length(), 16384); -} - -#[test] -fn test_evaluation_context_performance_config() { - let config = EvaluationConfig { - max_recursion_depth: 5, - max_string_length: 512, - stop_at_first_match: true, - enable_mime_types: false, - timeout_ms: Some(1000), - }; - let context = EvaluationContext::new(config); - - assert_eq!(context.config().max_recursion_depth, 5); - assert_eq!(context.max_string_length(), 512); - assert!(context.should_stop_at_first_match()); - assert!(!context.enable_mime_types()); - assert_eq!(context.timeout_ms(), Some(1000)); -} - -#[test] -fn test_rule_match_creation() { - let match_result = RuleMatch { - message: "ELF executable".to_string(), - offset: 0, - level: 0, - value: Value::Uint(0x7f), - confidence: RuleMatch::calculate_confidence(0), - }; - - assert_eq!(match_result.message, "ELF executable"); - assert_eq!(match_result.offset, 0); - assert_eq!(match_result.level, 0); - assert_eq!(match_result.value, Value::Uint(0x7f)); - assert!((match_result.confidence - 0.3).abs() < 0.001); -} - -#[test] -fn test_rule_match_clone() { - let original = RuleMatch { - message: "Test message".to_string(), - offset: 42, - level: 1, - value: Value::String("test".to_string()), - confidence: RuleMatch::calculate_confidence(1), - }; - - let cloned = original.clone(); - assert_eq!(original, cloned); -} - -#[test] -fn test_rule_match_debug() { - let match_result = RuleMatch { - message: "Debug test".to_string(), - offset: 10, - level: 2, - value: Value::Bytes(vec![0x01, 0x02]), - confidence: RuleMatch::calculate_confidence(2), - }; - - let debug_str = format!("{match_result:?}"); - assert!(debug_str.contains("RuleMatch")); - assert!(debug_str.contains("Debug test")); - assert!(debug_str.contains("10")); - assert!(debug_str.contains('2')); -} - -#[test] -fn test_confidence_calculation_depth_0() { - let confidence = RuleMatch::calculate_confidence(0); - assert!((confidence - 0.3).abs() < 0.001); -} - -#[test] -fn test_confidence_calculation_depth_1() { - let confidence = RuleMatch::calculate_confidence(1); - assert!((confidence - 0.5).abs() < 0.001); -} - -#[test] -fn test_confidence_calculation_depth_2() { - let confidence = RuleMatch::calculate_confidence(2); - assert!((confidence - 0.7).abs() < 0.001); -} - -#[test] -fn test_confidence_calculation_depth_3() { - let confidence = RuleMatch::calculate_confidence(3); - assert!((confidence - 0.9).abs() < 0.001); -} - -#[test] -fn test_confidence_calculation_capped_at_1() { - // Level 4+ should cap at 1.0 - let confidence_4 = RuleMatch::calculate_confidence(4); - assert!((confidence_4 - 1.0).abs() < 0.001); - - let confidence_10 = RuleMatch::calculate_confidence(10); - assert!((confidence_10 - 1.0).abs() < 0.001); - - let confidence_100 = RuleMatch::calculate_confidence(100); - assert!((confidence_100 - 1.0).abs() < 0.001); -} - -#[test] -fn test_evaluate_rules_empty_list() { - let rules = vec![]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert!(matches.is_empty()); -} - -#[test] -fn test_evaluate_rules_single_matching_rule() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 1); - assert_eq!(matches[0].message, "ELF magic"); - assert_eq!(matches[0].offset, 0); - assert_eq!(matches[0].level, 0); - // Signed byte read: 0x7f -> Value::Int(127) - assert_eq!(matches[0].value, Value::Int(0x7f)); -} - -#[test] -fn test_evaluate_rules_single_non_matching_rule() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x50), // ZIP magic, not ELF - message: "ZIP magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF buffer - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert!(matches.is_empty()); -} - -#[test] -fn test_evaluate_rules_multiple_rules_stop_at_first() { - let rule1 = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "First match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule2 = MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Second match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule_list = vec![rule1, rule2]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - stop_at_first_match: true, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rule_list, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 1); - assert_eq!(matches[0].message, "First match"); -} - -#[test] -fn test_evaluate_rules_multiple_rules_find_all() { - let rule1 = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "First match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule2 = MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Second match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule_set = vec![rule1, rule2]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - stop_at_first_match: false, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rule_set, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "First match"); - assert_eq!(matches[1].message, "Second match"); -} - -#[test] -fn test_evaluate_rules_hierarchical_parent_child() { - let child_rule = MagicRule { - offset: OffsetSpec::Absolute(4), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x02), // ELF class 64-bit - message: "64-bit".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }; - - let parent_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF".to_string(), - children: vec![child_rule], - level: 0, - strength_modifier: None, - }; - - let rules = vec![parent_rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; // ELF64 header - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "ELF"); - assert_eq!(matches[0].level, 0); - assert_eq!(matches[1].message, "64-bit"); - assert_eq!(matches[1].level, 1); -} - -#[test] -fn test_evaluate_rules_hierarchical_parent_no_match() { - let child_rule = MagicRule { - offset: OffsetSpec::Absolute(4), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x02), - message: "64-bit".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }; - - let parent_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x50), // ZIP magic, not ELF - message: "ZIP".to_string(), - children: vec![child_rule], - level: 0, - strength_modifier: None, - }; - - let rules = vec![parent_rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; // ELF buffer - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert!(matches.is_empty()); // Parent doesn't match, so children shouldn't be evaluated -} - -#[test] -fn test_evaluate_rules_hierarchical_parent_match_child_no_match() { - let child_rule = MagicRule { - offset: OffsetSpec::Absolute(4), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x01), // ELF class 32-bit, but buffer has 64-bit - message: "32-bit".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }; - - let parent_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF".to_string(), - children: vec![child_rule], - level: 0, - strength_modifier: None, - }; - - let rules = vec![parent_rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; // ELF64 header - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 1); // Only parent matches - assert_eq!(matches[0].message, "ELF"); - assert_eq!(matches[0].level, 0); -} - -#[test] -fn test_evaluate_rules_deep_hierarchy() { - let grandchild_rule = MagicRule { - offset: OffsetSpec::Absolute(5), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x01), // Little endian - message: "little-endian".to_string(), - children: vec![], - level: 2, - strength_modifier: None, - }; - - let child_rule = MagicRule { - offset: OffsetSpec::Absolute(4), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x02), // 64-bit - message: "64-bit".to_string(), - children: vec![grandchild_rule], - level: 1, - strength_modifier: None, - }; - - let parent_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF".to_string(), - children: vec![child_rule], - level: 0, - strength_modifier: None, - }; - - let rules = vec![parent_rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; // ELF64 little-endian header - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 3); - assert_eq!(matches[0].message, "ELF"); - assert_eq!(matches[0].level, 0); - assert_eq!(matches[1].message, "64-bit"); - assert_eq!(matches[1].level, 1); - assert_eq!(matches[2].message, "little-endian"); - assert_eq!(matches[2].level, 2); -} - -#[test] -fn test_evaluate_rules_multiple_children() { - let child1 = MagicRule { - offset: OffsetSpec::Absolute(4), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x02), - message: "64-bit".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }; - - let child2 = MagicRule { - offset: OffsetSpec::Absolute(5), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x01), - message: "little-endian".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }; - - let parent_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF".to_string(), - children: vec![child1, child2], - level: 0, - strength_modifier: None, - }; - - let rules = vec![parent_rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; - let config = EvaluationConfig { - stop_at_first_match: false, // Find all matches - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 3); - assert_eq!(matches[0].message, "ELF"); - assert_eq!(matches[1].message, "64-bit"); - assert_eq!(matches[2].message, "little-endian"); -} - -#[test] -fn test_evaluate_rules_recursion_depth_limit() { - // Create a deeply nested rule structure that exceeds the limit - let mut current_rule = MagicRule { - offset: OffsetSpec::Absolute(10), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Deep level".to_string(), - children: vec![], - level: 10, - strength_modifier: None, - }; - - // Build a chain of nested rules - for i in (0u32..10u32).rev() { - current_rule = MagicRule { - offset: OffsetSpec::Absolute(i64::from(i)), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(u64::from(i)), - message: format!("Level {i}"), - children: vec![current_rule], - level: i, - strength_modifier: None, - }; - } - - let rules = vec![current_rule]; - let buffer = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; // Matches all levels - let config = EvaluationConfig { - max_recursion_depth: 5, // Limit to 5 levels - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let result = evaluate_rules(&rules, buffer, &mut context); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Recursion limit exceeded")); - } - _ => panic!("Expected EvaluationError for recursion limit"), - } -} - -#[test] -fn test_evaluate_rules_with_config_convenience() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - - let matches = evaluate_rules_with_config(&rules, buffer, &config).unwrap(); - assert_eq!(matches.len(), 1); - assert_eq!(matches[0].message, "ELF magic"); -} - -#[test] -fn test_evaluate_rules_timeout() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - timeout_ms: Some(0), // Immediate timeout - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - // Note: This test might be flaky due to timing, but it demonstrates the timeout mechanism - let result = evaluate_rules(&rules, buffer, &mut context); - // The result could be either success (if evaluation is very fast) or timeout - // We just verify that timeout errors are handled correctly when they occur - if let Err(LibmagicError::Timeout { timeout_ms }) = result { - assert_eq!(timeout_ms, 0); - } -} - -#[test] -fn test_evaluate_rules_empty_buffer() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Should not match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[]; // Empty buffer - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // With graceful error handling, this should succeed but return no matches - let result = evaluate_rules(&rules, buffer, &mut context); - assert!(result.is_ok()); - - let matches = result.unwrap(); - assert_eq!(matches.len(), 0); // No matches due to buffer overrun being handled gracefully -} - -#[test] -fn test_evaluate_rules_mixed_matching_non_matching() { - let rule1 = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Matches".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule2 = MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x99), // Doesn't match - message: "Doesn't match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule3 = MagicRule { - offset: OffsetSpec::Absolute(2), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x4c), - message: "Also matches".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule_collection = vec![rule1, rule2, rule3]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - stop_at_first_match: false, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rule_collection, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "Matches"); - assert_eq!(matches[1].message, "Also matches"); -} - -#[test] -fn test_evaluate_rules_context_state_preservation() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Set some initial state - context.set_current_offset(100); - let initial_offset = context.current_offset(); - let initial_depth = context.recursion_depth(); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 1); - - // Context state should be preserved - assert_eq!(context.current_offset(), initial_offset); - assert_eq!(context.recursion_depth(), initial_depth); -} - -#[test] -fn test_evaluation_context_state_management_sequence() { - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Simulate a sequence of evaluation operations - assert_eq!(context.current_offset(), 0); - assert_eq!(context.recursion_depth(), 0); - - // Start evaluation at offset 10 - context.set_current_offset(10); - assert_eq!(context.current_offset(), 10); - - // Enter nested rule evaluation - context.increment_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 1); - - // Move to different offset during nested evaluation - context.set_current_offset(25); - assert_eq!(context.current_offset(), 25); - - // Enter deeper nesting - context.increment_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 2); - - // Exit nested evaluation - context.decrement_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 1); - - // Continue evaluation at different offset - context.set_current_offset(50); - assert_eq!(context.current_offset(), 50); - - // Exit all nesting - context.decrement_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 0); - - // Final state check - assert_eq!(context.current_offset(), 50); - assert_eq!(context.recursion_depth(), 0); -} -#[test] -fn test_error_recovery_skip_problematic_rules() { - // Test that evaluation continues when individual rules fail - let rules = vec![ - // Valid rule that should match - MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Valid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - // Invalid rule with out-of-bounds offset - MagicRule { - offset: OffsetSpec::Absolute(100), // Beyond buffer - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Invalid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - // Another valid rule that should match - MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Another valid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - ]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes - let config = EvaluationConfig { - max_recursion_depth: 20, - max_string_length: 8192, - stop_at_first_match: false, // Don't stop at first match - enable_mime_types: false, - timeout_ms: None, - }; - let mut context = EvaluationContext::new(config); - - // Evaluation should succeed despite the problematic rule - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - - // Should have 2 matches (skipping the problematic one) - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "Valid rule"); - assert_eq!(matches[1].message, "Another valid rule"); -} - -#[test] -fn test_error_recovery_child_rule_failures() { - // Test that parent evaluation continues when child rules fail - let rules = vec![MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Parent rule".to_string(), - children: vec![ - // Valid child rule - MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Valid child".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }, - // Invalid child rule - MagicRule { - offset: OffsetSpec::Absolute(100), // Beyond buffer - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Invalid child".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }, - ], - level: 0, - strength_modifier: None, - }]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Evaluation should succeed with parent and valid child - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - - // Should have parent match and valid child match - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "Parent rule"); - assert_eq!(matches[1].message, "Valid child"); -} - -#[test] -fn test_error_recovery_mixed_rule_types() { - // Test error recovery with different types of rule failures - let rules = vec![ - // Valid byte rule - MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Valid byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - // Invalid short rule (insufficient bytes) - MagicRule { - offset: OffsetSpec::Absolute(3), // Only 1 byte left for short - typ: TypeKind::Short { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234), - message: "Invalid short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - // Valid string rule - MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::String { - max_length: Some(3), - }, - op: Operator::Equal, - value: Value::String("ELF".to_string()), - message: "Valid string".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - ]; - - let buffer = &[0x7f, b'E', b'L', b'F']; // ELF magic bytes - let config = EvaluationConfig { - max_recursion_depth: 20, - max_string_length: 8192, - stop_at_first_match: false, // Don't stop at first match - enable_mime_types: false, - timeout_ms: None, - }; - let mut context = EvaluationContext::new(config); - - // Evaluation should succeed with valid rules - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - - // Should have 2 matches (byte and string, skipping invalid short) - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "Valid byte"); - assert_eq!(matches[1].message, "Valid string"); -} - -#[test] -fn test_error_recovery_all_rules_fail() { - // Test behavior when all rules fail - let rules = vec![ - // Out of bounds offset - MagicRule { - offset: OffsetSpec::Absolute(100), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Out of bounds".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - // Insufficient bytes for type - MagicRule { - offset: OffsetSpec::Absolute(2), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234_5678), - message: "Insufficient bytes".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - ]; - - let buffer = &[0x7f, 0x45]; // Short buffer - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Evaluation should succeed but return no matches - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 0); -} - -#[test] -fn test_error_recovery_timeout_propagation() { - // Test that timeout errors are properly propagated (not gracefully handled) - let rules = vec![MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Test rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - max_recursion_depth: 10, - max_string_length: 1024, - stop_at_first_match: false, - enable_mime_types: false, - timeout_ms: Some(0), // Immediate timeout - }; - let mut context = EvaluationContext::new(config); - - // The timeout test is inherently flaky due to timing, so we'll just test - // that the timeout configuration is properly set and the function doesn't panic - let result = evaluate_rules(&rules, buffer, &mut context); - - // The result should either be success (if evaluation was fast) or timeout error - match result { - Ok(_) | Err(LibmagicError::Timeout { .. }) => { - // Evaluation was fast enough or timeout occurred, both are acceptable - } - Err(e) => { - panic!("Unexpected error type: {e:?}"); - } - } -} - -#[test] -fn test_error_recovery_recursion_limit_propagation() { - // Test that recursion limit errors are properly propagated - let rules = vec![MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Parent".to_string(), - children: vec![MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Child".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }], - level: 0, - strength_modifier: None, - }]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - max_recursion_depth: 0, // No recursion allowed - max_string_length: 1024, - stop_at_first_match: false, - enable_mime_types: false, - timeout_ms: None, - }; - let mut context = EvaluationContext::new(config); - - // Should return recursion limit error when trying to evaluate children - let result = evaluate_rules(&rules, buffer, &mut context); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(crate::error::EvaluationError::RecursionLimitExceeded { - .. - }) => { - // Expected recursion limit error - } - _ => panic!("Expected recursion limit error"), - } -} - -#[test] -fn test_error_recovery_preserves_context_state() { - // Test that context state is preserved despite rule failures - let rules = vec![ - // Valid rule - MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Valid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - // Invalid rule - MagicRule { - offset: OffsetSpec::Absolute(100), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Invalid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - ]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Set initial context state - context.set_current_offset(42); - let initial_offset = context.current_offset(); - let initial_depth = context.recursion_depth(); - - // Evaluation should succeed - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 1); - - // Context state should be preserved - assert_eq!(context.current_offset(), initial_offset); - assert_eq!(context.recursion_depth(), initial_depth); -} - -// End-to-end tests: parse rules with `x` operator and evaluate them -#[test] -fn test_any_value_parse_and_evaluate_paren_message() { - use crate::parser::grammar::parse_magic_rule; - - let input = ">0 byte x (0)"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::AnyValue); - assert_eq!(rule.message, "(0)"); - - // AnyValue should match unconditionally regardless of buffer content - let buffer = &[0x00, 0x01, 0x02, 0x03]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_some(), - "AnyValue rule should match unconditionally" - ); -} - -#[test] -fn test_any_value_parse_and_evaluate_backslash_message() { - use crate::parser::grammar::parse_magic_rule; - - let input = "0 long x \\b, data"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::AnyValue); - assert_eq!(rule.message, "\\b, data"); - - let buffer = &[0xFF, 0xFE, 0xFD, 0xFC]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_some(), - "AnyValue rule should match unconditionally" - ); -} - -#[test] -fn test_any_value_parse_and_evaluate_no_message() { - use crate::parser::grammar::parse_magic_rule; - - let input = "0 byte x"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::AnyValue); - - let buffer = &[0x42]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_some(), - "AnyValue rule should match unconditionally" - ); -} - -// End-to-end tests: parse rules with `^` operator and evaluate them -#[test] -fn test_bitwise_xor_parse_and_evaluate_match() { - use crate::parser::grammar::parse_magic_rule; - - let input = "0 byte ^0x01 XOR match"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::BitwiseXor); - assert_eq!(rule.message, "XOR match"); - - // Buffer byte 0x0F XOR 0x01 = 0x0E (non-zero), should match - let buffer = &[0x0F]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_some(), - "BitwiseXor should match when XOR is non-zero" - ); -} - -#[test] -fn test_bitwise_xor_parse_and_evaluate_no_match() { - use crate::parser::grammar::parse_magic_rule; - - let input = "0 byte ^0x42 XOR no match"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::BitwiseXor); - - // Buffer byte 0x42 XOR 0x42 = 0 (zero), should NOT match - let buffer = &[0x42]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_none(), - "BitwiseXor should not match when XOR is zero" - ); -} - -// End-to-end tests: parse rules with `~` operator and evaluate them -#[test] -fn test_bitwise_not_parse_and_evaluate_match() { - use crate::parser::grammar::parse_magic_rule; - - // ubyte reads 0x00; at byte width, ~0x00 = 0xFF - let input = "0 ubyte ~0xFF NOT match"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::BitwiseNot); - assert_eq!(rule.message, "NOT match"); - - let buffer = &[0x00]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_some(), - "BitwiseNot should match when NOT(value) equals operand at byte width" - ); -} - -#[test] -fn test_bitwise_not_parse_and_evaluate_no_match() { - use crate::parser::grammar::parse_magic_rule; - - // NOT of 0x42 byte at byte width is 0xBD; comparing with 0x01 should NOT match - let input = "0 ubyte ~0x01 NOT no match"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::BitwiseNot); - - let buffer = &[0x42]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_none(), - "BitwiseNot should not match when NOT(value) != operand" - ); -} - -#[test] -fn test_debug_error_recovery() { - // Simple test to debug error recovery - let rule = MagicRule { - offset: OffsetSpec::Absolute(100), // Beyond buffer - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Out of bounds rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45]; // Short buffer - - // Test single rule evaluation - should fail - let single_result = evaluate_single_rule(&rule, buffer); - println!("Single rule result: {single_result:?}"); - assert!(single_result.is_err()); - - // Test rules evaluation - should succeed with no matches - let rules = vec![rule]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - println!("Rules evaluation matches: {}", matches.len()); - assert_eq!(matches.len(), 0); -} -#[test] -fn test_debug_mixed_rules() { - let rules = vec![ - // Valid rule that should match - MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Valid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - // Invalid rule with out-of-bounds offset - MagicRule { - offset: OffsetSpec::Absolute(100), // Beyond buffer - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Invalid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - // Another valid rule that should match - MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Another valid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - ]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes - - // Test each rule individually - for (i, rule) in rules.iter().enumerate() { - let result = evaluate_single_rule(rule, buffer); - println!("Rule {}: '{}' -> {:?}", i, rule.message, result); - } - - // Test rules evaluation - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - println!("Total matches: {}", matches.len()); - for (i, m) in matches.iter().enumerate() { - println!("Match {}: '{}'", i, m.message); - } -} From e1d095a212deae1476b4219cd3102add8ccef39e Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Fri, 6 Mar 2026 13:34:10 -0500 Subject: [PATCH 02/12] fix(evaluator): prevent error masking in child evaluation cleanup On error paths in evaluate_rules, decrement_recursion_depth() was called with ? which could mask the original error (Timeout, RecursionLimit) if the depth was already 0. Use `let _ =` for best-effort cleanup instead. Also fix timeout propagation to bind the original timeout_ms from the caught error rather than reconstructing it with unwrap_or(0), which would report "timed out after 0ms" if timeout_ms was None. Co-Authored-By: Claude Opus 4.6 Signed-off-by: UncleSp1d3r --- src/evaluator/engine.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/evaluator/engine.rs b/src/evaluator/engine.rs index f84f5288..2d13d364 100644 --- a/src/evaluator/engine.rs +++ b/src/evaluator/engine.rs @@ -145,18 +145,16 @@ pub fn evaluate_rules( Ok(child_matches) => { matches.extend(child_matches); } - Err(LibmagicError::Timeout { .. }) => { + Err(LibmagicError::Timeout { timeout_ms }) => { // Timeout is critical, propagate it up - context.decrement_recursion_depth()?; - return Err(LibmagicError::Timeout { - timeout_ms: context.timeout_ms().unwrap_or(0), - }); + let _ = context.decrement_recursion_depth(); + return Err(LibmagicError::Timeout { timeout_ms }); } Err(LibmagicError::EvaluationError( crate::error::EvaluationError::RecursionLimitExceeded { .. }, )) => { // Recursion limit is critical, propagate it up - context.decrement_recursion_depth()?; + let _ = context.decrement_recursion_depth(); return Err(LibmagicError::EvaluationError( crate::error::EvaluationError::RecursionLimitExceeded { depth: context.recursion_depth(), @@ -175,7 +173,7 @@ pub fn evaluate_rules( } Err(e) => { // Unexpected errors in children should propagate - context.decrement_recursion_depth()?; + let _ = context.decrement_recursion_depth(); return Err(e); } } From df8b8f7eca6d49a0a5b0eb9803ead2a657d115e7 Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Fri, 6 Mar 2026 13:35:18 -0500 Subject: [PATCH 03/12] docs(evaluator): restore rustdoc examples and fix comment accuracy Restore # Examples, # Arguments, and # Returns sections on all three public engine functions (evaluate_single_rule, evaluate_rules, evaluate_rules_with_config) that were dropped during the extraction to engine.rs. This restores 3 doc tests (142 -> 145). Also fix: - evaluate_single_rule doc described 3 steps but code has 4 (add coercion step) - mod.rs module doc now reflects its role as types + re-exports - RuleMatch doc no longer claims to contain "the rule that matched" (it contains extracted fields) Co-Authored-By: Claude Opus 4.6 Signed-off-by: UncleSp1d3r --- src/evaluator/engine.rs | 123 +++++++++++++++++++++++++++++++++++++++- src/evaluator/mod.rs | 9 +-- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/src/evaluator/engine.rs b/src/evaluator/engine.rs index 2d13d364..291db9eb 100644 --- a/src/evaluator/engine.rs +++ b/src/evaluator/engine.rs @@ -20,7 +20,8 @@ use super::{EvaluationContext, RuleMatch, offset, operators, types}; /// This function performs the core rule evaluation by: /// 1. Resolving the rule's offset specification to an absolute position /// 2. Reading and interpreting bytes at that position according to the rule's type -/// 3. Applying the rule's operator to compare the read value with the expected value +/// 3. Coercing the expected value to match the type's signedness and bit width +/// 4. Applying the rule's operator to compare the read value with the expected value /// /// # Arguments /// @@ -33,6 +34,33 @@ use super::{EvaluationContext, RuleMatch, offset, operators, types}; /// read value), `Ok(None)` if it doesn't match, or `Err(LibmagicError)` if evaluation /// fails due to buffer access issues or other errors. /// +/// # Examples +/// +/// ```rust +/// use libmagic_rs::evaluator::evaluate_single_rule; +/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; +/// +/// // Create a rule to check for ELF magic bytes at offset 0 +/// let rule = MagicRule { +/// offset: OffsetSpec::Absolute(0), +/// typ: TypeKind::Byte { signed: true }, +/// op: Operator::Equal, +/// value: Value::Uint(0x7f), +/// message: "ELF magic".to_string(), +/// children: vec![], +/// level: 0, +/// strength_modifier: None, +/// }; +/// +/// let elf_buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes +/// let result = evaluate_single_rule(&rule, elf_buffer).unwrap(); +/// assert!(result.is_some()); // Should match +/// +/// let non_elf_buffer = &[0x50, 0x4b, 0x03, 0x04]; // ZIP magic bytes +/// let result = evaluate_single_rule(&rule, non_elf_buffer).unwrap(); +/// assert!(result.is_none()); // Should not match +/// ``` +/// /// # Errors /// /// * `LibmagicError::EvaluationError` - If offset resolution fails, buffer access is out of bounds, @@ -82,10 +110,63 @@ pub fn evaluate_single_rule( /// - Recursion depth is limited to prevent infinite loops /// - Problematic rules are skipped to allow evaluation to continue /// +/// # Arguments +/// +/// * `rules` - The list of magic rules to evaluate +/// * `buffer` - The file buffer to evaluate against +/// * `context` - Mutable evaluation context for state management +/// +/// # Returns +/// +/// Returns `Ok(Vec)` containing all matches found. Errors in individual rules +/// are skipped to allow evaluation to continue. Only returns `Err(LibmagicError)` +/// for critical failures like timeout or recursion limit exceeded. +/// +/// # Examples +/// +/// ```rust +/// use libmagic_rs::evaluator::{evaluate_rules, EvaluationContext, RuleMatch}; +/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; +/// use libmagic_rs::EvaluationConfig; +/// +/// // Create a hierarchical rule set for ELF files +/// let parent_rule = MagicRule { +/// offset: OffsetSpec::Absolute(0), +/// typ: TypeKind::Byte { signed: true }, +/// op: Operator::Equal, +/// value: Value::Uint(0x7f), +/// message: "ELF".to_string(), +/// children: vec![ +/// MagicRule { +/// offset: OffsetSpec::Absolute(4), +/// typ: TypeKind::Byte { signed: true }, +/// op: Operator::Equal, +/// value: Value::Uint(2), +/// message: "64-bit".to_string(), +/// children: vec![], +/// level: 1, +/// strength_modifier: None, +/// } +/// ], +/// level: 0, +/// strength_modifier: None, +/// }; +/// +/// let rules = vec![parent_rule]; +/// let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; // ELF64 header +/// let config = EvaluationConfig::default(); +/// let mut context = EvaluationContext::new(config); +/// +/// let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); +/// assert_eq!(matches.len(), 2); // Parent and child should both match +/// ``` +/// /// # Errors /// /// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout -/// * `LibmagicError::EvaluationError` - For critical failures (e.g. recursion limit exceeded) +/// * `LibmagicError::EvaluationError` - Only for critical failures like recursion limit exceeded +/// +/// Individual rule evaluation errors are handled gracefully and do not stop the overall evaluation. pub fn evaluate_rules( rules: &[MagicRule], buffer: &[u8], @@ -197,6 +278,44 @@ pub fn evaluate_rules( /// This is a convenience function that creates a new evaluation context /// and evaluates the rules. Useful for simple evaluation scenarios. /// +/// # Arguments +/// +/// * `rules` - The list of magic rules to evaluate +/// * `buffer` - The file buffer to evaluate against +/// * `config` - Configuration for evaluation behavior +/// +/// # Returns +/// +/// Returns `Ok(Vec)` containing all matches found, or `Err(LibmagicError)` +/// if evaluation fails. +/// +/// # Examples +/// +/// ```rust +/// use libmagic_rs::evaluator::{evaluate_rules_with_config, RuleMatch}; +/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; +/// use libmagic_rs::EvaluationConfig; +/// +/// let rule = MagicRule { +/// offset: OffsetSpec::Absolute(0), +/// typ: TypeKind::Byte { signed: true }, +/// op: Operator::Equal, +/// value: Value::Uint(0x7f), +/// message: "ELF magic".to_string(), +/// children: vec![], +/// level: 0, +/// strength_modifier: None, +/// }; +/// +/// let rules = vec![rule]; +/// let buffer = &[0x7f, 0x45, 0x4c, 0x46]; +/// let config = EvaluationConfig::default(); +/// +/// let matches = evaluate_rules_with_config(&rules, buffer, &config).unwrap(); +/// assert_eq!(matches.len(), 1); +/// assert_eq!(matches[0].message, "ELF magic"); +/// ``` +/// /// # Errors /// /// * `LibmagicError::EvaluationError` - If rule evaluation fails diff --git a/src/evaluator/mod.rs b/src/evaluator/mod.rs index 57bd43a2..3fb42aab 100644 --- a/src/evaluator/mod.rs +++ b/src/evaluator/mod.rs @@ -3,8 +3,9 @@ //! Rule evaluation engine //! -//! This module contains the core evaluation logic for executing magic rules -//! against file buffers to identify file types. +//! This module provides the public interface for magic rule evaluation, +//! including data types for evaluation state and match results, and +//! re-exports the core evaluation functions from submodules. use crate::{EvaluationConfig, LibmagicError}; use serde::{Deserialize, Serialize}; @@ -204,8 +205,8 @@ impl EvaluationContext { /// Result of evaluating a magic rule /// -/// Contains information about a successful rule match, including the rule -/// that matched and its associated message. +/// Contains information extracted from a successful rule match, including +/// the matched value, position, and confidence score. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RuleMatch { /// The message associated with the matching rule From 272108b1a4c8f916590cd20058b73a8e3f3fff72 Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Fri, 6 Mar 2026 13:35:24 -0500 Subject: [PATCH 04/12] docs: update AGENTS.md evaluator module structure Reflect the refactoring from issue #59: remove deleted tests.rs, add new engine.rs, and update mod.rs description from "Main evaluation engine" to its actual role as public interface and re-exports. Co-Authored-By: Claude Opus 4.6 Signed-off-by: UncleSp1d3r --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a576cd37..f93aa6d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,8 +88,8 @@ parser/ // Evaluator module structure evaluator/ -├── mod.rs // Main evaluation engine -├── tests.rs // Evaluator tests +├── mod.rs // Public interface, EvaluationContext, RuleMatch, re-exports +├── engine.rs // Core evaluation engine (evaluate_single_rule, evaluate_rules, evaluate_rules_with_config) ├── types.rs // Type interpretation with endianness ├── strength.rs // Strength modifier application ├── offset/ // Offset resolution submodule From bf81694db7ca725c0660ca224fa72de271c7b462 Mon Sep 17 00:00:00 2001 From: "dosubot[bot]" <131922026+dosubot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:43:28 +0000 Subject: [PATCH 05/12] docs: Dosu updates for PR #153 --- docs/src/architecture.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/src/architecture.md b/docs/src/architecture.md index 4ffa4ff8..1b17ceca 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -126,7 +126,8 @@ The evaluator executes magic rules against file buffers to identify file types. **Structure:** -- `mod.rs`: Main evaluation engine with `EvaluationContext` and `RuleMatch` +- `mod.rs`: Public API surface (~720 lines) with `EvaluationContext`, `RuleMatch` types, and re-exports +- `engine.rs`: Core evaluation engine (~2,096 lines) with `evaluate_single_rule`, `evaluate_rules`, and `evaluate_rules_with_config` functions - `types.rs`: Type interpretation with endianness handling and signedness coercion - `offset/`: Offset resolution submodule - `mod.rs`: Dispatcher (`resolve_offset`) and re-exports @@ -139,6 +140,8 @@ The evaluator executes magic rules against file buffers to identify file types. - `comparison.rs`: `compare_values`, `apply_less_than`/`greater_than`/`less_equal`/`greater_equal` - `bitwise.rs`: `apply_bitwise_and`, `apply_bitwise_and_mask`, `apply_bitwise_xor`, `apply_bitwise_not` +**Organization Note:** The evaluator module was refactored to split a monolithic 2,638-line `mod.rs` into focused submodules, keeping the public API surface in `mod.rs` and moving core evaluation logic to `engine.rs`. This maintains the same public API through re-exports (no breaking changes) while improving code organization and staying within the 500-600 line module guideline. + **Implemented Features:** - ✅ **Hierarchical Evaluation**: Parent rules must match before children From 7d75a8efcac82de9186ef71b43b8806b092095f5 Mon Sep 17 00:00:00 2001 From: "dosubot[bot]" <131922026+dosubot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:43:50 +0000 Subject: [PATCH 06/12] docs: Dosu updates for PR #153 --- docs/ARCHITECTURE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e24726af..f3e92bf4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -86,7 +86,8 @@ libmagic-rs/ │ │ └── grammar.rs # nom-based parsing combinators │ │ │ ├── evaluator/ # Rule evaluation engine -│ │ ├── mod.rs # Main evaluation logic, EvaluationContext +│ │ ├── mod.rs # Public API surface with re-exports, EvaluationContext, RuleMatch +│ │ ├── engine.rs # Core evaluation logic (evaluate_single_rule, evaluate_rules, evaluate_rules_with_config) │ │ ├── offset.rs # Offset resolution │ │ ├── types.rs # Type reading with bounds checking │ │ ├── operators.rs # Comparison operations From 8192bf868d01fd5830c97477c7fc14e015862e38 Mon Sep 17 00:00:00 2001 From: "dosubot[bot]" <131922026+dosubot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:45:52 +0000 Subject: [PATCH 07/12] docs: Dosu updates for PR #153 --- docs/src/evaluator.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/src/evaluator.md b/docs/src/evaluator.md index f842190c..69b82909 100644 --- a/docs/src/evaluator.md +++ b/docs/src/evaluator.md @@ -21,9 +21,22 @@ File Buffer → Offset Resolution → Type Reading → Operator Application → Memory Map Context State Endian Handling Match Logic Hierarchical ``` +## Module Organization + +The evaluator module is organized into focused submodules: + +- **`evaluator/engine.rs`** - Core evaluation logic (`evaluate_single_rule`, `evaluate_rules`, `evaluate_rules_with_config`) +- **`evaluator/mod.rs`** - Public API surface (types, context, re-exports) +- **`evaluator/offset.rs`** - Offset resolution +- **`evaluator/operators.rs`** - Operator application +- **`evaluator/types.rs`** - Type reading and coercion +- **`evaluator/strength.rs`** - Rule strength calculation + +From a public API perspective, all types and functions are imported from the `evaluator` module as before -- the internal organization is transparent to library users. + ## Core Components -### EvaluationContext (`evaluator/mod.rs`) +### EvaluationContext Maintains state during rule processing: @@ -49,7 +62,7 @@ Note: Fields are private; use accessor methods like `current_offset()`, `recursi - `timeout_ms()` - Query configured timeout - `reset()` - Reset context state for reuse -### RuleMatch (`evaluator/mod.rs`) +### RuleMatch Represents a successful rule match: From dfe7aebd41dad0588d2c10bd66ced88b225b75c3 Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Fri, 6 Mar 2026 13:58:57 -0500 Subject: [PATCH 08/12] refactor(evaluator): extract engine tests to separate file Move the ~1880-line #[cfg(test)] block from engine.rs into engine/tests.rs, bringing engine/mod.rs down to 332 lines (within the 500-600 line guideline). The engine module is now a directory with mod.rs for production code and tests.rs for unit tests. Also rename test functions with unclear "debug" naming: - test_debug_error_recovery -> test_evaluate_rules_skips_out_of_bounds_rule - test_debug_mixed_rules -> test_mixed_valid_and_invalid_rules_yield_valid_matches Co-Authored-By: Claude Opus 4.6 Signed-off-by: UncleSp1d3r --- src/evaluator/engine.rs | 2213 --------------------------------- src/evaluator/engine/mod.rs | 331 +++++ src/evaluator/engine/tests.rs | 1882 ++++++++++++++++++++++++++++ 3 files changed, 2213 insertions(+), 2213 deletions(-) delete mode 100644 src/evaluator/engine.rs create mode 100644 src/evaluator/engine/mod.rs create mode 100644 src/evaluator/engine/tests.rs diff --git a/src/evaluator/engine.rs b/src/evaluator/engine.rs deleted file mode 100644 index 291db9eb..00000000 --- a/src/evaluator/engine.rs +++ /dev/null @@ -1,2213 +0,0 @@ -// Copyright (c) 2025-2026 the libmagic-rs contributors -// SPDX-License-Identifier: Apache-2.0 - -//! Core evaluation engine for magic rules. -//! -//! This module contains the core recursive evaluation logic for executing magic -//! rules against file buffers. It is responsible for: -//! - Evaluating individual rules (`evaluate_single_rule`) -//! - Evaluating hierarchical rule sets with context (`evaluate_rules`) -//! - Providing a convenience wrapper for evaluation with configuration -//! (`evaluate_rules_with_config`) - -use crate::parser::ast::MagicRule; -use crate::{EvaluationConfig, LibmagicError}; - -use super::{EvaluationContext, RuleMatch, offset, operators, types}; - -/// Evaluate a single magic rule against a file buffer -/// -/// This function performs the core rule evaluation by: -/// 1. Resolving the rule's offset specification to an absolute position -/// 2. Reading and interpreting bytes at that position according to the rule's type -/// 3. Coercing the expected value to match the type's signedness and bit width -/// 4. Applying the rule's operator to compare the read value with the expected value -/// -/// # Arguments -/// -/// * `rule` - The magic rule to evaluate -/// * `buffer` - The file buffer to evaluate against -/// -/// # Returns -/// -/// Returns `Ok(Some((offset, value)))` if the rule matches (with the resolved offset and -/// read value), `Ok(None)` if it doesn't match, or `Err(LibmagicError)` if evaluation -/// fails due to buffer access issues or other errors. -/// -/// # Examples -/// -/// ```rust -/// use libmagic_rs::evaluator::evaluate_single_rule; -/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; -/// -/// // Create a rule to check for ELF magic bytes at offset 0 -/// let rule = MagicRule { -/// offset: OffsetSpec::Absolute(0), -/// typ: TypeKind::Byte { signed: true }, -/// op: Operator::Equal, -/// value: Value::Uint(0x7f), -/// message: "ELF magic".to_string(), -/// children: vec![], -/// level: 0, -/// strength_modifier: None, -/// }; -/// -/// let elf_buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes -/// let result = evaluate_single_rule(&rule, elf_buffer).unwrap(); -/// assert!(result.is_some()); // Should match -/// -/// let non_elf_buffer = &[0x50, 0x4b, 0x03, 0x04]; // ZIP magic bytes -/// let result = evaluate_single_rule(&rule, non_elf_buffer).unwrap(); -/// assert!(result.is_none()); // Should not match -/// ``` -/// -/// # Errors -/// -/// * `LibmagicError::EvaluationError` - If offset resolution fails, buffer access is out of bounds, -/// or type interpretation fails -pub fn evaluate_single_rule( - rule: &MagicRule, - buffer: &[u8], -) -> Result, LibmagicError> { - // Step 1: Resolve the offset specification to an absolute position - let absolute_offset = offset::resolve_offset(&rule.offset, buffer)?; - - // Step 2: Read and interpret bytes at the resolved offset according to the rule's type - let read_value = types::read_typed_value(buffer, absolute_offset, &rule.typ) - .map_err(|e| LibmagicError::EvaluationError(e.into()))?; - - // Step 3: Coerce the rule's expected value to match the type's signedness/width - let expected_value = types::coerce_value_to_type(&rule.value, &rule.typ); - - // Step 4: Apply the operator to compare the read value with the expected value - // BitwiseNot needs type-aware bit-width masking so the complement is computed - // at the type's natural width (e.g., byte NOT of 0x00 = 0xFF, not u64::MAX). - let matched = match &rule.op { - crate::parser::ast::Operator::BitwiseNot => operators::apply_bitwise_not_with_width( - &read_value, - &expected_value, - rule.typ.bit_width(), - ), - op => operators::apply_operator(op, &read_value, &expected_value), - }; - Ok(matched.then_some((absolute_offset, read_value))) -} - -/// Evaluate a list of magic rules against a file buffer with hierarchical processing -/// -/// This function implements the core hierarchical rule evaluation algorithm with graceful -/// error handling: -/// 1. Evaluates each top-level rule in sequence -/// 2. If a parent rule matches, evaluates its child rules for refinement -/// 3. Collects all matches or stops at first match based on configuration -/// 4. Maintains evaluation context for recursion limits and state -/// 5. Implements graceful degradation by skipping problematic rules and continuing evaluation -/// -/// The hierarchical evaluation follows these principles: -/// - Parent rules must match before children are evaluated -/// - Child rules provide refinement and additional detail -/// - Evaluation can stop at first match or continue for all matches -/// - Recursion depth is limited to prevent infinite loops -/// - Problematic rules are skipped to allow evaluation to continue -/// -/// # Arguments -/// -/// * `rules` - The list of magic rules to evaluate -/// * `buffer` - The file buffer to evaluate against -/// * `context` - Mutable evaluation context for state management -/// -/// # Returns -/// -/// Returns `Ok(Vec)` containing all matches found. Errors in individual rules -/// are skipped to allow evaluation to continue. Only returns `Err(LibmagicError)` -/// for critical failures like timeout or recursion limit exceeded. -/// -/// # Examples -/// -/// ```rust -/// use libmagic_rs::evaluator::{evaluate_rules, EvaluationContext, RuleMatch}; -/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; -/// use libmagic_rs::EvaluationConfig; -/// -/// // Create a hierarchical rule set for ELF files -/// let parent_rule = MagicRule { -/// offset: OffsetSpec::Absolute(0), -/// typ: TypeKind::Byte { signed: true }, -/// op: Operator::Equal, -/// value: Value::Uint(0x7f), -/// message: "ELF".to_string(), -/// children: vec![ -/// MagicRule { -/// offset: OffsetSpec::Absolute(4), -/// typ: TypeKind::Byte { signed: true }, -/// op: Operator::Equal, -/// value: Value::Uint(2), -/// message: "64-bit".to_string(), -/// children: vec![], -/// level: 1, -/// strength_modifier: None, -/// } -/// ], -/// level: 0, -/// strength_modifier: None, -/// }; -/// -/// let rules = vec![parent_rule]; -/// let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; // ELF64 header -/// let config = EvaluationConfig::default(); -/// let mut context = EvaluationContext::new(config); -/// -/// let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); -/// assert_eq!(matches.len(), 2); // Parent and child should both match -/// ``` -/// -/// # Errors -/// -/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout -/// * `LibmagicError::EvaluationError` - Only for critical failures like recursion limit exceeded -/// -/// Individual rule evaluation errors are handled gracefully and do not stop the overall evaluation. -pub fn evaluate_rules( - rules: &[MagicRule], - buffer: &[u8], - context: &mut EvaluationContext, -) -> Result, LibmagicError> { - let mut matches = Vec::with_capacity(8); - let start_time = std::time::Instant::now(); - let mut rule_count = 0u32; - - for rule in rules { - // Check timeout periodically (every 16 rules) to reduce syscall overhead - rule_count = rule_count.wrapping_add(1); - if rule_count.trailing_zeros() >= 4 - && let Some(timeout_ms) = context.timeout_ms() - && start_time.elapsed().as_millis() > u128::from(timeout_ms) - { - return Err(LibmagicError::Timeout { timeout_ms }); - } - - // Evaluate the current rule with graceful error handling - let match_data = match evaluate_single_rule(rule, buffer) { - Ok(data) => data, - Err( - LibmagicError::EvaluationError( - crate::error::EvaluationError::BufferOverrun { .. } - | crate::error::EvaluationError::InvalidOffset { .. } - | crate::error::EvaluationError::TypeReadError(_), - ) - | LibmagicError::IoError(_), - ) => { - // Expected evaluation errors for individual rules -- skip gracefully - continue; - } - Err(e) => { - // Unexpected errors (InternalError, UnsupportedType, etc.) should propagate - return Err(e); - } - }; - - if let Some((absolute_offset, read_value)) = match_data { - let match_result = RuleMatch { - message: rule.message.clone(), - offset: absolute_offset, - level: rule.level, - value: read_value, - confidence: RuleMatch::calculate_confidence(rule.level), - }; - matches.push(match_result); - - // If this rule has children, evaluate them recursively - if !rule.children.is_empty() { - // Check recursion depth limit - this is a critical error that should stop evaluation - context.increment_recursion_depth()?; - - // Recursively evaluate child rules with graceful error handling - match evaluate_rules(&rule.children, buffer, context) { - Ok(child_matches) => { - matches.extend(child_matches); - } - Err(LibmagicError::Timeout { timeout_ms }) => { - // Timeout is critical, propagate it up - let _ = context.decrement_recursion_depth(); - return Err(LibmagicError::Timeout { timeout_ms }); - } - Err(LibmagicError::EvaluationError( - crate::error::EvaluationError::RecursionLimitExceeded { .. }, - )) => { - // Recursion limit is critical, propagate it up - let _ = context.decrement_recursion_depth(); - return Err(LibmagicError::EvaluationError( - crate::error::EvaluationError::RecursionLimitExceeded { - depth: context.recursion_depth(), - }, - )); - } - Err( - LibmagicError::EvaluationError( - crate::error::EvaluationError::BufferOverrun { .. } - | crate::error::EvaluationError::InvalidOffset { .. } - | crate::error::EvaluationError::TypeReadError(_), - ) - | LibmagicError::IoError(_), - ) => { - // Expected child evaluation errors -- skip gracefully - } - Err(e) => { - // Unexpected errors in children should propagate - let _ = context.decrement_recursion_depth(); - return Err(e); - } - } - - // Restore recursion depth - context.decrement_recursion_depth()?; - } - - // Stop at first match if configured to do so - if context.should_stop_at_first_match() { - break; - } - } - } - - Ok(matches) -} - -/// Evaluate magic rules with a fresh context -/// -/// This is a convenience function that creates a new evaluation context -/// and evaluates the rules. Useful for simple evaluation scenarios. -/// -/// # Arguments -/// -/// * `rules` - The list of magic rules to evaluate -/// * `buffer` - The file buffer to evaluate against -/// * `config` - Configuration for evaluation behavior -/// -/// # Returns -/// -/// Returns `Ok(Vec)` containing all matches found, or `Err(LibmagicError)` -/// if evaluation fails. -/// -/// # Examples -/// -/// ```rust -/// use libmagic_rs::evaluator::{evaluate_rules_with_config, RuleMatch}; -/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; -/// use libmagic_rs::EvaluationConfig; -/// -/// let rule = MagicRule { -/// offset: OffsetSpec::Absolute(0), -/// typ: TypeKind::Byte { signed: true }, -/// op: Operator::Equal, -/// value: Value::Uint(0x7f), -/// message: "ELF magic".to_string(), -/// children: vec![], -/// level: 0, -/// strength_modifier: None, -/// }; -/// -/// let rules = vec![rule]; -/// let buffer = &[0x7f, 0x45, 0x4c, 0x46]; -/// let config = EvaluationConfig::default(); -/// -/// let matches = evaluate_rules_with_config(&rules, buffer, &config).unwrap(); -/// assert_eq!(matches.len(), 1); -/// assert_eq!(matches[0].message, "ELF magic"); -/// ``` -/// -/// # Errors -/// -/// * `LibmagicError::EvaluationError` - If rule evaluation fails -/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout -pub fn evaluate_rules_with_config( - rules: &[MagicRule], - buffer: &[u8], - config: &EvaluationConfig, -) -> Result, LibmagicError> { - let mut context = EvaluationContext::new(config.clone()); - evaluate_rules(rules, buffer, &mut context) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::parser::ast::{Endianness, OffsetSpec, Operator, TypeKind, Value}; - - #[test] - fn test_evaluate_single_rule_byte_equal_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_byte_equal_no_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x50, 0x4b, 0x03, 0x04]; // ZIP magic bytes - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_none()); - } - - #[test] - fn test_evaluate_single_rule_byte_not_equal_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::NotEqual, - value: Value::Uint(0x00), - message: "Non-zero byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // 0x7f != 0x00 - } - - #[test] - fn test_evaluate_single_rule_byte_not_equal_no_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::NotEqual, - value: Value::Uint(0x7f), - message: "Not ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_none()); // 0x7f == 0x7f, so NotEqual is false - } - - #[test] - fn test_evaluate_single_rule_byte_bitwise_and_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::BitwiseAnd, - value: Value::Uint(0x80), // Check if high bit is set - message: "High bit set".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0xff, 0x45, 0x4c, 0x46]; // 0xff has high bit set - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); // 0xff & 0x80 = 0x80 (non-zero) - } - - #[test] - fn test_evaluate_single_rule_byte_bitwise_and_no_match() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::BitwiseAnd, - value: Value::Uint(0x80), // Check if high bit is set - message: "High bit set".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // 0x7f has high bit clear - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_none()); // 0x7f & 0x80 = 0x00 (zero) - } - - #[test] - fn test_evaluate_single_rule_short_little_endian() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234), - message: "Little-endian short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x34, 0x12, 0x56, 0x78]; // 0x1234 in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_short_big_endian() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Big, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234), - message: "Big-endian short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x12, 0x34, 0x56, 0x78]; // 0x1234 in big-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_short_signed_positive() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Little, - signed: true, - }, - op: Operator::Equal, - value: Value::Int(32767), // 0x7fff - message: "Positive signed short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0xff, 0x7f, 0x00, 0x00]; // 0x7fff in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_short_signed_negative() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Little, - signed: true, - }, - op: Operator::Equal, - value: Value::Int(-1), // 0xffff as signed - message: "Negative signed short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0xff, 0xff, 0x00, 0x00]; // 0xffff in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_long_little_endian() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234_5678), - message: "Little-endian long".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x78, 0x56, 0x34, 0x12, 0x00]; // 0x12345678 in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_long_big_endian() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Big, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234_5678), - message: "Big-endian long".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x12, 0x34, 0x56, 0x78, 0x00]; // 0x12345678 in big-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_long_signed_positive() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: true, - }, - op: Operator::Equal, - value: Value::Int(2_147_483_647), // 0x7fffffff - message: "Positive signed long".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0xff, 0xff, 0xff, 0x7f, 0x00]; // 0x7fffffff in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_long_signed_negative() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: true, - }, - op: Operator::Equal, - value: Value::Int(-1), // 0xffffffff as signed - message: "Negative signed long".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0xff, 0xff, 0xff, 0xff, 0x00]; // 0xffffffff in little-endian - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_different_offsets() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(2), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x4c), - message: "ELF class byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_negative_offset() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(-1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x46), - message: "Last byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_from_end_offset() { - let rule = MagicRule { - offset: OffsetSpec::FromEnd(-2), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x4c), - message: "Second to last byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_offset_out_of_bounds() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(10), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Out of bounds".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let result = evaluate_single_rule(&rule, buffer); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Buffer overrun")); - } - _ => panic!("Expected EvaluationError"), - } - } - - #[test] - fn test_evaluate_single_rule_short_insufficient_bytes() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(3), - typ: TypeKind::Short { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234), - message: "Insufficient bytes".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let result = evaluate_single_rule(&rule, buffer); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Buffer overrun")); - } - _ => panic!("Expected EvaluationError"), - } - } - - #[test] - fn test_evaluate_single_rule_long_insufficient_bytes() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(2), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234_5678), - message: "Insufficient bytes".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let result = evaluate_single_rule(&rule, buffer); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Buffer overrun")); - } - _ => panic!("Expected EvaluationError"), - } - } - - #[test] - fn test_evaluate_single_rule_empty_buffer() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Empty buffer".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[]; - let result = evaluate_single_rule(&rule, buffer); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Buffer overrun")); - } - _ => panic!("Expected EvaluationError"), - } - } - - #[test] - fn test_evaluate_single_rule_string_type_supported() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::String { max_length: None }, - op: Operator::Equal, - value: Value::String("test".to_string()), - message: "String type".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = b"test\x00 data"; - let result = evaluate_single_rule(&rule, buffer); - assert!(result.is_ok()); - let matches = result.unwrap(); - assert!(matches.is_some()); - - let rule_no_match = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::String { max_length: None }, - op: Operator::Equal, - value: Value::String("hello".to_string()), - message: "String type".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let result = evaluate_single_rule(&rule_no_match, buffer); - assert!(result.is_ok()); - let matches = result.unwrap(); - assert!(matches.is_none()); - } - - #[test] - fn test_evaluate_single_rule_cross_type_comparison() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Int(42), - message: "Cross-type comparison".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[42]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_bitwise_and_with_shorts() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Little, - signed: false, - }, - op: Operator::BitwiseAnd, - value: Value::Uint(0xff00), - message: "High byte check".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x34, 0x12]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_bitwise_and_with_longs() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Big, - signed: false, - }, - op: Operator::BitwiseAnd, - value: Value::Uint(0xffff_0000), - message: "High word check".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x12, 0x34, 0x56, 0x78]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_comprehensive_elf_check() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x464c_457f), - message: "ELF executable".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let elf_buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; - let result = evaluate_single_rule(&rule, elf_buffer).unwrap(); - assert!(result.is_some()); - - let non_elf_buffer = &[0x50, 0x4b, 0x03, 0x04, 0x14, 0x00]; - let result = evaluate_single_rule(&rule, non_elf_buffer).unwrap(); - assert!(result.is_none()); - } - - #[test] - fn test_evaluate_single_rule_native_endianness() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Short { - endian: Endianness::Native, - signed: false, - }, - op: Operator::NotEqual, - value: Value::Uint(0), - message: "Non-zero native short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x01, 0x02]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_all_operators() { - let buffer = &[0x42, 0x00, 0xff, 0x80]; - - let equal_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x42), - message: "Equal test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!(evaluate_single_rule(&equal_rule, buffer).unwrap().is_some()); - - let not_equal_rule = MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::NotEqual, - value: Value::Uint(0x42), - message: "NotEqual test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(¬_equal_rule, buffer) - .unwrap() - .is_some() - ); - - let bitwise_and_rule = MagicRule { - offset: OffsetSpec::Absolute(3), - typ: TypeKind::Byte { signed: true }, - op: Operator::BitwiseAnd, - value: Value::Uint(0x80), - message: "BitwiseAnd test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&bitwise_and_rule, buffer) - .unwrap() - .is_some() - ); - } - - #[test] - fn test_evaluate_single_rule_comparison_operators() { - let buffer = &[0x42, 0x00, 0xff, 0x80]; - - let less_than_rule = MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: false }, - op: Operator::LessThan, - value: Value::Uint(0x42), - message: "LessThan test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&less_than_rule, buffer) - .unwrap() - .is_some() - ); - - let greater_than_rule = MagicRule { - offset: OffsetSpec::Absolute(2), - typ: TypeKind::Byte { signed: false }, - op: Operator::GreaterThan, - value: Value::Uint(0x42), - message: "GreaterThan test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&greater_than_rule, buffer) - .unwrap() - .is_some() - ); - - let less_equal_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: false }, - op: Operator::LessEqual, - value: Value::Uint(0x42), - message: "LessEqual test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&less_equal_rule, buffer) - .unwrap() - .is_some() - ); - - let greater_equal_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: false }, - op: Operator::GreaterEqual, - value: Value::Uint(0x42), - message: "GreaterEqual test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&greater_equal_rule, buffer) - .unwrap() - .is_some() - ); - } - - #[test] - fn test_evaluate_comparison_with_signed_byte() { - let buffer = &[0x80]; - - let signed_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::LessThan, - value: Value::Uint(0), - message: "signed less".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&signed_rule, buffer) - .unwrap() - .is_some() - ); - - let unsigned_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: false }, - op: Operator::LessThan, - value: Value::Uint(0), - message: "unsigned less".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - assert!( - evaluate_single_rule(&unsigned_rule, buffer) - .unwrap() - .is_none() - ); - } - - #[test] - fn test_evaluate_comparison_operators_negative_cases() { - let buffer = &[0x42]; - - let cases: Vec<(Operator, u64, bool)> = vec![ - (Operator::LessThan, 66, false), - (Operator::LessThan, 67, true), - (Operator::GreaterThan, 66, false), - (Operator::GreaterThan, 65, true), - (Operator::LessEqual, 65, false), - (Operator::LessEqual, 66, true), - (Operator::GreaterEqual, 67, false), - (Operator::GreaterEqual, 66, true), - ]; - - for (op, value, expected) in cases { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: false }, - op: op.clone(), - value: Value::Uint(value), - message: "test".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert_eq!( - result.is_some(), - expected, - "{op:?} with value {value}: expected {expected}" - ); - } - } - - #[test] - fn test_evaluate_single_rule_edge_case_values() { - let max_uint_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0xffff_ffff), - message: "Max uint32".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let max_buffer = &[0xff, 0xff, 0xff, 0xff]; - let result = evaluate_single_rule(&max_uint_rule, max_buffer).unwrap(); - assert!(result.is_some()); - - let min_int_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: true, - }, - op: Operator::Equal, - value: Value::Int(-2_147_483_648), - message: "Min int32".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let min_buffer = &[0x00, 0x00, 0x00, 0x80]; - let result = evaluate_single_rule(&min_int_rule, min_buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_single_rule_various_buffer_sizes() { - let single_byte_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: false }, - op: Operator::Equal, - value: Value::Uint(0xaa), - message: "Single byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let single_buffer = &[0xaa]; - let result = evaluate_single_rule(&single_byte_rule, single_buffer).unwrap(); - assert!(result.is_some()); - - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let large_buffer: Vec = (0..1024).map(|i| (i % 256) as u8).collect(); - let large_rule = MagicRule { - offset: OffsetSpec::Absolute(1000), - typ: TypeKind::Byte { signed: false }, - op: Operator::Equal, - value: Value::Uint((1000 % 256) as u64), - message: "Large buffer".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let result = evaluate_single_rule(&large_rule, &large_buffer).unwrap(); - assert!(result.is_some()); - } - - #[test] - fn test_evaluate_rules_empty_list() { - let rules = vec![]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert!(matches.is_empty()); - } - - #[test] - fn test_evaluate_rules_single_matching_rule() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 1); - assert_eq!(matches[0].message, "ELF magic"); - assert_eq!(matches[0].offset, 0); - assert_eq!(matches[0].level, 0); - assert_eq!(matches[0].value, Value::Int(0x7f)); - } - - #[test] - fn test_evaluate_rules_single_non_matching_rule() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x50), - message: "ZIP magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert!(matches.is_empty()); - } - - #[test] - fn test_evaluate_rules_multiple_rules_stop_at_first() { - let rule1 = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "First match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule2 = MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Second match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule_list = vec![rule1, rule2]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - stop_at_first_match: true, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rule_list, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 1); - assert_eq!(matches[0].message, "First match"); - } - - #[test] - fn test_evaluate_rules_multiple_rules_find_all() { - let rule1 = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "First match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule2 = MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Second match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule_set = vec![rule1, rule2]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - stop_at_first_match: false, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rule_set, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "First match"); - assert_eq!(matches[1].message, "Second match"); - } - - #[test] - fn test_evaluate_rules_hierarchical_parent_child() { - let child_rule = MagicRule { - offset: OffsetSpec::Absolute(4), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x02), - message: "64-bit".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }; - - let parent_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF".to_string(), - children: vec![child_rule], - level: 0, - strength_modifier: None, - }; - - let rules = vec![parent_rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "ELF"); - assert_eq!(matches[0].level, 0); - assert_eq!(matches[1].message, "64-bit"); - assert_eq!(matches[1].level, 1); - } - - #[test] - fn test_evaluate_rules_hierarchical_parent_no_match() { - let child_rule = MagicRule { - offset: OffsetSpec::Absolute(4), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x02), - message: "64-bit".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }; - - let parent_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x50), - message: "ZIP".to_string(), - children: vec![child_rule], - level: 0, - strength_modifier: None, - }; - - let rules = vec![parent_rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert!(matches.is_empty()); - } - - #[test] - fn test_evaluate_rules_hierarchical_parent_match_child_no_match() { - let child_rule = MagicRule { - offset: OffsetSpec::Absolute(4), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x01), - message: "32-bit".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }; - - let parent_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF".to_string(), - children: vec![child_rule], - level: 0, - strength_modifier: None, - }; - - let rules = vec![parent_rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 1); - assert_eq!(matches[0].message, "ELF"); - assert_eq!(matches[0].level, 0); - } - - #[test] - fn test_evaluate_rules_deep_hierarchy() { - let grandchild_rule = MagicRule { - offset: OffsetSpec::Absolute(5), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x01), - message: "little-endian".to_string(), - children: vec![], - level: 2, - strength_modifier: None, - }; - - let child_rule = MagicRule { - offset: OffsetSpec::Absolute(4), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x02), - message: "64-bit".to_string(), - children: vec![grandchild_rule], - level: 1, - strength_modifier: None, - }; - - let parent_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF".to_string(), - children: vec![child_rule], - level: 0, - strength_modifier: None, - }; - - let rules = vec![parent_rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 3); - assert_eq!(matches[0].message, "ELF"); - assert_eq!(matches[0].level, 0); - assert_eq!(matches[1].message, "64-bit"); - assert_eq!(matches[1].level, 1); - assert_eq!(matches[2].message, "little-endian"); - assert_eq!(matches[2].level, 2); - } - - #[test] - fn test_evaluate_rules_multiple_children() { - let child1 = MagicRule { - offset: OffsetSpec::Absolute(4), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x02), - message: "64-bit".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }; - - let child2 = MagicRule { - offset: OffsetSpec::Absolute(5), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x01), - message: "little-endian".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }; - - let parent_rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF".to_string(), - children: vec![child1, child2], - level: 0, - strength_modifier: None, - }; - - let rules = vec![parent_rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; - let config = EvaluationConfig { - stop_at_first_match: false, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 3); - assert_eq!(matches[0].message, "ELF"); - assert_eq!(matches[1].message, "64-bit"); - assert_eq!(matches[2].message, "little-endian"); - } - - #[test] - fn test_evaluate_rules_recursion_depth_limit() { - let mut current_rule = MagicRule { - offset: OffsetSpec::Absolute(10), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Deep level".to_string(), - children: vec![], - level: 10, - strength_modifier: None, - }; - - for i in (0u32..10u32).rev() { - current_rule = MagicRule { - offset: OffsetSpec::Absolute(i64::from(i)), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(u64::from(i)), - message: format!("Level {i}"), - children: vec![current_rule], - level: i, - strength_modifier: None, - }; - } - - let rules = vec![current_rule]; - let buffer = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; - let config = EvaluationConfig { - max_recursion_depth: 5, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let result = evaluate_rules(&rules, buffer, &mut context); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Recursion limit exceeded")); - } - _ => panic!("Expected EvaluationError for recursion limit"), - } - } - - #[test] - fn test_evaluate_rules_with_config_convenience() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - - let matches = evaluate_rules_with_config(&rules, buffer, &config).unwrap(); - assert_eq!(matches.len(), 1); - assert_eq!(matches[0].message, "ELF magic"); - } - - #[test] - fn test_evaluate_rules_timeout() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - timeout_ms: Some(0), - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let result = evaluate_rules(&rules, buffer, &mut context); - if let Err(LibmagicError::Timeout { timeout_ms }) = result { - assert_eq!(timeout_ms, 0); - } - } - - #[test] - fn test_evaluate_rules_empty_buffer() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Should not match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let result = evaluate_rules(&rules, buffer, &mut context); - assert!(result.is_ok()); - - let matches = result.unwrap(); - assert_eq!(matches.len(), 0); - } - - #[test] - fn test_evaluate_rules_mixed_matching_non_matching() { - let rule1 = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Matches".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule2 = MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x99), - message: "Doesn't match".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule3 = MagicRule { - offset: OffsetSpec::Absolute(2), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x4c), - message: "Also matches".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rule_collection = vec![rule1, rule2, rule3]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - stop_at_first_match: false, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rule_collection, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "Matches"); - assert_eq!(matches[1].message, "Also matches"); - } - - #[test] - fn test_evaluate_rules_context_state_preservation() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "ELF magic".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let rules = vec![rule]; - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - context.set_current_offset(100); - let initial_offset = context.current_offset(); - let initial_depth = context.recursion_depth(); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 1); - - assert_eq!(context.current_offset(), initial_offset); - assert_eq!(context.recursion_depth(), initial_depth); - } - - #[test] - fn test_error_recovery_skip_problematic_rules() { - let rules = vec![ - MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Valid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - MagicRule { - offset: OffsetSpec::Absolute(100), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Invalid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Another valid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - ]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - max_recursion_depth: 20, - max_string_length: 8192, - stop_at_first_match: false, - enable_mime_types: false, - timeout_ms: None, - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "Valid rule"); - assert_eq!(matches[1].message, "Another valid rule"); - } - - #[test] - fn test_error_recovery_child_rule_failures() { - let rules = vec![MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Parent rule".to_string(), - children: vec![ - MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Valid child".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }, - MagicRule { - offset: OffsetSpec::Absolute(100), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Invalid child".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }, - ], - level: 0, - strength_modifier: None, - }]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "Parent rule"); - assert_eq!(matches[1].message, "Valid child"); - } - - #[test] - fn test_error_recovery_mixed_rule_types() { - let rules = vec![ - MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Valid byte".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - MagicRule { - offset: OffsetSpec::Absolute(3), - typ: TypeKind::Short { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234), - message: "Invalid short".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::String { - max_length: Some(3), - }, - op: Operator::Equal, - value: Value::String("ELF".to_string()), - message: "Valid string".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - ]; - - let buffer = &[0x7f, b'E', b'L', b'F']; - let config = EvaluationConfig { - max_recursion_depth: 20, - max_string_length: 8192, - stop_at_first_match: false, - enable_mime_types: false, - timeout_ms: None, - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - - assert_eq!(matches.len(), 2); - assert_eq!(matches[0].message, "Valid byte"); - assert_eq!(matches[1].message, "Valid string"); - } - - #[test] - fn test_error_recovery_all_rules_fail() { - let rules = vec![ - MagicRule { - offset: OffsetSpec::Absolute(100), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Out of bounds".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - MagicRule { - offset: OffsetSpec::Absolute(2), - typ: TypeKind::Long { - endian: Endianness::Little, - signed: false, - }, - op: Operator::Equal, - value: Value::Uint(0x1234_5678), - message: "Insufficient bytes".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - ]; - - let buffer = &[0x7f, 0x45]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 0); - } - - #[test] - fn test_error_recovery_timeout_propagation() { - let rules = vec![MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Test rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - max_recursion_depth: 10, - max_string_length: 1024, - stop_at_first_match: false, - enable_mime_types: false, - timeout_ms: Some(0), - }; - let mut context = EvaluationContext::new(config); - - let result = evaluate_rules(&rules, buffer, &mut context); - - match result { - Ok(_) | Err(LibmagicError::Timeout { .. }) => {} - Err(e) => { - panic!("Unexpected error type: {e:?}"); - } - } - } - - #[test] - fn test_error_recovery_recursion_limit_propagation() { - let rules = vec![MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Parent".to_string(), - children: vec![MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Child".to_string(), - children: vec![], - level: 1, - strength_modifier: None, - }], - level: 0, - strength_modifier: None, - }]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig { - max_recursion_depth: 0, - max_string_length: 1024, - stop_at_first_match: false, - enable_mime_types: false, - timeout_ms: None, - }; - let mut context = EvaluationContext::new(config); - - let result = evaluate_rules(&rules, buffer, &mut context); - assert!(result.is_err()); - - match result.unwrap_err() { - LibmagicError::EvaluationError( - crate::error::EvaluationError::RecursionLimitExceeded { .. }, - ) => {} - _ => panic!("Expected recursion limit error"), - } - } - - #[test] - fn test_error_recovery_preserves_context_state() { - let rules = vec![ - MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Valid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - MagicRule { - offset: OffsetSpec::Absolute(100), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Invalid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - ]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - context.set_current_offset(42); - let initial_offset = context.current_offset(); - let initial_depth = context.recursion_depth(); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 1); - - assert_eq!(context.current_offset(), initial_offset); - assert_eq!(context.recursion_depth(), initial_depth); - } - - #[test] - fn test_any_value_parse_and_evaluate_paren_message() { - use crate::parser::grammar::parse_magic_rule; - - let input = ">0 byte x (0)"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::AnyValue); - assert_eq!(rule.message, "(0)"); - - let buffer = &[0x00, 0x01, 0x02, 0x03]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_some(), - "AnyValue rule should match unconditionally" - ); - } - - #[test] - fn test_any_value_parse_and_evaluate_backslash_message() { - use crate::parser::grammar::parse_magic_rule; - - let input = "0 long x \\b, data"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::AnyValue); - assert_eq!(rule.message, "\\b, data"); - - let buffer = &[0xFF, 0xFE, 0xFD, 0xFC]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_some(), - "AnyValue rule should match unconditionally" - ); - } - - #[test] - fn test_any_value_parse_and_evaluate_no_message() { - use crate::parser::grammar::parse_magic_rule; - - let input = "0 byte x"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::AnyValue); - - let buffer = &[0x42]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_some(), - "AnyValue rule should match unconditionally" - ); - } - - #[test] - fn test_bitwise_xor_parse_and_evaluate_match() { - use crate::parser::grammar::parse_magic_rule; - - let input = "0 byte ^0x01 XOR match"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::BitwiseXor); - assert_eq!(rule.message, "XOR match"); - - let buffer = &[0x0F]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_some(), - "BitwiseXor should match when XOR is non-zero" - ); - } - - #[test] - fn test_bitwise_xor_parse_and_evaluate_no_match() { - use crate::parser::grammar::parse_magic_rule; - - let input = "0 byte ^0x42 XOR no match"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::BitwiseXor); - - let buffer = &[0x42]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_none(), - "BitwiseXor should not match when XOR is zero" - ); - } - - #[test] - fn test_bitwise_not_parse_and_evaluate_match() { - use crate::parser::grammar::parse_magic_rule; - - let input = "0 ubyte ~0xFF NOT match"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::BitwiseNot); - assert_eq!(rule.message, "NOT match"); - - let buffer = &[0x00]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_some(), - "BitwiseNot should match when NOT(value) equals operand at byte width" - ); - } - - #[test] - fn test_bitwise_not_parse_and_evaluate_no_match() { - use crate::parser::grammar::parse_magic_rule; - - let input = "0 ubyte ~0x01 NOT no match"; - let (_, rule) = parse_magic_rule(input).unwrap(); - assert_eq!(rule.op, Operator::BitwiseNot); - - let buffer = &[0x42]; - let result = evaluate_single_rule(&rule, buffer).unwrap(); - assert!( - result.is_none(), - "BitwiseNot should not match when NOT(value) != operand" - ); - } - - #[test] - fn test_debug_error_recovery() { - let rule = MagicRule { - offset: OffsetSpec::Absolute(100), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Out of bounds rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }; - - let buffer = &[0x7f, 0x45]; - - let single_result = evaluate_single_rule(&rule, buffer); - assert!(single_result.is_err()); - - let rules = vec![rule]; - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 0); - } - - #[test] - fn test_debug_mixed_rules() { - let rules = vec![ - MagicRule { - offset: OffsetSpec::Absolute(0), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x7f), - message: "Valid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - MagicRule { - offset: OffsetSpec::Absolute(100), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x00), - message: "Invalid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - MagicRule { - offset: OffsetSpec::Absolute(1), - typ: TypeKind::Byte { signed: true }, - op: Operator::Equal, - value: Value::Uint(0x45), - message: "Another valid rule".to_string(), - children: vec![], - level: 0, - strength_modifier: None, - }, - ]; - - let buffer = &[0x7f, 0x45, 0x4c, 0x46]; - - let config = EvaluationConfig { - stop_at_first_match: false, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); - assert_eq!(matches.len(), 2); - } -} diff --git a/src/evaluator/engine/mod.rs b/src/evaluator/engine/mod.rs new file mode 100644 index 00000000..29692fb2 --- /dev/null +++ b/src/evaluator/engine/mod.rs @@ -0,0 +1,331 @@ +// Copyright (c) 2025-2026 the libmagic-rs contributors +// SPDX-License-Identifier: Apache-2.0 + +//! Core evaluation engine for magic rules. +//! +//! This module contains the core recursive evaluation logic for executing magic +//! rules against file buffers. It is responsible for: +//! - Evaluating individual rules (`evaluate_single_rule`) +//! - Evaluating hierarchical rule sets with context (`evaluate_rules`) +//! - Providing a convenience wrapper for evaluation with configuration +//! (`evaluate_rules_with_config`) + +use crate::parser::ast::MagicRule; +use crate::{EvaluationConfig, LibmagicError}; + +use super::{EvaluationContext, RuleMatch, offset, operators, types}; + +/// Evaluate a single magic rule against a file buffer +/// +/// This function performs the core rule evaluation by: +/// 1. Resolving the rule's offset specification to an absolute position +/// 2. Reading and interpreting bytes at that position according to the rule's type +/// 3. Coercing the expected value to match the type's signedness and bit width +/// 4. Applying the rule's operator to compare the read value with the expected value +/// +/// # Arguments +/// +/// * `rule` - The magic rule to evaluate +/// * `buffer` - The file buffer to evaluate against +/// +/// # Returns +/// +/// Returns `Ok(Some((offset, value)))` if the rule matches (with the resolved offset and +/// read value), `Ok(None)` if it doesn't match, or `Err(LibmagicError)` if evaluation +/// fails due to buffer access issues or other errors. +/// +/// # Examples +/// +/// ```rust +/// use libmagic_rs::evaluator::evaluate_single_rule; +/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; +/// +/// // Create a rule to check for ELF magic bytes at offset 0 +/// let rule = MagicRule { +/// offset: OffsetSpec::Absolute(0), +/// typ: TypeKind::Byte { signed: true }, +/// op: Operator::Equal, +/// value: Value::Uint(0x7f), +/// message: "ELF magic".to_string(), +/// children: vec![], +/// level: 0, +/// strength_modifier: None, +/// }; +/// +/// let elf_buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes +/// let result = evaluate_single_rule(&rule, elf_buffer).unwrap(); +/// assert!(result.is_some()); // Should match +/// +/// let non_elf_buffer = &[0x50, 0x4b, 0x03, 0x04]; // ZIP magic bytes +/// let result = evaluate_single_rule(&rule, non_elf_buffer).unwrap(); +/// assert!(result.is_none()); // Should not match +/// ``` +/// +/// # Errors +/// +/// * `LibmagicError::EvaluationError` - If offset resolution fails, buffer access is out of bounds, +/// or type interpretation fails +pub fn evaluate_single_rule( + rule: &MagicRule, + buffer: &[u8], +) -> Result, LibmagicError> { + // Step 1: Resolve the offset specification to an absolute position + let absolute_offset = offset::resolve_offset(&rule.offset, buffer)?; + + // Step 2: Read and interpret bytes at the resolved offset according to the rule's type + let read_value = types::read_typed_value(buffer, absolute_offset, &rule.typ) + .map_err(|e| LibmagicError::EvaluationError(e.into()))?; + + // Step 3: Coerce the rule's expected value to match the type's signedness/width + let expected_value = types::coerce_value_to_type(&rule.value, &rule.typ); + + // Step 4: Apply the operator to compare the read value with the expected value + // BitwiseNot needs type-aware bit-width masking so the complement is computed + // at the type's natural width (e.g., byte NOT of 0x00 = 0xFF, not u64::MAX). + let matched = match &rule.op { + crate::parser::ast::Operator::BitwiseNot => operators::apply_bitwise_not_with_width( + &read_value, + &expected_value, + rule.typ.bit_width(), + ), + op => operators::apply_operator(op, &read_value, &expected_value), + }; + Ok(matched.then_some((absolute_offset, read_value))) +} + +/// Evaluate a list of magic rules against a file buffer with hierarchical processing +/// +/// This function implements the core hierarchical rule evaluation algorithm with graceful +/// error handling: +/// 1. Evaluates each top-level rule in sequence +/// 2. If a parent rule matches, evaluates its child rules for refinement +/// 3. Collects all matches or stops at first match based on configuration +/// 4. Maintains evaluation context for recursion limits and state +/// 5. Implements graceful degradation by skipping problematic rules and continuing evaluation +/// +/// The hierarchical evaluation follows these principles: +/// - Parent rules must match before children are evaluated +/// - Child rules provide refinement and additional detail +/// - Evaluation can stop at first match or continue for all matches +/// - Recursion depth is limited to prevent infinite loops +/// - Problematic rules are skipped to allow evaluation to continue +/// +/// # Arguments +/// +/// * `rules` - The list of magic rules to evaluate +/// * `buffer` - The file buffer to evaluate against +/// * `context` - Mutable evaluation context for state management +/// +/// # Returns +/// +/// Returns `Ok(Vec)` containing all matches found. Errors in individual rules +/// are skipped to allow evaluation to continue. Only returns `Err(LibmagicError)` +/// for critical failures like timeout or recursion limit exceeded. +/// +/// # Examples +/// +/// ```rust +/// use libmagic_rs::evaluator::{evaluate_rules, EvaluationContext, RuleMatch}; +/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; +/// use libmagic_rs::EvaluationConfig; +/// +/// // Create a hierarchical rule set for ELF files +/// let parent_rule = MagicRule { +/// offset: OffsetSpec::Absolute(0), +/// typ: TypeKind::Byte { signed: true }, +/// op: Operator::Equal, +/// value: Value::Uint(0x7f), +/// message: "ELF".to_string(), +/// children: vec![ +/// MagicRule { +/// offset: OffsetSpec::Absolute(4), +/// typ: TypeKind::Byte { signed: true }, +/// op: Operator::Equal, +/// value: Value::Uint(2), +/// message: "64-bit".to_string(), +/// children: vec![], +/// level: 1, +/// strength_modifier: None, +/// } +/// ], +/// level: 0, +/// strength_modifier: None, +/// }; +/// +/// let rules = vec![parent_rule]; +/// let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; // ELF64 header +/// let config = EvaluationConfig::default(); +/// let mut context = EvaluationContext::new(config); +/// +/// let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); +/// assert_eq!(matches.len(), 2); // Parent and child should both match +/// ``` +/// +/// # Errors +/// +/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout +/// * `LibmagicError::EvaluationError` - Only for critical failures like recursion limit exceeded +/// +/// Individual rule evaluation errors are handled gracefully and do not stop the overall evaluation. +pub fn evaluate_rules( + rules: &[MagicRule], + buffer: &[u8], + context: &mut EvaluationContext, +) -> Result, LibmagicError> { + let mut matches = Vec::with_capacity(8); + let start_time = std::time::Instant::now(); + let mut rule_count = 0u32; + + for rule in rules { + // Check timeout periodically (every 16 rules) to reduce syscall overhead + rule_count = rule_count.wrapping_add(1); + if rule_count.trailing_zeros() >= 4 + && let Some(timeout_ms) = context.timeout_ms() + && start_time.elapsed().as_millis() > u128::from(timeout_ms) + { + return Err(LibmagicError::Timeout { timeout_ms }); + } + + // Evaluate the current rule with graceful error handling + let match_data = match evaluate_single_rule(rule, buffer) { + Ok(data) => data, + Err( + LibmagicError::EvaluationError( + crate::error::EvaluationError::BufferOverrun { .. } + | crate::error::EvaluationError::InvalidOffset { .. } + | crate::error::EvaluationError::TypeReadError(_), + ) + | LibmagicError::IoError(_), + ) => { + // Expected evaluation errors for individual rules -- skip gracefully + continue; + } + Err(e) => { + // Unexpected errors (InternalError, UnsupportedType, etc.) should propagate + return Err(e); + } + }; + + if let Some((absolute_offset, read_value)) = match_data { + let match_result = RuleMatch { + message: rule.message.clone(), + offset: absolute_offset, + level: rule.level, + value: read_value, + confidence: RuleMatch::calculate_confidence(rule.level), + }; + matches.push(match_result); + + // If this rule has children, evaluate them recursively + if !rule.children.is_empty() { + // Check recursion depth limit - this is a critical error that should stop evaluation + context.increment_recursion_depth()?; + + // Recursively evaluate child rules with graceful error handling + match evaluate_rules(&rule.children, buffer, context) { + Ok(child_matches) => { + matches.extend(child_matches); + } + Err(LibmagicError::Timeout { timeout_ms }) => { + // Timeout is critical, propagate it up + let _ = context.decrement_recursion_depth(); + return Err(LibmagicError::Timeout { timeout_ms }); + } + Err( + e @ LibmagicError::EvaluationError( + crate::error::EvaluationError::RecursionLimitExceeded { .. }, + ), + ) => { + // Recursion limit is critical, propagate the original error + let _ = context.decrement_recursion_depth(); + return Err(e); + } + Err( + LibmagicError::EvaluationError( + crate::error::EvaluationError::BufferOverrun { .. } + | crate::error::EvaluationError::InvalidOffset { .. } + | crate::error::EvaluationError::TypeReadError(_), + ) + | LibmagicError::IoError(_), + ) => { + // Expected child evaluation errors -- skip gracefully + } + Err(e) => { + // Unexpected errors in children should propagate + let _ = context.decrement_recursion_depth(); + return Err(e); + } + } + + // Restore recursion depth + context.decrement_recursion_depth()?; + } + + // Stop at first match if configured to do so + if context.should_stop_at_first_match() { + break; + } + } + } + + Ok(matches) +} + +/// Evaluate magic rules with a fresh context +/// +/// This is a convenience function that creates a new evaluation context +/// and evaluates the rules. Useful for simple evaluation scenarios. +/// +/// # Arguments +/// +/// * `rules` - The list of magic rules to evaluate +/// * `buffer` - The file buffer to evaluate against +/// * `config` - Configuration for evaluation behavior +/// +/// # Returns +/// +/// Returns `Ok(Vec)` containing all matches found, or `Err(LibmagicError)` +/// if evaluation fails. +/// +/// # Examples +/// +/// ```rust +/// use libmagic_rs::evaluator::{evaluate_rules_with_config, RuleMatch}; +/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value}; +/// use libmagic_rs::EvaluationConfig; +/// +/// let rule = MagicRule { +/// offset: OffsetSpec::Absolute(0), +/// typ: TypeKind::Byte { signed: true }, +/// op: Operator::Equal, +/// value: Value::Uint(0x7f), +/// message: "ELF magic".to_string(), +/// children: vec![], +/// level: 0, +/// strength_modifier: None, +/// }; +/// +/// let rules = vec![rule]; +/// let buffer = &[0x7f, 0x45, 0x4c, 0x46]; +/// let config = EvaluationConfig::default(); +/// +/// let matches = evaluate_rules_with_config(&rules, buffer, &config).unwrap(); +/// assert_eq!(matches.len(), 1); +/// assert_eq!(matches[0].message, "ELF magic"); +/// ``` +/// +/// # Errors +/// +/// * `LibmagicError::EvaluationError` - If rule evaluation fails +/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout +pub fn evaluate_rules_with_config( + rules: &[MagicRule], + buffer: &[u8], + config: &EvaluationConfig, +) -> Result, LibmagicError> { + let mut context = EvaluationContext::new(config.clone()); + evaluate_rules(rules, buffer, &mut context) +} + +#[cfg(test)] +mod tests; diff --git a/src/evaluator/engine/tests.rs b/src/evaluator/engine/tests.rs new file mode 100644 index 00000000..09edcbdd --- /dev/null +++ b/src/evaluator/engine/tests.rs @@ -0,0 +1,1882 @@ +// Copyright (c) 2025-2026 the libmagic-rs contributors +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::parser::ast::{Endianness, OffsetSpec, Operator, TypeKind, Value}; + +#[test] +fn test_evaluate_single_rule_byte_equal_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_byte_equal_no_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x50, 0x4b, 0x03, 0x04]; // ZIP magic bytes + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_none()); +} + +#[test] +fn test_evaluate_single_rule_byte_not_equal_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::NotEqual, + value: Value::Uint(0x00), + message: "Non-zero byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); // 0x7f != 0x00 +} + +#[test] +fn test_evaluate_single_rule_byte_not_equal_no_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::NotEqual, + value: Value::Uint(0x7f), + message: "Not ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_none()); // 0x7f == 0x7f, so NotEqual is false +} + +#[test] +fn test_evaluate_single_rule_byte_bitwise_and_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::BitwiseAnd, + value: Value::Uint(0x80), // Check if high bit is set + message: "High bit set".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0xff, 0x45, 0x4c, 0x46]; // 0xff has high bit set + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); // 0xff & 0x80 = 0x80 (non-zero) +} + +#[test] +fn test_evaluate_single_rule_byte_bitwise_and_no_match() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::BitwiseAnd, + value: Value::Uint(0x80), // Check if high bit is set + message: "High bit set".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; // 0x7f has high bit clear + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_none()); // 0x7f & 0x80 = 0x00 (zero) +} + +#[test] +fn test_evaluate_single_rule_short_little_endian() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234), + message: "Little-endian short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x34, 0x12, 0x56, 0x78]; // 0x1234 in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_short_big_endian() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Big, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234), + message: "Big-endian short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x12, 0x34, 0x56, 0x78]; // 0x1234 in big-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_short_signed_positive() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: true, + }, + op: Operator::Equal, + value: Value::Int(32767), // 0x7fff + message: "Positive signed short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0xff, 0x7f, 0x00, 0x00]; // 0x7fff in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_short_signed_negative() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: true, + }, + op: Operator::Equal, + value: Value::Int(-1), // 0xffff as signed + message: "Negative signed short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0xff, 0xff, 0x00, 0x00]; // 0xffff in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_long_little_endian() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234_5678), + message: "Little-endian long".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x78, 0x56, 0x34, 0x12, 0x00]; // 0x12345678 in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_long_big_endian() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Big, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234_5678), + message: "Big-endian long".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x12, 0x34, 0x56, 0x78, 0x00]; // 0x12345678 in big-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_long_signed_positive() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: true, + }, + op: Operator::Equal, + value: Value::Int(2_147_483_647), // 0x7fffffff + message: "Positive signed long".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0xff, 0xff, 0xff, 0x7f, 0x00]; // 0x7fffffff in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_long_signed_negative() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: true, + }, + op: Operator::Equal, + value: Value::Int(-1), // 0xffffffff as signed + message: "Negative signed long".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0xff, 0xff, 0xff, 0xff, 0x00]; // 0xffffffff in little-endian + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_different_offsets() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(2), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x4c), + message: "ELF class byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_negative_offset() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(-1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x46), + message: "Last byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_from_end_offset() { + let rule = MagicRule { + offset: OffsetSpec::FromEnd(-2), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x4c), + message: "Second to last byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_offset_out_of_bounds() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(10), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Out of bounds".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Buffer overrun")); + } + _ => panic!("Expected EvaluationError"), + } +} + +#[test] +fn test_evaluate_single_rule_short_insufficient_bytes() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(3), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234), + message: "Insufficient bytes".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Buffer overrun")); + } + _ => panic!("Expected EvaluationError"), + } +} + +#[test] +fn test_evaluate_single_rule_long_insufficient_bytes() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(2), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234_5678), + message: "Insufficient bytes".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let result = evaluate_single_rule(&rule, buffer); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Buffer overrun")); + } + _ => panic!("Expected EvaluationError"), + } +} + +#[test] +fn test_evaluate_single_rule_empty_buffer() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Empty buffer".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[]; + let result = evaluate_single_rule(&rule, buffer); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Buffer overrun")); + } + _ => panic!("Expected EvaluationError"), + } +} + +#[test] +fn test_evaluate_single_rule_string_type_supported() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::String { max_length: None }, + op: Operator::Equal, + value: Value::String("test".to_string()), + message: "String type".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = b"test\x00 data"; + let result = evaluate_single_rule(&rule, buffer); + assert!(result.is_ok()); + let matches = result.unwrap(); + assert!(matches.is_some()); + + let rule_no_match = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::String { max_length: None }, + op: Operator::Equal, + value: Value::String("hello".to_string()), + message: "String type".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let result = evaluate_single_rule(&rule_no_match, buffer); + assert!(result.is_ok()); + let matches = result.unwrap(); + assert!(matches.is_none()); +} + +#[test] +fn test_evaluate_single_rule_cross_type_comparison() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Int(42), + message: "Cross-type comparison".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[42]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_bitwise_and_with_shorts() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: false, + }, + op: Operator::BitwiseAnd, + value: Value::Uint(0xff00), + message: "High byte check".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x34, 0x12]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_bitwise_and_with_longs() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Big, + signed: false, + }, + op: Operator::BitwiseAnd, + value: Value::Uint(0xffff_0000), + message: "High word check".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x12, 0x34, 0x56, 0x78]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_comprehensive_elf_check() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x464c_457f), + message: "ELF executable".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let elf_buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let result = evaluate_single_rule(&rule, elf_buffer).unwrap(); + assert!(result.is_some()); + + let non_elf_buffer = &[0x50, 0x4b, 0x03, 0x04, 0x14, 0x00]; + let result = evaluate_single_rule(&rule, non_elf_buffer).unwrap(); + assert!(result.is_none()); +} + +#[test] +fn test_evaluate_single_rule_native_endianness() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Short { + endian: Endianness::Native, + signed: false, + }, + op: Operator::NotEqual, + value: Value::Uint(0), + message: "Non-zero native short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x01, 0x02]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_all_operators() { + let buffer = &[0x42, 0x00, 0xff, 0x80]; + + let equal_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x42), + message: "Equal test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!(evaluate_single_rule(&equal_rule, buffer).unwrap().is_some()); + + let not_equal_rule = MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::NotEqual, + value: Value::Uint(0x42), + message: "NotEqual test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(¬_equal_rule, buffer) + .unwrap() + .is_some() + ); + + let bitwise_and_rule = MagicRule { + offset: OffsetSpec::Absolute(3), + typ: TypeKind::Byte { signed: true }, + op: Operator::BitwiseAnd, + value: Value::Uint(0x80), + message: "BitwiseAnd test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&bitwise_and_rule, buffer) + .unwrap() + .is_some() + ); +} + +#[test] +fn test_evaluate_single_rule_comparison_operators() { + let buffer = &[0x42, 0x00, 0xff, 0x80]; + + let less_than_rule = MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: false }, + op: Operator::LessThan, + value: Value::Uint(0x42), + message: "LessThan test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&less_than_rule, buffer) + .unwrap() + .is_some() + ); + + let greater_than_rule = MagicRule { + offset: OffsetSpec::Absolute(2), + typ: TypeKind::Byte { signed: false }, + op: Operator::GreaterThan, + value: Value::Uint(0x42), + message: "GreaterThan test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&greater_than_rule, buffer) + .unwrap() + .is_some() + ); + + let less_equal_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: false }, + op: Operator::LessEqual, + value: Value::Uint(0x42), + message: "LessEqual test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&less_equal_rule, buffer) + .unwrap() + .is_some() + ); + + let greater_equal_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: false }, + op: Operator::GreaterEqual, + value: Value::Uint(0x42), + message: "GreaterEqual test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&greater_equal_rule, buffer) + .unwrap() + .is_some() + ); +} + +#[test] +fn test_evaluate_comparison_with_signed_byte() { + let buffer = &[0x80]; + + let signed_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::LessThan, + value: Value::Uint(0), + message: "signed less".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&signed_rule, buffer) + .unwrap() + .is_some() + ); + + let unsigned_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: false }, + op: Operator::LessThan, + value: Value::Uint(0), + message: "unsigned less".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + assert!( + evaluate_single_rule(&unsigned_rule, buffer) + .unwrap() + .is_none() + ); +} + +#[test] +fn test_evaluate_comparison_operators_negative_cases() { + let buffer = &[0x42]; + + let cases: Vec<(Operator, u64, bool)> = vec![ + (Operator::LessThan, 66, false), + (Operator::LessThan, 67, true), + (Operator::GreaterThan, 66, false), + (Operator::GreaterThan, 65, true), + (Operator::LessEqual, 65, false), + (Operator::LessEqual, 66, true), + (Operator::GreaterEqual, 67, false), + (Operator::GreaterEqual, 66, true), + ]; + + for (op, value, expected) in cases { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: false }, + op: op.clone(), + value: Value::Uint(value), + message: "test".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert_eq!( + result.is_some(), + expected, + "{op:?} with value {value}: expected {expected}" + ); + } +} + +#[test] +fn test_evaluate_single_rule_edge_case_values() { + let max_uint_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0xffff_ffff), + message: "Max uint32".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let max_buffer = &[0xff, 0xff, 0xff, 0xff]; + let result = evaluate_single_rule(&max_uint_rule, max_buffer).unwrap(); + assert!(result.is_some()); + + let min_int_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: true, + }, + op: Operator::Equal, + value: Value::Int(-2_147_483_648), + message: "Min int32".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let min_buffer = &[0x00, 0x00, 0x00, 0x80]; + let result = evaluate_single_rule(&min_int_rule, min_buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_single_rule_various_buffer_sizes() { + let single_byte_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: false }, + op: Operator::Equal, + value: Value::Uint(0xaa), + message: "Single byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let single_buffer = &[0xaa]; + let result = evaluate_single_rule(&single_byte_rule, single_buffer).unwrap(); + assert!(result.is_some()); + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let large_buffer: Vec = (0..1024).map(|i| (i % 256) as u8).collect(); + let large_rule = MagicRule { + offset: OffsetSpec::Absolute(1000), + typ: TypeKind::Byte { signed: false }, + op: Operator::Equal, + value: Value::Uint((1000 % 256) as u64), + message: "Large buffer".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let result = evaluate_single_rule(&large_rule, &large_buffer).unwrap(); + assert!(result.is_some()); +} + +#[test] +fn test_evaluate_rules_empty_list() { + let rules = vec![]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert!(matches.is_empty()); +} + +#[test] +fn test_evaluate_rules_single_matching_rule() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].message, "ELF magic"); + assert_eq!(matches[0].offset, 0); + assert_eq!(matches[0].level, 0); + assert_eq!(matches[0].value, Value::Int(0x7f)); +} + +#[test] +fn test_evaluate_rules_single_non_matching_rule() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x50), + message: "ZIP magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert!(matches.is_empty()); +} + +#[test] +fn test_evaluate_rules_multiple_rules_stop_at_first() { + let rule1 = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "First match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule2 = MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Second match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule_list = vec![rule1, rule2]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + stop_at_first_match: true, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rule_list, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].message, "First match"); +} + +#[test] +fn test_evaluate_rules_multiple_rules_find_all() { + let rule1 = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "First match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule2 = MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Second match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule_set = vec![rule1, rule2]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + stop_at_first_match: false, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rule_set, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "First match"); + assert_eq!(matches[1].message, "Second match"); +} + +#[test] +fn test_evaluate_rules_hierarchical_parent_child() { + let child_rule = MagicRule { + offset: OffsetSpec::Absolute(4), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x02), + message: "64-bit".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }; + + let parent_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF".to_string(), + children: vec![child_rule], + level: 0, + strength_modifier: None, + }; + + let rules = vec![parent_rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "ELF"); + assert_eq!(matches[0].level, 0); + assert_eq!(matches[1].message, "64-bit"); + assert_eq!(matches[1].level, 1); +} + +#[test] +fn test_evaluate_rules_hierarchical_parent_no_match() { + let child_rule = MagicRule { + offset: OffsetSpec::Absolute(4), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x02), + message: "64-bit".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }; + + let parent_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x50), + message: "ZIP".to_string(), + children: vec![child_rule], + level: 0, + strength_modifier: None, + }; + + let rules = vec![parent_rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert!(matches.is_empty()); +} + +#[test] +fn test_evaluate_rules_hierarchical_parent_match_child_no_match() { + let child_rule = MagicRule { + offset: OffsetSpec::Absolute(4), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x01), + message: "32-bit".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }; + + let parent_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF".to_string(), + children: vec![child_rule], + level: 0, + strength_modifier: None, + }; + + let rules = vec![parent_rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].message, "ELF"); + assert_eq!(matches[0].level, 0); +} + +#[test] +fn test_evaluate_rules_deep_hierarchy() { + let grandchild_rule = MagicRule { + offset: OffsetSpec::Absolute(5), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x01), + message: "little-endian".to_string(), + children: vec![], + level: 2, + strength_modifier: None, + }; + + let child_rule = MagicRule { + offset: OffsetSpec::Absolute(4), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x02), + message: "64-bit".to_string(), + children: vec![grandchild_rule], + level: 1, + strength_modifier: None, + }; + + let parent_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF".to_string(), + children: vec![child_rule], + level: 0, + strength_modifier: None, + }; + + let rules = vec![parent_rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 3); + assert_eq!(matches[0].message, "ELF"); + assert_eq!(matches[0].level, 0); + assert_eq!(matches[1].message, "64-bit"); + assert_eq!(matches[1].level, 1); + assert_eq!(matches[2].message, "little-endian"); + assert_eq!(matches[2].level, 2); +} + +#[test] +fn test_evaluate_rules_multiple_children() { + let child1 = MagicRule { + offset: OffsetSpec::Absolute(4), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x02), + message: "64-bit".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }; + + let child2 = MagicRule { + offset: OffsetSpec::Absolute(5), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x01), + message: "little-endian".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }; + + let parent_rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF".to_string(), + children: vec![child1, child2], + level: 0, + strength_modifier: None, + }; + + let rules = vec![parent_rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; + let config = EvaluationConfig { + stop_at_first_match: false, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 3); + assert_eq!(matches[0].message, "ELF"); + assert_eq!(matches[1].message, "64-bit"); + assert_eq!(matches[2].message, "little-endian"); +} + +#[test] +fn test_evaluate_rules_recursion_depth_limit() { + let mut current_rule = MagicRule { + offset: OffsetSpec::Absolute(10), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Deep level".to_string(), + children: vec![], + level: 10, + strength_modifier: None, + }; + + for i in (0u32..10u32).rev() { + current_rule = MagicRule { + offset: OffsetSpec::Absolute(i64::from(i)), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(u64::from(i)), + message: format!("Level {i}"), + children: vec![current_rule], + level: i, + strength_modifier: None, + }; + } + + let rules = vec![current_rule]; + let buffer = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + let config = EvaluationConfig { + max_recursion_depth: 5, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let result = evaluate_rules(&rules, buffer, &mut context); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(msg) => { + let error_string = format!("{msg}"); + assert!(error_string.contains("Recursion limit exceeded")); + } + _ => panic!("Expected EvaluationError for recursion limit"), + } +} + +#[test] +fn test_evaluate_rules_with_config_convenience() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + + let matches = evaluate_rules_with_config(&rules, buffer, &config).unwrap(); + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].message, "ELF magic"); +} + +#[test] +fn test_evaluate_rules_timeout() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + timeout_ms: Some(0), + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let result = evaluate_rules(&rules, buffer, &mut context); + if let Err(LibmagicError::Timeout { timeout_ms }) = result { + assert_eq!(timeout_ms, 0); + } +} + +#[test] +fn test_evaluate_rules_empty_buffer() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Should not match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let result = evaluate_rules(&rules, buffer, &mut context); + assert!(result.is_ok()); + + let matches = result.unwrap(); + assert_eq!(matches.len(), 0); +} + +#[test] +fn test_evaluate_rules_mixed_matching_non_matching() { + let rule1 = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Matches".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule2 = MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x99), + message: "Doesn't match".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule3 = MagicRule { + offset: OffsetSpec::Absolute(2), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x4c), + message: "Also matches".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rule_collection = vec![rule1, rule2, rule3]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + stop_at_first_match: false, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rule_collection, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "Matches"); + assert_eq!(matches[1].message, "Also matches"); +} + +#[test] +fn test_evaluate_rules_context_state_preservation() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "ELF magic".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let rules = vec![rule]; + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + context.set_current_offset(100); + let initial_offset = context.current_offset(); + let initial_depth = context.recursion_depth(); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1); + + assert_eq!(context.current_offset(), initial_offset); + assert_eq!(context.recursion_depth(), initial_depth); +} + +#[test] +fn test_error_recovery_skip_problematic_rules() { + let rules = vec![ + MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Valid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Invalid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Another valid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + ]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + max_recursion_depth: 20, + max_string_length: 8192, + stop_at_first_match: false, + enable_mime_types: false, + timeout_ms: None, + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "Valid rule"); + assert_eq!(matches[1].message, "Another valid rule"); +} + +#[test] +fn test_error_recovery_child_rule_failures() { + let rules = vec![MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Parent rule".to_string(), + children: vec![ + MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Valid child".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Invalid child".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }, + ], + level: 0, + strength_modifier: None, + }]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "Parent rule"); + assert_eq!(matches[1].message, "Valid child"); +} + +#[test] +fn test_error_recovery_mixed_rule_types() { + let rules = vec![ + MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Valid byte".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(3), + typ: TypeKind::Short { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234), + message: "Invalid short".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::String { + max_length: Some(3), + }, + op: Operator::Equal, + value: Value::String("ELF".to_string()), + message: "Valid string".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + ]; + + let buffer = &[0x7f, b'E', b'L', b'F']; + let config = EvaluationConfig { + max_recursion_depth: 20, + max_string_length: 8192, + stop_at_first_match: false, + enable_mime_types: false, + timeout_ms: None, + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + + assert_eq!(matches.len(), 2); + assert_eq!(matches[0].message, "Valid byte"); + assert_eq!(matches[1].message, "Valid string"); +} + +#[test] +fn test_error_recovery_all_rules_fail() { + let rules = vec![ + MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Out of bounds".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(2), + typ: TypeKind::Long { + endian: Endianness::Little, + signed: false, + }, + op: Operator::Equal, + value: Value::Uint(0x1234_5678), + message: "Insufficient bytes".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + ]; + + let buffer = &[0x7f, 0x45]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 0); +} + +#[test] +fn test_error_recovery_timeout_propagation() { + let rules = vec![MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Test rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + max_recursion_depth: 10, + max_string_length: 1024, + stop_at_first_match: false, + enable_mime_types: false, + timeout_ms: Some(0), + }; + let mut context = EvaluationContext::new(config); + + let result = evaluate_rules(&rules, buffer, &mut context); + + match result { + Ok(_) | Err(LibmagicError::Timeout { .. }) => {} + Err(e) => { + panic!("Unexpected error type: {e:?}"); + } + } +} + +#[test] +fn test_error_recovery_recursion_limit_propagation() { + let rules = vec![MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Parent".to_string(), + children: vec![MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Child".to_string(), + children: vec![], + level: 1, + strength_modifier: None, + }], + level: 0, + strength_modifier: None, + }]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig { + max_recursion_depth: 0, + max_string_length: 1024, + stop_at_first_match: false, + enable_mime_types: false, + timeout_ms: None, + }; + let mut context = EvaluationContext::new(config); + + let result = evaluate_rules(&rules, buffer, &mut context); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(crate::error::EvaluationError::RecursionLimitExceeded { + .. + }) => {} + _ => panic!("Expected recursion limit error"), + } +} + +#[test] +fn test_error_recovery_preserves_context_state() { + let rules = vec![ + MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Valid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Invalid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + ]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + context.set_current_offset(42); + let initial_offset = context.current_offset(); + let initial_depth = context.recursion_depth(); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 1); + + assert_eq!(context.current_offset(), initial_offset); + assert_eq!(context.recursion_depth(), initial_depth); +} + +#[test] +fn test_any_value_parse_and_evaluate_paren_message() { + use crate::parser::grammar::parse_magic_rule; + + let input = ">0 byte x (0)"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::AnyValue); + assert_eq!(rule.message, "(0)"); + + let buffer = &[0x00, 0x01, 0x02, 0x03]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_some(), + "AnyValue rule should match unconditionally" + ); +} + +#[test] +fn test_any_value_parse_and_evaluate_backslash_message() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 long x \\b, data"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::AnyValue); + assert_eq!(rule.message, "\\b, data"); + + let buffer = &[0xFF, 0xFE, 0xFD, 0xFC]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_some(), + "AnyValue rule should match unconditionally" + ); +} + +#[test] +fn test_any_value_parse_and_evaluate_no_message() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 byte x"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::AnyValue); + + let buffer = &[0x42]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_some(), + "AnyValue rule should match unconditionally" + ); +} + +#[test] +fn test_bitwise_xor_parse_and_evaluate_match() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 byte ^0x01 XOR match"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::BitwiseXor); + assert_eq!(rule.message, "XOR match"); + + let buffer = &[0x0F]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_some(), + "BitwiseXor should match when XOR is non-zero" + ); +} + +#[test] +fn test_bitwise_xor_parse_and_evaluate_no_match() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 byte ^0x42 XOR no match"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::BitwiseXor); + + let buffer = &[0x42]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_none(), + "BitwiseXor should not match when XOR is zero" + ); +} + +#[test] +fn test_bitwise_not_parse_and_evaluate_match() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 ubyte ~0xFF NOT match"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::BitwiseNot); + assert_eq!(rule.message, "NOT match"); + + let buffer = &[0x00]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_some(), + "BitwiseNot should match when NOT(value) equals operand at byte width" + ); +} + +#[test] +fn test_bitwise_not_parse_and_evaluate_no_match() { + use crate::parser::grammar::parse_magic_rule; + + let input = "0 ubyte ~0x01 NOT no match"; + let (_, rule) = parse_magic_rule(input).unwrap(); + assert_eq!(rule.op, Operator::BitwiseNot); + + let buffer = &[0x42]; + let result = evaluate_single_rule(&rule, buffer).unwrap(); + assert!( + result.is_none(), + "BitwiseNot should not match when NOT(value) != operand" + ); +} + +#[test] +fn test_evaluate_rules_skips_out_of_bounds_rule() { + let rule = MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Out of bounds rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }; + + let buffer = &[0x7f, 0x45]; + + let single_result = evaluate_single_rule(&rule, buffer); + assert!(single_result.is_err()); + + let rules = vec![rule]; + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 0); +} + +#[test] +fn test_mixed_valid_and_invalid_rules_yield_valid_matches() { + let rules = vec![ + MagicRule { + offset: OffsetSpec::Absolute(0), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x7f), + message: "Valid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(100), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x00), + message: "Invalid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + MagicRule { + offset: OffsetSpec::Absolute(1), + typ: TypeKind::Byte { signed: true }, + op: Operator::Equal, + value: Value::Uint(0x45), + message: "Another valid rule".to_string(), + children: vec![], + level: 0, + strength_modifier: None, + }, + ]; + + let buffer = &[0x7f, 0x45, 0x4c, 0x46]; + + let config = EvaluationConfig { + stop_at_first_match: false, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + let matches = evaluate_rules(&rules, buffer, &mut context).unwrap(); + assert_eq!(matches.len(), 2); +} From 6db5f023e4d8a8a0fb64677452ef555a7acf964a Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Fri, 6 Mar 2026 13:59:04 -0500 Subject: [PATCH 09/12] fix(evaluator): remove glob re-exports and preserve original error depth Remove `pub use offset::*`, `pub use operators::*`, etc. that were introduced in the refactor. On main these were `pub mod` only, so the glob re-exports expanded the public API surface unintentionally. Also fix RecursionLimitExceeded propagation to bind and return the original error instead of reconstructing it with a potentially under-reported depth after decrementing. Co-Authored-By: Claude Opus 4.6 Signed-off-by: UncleSp1d3r --- src/evaluator/mod.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/evaluator/mod.rs b/src/evaluator/mod.rs index 3fb42aab..40b0b3a2 100644 --- a/src/evaluator/mod.rs +++ b/src/evaluator/mod.rs @@ -17,10 +17,6 @@ pub mod strength; pub mod types; pub use engine::{evaluate_rules, evaluate_rules_with_config, evaluate_single_rule}; -pub use offset::*; -pub use operators::*; -pub use strength::*; -pub use types::*; /// Context for maintaining evaluation state during rule processing /// From 6dbba2e2cca8fee7eae0c6cf1340b8a640309e22 Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Fri, 6 Mar 2026 13:59:08 -0500 Subject: [PATCH 10/12] docs: update AGENTS.md for engine directory structure Reflect that engine is now a directory submodule (engine/mod.rs + engine/tests.rs) instead of a single engine.rs file. Co-Authored-By: Claude Opus 4.6 Signed-off-by: UncleSp1d3r --- AGENTS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f93aa6d1..d6b3b6d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,7 +89,9 @@ parser/ // Evaluator module structure evaluator/ ├── mod.rs // Public interface, EvaluationContext, RuleMatch, re-exports -├── engine.rs // Core evaluation engine (evaluate_single_rule, evaluate_rules, evaluate_rules_with_config) +├── engine/ // Core evaluation engine submodule +│ ├── mod.rs // evaluate_single_rule, evaluate_rules, evaluate_rules_with_config +│ └── tests.rs // Engine unit tests ├── types.rs // Type interpretation with endianness ├── strength.rs // Strength modifier application ├── offset/ // Offset resolution submodule From 8ebaac62513c39d4c9e2135737f978187db94c42 Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Fri, 6 Mar 2026 15:44:49 -0500 Subject: [PATCH 11/12] refactor(evaluator): extract mod.rs tests to separate file Move the ~418-line #[cfg(test)] block from mod.rs into tests.rs, bringing mod.rs down to 250 lines (well within the 500-600 line guideline). Also replace brittle string-based error assertions with pattern matching on concrete error variants: - RecursionLimitExceeded: match variant and assert depth - InternalError: match variant directly Co-Authored-By: Claude Opus 4.6 Signed-off-by: UncleSp1d3r --- src/evaluator/mod.rs | 419 +---------------------------------------- src/evaluator/tests.rs | 417 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 419 insertions(+), 417 deletions(-) create mode 100644 src/evaluator/tests.rs diff --git a/src/evaluator/mod.rs b/src/evaluator/mod.rs index 40b0b3a2..77b591d8 100644 --- a/src/evaluator/mod.rs +++ b/src/evaluator/mod.rs @@ -245,421 +245,6 @@ impl RuleMatch { (0.3 + (f64::from(level) * 0.2)).min(1.0) } } -#[cfg(test)] -mod tests { - use super::*; - use crate::parser::ast::Value; - - #[test] - fn test_evaluation_context_new() { - let config = EvaluationConfig::default(); - let context = EvaluationContext::new(config.clone()); - - assert_eq!(context.current_offset(), 0); - assert_eq!(context.recursion_depth(), 0); - assert_eq!( - context.config().max_recursion_depth, - config.max_recursion_depth - ); - assert_eq!(context.config().max_string_length, config.max_string_length); - assert_eq!( - context.config().stop_at_first_match, - config.stop_at_first_match - ); - } - - #[test] - fn test_evaluation_context_offset_management() { - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Test initial offset - assert_eq!(context.current_offset(), 0); - - // Test setting offset - context.set_current_offset(42); - assert_eq!(context.current_offset(), 42); - - // Test setting different offset - context.set_current_offset(1024); - assert_eq!(context.current_offset(), 1024); - - // Test setting offset to 0 - context.set_current_offset(0); - assert_eq!(context.current_offset(), 0); - } - - #[test] - fn test_evaluation_context_recursion_depth_management() { - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Test initial recursion depth - assert_eq!(context.recursion_depth(), 0); - - // Test incrementing recursion depth - context.increment_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 1); - - context.increment_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 2); - - // Test decrementing recursion depth - context.decrement_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 1); - - context.decrement_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 0); - } - - #[test] - fn test_evaluation_context_recursion_depth_limit() { - let config = EvaluationConfig { - max_recursion_depth: 2, - ..Default::default() - }; - let mut context = EvaluationContext::new(config); - - // Should be able to increment up to the limit - assert!(context.increment_recursion_depth().is_ok()); - assert_eq!(context.recursion_depth(), 1); - - assert!(context.increment_recursion_depth().is_ok()); - assert_eq!(context.recursion_depth(), 2); - - // Should fail when exceeding the limit - let result = context.increment_recursion_depth(); - assert!(result.is_err()); - assert_eq!(context.recursion_depth(), 2); // Should not have changed - - match result.unwrap_err() { - LibmagicError::EvaluationError(msg) => { - let error_string = format!("{msg}"); - assert!(error_string.contains("Recursion limit exceeded")); - } - _ => panic!("Expected EvaluationError"), - } - } - - #[test] - fn test_evaluation_context_recursion_depth_underflow() { - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - // Should return an error when trying to decrement below 0 - let result = context.decrement_recursion_depth(); - assert!(result.is_err()); - - let err = result.unwrap_err(); - let err_msg = err.to_string(); - assert!( - err_msg.contains("decrement recursion depth below 0"), - "Expected error about decrementing below 0, got: {err_msg}" - ); - } - - #[test] - fn test_evaluation_context_config_access() { - let config = EvaluationConfig { - max_recursion_depth: 10, - max_string_length: 4096, - stop_at_first_match: false, - enable_mime_types: true, - timeout_ms: Some(2000), - }; - - let context = EvaluationContext::new(config); - - // Test config access - assert_eq!(context.config().max_recursion_depth, 10); - assert_eq!(context.config().max_string_length, 4096); - assert!(!context.config().stop_at_first_match); - - // Test convenience methods - assert!(!context.should_stop_at_first_match()); - assert_eq!(context.max_string_length(), 4096); - } - - #[test] - fn test_evaluation_context_reset() { - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config.clone()); - - // Modify the context state - context.set_current_offset(100); - context.increment_recursion_depth().unwrap(); - context.increment_recursion_depth().unwrap(); - - assert_eq!(context.current_offset(), 100); - assert_eq!(context.recursion_depth(), 2); - - // Reset should restore initial state but keep config - context.reset(); - - assert_eq!(context.current_offset(), 0); - assert_eq!(context.recursion_depth(), 0); - assert_eq!( - context.config().max_recursion_depth, - config.max_recursion_depth - ); - } - - #[test] - fn test_evaluation_context_clone() { - let config = EvaluationConfig { - max_recursion_depth: 5, - max_string_length: 2048, - ..Default::default() - }; - - let mut context = EvaluationContext::new(config); - context.set_current_offset(50); - context.increment_recursion_depth().unwrap(); - - // Clone the context - let cloned_context = context.clone(); - - // Both should have the same state - assert_eq!(context.current_offset(), cloned_context.current_offset()); - assert_eq!(context.recursion_depth(), cloned_context.recursion_depth()); - assert_eq!( - context.config().max_recursion_depth, - cloned_context.config().max_recursion_depth - ); - assert_eq!( - context.config().max_string_length, - cloned_context.config().max_string_length - ); - - // Modifying one should not affect the other - context.set_current_offset(75); - assert_eq!(context.current_offset(), 75); - assert_eq!(cloned_context.current_offset(), 50); - } - - #[test] - fn test_evaluation_context_with_custom_config() { - let config = EvaluationConfig { - max_recursion_depth: 15, - max_string_length: 16384, - stop_at_first_match: false, - enable_mime_types: true, - timeout_ms: Some(5000), - }; - - let context = EvaluationContext::new(config); - - assert_eq!(context.config().max_recursion_depth, 15); - assert_eq!(context.max_string_length(), 16384); - assert!(!context.should_stop_at_first_match()); - - // Test that we can increment up to the custom limit - let mut mutable_context = context; - for i in 1..=15 { - assert!(mutable_context.increment_recursion_depth().is_ok()); - assert_eq!(mutable_context.recursion_depth(), i); - } - - // Should fail on the 16th increment - let result = mutable_context.increment_recursion_depth(); - assert!(result.is_err()); - } - - #[test] - fn test_evaluation_context_mime_types_access() { - let config_with_mime = EvaluationConfig { - enable_mime_types: true, - ..Default::default() - }; - let context_with_mime = EvaluationContext::new(config_with_mime); - assert!(context_with_mime.enable_mime_types()); - - let config_without_mime = EvaluationConfig { - enable_mime_types: false, - ..Default::default() - }; - let context_without_mime = EvaluationContext::new(config_without_mime); - assert!(!context_without_mime.enable_mime_types()); - } - - #[test] - fn test_evaluation_context_timeout_access() { - let config_with_timeout = EvaluationConfig { - timeout_ms: Some(5000), - ..Default::default() - }; - let context_with_timeout = EvaluationContext::new(config_with_timeout); - assert_eq!(context_with_timeout.timeout_ms(), Some(5000)); - - let config_without_timeout = EvaluationConfig { - timeout_ms: None, - ..Default::default() - }; - let context_without_timeout = EvaluationContext::new(config_without_timeout); - assert_eq!(context_without_timeout.timeout_ms(), None); - } - - #[test] - fn test_evaluation_context_comprehensive_config() { - let config = EvaluationConfig { - max_recursion_depth: 30, - max_string_length: 16384, - stop_at_first_match: false, - enable_mime_types: true, - timeout_ms: Some(10000), - }; - let context = EvaluationContext::new(config); - - assert_eq!(context.config().max_recursion_depth, 30); - assert_eq!(context.config().max_string_length, 16384); - assert!(!context.should_stop_at_first_match()); - assert!(context.enable_mime_types()); - assert_eq!(context.timeout_ms(), Some(10000)); - assert_eq!(context.max_string_length(), 16384); - } - - #[test] - fn test_evaluation_context_performance_config() { - let config = EvaluationConfig { - max_recursion_depth: 5, - max_string_length: 512, - stop_at_first_match: true, - enable_mime_types: false, - timeout_ms: Some(1000), - }; - let context = EvaluationContext::new(config); - - assert_eq!(context.config().max_recursion_depth, 5); - assert_eq!(context.max_string_length(), 512); - assert!(context.should_stop_at_first_match()); - assert!(!context.enable_mime_types()); - assert_eq!(context.timeout_ms(), Some(1000)); - } - - #[test] - fn test_evaluation_context_state_management_sequence() { - let config = EvaluationConfig::default(); - let mut context = EvaluationContext::new(config); - - // Simulate a sequence of evaluation operations - assert_eq!(context.current_offset(), 0); - assert_eq!(context.recursion_depth(), 0); - - // Start evaluation at offset 10 - context.set_current_offset(10); - assert_eq!(context.current_offset(), 10); - - // Enter nested rule evaluation - context.increment_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 1); - - // Move to different offset during nested evaluation - context.set_current_offset(25); - assert_eq!(context.current_offset(), 25); - - // Enter deeper nesting - context.increment_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 2); - - // Exit nested evaluation - context.decrement_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 1); - - // Continue evaluation at different offset - context.set_current_offset(50); - assert_eq!(context.current_offset(), 50); - - // Exit all nesting - context.decrement_recursion_depth().unwrap(); - assert_eq!(context.recursion_depth(), 0); - - // Final state check - assert_eq!(context.current_offset(), 50); - assert_eq!(context.recursion_depth(), 0); - } - - #[test] - fn test_rule_match_creation() { - let match_result = RuleMatch { - message: "ELF executable".to_string(), - offset: 0, - level: 0, - value: Value::Uint(0x7f), - confidence: RuleMatch::calculate_confidence(0), - }; - - assert_eq!(match_result.message, "ELF executable"); - assert_eq!(match_result.offset, 0); - assert_eq!(match_result.level, 0); - assert_eq!(match_result.value, Value::Uint(0x7f)); - assert!((match_result.confidence - 0.3).abs() < 0.001); - } - - #[test] - fn test_rule_match_clone() { - let original = RuleMatch { - message: "Test message".to_string(), - offset: 42, - level: 1, - value: Value::String("test".to_string()), - confidence: RuleMatch::calculate_confidence(1), - }; - - let cloned = original.clone(); - assert_eq!(original, cloned); - } - - #[test] - fn test_rule_match_debug() { - let match_result = RuleMatch { - message: "Debug test".to_string(), - offset: 10, - level: 2, - value: Value::Bytes(vec![0x01, 0x02]), - confidence: RuleMatch::calculate_confidence(2), - }; - - let debug_str = format!("{match_result:?}"); - assert!(debug_str.contains("RuleMatch")); - assert!(debug_str.contains("Debug test")); - assert!(debug_str.contains("10")); - assert!(debug_str.contains('2')); - } - - #[test] - fn test_confidence_calculation_depth_0() { - let confidence = RuleMatch::calculate_confidence(0); - assert!((confidence - 0.3).abs() < 0.001); - } - - #[test] - fn test_confidence_calculation_depth_1() { - let confidence = RuleMatch::calculate_confidence(1); - assert!((confidence - 0.5).abs() < 0.001); - } - - #[test] - fn test_confidence_calculation_depth_2() { - let confidence = RuleMatch::calculate_confidence(2); - assert!((confidence - 0.7).abs() < 0.001); - } - - #[test] - fn test_confidence_calculation_depth_3() { - let confidence = RuleMatch::calculate_confidence(3); - assert!((confidence - 0.9).abs() < 0.001); - } - - #[test] - fn test_confidence_calculation_capped_at_1() { - // Level 4+ should cap at 1.0 - let confidence_4 = RuleMatch::calculate_confidence(4); - assert!((confidence_4 - 1.0).abs() < 0.001); - - let confidence_10 = RuleMatch::calculate_confidence(10); - assert!((confidence_10 - 1.0).abs() < 0.001); - - let confidence_100 = RuleMatch::calculate_confidence(100); - assert!((confidence_100 - 1.0).abs() < 0.001); - } -} +#[cfg(test)] +mod tests; diff --git a/src/evaluator/tests.rs b/src/evaluator/tests.rs new file mode 100644 index 00000000..97e918fc --- /dev/null +++ b/src/evaluator/tests.rs @@ -0,0 +1,417 @@ +// Copyright (c) 2025-2026 the libmagic-rs contributors +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::parser::ast::Value; + +#[test] +fn test_evaluation_context_new() { + let config = EvaluationConfig::default(); + let context = EvaluationContext::new(config.clone()); + + assert_eq!(context.current_offset(), 0); + assert_eq!(context.recursion_depth(), 0); + assert_eq!( + context.config().max_recursion_depth, + config.max_recursion_depth + ); + assert_eq!(context.config().max_string_length, config.max_string_length); + assert_eq!( + context.config().stop_at_first_match, + config.stop_at_first_match + ); +} + +#[test] +fn test_evaluation_context_offset_management() { + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + // Test initial offset + assert_eq!(context.current_offset(), 0); + + // Test setting offset + context.set_current_offset(42); + assert_eq!(context.current_offset(), 42); + + // Test setting different offset + context.set_current_offset(1024); + assert_eq!(context.current_offset(), 1024); + + // Test setting offset to 0 + context.set_current_offset(0); + assert_eq!(context.current_offset(), 0); +} + +#[test] +fn test_evaluation_context_recursion_depth_management() { + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + // Test initial recursion depth + assert_eq!(context.recursion_depth(), 0); + + // Test incrementing recursion depth + context.increment_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 1); + + context.increment_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 2); + + // Test decrementing recursion depth + context.decrement_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 1); + + context.decrement_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 0); +} + +#[test] +fn test_evaluation_context_recursion_depth_limit() { + let config = EvaluationConfig { + max_recursion_depth: 2, + ..Default::default() + }; + let mut context = EvaluationContext::new(config); + + // Should be able to increment up to the limit + assert!(context.increment_recursion_depth().is_ok()); + assert_eq!(context.recursion_depth(), 1); + + assert!(context.increment_recursion_depth().is_ok()); + assert_eq!(context.recursion_depth(), 2); + + // Should fail when exceeding the limit + let result = context.increment_recursion_depth(); + assert!(result.is_err()); + assert_eq!(context.recursion_depth(), 2); // Should not have changed + + match result.unwrap_err() { + LibmagicError::EvaluationError(crate::error::EvaluationError::RecursionLimitExceeded { + depth, + }) => { + assert_eq!(depth, 2); + } + other => panic!("Expected RecursionLimitExceeded, got: {other:?}"), + } +} + +#[test] +fn test_evaluation_context_recursion_depth_underflow() { + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + // Should return an error when trying to decrement below 0 + let result = context.decrement_recursion_depth(); + assert!(result.is_err()); + + match result.unwrap_err() { + LibmagicError::EvaluationError(crate::error::EvaluationError::InternalError { .. }) => {} + other => panic!("Expected InternalError, got: {other:?}"), + } +} + +#[test] +fn test_evaluation_context_config_access() { + let config = EvaluationConfig { + max_recursion_depth: 10, + max_string_length: 4096, + stop_at_first_match: false, + enable_mime_types: true, + timeout_ms: Some(2000), + }; + + let context = EvaluationContext::new(config); + + // Test config access + assert_eq!(context.config().max_recursion_depth, 10); + assert_eq!(context.config().max_string_length, 4096); + assert!(!context.config().stop_at_first_match); + + // Test convenience methods + assert!(!context.should_stop_at_first_match()); + assert_eq!(context.max_string_length(), 4096); +} + +#[test] +fn test_evaluation_context_reset() { + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config.clone()); + + // Modify the context state + context.set_current_offset(100); + context.increment_recursion_depth().unwrap(); + context.increment_recursion_depth().unwrap(); + + assert_eq!(context.current_offset(), 100); + assert_eq!(context.recursion_depth(), 2); + + // Reset should restore initial state but keep config + context.reset(); + + assert_eq!(context.current_offset(), 0); + assert_eq!(context.recursion_depth(), 0); + assert_eq!( + context.config().max_recursion_depth, + config.max_recursion_depth + ); +} + +#[test] +fn test_evaluation_context_clone() { + let config = EvaluationConfig { + max_recursion_depth: 5, + max_string_length: 2048, + ..Default::default() + }; + + let mut context = EvaluationContext::new(config); + context.set_current_offset(50); + context.increment_recursion_depth().unwrap(); + + // Clone the context + let cloned_context = context.clone(); + + // Both should have the same state + assert_eq!(context.current_offset(), cloned_context.current_offset()); + assert_eq!(context.recursion_depth(), cloned_context.recursion_depth()); + assert_eq!( + context.config().max_recursion_depth, + cloned_context.config().max_recursion_depth + ); + assert_eq!( + context.config().max_string_length, + cloned_context.config().max_string_length + ); + + // Modifying one should not affect the other + context.set_current_offset(75); + assert_eq!(context.current_offset(), 75); + assert_eq!(cloned_context.current_offset(), 50); +} + +#[test] +fn test_evaluation_context_with_custom_config() { + let config = EvaluationConfig { + max_recursion_depth: 15, + max_string_length: 16384, + stop_at_first_match: false, + enable_mime_types: true, + timeout_ms: Some(5000), + }; + + let context = EvaluationContext::new(config); + + assert_eq!(context.config().max_recursion_depth, 15); + assert_eq!(context.max_string_length(), 16384); + assert!(!context.should_stop_at_first_match()); + + // Test that we can increment up to the custom limit + let mut mutable_context = context; + for i in 1..=15 { + assert!(mutable_context.increment_recursion_depth().is_ok()); + assert_eq!(mutable_context.recursion_depth(), i); + } + + // Should fail on the 16th increment + let result = mutable_context.increment_recursion_depth(); + assert!(result.is_err()); +} + +#[test] +fn test_evaluation_context_mime_types_access() { + let config_with_mime = EvaluationConfig { + enable_mime_types: true, + ..Default::default() + }; + let context_with_mime = EvaluationContext::new(config_with_mime); + assert!(context_with_mime.enable_mime_types()); + + let config_without_mime = EvaluationConfig { + enable_mime_types: false, + ..Default::default() + }; + let context_without_mime = EvaluationContext::new(config_without_mime); + assert!(!context_without_mime.enable_mime_types()); +} + +#[test] +fn test_evaluation_context_timeout_access() { + let config_with_timeout = EvaluationConfig { + timeout_ms: Some(5000), + ..Default::default() + }; + let context_with_timeout = EvaluationContext::new(config_with_timeout); + assert_eq!(context_with_timeout.timeout_ms(), Some(5000)); + + let config_without_timeout = EvaluationConfig { + timeout_ms: None, + ..Default::default() + }; + let context_without_timeout = EvaluationContext::new(config_without_timeout); + assert_eq!(context_without_timeout.timeout_ms(), None); +} + +#[test] +fn test_evaluation_context_comprehensive_config() { + let config = EvaluationConfig { + max_recursion_depth: 30, + max_string_length: 16384, + stop_at_first_match: false, + enable_mime_types: true, + timeout_ms: Some(10000), + }; + let context = EvaluationContext::new(config); + + assert_eq!(context.config().max_recursion_depth, 30); + assert_eq!(context.config().max_string_length, 16384); + assert!(!context.should_stop_at_first_match()); + assert!(context.enable_mime_types()); + assert_eq!(context.timeout_ms(), Some(10000)); + assert_eq!(context.max_string_length(), 16384); +} + +#[test] +fn test_evaluation_context_performance_config() { + let config = EvaluationConfig { + max_recursion_depth: 5, + max_string_length: 512, + stop_at_first_match: true, + enable_mime_types: false, + timeout_ms: Some(1000), + }; + let context = EvaluationContext::new(config); + + assert_eq!(context.config().max_recursion_depth, 5); + assert_eq!(context.max_string_length(), 512); + assert!(context.should_stop_at_first_match()); + assert!(!context.enable_mime_types()); + assert_eq!(context.timeout_ms(), Some(1000)); +} + +#[test] +fn test_evaluation_context_state_management_sequence() { + let config = EvaluationConfig::default(); + let mut context = EvaluationContext::new(config); + + // Simulate a sequence of evaluation operations + assert_eq!(context.current_offset(), 0); + assert_eq!(context.recursion_depth(), 0); + + // Start evaluation at offset 10 + context.set_current_offset(10); + assert_eq!(context.current_offset(), 10); + + // Enter nested rule evaluation + context.increment_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 1); + + // Move to different offset during nested evaluation + context.set_current_offset(25); + assert_eq!(context.current_offset(), 25); + + // Enter deeper nesting + context.increment_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 2); + + // Exit nested evaluation + context.decrement_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 1); + + // Continue evaluation at different offset + context.set_current_offset(50); + assert_eq!(context.current_offset(), 50); + + // Exit all nesting + context.decrement_recursion_depth().unwrap(); + assert_eq!(context.recursion_depth(), 0); + + // Final state check + assert_eq!(context.current_offset(), 50); + assert_eq!(context.recursion_depth(), 0); +} + +#[test] +fn test_rule_match_creation() { + let match_result = RuleMatch { + message: "ELF executable".to_string(), + offset: 0, + level: 0, + value: Value::Uint(0x7f), + confidence: RuleMatch::calculate_confidence(0), + }; + + assert_eq!(match_result.message, "ELF executable"); + assert_eq!(match_result.offset, 0); + assert_eq!(match_result.level, 0); + assert_eq!(match_result.value, Value::Uint(0x7f)); + assert!((match_result.confidence - 0.3).abs() < 0.001); +} + +#[test] +fn test_rule_match_clone() { + let original = RuleMatch { + message: "Test message".to_string(), + offset: 42, + level: 1, + value: Value::String("test".to_string()), + confidence: RuleMatch::calculate_confidence(1), + }; + + let cloned = original.clone(); + assert_eq!(original, cloned); +} + +#[test] +fn test_rule_match_debug() { + let match_result = RuleMatch { + message: "Debug test".to_string(), + offset: 10, + level: 2, + value: Value::Bytes(vec![0x01, 0x02]), + confidence: RuleMatch::calculate_confidence(2), + }; + + let debug_str = format!("{match_result:?}"); + assert!(debug_str.contains("RuleMatch")); + assert!(debug_str.contains("Debug test")); + assert!(debug_str.contains("10")); + assert!(debug_str.contains('2')); +} + +#[test] +fn test_confidence_calculation_depth_0() { + let confidence = RuleMatch::calculate_confidence(0); + assert!((confidence - 0.3).abs() < 0.001); +} + +#[test] +fn test_confidence_calculation_depth_1() { + let confidence = RuleMatch::calculate_confidence(1); + assert!((confidence - 0.5).abs() < 0.001); +} + +#[test] +fn test_confidence_calculation_depth_2() { + let confidence = RuleMatch::calculate_confidence(2); + assert!((confidence - 0.7).abs() < 0.001); +} + +#[test] +fn test_confidence_calculation_depth_3() { + let confidence = RuleMatch::calculate_confidence(3); + assert!((confidence - 0.9).abs() < 0.001); +} + +#[test] +fn test_confidence_calculation_capped_at_1() { + // Level 4+ should cap at 1.0 + let confidence_4 = RuleMatch::calculate_confidence(4); + assert!((confidence_4 - 1.0).abs() < 0.001); + + let confidence_10 = RuleMatch::calculate_confidence(10); + assert!((confidence_10 - 1.0).abs() < 0.001); + + let confidence_100 = RuleMatch::calculate_confidence(100); + assert!((confidence_100 - 1.0).abs() < 0.001); +} From e879b4e62bfd0b6226f9895374338ac929c52c96 Mon Sep 17 00:00:00 2001 From: UncleSp1d3r Date: Fri, 6 Mar 2026 15:45:06 -0500 Subject: [PATCH 12/12] docs: fix module paths in evaluator documentation Update evaluator.md, architecture.md, and AGENTS.md to reflect the actual directory structure: engine/ is a directory (not engine.rs), offset/ and operators/ are directories (not flat files), and mod.rs now has a companion tests.rs file. Co-Authored-By: Claude Opus 4.6 Signed-off-by: UncleSp1d3r --- AGENTS.md | 3 ++- docs/src/architecture.md | 6 ++++-- docs/src/evaluator.md | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d6b3b6d6..cd26967a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,7 +88,8 @@ parser/ // Evaluator module structure evaluator/ -├── mod.rs // Public interface, EvaluationContext, RuleMatch, re-exports +├── mod.rs // Public interface: EvaluationContext, RuleMatch, re-exports +├── tests.rs // Unit tests for EvaluationContext and RuleMatch ├── engine/ // Core evaluation engine submodule │ ├── mod.rs // evaluate_single_rule, evaluate_rules, evaluate_rules_with_config │ └── tests.rs // Engine unit tests diff --git a/docs/src/architecture.md b/docs/src/architecture.md index 1b17ceca..b0db2168 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -127,7 +127,9 @@ The evaluator executes magic rules against file buffers to identify file types. **Structure:** - `mod.rs`: Public API surface (~720 lines) with `EvaluationContext`, `RuleMatch` types, and re-exports -- `engine.rs`: Core evaluation engine (~2,096 lines) with `evaluate_single_rule`, `evaluate_rules`, and `evaluate_rules_with_config` functions +- `engine/`: Core evaluation engine submodule + - `mod.rs`: `evaluate_single_rule`, `evaluate_rules`, and `evaluate_rules_with_config` functions + - `tests.rs`: Engine unit tests - `types.rs`: Type interpretation with endianness handling and signedness coercion - `offset/`: Offset resolution submodule - `mod.rs`: Dispatcher (`resolve_offset`) and re-exports @@ -140,7 +142,7 @@ The evaluator executes magic rules against file buffers to identify file types. - `comparison.rs`: `compare_values`, `apply_less_than`/`greater_than`/`less_equal`/`greater_equal` - `bitwise.rs`: `apply_bitwise_and`, `apply_bitwise_and_mask`, `apply_bitwise_xor`, `apply_bitwise_not` -**Organization Note:** The evaluator module was refactored to split a monolithic 2,638-line `mod.rs` into focused submodules, keeping the public API surface in `mod.rs` and moving core evaluation logic to `engine.rs`. This maintains the same public API through re-exports (no breaking changes) while improving code organization and staying within the 500-600 line module guideline. +**Organization Note:** The evaluator module was refactored to split a monolithic 2,638-line `mod.rs` into focused submodules, keeping the public API surface in `mod.rs` and moving core evaluation logic to `engine/mod.rs`. This maintains the same public API through re-exports (no breaking changes) while improving code organization and staying within the 500-600 line module guideline. **Implemented Features:** diff --git a/docs/src/evaluator.md b/docs/src/evaluator.md index 69b82909..43238525 100644 --- a/docs/src/evaluator.md +++ b/docs/src/evaluator.md @@ -25,10 +25,10 @@ Memory Map Context State Endian Handling Match Logic Hierarchical The evaluator module is organized into focused submodules: -- **`evaluator/engine.rs`** - Core evaluation logic (`evaluate_single_rule`, `evaluate_rules`, `evaluate_rules_with_config`) +- **`evaluator/engine/mod.rs`** - Core evaluation logic (`evaluate_single_rule`, `evaluate_rules`, `evaluate_rules_with_config`) - **`evaluator/mod.rs`** - Public API surface (types, context, re-exports) -- **`evaluator/offset.rs`** - Offset resolution -- **`evaluator/operators.rs`** - Operator application +- **`evaluator/offset/mod.rs`** - Offset resolution +- **`evaluator/operators/mod.rs`** - Operator application - **`evaluator/types.rs`** - Type reading and coercion - **`evaluator/strength.rs`** - Rule strength calculation