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}