1use super::*;
4
5#[derive(Debug, Clone)]
7pub struct Found {
8 pub spec: Arc<Spec>,
9 pub overrides: Vec<PathBuf>,
10}
11
12#[derive(Debug, Clone, Default)]
14pub struct Registry {
15 pub specs: Vec<Found>,
16 pub fix: Vec<FixFound>,
18 pub dbc: Vec<DbcFound>,
21 pub errors: Vec<SpecError>,
23}
24
25#[derive(Debug, Clone)]
27pub struct DbcFound {
28 pub dbc: Arc<crate::formats::dbc::Dbc>,
29 pub path: PathBuf,
31}
32
33#[derive(Debug, Clone)]
35pub struct FixFound {
36 pub dict: Arc<crate::formats::fix::dict::Dictionary>,
37 pub overrides: Vec<PathBuf>,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Chosen {
43 SpecFile,
45 Named,
47 Glob,
48 Magic,
49}
50
51pub fn chosen_words(spec: &Spec, by: Chosen) -> String {
54 let chips = match by {
55 Chosen::Glob | Chosen::Magic => spec.match_chips_chosen(by),
56 Chosen::SpecFile | Chosen::Named => Vec::new(),
57 };
58 if chips.is_empty() {
59 format!("chosen by {}", by.words())
60 } else {
61 format!("matched by {}", chips_plain(&chips))
62 }
63}
64
65impl Chosen {
66 pub fn words(self) -> &'static str {
67 match self {
68 Self::SpecFile => "--format FILE",
69 Self::Named => "its name",
70 Self::Glob => "its glob",
71 Self::Magic => "its magic",
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct Matched {
79 pub specs: Vec<Arc<Spec>>,
80 pub by: Chosen,
81}
82
83pub fn search_path(
86 config_dir: Option<&Path>,
87 env: Option<std::ffi::OsString>,
88 configured: &[String],
89) -> Vec<PathBuf> {
90 let mut path = Vec::new();
91 if let Some(dir) = config_dir {
92 path.push(dir.join("formats"));
93 }
94 if let Some(env) = env {
95 path.extend(std::env::split_paths(&env).filter(|p| !p.as_os_str().is_empty()));
96 }
97 path.extend(
98 configured
99 .iter()
100 .filter(|p| !p.trim().is_empty())
101 .map(|p| crate::config::expand_path(p)),
102 );
103 path
104}
105
106pub fn search_path_for(config: &crate::config::AppConfig) -> Vec<PathBuf> {
109 let config_dir = crate::config::ConfigManager::new(crate::APP_NAME)
110 .ok()
111 .map(|m| m.config_dir().to_path_buf());
112 search_path(
113 config_dir.as_deref(),
114 std::env::var_os(PATH_VAR),
115 &config.formats.path,
116 )
117}
118
119impl Registry {
120 pub fn load(path: &[PathBuf]) -> Self {
125 let mut registry = Self::default();
126 for entry in path {
127 let files: Vec<PathBuf> = if entry.is_dir() {
128 let Ok(listing) = std::fs::read_dir(entry) else {
129 continue;
130 };
131 let mut files: Vec<PathBuf> = listing
132 .flatten()
133 .map(|e| e.path())
134 .filter(|p| {
135 p.is_file()
136 && p.extension().is_some_and(|e| {
137 e.eq_ignore_ascii_case("toml")
138 || e.eq_ignore_ascii_case("xml")
139 || e.eq_ignore_ascii_case("dbc")
140 })
141 })
142 .collect();
143 files.sort();
144 files
145 } else if entry.is_file() {
146 vec![entry.clone()]
147 } else {
148 continue;
149 };
150 for file in files {
151 let dbc_like = file.extension().is_some_and(|e| {
153 e.eq_ignore_ascii_case("dbc") || e.eq_ignore_ascii_case("toml")
154 });
155 if dbc_like {
156 match crate::formats::dbc::load(&file) {
157 Ok(Some(dbc)) => {
158 registry.dbc.push(DbcFound {
159 dbc: Arc::new(dbc),
160 path: file,
161 });
162 continue;
163 }
164 Err(e) => {
165 registry.errors.push(e);
166 continue;
167 }
168 Ok(None) => {}
169 }
170 }
171 match crate::formats::fix::dict::Dictionary::load(&file) {
172 Ok(Some(dict)) => {
173 registry.add_fix(dict, file);
174 continue;
175 }
176 Err(e) => {
177 registry.errors.push(e);
178 continue;
179 }
180 Ok(None)
182 if !file
183 .extension()
184 .is_some_and(|e| e.eq_ignore_ascii_case("toml")) =>
185 {
186 continue;
187 }
188 Ok(None) => {}
189 }
190 match Spec::load(&file) {
191 Ok(spec) => registry.add(spec, file),
192 Err(e) => registry.errors.push(e),
193 }
194 }
195 }
196 registry
197 }
198
199 fn add(&mut self, spec: Spec, file: PathBuf) {
200 if let Some(found) = self.specs.iter_mut().find(|f| f.spec.name == spec.name) {
201 found.overrides.push(file);
202 } else {
203 self.specs.push(Found {
204 spec: Arc::new(spec),
205 overrides: Vec::new(),
206 });
207 }
208 }
209
210 fn add_fix(&mut self, dict: crate::formats::fix::dict::Dictionary, file: PathBuf) {
211 if let Some(found) = self.fix.iter_mut().find(|f| f.dict.name == dict.name) {
212 found.overrides.push(file);
213 } else {
214 self.fix.push(FixFound {
215 dict: Arc::new(dict),
216 overrides: Vec::new(),
217 });
218 }
219 }
220
221 pub fn fix_dict(&self, name: &str) -> Option<&Arc<crate::formats::fix::dict::Dictionary>> {
223 self.fix
224 .iter()
225 .find(|f| f.dict.name == name)
226 .map(|f| &f.dict)
227 }
228
229 pub fn of(specs: Vec<Spec>) -> Self {
231 let mut registry = Self::default();
232 for spec in specs {
233 let file = spec.path.clone().unwrap_or_default();
234 registry.add(spec, file);
235 }
236 registry
237 }
238
239 pub fn get(&self, name: &str) -> Option<&Arc<Spec>> {
240 self.specs
241 .iter()
242 .find(|f| f.spec.name == name)
243 .map(|f| &f.spec)
244 }
245
246 pub fn is_empty(&self) -> bool {
247 self.specs.is_empty()
248 }
249
250 pub fn variants_of(&self, file: &Path) -> Option<Arc<Spec>> {
254 let globbed = self.by_glob(file, false);
255 let spec = if globbed.is_empty() {
256 let wanted = unnamed_may(file, false, false)?;
257 let compression = crate::CompressionFormat::from_extension(file);
258 if compression.is_some() || !self.specs.iter().any(|f| !f.spec.magic.is_empty()) {
259 return None;
260 }
261 self.matching_among(file, false, wanted, |reach| spec_head(file, None, reach))?
262 .specs
263 .into_iter()
264 .next()?
265 } else {
266 globbed.into_iter().find(|s| !s.is_delimited())?
267 };
268 spec.lists_variants().then_some(spec)
269 }
270
271 pub fn listed(&self, path: &Path, head: &[u8], whole: bool) -> Option<Arc<Spec>> {
276 if self.is_empty() || crate::CompressionFormat::from_extension(path).is_some() {
277 return None;
278 }
279 let wanted = unnamed_may(path, false, false)?;
280 let held = head.len() as u64;
281 let within = |s: &Spec| wanted(s) && (whole || s.match_reach() <= held);
282 self.matching_among(path, false, within, |_| Some(head.to_vec()))?
283 .specs
284 .into_iter()
285 .next()
286 }
287
288 pub fn by_glob(&self, path: &Path, is_dir: bool) -> Vec<Arc<Spec>> {
291 self.specs
292 .iter()
293 .map(|f| &f.spec)
294 .filter(|s| s.reads_directory() == is_dir && s.glob_matches(path))
295 .cloned()
296 .collect()
297 }
298
299 pub fn matching(
303 &self,
304 path: &Path,
305 is_dir: bool,
306 head: impl FnOnce(u64) -> Option<Vec<u8>>,
307 ) -> Option<Matched> {
308 self.matching_among(path, is_dir, |_| true, head)
309 }
310
311 pub fn matching_among(
313 &self,
314 path: &Path,
315 is_dir: bool,
316 wanted: impl Fn(&Spec) -> bool,
317 head: impl FnOnce(u64) -> Option<Vec<u8>>,
318 ) -> Option<Matched> {
319 let mut globbed = self.by_glob(path, is_dir);
320 globbed.retain(|s| wanted(s));
321 let (candidates, by) = if !globbed.is_empty() {
322 (globbed, Chosen::Glob)
323 } else if is_dir {
324 return None;
325 } else {
326 let magic: Vec<Arc<Spec>> = self
327 .specs
328 .iter()
329 .map(|f| &f.spec)
330 .filter(|s| !s.reads_directory() && !s.magic.is_empty() && wanted(s))
331 .cloned()
332 .collect();
333 (magic, Chosen::Magic)
334 };
335 let reach = candidates
336 .iter()
337 .map(|s| match by {
338 Chosen::Magic => s.match_reach(),
339 _ if s.expect.is_empty() => 0,
340 _ => s.match_reach(),
341 })
342 .max()
343 .unwrap_or(0);
344 let head = if reach > 0 && !is_dir {
345 head(reach)
346 } else {
347 None
348 };
349 let specs: Vec<Arc<Spec>> = candidates
350 .into_iter()
351 .filter(|s| {
352 let Some(head) = &head else {
353 return by == Chosen::Glob && s.expect.is_empty();
355 };
356 (by != Chosen::Magic || s.magic_matches(head)) && s.header_matches(head)
357 })
358 .collect();
359 (!specs.is_empty()).then_some(Matched { specs, by })
360 }
361
362 pub fn listing(&self, path: &[PathBuf]) -> String {
365 let mut out = String::new();
366 if self.specs.is_empty() {
367 out.push_str("No format specs found.\n");
368 }
369 for found in &self.specs {
370 let spec = &found.spec;
371 out.push_str(&spec.name);
372 let said = match_words(spec);
373 if spec.is_delimited() {
374 out.push_str(&format!(" (delimited; {said})"));
375 } else {
376 out.push_str(&format!(" ({said})"));
377 }
378 out.push('\n');
379 if let Some(description) = &spec.description {
380 out.push_str(&format!(" {description}\n"));
381 }
382 if let Some(file) = &spec.path {
383 out.push_str(&format!(" {}\n", file.display()));
384 }
385 for hidden in &found.overrides {
386 out.push_str(&format!(" overrides {}\n", hidden.display()));
387 }
388 }
389 if !self.fix.is_empty() {
390 out.push_str("\nDictionaries (FIX):\n");
391 }
392 for found in &self.fix {
393 let dict = &found.dict;
394 out.push_str(&dict.name);
395 let summary = dict.matcher.summary();
396 if !summary.is_empty() {
397 out.push_str(&format!(" ({summary})"));
398 }
399 out.push('\n');
400 if let Some(file) = &dict.path {
401 out.push_str(&format!(" {}\n", file.display()));
402 }
403 for hidden in &found.overrides {
404 out.push_str(&format!(" overrides {}\n", hidden.display()));
405 }
406 }
407 if !self.dbc.is_empty() {
408 out.push_str("\nDictionaries (DBC):\n");
409 }
410 for found in &self.dbc {
411 let dbc = &found.dbc;
412 out.push_str(&format!(
413 "{} ({}{})\n {}\n",
414 dbc.name,
415 crate::formats::text_formats::count(
416 dbc.messages.len() as u64,
417 "message",
418 "messages"
419 ),
420 dbc.interface
421 .as_ref()
422 .map(|i| format!(", interface {i}"))
423 .unwrap_or_default(),
424 found.path.display()
425 ));
426 }
427 if !self.errors.is_empty() {
428 out.push_str("\nCould not read:\n");
429 for e in &self.errors {
430 out.push_str(&format!(" {e}\n"));
431 }
432 }
433 out.push_str("\nSearched, in order:\n");
434 for entry in path {
435 out.push_str(&format!(" {}\n", entry.display()));
436 }
437 out
438 }
439}