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 type_kind: rule.typ.clone(),
216 confidence: RuleMatch::calculate_confidence(rule.level),
217 };
218 matches.push(match_result);
219
220 // If this rule has children, evaluate them recursively
221 if !rule.children.is_empty() {
222 // Check recursion depth limit - this is a critical error that should stop evaluation
223 context.increment_recursion_depth()?;
224
225 // Recursively evaluate child rules with graceful error handling
226 match evaluate_rules(&rule.children, buffer, context) {
227 Ok(child_matches) => {
228 matches.extend(child_matches);
229 }
230 Err(LibmagicError::Timeout { timeout_ms }) => {
231 // Timeout is critical, propagate it up
232 let _ = context.decrement_recursion_depth();
233 return Err(LibmagicError::Timeout { timeout_ms });
234 }
235 Err(
236 e @ LibmagicError::EvaluationError(
237 crate::error::EvaluationError::RecursionLimitExceeded { .. },
238 ),
239 ) => {
240 // Recursion limit is critical, propagate the original error
241 let _ = context.decrement_recursion_depth();
242 return Err(e);
243 }
244 Err(
245 LibmagicError::EvaluationError(
246 crate::error::EvaluationError::BufferOverrun { .. }
247 | crate::error::EvaluationError::InvalidOffset { .. }
248 | crate::error::EvaluationError::TypeReadError(_),
249 )
250 | LibmagicError::IoError(_),
251 ) => {
252 // Expected child evaluation errors -- skip gracefully
253 }
254 Err(e) => {
255 // Unexpected errors in children should propagate
256 let _ = context.decrement_recursion_depth();
257 return Err(e);
258 }
259 }
260
261 // Restore recursion depth
262 context.decrement_recursion_depth()?;
263 }
264
265 // Stop at first match if configured to do so
266 if context.should_stop_at_first_match() {
267 break;
268 }
269 }
270 }
271
272 Ok(matches)
273}
274
275/// Evaluate magic rules with a fresh context
276///
277/// This is a convenience function that creates a new evaluation context
278/// and evaluates the rules. Useful for simple evaluation scenarios.
279///
280/// # Arguments
281///
282/// * `rules` - The list of magic rules to evaluate
283/// * `buffer` - The file buffer to evaluate against
284/// * `config` - Configuration for evaluation behavior
285///
286/// # Returns
287///
288/// Returns `Ok(Vec<RuleMatch>)` containing all matches found, or `Err(LibmagicError)`
289/// if evaluation fails.
290///
291/// # Examples
292///
293/// ```rust
294/// use libmagic_rs::evaluator::{evaluate_rules_with_config, RuleMatch};
295/// use libmagic_rs::parser::ast::{MagicRule, OffsetSpec, TypeKind, Operator, Value};
296/// use libmagic_rs::EvaluationConfig;
297///
298/// let rule = MagicRule {
299/// offset: OffsetSpec::Absolute(0),
300/// typ: TypeKind::Byte { signed: true },
301/// op: Operator::Equal,
302/// value: Value::Uint(0x7f),
303/// message: "ELF magic".to_string(),
304/// children: vec![],
305/// level: 0,
306/// strength_modifier: None,
307/// };
308///
309/// let rules = vec![rule];
310/// let buffer = &[0x7f, 0x45, 0x4c, 0x46];
311/// let config = EvaluationConfig::default();
312///
313/// let matches = evaluate_rules_with_config(&rules, buffer, &config).unwrap();
314/// assert_eq!(matches.len(), 1);
315/// assert_eq!(matches[0].message, "ELF magic");
316/// ```
317///
318/// # Errors
319///
320/// * `LibmagicError::EvaluationError` - If rule evaluation fails
321/// * `LibmagicError::Timeout` - If evaluation exceeds configured timeout
322pub fn evaluate_rules_with_config(
323 rules: &[MagicRule],
324 buffer: &[u8],
325 config: &EvaluationConfig,
326) -> Result<Vec<RuleMatch>, LibmagicError> {
327 let mut context = EvaluationContext::new(config.clone());
328 evaluate_rules(rules, buffer, &mut context)
329}
330
331#[cfg(test)]
332mod tests;