1use std::path::Path;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum FileFormat {
24 Parquet,
25 Csv,
26 Tsv,
27 Psv,
28 Json,
29 Jsonl,
30 Arrow,
31 Avro,
32 Orc,
33 Excel,
34 Safetensors,
35 Gguf,
36 Nmea,
37 Gpx,
38 Audio,
39 Midi,
40 Sqlite,
41 Vcd,
42 Fix,
43 Sdf,
44 Numpy,
45 Elf,
46 Ulog,
47 Dataflash,
48 Candump,
49 Text,
50 Journal,
51}
52
53#[derive(Debug)]
55pub struct Descriptor {
56 pub name: &'static str,
59 pub title: &'static str,
61 pub extensions: &'static [&'static str],
64 pub name_endings: &'static [&'static str],
67 pub read: ReadMode,
69 pub compressed: Compressed,
71 pub stream: Option<ReadMode>,
73 pub http: RemoteRead,
75 pub bucket_object: RemoteRead,
77 pub bucket_prefix: Option<RemoteRead>,
80 pub many_files: bool,
82 pub lines: Option<Lines>,
85 pub tables: Option<Tables>,
89 pub summary: Summary,
91 pub declares_types: bool,
94 pub conversion: Option<Conversion>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum Lines {
102 Delimited(u8),
104 Json,
106 Text,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum Compressed {
113 Refused,
115 Decompressed,
117 ReadThrough,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum Summary {
124 Tab(&'static str),
127 None(&'static str),
129}
130
131const TEXT_ONLY: Summary = Summary::None("text holds its rows and nothing else");
133
134#[derive(Debug)]
136pub struct Tables {
137 pub opens: Option<&'static str>,
141 pub by_name: bool,
143 pub help: &'static str,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct Conversion {
150 pub label: &'static str,
152 pub status: &'static str,
154}
155
156const BASE: Descriptor = Descriptor {
159 name: "",
160 title: "",
161 extensions: &[],
162 name_endings: &[],
163 read: ReadMode::InMemory,
164 compressed: Compressed::Refused,
165 stream: None,
166 http: RemoteRead::Downloaded,
167 bucket_object: RemoteRead::Downloaded,
168 bucket_prefix: None,
169 many_files: false,
170 lines: None,
171 tables: None,
172 summary: TEXT_ONLY,
173 declares_types: false,
174 conversion: None,
175};
176
177const DELIMITED: Descriptor = Descriptor {
180 read: ReadMode::Lazy,
181 compressed: Compressed::Decompressed,
182 lines: Some(Lines::Delimited(b',')),
183 ..BASE
184};
185
186const READ_INTO: Descriptor = Descriptor {
189 read: ReadMode::Converted,
190 compressed: Compressed::ReadThrough,
191 ..BASE
192};
193
194const MODEL: Descriptor = Descriptor {
197 http: RemoteRead::InPlace,
198 bucket_object: RemoteRead::InPlace,
199 bucket_prefix: Some(RemoteRead::InPlace),
200 many_files: true,
201 ..BASE
202};
203
204const PARQUET: Descriptor = Descriptor {
205 name: "parquet",
206 title: "Parquet",
207 extensions: &["parquet"],
208 read: ReadMode::Lazy,
209 bucket_object: RemoteRead::InPlace,
211 bucket_prefix: Some(RemoteRead::InPlace),
212 many_files: true,
213 declares_types: true,
214 summary: Summary::Tab("Parquet"),
215 ..BASE
216};
217
218const CSV: Descriptor = Descriptor {
219 name: "csv",
220 title: "CSV",
221 extensions: &["csv"],
222 bucket_prefix: Some(RemoteRead::InPlace),
223 many_files: true,
224 ..DELIMITED
225};
226
227const TSV: Descriptor = Descriptor {
229 name: "tsv",
230 title: "TSV",
231 extensions: &["tsv"],
232 lines: Some(Lines::Delimited(b'\t')),
233 ..DELIMITED
234};
235
236const PSV: Descriptor = Descriptor {
237 name: "psv",
238 title: "PSV",
239 extensions: &["psv"],
240 lines: Some(Lines::Delimited(b'|')),
241 ..DELIMITED
242};
243
244const JSON: Descriptor = Descriptor {
245 name: "json",
246 title: "JSON",
247 extensions: &["json"],
248 many_files: true,
249 ..BASE
250};
251
252const JSONL: Descriptor = Descriptor {
253 name: "jsonl",
254 title: "NDJSON",
255 extensions: &["jsonl", "ndjson"],
256 bucket_prefix: Some(RemoteRead::InPlace),
257 many_files: true,
258 lines: Some(Lines::Json),
259 ..BASE
260};
261
262const ARROW: Descriptor = Descriptor {
263 name: "arrow",
264 title: "Arrow IPC",
265 extensions: &["arrow", "arrows", "ipc", "feather"],
266 read: ReadMode::Lazy,
267 stream: Some(ReadMode::Converted),
269 bucket_object: RemoteRead::InPlace,
270 bucket_prefix: Some(RemoteRead::InPlace),
271 many_files: true,
272 declares_types: true,
273 summary: Summary::Tab("Arrow"),
274 ..BASE
275};
276
277const AVRO: Descriptor = Descriptor {
278 name: "avro",
279 title: "Avro",
280 extensions: &["avro"],
281 many_files: true,
282 declares_types: true,
283 summary: Summary::Tab("Avro"),
284 ..BASE
285};
286
287const ORC: Descriptor = Descriptor {
288 name: "orc",
289 title: "ORC",
290 extensions: &["orc"],
291 many_files: true,
292 declares_types: true,
293 summary: Summary::Tab("ORC"),
294 ..BASE
295};
296
297const EXCEL: Descriptor = Descriptor {
299 name: "excel",
300 title: "Excel",
301 extensions: &["xls", "xlsx", "xlsm", "xlsb"],
302 tables: Some(Tables {
303 opens: Some("its first table"),
304 by_name: false,
305 help: "a worksheet by name, or by 0-based index when no worksheet is so named",
306 }),
307 summary: Summary::Tab("Excel"),
308 ..BASE
309};
310
311const SAFETENSORS: Descriptor = Descriptor {
312 name: "safetensors",
313 title: "SafeTensors",
314 extensions: &["safetensors"],
315 name_endings: &[".safetensors.index.json"],
316 summary: Summary::Tab("Model"),
317 ..MODEL
318};
319
320const GGUF: Descriptor = Descriptor {
321 name: "gguf",
322 title: "GGUF",
323 extensions: &["gguf"],
324 summary: Summary::Tab("Model"),
325 ..MODEL
326};
327
328const NMEA: Descriptor = Descriptor {
329 name: "nmea",
330 title: "NMEA",
331 extensions: &["nmea"],
332 many_files: true,
333 tables: Some(Tables {
334 opens: Some("its fixes"),
335 by_name: false,
336 help: "fixes (default), GGA, RMC, VTG, GSA, GSV, GLL, ZDA or sentences",
337 }),
338 conversion: Some(Conversion {
339 label: "Reading NMEA log",
340 status: "Reading NMEA...",
341 }),
342 summary: Summary::Tab("GPS"),
343 ..READ_INTO
344};
345
346const GPX: Descriptor = Descriptor {
347 name: "gpx",
348 title: "GPX",
349 extensions: &["gpx"],
350 many_files: true,
351 conversion: Some(Conversion {
352 label: "Reading GPX file",
353 status: "Reading GPX...",
354 }),
355 summary: Summary::Tab("GPS"),
356 ..READ_INTO
357};
358
359const AUDIO: Descriptor = Descriptor {
362 name: "audio",
363 title: "audio",
364 extensions: &["wav", "wave", "bwf", "rf64", "aif", "aiff", "aifc"],
365 read: ReadMode::Lazy,
366 summary: Summary::Tab("Audio"),
367 ..BASE
368};
369
370const MIDI: Descriptor = Descriptor {
372 name: "midi",
373 title: "MIDI",
374 extensions: &["mid", "midi", "smf", "kar", "rmi"],
375 many_files: true,
376 summary: Summary::Tab("MIDI"),
377 ..BASE
378};
379
380const SQLITE: Descriptor = Descriptor {
382 name: "sqlite",
383 title: "SQLite",
384 extensions: &["db", "db3", "sqlite", "sqlite3"],
385 read: ReadMode::Lazy,
386 tables: Some(Tables {
387 opens: None,
388 by_name: true,
389 help: "a table or view by name",
390 }),
391 summary: Summary::Tab("SQLite"),
392 ..BASE
393};
394
395const VCD: Descriptor = Descriptor {
396 name: "vcd",
397 title: "VCD",
398 extensions: &["vcd"],
399 conversion: Some(Conversion {
400 label: "Reading value change dump",
401 status: "Reading VCD...",
402 }),
403 summary: Summary::Tab("VCD"),
404 ..READ_INTO
405};
406
407const FIX: Descriptor = Descriptor {
409 name: "fix",
410 title: "FIX",
411 conversion: Some(Conversion {
412 label: "Reading FIX log",
413 status: "Reading FIX log...",
414 }),
415 summary: Summary::Tab("FIX"),
416 ..READ_INTO
417};
418
419const SDF: Descriptor = Descriptor {
420 name: "sdf",
421 title: "SDF",
422 extensions: &["sdf", "sd"],
423 conversion: Some(Conversion {
424 label: "Reading SDF records",
425 status: "Reading SDF...",
426 }),
427 summary: Summary::Tab("SDF"),
428 ..READ_INTO
429};
430
431const NUMPY: Descriptor = Descriptor {
434 name: "numpy",
435 title: "NumPy",
436 extensions: &["npy", "npz"],
437 read: ReadMode::Lazy,
438 tables: Some(Tables {
439 opens: None,
440 by_name: false,
441 help: "an array of an archive (.npz) by name",
442 }),
443 conversion: Some(Conversion {
444 label: "Decompressing NumPy array",
445 status: "Decompressing...",
446 }),
447 summary: Summary::Tab("NumPy"),
448 ..BASE
449};
450
451const ELF: Descriptor = Descriptor {
453 name: "elf",
454 title: "ELF",
455 extensions: &["elf", "axf"],
456 tables: Some(Tables {
457 opens: Some("its symbols"),
458 by_name: false,
459 help: "symbols (default) or sections",
460 }),
461 summary: Summary::Tab("ELF"),
462 ..BASE
463};
464
465const ULOG: Descriptor = Descriptor {
468 name: "ulog",
469 title: "ULog",
470 extensions: &["ulg"],
471 read: ReadMode::Lazy,
472 tables: Some(Tables {
473 opens: None,
474 by_name: false,
475 help: "a topic",
476 }),
477 summary: Summary::Tab("ULog"),
478 ..BASE
479};
480
481const DATAFLASH: Descriptor = Descriptor {
483 name: "dataflash",
484 title: "DataFlash",
485 read: ReadMode::Lazy,
486 tables: Some(Tables {
487 opens: None,
488 by_name: false,
489 help: "a message type",
490 }),
491 summary: Summary::Tab("DataFlash"),
492 ..BASE
493};
494
495const CANDUMP: Descriptor = Descriptor {
497 name: "candump",
498 title: "candump",
499 read: ReadMode::Lazy,
500 tables: Some(Tables {
501 opens: None,
502 by_name: false,
503 help: "frames (default), signals, or a message a dictionary names",
504 }),
505 summary: Summary::Tab("CAN"),
506 ..BASE
507};
508
509const TEXT: Descriptor = Descriptor {
512 name: "text",
513 title: "text",
514 extensions: &["log", "txt"],
515 read: ReadMode::Lazy,
516 compressed: Compressed::Decompressed,
517 many_files: true,
518 lines: Some(Lines::Text),
519 ..BASE
520};
521
522const JOURNAL: Descriptor = Descriptor {
525 name: "journal",
526 title: "systemd journal",
527 many_files: true,
528 lines: Some(Lines::Json),
529 summary: Summary::Tab("Journal"),
530 ..BASE
531};
532
533impl FileFormat {
534 pub const ALL: [Self; 27] = [
543 Self::Parquet,
544 Self::Csv,
545 Self::Tsv,
546 Self::Psv,
547 Self::Json,
548 Self::Jsonl,
549 Self::Arrow,
550 Self::Avro,
551 Self::Orc,
552 Self::Excel,
553 Self::Safetensors,
554 Self::Gguf,
555 Self::Nmea,
556 Self::Gpx,
557 Self::Audio,
558 Self::Midi,
559 Self::Sqlite,
560 Self::Vcd,
561 Self::Fix,
562 Self::Sdf,
563 Self::Numpy,
564 Self::Elf,
565 Self::Ulog,
566 Self::Dataflash,
567 Self::Candump,
568 Self::Text,
569 Self::Journal,
570 ];
571
572 pub const TEXT: Self = Self::Text;
575
576 pub const fn descriptor(self) -> &'static Descriptor {
578 match self {
579 Self::Parquet => &PARQUET,
580 Self::Csv => &CSV,
581 Self::Tsv => &TSV,
582 Self::Psv => &PSV,
583 Self::Json => &JSON,
584 Self::Jsonl => &JSONL,
585 Self::Arrow => &ARROW,
586 Self::Avro => &AVRO,
587 Self::Orc => &ORC,
588 Self::Excel => &EXCEL,
589 Self::Safetensors => &SAFETENSORS,
590 Self::Gguf => &GGUF,
591 Self::Nmea => &NMEA,
592 Self::Gpx => &GPX,
593 Self::Audio => &AUDIO,
594 Self::Midi => &MIDI,
595 Self::Sqlite => &SQLITE,
596 Self::Vcd => &VCD,
597 Self::Fix => &FIX,
598 Self::Sdf => &SDF,
599 Self::Numpy => &NUMPY,
600 Self::Elf => &ELF,
601 Self::Ulog => &ULOG,
602 Self::Dataflash => &DATAFLASH,
603 Self::Candump => &CANDUMP,
604 Self::Text => &TEXT,
605 Self::Journal => &JOURNAL,
606 }
607 }
608
609 pub fn from_path(path: &Path) -> Option<Self> {
613 Self::from_name_ending(path).or_else(|| {
614 path.extension()
615 .and_then(|e| e.to_str())
616 .and_then(Self::from_extension)
617 })
618 }
619
620 pub fn from_name_ending(path: &Path) -> Option<Self> {
623 let name = path.file_name()?.to_str()?.to_ascii_lowercase();
624 Self::ALL.into_iter().find(|f| {
625 f.descriptor()
626 .name_endings
627 .iter()
628 .any(|end| name.ends_with(end))
629 })
630 }
631
632 pub fn name(self) -> &'static str {
637 self.descriptor().name
638 }
639
640 pub fn title(self) -> &'static str {
642 self.descriptor().title
643 }
644
645 pub fn from_name(name: &str) -> Option<Self> {
649 Self::ALL.into_iter().find(|f| f.name() == name)
650 }
651
652 pub fn reads_many_files(self) -> bool {
657 self.descriptor().many_files
658 }
659
660 pub fn holds_tables(self) -> bool {
664 self.descriptor().tables.is_some()
665 }
666
667 pub fn opens_one_table(self) -> bool {
670 self.descriptor()
671 .tables
672 .as_ref()
673 .is_some_and(|t| t.opens.is_some())
674 }
675
676 pub fn summary_tab(self) -> Option<&'static str> {
678 match self.descriptor().summary {
679 Summary::Tab(tab) => Some(tab),
680 Summary::None(_) => None,
681 }
682 }
683
684 pub fn takes_table(self) -> bool {
686 self.descriptor().tables.is_some()
687 }
688
689 pub fn reads_into(self) -> bool {
692 self.descriptor().compressed == Compressed::ReadThrough
693 }
694
695 pub fn read_mode(self, stored: Stored) -> Option<ReadMode> {
702 let d = self.descriptor();
703 match stored {
704 Stored::Plain => Some(d.read),
705 Stored::Stream => d.stream,
706 Stored::Compressed { in_memory } => match d.compressed {
707 Compressed::Refused => None,
708 Compressed::ReadThrough => Some(ReadMode::Converted),
709 Compressed::Decompressed if in_memory => Some(ReadMode::InMemory),
710 Compressed::Decompressed => Some(ReadMode::Decompressed),
711 },
712 }
713 }
714
715 pub fn bucket_object(self, stored: Stored) -> RemoteRead {
719 match stored {
720 Stored::Plain => self.descriptor().bucket_object,
721 _ => RemoteRead::Downloaded,
722 }
723 }
724
725 pub fn http_file(self) -> RemoteRead {
728 self.descriptor().http
729 }
730
731 pub fn bucket_prefix(self, stored: Stored) -> Option<RemoteRead> {
736 let d = self.descriptor();
737 match stored {
738 Stored::Plain => d.bucket_prefix,
739 Stored::Stream => d.stream.map(|_| RemoteRead::Downloaded),
740 Stored::Compressed { .. } => None,
741 }
742 }
743
744 pub fn reads_bucket_prefix(self) -> bool {
747 self.bucket_prefix(Stored::Plain) == Some(RemoteRead::InPlace)
748 }
749
750 pub fn separator(self) -> Option<u8> {
753 match self.descriptor().lines {
754 Some(Lines::Delimited(separator)) => Some(separator),
755 _ => None,
756 }
757 }
758
759 pub fn follows(self) -> bool {
762 self.descriptor().lines.is_some()
763 }
764
765 pub fn is_lines(self) -> bool {
767 self.descriptor().lines == Some(Lines::Text)
768 }
769
770 pub fn decompressed_once(self) -> bool {
773 self.descriptor().compressed == Compressed::Decompressed
774 }
775
776 pub fn conversion(self) -> Conversion {
779 self.descriptor().conversion.unwrap_or(Conversion {
780 label: "Converting",
781 status: "Converting...",
782 })
783 }
784
785 pub fn from_extension(ext: &str) -> Option<Self> {
795 let ext = ext.to_ascii_lowercase();
796 Self::ALL
797 .into_iter()
798 .find(|f| f.descriptor().extensions.contains(&ext.as_str()))
799 }
800}
801
802pub fn format_help() -> String {
804 let names: Vec<&str> = FileFormat::ALL.iter().map(|f| f.name()).collect();
805 format!(
806 "File format, when the extension does not say: {}; or a format spec: its name (`datui formats` lists them), its file (a path with a / or ending .toml), or its http(s), s3, gs or az URL (at most 1 MiB)",
807 names.join(", ")
808 )
809}
810
811pub fn table_help() -> String {
813 let mut help = String::from("Table to open from a file that holds several.");
814 for format in FileFormat::ALL {
815 if let Some(tables) = &format.descriptor().tables {
816 help.push_str(&format!(" {}: {}.", format.title(), tables.help));
817 }
818 }
819 help.push_str(" Hugging Face cache and DatasetDict directories: a split (default train)");
820 help
821}
822
823pub fn one_table(format: Option<FileFormat>) -> String {
826 let what = format.map_or("This file".to_string(), |f| format!("A {} file", f.name()));
827 let titles: Vec<&str> = FileFormat::ALL
828 .into_iter()
829 .filter(|f| f.takes_table())
830 .map(FileFormat::title)
831 .collect();
832 format!(
833 "{what} holds one table. --table picks one from {} files, or a Hugging Face dataset's split.",
834 titles.join(", ")
835 )
836}
837
838#[derive(Debug, Clone, Copy, PartialEq, Eq)]
840pub enum ReadMode {
841 Lazy,
843 Decompressed,
846 Converted,
848 InMemory,
850}
851
852impl ReadMode {
853 pub fn label(self) -> &'static str {
855 match self {
856 Self::Lazy => "lazy scan",
857 Self::Decompressed => "decompressed copy",
858 Self::Converted => "converted to Arrow",
859 Self::InMemory => "in memory",
860 }
861 }
862
863 pub fn marker(self) -> Option<&'static str> {
866 match self {
867 Self::Lazy => None,
868 Self::Decompressed => Some("decompresses"),
869 Self::Converted => Some("converts"),
870 Self::InMemory => Some("in memory"),
871 }
872 }
873}
874
875#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
877pub enum Stored {
878 #[default]
880 Plain,
881 Compressed { in_memory: bool },
883 Stream,
885}
886
887#[derive(Debug, Clone, Copy, PartialEq, Eq)]
889pub enum RemoteRead {
890 InPlace,
892 Downloaded,
894}
895
896impl RemoteRead {
897 pub fn label(self) -> &'static str {
899 match self {
900 Self::InPlace => "in place",
901 Self::Downloaded => "downloaded",
902 }
903 }
904}
905
906#[cfg(test)]
907mod tests {
908 use super::*;
909
910 #[test]
913 fn names_and_extensions_say_one_format() {
914 let mut seen: Vec<&str> = Vec::new();
915 for format in FileFormat::ALL {
916 let d = format.descriptor();
917 assert!(!d.title.is_empty(), "{format:?}");
918 for ext in d.extensions {
919 assert!(!seen.contains(ext), ".{ext} names two formats");
920 assert_eq!(*ext, ext.to_ascii_lowercase());
921 seen.push(ext);
922 assert_eq!(FileFormat::from_extension(ext), Some(format));
923 }
924 }
925 let names: Vec<&str> = FileFormat::ALL.map(FileFormat::name).to_vec();
926 for name in &names {
927 assert_eq!(names.iter().filter(|n| *n == name).count(), 1, "{name}");
928 }
929 }
930
931 #[test]
934 fn help_and_refusals_come_from_the_descriptors() {
935 let format = format_help();
936 let table = table_help();
937 let refused = one_table(Some(FileFormat::Csv));
938 for f in FileFormat::ALL {
939 assert!(format.contains(f.name()), "{}", f.name());
940 let picked = format!("{}: ", f.title());
941 assert_eq!(table.contains(&picked), f.takes_table(), "{}", f.name());
942 if f.takes_table() {
943 assert!(refused.contains(f.title()), "{refused}");
944 }
945 }
946 assert!(
947 refused.starts_with("A csv file holds one table"),
948 "{refused}"
949 );
950 assert!(one_table(None).starts_with("This file holds one table"));
951 assert!(FileFormat::Excel.takes_table(), "a sheet is a table");
952 assert!(!format.contains("acme"), "{format}");
954 }
955
956 #[test]
959 fn conversions_say_what_they_read() {
960 let mut seen: Vec<Conversion> = Vec::new();
961 for f in FileFormat::ALL.into_iter().filter(|f| f.reads_into()) {
962 let c = f.descriptor().conversion.expect("a conversion's words");
963 assert!(c.status.ends_with("..."), "{}", c.status);
964 assert!(
965 !seen
966 .iter()
967 .any(|s| s.label == c.label || s.status == c.status),
968 "{c:?}"
969 );
970 seen.push(c);
971 }
972 }
973}