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