use super::*;
#[test]
fn test_use_known_name_evaluates_subroutine() {
let subroutine = vec![MagicRule {
offset: OffsetSpec::Absolute(3),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x42),
message: "sub-match".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
}];
let table = build_name_table(vec![("part2", subroutine)]);
let mut context = make_context_with_env(table, &[]);
let buffer = [0x00u8, 0x00, 0x00, 0x42, 0x00];
let rules = vec![use_rule("part2")];
let matches = evaluate_rules(&rules, &buffer, &mut context).unwrap();
assert_eq!(
matches.len(),
1,
"subroutine should produce exactly one match"
);
assert_eq!(matches[0].message, "sub-match");
}
#[test]
fn test_use_unknown_name_returns_no_match() {
let table = NameTable::empty();
let mut context = make_context_with_env(table, &[]);
let buffer = [0x00u8, 0x42];
let rules = vec![use_rule("nonexistent")];
let matches = evaluate_rules(&rules, &buffer, &mut context).unwrap();
assert!(matches.is_empty(), "unknown name should yield no matches");
}
#[test]
fn test_use_without_rule_env_returns_no_match() {
let mut context = EvaluationContext::new(EvaluationConfig::default());
let buffer = [0x00u8, 0x42];
let rules = vec![use_rule("part2")];
let matches = evaluate_rules(&rules, &buffer, &mut context).unwrap();
assert!(
matches.is_empty(),
"Use with no rule_env should produce no matches"
);
}
#[test]
fn test_use_recursion_limit() {
let a_body = vec![use_rule("b")];
let b_body = vec![use_rule("a")];
let table = build_name_table(vec![("a", a_body), ("b", b_body)]);
let mut context = make_context_with_env(table, &[]);
let buffer = [0u8; 8];
let rules = vec![use_rule("a")];
let result = evaluate_rules(&rules, &buffer, &mut context);
assert!(
matches!(
result,
Err(LibmagicError::EvaluationError(
crate::error::EvaluationError::RecursionLimitExceeded { .. }
))
),
"mutual recursion through use must surface RecursionLimitExceeded, got {result:?}"
);
}
#[test]
fn test_use_child_rules_evaluated_after_subroutine() {
let config = EvaluationConfig {
stop_at_first_match: false,
..EvaluationConfig::default()
};
let subroutine = vec![MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0xAA),
message: "sub-head".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
}];
let table = build_name_table(vec![("sub", subroutine)]);
let env = std::sync::Arc::new(RuleEnvironment {
name_table: std::sync::Arc::new(table),
root_rules: std::sync::Arc::from(&[] as &[MagicRule]),
});
let mut context = EvaluationContext::new(config).with_rule_env(env);
let buffer = [0xAAu8, 0xBB, 0xCC];
let sibling = MagicRule {
offset: OffsetSpec::Absolute(1),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0xBB),
message: "sibling".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let rules = vec![use_rule("sub"), sibling];
let matches = evaluate_rules(&rules, &buffer, &mut context).unwrap();
assert_eq!(matches.len(), 2);
assert_eq!(matches[0].message, "sub-head");
assert_eq!(matches[1].message, "sibling");
}
#[test]
fn test_use_stop_at_first_match_short_circuits_siblings() {
let subroutine = vec![MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0xAA),
message: "sub-head".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
}];
let table = build_name_table(vec![("sub", subroutine)]);
let mut context = make_context_with_env(table, &[]);
let buffer = [0xAAu8, 0xBB, 0xCC];
let sibling = MagicRule {
offset: OffsetSpec::Absolute(1),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0xBB),
message: "sibling".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let rules = vec![use_rule("sub"), sibling];
let matches = evaluate_rules(&rules, &buffer, &mut context).unwrap();
assert_eq!(
matches.len(),
1,
"stop-at-first-match must halt sibling iteration once the use path produces a match"
);
assert_eq!(matches[0].message, "sub-head");
}
#[test]
fn test_use_rule_children_are_evaluated() {
let subroutine = vec![MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0xAA),
message: "sub-head".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
}];
let table = build_name_table(vec![("sub", subroutine)]);
let config = EvaluationConfig {
stop_at_first_match: false,
..EvaluationConfig::default()
};
let env = std::sync::Arc::new(RuleEnvironment {
name_table: std::sync::Arc::new(table),
root_rules: std::sync::Arc::from(&[] as &[MagicRule]),
});
let mut context = EvaluationContext::new(config).with_rule_env(env);
let child = MagicRule {
offset: OffsetSpec::Absolute(1),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0xBB),
message: "use-child".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
};
let mut use_with_child = use_rule("sub");
use_with_child.children = vec![child];
let buffer = [0xAAu8, 0xBB, 0xCC];
let matches = evaluate_rules(&[use_with_child], &buffer, &mut context).unwrap();
assert_eq!(
matches.len(),
2,
"use rule's own children must run after the subroutine"
);
assert_eq!(matches[0].message, "sub-head");
assert_eq!(matches[1].message, "use-child");
}
#[test]
fn test_name_rule_leaked_is_noop() {
let leaked = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Meta(MetaType::Name("orphan".to_string())),
op: Operator::Equal,
value: Value::Uint(0),
message: String::new(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut context = EvaluationContext::new(EvaluationConfig::default());
let matches = evaluate_rules(&[leaked], &[0u8; 4], &mut context).unwrap();
assert!(matches.is_empty(), "leaked Name rule should be a no-op");
}
#[test]
fn test_use_subroutine_absolute_offset_biased_by_use_site() {
let config = EvaluationConfig {
stop_at_first_match: false,
..EvaluationConfig::default()
};
let subroutine_body = vec![byte_eq_rule(0, 0x42, "sub-match-at-base")];
let name_table = build_name_table(vec![("sub", subroutine_body)]);
let env = std::sync::Arc::new(RuleEnvironment {
name_table: std::sync::Arc::new(name_table),
root_rules: std::sync::Arc::from(&[] as &[MagicRule]),
});
let mut buffer = vec![0u8; 16];
buffer[8] = 0x42;
let mut context = EvaluationContext::new(config).with_rule_env(env);
let rules = vec![use_rule_at("sub", 8)];
let matches = evaluate_rules(&rules, &buffer, &mut context).unwrap();
assert!(
matches.iter().any(|m| m.message == "sub-match-at-base"),
"subroutine rule at Absolute(0) must be biased by use-site offset 8 \
-- reading buffer[8] = 0x42. If bias missing, reads buffer[0] = 0x00 \
and the test fails. got {matches:?}"
);
}
#[test]
fn test_use_subroutine_relative_offset_unaffected_by_use_site() {
let config = EvaluationConfig {
stop_at_first_match: false,
..EvaluationConfig::default()
};
let mut rel_rule = byte_eq_rule(0, 0x42, "rel-sub-match");
rel_rule.offset = OffsetSpec::Relative(0);
let subroutine_body = vec![rel_rule];
let name_table = build_name_table(vec![("rsub", subroutine_body)]);
let env = std::sync::Arc::new(RuleEnvironment {
name_table: std::sync::Arc::new(name_table),
root_rules: std::sync::Arc::from(&[] as &[MagicRule]),
});
let mut buffer = vec![0u8; 16];
buffer[5] = 0x42;
let mut context = EvaluationContext::new(config).with_rule_env(env);
let rules = vec![use_rule_at("rsub", 5)];
let matches = evaluate_rules(&rules, &buffer, &mut context).unwrap();
assert!(
matches.iter().any(|m| m.message == "rel-sub-match"),
"subroutine Relative(0) rule must read at use-site (5) via last_match_end, \
not at use-site+base (10). got {matches:?}"
);
}
#[test]
fn test_continuation_sibling_reset_after_bytes_consumed() {
let config = EvaluationConfig {
stop_at_first_match: false,
..EvaluationConfig::default()
};
let long_sibling = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Long {
endian: crate::parser::ast::Endianness::Little,
signed: false,
},
op: Operator::Equal,
value: Value::Uint(0x0403_0201),
message: "long-sibling".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
};
let byte_sibling = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x01),
message: "byte-sibling-sees-parent-anchor".to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
};
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x01),
message: "parent".to_string(),
children: vec![long_sibling, byte_sibling],
level: 0,
strength_modifier: None,
value_transform: None,
};
let buffer = [0x01u8, 0x01, 0x02, 0x03, 0x04, 0x42, 0x00];
let mut context = EvaluationContext::new(config);
let matches = evaluate_rules(&[parent], &buffer, &mut context).unwrap();
let messages: Vec<&str> = matches.iter().map(|m| m.message.as_str()).collect();
assert_eq!(
messages,
vec!["parent", "long-sibling", "byte-sibling-sees-parent-anchor"],
"byte-sibling must read buffer[1]=0x01 via parent-level anchor reset; \
if reset is missing it reads buffer[5]=0x42 and test fails. got {matches:?}"
);
}