1use std::path::Path;
20
21#[derive(Debug, Clone)]
23pub struct UserTag {
24 pub tag_id: u16,
25 pub name: String,
26 pub writable: Option<String>,
27 pub group: String,
28}
29
30#[derive(Debug, Clone)]
32pub struct Shortcut {
33 pub name: String,
34 pub tags: Vec<String>,
35}
36
37#[derive(Debug, Default)]
39pub struct Config {
40 pub user_tags: Vec<UserTag>,
41 pub shortcuts: Vec<Shortcut>,
42}
43
44impl Config {
45 pub fn load_default() -> Self {
47 let candidates = [
49 dirs_home().map(|h| h.join(".ExifTool_config")),
50 Some(std::path::PathBuf::from(".ExifTool_config")),
51 ];
52
53 for candidate in candidates.iter().flatten() {
54 if candidate.exists() {
55 if let Some(config) = Self::load(candidate) {
56 return config;
57 }
58 }
59 }
60
61 Self::default()
62 }
63
64 pub fn load<P: AsRef<Path>>(path: P) -> Option<Self> {
66 let content = std::fs::read_to_string(path).ok()?;
67 Some(Self::parse(&content))
68 }
69
70 fn parse(content: &str) -> Self {
72 let mut config = Config::default();
73
74 let lines: Vec<&str> = content
76 .lines()
77 .map(|l| l.split('#').next().unwrap_or("").trim())
78 .collect();
79 let text = lines.join("\n");
80
81 if let Some(start) = text.find("%Image::ExifTool::UserDefined") {
83 if let Some(paren_start) = text[start..].find('(') {
84 let block_start = start + paren_start + 1;
85 if let Some(block_end) = find_matching_paren(&text, block_start) {
86 let block = &text[block_start..block_end];
87 parse_user_tags(block, &mut config.user_tags);
88 }
89 }
90 }
91
92 if let Some(start) = text.find("Shortcuts") {
94 if let Some(paren_start) = text[start..].find('(') {
95 let block_start = start + paren_start + 1;
96 if let Some(block_end) = find_matching_paren(&text, block_start) {
97 let block = &text[block_start..block_end];
98 parse_shortcuts(block, &mut config.shortcuts);
99 }
100 }
101 }
102
103 config
104 }
105}
106
107fn parse_user_tags(block: &str, tags: &mut Vec<UserTag>) {
108 let mut pos = 0;
110 while let Some(hex_pos) = block[pos..].find("0x") {
111 let abs_pos = pos + hex_pos;
112 let rest = &block[abs_pos + 2..];
113
114 let hex_end = rest
116 .find(|c: char| !c.is_ascii_hexdigit())
117 .unwrap_or(rest.len());
118 if let Ok(tag_id) = u16::from_str_radix(&rest[..hex_end], 16) {
119 if let Some(name_pos) = rest.find("Name") {
121 let after_name = &rest[name_pos..];
122 if let Some(name) = extract_perl_string(after_name) {
123 tags.push(UserTag {
124 tag_id,
125 name: name.clone(),
126 writable: extract_after_key(after_name, "Writable"),
127 group: "UserDefined".to_string(),
128 });
129 }
130 }
131 }
132
133 pos = abs_pos + hex_end + 2;
134 }
135}
136
137fn parse_shortcuts(block: &str, shortcuts: &mut Vec<Shortcut>) {
138 for line in block.lines() {
140 let line = line.trim();
141 if let Some(arrow) = line.find("=>") {
142 let name = line[..arrow]
143 .trim()
144 .trim_matches('\'')
145 .trim_matches('"')
146 .to_string();
147 let rest = &line[arrow + 2..];
148
149 if let Some(bracket_start) = rest.find('[') {
151 if let Some(bracket_end) = rest.find(']') {
152 let array_content = &rest[bracket_start + 1..bracket_end];
153 let tags: Vec<String> = array_content
154 .split(',')
155 .map(|s| s.trim().trim_matches('\'').trim_matches('"').to_string())
156 .filter(|s| !s.is_empty())
157 .collect();
158
159 if !name.is_empty() && !tags.is_empty() {
160 shortcuts.push(Shortcut { name, tags });
161 }
162 }
163 }
164 }
165 }
166}
167
168fn extract_perl_string(text: &str) -> Option<String> {
169 let arrow = text.find("=>")?;
171 let rest = &text[arrow + 2..];
172 let rest = rest.trim();
173
174 if let Some(stripped) = rest.strip_prefix('\'') {
175 let end = stripped.find('\'')?;
176 Some(stripped[..end].to_string())
177 } else if let Some(stripped) = rest.strip_prefix('"') {
178 let end = stripped.find('"')?;
179 Some(stripped[..end].to_string())
180 } else {
181 None
182 }
183}
184
185fn extract_after_key(text: &str, key: &str) -> Option<String> {
186 let pos = text.find(key)?;
187 extract_perl_string(&text[pos..])
188}
189
190fn find_matching_paren(text: &str, start: usize) -> Option<usize> {
191 let mut depth = 1;
192 let bytes = text.as_bytes();
193 let mut i = start;
194 while i < bytes.len() && depth > 0 {
195 match bytes[i] {
196 b'(' => depth += 1,
197 b')' => {
198 depth -= 1;
199 if depth == 0 {
200 return Some(i);
201 }
202 }
203 _ => {}
204 }
205 i += 1;
206 }
207 None
208}
209
210fn dirs_home() -> Option<std::path::PathBuf> {
211 std::env::var("HOME").ok().map(std::path::PathBuf::from)
212}