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