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                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;