Skip to main content

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;