use libmagic_rs::evaluator::{EvaluationContext, evaluate_rules};
use libmagic_rs::parser::ast::PStringLengthWidth;
use libmagic_rs::{Endianness, EvaluationConfig, MagicRule, OffsetSpec, Operator, TypeKind, Value};
fn cfg() -> EvaluationConfig {
EvaluationConfig::default().with_stop_at_first_match(false)
}
fn child_rule(offset: OffsetSpec, typ: TypeKind, value: Value, message: &str) -> MagicRule {
MagicRule {
offset,
typ,
op: Operator::Equal,
value,
message: message.to_string(),
children: vec![],
level: 1,
strength_modifier: None,
value_transform: None,
}
}
#[test]
fn relative_child_after_long_parent() {
let buffer = [0x78, 0x56, 0x34, 0x12, 0xBE, 0xBA, 0xFE, 0xCA];
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
op: Operator::Equal,
value: Value::Uint(0x1234_5678),
message: "parent-long".to_string(),
children: vec![child_rule(
OffsetSpec::Relative(0),
TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
Value::Uint(0xCAFE_BABE),
"child-long",
)],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[parent], &buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 2, "expected parent + child match");
assert_eq!(matches[0].message, "parent-long");
assert_eq!(matches[0].offset, 0);
assert_eq!(matches[1].message, "child-long");
assert_eq!(matches[1].offset, 4);
}
#[test]
fn relative_child_with_positive_delta() {
let buffer = [0x7F, 0xAA, 0xBB, 0x42, 0xCC];
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x7F),
message: "p".to_string(),
children: vec![child_rule(
OffsetSpec::Relative(2),
TypeKind::Byte { signed: false },
Value::Uint(0x42),
"c",
)],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[parent], &buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 2);
assert_eq!(matches[1].offset, 3);
}
#[test]
fn relative_child_with_negative_delta() {
let buffer = [0x00, 0xAA, 0x00, 0x00, 0x78, 0x56, 0x34, 0x12, 0x00];
let parent = MagicRule {
offset: OffsetSpec::Absolute(4),
typ: TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
op: Operator::Equal,
value: Value::Uint(0x1234_5678),
message: "p".to_string(),
children: vec![child_rule(
OffsetSpec::Relative(-7),
TypeKind::Byte { signed: false },
Value::Uint(0xAA),
"c",
)],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[parent], &buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 2);
assert_eq!(matches[1].offset, 1);
assert_eq!(matches[1].value, Value::Uint(0xAA));
}
#[test]
fn relative_chain_marches_forward() {
let buffer = [
0x78, 0x56, 0x34, 0x12, 0xBE, 0xBA, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE, ];
let leaf = child_rule(
OffsetSpec::Relative(0),
TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
Value::Uint(0xDEAD_BEEF),
"leaf",
);
let mut middle = child_rule(
OffsetSpec::Relative(0),
TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
Value::Uint(0xCAFE_BABE),
"middle",
);
middle.children = vec![leaf];
let root = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
op: Operator::Equal,
value: Value::Uint(0x1234_5678),
message: "root".to_string(),
children: vec![middle],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[root], &buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 3);
let offsets: Vec<usize> = matches.iter().map(|m| m.offset).collect();
assert_eq!(offsets, vec![0, 4, 8]);
}
#[test]
fn relative_after_string_parent_includes_nul_terminator() {
let buffer = b"MZ\x00\x42rest";
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::String { max_length: None },
op: Operator::Equal,
value: Value::String("MZ".to_string()),
message: "mz".to_string(),
children: vec![child_rule(
OffsetSpec::Relative(0),
TypeKind::Byte { signed: false },
Value::Uint(0x42),
"byte-after-mz",
)],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[parent], buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 2, "child should match after MZ + NUL");
assert_eq!(matches[1].offset, 3);
}
#[test]
fn relative_after_pstring_parent_consumes_prefix_and_payload() {
let buffer = b"\x05Hello\x42tail";
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::PString {
max_length: None,
length_width: PStringLengthWidth::OneByte,
length_includes_itself: false,
},
op: Operator::Equal,
value: Value::String("Hello".to_string()),
message: "pstr".to_string(),
children: vec![child_rule(
OffsetSpec::Relative(0),
TypeKind::Byte { signed: false },
Value::Uint(0x42),
"byte-after-pstr",
)],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[parent], buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 2);
assert_eq!(matches[1].offset, 6);
}
#[test]
fn relative_top_level_resolves_from_zero_anchor() {
let buffer = [0xAA, 0xBB, 0x42, 0xCC];
let rule = MagicRule {
offset: OffsetSpec::Relative(2),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x42),
message: "top".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[rule], &buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].offset, 2);
}
#[test]
fn relative_sibling_propagation_at_top_level() {
let buffer = [0x78, 0x56, 0x34, 0x12, 0x42, 0x00, 0x00, 0x00];
let first = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
op: Operator::Equal,
value: Value::Uint(0x1234_5678),
message: "first".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let second = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x42),
message: "second".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[first, second], &buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 2);
assert_eq!(matches[0].offset, 0);
assert_eq!(matches[1].offset, 4);
}
#[test]
fn relative_out_of_bounds_skips_child_gracefully() {
let buffer = [0x7F, 0xAA, 0xBB];
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x7F),
message: "p".to_string(),
children: vec![child_rule(
OffsetSpec::Relative(50),
TypeKind::Byte { signed: false },
Value::Uint(0x00),
"c",
)],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[parent], &buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 1, "only the parent should match");
assert_eq!(matches[0].message, "p");
}
#[test]
fn relative_anchor_can_decrease_when_later_sibling_matches_at_lower_position() {
let buffer = [
0x00, 0x00, 0xAA, 0x99, 0x00, 0x00, 0x00, 0x00, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, ];
let rule_a = MagicRule {
offset: OffsetSpec::Absolute(8),
typ: TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
op: Operator::Equal,
value: Value::Uint(0x1234_5678),
message: "rule-a-high".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let rule_b = MagicRule {
offset: OffsetSpec::Absolute(2),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0xAA),
message: "rule-b-low".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let rule_c = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x99),
message: "rule-c-relative".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[rule_a, rule_b, rule_c], &buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 3, "all three rules should match");
assert_eq!(matches[0].message, "rule-a-high");
assert_eq!(matches[0].offset, 8);
assert_eq!(matches[1].message, "rule-b-low");
assert_eq!(matches[1].offset, 2);
assert_eq!(
matches[2].offset, 3,
"rule C must read at offset 3 (rule B's end), proving the anchor moved backwards from 12 -> 3"
);
}
#[test]
fn relative_anchor_persists_across_non_matching_intermediate_sibling() {
let buffer = [0x78, 0x56, 0x34, 0x12, 0x42, 0x00, 0x00, 0x00];
let first = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
op: Operator::Equal,
value: Value::Uint(0x1234_5678),
message: "first".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let middle_no_match = MagicRule {
offset: OffsetSpec::Absolute(4),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0xDE), message: "middle-skip".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let third = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x42),
message: "third".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[first, middle_no_match, third], &buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 2, "first + third match, middle skipped");
assert_eq!(matches[0].message, "first");
assert_eq!(matches[1].message, "third");
assert_eq!(matches[1].offset, 4);
}
#[test]
fn relative_anchor_resets_between_evaluations_via_reset() {
let buffer_a = [0x78, 0x56, 0x34, 0x12];
let buffer_b = [0x42, 0xAA, 0xBB, 0xCC];
let pass_one = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Long {
endian: Endianness::Little,
signed: false,
},
op: Operator::Equal,
value: Value::Uint(0x1234_5678),
message: "pass-one".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let pass_two = MagicRule {
offset: OffsetSpec::Relative(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x42),
message: "pass-two".to_string(),
children: vec![],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let _ = evaluate_rules(&[pass_one], &buffer_a, &mut ctx).unwrap();
ctx.reset();
let matches = evaluate_rules(&[pass_two], &buffer_b, &mut ctx).unwrap();
assert_eq!(matches.len(), 1);
assert_eq!(
matches[0].offset, 0,
"Relative(0) should resolve to 0 after reset"
);
}
#[test]
fn relative_underflow_skips_child_gracefully() {
let buffer = [0x7F, 0xAA];
let parent = MagicRule {
offset: OffsetSpec::Absolute(0),
typ: TypeKind::Byte { signed: false },
op: Operator::Equal,
value: Value::Uint(0x7F),
message: "p".to_string(),
children: vec![child_rule(
OffsetSpec::Relative(-100),
TypeKind::Byte { signed: false },
Value::Uint(0x00),
"c",
)],
level: 0,
strength_modifier: None,
value_transform: None,
};
let mut ctx = EvaluationContext::new(cfg());
let matches = evaluate_rules(&[parent], &buffer, &mut ctx).unwrap();
assert_eq!(matches.len(), 1);
}