use super::*;
use crate::parser::ast::{
Endianness, IndirectAdjustmentOp, MetaType, PStringLengthWidth, RegexCount, RegexFlags,
StringFlags,
};
#[test]
fn search_strength_falls_as_the_scan_range_widens() {
use crate::parser::ast::{Endianness, OffsetSpec, Operator, TypeKind, Value};
use std::num::NonZeroUsize;
fn search_rule(range: usize) -> MagicRule {
MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::Search {
range: NonZeroUsize::new(range),
flags: crate::parser::ast::SearchFlags::default(),
},
Operator::Equal,
Value::String("<!--".to_string()),
"exported SGML document text".to_string(),
)
}
let long_detector = MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::Long {
endian: Endianness::Little,
signed: true,
},
Operator::BitwiseAndMask(0xffff_fffe),
Value::Uint(0xfeed_face),
"Mach-O".to_string(),
);
let long_score = calculate_default_strength(&long_detector);
let cases: &[(usize, i32, &str)] = &[
(4, 32, "tight scan -- multiplier 2"),
(16, 28, "at the saturation floor -- multiplier 1"),
(4096, 28, "wide scan, the sgml shape -- still multiplier 1"),
];
let mut previous: Option<i32> = None;
for &(range, expected, label) in cases {
let score = calculate_default_strength(&search_rule(range));
assert_eq!(
score, expected,
"{label} (range {range}) scored {score}, expected {expected}"
);
if let Some(prev) = previous {
assert!(
score <= prev,
"{label} (range {range}) must not outrank a narrower scan, \
got {score} after {prev}"
);
}
previous = Some(score);
}
let wide_score = calculate_default_strength(&search_rule(4096));
assert!(
wide_score < long_score,
"a 4-byte pattern over a 4096-byte window must rank below the long \
detector, got search={wide_score} long={long_score}"
);
}
fn make_rule(typ: TypeKind, op: Operator, offset: OffsetSpec, value: Value) -> MagicRule {
MagicRule {
offset,
typ,
op,
value,
message: "test".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
}
}
#[test]
#[allow(clippy::too_many_lines)]
fn test_calculate_default_strength_table() {
type Case = (fn() -> MagicRule, i32, &'static str);
let cases: &[Case] = &[
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
25, "type=byte",
),
(
|| {
make_rule(
TypeKind::Short {
endian: Endianness::Little,
signed: false,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
30, "type=short",
),
(
|| {
make_rule(
TypeKind::Long {
endian: Endianness::Big,
signed: false,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
35, "type=long",
),
(
|| {
make_rule(
TypeKind::Quad {
endian: Endianness::Little,
signed: false,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
36, "type=quad",
),
(
|| {
make_rule(
TypeKind::Date {
endian: Endianness::Big,
utc: true,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
35, "type=date",
),
(
|| {
make_rule(
TypeKind::QDate {
endian: Endianness::Little,
utc: false,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
36, "type=qdate",
),
(
|| {
make_rule(
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("ELF".to_string()),
)
},
43, "type=string len=3",
),
(
|| {
make_rule(
TypeKind::String {
max_length: Some(10),
flags: StringFlags::default(),
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("TEST".to_string()),
)
},
49, "type=string max_length=10",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::NotEqual,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
20, "op=not_equal",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::BitwiseAnd,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
18, "op=bitwise_and",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::BitwiseAndMask(0xFF),
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
22, "op=bitwise_and_mask",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::LessThan,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
21, "op=less_than",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::GreaterThan,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
21,
"op=greater_than",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::LessEqual,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
21,
"op=less_equal",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::GreaterEqual,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
21,
"op=greater_equal",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Indirect {
base_offset: 0,
base_relative: false,
pointer_type: TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
adjustment: 0,
adjustment_op: IndirectAdjustmentOp::Add,
result_relative: false,
endian: Endianness::Little,
},
Value::Uint(0),
)
},
20, "offset=indirect",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Relative(4),
Value::Uint(0),
)
},
18, "offset=relative",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::FromEnd(-4),
Value::Uint(0),
)
},
23, "offset=from_end",
),
(
|| {
make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46]),
)
},
29, "value=bytes len=4",
),
(
|| {
make_rule(
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("This is a very long string that exceeds the cap".to_string()),
)
},
60, "value=long_string (cap)",
),
(
|| {
make_rule(
TypeKind::Regex {
flags: RegexFlags::default(),
count: RegexCount::Default,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("x".to_string()),
)
},
41, "type=regex count=default",
),
(
|| {
make_rule(
TypeKind::Regex {
flags: RegexFlags::default(),
count: RegexCount::Lines(None),
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("x".to_string()),
)
},
41, "type=regex count=lines(none)",
),
(
|| {
make_rule(
TypeKind::Regex {
flags: RegexFlags::default(),
count: RegexCount::Lines(std::num::NonZeroU32::new(3)),
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("x".to_string()),
)
},
46, "type=regex count=lines(3)",
),
(
|| {
make_rule(
TypeKind::Regex {
flags: RegexFlags::default(),
count: RegexCount::Bytes(
std::num::NonZeroU32::new(100).expect("100 is non-zero"),
),
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("x".to_string()),
)
},
46, "type=regex count=bytes(100)",
),
(
|| {
make_rule(
TypeKind::String16 {
endian: Endianness::Little,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("x".to_string()),
)
},
41, "type=string16",
),
(
|| {
make_rule(
TypeKind::PString {
max_length: None,
length_width: PStringLengthWidth::OneByte,
length_includes_itself: false,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("x".to_string()),
)
},
41, "type=pstring max_length=none",
),
(
|| {
make_rule(
TypeKind::PString {
max_length: Some(8),
length_width: PStringLengthWidth::OneByte,
length_includes_itself: false,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("x".to_string()),
)
},
46, "type=pstring max_length=8",
),
(
|| {
make_rule(
TypeKind::Meta(MetaType::Offset),
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
)
},
20, "type=meta(offset)",
),
];
for (factory, expected, desc) in cases {
let rule = factory();
let strength = calculate_default_strength(&rule);
assert_eq!(
strength, *expected,
"calculate_default_strength mismatch for case '{desc}'"
);
}
}
#[test]
fn test_apply_modifier_add() {
assert_eq!(apply_strength_modifier(50, &StrengthModifier::Add(10)), 60);
}
#[test]
fn test_apply_modifier_subtract() {
assert_eq!(
apply_strength_modifier(50, &StrengthModifier::Subtract(10)),
40
);
}
#[test]
fn test_apply_modifier_multiply() {
assert_eq!(
apply_strength_modifier(50, &StrengthModifier::Multiply(2)),
100
);
}
#[test]
fn test_apply_modifier_divide() {
assert_eq!(
apply_strength_modifier(50, &StrengthModifier::Divide(2)),
25
);
}
#[test]
fn test_apply_modifier_set() {
assert_eq!(apply_strength_modifier(50, &StrengthModifier::Set(75)), 75);
}
#[test]
fn test_apply_modifier_add_overflow() {
assert_eq!(
apply_strength_modifier(250, &StrengthModifier::Add(100)),
MAX_STRENGTH
);
}
#[test]
fn test_apply_modifier_subtract_underflow() {
assert_eq!(
apply_strength_modifier(10, &StrengthModifier::Subtract(100)),
MIN_STRENGTH
);
}
#[test]
fn test_apply_modifier_multiply_overflow() {
assert_eq!(
apply_strength_modifier(200, &StrengthModifier::Multiply(10)),
MAX_STRENGTH
);
}
#[test]
fn test_apply_modifier_divide_by_zero() {
assert_eq!(
apply_strength_modifier(50, &StrengthModifier::Divide(0)),
50
);
}
#[test]
fn test_apply_modifier_set_negative() {
assert_eq!(
apply_strength_modifier(50, &StrengthModifier::Set(-10)),
MIN_STRENGTH
);
}
#[test]
fn test_apply_modifier_set_over_max() {
assert_eq!(
apply_strength_modifier(50, &StrengthModifier::Set(1000)),
MAX_STRENGTH
);
}
#[test]
fn test_rule_strength_without_modifier() {
let rule = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
assert_eq!(calculate_rule_strength(&rule), 25);
}
#[test]
fn test_rule_strength_with_add_modifier() {
let mut rule = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
rule.strength_modifier = Some(StrengthModifier::Add(20));
assert_eq!(calculate_rule_strength(&rule), 45);
}
#[test]
fn test_rule_strength_with_multiply_modifier() {
let mut rule = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
rule.strength_modifier = Some(StrengthModifier::Multiply(2));
assert_eq!(calculate_rule_strength(&rule), 50);
}
#[test]
fn test_rule_strength_with_set_modifier() {
let mut rule = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
rule.strength_modifier = Some(StrengthModifier::Set(100));
assert_eq!(calculate_rule_strength(&rule), 100);
}
#[test]
fn test_sort_rules_by_strength_basic() {
let mut rules = vec![
{
let mut r = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
r.message = "byte rule".to_string();
r
},
{
let mut r = make_rule(
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("MAGIC".to_string()),
);
r.message = "string rule".to_string();
r
},
];
sort_rules_by_strength(&mut rules);
assert_eq!(rules[0].message, "string rule");
assert_eq!(rules[1].message, "byte rule");
}
#[test]
fn test_sort_rules_by_strength_preserves_child_file_order() {
let low_first = {
let mut r = make_rule(
TypeKind::Meta(crate::parser::ast::MetaType::Default),
Operator::AnyValue,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
r.message = "default-child".to_string();
r.level = 1;
r
};
let high_second = {
let mut r = make_rule(
TypeKind::Long {
endian: crate::parser::ast::Endianness::Big,
signed: false,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0xDEAD_BEEF),
);
r.message = "strong-child".to_string();
r.level = 1;
r
};
let mut parent = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
parent.message = "parent".to_string();
parent.children = vec![low_first, high_second];
let mut rules = vec![parent];
sort_rules_by_strength(&mut rules);
let child_order: Vec<&str> = rules[0]
.children
.iter()
.map(|c| c.message.as_str())
.collect();
assert_eq!(
child_order,
vec!["default-child", "strong-child"],
"child rules must stay in file order; the non-recursive sort must \
not reorder continuation rules by strength"
);
}
#[test]
fn test_sort_rules_by_strength_with_modifier() {
let mut rules = vec![
{
let mut r = make_rule(
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("TEST".to_string()),
);
r.message = "string rule".to_string();
r.strength_modifier = Some(StrengthModifier::Set(10));
r
},
{
let mut r = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
r.message = "byte rule".to_string();
r.strength_modifier = Some(StrengthModifier::Set(100));
r
},
];
sort_rules_by_strength(&mut rules);
assert_eq!(rules[0].message, "byte rule");
assert_eq!(rules[1].message, "string rule");
}
#[test]
fn test_sort_rules_empty() {
let mut rules: Vec<MagicRule> = vec![];
sort_rules_by_strength(&mut rules);
assert!(rules.is_empty());
}
#[test]
fn test_sort_rules_single() {
let mut rules = vec![make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
)];
sort_rules_by_strength(&mut rules);
assert_eq!(rules.len(), 1);
}
#[test]
fn test_into_sorted_by_strength() {
let rules = vec![
{
let mut r = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
r.message = "byte rule".to_string();
r
},
{
let mut r = make_rule(
TypeKind::Long {
endian: Endianness::Big,
signed: false,
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
r.message = "long rule".to_string();
r
},
];
let sorted = into_sorted_by_strength(rules);
assert_eq!(sorted[0].message, "long rule");
assert_eq!(sorted[1].message, "byte rule");
}
#[test]
fn test_strength_comparison_string_vs_byte() {
let string_rule = make_rule(
TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
Operator::Equal,
OffsetSpec::Absolute(0),
Value::String("AB".to_string()),
);
let byte_rule = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0x7f),
);
let string_strength = calculate_rule_strength(&string_rule);
let byte_strength = calculate_rule_strength(&byte_rule);
assert!(
string_strength > byte_strength,
"String strength {string_strength} should be > byte strength {byte_strength}"
);
}
#[test]
fn test_strength_comparison_absolute_vs_relative_offset() {
let absolute_rule = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0x7f),
);
let relative_rule = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Relative(4),
Value::Uint(0x7f),
);
let absolute_strength = calculate_rule_strength(&absolute_rule);
let relative_strength = calculate_rule_strength(&relative_rule);
assert!(
absolute_strength > relative_strength,
"Absolute strength {absolute_strength} should be > relative strength {relative_strength}"
);
}
fn meta_rule(meta: crate::parser::ast::MetaType, msg: &str) -> MagicRule {
let mut rule = make_rule(
TypeKind::Meta(meta),
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
rule.message = msg.to_string();
rule
}
#[test]
fn test_meta_default_and_clear_sort_to_bottom() {
use crate::parser::ast::MetaType;
let mut rules = vec![
meta_rule(MetaType::Default, "default"),
meta_rule(MetaType::Clear, "clear"),
{
let mut r = make_rule(
TypeKind::Byte { signed: true },
Operator::Equal,
OffsetSpec::Absolute(0),
Value::Uint(0),
);
r.message = "byte".to_string();
r
},
];
sort_rules_by_strength(&mut rules);
assert_eq!(rules[0].message, "byte");
}
#[test]
fn test_meta_use_and_indirect_sort_above_default() {
use crate::parser::ast::MetaType;
let use_rule = meta_rule(
MetaType::Use {
name: "sub".to_string(),
flip_endian: false,
},
"use",
);
let indirect_rule = meta_rule(MetaType::Indirect, "indirect");
let default_rule = meta_rule(MetaType::Default, "default");
let clear_rule = meta_rule(MetaType::Clear, "clear");
assert!(
calculate_default_strength(&use_rule) > calculate_default_strength(&default_rule),
"use should sort above default"
);
assert!(
calculate_default_strength(&indirect_rule) > calculate_default_strength(&default_rule),
"indirect should sort above default"
);
assert!(
calculate_default_strength(&use_rule) > calculate_default_strength(&clear_rule),
"use should sort above clear"
);
assert!(
calculate_default_strength(&indirect_rule) > calculate_default_strength(&clear_rule),
"indirect should sort above clear"
);
}
#[test]
fn test_meta_name_strength_is_zero() {
use crate::parser::ast::MetaType;
let name_rule = meta_rule(MetaType::Name("foo".to_string()), "name");
let default_rule = meta_rule(MetaType::Default, "default");
assert_eq!(
calculate_default_strength(&name_rule),
calculate_default_strength(&default_rule),
"Name strength should equal Default strength (both type-axis 0)"
);
}
#[test]
fn string_flag_specificity_penalty_per_flag_table() {
let cases: &[(&str, StringFlags, i32)] = &[
("no flags", StringFlags::default(), 0),
(
"/c only",
StringFlags::default().with_ignore_lowercase(true),
1,
),
(
"/C only",
StringFlags::default().with_ignore_uppercase(true),
1,
),
(
"/w only",
StringFlags::default().with_compact_optional_whitespace(true),
1,
),
(
"/W only",
StringFlags::default().with_compact_whitespace(true),
1,
),
(
"/T only (non-penalized)",
StringFlags::default().with_trim(true),
0,
),
(
"/t only (non-penalized)",
StringFlags::default().with_text_test(true),
0,
),
(
"/b only (non-penalized)",
StringFlags::default().with_bin_test(true),
0,
),
(
"/f only (non-penalized)",
StringFlags::default().with_full_word(true),
0,
),
(
"/cw stacks (case + whitespace)",
StringFlags::default()
.with_ignore_lowercase(true)
.with_compact_optional_whitespace(true),
2,
),
(
"/cC stacks (both case folds)",
StringFlags::default()
.with_ignore_lowercase(true)
.with_ignore_uppercase(true),
2,
),
(
"all four penalized flags",
StringFlags::default()
.with_ignore_lowercase(true)
.with_ignore_uppercase(true)
.with_compact_whitespace(true)
.with_compact_optional_whitespace(true),
4,
),
(
"mixed: 2 penalized + 4 non-penalized",
StringFlags::default()
.with_ignore_lowercase(true)
.with_compact_whitespace(true)
.with_trim(true)
.with_text_test(true)
.with_bin_test(true)
.with_full_word(true),
2,
),
];
for (label, flags, expected) in cases {
let actual = string_flag_specificity_penalty(*flags);
assert_eq!(
actual, *expected,
"case {label}: expected penalty {expected}, got {actual}"
);
}
}
proptest::proptest! {
#[test]
fn prop_search_strength_stays_within_clamp(
pattern_len in 0usize..2048,
range in proptest::option::of(1usize..100_000),
) {
let rule = MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::Search {
range: range.and_then(std::num::NonZeroUsize::new),
flags: crate::parser::ast::SearchFlags::default(),
},
Operator::Equal,
Value::String("x".repeat(pattern_len)),
"prop".to_string(),
);
let score = calculate_default_strength(&rule);
proptest::prop_assert!(
(MIN_STRENGTH..=MAX_STRENGTH).contains(&score),
"score {score} outside [{MIN_STRENGTH}, {MAX_STRENGTH}] for len={pattern_len} range={range:?}"
);
}
#[test]
fn prop_widening_the_scan_never_raises_strength(
pattern_len in 1usize..64,
a in 1usize..5000,
b in 1usize..5000,
) {
let score_for = |range: usize| {
calculate_default_strength(&MagicRule::new(
OffsetSpec::Absolute(0),
TypeKind::Search {
range: std::num::NonZeroUsize::new(range),
flags: crate::parser::ast::SearchFlags::default(),
},
Operator::Equal,
Value::String("x".repeat(pattern_len)),
"prop".to_string(),
))
};
let (narrow, wide) = if a <= b { (a, b) } else { (b, a) };
proptest::prop_assert!(
score_for(wide) <= score_for(narrow),
"widening {narrow} -> {wide} raised the score for len={pattern_len}"
);
}
}