use super::*;
#[test]
fn test_evaluation_config_default() {
let config = EvaluationConfig::default();
assert_eq!(config.max_recursion_depth, 20);
assert_eq!(config.max_string_length, 8192);
assert!(config.stop_at_first_match);
assert!(!config.enable_mime_types);
assert_eq!(config.timeout_ms, None);
}
#[test]
fn test_evaluation_config_new() {
let config = EvaluationConfig::new();
let default_config = EvaluationConfig::default();
assert_eq!(config, default_config);
}
#[test]
fn test_evaluation_config_performance() {
let config = EvaluationConfig::performance();
assert_eq!(config.max_recursion_depth, 10);
assert_eq!(config.max_string_length, 1024);
assert!(config.stop_at_first_match);
assert!(!config.enable_mime_types);
assert_eq!(config.timeout_ms, Some(1000));
}
#[test]
fn test_evaluation_config_comprehensive() {
let config = EvaluationConfig::comprehensive();
assert_eq!(config.max_recursion_depth, 50);
assert_eq!(config.max_string_length, 32768);
assert!(!config.stop_at_first_match);
assert!(config.enable_mime_types);
assert_eq!(config.timeout_ms, Some(30000));
}
#[test]
fn test_evaluation_config_validate_valid() {
let config = EvaluationConfig::default();
assert!(config.validate().is_ok());
let performance_config = EvaluationConfig::performance();
assert!(performance_config.validate().is_ok());
let comprehensive_config = EvaluationConfig::comprehensive();
assert!(comprehensive_config.validate().is_ok());
}
#[test]
fn test_evaluation_config_validate_zero_recursion_depth() {
let config = EvaluationConfig {
max_recursion_depth: 0,
..Default::default()
};
let result = config.validate();
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::ConfigError { reason: message } => {
assert!(message.contains("max_recursion_depth must be greater than 0"));
}
_ => panic!("Expected ConfigError"),
}
}
#[test]
fn test_evaluation_config_validate_excessive_recursion_depth() {
let config = EvaluationConfig {
max_recursion_depth: 1001,
..Default::default()
};
let result = config.validate();
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::ConfigError { reason: message } => {
assert!(message.contains("max_recursion_depth must not exceed 1000"));
}
_ => panic!("Expected ConfigError"),
}
}
#[test]
fn test_evaluation_config_validate_zero_string_length() {
let config = EvaluationConfig {
max_string_length: 0,
..Default::default()
};
let result = config.validate();
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::ConfigError { reason: message } => {
assert!(message.contains("max_string_length must be greater than 0"));
}
_ => panic!("Expected ConfigError"),
}
}
#[test]
fn test_evaluation_config_validate_excessive_string_length() {
let config = EvaluationConfig {
max_string_length: 1_048_577, ..Default::default()
};
let result = config.validate();
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::ConfigError { reason: message } => {
assert!(message.contains("max_string_length must not exceed"));
assert!(message.contains("bytes to prevent memory exhaustion"));
}
_ => panic!("Expected ConfigError"),
}
}
#[test]
fn test_evaluation_config_validate_zero_timeout() {
let config = EvaluationConfig {
timeout_ms: Some(0),
..Default::default()
};
let result = config.validate();
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::ConfigError { reason: message } => {
assert!(message.contains("timeout_ms must be greater than 0 if specified"));
}
_ => panic!("Expected ConfigError"),
}
}
#[test]
fn test_evaluation_config_validate_excessive_timeout() {
let config = EvaluationConfig {
timeout_ms: Some(300_001), ..Default::default()
};
let result = config.validate();
assert!(result.is_err());
match result.unwrap_err() {
LibmagicError::ConfigError { reason: message } => {
assert!(message.contains("timeout_ms must not exceed 300000"));
}
_ => panic!("Expected ConfigError"),
}
}
#[test]
fn test_evaluation_config_validate_boundary_values() {
let min_config = EvaluationConfig {
max_recursion_depth: 1,
max_string_length: 1,
timeout_ms: Some(1),
..Default::default()
};
assert!(min_config.validate().is_ok());
let max_config = EvaluationConfig {
max_recursion_depth: 100, max_string_length: 1_048_576, timeout_ms: Some(300_000), ..Default::default()
};
assert!(max_config.validate().is_ok());
let max_recursion_config = EvaluationConfig {
max_recursion_depth: 1000,
max_string_length: 65536, timeout_ms: Some(300_000),
..Default::default()
};
assert!(max_recursion_config.validate().is_ok());
}
#[test]
fn test_evaluation_config_clone() {
let config = EvaluationConfig {
max_recursion_depth: 15,
max_string_length: 4096,
stop_at_first_match: false,
enable_mime_types: true,
timeout_ms: Some(5000),
};
let cloned_config = config.clone();
assert_eq!(config, cloned_config);
}
#[test]
fn test_evaluation_config_debug() {
let config = EvaluationConfig::default();
let debug_str = format!("{config:?}");
assert!(debug_str.contains("EvaluationConfig"));
assert!(debug_str.contains("max_recursion_depth"));
assert!(debug_str.contains("max_string_length"));
assert!(debug_str.contains("stop_at_first_match"));
assert!(debug_str.contains("enable_mime_types"));
assert!(debug_str.contains("timeout_ms"));
}
#[test]
fn test_evaluation_config_partial_eq() {
let config1 = EvaluationConfig::default();
let config2 = EvaluationConfig::default();
let config3 = EvaluationConfig::performance();
assert_eq!(config1, config2);
assert_ne!(config1, config3);
}
#[test]
fn test_evaluation_config_custom_values() {
let config = EvaluationConfig {
max_recursion_depth: 25,
max_string_length: 16384,
stop_at_first_match: false,
enable_mime_types: true,
timeout_ms: Some(10000),
};
assert_eq!(config.max_recursion_depth, 25);
assert_eq!(config.max_string_length, 16384);
assert!(!config.stop_at_first_match);
assert!(config.enable_mime_types);
assert_eq!(config.timeout_ms, Some(10000));
assert!(config.validate().is_ok());
}
#[test]
fn test_libmagic_error_from_parse_error() {
let parse_error = ParseError::invalid_syntax(10, "test error");
let libmagic_error = LibmagicError::from(parse_error);
match libmagic_error {
LibmagicError::ParseError(_) => (),
_ => panic!("Expected ParseError variant"),
}
}
#[test]
fn test_libmagic_error_from_evaluation_error() {
let eval_error = EvaluationError::buffer_overrun(100);
let libmagic_error = LibmagicError::from(eval_error);
match libmagic_error {
LibmagicError::EvaluationError(_) => (),
_ => panic!("Expected EvaluationError variant"),
}
}
#[test]
fn test_with_builtin_rules() {
let db = MagicDatabase::with_builtin_rules().expect("builtin rules should load");
assert!(
!db.root_rules.is_empty(),
"Built-in rules should not be empty"
);
assert!(db.source_path().is_none());
let elf_header = b"\x7fELF\x02\x01\x01\x00";
let elf_result = db.evaluate_buffer(elf_header).unwrap();
assert!(
elf_result.description.contains("ELF"),
"Expected ELF detection, got: {}",
elf_result.description
);
let zip_header = b"PK\x03\x04";
let zip_result = db.evaluate_buffer(zip_header).unwrap();
assert!(
zip_result.description.contains("ZIP"),
"Expected ZIP detection, got: {}",
zip_result.description
);
let unknown_text_result = db.evaluate_buffer(b"random unknown content").unwrap();
assert_eq!(
unknown_text_result.description,
"ASCII text, with no line terminators"
);
let unknown_binary_result = db
.evaluate_buffer(&[0x00, 0x01, 0x02, 0xFF, 0xFE, 0x10])
.unwrap();
assert_eq!(unknown_binary_result.description, "data");
}
#[test]
fn test_evaluation_metadata_default() {
let metadata = EvaluationMetadata::default();
assert_eq!(metadata.file_size, 0);
assert!((metadata.evaluation_time_ms - 0.0).abs() < 0.001);
assert_eq!(metadata.rules_evaluated, 0);
assert!(metadata.magic_file.is_none());
assert!(!metadata.timed_out);
}
#[test]
fn test_evaluation_result_has_metadata() {
let db = MagicDatabase::with_builtin_rules().expect("builtin rules should load");
let elf_header = b"\x7fELF\x02\x01\x01\x00";
let result = db.evaluate_buffer(elf_header).unwrap();
assert_eq!(result.metadata.file_size, 8);
assert!(result.metadata.evaluation_time_ms >= 0.0);
assert!(result.metadata.rules_evaluated > 0);
assert!(result.metadata.magic_file.is_none()); assert!(!result.metadata.timed_out);
}
#[test]
fn test_evaluation_result_has_matches() {
let db = MagicDatabase::with_builtin_rules().expect("builtin rules should load");
let elf_header = b"\x7fELF\x02\x01\x01\x00";
let result = db.evaluate_buffer(elf_header).unwrap();
assert!(!result.matches.is_empty());
let first_match = &result.matches[0];
assert!(first_match.confidence > 0.0);
}
#[test]
fn test_evaluation_result_confidence_from_matches() {
let db = MagicDatabase::with_builtin_rules().expect("builtin rules should load");
let elf_header = b"\x7fELF\x02\x01\x01\x00";
let result = db.evaluate_buffer(elf_header).unwrap();
if !result.matches.is_empty() {
assert!((result.confidence - result.matches[0].confidence).abs() < 0.001);
}
}
#[test]
fn confidence_comes_from_the_first_message_bearing_match() {
use crate::evaluator::is_message_bearing;
use std::io::Write;
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("magic");
std::fs::File::create(&path)
.unwrap()
.write_all(b"0 byte x\n>0 string QQ GATED\n")
.unwrap();
let db = MagicDatabase::load_from_file(&path).unwrap();
let result = db.evaluate_buffer(b"QQ\x01\x02\xff").unwrap();
assert_eq!(result.description, "GATED");
assert!(!is_message_bearing(&result.matches[0].message));
assert!((result.confidence - result.matches[1].confidence).abs() < 0.001);
assert!((result.confidence - result.matches[0].confidence).abs() > 0.001);
}
#[test]
fn test_evaluation_result_no_match_has_zero_confidence() {
let db = MagicDatabase::with_builtin_rules().expect("builtin rules should load");
let unknown_result = db
.evaluate_buffer(&[0x00, 0x01, 0x02, 0xFF, 0xFE, 0x10])
.unwrap();
assert_eq!(unknown_result.description, "data");
assert!((unknown_result.confidence - 0.0).abs() < 0.001);
assert!(unknown_result.matches.is_empty());
}
#[test]
fn test_concatenate_messages_simple() {
let matches = vec![
evaluator::RuleMatch {
message: "ELF".to_string(),
offset: 0,
level: 0,
value: Value::Bytes(vec![0x7f]),
type_kind: TypeKind::Byte { signed: false },
confidence: 0.3,
},
evaluator::RuleMatch {
message: "64-bit".to_string(),
offset: 4,
level: 1,
value: Value::Uint(2),
type_kind: TypeKind::Byte { signed: false },
confidence: 0.5,
},
];
let result = MagicDatabase::concatenate_messages(&matches);
assert_eq!(result, "ELF 64-bit");
}
#[test]
fn test_concatenate_messages_with_backspace() {
let matches = vec![
evaluator::RuleMatch {
message: "ELF".to_string(),
offset: 0,
level: 0,
value: Value::Bytes(vec![0x7f]),
type_kind: TypeKind::Byte { signed: false },
confidence: 0.3,
},
evaluator::RuleMatch {
message: "\u{0008}, 64-bit".to_string(), offset: 4,
level: 1,
value: Value::Uint(2),
type_kind: TypeKind::Byte { signed: false },
confidence: 0.5,
},
];
let result = MagicDatabase::concatenate_messages(&matches);
assert_eq!(result, "ELF, 64-bit"); }
#[test]
fn test_concatenate_messages_with_literal_backslash_b_marker() {
let matches = vec![
evaluator::RuleMatch {
message: "PE STUB".to_string(),
offset: 0,
level: 0,
value: Value::Uint(0),
type_kind: TypeKind::Byte { signed: false },
confidence: 0.3,
},
evaluator::RuleMatch {
message: "\\b, for MS Windows".to_string(),
offset: 4,
level: 1,
value: Value::Uint(0),
type_kind: TypeKind::Byte { signed: false },
confidence: 0.5,
},
];
let result = MagicDatabase::concatenate_messages(&matches);
assert_eq!(
result, "PE STUB, for MS Windows",
"literal \\b marker must suppress the space and not appear in the output"
);
}
#[test]
fn test_concatenate_messages_backspace_on_first_match() {
let matches = vec![evaluator::RuleMatch {
message: "\u{0008}leading-strip".to_string(),
offset: 0,
level: 0,
value: Value::Uint(0),
type_kind: TypeKind::Byte { signed: false },
confidence: 0.5,
}];
let result = MagicDatabase::concatenate_messages(&matches);
assert_eq!(
result, "leading-strip",
"backspace-prefixed first match must strip the backspace byte"
);
}
#[test]
fn test_concatenate_messages_consecutive_backspaces() {
let make_match = |message: &str| evaluator::RuleMatch {
message: message.to_string(),
offset: 0,
level: 0,
value: Value::Uint(0),
type_kind: TypeKind::Byte { signed: false },
confidence: 0.5,
};
let matches = vec![
make_match("ELF"),
make_match("\u{0008}, 64-bit"),
make_match("\u{0008}, LSB"),
];
let result = MagicDatabase::concatenate_messages(&matches);
assert_eq!(
result, "ELF, 64-bit, LSB",
"consecutive backspace-prefixed messages must each strip cleanly"
);
}
#[test]
fn test_concatenate_messages_backspace_empty_rest() {
let matches = vec![
evaluator::RuleMatch {
message: "prefix".to_string(),
offset: 0,
level: 0,
value: Value::Uint(0),
type_kind: TypeKind::Byte { signed: false },
confidence: 0.5,
},
evaluator::RuleMatch {
message: "\u{0008}".to_string(),
offset: 0,
level: 0,
value: Value::Uint(0),
type_kind: TypeKind::Byte { signed: false },
confidence: 0.5,
},
];
let result = MagicDatabase::concatenate_messages(&matches);
assert_eq!(
result, "prefix",
"backspace with empty rest must contribute nothing to the output"
);
}
#[cfg(test)]
mod newline_gate_end_to_end_tests {
use super::MagicDatabase;
use crate::evaluator::evaluate_rules;
use crate::parser::ast::StringFlags;
use crate::{EvaluationContext, MagicRule, OffsetSpec, Operator, TypeKind, Value};
#[test]
fn test_any_value_string_x_renders_single_line_across_embedded_newline() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
Operator::AnyValue,
Value::Uint(0),
"STR=[%s]".to_string(),
);
let buffer = b"ZZZZABC\nSECOND\n";
let mut ctx = EvaluationContext::new(crate::EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
assert_eq!(matches.len(), 1, "the any-value rule should match once");
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(
description.lines().count(),
1,
"description must be a single line, got: {description:?}"
);
assert_eq!(description, "STR=[ZZZZABC]");
}
#[test]
fn test_equality_compared_rule_with_embedded_newline_does_not_stop() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
Operator::Equal,
Value::String("AB\nCD".to_string()),
"eq=[%s]".to_string(),
);
let buffer = b"AB\nCD";
let mut ctx = EvaluationContext::new(crate::EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
assert_eq!(matches.len(), 1, "the equality rule should match once");
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(
description, "eq=[AB\nCD]",
"an equality-compared rule must never stop at an embedded newline"
);
}
#[test]
fn test_ordering_rule_null_first_byte_gates_end_to_end() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
Operator::GreaterThan,
Value::Bytes(vec![0, b'A']), "gated=[%s]".to_string(),
);
let buffer = b"ZZZZ\nSECOND";
let mut ctx = EvaluationContext::new(crate::EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
assert_eq!(matches.len(), 1);
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(description, "gated=[ZZZZ]");
}
#[test]
fn test_ordering_rule_non_null_first_byte_does_not_gate_end_to_end() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
Operator::GreaterThan,
Value::String("A".to_string()), "ungated=[%s]".to_string(),
);
let buffer = b"ZZZZ\nSECOND";
let mut ctx = EvaluationContext::new(crate::EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
assert_eq!(matches.len(), 1);
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(
description, "ungated=[ZZZZ\nSECOND]",
"non-gated ordering render must keep the embedded newline"
);
}
}
#[cfg(test)]
mod pstring_string16_newline_gate_end_to_end_tests {
use super::MagicDatabase;
use crate::evaluator::evaluate_rules;
use crate::parser::ast::PStringLengthWidth;
use crate::{
EvaluationConfig, EvaluationContext, MagicRule, OffsetSpec, Operator, TypeKind, Value,
};
fn pstring_type() -> TypeKind {
TypeKind::PString {
max_length: None,
length_width: PStringLengthWidth::OneByte,
length_includes_itself: false,
}
}
fn le_string16_type() -> TypeKind {
TypeKind::String16 {
endian: crate::Endianness::Little,
}
}
fn ucs2le(s: &str) -> Vec<u8> {
let mut out = Vec::new();
for ch in s.chars() {
out.extend_from_slice(&(ch as u16).to_le_bytes());
}
out
}
#[test]
fn test_pstring_any_value_renders_single_line_across_embedded_newline() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
pstring_type(),
Operator::AnyValue,
Value::Uint(0),
"PS=[%s]".to_string(),
);
let buffer = b"\x07ABC\ndeftail\n";
let mut ctx = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
assert_eq!(matches.len(), 1);
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(description.lines().count(), 1);
assert_eq!(description, "PS=[ABC]");
}
#[test]
fn test_string16_any_value_renders_single_line_across_embedded_newline() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
le_string16_type(),
Operator::AnyValue,
Value::Uint(0),
"S16=[%s]".to_string(),
);
let mut buffer = ucs2le("AB\ncd");
buffer.extend_from_slice(&[0, 0]);
buffer.extend_from_slice(b"tail");
let mut ctx = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), &buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
assert_eq!(matches.len(), 1);
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(description.lines().count(), 1);
assert_eq!(description, "S16=[AB]");
}
#[test]
fn test_pstring_equality_compared_with_embedded_newline_does_not_stop() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
pstring_type(),
Operator::Equal,
Value::String("ABC\ndef".to_string()),
"EQ=[%s]".to_string(),
);
let buffer = b"\x07ABC\ndef";
let mut ctx = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
assert_eq!(matches.len(), 1);
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(description, "EQ=[ABC\ndef]");
}
#[test]
fn test_string16_comparison_is_prefix_limited_to_pattern_length() {
let buffer = ucs2le("ABCD");
let table: &[(&str, Operator, usize)] = &[
("equal to a prefix matches", Operator::Equal, 1),
("not-equal to a prefix does not fire", Operator::NotEqual, 0),
];
for (name, op, want_matches) in table {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
le_string16_type(),
op.clone(),
Value::String("ABC".to_string()),
"S16".to_string(),
);
let mut ctx = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), &buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
assert_eq!(matches.len(), *want_matches, "{name}");
}
}
#[test]
fn test_string16_equality_compared_with_embedded_newline_does_not_stop() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
le_string16_type(),
Operator::Equal,
Value::String("AB\ncd".to_string()),
"EQ16=[%s]".to_string(),
);
let mut buffer = ucs2le("AB\ncd");
buffer.extend_from_slice(&[0, 0]);
let mut ctx = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), &buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
assert_eq!(matches.len(), 1);
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(description, "EQ16=[AB\ncd]");
}
#[test]
fn test_pstring_any_value_caps_at_127_bytes() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
pstring_type(),
Operator::AnyValue,
Value::Uint(0),
"PS=[%s]".to_string(),
);
let mut buffer = vec![200u8];
buffer.extend(std::iter::repeat_n(b'Q', 200));
let mut ctx = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), &buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(description, format!("PS=[{}]", "Q".repeat(127)));
}
#[test]
fn test_string16_any_value_caps_at_127_chars() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
le_string16_type(),
Operator::AnyValue,
Value::Uint(0),
"S16=[%s]".to_string(),
);
let buffer = ucs2le(&"Q".repeat(300));
let mut ctx = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), &buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(description, format!("S16=[{}]", "Q".repeat(127)));
}
#[test]
fn test_max_string_length_config_wins_over_127_for_pstring_and_string16() {
let config = EvaluationConfig::default().with_max_string_length(10);
let pstring_rule = MagicRule::new(
OffsetSpec::Absolute(0),
pstring_type(),
Operator::AnyValue,
Value::Uint(0),
"PS=[%s]".to_string(),
);
let mut pstring_buffer = vec![200u8];
pstring_buffer.extend(std::iter::repeat_n(b'Q', 200));
let mut ctx = EvaluationContext::new(config.clone());
let matches = evaluate_rules(
std::slice::from_ref(&pstring_rule),
&pstring_buffer,
&mut ctx,
)
.expect("evaluate_rules should not error for this simple rule");
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(description, format!("PS=[{}]", "Q".repeat(10)));
let string16_rule = MagicRule::new(
OffsetSpec::Absolute(0),
le_string16_type(),
Operator::AnyValue,
Value::Uint(0),
"S16=[%s]".to_string(),
);
let string16_buffer = ucs2le(&"Q".repeat(300));
let mut ctx = EvaluationContext::new(config);
let matches = evaluate_rules(
std::slice::from_ref(&string16_rule),
&string16_buffer,
&mut ctx,
)
.expect("evaluate_rules should not error for this simple rule");
let description = MagicDatabase::concatenate_messages(&matches);
assert_eq!(description, format!("S16=[{}]", "Q".repeat(10)));
}
#[test]
fn test_pstring_any_value_anchor_advances_past_prefix_width_and_stops_at_newline() {
let child = MagicRule::new(
OffsetSpec::Relative(0),
TypeKind::Byte { signed: false },
Operator::AnyValue,
Value::Uint(0),
"next=%d".to_string(),
);
let parent = MagicRule::new(
OffsetSpec::Absolute(0),
pstring_type(),
Operator::AnyValue,
Value::Uint(0),
"PS=[%s]".to_string(),
)
.with_children(vec![child]);
let buffer = b"\x07ABC\ndeftail\n";
let mut ctx = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&parent), buffer, &mut ctx)
.expect("evaluate_rules should not error for this simple rule");
assert_eq!(matches.len(), 2);
assert_eq!(matches[1].value, Value::Uint(u64::from(b'\n')));
}
#[test]
fn test_pstring_any_value_declared_prefix_exceeds_buffer_degrades_to_non_match() {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
pstring_type(),
Operator::AnyValue,
Value::Uint(0),
"PS=[%s]".to_string(),
);
let buffer = b"\x05ab"; let mut ctx = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(std::slice::from_ref(&rule), buffer, &mut ctx)
.expect("evaluate_rules should not error (a read failure is a non-match, not a propagated error)");
assert!(matches.is_empty());
}
}
fn regex_db(n: usize, timeout_ms: Option<u64>) -> (tempfile::TempDir, MagicDatabase) {
use std::io::Write;
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("magic");
let mut file = std::fs::File::create(&path).unwrap();
for i in 0..n {
writeln!(file, "0 regex ZZTEXTQ{i}[a-z]+Q[0-9]{{2,8}} MSG{i}").unwrap();
}
let config = EvaluationConfig::default().with_timeout_ms(timeout_ms);
let db = MagicDatabase::load_from_file_with_config(&path, config).unwrap();
(dir, db)
}
#[test]
fn remaining_timeout_ms_table() {
use std::time::{Duration, Instant};
let (_d1, unbounded) = regex_db(1, None);
let (_d2, bounded) = regex_db(1, Some(1000));
let ago = |ms| {
Instant::now()
.checked_sub(Duration::from_millis(ms))
.expect("the monotonic clock has at least one second of history")
};
assert!(matches!(
unbounded.remaining_timeout_ms(Instant::now()),
Ok(None)
));
let left = bounded.remaining_timeout_ms(ago(400)).unwrap().unwrap();
assert!((400..=600).contains(&left), "left {left}");
assert!(matches!(
bounded.remaining_timeout_ms(ago(1000)),
Err(LibmagicError::Timeout { timeout_ms: 1000 })
));
}
#[test]
fn second_pass_timeout_reports_the_configured_budget() {
let (_dir, db) = regex_db(1, Some(1000));
assert!(matches!(
db.with_configured_timeout(LibmagicError::Timeout { timeout_ms: 7 }),
LibmagicError::Timeout { timeout_ms: 1000 }
));
assert!(matches!(
db.with_configured_timeout(LibmagicError::FileError("x".into())),
LibmagicError::FileError(_)
));
}
#[test]
fn second_pass_is_not_started_when_the_timeout_budget_is_spent() {
use std::io::Write;
use std::time::{Duration, Instant};
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("magic");
std::fs::File::create(&path)
.unwrap()
.write_all(b"0 regex ZZTEXTQ MSG\n")
.unwrap();
let config = EvaluationConfig::default().with_timeout_ms(Some(1000));
let db = MagicDatabase::load_from_file_with_config(&path, config).unwrap();
let buffer = b"ZZTEXTQ\n";
let fresh = db.evaluate_buffer_internal(buffer, Instant::now()).unwrap();
assert_eq!(fresh.description, "MSG, ASCII text");
let spent = Instant::now().checked_sub(Duration::from_secs(5)).unwrap();
let result = db.evaluate_buffer_internal(buffer, spent);
assert!(
matches!(result, Err(LibmagicError::Timeout { timeout_ms: 1000 })),
"expected Timeout once the budget is spent, got {result:?}"
);
}