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