use std::path::Path;
#[derive(Debug, Clone)]
pub struct UserTag {
pub tag_id: u16,
pub name: String,
pub writable: Option<String>,
pub group: String,
}
#[derive(Debug, Clone)]
pub struct Shortcut {
pub name: String,
pub tags: Vec<String>,
}
#[derive(Debug, Default)]
pub struct Config {
pub user_tags: Vec<UserTag>,
pub shortcuts: Vec<Shortcut>,
}
impl Config {
pub fn load_default() -> Self {
let candidates = [
dirs_home().map(|h| h.join(".ExifTool_config")),
Some(std::path::PathBuf::from(".ExifTool_config")),
];
for candidate in candidates.iter().flatten() {
if candidate.exists() {
if let Some(config) = Self::load(candidate) {
return config;
}
}
}
Self::default()
}
pub fn load<P: AsRef<Path>>(path: P) -> Option<Self> {
let content = std::fs::read_to_string(path).ok()?;
Some(Self::parse(&content))
}
fn parse(content: &str) -> Self {
let mut config = Config::default();
let lines: Vec<&str> = content
.lines()
.map(|l| l.split('#').next().unwrap_or("").trim())
.collect();
let text = lines.join("\n");
if let Some(start) = text.find("%Image::ExifTool::UserDefined") {
if let Some(paren_start) = text[start..].find('(') {
let block_start = start + paren_start + 1;
if let Some(block_end) = find_matching_paren(&text, block_start) {
let block = &text[block_start..block_end];
parse_user_tags(block, &mut config.user_tags);
}
}
}
if let Some(start) = text.find("Shortcuts") {
if let Some(paren_start) = text[start..].find('(') {
let block_start = start + paren_start + 1;
if let Some(block_end) = find_matching_paren(&text, block_start) {
let block = &text[block_start..block_end];
parse_shortcuts(block, &mut config.shortcuts);
}
}
}
config
}
}
fn parse_user_tags(block: &str, tags: &mut Vec<UserTag>) {
let mut pos = 0;
while let Some(hex_pos) = block[pos..].find("0x") {
let abs_pos = pos + hex_pos;
let rest = &block[abs_pos + 2..];
let hex_end = rest
.find(|c: char| !c.is_ascii_hexdigit())
.unwrap_or(rest.len());
if let Ok(tag_id) = u16::from_str_radix(&rest[..hex_end], 16) {
if let Some(name_pos) = rest.find("Name") {
let after_name = &rest[name_pos..];
if let Some(name) = extract_perl_string(after_name) {
tags.push(UserTag {
tag_id,
name: name.clone(),
writable: extract_after_key(after_name, "Writable"),
group: "UserDefined".to_string(),
});
}
}
}
pos = abs_pos + hex_end + 2;
}
}
fn parse_shortcuts(block: &str, shortcuts: &mut Vec<Shortcut>) {
for line in block.lines() {
let line = line.trim();
if let Some(arrow) = line.find("=>") {
let name = line[..arrow]
.trim()
.trim_matches('\'')
.trim_matches('"')
.to_string();
let rest = &line[arrow + 2..];
if let Some(bracket_start) = rest.find('[') {
if let Some(bracket_end) = rest.find(']') {
let array_content = &rest[bracket_start + 1..bracket_end];
let tags: Vec<String> = array_content
.split(',')
.map(|s| s.trim().trim_matches('\'').trim_matches('"').to_string())
.filter(|s| !s.is_empty())
.collect();
if !name.is_empty() && !tags.is_empty() {
shortcuts.push(Shortcut { name, tags });
}
}
}
}
}
}
fn extract_perl_string(text: &str) -> Option<String> {
let arrow = text.find("=>")?;
let rest = &text[arrow + 2..];
let rest = rest.trim();
if let Some(stripped) = rest.strip_prefix('\'') {
let end = stripped.find('\'')?;
Some(stripped[..end].to_string())
} else if let Some(stripped) = rest.strip_prefix('"') {
let end = stripped.find('"')?;
Some(stripped[..end].to_string())
} else {
None
}
}
fn extract_after_key(text: &str, key: &str) -> Option<String> {
let pos = text.find(key)?;
extract_perl_string(&text[pos..])
}
fn find_matching_paren(text: &str, start: usize) -> Option<usize> {
let mut depth = 1;
let bytes = text.as_bytes();
let mut i = start;
while i < bytes.len() && depth > 0 {
match bytes[i] {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
i += 1;
}
None
}
fn dirs_home() -> Option<std::path::PathBuf> {
std::env::var("HOME").ok().map(std::path::PathBuf::from)
}