1use super::*;
4
5pub fn head_of(
7 path: &Path,
8 compression: Option<crate::CompressionFormat>,
9 reach: u64,
10) -> Option<Vec<u8>> {
11 use std::io::Read;
12 let file = std::fs::File::open(path).ok()?;
13 let reader: Box<dyn Read> = match compression {
14 None => Box::new(file),
15 Some(crate::CompressionFormat::Gzip) => Box::new(flate2::read::GzDecoder::new(file)),
16 Some(crate::CompressionFormat::Zstd) => Box::new(zstd::Decoder::new(file).ok()?),
17 Some(crate::CompressionFormat::Bzip2) => Box::new(bzip2::read::BzDecoder::new(file)),
18 Some(crate::CompressionFormat::Xz) => Box::new(xz2::read::XzDecoder::new(file)),
19 };
20 let mut head = Vec::new();
21 reader
22 .take(reach.min(MAX_MATCH_READ))
23 .read_to_end(&mut head)
24 .ok()?;
25 Some(head)
26}
27
28pub struct Read {
30 pub spec: Arc<Spec>,
31 pub by: Chosen,
32 pub also: Vec<String>,
34 pub notes: Vec<String>,
36 pub header: HeaderValues,
37 pub records: Arc<dyn SpecRecords>,
38}
39
40impl std::fmt::Debug for Read {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("Read")
43 .field("spec", &self.spec.name)
44 .field("by", &self.by)
45 .field("also", &self.also)
46 .field("rows", &self.records.rows())
47 .finish()
48 }
49}
50
51#[derive(Debug, Clone, Default)]
53pub struct Asked {
54 pub spec_file: Option<PathBuf>,
56 pub spec_name: Option<String>,
58 pub spec: Option<Arc<Spec>>,
60 pub variant: Option<String>,
63 pub builtin: bool,
65 pub compression: Option<crate::CompressionFormat>,
66 pub text_only: bool,
68}
69
70#[derive(Clone)]
72pub struct Choice {
73 pub spec: Arc<Spec>,
74 pub by: Chosen,
75 pub also: Vec<String>,
76}
77
78pub enum Route {
80 Elsewhere,
82 Read(Box<Read>),
83 Decompress(Choice),
85 Delimited(Choice),
87}
88
89pub(crate) fn delimited_spec_keys() -> Vec<&'static str> {
93 use datui_cli::settings::{OPEN, SETTINGS};
94 let mut keys = vec![
95 "name",
96 "description",
97 "documentation",
98 "kind",
99 "match",
100 "metadata_line",
101 "columns",
102 ];
103 keys.extend(SETTINGS.iter().filter_map(|s| s.spec));
104 keys.extend(OPEN.iter().filter_map(|o| o.spec));
105 keys
106}
107
108pub(crate) fn unnamed_may(
113 path: &Path,
114 is_dir: bool,
115 text_only: bool,
116) -> Option<impl Fn(&Spec) -> bool> {
117 let said = (!is_dir)
118 .then(|| crate::home::discover::data_format(path))
119 .flatten()
120 .filter(|f| !f.is_lines());
122 let parquet_key =
123 crate::home::discover::is_parquet_key(&crate::home::discover::directory_and_name(path));
124 let records_may = said.is_none() && !parquet_key && !text_only;
125 let text_may =
126 !is_dir && !parquet_key && said.is_none_or(|f| crate::FileFormat::separator(f).is_some());
127 (records_may || text_may).then_some(move |s: &Spec| {
128 if s.is_delimited() {
129 text_may
130 } else {
131 records_may
132 }
133 })
134}
135
136pub(crate) fn spec_head(
140 path: &Path,
141 compression: Option<crate::CompressionFormat>,
142 reach: u64,
143) -> Option<Vec<u8>> {
144 if compression.is_none() && crate::home::discover::sniff_format(path).is_some() {
145 return None;
146 }
147 head_of(path, compression, reach)
148}
149
150pub fn route(path: &Path, asked: &Asked, registry: &Registry) -> Result<Route, String> {
154 let compression = asked.compression.or_else(|| {
155 path.is_file()
156 .then(|| crate::CompressionFormat::from_extension(path))
157 .flatten()
158 });
159 let named = path.file_name().map_or_else(
160 || path.display().to_string(),
161 |n| n.to_string_lossy().into_owned(),
162 );
163 let explicit = if let Some(spec) = &asked.spec {
164 Some(Choice {
165 spec: spec.clone(),
166 by: Chosen::SpecFile,
167 also: Vec::new(),
168 })
169 } else if let Some(file) = &asked.spec_file {
170 if crate::cloud::source::is_remote_url(file) {
171 return Err(format!(
172 "{}: a remote spec is fetched by the open, and was not",
173 file.display()
174 ));
175 }
176 let spec = Spec::load(file).map_err(|e| e.to_string())?;
177 Some(Choice {
178 spec: Arc::new(spec),
179 by: Chosen::SpecFile,
180 also: Vec::new(),
181 })
182 } else if let Some(name) = &asked.spec_name {
183 let spec = registry.get(name).ok_or_else(|| {
184 format!("no format named {name} on the search path; `datui formats` lists them")
185 })?;
186 Some(Choice {
187 spec: spec.clone(),
188 by: Chosen::Named,
189 also: Vec::new(),
190 })
191 } else {
192 None
193 };
194 if asked.text_only
195 && let Some(choice) = &explicit
196 && !choice.spec.is_delimited()
197 {
198 return Err(format!(
199 "{} reads one file, or one directory of column files",
200 choice.spec.name
201 ));
202 }
203 let choice = match explicit {
204 Some(choice) => choice,
205 None => {
206 if asked.builtin || registry.is_empty() {
207 return Ok(Route::Elsewhere);
208 }
209 let is_dir = path.is_dir();
210 let Some(wanted) = unnamed_may(path, is_dir, asked.text_only) else {
211 return Ok(Route::Elsewhere);
212 };
213 let inner = match compression {
215 Some(_) => path.with_extension(""),
216 None => path.to_path_buf(),
217 };
218 let matched = registry.matching_among(&inner, is_dir, wanted, |reach| {
219 spec_head(path, compression, reach)
220 });
221 let Some(matched) = matched else {
222 return Ok(Route::Elsewhere);
223 };
224 let mut specs = matched.specs.into_iter();
225 let spec = specs.next().expect("a match has a spec");
226 Choice {
227 spec,
228 by: matched.by,
229 also: specs.map(|s| s.name.clone()).collect(),
230 }
231 }
232 };
233 if choice.spec.is_delimited() {
235 return Ok(Route::Delimited(choice));
236 }
237 let choice = match &asked.variant {
238 Some(variant) => Choice {
239 spec: Arc::new(choice.spec.with_variant(variant)?),
240 ..choice
241 },
242 None => choice,
243 };
244 if compression.is_some() && path.is_file() {
245 return Ok(Route::Decompress(choice));
246 }
247 read(path, &named, choice).map(|r| Route::Read(Box::new(r)))
248}
249
250pub fn read(path: &Path, named: &str, choice: Choice) -> Result<Read, String> {
252 let opened = choice.spec.open(path, named)?;
253 Ok(Read {
254 spec: choice.spec,
255 by: choice.by,
256 also: choice.also,
257 notes: opened.notes,
258 header: opened.header,
259 records: opened.records,
260 })
261}
262
263impl Read {
264 pub fn notes(&self) -> Vec<crate::notes::Note> {
267 let note = |summary: String, scope: String| crate::notes::Note {
268 summary,
269 scope,
270 read_as_text: None,
271 passed_over: None,
272 };
273 let from = self
274 .spec
275 .path
276 .as_ref()
277 .map_or_else(|| "the spec".to_string(), |p| p.display().to_string());
278 let mut notes = vec![note(
279 format!(
280 "read as {}, {}",
281 self.spec.name,
282 chosen_words(&self.spec, self.by)
283 ),
284 format!("from {from}"),
285 )];
286 if !self.also.is_empty() {
287 notes.push(note(
288 format!(
289 "{} also {} this file; press b to pick another",
290 self.also.join(", "),
291 if self.also.len() == 1 {
292 "matches"
293 } else {
294 "match"
295 }
296 ),
297 format!("by {}", self.by.words()),
298 ));
299 }
300 if !self.header.values.is_empty() {
301 let said: Vec<String> = self
302 .header
303 .values
304 .iter()
305 .map(|(name, value)| format!("{name} = {value}"))
306 .collect();
307 notes.push(note(
308 format!("header: {}", said.join(", ")),
309 "from the file's header".to_string(),
310 ));
311 }
312 for warning in &self.notes {
313 notes.push(note(
314 warning.clone(),
315 "from the file's length and the spec".to_string(),
316 ));
317 }
318 notes
319 }
320}
321
322pub(crate) fn match_words(spec: &Spec) -> String {
325 let chips = spec.match_chips();
326 if chips.is_empty() {
327 FORMAT_ONLY.to_string()
328 } else {
329 chips_plain(&chips)
330 }
331}
332
333pub const FORMAT_ONLY: &str = "no match";
335
336pub fn command(
339 action: Option<&crate::cli::FormatsAction>,
340 args: &crate::cli::Args,
341 config: &crate::config::AppConfig,
342) -> (String, i32) {
343 crate::limits::set(config.limits);
344 let path = search_path_for(config);
345 let registry = Registry::load(&path);
346 match action {
347 None => (registry.listing(&path), 0),
348 Some(crate::cli::FormatsAction::Check { spec, file }) => {
349 let options = crate::OpenOptions::from_args_and_config(args, config);
350 match check(spec, file.as_deref(), ®istry, &options) {
351 Ok(text) => (text, 0),
352 Err(text) => (text, 1),
353 }
354 }
355 }
356}
357
358const CHECK_ROWS: usize = 10;
360
361pub(crate) fn check(
364 named: &str,
365 file: Option<&Path>,
366 registry: &Registry,
367 options: &crate::OpenOptions,
368) -> Result<String, String> {
369 let as_file = Path::new(named);
370 if let Some(dict) = fix_dict_named(named, registry)? {
371 return check_fix(&dict, file);
372 }
373 if let Some(dbc) = dbc_named(named, registry)? {
374 return check_dbc(&dbc, file);
375 }
376 let spec = if as_file.is_file() {
377 Arc::new(Spec::load(as_file).map_err(|e| format!("error: {e}\n"))?)
378 } else if let Some(spec) = registry.get(named) {
379 spec.clone()
380 } else {
381 let mut said = format!("error: no spec file or format named {named}\n");
382 if let Some(e) = registry.errors.iter().find(|e| {
383 e.path
384 .as_ref()
385 .is_some_and(|p| p.file_stem() == as_file.file_stem())
386 }) {
387 said.push_str(&format!("error: {e}\n"));
388 }
389 return Err(said);
390 };
391 let mut out = format!("{}: ok\n", spec.name);
392 if let Some(from) = &spec.path {
393 out.push_str(&format!(" from {}\n", from.display()));
394 }
395 out.push_str(&format!(" matches {}\n", match_words(&spec)));
396 if spec.is_delimited() {
397 return crate::formats::delimited_spec::check(&spec, file, CHECK_ROWS, options)
398 .map(|rest| out.clone() + &rest)
399 .map_err(|rest| out.clone() + &rest);
400 }
401 let named_fields = spec
402 .records
403 .fields
404 .iter()
405 .filter(|f| f.name.is_some())
406 .count();
407 out.push_str(&format!(" {named_fields} record fields"));
408 if let Some(width) = given_width(&spec.records.fields) {
409 let size = match spec.records.size {
410 Some(Amount::Given(size)) => size,
411 _ => width,
412 };
413 if spec.layout == Layout::Rows
414 && spec
415 .records
416 .size
417 .as_ref()
418 .is_none_or(|s| matches!(s, Amount::Given(_)))
419 {
420 out.push_str(&format!(", {size} bytes a record"));
421 }
422 }
423 out.push('\n');
424 let Some(file) = file else {
425 return Ok(out);
426 };
427 let compression = crate::CompressionFormat::from_extension(file).filter(|_| file.is_file());
428 let shown = file.file_name().map_or_else(
429 || file.display().to_string(),
430 |n| n.to_string_lossy().into_owned(),
431 );
432 let choice = Choice {
433 spec: spec.clone(),
434 by: Chosen::SpecFile,
435 also: Vec::new(),
436 };
437 let copy;
439 let readable = match compression {
440 None => file,
441 Some(compression) => {
442 copy = decompressed_copy(file, compression)
443 .map_err(|e| format!("{out}error: {shown}: {e}\n"))?;
444 copy.path()
445 }
446 };
447 let read = read(readable, &shown, choice).map_err(|e| format!("{out}error: {e}\n"))?;
448 for note in &read.notes {
449 out.push_str(&format!("warning: {note}\n"));
450 }
451 if !read.header.values.is_empty() {
452 let said: Vec<String> = read
453 .header
454 .values
455 .iter()
456 .map(|(name, value)| format!("{name} = {value}"))
457 .collect();
458 out.push_str(&format!("header: {}\n", said.join(", ")));
459 }
460 out.push_str(&format!("{} records\n", read.records.rows()));
461 let df = read
462 .records
463 .collect(CHECK_ROWS)
464 .map_err(|e| format!("{out}error: {e}\n"))?;
465 out.push_str(&text_table(&df));
466 Ok(out)
467}
468
469fn fix_dict_named(
471 named: &str,
472 registry: &Registry,
473) -> Result<Option<Arc<crate::formats::fix::dict::Dictionary>>, String> {
474 let as_file = Path::new(named);
475 if as_file.is_file() {
476 return match crate::formats::fix::dict::Dictionary::load(as_file) {
477 Ok(dict) => Ok(dict.map(Arc::new)),
478 Err(e) => Err(format!("error: {e}\n")),
479 };
480 }
481 Ok(registry.fix_dict(named).cloned())
482}
483
484fn dbc_named(
487 named: &str,
488 registry: &Registry,
489) -> Result<Option<Arc<crate::formats::dbc::Dbc>>, String> {
490 let as_file = Path::new(named);
491 if as_file.is_file() {
492 let dbc_like = as_file
494 .extension()
495 .is_some_and(|e| e.eq_ignore_ascii_case("dbc") || e.eq_ignore_ascii_case("toml"));
496 if !dbc_like {
497 return Ok(None);
498 }
499 return match crate::formats::dbc::load(as_file) {
500 Ok(dbc) => Ok(dbc.map(Arc::new)),
501 Err(e) => Err(format!("error: {e}\n")),
502 };
503 }
504 Ok(registry
505 .dbc
506 .iter()
507 .find(|found| found.dbc.name == named)
508 .map(|found| found.dbc.clone()))
509}
510
511fn check_dbc(dbc: &Arc<crate::formats::dbc::Dbc>, file: Option<&Path>) -> Result<String, String> {
515 use std::io::Read;
516 let mut out = format!("{}: ok\n", dbc.name);
517 if let Some(from) = &dbc.path {
518 out.push_str(&format!(" from {}\n", from.display()));
519 }
520 if let Some(interface) = &dbc.interface {
521 out.push_str(&format!(" matches interface {interface}\n"));
522 }
523 let signals: usize = dbc.messages.iter().map(|m| m.signals.len()).sum();
524 out.push_str(&format!(
525 " {}, {}\n",
526 crate::formats::text_formats::count(dbc.messages.len() as u64, "message", "messages"),
527 crate::formats::text_formats::count(signals as u64, "signal", "signals"),
528 ));
529 for note in &dbc.notes {
530 out.push_str(&format!("warning: {note}\n"));
531 }
532 let Some(file) = file else {
533 return Ok(out);
534 };
535 let failed =
536 |out: &str, e: &dyn std::fmt::Display| format!("{out}error: {}: {e}\n", file.display());
537 let read = std::sync::atomic::AtomicU64::new(0);
538 let mut bytes = Vec::new();
539 crate::formats::text_formats::open_reader(file, &crate::OpenOptions::default(), &read)
540 .and_then(|mut reader| reader.read_to_end(&mut bytes).map_err(Into::into))
541 .map_err(|e| failed(&out, &e))?;
542 let index = crate::formats::candump::index(&bytes).map_err(|e| failed(&out, &e))?;
543 let layers = crate::formats::candump::Layers {
544 dbcs: vec![dbc.clone()],
545 };
546 let listing = crate::formats::candump::Listing::resolve(&index, layers);
547 let frames = index.keys.len();
548 out.push_str(&format!(
549 "{}, {} of them named by {}\n",
550 crate::formats::text_formats::count(frames as u64, "frame", "frames"),
551 frames - listing.unknown,
552 dbc.name
553 ));
554 let named: Vec<String> = listing
555 .messages
556 .iter()
557 .map(|(name, (_, rows))| format!("{name} ({})", rows.len()))
558 .collect();
559 if !named.is_empty() {
560 out.push_str(&format!("messages in the log: {}\n", named.join(", ")));
561 }
562 Ok(out)
563}
564
565fn check_fix(
568 dict: &Arc<crate::formats::fix::dict::Dictionary>,
569 file: Option<&Path>,
570) -> Result<String, String> {
571 use std::io::Read;
572 let mut out = format!("{}: ok\n", dict.name);
573 if let Some(from) = &dict.path {
574 out.push_str(&format!(" from {}\n", from.display()));
575 }
576 let summary = dict.matcher.summary();
577 if !summary.is_empty() {
578 out.push_str(&format!(" matches {summary}\n"));
579 }
580 let enums = dict.tags.values().filter(|t| !t.enums.is_empty()).count();
581 out.push_str(&format!(" {} tags, {enums} with enums\n", dict.tags.len()));
582 let Some(file) = file else {
583 return Ok(out);
584 };
585 let read = std::sync::atomic::AtomicU64::new(0);
586 let mut reader =
587 crate::formats::text_formats::open_reader(file, &crate::OpenOptions::default(), &read)
588 .map_err(|e| format!("{out}error: {}: {e}\n", file.display()))?;
589 let mut log =
590 crate::formats::fix::FixReader::new(crate::formats::fix::dict::Layers::new(vec![
591 dict.clone(),
592 ]));
593 let mut chunk = vec![0u8; 1 << 16];
594 loop {
595 let n = reader
596 .read(&mut chunk)
597 .map_err(|e| format!("{out}error: {}: {e}\n", file.display()))?;
598 if n == 0 {
599 break;
600 }
601 log.push(&chunk[..n]);
602 let _ = log.take_batch();
603 }
604 let _ = log.finish();
605 let stats = log.stats();
606 out.push_str(&format!(
607 "{} messages, {} of them matched by {}\n",
608 stats.messages,
609 stats.applied.get(1).copied().unwrap_or(0),
610 dict.name
611 ));
612 let named: Vec<String> = log
613 .tag_names()
614 .into_iter()
615 .filter(|(_, _, by)| *by == 1)
616 .map(|(tag, name, _)| format!("{tag} {name}"))
617 .collect();
618 if !named.is_empty() {
619 out.push_str(&format!("names in the log: {}\n", named.join(", ")));
620 }
621 Ok(out)
622}
623
624pub(crate) fn text_table(df: &polars::prelude::DataFrame) -> String {
626 let mut rows: Vec<Vec<String>> = vec![
627 df.get_column_names()
628 .iter()
629 .map(|n| n.to_string())
630 .collect(),
631 ];
632 for i in 0..df.height() {
633 rows.push(
634 df.columns()
635 .iter()
636 .map(|c| match c.get(i) {
637 Ok(polars::prelude::AnyValue::String(s)) => s.to_string(),
638 Ok(polars::prelude::AnyValue::StringOwned(s)) => s.to_string(),
639 Ok(v) => v.to_string(),
640 Err(_) => String::new(),
641 })
642 .collect(),
643 );
644 }
645 let widths: Vec<usize> = (0..rows[0].len())
646 .map(|c| rows.iter().map(|r| r[c].chars().count()).max().unwrap_or(0))
647 .collect();
648 let mut out = String::new();
649 for row in rows {
650 let cells: Vec<String> = row
651 .iter()
652 .zip(&widths)
653 .map(|(cell, width)| format!("{cell:<width$}"))
654 .collect();
655 out.push_str(cells.join(" ").trim_end());
656 out.push('\n');
657 }
658 out
659}
660
661fn decompressed_copy(
663 path: &Path,
664 compression: crate::CompressionFormat,
665) -> std::io::Result<tempfile::NamedTempFile> {
666 use std::io::Read as _;
667 let file = std::fs::File::open(path)?;
668 let mut reader: Box<dyn std::io::Read> = match compression {
669 crate::CompressionFormat::Gzip => Box::new(flate2::read::GzDecoder::new(file)),
670 crate::CompressionFormat::Zstd => Box::new(zstd::Decoder::new(file)?),
671 crate::CompressionFormat::Bzip2 => Box::new(bzip2::read::BzDecoder::new(file)),
672 crate::CompressionFormat::Xz => Box::new(xz2::read::XzDecoder::new(file)),
673 };
674 let mut copy = tempfile::NamedTempFile::new()?;
675 std::io::copy(&mut reader.by_ref(), copy.as_file_mut())?;
676 Ok(copy)
677}