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