use std::collections::HashMap;
use std::sync::Arc;
use log::warn;
use crate::parser::ast::{MagicRule, MetaType, TypeKind};
#[derive(Debug, Clone)]
struct Subroutine {
name_message: String,
rules: Arc<[MagicRule]>,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct NameTable {
inner: HashMap<String, Subroutine>,
}
impl NameTable {
#[must_use]
pub(crate) fn empty() -> Self {
Self {
inner: HashMap::new(),
}
}
#[must_use]
pub(crate) fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[must_use]
pub(crate) fn get(&self, name: &str) -> Option<Arc<[MagicRule]>> {
self.inner.get(name).map(|s| Arc::clone(&s.rules))
}
#[must_use]
pub(crate) fn name_message(&self, name: &str) -> Option<String> {
self.inner
.get(name)
.filter(|s| !s.name_message.is_empty())
.map(|s| s.name_message.clone())
}
pub(crate) fn merge(&mut self, other: Self) {
for (name, subroutine) in other.inner {
if self.inner.contains_key(&name) {
warn!("duplicate name definition '{name}' across magic files; keeping first");
continue;
}
self.inner.insert(name, subroutine);
}
}
}
pub(crate) fn extract_name_table(rules: Vec<MagicRule>) -> (Vec<MagicRule>, NameTable) {
let mut table = NameTable::empty();
let mut kept = Vec::with_capacity(rules.len());
for rule in rules {
if let TypeKind::Meta(MetaType::Name(ref name)) = rule.typ {
if table.inner.contains_key(name) {
warn!("duplicate name definition '{name}'; keeping first");
continue;
}
let name = name.clone();
let name_message = rule.message;
let children = scrub_nested_names(rule.children, rule.level);
table.inner.insert(
name,
Subroutine {
name_message,
rules: Arc::from(children),
},
);
} else {
let scrubbed_children = scrub_nested_names(rule.children, rule.level);
kept.push(MagicRule {
children: scrubbed_children,
..rule
});
}
}
(kept, table)
}
fn scrub_nested_names(children: Vec<MagicRule>, parent_level: u32) -> Vec<MagicRule> {
let mut kept = Vec::with_capacity(children.len());
for child in children {
if let TypeKind::Meta(MetaType::Name(ref name)) = child.typ {
warn!(
"name directive '{name}' at level {} under parent level {parent_level} is not top-level; skipping",
child.level
);
continue;
}
let scrubbed = scrub_nested_names(child.children, child.level);
kept.push(MagicRule {
children: scrubbed,
..child
});
}
kept
}
#[cfg(test)]
mod tests {
#![allow(clippy::unreachable)]
use super::*;
use crate::parser::ast::{OffsetSpec, Operator, Value};
fn make_rule(level: u32, typ: TypeKind, message: &str, children: Vec<MagicRule>) -> MagicRule {
MagicRule {
offset: OffsetSpec::Absolute(0),
typ,
op: Operator::Equal,
value: Value::Uint(0),
message: message.to_string(),
children,
level,
strength_modifier: None,
value_transform: None,
}
}
#[test]
fn test_extract_empty() {
let (rules, table) = extract_name_table(vec![]);
assert!(rules.is_empty());
assert!(table.get("anything").is_none());
}
#[test]
fn test_extract_single_name_rule() {
let child = make_rule(1, TypeKind::Byte { signed: false }, "child", vec![]);
let name_rule = make_rule(
0,
TypeKind::Meta(MetaType::Name("sub".to_string())),
"",
vec![child],
);
let (rules, table) = extract_name_table(vec![name_rule]);
assert!(rules.is_empty());
let subroutine = table.get("sub").expect("sub subroutine");
assert_eq!(subroutine.len(), 1);
assert_eq!(subroutine[0].message, "child");
}
#[test]
fn test_extract_captures_name_line_message() {
let child = make_rule(1, TypeKind::Byte { signed: false }, "child", vec![]);
let name_rule = make_rule(
0,
TypeKind::Meta(MetaType::Name("mach-o".to_string())),
"\\b [",
vec![child],
);
let bare = make_rule(
0,
TypeKind::Meta(MetaType::Name("bare".to_string())),
"",
vec![make_rule(1, TypeKind::Byte { signed: false }, "c2", vec![])],
);
let (rules, table) = extract_name_table(vec![name_rule, bare]);
assert!(rules.is_empty());
assert_eq!(table.get("mach-o").expect("mach-o body").len(), 1);
assert_eq!(table.name_message("mach-o").as_deref(), Some("\\b ["));
assert_eq!(table.name_message("bare"), None);
assert_eq!(table.name_message("nope"), None);
}
#[test]
fn test_extract_preserves_non_name_rules() {
let byte_rule = make_rule(0, TypeKind::Byte { signed: false }, "hello", vec![]);
let (rules, table) = extract_name_table(vec![byte_rule]);
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].message, "hello");
assert!(table.get("anything").is_none());
}
#[test]
fn test_extract_duplicate_name_keeps_first() {
let first = make_rule(
0,
TypeKind::Meta(MetaType::Name("dup".to_string())),
"first",
vec![make_rule(
1,
TypeKind::Byte { signed: false },
"first-child",
vec![],
)],
);
let second = make_rule(
0,
TypeKind::Meta(MetaType::Name("dup".to_string())),
"second",
vec![make_rule(
1,
TypeKind::Byte { signed: false },
"second-child",
vec![],
)],
);
let (_, table) = extract_name_table(vec![first, second]);
let subroutine = table.get("dup").expect("first dup kept");
assert_eq!(subroutine.len(), 1);
assert_eq!(subroutine[0].message, "first-child");
}
#[test]
fn test_merge_combines_tables() {
let sub_a = make_rule(
0,
TypeKind::Meta(MetaType::Name("a".to_string())),
"",
vec![],
);
let sub_b = make_rule(
0,
TypeKind::Meta(MetaType::Name("b".to_string())),
"",
vec![],
);
let (_, mut table_a) = extract_name_table(vec![sub_a]);
let (_, table_b) = extract_name_table(vec![sub_b]);
table_a.merge(table_b);
assert!(table_a.get("a").is_some());
assert!(table_a.get("b").is_some());
}
#[test]
fn test_merge_duplicate_keeps_existing() {
let first = make_rule(
0,
TypeKind::Meta(MetaType::Name("dup".to_string())),
"",
vec![make_rule(
1,
TypeKind::Byte { signed: false },
"first-child",
vec![],
)],
);
let second = make_rule(
0,
TypeKind::Meta(MetaType::Name("dup".to_string())),
"",
vec![make_rule(
1,
TypeKind::Byte { signed: false },
"second-child",
vec![],
)],
);
let (_, mut table_a) = extract_name_table(vec![first]);
let (_, table_b) = extract_name_table(vec![second]);
table_a.merge(table_b);
let subroutine = table_a.get("dup").expect("dup kept from first table");
assert_eq!(subroutine[0].message, "first-child");
}
}