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