Skip to main content

libmagic_rs/output/
mod.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Output formatting module for magic rule evaluation results
5//!
6//! This module provides data structures and functionality for storing and formatting
7//! the results of magic rule evaluation, supporting both text and JSON output formats.
8//!
9//! The module follows a structured approach where evaluation results contain metadata
10//! about the evaluation process and a list of matches found during rule processing.
11
12pub mod json;
13pub mod text;
14
15use serde::{Deserialize, Serialize};
16use std::path::PathBuf;
17
18use std::sync::LazyLock;
19
20use crate::parser::ast::Value;
21
22/// Shared `TagExtractor` instance, initialized once on first use.
23/// Avoids allocating the 16-keyword `HashSet` on every call to
24/// `from_evaluator_match` or `from_library_result`.
25static DEFAULT_TAG_EXTRACTOR: LazyLock<crate::tags::TagExtractor> =
26    LazyLock::new(crate::tags::TagExtractor::new);
27
28/// Result of a single magic rule match
29///
30/// Contains all information about a successful rule match, including the matched
31/// value, its location in the file, and metadata about the rule that matched.
32///
33/// # Examples
34///
35/// ```
36/// use libmagic_rs::output::MatchResult;
37/// use libmagic_rs::parser::ast::Value;
38///
39/// let result = MatchResult {
40///     message: "ELF 64-bit LSB executable".to_string(),
41///     offset: 0,
42///     length: 4,
43///     value: Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46]),
44///     rule_path: vec!["elf".to_string(), "elf64".to_string()],
45///     confidence: 90,
46///     mime_type: Some("application/x-executable".to_string()),
47/// };
48///
49/// assert_eq!(result.message, "ELF 64-bit LSB executable");
50/// assert_eq!(result.offset, 0);
51/// ```
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct MatchResult {
54    /// Human-readable description of the file type or pattern match
55    pub message: String,
56
57    /// Byte offset in the file where the match occurred
58    pub offset: usize,
59
60    /// Number of bytes that were examined for this match
61    pub length: usize,
62
63    /// The actual value that was matched at the specified offset
64    pub value: Value,
65
66    /// Hierarchical path of rule names that led to this match
67    ///
68    /// For nested rules, this contains the sequence of rule identifiers
69    /// from the root rule down to the specific rule that matched.
70    pub rule_path: Vec<String>,
71
72    /// Confidence score for this match (0-100)
73    ///
74    /// Higher values indicate more specific or reliable matches.
75    /// Generic patterns typically have lower confidence scores.
76    pub confidence: u8,
77
78    /// Optional MIME type associated with this match
79    ///
80    /// When available, provides the standard MIME type corresponding
81    /// to the detected file format.
82    pub mime_type: Option<String>,
83}
84
85/// Complete evaluation result for a file
86///
87/// Contains all matches found during rule evaluation, along with metadata
88/// about the evaluation process and the file being analyzed.
89///
90/// # Examples
91///
92/// ```
93/// use libmagic_rs::output::{EvaluationResult, MatchResult, EvaluationMetadata};
94/// use libmagic_rs::parser::ast::Value;
95/// use std::path::PathBuf;
96///
97/// let result = EvaluationResult {
98///     filename: PathBuf::from("example.bin"),
99///     matches: vec![
100///         MatchResult {
101///             message: "ELF executable".to_string(),
102///             offset: 0,
103///             length: 4,
104///             value: Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46]),
105///             rule_path: vec!["elf".to_string()],
106///             confidence: 95,
107///             mime_type: Some("application/x-executable".to_string()),
108///         }
109///     ],
110///     metadata: EvaluationMetadata {
111///         file_size: 8192,
112///         evaluation_time_ms: 2.5,
113///         rules_evaluated: 42,
114///         rules_matched: 1,
115///     },
116///     error: None,
117/// };
118///
119/// assert_eq!(result.matches.len(), 1);
120/// assert_eq!(result.metadata.file_size, 8192);
121/// ```
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct EvaluationResult {
124    /// Path to the file that was analyzed
125    pub filename: PathBuf,
126
127    /// All successful rule matches found during evaluation
128    ///
129    /// Matches are typically ordered by offset, then by confidence score.
130    /// The first match is often considered the primary file type.
131    pub matches: Vec<MatchResult>,
132
133    /// Metadata about the evaluation process
134    pub metadata: EvaluationMetadata,
135
136    /// Error that occurred during evaluation, if any
137    ///
138    /// When present, indicates that evaluation was incomplete or failed.
139    /// Partial results may still be available in the matches vector.
140    pub error: Option<String>,
141}
142
143/// Metadata about the evaluation process
144///
145/// Provides diagnostic information about how the evaluation was performed,
146/// including performance metrics and statistics about rule processing.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct EvaluationMetadata {
149    /// Size of the analyzed file in bytes
150    pub file_size: u64,
151
152    /// Time taken for evaluation in milliseconds
153    pub evaluation_time_ms: f64,
154
155    /// Total number of rules that were evaluated
156    ///
157    /// This includes rules that were tested but did not match.
158    pub rules_evaluated: u32,
159
160    /// Number of rules that successfully matched
161    pub rules_matched: u32,
162}
163
164impl MatchResult {
165    /// Create a new match result with basic information
166    ///
167    /// # Arguments
168    ///
169    /// * `message` - Human-readable description of the match
170    /// * `offset` - Byte offset where the match occurred
171    /// * `value` - The matched value
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use libmagic_rs::output::MatchResult;
177    /// use libmagic_rs::parser::ast::Value;
178    ///
179    /// let result = MatchResult::new(
180    ///     "PNG image".to_string(),
181    ///     0,
182    ///     Value::Bytes(vec![0x89, 0x50, 0x4e, 0x47])
183    /// );
184    ///
185    /// assert_eq!(result.message, "PNG image");
186    /// assert_eq!(result.offset, 0);
187    /// assert_eq!(result.confidence, 50); // Default confidence
188    /// ```
189    #[must_use]
190    pub fn new(message: String, offset: usize, value: Value) -> Self {
191        Self {
192            message,
193            offset,
194            length: match &value {
195                Value::Bytes(bytes) => bytes.len(),
196                Value::String(s) => s.len(),
197                Value::Uint(_) | Value::Int(_) => std::mem::size_of::<u64>(),
198            },
199            value,
200            rule_path: Vec::new(),
201            confidence: 50, // Default moderate confidence
202            mime_type: None,
203        }
204    }
205
206    /// Create a new match result with full metadata
207    ///
208    /// # Arguments
209    ///
210    /// * `message` - Human-readable description of the match
211    /// * `offset` - Byte offset where the match occurred
212    /// * `length` - Number of bytes examined
213    /// * `value` - The matched value
214    /// * `rule_path` - Hierarchical path of rules that led to this match
215    /// * `confidence` - Confidence score (0-100)
216    /// * `mime_type` - Optional MIME type
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// use libmagic_rs::output::MatchResult;
222    /// use libmagic_rs::parser::ast::Value;
223    ///
224    /// let result = MatchResult::with_metadata(
225    ///     "JPEG image".to_string(),
226    ///     0,
227    ///     2,
228    ///     Value::Bytes(vec![0xff, 0xd8]),
229    ///     vec!["image".to_string(), "jpeg".to_string()],
230    ///     85,
231    ///     Some("image/jpeg".to_string())
232    /// );
233    ///
234    /// assert_eq!(result.rule_path.len(), 2);
235    /// assert_eq!(result.confidence, 85);
236    /// assert_eq!(result.mime_type, Some("image/jpeg".to_string()));
237    /// ```
238    #[must_use]
239    pub fn with_metadata(
240        message: String,
241        offset: usize,
242        length: usize,
243        value: Value,
244        rule_path: Vec<String>,
245        confidence: u8,
246        mime_type: Option<String>,
247    ) -> Self {
248        Self {
249            message,
250            offset,
251            length,
252            value,
253            rule_path,
254            confidence: confidence.min(100), // Clamp to valid range
255            mime_type,
256        }
257    }
258
259    /// Convert from an evaluator `MatchResult` to an output `MatchResult`
260    ///
261    /// This adapts the internal evaluation result format to the richer output format
262    /// used for JSON and structured output. It extracts rule paths from match messages
263    /// and converts confidence from 0.0-1.0 to 0-100 scale.
264    ///
265    /// # Arguments
266    ///
267    /// * `m` - The evaluator match result to convert
268    /// * `mime_type` - Optional MIME type to associate with this match
269    #[must_use]
270    pub fn from_evaluator_match(
271        m: &crate::evaluator::MatchResult,
272        mime_type: Option<&str>,
273    ) -> Self {
274        let rule_path =
275            DEFAULT_TAG_EXTRACTOR.extract_rule_path(std::iter::once(m.message.as_str()));
276
277        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
278        let confidence = (m.confidence * 100.0).min(100.0) as u8;
279
280        let length = match &m.value {
281            Value::Bytes(b) => b.len(),
282            Value::String(s) => s.len(),
283            Value::Uint(_) | Value::Int(_) => 4,
284        };
285
286        Self::with_metadata(
287            m.message.clone(),
288            m.offset,
289            length,
290            m.value.clone(),
291            rule_path,
292            confidence,
293            mime_type.map(String::from),
294        )
295    }
296
297    /// Set the confidence score for this match
298    ///
299    /// The confidence score is automatically clamped to the range 0-100.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use libmagic_rs::output::MatchResult;
305    /// use libmagic_rs::parser::ast::Value;
306    ///
307    /// let mut result = MatchResult::new(
308    ///     "Text file".to_string(),
309    ///     0,
310    ///     Value::String("Hello".to_string())
311    /// );
312    ///
313    /// result.set_confidence(75);
314    /// assert_eq!(result.confidence, 75);
315    ///
316    /// // Values over 100 are clamped
317    /// result.set_confidence(150);
318    /// assert_eq!(result.confidence, 100);
319    /// ```
320    pub fn set_confidence(&mut self, confidence: u8) {
321        self.confidence = confidence.min(100);
322    }
323
324    /// Add a rule name to the rule path
325    ///
326    /// This is typically used during evaluation to build up the hierarchical
327    /// path of rules that led to a match.
328    ///
329    /// # Examples
330    ///
331    /// ```
332    /// use libmagic_rs::output::MatchResult;
333    /// use libmagic_rs::parser::ast::Value;
334    ///
335    /// let mut result = MatchResult::new(
336    ///     "Archive".to_string(),
337    ///     0,
338    ///     Value::String("PK".to_string())
339    /// );
340    ///
341    /// result.add_rule_path("archive".to_string());
342    /// result.add_rule_path("zip".to_string());
343    ///
344    /// assert_eq!(result.rule_path, vec!["archive", "zip"]);
345    /// ```
346    pub fn add_rule_path(&mut self, rule_name: String) {
347        self.rule_path.push(rule_name);
348    }
349
350    /// Set the MIME type for this match
351    ///
352    /// # Examples
353    ///
354    /// ```
355    /// use libmagic_rs::output::MatchResult;
356    /// use libmagic_rs::parser::ast::Value;
357    ///
358    /// let mut result = MatchResult::new(
359    ///     "PDF document".to_string(),
360    ///     0,
361    ///     Value::String("%PDF".to_string())
362    /// );
363    ///
364    /// result.set_mime_type(Some("application/pdf".to_string()));
365    /// assert_eq!(result.mime_type, Some("application/pdf".to_string()));
366    /// ```
367    pub fn set_mime_type(&mut self, mime_type: Option<String>) {
368        self.mime_type = mime_type;
369    }
370}
371
372impl EvaluationResult {
373    /// Create a new evaluation result
374    ///
375    /// # Arguments
376    ///
377    /// * `filename` - Path to the analyzed file
378    /// * `matches` - Vector of successful matches
379    /// * `metadata` - Evaluation metadata
380    ///
381    /// # Examples
382    ///
383    /// ```
384    /// use libmagic_rs::output::{EvaluationResult, EvaluationMetadata};
385    /// use std::path::PathBuf;
386    ///
387    /// let result = EvaluationResult::new(
388    ///     PathBuf::from("test.txt"),
389    ///     vec![],
390    ///     EvaluationMetadata {
391    ///         file_size: 1024,
392    ///         evaluation_time_ms: 1.2,
393    ///         rules_evaluated: 10,
394    ///         rules_matched: 0,
395    ///     }
396    /// );
397    ///
398    /// assert_eq!(result.filename, PathBuf::from("test.txt"));
399    /// assert!(result.matches.is_empty());
400    /// assert!(result.error.is_none());
401    /// ```
402    #[must_use]
403    pub fn new(filename: PathBuf, matches: Vec<MatchResult>, metadata: EvaluationMetadata) -> Self {
404        Self {
405            filename,
406            matches,
407            metadata,
408            error: None,
409        }
410    }
411
412    /// Convert from a library `EvaluationResult` to an output `EvaluationResult`
413    ///
414    /// This adapts the library's evaluation result into the output format used for
415    /// JSON and structured output. Converts all matches and metadata, and enriches
416    /// the first match's rule path with tags extracted from the overall description.
417    ///
418    /// # Arguments
419    ///
420    /// * `result` - The library evaluation result to convert
421    /// * `filename` - Path to the file that was evaluated
422    #[must_use]
423    pub fn from_library_result(
424        result: &crate::EvaluationResult,
425        filename: &std::path::Path,
426    ) -> Self {
427        let mut output_matches: Vec<MatchResult> = result
428            .matches
429            .iter()
430            .map(|m| MatchResult::from_evaluator_match(m, result.mime_type.as_deref()))
431            .collect();
432
433        // Enrich the first match with tags from the overall description
434        if let Some(first) = output_matches.first_mut() {
435            if first.rule_path.is_empty() {
436                first.rule_path = DEFAULT_TAG_EXTRACTOR.extract_tags(&result.description);
437            }
438        }
439
440        #[allow(clippy::cast_possible_truncation)]
441        let rules_evaluated = result.metadata.rules_evaluated as u32;
442        #[allow(clippy::cast_possible_truncation)]
443        let rules_matched = output_matches.len() as u32;
444
445        Self::new(
446            filename.to_path_buf(),
447            output_matches,
448            EvaluationMetadata::new(
449                result.metadata.file_size,
450                result.metadata.evaluation_time_ms,
451                rules_evaluated,
452                rules_matched,
453            ),
454        )
455    }
456
457    /// Create an evaluation result with an error
458    ///
459    /// # Arguments
460    ///
461    /// * `filename` - Path to the analyzed file
462    /// * `error` - Error message describing what went wrong
463    /// * `metadata` - Evaluation metadata (may be partial)
464    ///
465    /// # Examples
466    ///
467    /// ```
468    /// use libmagic_rs::output::{EvaluationResult, EvaluationMetadata};
469    /// use std::path::PathBuf;
470    ///
471    /// let result = EvaluationResult::with_error(
472    ///     PathBuf::from("missing.txt"),
473    ///     "File not found".to_string(),
474    ///     EvaluationMetadata {
475    ///         file_size: 0,
476    ///         evaluation_time_ms: 0.0,
477    ///         rules_evaluated: 0,
478    ///         rules_matched: 0,
479    ///     }
480    /// );
481    ///
482    /// assert_eq!(result.error, Some("File not found".to_string()));
483    /// assert!(result.matches.is_empty());
484    /// ```
485    #[must_use]
486    pub fn with_error(filename: PathBuf, error: String, metadata: EvaluationMetadata) -> Self {
487        Self {
488            filename,
489            matches: Vec::new(),
490            metadata,
491            error: Some(error),
492        }
493    }
494
495    /// Add a match result to this evaluation
496    ///
497    /// # Examples
498    ///
499    /// ```
500    /// use libmagic_rs::output::{EvaluationResult, MatchResult, EvaluationMetadata};
501    /// use libmagic_rs::parser::ast::Value;
502    /// use std::path::PathBuf;
503    ///
504    /// let mut result = EvaluationResult::new(
505    ///     PathBuf::from("data.bin"),
506    ///     vec![],
507    ///     EvaluationMetadata {
508    ///         file_size: 512,
509    ///         evaluation_time_ms: 0.8,
510    ///         rules_evaluated: 5,
511    ///         rules_matched: 0,
512    ///     }
513    /// );
514    ///
515    /// let match_result = MatchResult::new(
516    ///     "Binary data".to_string(),
517    ///     0,
518    ///     Value::Bytes(vec![0x00, 0x01, 0x02])
519    /// );
520    ///
521    /// result.add_match(match_result);
522    /// assert_eq!(result.matches.len(), 1);
523    /// ```
524    pub fn add_match(&mut self, match_result: MatchResult) {
525        #[cfg(debug_assertions)]
526        Self::validate_match_result(&match_result);
527
528        self.matches.push(match_result);
529    }
530
531    /// Validate a match result before adding it
532    #[cfg(debug_assertions)]
533    fn validate_match_result(match_result: &MatchResult) {
534        // Validate confidence score range
535        if match_result.confidence > 100 {
536            eprintln!(
537                "Warning: Match result has confidence score > 100: {}",
538                match_result.confidence
539            );
540        }
541    }
542
543    /// Get the primary match (first match with highest confidence)
544    ///
545    /// Returns the match that is most likely to represent the primary file type.
546    /// This is typically the first match, but if multiple matches exist, the one
547    /// with the highest confidence score is preferred.
548    ///
549    /// # Examples
550    ///
551    /// ```
552    /// use libmagic_rs::output::{EvaluationResult, MatchResult, EvaluationMetadata};
553    /// use libmagic_rs::parser::ast::Value;
554    /// use std::path::PathBuf;
555    ///
556    /// let mut result = EvaluationResult::new(
557    ///     PathBuf::from("test.exe"),
558    ///     vec![
559    ///         MatchResult::with_metadata(
560    ///             "Executable".to_string(),
561    ///             0, 2,
562    ///             Value::String("MZ".to_string()),
563    ///             vec!["pe".to_string()],
564    ///             60,
565    ///             None
566    ///         ),
567    ///         MatchResult::with_metadata(
568    ///             "PE32 executable".to_string(),
569    ///             60, 4,
570    ///             Value::String("PE\0\0".to_string()),
571    ///             vec!["pe".to_string(), "pe32".to_string()],
572    ///             90,
573    ///             Some("application/x-msdownload".to_string())
574    ///         ),
575    ///     ],
576    ///     EvaluationMetadata {
577    ///         file_size: 4096,
578    ///         evaluation_time_ms: 1.5,
579    ///         rules_evaluated: 15,
580    ///         rules_matched: 2,
581    ///     }
582    /// );
583    ///
584    /// let primary = result.primary_match();
585    /// assert!(primary.is_some());
586    /// assert_eq!(primary.unwrap().confidence, 90);
587    /// ```
588    #[must_use]
589    pub fn primary_match(&self) -> Option<&MatchResult> {
590        self.matches
591            .iter()
592            .max_by_key(|match_result| match_result.confidence)
593    }
594
595    /// Check if the evaluation was successful (no errors)
596    ///
597    /// # Examples
598    ///
599    /// ```
600    /// use libmagic_rs::output::{EvaluationResult, EvaluationMetadata};
601    /// use std::path::PathBuf;
602    ///
603    /// let success = EvaluationResult::new(
604    ///     PathBuf::from("good.txt"),
605    ///     vec![],
606    ///     EvaluationMetadata {
607    ///         file_size: 100,
608    ///         evaluation_time_ms: 0.5,
609    ///         rules_evaluated: 3,
610    ///         rules_matched: 0,
611    ///     }
612    /// );
613    ///
614    /// let failure = EvaluationResult::with_error(
615    ///     PathBuf::from("bad.txt"),
616    ///     "Parse error".to_string(),
617    ///     EvaluationMetadata {
618    ///         file_size: 0,
619    ///         evaluation_time_ms: 0.0,
620    ///         rules_evaluated: 0,
621    ///         rules_matched: 0,
622    ///     }
623    /// );
624    ///
625    /// assert!(success.is_success());
626    /// assert!(!failure.is_success());
627    /// ```
628    #[must_use]
629    pub fn is_success(&self) -> bool {
630        self.error.is_none()
631    }
632}
633
634impl EvaluationMetadata {
635    /// Create new evaluation metadata
636    ///
637    /// # Arguments
638    ///
639    /// * `file_size` - Size of the analyzed file in bytes
640    /// * `evaluation_time_ms` - Time taken for evaluation in milliseconds
641    /// * `rules_evaluated` - Number of rules that were tested
642    /// * `rules_matched` - Number of rules that matched
643    ///
644    /// # Examples
645    ///
646    /// ```
647    /// use libmagic_rs::output::EvaluationMetadata;
648    ///
649    /// let metadata = EvaluationMetadata::new(2048, 3.7, 25, 3);
650    ///
651    /// assert_eq!(metadata.file_size, 2048);
652    /// assert_eq!(metadata.evaluation_time_ms, 3.7);
653    /// assert_eq!(metadata.rules_evaluated, 25);
654    /// assert_eq!(metadata.rules_matched, 3);
655    /// ```
656    #[must_use]
657    pub fn new(
658        file_size: u64,
659        evaluation_time_ms: f64,
660        rules_evaluated: u32,
661        rules_matched: u32,
662    ) -> Self {
663        Self {
664            file_size,
665            evaluation_time_ms,
666            rules_evaluated,
667            rules_matched,
668        }
669    }
670
671    /// Get the match rate as a percentage
672    ///
673    /// Returns the percentage of evaluated rules that resulted in matches.
674    ///
675    /// # Examples
676    ///
677    /// ```
678    /// use libmagic_rs::output::EvaluationMetadata;
679    ///
680    /// let metadata = EvaluationMetadata::new(1024, 1.0, 20, 5);
681    /// assert_eq!(metadata.match_rate(), 25.0);
682    ///
683    /// let no_rules = EvaluationMetadata::new(1024, 1.0, 0, 0);
684    /// assert_eq!(no_rules.match_rate(), 0.0);
685    /// ```
686    #[must_use]
687    pub fn match_rate(&self) -> f64 {
688        if self.rules_evaluated == 0 {
689            0.0
690        } else {
691            (f64::from(self.rules_matched) / f64::from(self.rules_evaluated)) * 100.0
692        }
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699
700    #[test]
701    fn test_match_result_new() {
702        let result = MatchResult::new(
703            "Test file".to_string(),
704            42,
705            Value::String("test".to_string()),
706        );
707
708        assert_eq!(result.message, "Test file");
709        assert_eq!(result.offset, 42);
710        assert_eq!(result.length, 4); // Length of "test"
711        assert_eq!(result.value, Value::String("test".to_string()));
712        assert!(result.rule_path.is_empty());
713        assert_eq!(result.confidence, 50);
714        assert!(result.mime_type.is_none());
715    }
716
717    #[test]
718    fn test_match_result_with_metadata() {
719        let result = MatchResult::with_metadata(
720            "ELF executable".to_string(),
721            0,
722            4,
723            Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46]),
724            vec!["elf".to_string()],
725            95,
726            Some("application/x-executable".to_string()),
727        );
728
729        assert_eq!(result.message, "ELF executable");
730        assert_eq!(result.offset, 0);
731        assert_eq!(result.length, 4);
732        assert_eq!(result.rule_path, vec!["elf"]);
733        assert_eq!(result.confidence, 95);
734        assert_eq!(
735            result.mime_type,
736            Some("application/x-executable".to_string())
737        );
738    }
739
740    #[test]
741    fn test_match_result_length_calculation() {
742        // Test length calculation for different value types
743        let bytes_result = MatchResult::new("Bytes".to_string(), 0, Value::Bytes(vec![1, 2, 3]));
744        assert_eq!(bytes_result.length, 3);
745
746        let string_result =
747            MatchResult::new("String".to_string(), 0, Value::String("hello".to_string()));
748        assert_eq!(string_result.length, 5);
749
750        let uint_result = MatchResult::new("Uint".to_string(), 0, Value::Uint(42));
751        assert_eq!(uint_result.length, 8); // size_of::<u64>()
752
753        let int_result = MatchResult::new("Int".to_string(), 0, Value::Int(-42));
754        assert_eq!(int_result.length, 8); // size_of::<u64>()
755    }
756
757    #[test]
758    fn test_match_result_set_confidence() {
759        let mut result = MatchResult::new("Test".to_string(), 0, Value::Uint(0));
760
761        result.set_confidence(75);
762        assert_eq!(result.confidence, 75);
763
764        // Test clamping to 100
765        result.set_confidence(150);
766        assert_eq!(result.confidence, 100);
767
768        result.set_confidence(0);
769        assert_eq!(result.confidence, 0);
770    }
771
772    #[test]
773    fn test_match_result_confidence_clamping_in_constructor() {
774        let result = MatchResult::with_metadata(
775            "Test".to_string(),
776            0,
777            1,
778            Value::Uint(0),
779            vec![],
780            200, // Over 100
781            None,
782        );
783
784        assert_eq!(result.confidence, 100);
785    }
786
787    #[test]
788    fn test_match_result_add_rule_path() {
789        let mut result = MatchResult::new("Test".to_string(), 0, Value::Uint(0));
790
791        result.add_rule_path("root".to_string());
792        result.add_rule_path("child".to_string());
793        result.add_rule_path("grandchild".to_string());
794
795        assert_eq!(result.rule_path, vec!["root", "child", "grandchild"]);
796    }
797
798    #[test]
799    fn test_match_result_set_mime_type() {
800        let mut result = MatchResult::new("Test".to_string(), 0, Value::Uint(0));
801
802        result.set_mime_type(Some("text/plain".to_string()));
803        assert_eq!(result.mime_type, Some("text/plain".to_string()));
804
805        result.set_mime_type(None);
806        assert!(result.mime_type.is_none());
807    }
808
809    #[test]
810    fn test_match_result_serialization() {
811        let result = MatchResult::with_metadata(
812            "PNG image".to_string(),
813            0,
814            8,
815            Value::Bytes(vec![0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
816            vec!["image".to_string(), "png".to_string()],
817            90,
818            Some("image/png".to_string()),
819        );
820
821        let json = serde_json::to_string(&result).expect("Failed to serialize MatchResult");
822        let deserialized: MatchResult =
823            serde_json::from_str(&json).expect("Failed to deserialize MatchResult");
824
825        assert_eq!(result, deserialized);
826    }
827
828    #[test]
829    fn test_evaluation_result_new() {
830        let metadata = EvaluationMetadata::new(1024, 2.5, 10, 2);
831        let result = EvaluationResult::new(PathBuf::from("test.bin"), vec![], metadata);
832
833        assert_eq!(result.filename, PathBuf::from("test.bin"));
834        assert!(result.matches.is_empty());
835        assert!(result.error.is_none());
836        assert_eq!(result.metadata.file_size, 1024);
837    }
838
839    #[test]
840    fn test_evaluation_result_with_error() {
841        let metadata = EvaluationMetadata::new(0, 0.0, 0, 0);
842        let result = EvaluationResult::with_error(
843            PathBuf::from("missing.txt"),
844            "File not found".to_string(),
845            metadata,
846        );
847
848        assert_eq!(result.error, Some("File not found".to_string()));
849        assert!(result.matches.is_empty());
850        assert!(!result.is_success());
851    }
852
853    #[test]
854    fn test_evaluation_result_add_match() {
855        let metadata = EvaluationMetadata::new(512, 1.0, 5, 0);
856        let mut result = EvaluationResult::new(PathBuf::from("data.bin"), vec![], metadata);
857
858        let match_result =
859            MatchResult::new("Binary data".to_string(), 0, Value::Bytes(vec![0x00, 0x01]));
860
861        result.add_match(match_result);
862        assert_eq!(result.matches.len(), 1);
863        assert_eq!(result.matches[0].message, "Binary data");
864    }
865
866    #[test]
867    fn test_evaluation_result_primary_match() {
868        let metadata = EvaluationMetadata::new(2048, 3.0, 20, 3);
869        let matches = vec![
870            MatchResult::with_metadata(
871                "Low confidence".to_string(),
872                0,
873                2,
874                Value::String("AB".to_string()),
875                vec![],
876                30,
877                None,
878            ),
879            MatchResult::with_metadata(
880                "High confidence".to_string(),
881                10,
882                4,
883                Value::String("TEST".to_string()),
884                vec![],
885                95,
886                None,
887            ),
888            MatchResult::with_metadata(
889                "Medium confidence".to_string(),
890                20,
891                3,
892                Value::String("XYZ".to_string()),
893                vec![],
894                60,
895                None,
896            ),
897        ];
898
899        let result = EvaluationResult::new(PathBuf::from("test.dat"), matches, metadata);
900
901        let primary = result.primary_match();
902        assert!(primary.is_some());
903        assert_eq!(primary.unwrap().message, "High confidence");
904        assert_eq!(primary.unwrap().confidence, 95);
905    }
906
907    #[test]
908    fn test_evaluation_result_primary_match_empty() {
909        let metadata = EvaluationMetadata::new(0, 0.0, 0, 0);
910        let result = EvaluationResult::new(PathBuf::from("empty.txt"), vec![], metadata);
911
912        assert!(result.primary_match().is_none());
913    }
914
915    #[test]
916    fn test_evaluation_result_is_success() {
917        let metadata = EvaluationMetadata::new(100, 0.5, 3, 1);
918
919        let success = EvaluationResult::new(PathBuf::from("good.txt"), vec![], metadata.clone());
920
921        let failure = EvaluationResult::with_error(
922            PathBuf::from("bad.txt"),
923            "Error occurred".to_string(),
924            metadata,
925        );
926
927        assert!(success.is_success());
928        assert!(!failure.is_success());
929    }
930
931    #[test]
932    fn test_evaluation_result_serialization() {
933        let match_result = MatchResult::new(
934            "Text file".to_string(),
935            0,
936            Value::String("Hello".to_string()),
937        );
938
939        let metadata = EvaluationMetadata::new(1024, 1.5, 8, 1);
940        let result =
941            EvaluationResult::new(PathBuf::from("hello.txt"), vec![match_result], metadata);
942
943        let json = serde_json::to_string(&result).expect("Failed to serialize EvaluationResult");
944        let deserialized: EvaluationResult =
945            serde_json::from_str(&json).expect("Failed to deserialize EvaluationResult");
946
947        assert_eq!(result.filename, deserialized.filename);
948        assert_eq!(result.matches.len(), deserialized.matches.len());
949        assert_eq!(result.metadata.file_size, deserialized.metadata.file_size);
950    }
951
952    #[test]
953    fn test_evaluation_metadata_new() {
954        let metadata = EvaluationMetadata::new(4096, 5.2, 50, 8);
955
956        assert_eq!(metadata.file_size, 4096);
957        assert!((metadata.evaluation_time_ms - 5.2).abs() < f64::EPSILON);
958        assert_eq!(metadata.rules_evaluated, 50);
959        assert_eq!(metadata.rules_matched, 8);
960    }
961
962    #[test]
963    fn test_evaluation_metadata_match_rate() {
964        let metadata = EvaluationMetadata::new(1024, 1.0, 20, 5);
965        assert!((metadata.match_rate() - 25.0).abs() < f64::EPSILON);
966
967        let perfect_match = EvaluationMetadata::new(1024, 1.0, 10, 10);
968        assert!((perfect_match.match_rate() - 100.0).abs() < f64::EPSILON);
969
970        let no_matches = EvaluationMetadata::new(1024, 1.0, 15, 0);
971        assert!((no_matches.match_rate() - 0.0).abs() < f64::EPSILON);
972
973        let no_rules = EvaluationMetadata::new(1024, 1.0, 0, 0);
974        assert!((no_rules.match_rate() - 0.0).abs() < f64::EPSILON);
975    }
976
977    #[test]
978    fn test_evaluation_metadata_serialization() {
979        let metadata = EvaluationMetadata::new(2048, 3.7, 25, 4);
980
981        let json =
982            serde_json::to_string(&metadata).expect("Failed to serialize EvaluationMetadata");
983        let deserialized: EvaluationMetadata =
984            serde_json::from_str(&json).expect("Failed to deserialize EvaluationMetadata");
985
986        assert_eq!(metadata.file_size, deserialized.file_size);
987        assert!(
988            (metadata.evaluation_time_ms - deserialized.evaluation_time_ms).abs() < f64::EPSILON
989        );
990        assert_eq!(metadata.rules_evaluated, deserialized.rules_evaluated);
991        assert_eq!(metadata.rules_matched, deserialized.rules_matched);
992    }
993
994    #[test]
995    fn test_match_result_equality() {
996        let result1 = MatchResult::new("Test".to_string(), 0, Value::Uint(42));
997
998        let result2 = MatchResult::new("Test".to_string(), 0, Value::Uint(42));
999
1000        let result3 = MatchResult::new("Different".to_string(), 0, Value::Uint(42));
1001
1002        assert_eq!(result1, result2);
1003        assert_ne!(result1, result3);
1004    }
1005
1006    #[test]
1007    fn test_complex_evaluation_result() {
1008        // Test a complex scenario with multiple matches and full metadata
1009        let matches = vec![
1010            MatchResult::with_metadata(
1011                "ELF 64-bit LSB executable".to_string(),
1012                0,
1013                4,
1014                Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46]),
1015                vec!["elf".to_string(), "elf64".to_string()],
1016                95,
1017                Some("application/x-executable".to_string()),
1018            ),
1019            MatchResult::with_metadata(
1020                "x86-64 architecture".to_string(),
1021                18,
1022                2,
1023                Value::Uint(0x3e),
1024                vec!["elf".to_string(), "elf64".to_string(), "x86_64".to_string()],
1025                85,
1026                None,
1027            ),
1028            MatchResult::with_metadata(
1029                "dynamically linked".to_string(),
1030                16,
1031                2,
1032                Value::Uint(0x02),
1033                vec![
1034                    "elf".to_string(),
1035                    "elf64".to_string(),
1036                    "dynamic".to_string(),
1037                ],
1038                80,
1039                None,
1040            ),
1041        ];
1042
1043        let metadata = EvaluationMetadata::new(8192, 4.2, 35, 3);
1044        let result = EvaluationResult::new(PathBuf::from("/usr/bin/ls"), matches, metadata);
1045
1046        assert_eq!(result.matches.len(), 3);
1047        let expected_rate = (3.0 / 35.0) * 100.0;
1048        assert!((result.metadata.match_rate() - expected_rate).abs() < f64::EPSILON);
1049
1050        let primary = result.primary_match().unwrap();
1051        assert_eq!(primary.message, "ELF 64-bit LSB executable");
1052        assert_eq!(primary.confidence, 95);
1053        assert_eq!(
1054            primary.mime_type,
1055            Some("application/x-executable".to_string())
1056        );
1057
1058        // Verify all matches have proper rule paths
1059        for match_result in &result.matches {
1060            assert!(!match_result.rule_path.is_empty());
1061            assert!(match_result.rule_path[0] == "elf");
1062        }
1063    }
1064}