Skip to main content

libmagic_rs/
config.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Evaluation configuration for magic rule processing.
5//!
6//! Defines [`EvaluationConfig`], which controls recursion depth, string length
7//! limits, matching strategy, MIME type mapping, and timeouts during rule
8//! evaluation. Extracted from `lib.rs` to keep that module under the project's
9//! file-size limit.
10
11use crate::Result;
12use crate::error::LibmagicError;
13
14/// Configuration for rule evaluation
15///
16/// This struct controls various aspects of magic rule evaluation behavior,
17/// including performance limits, output options, and matching strategies.
18///
19/// # Forward compatibility
20///
21/// This struct is marked `#[non_exhaustive]`: new configuration fields may
22/// be added in any release without it being a breaking change. Construct
23/// instances via one of the factory constructors
24/// ([`EvaluationConfig::default()`], [`EvaluationConfig::new()`],
25/// [`EvaluationConfig::performance()`],
26/// [`EvaluationConfig::comprehensive()`]) and then chain `with_*`
27/// builder-style setters:
28///
29/// ```rust
30/// use libmagic_rs::EvaluationConfig;
31///
32/// let custom_config = EvaluationConfig::default()
33///     .with_max_recursion_depth(10)
34///     .with_timeout_ms(Some(5_000));
35/// ```
36///
37/// Direct struct-literal construction (`EvaluationConfig { .. }`) is
38/// rejected by the compiler from outside this crate because of
39/// `#[non_exhaustive]`.
40#[derive(Debug, Clone, PartialEq, Eq)]
41#[non_exhaustive]
42pub struct EvaluationConfig {
43    /// Maximum recursion depth for nested rules
44    ///
45    /// This prevents infinite recursion in malformed magic files and limits
46    /// the depth of rule hierarchy traversal. Default is 20.
47    pub max_recursion_depth: u32,
48
49    /// Maximum string length to read
50    ///
51    /// Caps the buffer-length allocation for scan-mode reads of
52    /// `TypeKind::String` (both the unflagged `(None, _)` arm and the
53    /// flagged `/c`/`/C`/`/w`/`/W`/`/T`/`/f` arm). Without this cap, a
54    /// `string x` rule against an attacker-controlled NUL-free buffer
55    /// could allocate up to the full buffer length -- the CWE-770
56    /// control documented at this field. Default is 8192 bytes.
57    ///
58    /// Does NOT apply to:
59    /// - `TypeKind::PString`: returns `TypeReadError::BufferOverrun`
60    ///   rather than truncating when the length prefix exceeds the
61    ///   remaining buffer (per GOTCHAS S3.8's pstring anchor-clamp
62    ///   invariant).
63    /// - `TypeKind::String16`: bounded by a hardcoded
64    ///   `STRING16_MAX_UNITS = 8192` ceiling at 2 bytes per unit.
65    pub max_string_length: usize,
66
67    /// Stop at first match or continue for all matches
68    ///
69    /// When `true`, evaluation stops after the first matching rule.
70    /// When `false`, all rules are evaluated to find all matches, and on
71    /// a text buffer the text pass also runs after a printing binary pass,
72    /// as `file -k` does (GOTCHAS S13.7).
73    /// Default is `true` for performance.
74    ///
75    /// # Semantics
76    ///
77    /// "First match" refers to the first *top-level* rule that matches.
78    /// Children of the first matching top-level rule are always evaluated
79    /// before the stop check; the stop check applies to subsequent
80    /// top-level rules. In other words, `stop_at_first_match = true` does
81    /// not truncate the child subtree of the matching rule -- it only
82    /// prevents later sibling top-level rules from being evaluated. A
83    /// successful top-level match therefore returns one parent `RuleMatch`
84    /// plus any descendant `RuleMatch` values its children produced.
85    pub stop_at_first_match: bool,
86
87    /// Enable MIME type mapping in results
88    ///
89    /// When `true`, the evaluator will attempt to map file type descriptions
90    /// to standard MIME types. Default is `false`.
91    pub enable_mime_types: bool,
92
93    /// Timeout for evaluation in milliseconds
94    ///
95    /// If set, evaluation will be aborted if it takes longer than this duration.
96    /// `None` means no timeout. Default is `None`.
97    pub timeout_ms: Option<u64>,
98}
99
100impl Default for EvaluationConfig {
101    /// Returns the default evaluation configuration.
102    ///
103    /// # Security
104    ///
105    /// The default configuration has no timeout. When processing untrusted
106    /// input, use [`EvaluationConfig::performance()`] or set `timeout_ms`
107    /// explicitly to prevent denial of service.
108    fn default() -> Self {
109        Self {
110            max_recursion_depth: 20,
111            max_string_length: 8192,
112            stop_at_first_match: true,
113            enable_mime_types: false,
114            timeout_ms: None,
115        }
116    }
117}
118
119impl EvaluationConfig {
120    /// Create a new configuration with default values
121    ///
122    /// # Examples
123    ///
124    /// ```rust
125    /// use libmagic_rs::EvaluationConfig;
126    ///
127    /// let config = EvaluationConfig::new();
128    /// assert_eq!(config.max_recursion_depth, 20);
129    /// assert_eq!(config.max_string_length, 8192);
130    /// assert!(config.stop_at_first_match);
131    /// assert!(!config.enable_mime_types);
132    /// assert_eq!(config.timeout_ms, None);
133    /// ```
134    #[must_use]
135    pub fn new() -> Self {
136        Self::default()
137    }
138
139    /// Create a configuration optimized for performance
140    ///
141    /// This configuration prioritizes speed over completeness:
142    /// - Lower recursion depth limit
143    /// - Smaller string length limit
144    /// - Stop at first match
145    /// - No MIME type mapping
146    /// - Short timeout
147    ///
148    /// # Examples
149    ///
150    /// ```rust
151    /// use libmagic_rs::EvaluationConfig;
152    ///
153    /// let config = EvaluationConfig::performance();
154    /// assert_eq!(config.max_recursion_depth, 10);
155    /// assert_eq!(config.max_string_length, 1024);
156    /// assert!(config.stop_at_first_match);
157    /// assert!(!config.enable_mime_types);
158    /// assert_eq!(config.timeout_ms, Some(1000));
159    /// ```
160    #[must_use]
161    pub const fn performance() -> Self {
162        Self {
163            max_recursion_depth: 10,
164            max_string_length: 1024,
165            stop_at_first_match: true,
166            enable_mime_types: false,
167            timeout_ms: Some(1000), // 1 second
168        }
169    }
170
171    /// Create a configuration optimized for completeness
172    ///
173    /// This configuration prioritizes finding all matches over speed:
174    /// - Higher recursion depth limit
175    /// - Larger string length limit
176    /// - Find all matches
177    /// - Enable MIME type mapping
178    /// - Longer timeout
179    ///
180    /// # Examples
181    ///
182    /// ```rust
183    /// use libmagic_rs::EvaluationConfig;
184    ///
185    /// let config = EvaluationConfig::comprehensive();
186    /// assert_eq!(config.max_recursion_depth, 50);
187    /// assert_eq!(config.max_string_length, 32768);
188    /// assert!(!config.stop_at_first_match);
189    /// assert!(config.enable_mime_types);
190    /// assert_eq!(config.timeout_ms, Some(30000));
191    /// ```
192    #[must_use]
193    pub const fn comprehensive() -> Self {
194        Self {
195            max_recursion_depth: 50,
196            max_string_length: 32768,
197            stop_at_first_match: false,
198            enable_mime_types: true,
199            timeout_ms: Some(30000), // 30 seconds
200        }
201    }
202
203    /// Sets the maximum recursion depth for nested rule evaluation.
204    ///
205    /// Builder-style setter for consumers outside this crate. Direct
206    /// struct-literal construction is blocked by `#[non_exhaustive]`, so
207    /// chain `with_*` calls after one of the factory constructors
208    /// (`default`, `performance`, `comprehensive`, `new`).
209    #[must_use]
210    pub const fn with_max_recursion_depth(mut self, depth: u32) -> Self {
211        self.max_recursion_depth = depth;
212        self
213    }
214
215    /// Sets the maximum string length (in bytes) read for string types.
216    #[must_use]
217    pub const fn with_max_string_length(mut self, length: usize) -> Self {
218        self.max_string_length = length;
219        self
220    }
221
222    /// Sets whether evaluation stops after the first top-level match.
223    #[must_use]
224    pub const fn with_stop_at_first_match(mut self, stop: bool) -> Self {
225        self.stop_at_first_match = stop;
226        self
227    }
228
229    /// Enables or disables MIME type mapping in results.
230    #[must_use]
231    pub const fn with_mime_types(mut self, enable: bool) -> Self {
232        self.enable_mime_types = enable;
233        self
234    }
235
236    /// Sets the evaluation timeout in milliseconds. Pass `None` for
237    /// unbounded evaluation (not recommended on untrusted input).
238    #[must_use]
239    pub const fn with_timeout_ms(mut self, timeout_ms: Option<u64>) -> Self {
240        self.timeout_ms = timeout_ms;
241        self
242    }
243
244    /// Validate the configuration settings
245    ///
246    /// Performs comprehensive security validation of all configuration values
247    /// to prevent malicious configurations that could lead to resource exhaustion,
248    /// denial of service, or other security issues.
249    ///
250    /// # Security
251    ///
252    /// This validation prevents:
253    /// - Stack overflow attacks through excessive recursion depth
254    /// - Memory exhaustion through oversized string limits
255    /// - Denial of service through excessive timeouts
256    /// - Integer overflow in configuration calculations
257    ///
258    /// # Errors
259    ///
260    /// Returns `LibmagicError::ConfigError` if any configuration values
261    /// are invalid or out of reasonable bounds.
262    ///
263    /// # Examples
264    ///
265    /// ```rust
266    /// use libmagic_rs::EvaluationConfig;
267    ///
268    /// let config = EvaluationConfig::default();
269    /// assert!(config.validate().is_ok());
270    ///
271    /// let invalid_config = EvaluationConfig::default().with_max_recursion_depth(0);
272    /// assert!(invalid_config.validate().is_err());
273    /// ```
274    pub fn validate(&self) -> Result<()> {
275        self.validate_recursion_depth()?;
276        self.validate_string_length()?;
277        self.validate_timeout()?;
278        self.validate_resource_combination()?;
279        Ok(())
280    }
281
282    /// Validate recursion depth to prevent stack overflow attacks
283    fn validate_recursion_depth(&self) -> Result<()> {
284        const MAX_SAFE_RECURSION_DEPTH: u32 = 1000;
285
286        if self.max_recursion_depth == 0 {
287            return Err(LibmagicError::ConfigError {
288                reason: "max_recursion_depth must be greater than 0".to_string(),
289            });
290        }
291
292        if self.max_recursion_depth > MAX_SAFE_RECURSION_DEPTH {
293            return Err(LibmagicError::ConfigError {
294                reason: format!(
295                    "max_recursion_depth must not exceed {MAX_SAFE_RECURSION_DEPTH} to prevent stack overflow"
296                ),
297            });
298        }
299
300        Ok(())
301    }
302
303    /// Validate string length to prevent memory exhaustion
304    fn validate_string_length(&self) -> Result<()> {
305        const MAX_SAFE_STRING_LENGTH: usize = 1_048_576; // 1MB
306
307        if self.max_string_length == 0 {
308            return Err(LibmagicError::ConfigError {
309                reason: "max_string_length must be greater than 0".to_string(),
310            });
311        }
312
313        if self.max_string_length > MAX_SAFE_STRING_LENGTH {
314            return Err(LibmagicError::ConfigError {
315                reason: format!(
316                    "max_string_length must not exceed {MAX_SAFE_STRING_LENGTH} bytes to prevent memory exhaustion"
317                ),
318            });
319        }
320
321        Ok(())
322    }
323
324    /// Validate timeout to prevent denial of service
325    fn validate_timeout(&self) -> Result<()> {
326        const MAX_SAFE_TIMEOUT_MS: u64 = 300_000; // 5 minutes
327
328        if let Some(timeout) = self.timeout_ms {
329            if timeout == 0 {
330                return Err(LibmagicError::ConfigError {
331                    reason: "timeout_ms must be greater than 0 if specified".to_string(),
332                });
333            }
334
335            if timeout > MAX_SAFE_TIMEOUT_MS {
336                return Err(LibmagicError::ConfigError {
337                    reason: format!(
338                        "timeout_ms must not exceed {MAX_SAFE_TIMEOUT_MS} (5 minutes) to prevent denial of service"
339                    ),
340                });
341            }
342        }
343
344        Ok(())
345    }
346
347    /// Validate resource combination to prevent resource exhaustion
348    fn validate_resource_combination(&self) -> Result<()> {
349        const HIGH_RECURSION_THRESHOLD: u32 = 100;
350        const LARGE_STRING_THRESHOLD: usize = 65536;
351
352        if self.max_recursion_depth > HIGH_RECURSION_THRESHOLD
353            && self.max_string_length > LARGE_STRING_THRESHOLD
354        {
355            return Err(LibmagicError::ConfigError {
356                reason: format!(
357                    "High recursion depth (>{HIGH_RECURSION_THRESHOLD}) combined with large string length (>{LARGE_STRING_THRESHOLD}) may cause resource exhaustion"
358                ),
359            });
360        }
361
362        Ok(())
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    // ── Presets ──────────────────────────────────────────────────
371
372    #[test]
373    fn test_default_validates() {
374        assert!(EvaluationConfig::default().validate().is_ok());
375    }
376
377    #[test]
378    fn test_performance_validates() {
379        assert!(EvaluationConfig::performance().validate().is_ok());
380    }
381
382    #[test]
383    fn test_comprehensive_validates() {
384        assert!(EvaluationConfig::comprehensive().validate().is_ok());
385    }
386
387    // ── Recursion depth boundaries ──────────────────────────────
388
389    #[test]
390    fn test_recursion_depth_zero_rejected() {
391        let cfg = EvaluationConfig {
392            max_recursion_depth: 0,
393            ..Default::default()
394        };
395        assert!(cfg.validate().is_err());
396    }
397
398    #[test]
399    fn test_recursion_depth_one_accepted() {
400        let cfg = EvaluationConfig {
401            max_recursion_depth: 1,
402            ..Default::default()
403        };
404        assert!(cfg.validate().is_ok());
405    }
406
407    #[test]
408    fn test_recursion_depth_at_max_accepted() {
409        let cfg = EvaluationConfig {
410            max_recursion_depth: 1000,
411            ..Default::default()
412        };
413        assert!(cfg.validate().is_ok());
414    }
415
416    #[test]
417    fn test_recursion_depth_above_max_rejected() {
418        let cfg = EvaluationConfig {
419            max_recursion_depth: 1001,
420            ..Default::default()
421        };
422        assert!(cfg.validate().is_err());
423    }
424
425    // ── String length boundaries ────────────────────────────────
426
427    #[test]
428    fn test_string_length_zero_rejected() {
429        let cfg = EvaluationConfig {
430            max_string_length: 0,
431            ..Default::default()
432        };
433        assert!(cfg.validate().is_err());
434    }
435
436    #[test]
437    fn test_string_length_one_accepted() {
438        let cfg = EvaluationConfig {
439            max_string_length: 1,
440            ..Default::default()
441        };
442        assert!(cfg.validate().is_ok());
443    }
444
445    #[test]
446    fn test_string_length_at_max_accepted() {
447        let cfg = EvaluationConfig {
448            max_string_length: 1_048_576,
449            ..Default::default()
450        };
451        assert!(cfg.validate().is_ok());
452    }
453
454    #[test]
455    fn test_string_length_above_max_rejected() {
456        let cfg = EvaluationConfig {
457            max_string_length: 1_048_577,
458            ..Default::default()
459        };
460        assert!(cfg.validate().is_err());
461    }
462
463    // ── Timeout boundaries ──────────────────────────────────────
464
465    #[test]
466    fn test_timeout_none_accepted() {
467        let cfg = EvaluationConfig {
468            timeout_ms: None,
469            ..Default::default()
470        };
471        assert!(cfg.validate().is_ok());
472    }
473
474    #[test]
475    fn test_timeout_zero_rejected() {
476        let cfg = EvaluationConfig {
477            timeout_ms: Some(0),
478            ..Default::default()
479        };
480        assert!(cfg.validate().is_err());
481    }
482
483    #[test]
484    fn test_timeout_one_accepted() {
485        let cfg = EvaluationConfig {
486            timeout_ms: Some(1),
487            ..Default::default()
488        };
489        assert!(cfg.validate().is_ok());
490    }
491
492    #[test]
493    fn test_timeout_at_max_accepted() {
494        let cfg = EvaluationConfig {
495            timeout_ms: Some(300_000),
496            ..Default::default()
497        };
498        assert!(cfg.validate().is_ok());
499    }
500
501    #[test]
502    fn test_timeout_above_max_rejected() {
503        let cfg = EvaluationConfig {
504            timeout_ms: Some(300_001),
505            ..Default::default()
506        };
507        assert!(cfg.validate().is_err());
508    }
509
510    // ── Resource combination guard ──────────────────────────────
511
512    #[test]
513    fn test_high_recursion_with_large_string_rejected() {
514        let cfg = EvaluationConfig {
515            max_recursion_depth: 101,
516            max_string_length: 65537,
517            ..Default::default()
518        };
519        assert!(cfg.validate().is_err());
520    }
521
522    #[test]
523    fn test_high_recursion_with_normal_string_accepted() {
524        let cfg = EvaluationConfig {
525            max_recursion_depth: 101,
526            max_string_length: 65536,
527            ..Default::default()
528        };
529        assert!(cfg.validate().is_ok());
530    }
531
532    #[test]
533    fn test_normal_recursion_with_large_string_accepted() {
534        let cfg = EvaluationConfig {
535            max_recursion_depth: 100,
536            max_string_length: 65537,
537            ..Default::default()
538        };
539        assert!(cfg.validate().is_ok());
540    }
541
542    // ── evaluate_rules_with_config rejects invalid config ───────
543
544    #[test]
545    fn test_evaluate_rules_with_config_rejects_invalid() {
546        use crate::evaluator::evaluate_rules_with_config;
547
548        let invalid_cfg = EvaluationConfig {
549            max_recursion_depth: 0,
550            ..Default::default()
551        };
552        let result = evaluate_rules_with_config(&[], &[], &invalid_cfg);
553        assert!(result.is_err());
554    }
555}