use std::path::{Path, PathBuf};
use std::sync::OnceLock;
static REGISTRY: OnceLock<Vec<(u32, &'static str)>> = OnceLock::new();
static OVERRIDE: OnceLock<Vec<(u32, &'static str)>> = OnceLock::new();
pub fn all() -> &'static [(u32, &'static str)] {
if let Some(installed) = OVERRIDE.get() {
return installed;
}
REGISTRY.get_or_init(|| {
if cfg!(test) {
crate::formats::BUILTIN_MAGICS.to_vec()
} else {
registry_for_entries(&user_entries())
}
})
}
#[allow(dead_code)]
pub(crate) fn registry_for(path: &Path) -> Vec<(u32, &'static str)> {
registry_for_entries(&parse_file(path))
}
fn registry_for_entries(user: &[(u32, String)]) -> Vec<(u32, &'static str)> {
merge(crate::formats::BUILTIN_MAGICS, user)
}
#[allow(dead_code)]
pub(crate) fn install(entries: Vec<(u32, &'static str)>) -> bool {
OVERRIDE.set(entries).is_ok()
}
pub fn name_of(magic: u32) -> Option<&'static str> {
all().iter().find(|(m, _)| *m == magic).map(|(_, n)| *n)
}
pub fn by_name(name: &str) -> Option<&'static str> {
all().iter().find(|(_, n)| *n == name).map(|(_, n)| *n)
}
pub(crate) fn registry_path() -> Option<PathBuf> {
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))?;
Some(base.join("zdbview").join("magics"))
}
fn user_entries() -> Vec<(u32, String)> {
registry_path()
.as_deref()
.map(parse_file)
.unwrap_or_default()
}
pub(crate) fn parse_file(path: &Path) -> Vec<(u32, String)> {
std::fs::read_to_string(path)
.map(|s| parse(&s))
.unwrap_or_default()
}
pub(crate) fn parse(contents: &str) -> Vec<(u32, String)> {
let mut out = Vec::new();
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((tag, name)) = line.split_once('=') else {
continue;
};
let (tag, name) = (tag.trim(), name.trim());
if name.is_empty() {
continue;
}
if let Some(m) = parse_tag(tag) {
out.push((m, name.to_string()));
}
}
out
}
pub(crate) fn parse_tag(tag: &str) -> Option<u32> {
if let Some(hex) = tag.strip_prefix("0x").or_else(|| tag.strip_prefix("0X")) {
return u32::from_str_radix(&hex.replace('_', ""), 16).ok();
}
let b = tag.as_bytes();
(b.len() == 4 && b.iter().all(|c| c.is_ascii_graphic()))
.then(|| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
fn merge(builtin: &[(u32, &'static str)], user: &[(u32, String)]) -> Vec<(u32, &'static str)> {
let mut out: Vec<(u32, &'static str)> = builtin.to_vec();
for (magic, name) in user {
if out.iter().any(|(m, n)| m == magic && n == name) {
continue;
}
let leaked: &'static str = Box::leak(name.clone().into_boxed_str());
match out.iter_mut().find(|(m, _)| m == magic) {
Some(slot) => slot.1 = leaked,
None => out.push((*magic, leaked)),
}
}
out
}
#[cfg(test)]
mod tests {
use super::{merge, parse, parse_tag};
#[test]
fn a_tag_is_four_characters_or_explicit_hex() {
assert_eq!(parse_tag("ZRSC"), Some(0x5A52_5343));
assert_eq!(parse_tag("0x5A52_5343"), Some(0x5A52_5343));
assert_eq!(parse_tag("ZRS"), None);
assert_eq!(parse_tag("ZRSCX"), None);
assert_eq!(parse_tag("ZR C"), None);
assert_eq!(parse_tag("0xnothex"), None);
}
#[test]
fn bad_lines_do_not_cost_the_good_ones() {
let got =
parse("# a comment\n\nLUAR = luars cache (LUAR)\ngarbage\nNOPE =\n0x1234 = raw tag\n");
assert_eq!(
got,
vec![
(0x4C55_4152, "luars cache (LUAR)".to_string()),
(0x0000_1234, "raw tag".to_string()),
]
);
}
#[test]
fn a_user_entry_renames_a_builtin_and_adds_a_new_tag() {
let builtin: &[(u32, &'static str)] = &[(1, "one"), (2, "two")];
let user = vec![(2, "TWO, renamed".to_string()), (3, "three".to_string())];
assert_eq!(
merge(builtin, &user),
vec![(1, "one"), (2, "TWO, renamed"), (3, "three")]
);
}
}