libmagic_rs/evaluator/engine/mod.rs
1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Core evaluation engine for magic rules.
5//!
6//! This module contains the core recursive evaluation logic for executing magic
7//! rules against file buffers. It is responsible for:
8//! - Evaluating individual rules (`evaluate_single_rule`)
9//! - Evaluating hierarchical rule sets with context (`evaluate_rules`)
10//! - Providing a convenience wrapper for evaluation with configuration
11//! (`evaluate_rules_with_config`)
12
13use crate::parser::ast::MagicRule;
14use crate::{EvaluationConfig, LibmagicError};
15
16use super::{EvaluationContext, RuleMatch, offset, operators, types};
17
18/// Evaluate a single magic rule against a file buffer
19///
20/// This function performs the core rule evaluation by:
21/// 1. Resolving the rule's offset specification to an absolute position
22/// 2. Reading and interpreting bytes at that position according to the rule's type
23/// 3. Coercing the expected value to match the type's signedness and bit width
24/// 4. Applying the rule's operator to compare the read value with the expected value
25///
26/// # Arguments
27///
28/// * `rule` - The magic rule to evaluate
29/// * `buffer` - The file buffer to evaluate against
30///
31/// # Returns
32///
33/// Returns `Ok(Some((offset, value)))` if the rule matches (with the resolved offset and
34/// read value), `Ok(None)` if it doesn't match, or `Err(LibmagicError)` if evaluation
35/// fails due to buffer access issues or other errors.
36///
37/// # Examples
38///
39/// ```rust
40/// use libmagic_rs::evaluator::evaluate_single_rule;
41/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value};
42///
43/// // Create a rule to check for ELF magic bytes at offset 0
44/// let rule = MagicRule {
45/// offset: OffsetSpec::Absolute(0),
46/// typ: TypeKind::Byte { signed: true },
47/// op: Operator::Equal,
48/// value: Value::Uint(0x7f),
49/// message: "ELF magic".to_string(),
50/// children: vec![],
51/// level: 0,
52/// strength_modifier: None,
53/// };
54///
55/// let elf_buffer = &[0x7f, 0x45, 0x4c, 0x46]; // ELF magic bytes
56/// let result = evaluate_single_rule(&rule, elf_buffer).unwrap();
57/// assert!(result.is_some()); // Should match
58///
59/// let non_elf_buffer = &[0x50, 0x4b, 0x03, 0x04]; // ZIP magic bytes
60/// let result = evaluate_single_rule(&rule, non_elf_buffer).unwrap();
61/// assert!(result.is_none()); // Should not match
62/// ```
63///
64/// # Errors
65///
66/// * `LibmagicError::EvaluationError` - If offset resolution fails, buffer access is out of bounds,
67/// or type interpretation fails
68pub fn evaluate_single_rule(
69 rule: &MagicRule,
70 buffer: &[u8],
71) -> Result<Option<(usize, crate::parser::ast::Value)>, LibmagicError> {
72 // Step 1: Resolve the offset specification to an absolute position
73 let absolute_offset = offset::resolve_offset(&rule.offset, buffer)?;
74
75 // Step 2: Read and interpret bytes at the resolved offset according to the rule's type
76 let read_value = types::read_typed_value(buffer, absolute_offset, &rule.typ)
77 .map_err(|e| LibmagicError::EvaluationError(e.into()))?;
78
79 // Step 3: Coerce the rule's expected value to match the type's signedness/width
80 let expected_value = types::coerce_value_to_type(&rule.value, &rule.typ);
81
82 // Step 4: Apply the operator to compare the read value with the expected value
83 // BitwiseNot needs type-aware bit-width masking so the complement is computed
84 // at the type's natural width (e.g., byte NOT of 0x00 = 0xFF, not u64::MAX).
85 let matched = match &rule.op {
86 crate::parser::ast::Operator::BitwiseNot => operators::apply_bitwise_not_with_width(
87 &read_value,
88 &expected_value,
89 rule.typ.bit_width(),
90 ),
91 op => operators::apply_operator(op, &read_value, &expected_value),
92 };
93 Ok(matched.then_some((absolute_offset, read_value)))
94}
95
96/// Evaluate a list of magic rules against a file buffer with hierarchical processing
97///
98/// This function implements the core hierarchical rule evaluation algorithm with graceful
99/// error handling:
100/// 1. Evaluates each top-level rule in sequence
101/// 2. If a parent rule matches, evaluates its child rules for refinement
102/// 3. Collects all matches or stops at first match based on configuration
103/// 4. Maintains evaluation context for recursion limits and state
104/// 5. Implements graceful degradation by skipping problematic rules and continuing evaluation
105///
106/// The hierarchical evaluation follows these principles:
107/// - Parent rules must match before children are evaluated
108/// - Child rules provide refinement and additional detail
109/// - Evaluation can stop at first match or continue for all matches
110/// - Recursion depth is limited to prevent infinite loops
111/// - Problematic rules are skipped to allow evaluation to continue
112///
113/// # Arguments
114///
115/// * `rules` - The list of magic rules to evaluate
116/// * `buffer` - The file buffer to evaluate against
117/// * `context` - Mutable evaluation context for state management
118///
119/// # Returns
120///
121/// Returns `Ok(Vec<RuleMatch>)` containing all matches found. Errors in individual rules
122/// are skipped to allow evaluation to continue. Only returns `Err(LibmagicError)`
123/// for critical failures like timeout or recursion limit exceeded.
124///
125/// # Examples
126///
127/// ```rust
128/// use libmagic_rs::evaluator::{evaluate_rules, EvaluationContext, RuleMatch};
129/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value};
130/// use libmagic_rs::EvaluationConfig;
131///
132/// // Create a hierarchical rule set for ELF files
133/// let parent_rule = MagicRule {
134/// offset: OffsetSpec::Absolute(0),
135/// typ: TypeKind::Byte { signed: true },
136/// op: Operator::Equal,
137/// value: Value::Uint(0x7f),
138/// message: "ELF".to_string(),
139/// children: vec![
140/// MagicRule {
141/// offset: OffsetSpec::Absolute(4),
142/// typ: TypeKind::Byte { signed: true },
143/// op: Operator::Equal,
144/// value: Value::Uint(2),
145/// message: "64-bit".to_string(),
146/// children: vec![],
147/// level: 1,
148/// strength_modifier: None,
149/// }
150/// ],
151/// level: 0,
152/// strength_modifier: None,
153/// };
154///
155/// let rules = vec![parent_rule];
156/// let buffer = &[0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01]; // ELF64 header
157/// let config = EvaluationConfig::default();
158/// let mut context = EvaluationContext::new(config);
159///
160/// let matches = evaluate_rules(&rules, buffer, &mut context).unwrap();
161/// assert_eq!(matches.len(), 2); // Parent and child should both match
162/// ```
163///
164/// # Errors
165///
166/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout
167/// * `LibmagicError::EvaluationError` - Only for critical failures like recursion limit exceeded
168///
169/// Individual rule evaluation errors are handled gracefully and do not stop the overall evaluation.
170pub fn evaluate_rules(
171 rules: &[MagicRule],
172 buffer: &[u8],
173 context: &mut EvaluationContext,
174) -> Result<Vec<RuleMatch>, LibmagicError> {
175 let mut matches = Vec::with_capacity(8);
176 let start_time = std::time::Instant::now();
177 let mut rule_count = 0u32;
178
179 for rule in rules {
180 // Check timeout periodically (every 16 rules) to reduce syscall overhead
181 rule_count = rule_count.wrapping_add(1);
182 if rule_count.trailing_zeros() >= 4
183 && let Some(timeout_ms) = context.timeout_ms()
184 && start_time.elapsed().as_millis() > u128::from(timeout_ms)
185 {
186 return Err(LibmagicError::Timeout { timeout_ms });
187 }
188
189 // Evaluate the current rule with graceful error handling
190 let match_data = match evaluate_single_rule(rule, buffer) {
191 Ok(data) => data,
192 Err(
193 LibmagicError::EvaluationError(
194 crate::error::EvaluationError::BufferOverrun { .. }
195 | crate::error::EvaluationError::InvalidOffset { .. }
196 | crate::error::EvaluationError::TypeReadError(_),
197 )
198 | LibmagicError::IoError(_),
199 ) => {
200 // Expected evaluation errors for individual rules -- skip gracefully
201 continue;
202 }
203 Err(e) => {
204 // Unexpected errors (InternalError, UnsupportedType, etc.) should propagate
205 return Err(e);
206 }
207 };
208
209 if let Some((absolute_offset, read_value)) = match_data {
210 let match_result = RuleMatch {
211 message: rule.message.clone(),
212 offset: absolute_offset,
213 level: rule.level,
214 value: read_value,
215 confidence: RuleMatch::calculate_confidence(rule.level),
216 };
217 matches.push(match_result);
218
219 // If this rule has children, evaluate them recursively
220 if !rule.children.is_empty() {
221 // Check recursion depth limit - this is a critical error that should stop evaluation
222 context.increment_recursion_depth()?;
223
224 // Recursively evaluate child rules with graceful error handling
225 match evaluate_rules(&rule.children, buffer, context) {
226 Ok(child_matches) => {
227 matches.extend(child_matches);
228 }
229 Err(LibmagicError::Timeout { timeout_ms }) => {
230 // Timeout is critical, propagate it up
231 let _ = context.decrement_recursion_depth();
232 return Err(LibmagicError::Timeout { timeout_ms });
233 }
234 Err(
235 e @ LibmagicError::EvaluationError(
236 crate::error::EvaluationError::RecursionLimitExceeded { .. },
237 ),
238 ) => {
239 // Recursion limit is critical, propagate the original error
240 let _ = context.decrement_recursion_depth();
241 return Err(e);
242 }
243 Err(
244 LibmagicError::EvaluationError(
245 crate::error::EvaluationError::BufferOverrun { .. }
246 | crate::error::EvaluationError::InvalidOffset { .. }
247 | crate::error::EvaluationError::TypeReadError(_),
248 )
249 | LibmagicError::IoError(_),
250 ) => {
251 // Expected child evaluation errors -- skip gracefully
252 }
253 Err(e) => {
254 // Unexpected errors in children should propagate
255 let _ = context.decrement_recursion_depth();
256 return Err(e);
257 }
258 }
259
260 // Restore recursion depth
261 context.decrement_recursion_depth()?;
262 }
263
264 // Stop at first match if configured to do so
265 if context.should_stop_at_first_match() {
266 break;
267 }
268 }
269 }
270
271 Ok(matches)
272}
273
274/// Evaluate magic rules with a fresh context
275///
276/// This is a convenience function that creates a new evaluation context
277/// and evaluates the rules. Useful for simple evaluation scenarios.
278///
279/// # Arguments
280///
281/// * `rules` - The list of magic rules to evaluate
282/// * `buffer` - The file buffer to evaluate against
283/// * `config` - Configuration for evaluation behavior
284///
285/// # Returns
286///
287/// Returns `Ok(Vec<RuleMatch>)` containing all matches found, or `Err(LibmagicError)`
288/// if evaluation fails.
289///
290/// # Examples
291///
292/// ```rust
293/// use libmagic_rs::evaluator::{evaluate_rules_with_config, RuleMatch};
294/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value};
295/// use libmagic_rs::EvaluationConfig;
296///
297/// let rule = MagicRule {
298/// offset: OffsetSpec::Absolute(0),
299/// typ: TypeKind::Byte { signed: true },
300/// op: Operator::Equal,
301/// value: Value::Uint(0x7f),
302/// message: "ELF magic".to_string(),
303/// children: vec![],
304/// level: 0,
305/// strength_modifier: None,
306/// };
307///
308/// let rules = vec![rule];
309/// let buffer = &[0x7f, 0x45, 0x4c, 0x46];
310/// let config = EvaluationConfig::default();
311///
312/// let matches = evaluate_rules_with_config(&rules, buffer, &config).unwrap();
313/// assert_eq!(matches.len(), 1);
314/// assert_eq!(matches[0].message, "ELF magic");
315/// ```
316///
317/// # Errors
318///
319/// * `LibmagicError::EvaluationError` - If rule evaluation fails
320/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout
321pub fn evaluate_rules_with_config(
322 rules: &[MagicRule],
323 buffer: &[u8],
324 config: &EvaluationConfig,
325) -> Result<Vec<RuleMatch>, LibmagicError> {
326 let mut context = EvaluationContext::new(config.clone());
327 evaluate_rules(rules, buffer, &mut context)
328}
329
330#[cfg(test)]
331mod tests;