Skip to main content

libmagic_rs/evaluator/
mod.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Rule evaluation engine
5//!
6//! This module provides the public interface for magic rule evaluation,
7//! including data types for evaluation state and match results, and
8//! re-exports the core evaluation functions from submodules.
9
10use crate::{EvaluationConfig, LibmagicError};
11use serde::Serialize;
12
13mod engine;
14pub mod offset;
15pub mod operators;
16pub mod strength;
17pub mod types;
18
19pub use engine::{evaluate_rules, evaluate_rules_with_config, evaluate_single_rule};
20
21/// Strip a single leading GNU `file` no-separator marker from `s`, if present.
22///
23/// The marker (GOTCHAS S14.1) suppresses the separating space when a
24/// description is appended to a sibling's text. It reaches evaluator code in
25/// two forms: the raw byte `U+0008` (backspace), used for programmatically
26/// constructed messages, and -- far more commonly -- the literal
27/// two-character sequence `\b` (backslash + `'b'`), because the message parser
28/// preserves description text verbatim (matching GNU `file`, which keeps the
29/// desc literal and special-cases a leading `\b` at print time).
30///
31/// Returns `Some(rest)` with the marker removed when one is present, else
32/// `None`. This is the single source of truth for the two-form marker so the
33/// three call sites (`concatenate_messages` output rendering,
34/// `is_message_bearing` gating, and `evaluate_use_rule` name-message
35/// attachment) cannot drift on which forms they recognize.
36pub(crate) fn strip_no_separator_marker(s: &str) -> Option<&str> {
37    s.strip_prefix('\u{0008}').or_else(|| s.strip_prefix("\\b"))
38}
39
40/// Shared environment attached to an [`EvaluationContext`] so the engine can
41/// resolve whole-database operations (currently: `Use` subroutine lookups;
42/// eventually `indirect` whole-tree re-entry).
43///
44/// Stored as an `Arc` so cloning a context across recursive calls is cheap
45/// and the rule data can be shared safely across threads.
46#[derive(Debug, Clone)]
47pub(crate) struct RuleEnvironment {
48    /// Named subroutine table, keyed by identifier.
49    pub(crate) name_table: std::sync::Arc<crate::parser::name_table::NameTable>,
50    /// Top-level rule list retained for future whole-database operations.
51    #[allow(dead_code)]
52    pub(crate) root_rules: std::sync::Arc<[crate::parser::ast::MagicRule]>,
53}
54
55/// Context for maintaining evaluation state during rule processing
56///
57/// The `EvaluationContext` tracks the current state of rule evaluation,
58/// including the current offset position, recursion depth for nested rules,
59/// and configuration settings that control evaluation behavior.
60///
61/// # Examples
62///
63/// ```rust
64/// use libmagic_rs::evaluator::EvaluationContext;
65/// use libmagic_rs::EvaluationConfig;
66///
67/// let config = EvaluationConfig::default();
68/// let context = EvaluationContext::new(config);
69///
70/// assert_eq!(context.current_offset(), 0);
71/// assert_eq!(context.recursion_depth(), 0);
72/// ```
73#[derive(Debug, Clone)]
74#[non_exhaustive]
75pub struct EvaluationContext {
76    /// Current offset position in the file buffer
77    current_offset: usize,
78    /// End offset of the most recent successful match.
79    ///
80    /// This is the GNU `file`/libmagic anchor used to resolve relative
81    /// (`&+N` / `&-N`) offsets. It is updated to the end of the most
82    /// recently matched rule -- the value may *increase or decrease* as
83    /// successive rules match at different positions; it is not a
84    /// high-watermark. A fresh context starts with this set to 0, which
85    /// matches libmagic's behavior of resolving top-level relative offsets
86    /// from the file start.
87    last_match_end: usize,
88    /// Current recursion depth for nested rule evaluation
89    recursion_depth: u32,
90    /// Configuration settings for evaluation behavior
91    config: EvaluationConfig,
92    /// Optional rule environment (name table + root rules) threaded from
93    /// [`MagicDatabase`](crate::MagicDatabase). Evaluations that come in
94    /// through the low-level [`evaluate_rules`] / [`evaluate_rules_with_config`]
95    /// surface (tests, programmatic consumers) run with `rule_env = None`,
96    /// in which case `MetaType::Use` rules are silent no-ops.
97    rule_env: Option<std::sync::Arc<RuleEnvironment>>,
98    /// Base offset applied to absolute offset resolution.
99    ///
100    /// Normally 0. When evaluating a subroutine body via `MetaType::Use`,
101    /// this is set to the use-site offset so that the subroutine's
102    /// `OffsetSpec::Absolute(n)` rules resolve to `base + n` (matching
103    /// magic(5) / libmagic semantics: subroutines see offsets relative
104    /// to the caller's invocation point, not absolute file positions).
105    /// Restored to the caller's value on subroutine exit via the
106    /// `SubroutineScope` RAII guard in `engine/mod.rs`, which saves
107    /// and restores both `last_match_end` and `base_offset` together.
108    base_offset: usize,
109    /// One-shot flag set by `MetaType::Indirect` dispatch before
110    /// re-entering the root rule list. When true, the next entry to
111    /// `evaluate_rules` treats the iteration as a top-level sibling
112    /// chain (anchor chains across siblings per GOTCHAS S3.8) rather
113    /// than as a continuation list (anchor resets between siblings).
114    /// Consumed at entry — children of a matched rule inside the
115    /// re-entry see the flag cleared, so their own continuation-reset
116    /// semantics kick in via the `recursion_depth > 0` gate.
117    ///
118    /// Without this flag, `indirect` wrapping re-entry under
119    /// `RecursionGuard` forces `recursion_depth > 0`, which forces
120    /// continuation-reset semantics on the root rule list — wrong,
121    /// because top-level rules in the re-entered database should
122    /// chain sibling anchors like any other top-level evaluation.
123    indirect_reentry: bool,
124    /// Endian-flip state for `use \^name` subroutine invocation (magic(5)
125    /// `\^` prefix; libmagic `softmagic.c` `cvt_flip`). When true, every
126    /// endian-bearing typed read inside the current subroutine body
127    /// (`short`/`long`/`quad`/`float`/`double`/date families) has its
128    /// declared little/big endianness swapped before the read. A `use
129    /// \^name` site *toggles* this flag (not sets it) and the toggle
130    /// propagates into nested `use` calls, matching libmagic's `flip =
131    /// !flip` parameter threading. Saved and restored around the
132    /// subroutine call by the `SubroutineScope` RAII guard in
133    /// `engine/mod.rs`. Native-endian types and `String16` are never
134    /// flipped (they are absent from `cvt_flip`).
135    flip_endian: bool,
136}
137
138impl EvaluationContext {
139    /// Create a new evaluation context with the given configuration
140    ///
141    /// # Arguments
142    ///
143    /// * `config` - Configuration settings for evaluation behavior
144    ///
145    /// # Examples
146    ///
147    /// ```rust
148    /// use libmagic_rs::evaluator::EvaluationContext;
149    /// use libmagic_rs::EvaluationConfig;
150    ///
151    /// let config = EvaluationConfig::default();
152    /// let context = EvaluationContext::new(config);
153    /// ```
154    #[must_use]
155    pub fn new(mut config: EvaluationConfig) -> Self {
156        // Defensive clamp on `max_string_length`: `EvaluationConfig::validate()`
157        // rejects 0, but callers can bypass validation by setting the field
158        // via struct-literal syntax (or via the `with_max_string_length`
159        // builder, which doesn't validate). Without this clamp, a `cap = 0`
160        // would silently produce zero-byte reads on every scan-mode `string x`
161        // rule and disable the CWE-770 control documented at this field.
162        //
163        // The clamp rewrites an invalid 0 to
164        // `crate::evaluator::types::DEFAULT_MAX_STRING_LENGTH` (8192,
165        // matching `EvaluationConfig::default()`). A `warn!` records the
166        // correction so embedders see it in logs. Closes PR #304 review
167        // finding SF-1.
168        if config.max_string_length == 0 {
169            log::warn!(
170                "EvaluationContext::new received max_string_length=0 \
171                 (likely a struct-literal or builder bypass of \
172                 EvaluationConfig::validate); clamping to {} (the documented \
173                 default). Construct the config via EvaluationConfig::new() \
174                 / EvaluationConfig::default() and use the with_* builders \
175                 to avoid this warning.",
176                crate::evaluator::types::DEFAULT_MAX_STRING_LENGTH,
177            );
178            config.max_string_length = crate::evaluator::types::DEFAULT_MAX_STRING_LENGTH;
179        }
180        Self {
181            current_offset: 0,
182            last_match_end: 0,
183            recursion_depth: 0,
184            config,
185            rule_env: None,
186            base_offset: 0,
187            indirect_reentry: false,
188            flip_endian: false,
189        }
190    }
191
192    /// Read-only access to the `use \^name` endian-flip state. True only
193    /// while evaluating a subroutine body reached through an odd number of
194    /// `\^`-prefixed `use` invocations.
195    #[must_use]
196    pub(crate) const fn flip_endian(&self) -> bool {
197        self.flip_endian
198    }
199
200    /// Set the endian-flip state.
201    ///
202    /// `pub(crate)` and owned by the engine's `SubroutineScope` RAII guard
203    /// -- no external caller should set this directly.
204    pub(crate) fn set_flip_endian(&mut self, flip: bool) {
205        self.flip_endian = flip;
206    }
207
208    /// Read-only access to the subroutine base offset. Non-zero only
209    /// during a `MetaType::Use` body evaluation.
210    #[must_use]
211    pub(crate) const fn base_offset(&self) -> usize {
212        self.base_offset
213    }
214
215    /// Set the subroutine base offset.
216    ///
217    /// `pub(crate)` and owned by the engine's `SubroutineScope` RAII
218    /// guard -- no external caller should set this directly.
219    pub(crate) fn set_base_offset(&mut self, offset: usize) {
220        self.base_offset = offset;
221    }
222
223    /// Read-and-clear the indirect-reentry flag. Used by `evaluate_rules`
224    /// at entry to decide whether the iteration is a top-level re-entry
225    /// (no anchor reset between siblings) or a continuation list (reset
226    /// between siblings). Cleared on read so children of a matched rule
227    /// inside the re-entry see the flag as false and fall back to the
228    /// `recursion_depth > 0` gate for their own continuation semantics.
229    pub(crate) fn take_indirect_reentry(&mut self) -> bool {
230        std::mem::take(&mut self.indirect_reentry)
231    }
232
233    /// Set the indirect-reentry flag.
234    ///
235    /// `pub(crate)` and owned by the `MetaType::Indirect` dispatch in
236    /// `engine/mod.rs`. Callers should set this true exactly once
237    /// before invoking `evaluate_rules` on the root rule list.
238    pub(crate) fn set_indirect_reentry(&mut self, flag: bool) {
239        self.indirect_reentry = flag;
240    }
241
242    /// Attach a rule environment to this context.
243    ///
244    /// The environment carries the name-subroutine table and root rule list
245    /// so the engine can resolve `MetaType::Use` rules and (eventually)
246    /// `MetaType::Indirect` re-entries. Intended to be called once by
247    /// [`MagicDatabase`](crate::MagicDatabase) before handing the context
248    /// to [`evaluate_rules`].
249    #[must_use]
250    pub(crate) fn with_rule_env(mut self, env: std::sync::Arc<RuleEnvironment>) -> Self {
251        self.rule_env = Some(env);
252        self
253    }
254
255    /// Read-only access to the attached rule environment, if any.
256    #[must_use]
257    pub(crate) fn rule_env(&self) -> Option<&RuleEnvironment> {
258        self.rule_env.as_deref()
259    }
260
261    /// Get the current offset position
262    ///
263    /// # Returns
264    ///
265    /// The current offset position in the file buffer
266    #[must_use]
267    pub const fn current_offset(&self) -> usize {
268        self.current_offset
269    }
270
271    /// Set the current offset position
272    ///
273    /// # Arguments
274    ///
275    /// * `offset` - The new offset position
276    pub fn set_current_offset(&mut self, offset: usize) {
277        self.current_offset = offset;
278    }
279
280    /// Get the end offset of the most recent successful match.
281    ///
282    /// This is the GNU `file`/libmagic anchor used to resolve relative
283    /// (`&+N` / `&-N`) offset specifications. A fresh context returns 0,
284    /// which makes top-level relative offsets resolve from the file start.
285    ///
286    /// `pub(crate)` because the anchor is an internal engine detail; external
287    /// consumers should not couple to it.
288    #[must_use]
289    pub(crate) const fn last_match_end(&self) -> usize {
290        self.last_match_end
291    }
292
293    /// Set the end offset of the most recent successful match.
294    ///
295    /// Called by the evaluation engine after a rule matches, to advance the
296    /// anchor used by subsequent relative offset resolution. The new value
297    /// is typically `match_offset + bytes_consumed_by_type`.
298    ///
299    /// `pub(crate)` because external callers should not be able to inject
300    /// arbitrary anchor state. External callers that need to clear the
301    /// anchor between buffer evaluations should call
302    /// `EvaluationContext::reset()`, which resets the anchor, current
303    /// offset, and recursion depth together.
304    pub(crate) fn set_last_match_end(&mut self, offset: usize) {
305        self.last_match_end = offset;
306    }
307
308    /// Get the current recursion depth
309    ///
310    /// # Returns
311    ///
312    /// The current recursion depth for nested rule evaluation
313    #[must_use]
314    pub const fn recursion_depth(&self) -> u32 {
315        self.recursion_depth
316    }
317
318    /// Increment the recursion depth
319    ///
320    /// # Returns
321    ///
322    /// `Ok(())` if the recursion depth is within limits, or `Err(LibmagicError)`
323    /// if the maximum recursion depth would be exceeded
324    ///
325    /// # Errors
326    ///
327    /// Returns `LibmagicError::EvaluationError` if incrementing would exceed
328    /// the maximum recursion depth configured in the evaluation config.
329    pub(crate) fn increment_recursion_depth(&mut self) -> Result<(), LibmagicError> {
330        if self.recursion_depth >= self.config.max_recursion_depth {
331            return Err(LibmagicError::EvaluationError(
332                crate::error::EvaluationError::recursion_limit_exceeded(self.recursion_depth),
333            ));
334        }
335        self.recursion_depth += 1;
336        Ok(())
337    }
338
339    /// Decrement the recursion depth
340    ///
341    /// # Errors
342    ///
343    /// Returns an error if the recursion depth is already 0, as this indicates
344    /// a programming error in the evaluation logic (mismatched increment/decrement calls).
345    pub(crate) fn decrement_recursion_depth(&mut self) -> Result<(), LibmagicError> {
346        if self.recursion_depth == 0 {
347            return Err(LibmagicError::EvaluationError(
348                crate::error::EvaluationError::internal_error(
349                    "Attempted to decrement recursion depth below 0",
350                ),
351            ));
352        }
353        self.recursion_depth -= 1;
354        Ok(())
355    }
356
357    /// Get a reference to the evaluation configuration
358    ///
359    /// # Returns
360    ///
361    /// A reference to the `EvaluationConfig` used by this context
362    #[must_use]
363    pub const fn config(&self) -> &EvaluationConfig {
364        &self.config
365    }
366
367    /// Check if evaluation should stop at the first match
368    ///
369    /// # Returns
370    ///
371    /// `true` if evaluation should stop at the first match, `false` otherwise
372    #[must_use]
373    pub const fn should_stop_at_first_match(&self) -> bool {
374        self.config.stop_at_first_match
375    }
376
377    /// Get the maximum string length allowed for scan-mode string reads.
378    ///
379    /// Threaded into both string-read dispatchers
380    /// (`read_typed_value_with_pattern` for the unflagged `(None, _)` arm
381    /// and `read_pattern_match` for the flagged `/c`/`/C`/`/w`/`/W`/`/T`/`/f`
382    /// arm) so they cap the buffer-length allocation against this value.
383    /// Does NOT apply to `TypeKind::PString` (which errors on oversized
384    /// length prefixes per GOTCHAS S6.1) or `TypeKind::String16` (capped
385    /// at a hardcoded `STRING16_MAX_UNITS = 8192` ceiling).
386    ///
387    /// # Returns
388    ///
389    /// The configured `max_string_length` (default 8192 bytes per
390    /// `EvaluationConfig::default()`).
391    #[must_use]
392    pub const fn max_string_length(&self) -> usize {
393        self.config.max_string_length
394    }
395
396    /// Check if MIME type mapping is enabled
397    ///
398    /// # Returns
399    ///
400    /// `true` if MIME type mapping should be performed, `false` otherwise
401    #[must_use]
402    pub const fn enable_mime_types(&self) -> bool {
403        self.config.enable_mime_types
404    }
405
406    /// Get the evaluation timeout in milliseconds
407    ///
408    /// # Returns
409    ///
410    /// The timeout duration in milliseconds, or `None` if no timeout is set
411    #[must_use]
412    pub const fn timeout_ms(&self) -> Option<u64> {
413        self.config.timeout_ms
414    }
415
416    /// Reset the context to initial state while preserving configuration
417    ///
418    /// This resets the current offset and recursion depth to 0, but keeps
419    /// the same configuration settings.
420    pub fn reset(&mut self) {
421        self.current_offset = 0;
422        self.last_match_end = 0;
423        self.recursion_depth = 0;
424        self.base_offset = 0;
425        self.indirect_reentry = false;
426        self.flip_endian = false;
427    }
428}
429
430/// RAII guard that increments recursion depth on entry and decrements on drop.
431///
432/// Replaces the manual `increment_recursion_depth` / `decrement_recursion_depth`
433/// pair with a scope-based guard, eliminating the risk of mismatched calls and
434/// the need to swallow cleanup errors on error-return paths.
435///
436/// Obtain a guard via [`RecursionGuard::enter`], which borrows the context
437/// mutably for the guard's lifetime. Use [`RecursionGuard::context`] to access
438/// the borrowed context for the duration of the recursive call. The guard
439/// automatically decrements the recursion depth when it goes out of scope.
440///
441/// The guard is `pub(crate)` because recursion-depth management is an internal
442/// detail of the evaluation engine.
443pub(crate) struct RecursionGuard<'a> {
444    context: &'a mut EvaluationContext,
445}
446
447impl<'a> RecursionGuard<'a> {
448    /// Enter a new recursion level, incrementing the context's recursion depth.
449    ///
450    /// # Errors
451    ///
452    /// Returns `LibmagicError::EvaluationError` if incrementing would exceed
453    /// the maximum recursion depth configured in the evaluation config.
454    pub(crate) fn enter(context: &'a mut EvaluationContext) -> Result<Self, LibmagicError> {
455        context.increment_recursion_depth()?;
456        Ok(Self { context })
457    }
458
459    /// Access the underlying context for the duration of the guard.
460    pub(crate) fn context(&mut self) -> &mut EvaluationContext {
461        self.context
462    }
463}
464
465impl Drop for RecursionGuard<'_> {
466    fn drop(&mut self) {
467        // Safe to ignore: `decrement_recursion_depth` only fails when the
468        // depth is already 0, which is impossible here because `enter` just
469        // incremented it and the depth is only mutated through guard pairs.
470        let result = self.context.decrement_recursion_depth();
471        debug_assert!(
472            result.is_ok(),
473            "RecursionGuard invariant violated: decrement failed after successful enter()"
474        );
475    }
476}
477
478/// Result of evaluating a magic rule
479///
480/// Contains information extracted from a successful rule match, including
481/// the matched value, position, and confidence score.
482///
483/// This type derives `Serialize` so callers can convert evaluation results
484/// to JSON, but intentionally does NOT derive `Deserialize`: a
485/// reconstructed `RuleMatch` would lack the buffer context it was
486/// produced against, so deserialization is not a meaningful operation.
487/// The output-side conversion layer (`output::MatchResult` /
488/// `output::json::JsonMatchResult`) is the documented JSON contract.
489#[derive(Debug, Clone, PartialEq, Serialize)]
490#[non_exhaustive]
491pub struct RuleMatch {
492    /// The message associated with the matching rule
493    pub message: String,
494    /// The offset where the match occurred
495    pub offset: usize,
496    /// The rule level (depth in hierarchy)
497    pub level: u32,
498    /// The matched value
499    pub value: crate::parser::ast::Value,
500    /// The type used to read the matched value
501    ///
502    /// Carries the source `TypeKind` so downstream consumers (e.g., output
503    /// formatting) can determine the on-disk width of the matched value.
504    ///
505    /// `#[serde(skip)]` keeps the parser AST out of JSON output produced
506    /// by serializing `EvaluationResult` directly via
507    /// `serde_json::to_string(&result)`. The documented JSON contract is
508    /// `JsonMatchResult` in `src/output/json.rs`, which omits this field.
509    /// Origin findings 1B-H2 / 2A-M1 (CWE-200 information exposure).
510    /// Rust-side consumers continue to access `type_kind` via field access
511    /// for runtime needs (`format_magic_message` width-masking,
512    /// `bit_width()` derivation).
513    #[serde(skip)]
514    pub type_kind: crate::parser::ast::TypeKind,
515    /// Confidence score (0.0 to 1.0)
516    ///
517    /// Calculated based on match depth in the rule hierarchy.
518    /// Deeper matches indicate more specific file type identification
519    /// and thus higher confidence.
520    pub confidence: f64,
521}
522
523impl RuleMatch {
524    /// Construct a new `RuleMatch`.
525    ///
526    /// `confidence` is typically derived from `level` via
527    /// [`RuleMatch::calculate_confidence`]; pass it explicitly here so
528    /// callers can supply an alternative score when needed (e.g. when
529    /// post-processing a series of matches).
530    #[must_use]
531    pub fn new(
532        message: String,
533        offset: usize,
534        level: u32,
535        value: crate::parser::ast::Value,
536        type_kind: crate::parser::ast::TypeKind,
537        confidence: f64,
538    ) -> Self {
539        Self {
540            message,
541            offset,
542            level,
543            value,
544            type_kind,
545            confidence,
546        }
547    }
548
549    /// Calculate confidence score based on rule depth
550    ///
551    /// Formula: min(1.0, 0.3 + (level * 0.2))
552    /// - Level 0 (root): 0.3
553    /// - Level 1: 0.5
554    /// - Level 2: 0.7
555    /// - Level 3: 0.9
556    /// - Level 4+: 1.0 (capped)
557    ///
558    /// # Examples
559    ///
560    /// ```
561    /// use libmagic_rs::evaluator::RuleMatch;
562    ///
563    /// assert!((RuleMatch::calculate_confidence(0) - 0.3).abs() < 0.001);
564    /// assert!((RuleMatch::calculate_confidence(3) - 0.9).abs() < 0.001);
565    /// assert!((RuleMatch::calculate_confidence(10) - 1.0).abs() < 0.001);
566    /// ```
567    #[must_use]
568    pub fn calculate_confidence(level: u32) -> f64 {
569        (0.3 + (f64::from(level) * 0.2)).min(1.0)
570    }
571}
572
573#[cfg(test)]
574mod tests;