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_consumes_extra_file_bytes_for_anchor() {
let child = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(b'!'.into()),
message: "exclaim".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, "exclaim");
}
#[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"
);
}