use super::*;
use crate::parser::ast::StringFlags;
fn make_flagged_string_rule(pattern: &str, flags: StringFlags, op: Operator) -> MagicRule {
MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::String {
max_length: None,
flags,
},
op,
value: Value::String(pattern.to_string()),
message: format!("matched {pattern}"),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
}
}
#[test]
fn test_flagged_string_c_case_insensitive_matches_via_engine() {
let rule = make_flagged_string_rule(
"foo",
StringFlags::default().with_ignore_lowercase(true),
Operator::Equal,
);
let mut context = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(&[rule], b"FOObar", &mut context).unwrap();
assert_eq!(matches.len(), 1, "string/c foo should match FOObar");
assert_eq!(matches[0].message, "matched foo");
}
#[test]
fn test_flagged_string_c_does_not_match_when_uppercase_pattern_position_differs() {
let rule = make_flagged_string_rule(
"FoO",
StringFlags::default().with_ignore_lowercase(true),
Operator::Equal,
);
let mut context = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(&[rule], b"fOOrest", &mut context).unwrap();
assert!(
matches.is_empty(),
"string/c FoO must not match fOO (asymmetric /c contract)"
);
}
#[test]
fn test_flagged_string_default_flags_use_value_rule_fast_path() {
let rule = make_flagged_string_rule("foo", StringFlags::default(), Operator::Equal);
let mut context = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(&[rule], b"FOObar", &mut context).unwrap();
assert!(
matches.is_empty(),
"default-flag string must use case-sensitive matching"
);
}
#[test]
fn test_flagged_string_not_equal_inverts_match() {
let rule = make_flagged_string_rule(
"xyz",
StringFlags::default().with_ignore_lowercase(true),
Operator::NotEqual,
);
let mut context = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(&[rule], b"FOObar", &mut context).unwrap();
assert_eq!(matches.len(), 1, "NotEqual fires when pattern misses");
}
#[test]
fn test_flagged_string_ordering_operator_uses_lexicographic_value_path() {
let rule = make_flagged_string_rule(
"\0",
StringFlags::default().with_text_test(true),
Operator::GreaterThan,
);
let mut context = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(&[rule], b"FOObar", &mut context)
.expect("ordering operator on a flagged string must not fatally abort");
assert_eq!(
matches.len(),
1,
"a non-empty string is lexicographically greater than the null byte"
);
let rule2 = make_flagged_string_rule(
"\0",
StringFlags::default().with_text_test(true),
Operator::GreaterThan,
);
let mut ctx2 = EvaluationContext::new(EvaluationConfig::default());
let empty = evaluate_rules(&[rule2], b"", &mut ctx2)
.expect("ordering operator must not abort even on an empty buffer");
assert!(
empty.is_empty(),
"no string is present in an empty buffer, so `>\\0` must not match"
);
}
#[test]
fn test_flagged_string_w_whitespace_anchor_uses_declared_pattern_length() {
let child = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(u64::from(b' ')),
message: "space-at-declared-length".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
};
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::String {
max_length: None,
flags: StringFlags::default().with_compact_whitespace(true),
},
op: Operator::Equal,
value: Value::String("a b".to_string()),
message: "match".to_string(),
children: vec![child],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut context =
EvaluationContext::new(EvaluationConfig::default().with_stop_at_first_match(false));
let matches = evaluate_rules(&[parent], b"a b!", &mut context).unwrap();
assert_eq!(matches.len(), 2, "parent + child must both match");
assert_eq!(matches[1].message, "space-at-declared-length");
}
#[test]
fn test_flagged_string_w_optional_whitespace_anchor_matches_measured_oracle_fixtures() {
struct Fixture {
pattern: &'static str,
buffer: &'static [u8],
expected_byte: u8,
}
let fixtures = [
Fixture {
pattern: "#! ",
buffer: b"#!/bin/xx",
expected_byte: b'b',
},
Fixture {
pattern: "#! ",
buffer: b"#! /bin/xx",
expected_byte: b'/',
},
Fixture {
pattern: "#! ",
buffer: b"#! /bin/xx",
expected_byte: b' ',
},
Fixture {
pattern: "#! X",
buffer: b"#!Xbin/xx",
expected_byte: b'i',
},
Fixture {
pattern: "#! X",
buffer: b"#! Xbin/xx",
expected_byte: b'X',
},
];
for fx in fixtures {
let child = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(u64::from(fx.expected_byte)),
message: "anchor-byte".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
};
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::String {
max_length: None,
flags: StringFlags::default().with_compact_optional_whitespace(true),
},
op: Operator::Equal,
value: Value::String(fx.pattern.to_string()),
message: "shebang".to_string(),
children: vec![child],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut context =
EvaluationContext::new(EvaluationConfig::default().with_stop_at_first_match(false));
let matches = evaluate_rules(&[parent], fx.buffer, &mut context).unwrap_or_else(|e| {
panic!(
"fixture pattern {:?} buffer {:?} failed to evaluate: {e}",
fx.pattern, fx.buffer
)
});
assert_eq!(
matches.len(),
2,
"fixture pattern {:?} buffer {:?}: parent + child must both match at the \
declared-length anchor (byte {:?})",
fx.pattern,
fx.buffer,
fx.expected_byte as char,
);
}
}
#[test]
fn test_flagged_string_w_near_end_of_buffer_anchor_does_not_panic_and_child_is_non_match() {
let child = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(u64::from(b'X')),
message: "unreachable".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
};
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::String {
max_length: None,
flags: StringFlags::default().with_compact_optional_whitespace(true),
},
op: Operator::Equal,
value: Value::String("#! X".to_string()),
message: "shebang".to_string(),
children: vec![child],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut context =
EvaluationContext::new(EvaluationConfig::default().with_stop_at_first_match(false));
let matches = evaluate_rules(&[parent], b"#!X", &mut context).unwrap();
assert_eq!(
matches.len(),
1,
"parent matches but the child's declared-length anchor (4) is past EOF (3), \
so it must not match -- and evaluation must not panic"
);
}
#[test]
fn test_string_ordering_renders_full_field_not_compared_prefix() {
let rule = make_flagged_string_rule("0.6.1", StringFlags::default(), Operator::GreaterThan);
let mut context = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(&[rule], b"0.6.2 and more text", &mut context).unwrap();
assert_eq!(matches.len(), 1, "0.6.2 > 0.6.1 must match");
assert_eq!(
matches[0].value,
Value::String("0.6.2 and more text".to_string()),
"display value must be the FULL string field, not the compared 5-byte prefix"
);
}
#[test]
fn test_string_ordering_comparison_stays_prefix_limited() {
let rule = make_flagged_string_rule("0.6.1", StringFlags::default(), Operator::GreaterThan);
let mut context = EvaluationContext::new(EvaluationConfig::default());
let no_match = evaluate_rules(&[rule], b"0.6.10more", &mut context).unwrap();
assert!(
no_match.is_empty(),
"prefix '0.6.1' equals the pattern, so `> 0.6.1` must be false (prefix-limited compare)"
);
let rule2 = make_flagged_string_rule("0.6.1", StringFlags::default(), Operator::GreaterThan);
let mut ctx2 = EvaluationContext::new(EvaluationConfig::default());
let m = evaluate_rules(&[rule2], b"0.6.2", &mut ctx2).unwrap();
assert_eq!(m.len(), 1, "0.6.2 > 0.6.1 within the compared prefix");
}
#[test]
fn test_string_ordering_full_field_display_does_not_move_relative_anchor() {
let child = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(u64::from(b'X')),
message: "at-offset-5".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
};
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::String {
max_length: None,
flags: StringFlags::default(),
},
op: Operator::GreaterThan,
value: Value::String("0.6.1".to_string()),
message: "ver".to_string(),
children: vec![child],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut context =
EvaluationContext::new(EvaluationConfig::default().with_stop_at_first_match(false));
let matches =
evaluate_rules(&[parent], b"0.6.2X and a long trailing field", &mut context).unwrap();
assert_eq!(matches.len(), 2, "parent + child must both match");
assert_eq!(matches[1].message, "at-offset-5");
assert_eq!(
matches[1].offset, 5,
"child must resolve just past the 5-byte pattern, not the full field"
);
}
#[test]
fn test_flagged_string_any_value_operator_does_not_panic_end_to_end() {
use crate::parser::grammar::parse_magic_rule;
let (_, rule) = parse_magic_rule("0\tstring/b\tx\tgating").expect("rule must parse");
let mut context = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(&[rule], b"\x0b\x30 arbitrary binary bytes", &mut context)
.expect("a flagged string with the AnyValue operator must evaluate without panicking");
assert!(
matches.iter().any(|m| m.message.contains("gating")),
"AnyValue (`x`) matches any buffer, so the gating rule fires"
);
}
#[test]
fn test_string16_ordering_renders_full_narrowed_field() {
use crate::parser::ast::Endianness;
let cases: &[(Endianness, &[u8], Operator, &str, &str)] = &[
(
Endianness::Little,
b"B\0C\0D\0\0\0",
Operator::GreaterThan,
"A",
"BCD",
),
(
Endianness::Big,
b"\0B\0C\0D\0\0",
Operator::GreaterThan,
"A",
"BCD",
),
(
Endianness::Little,
b"B\0C\0D\0\0\0",
Operator::Equal,
"B",
"B",
),
(Endianness::Big, b"\0B\0C\0D\0\0", Operator::Equal, "B", "B"),
];
for (endian, buf, op, pat, want) in cases {
let child = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::AnyValue,
value: Value::Uint(0),
message: "child".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
};
let rule = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::String16 { endian: *endian },
op: op.clone(),
value: Value::String((*pat).to_string()),
message: "S=[%s]".to_string(),
children: vec![child],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx =
EvaluationContext::new(EvaluationConfig::default().with_stop_at_first_match(false));
let m = evaluate_rules(&[rule], buf, &mut ctx).unwrap();
assert_eq!(m.len(), 2, "{endian:?} {op:?}: parent and child match");
assert_eq!(
m[0].value,
Value::String((*want).to_string()),
"{endian:?} {op:?}: display value"
);
assert_eq!(
m[1].offset, 1,
"{endian:?} {op:?}: anchor at pattern length"
);
}
}