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