use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FileFormat {
Parquet,
Csv,
Tsv,
Psv,
Json,
Jsonl,
Arrow,
Avro,
Orc,
Excel,
Safetensors,
Gguf,
Nmea,
Gpx,
Audio,
Midi,
Sqlite,
Vcd,
Fix,
Sdf,
Numpy,
Elf,
Ulog,
Dataflash,
Candump,
Text,
Journal,
}
#[derive(Debug)]
pub struct Descriptor {
pub name: &'static str,
pub title: &'static str,
pub extensions: &'static [&'static str],
pub name_endings: &'static [&'static str],
pub read: ReadMode,
pub compressed: Compressed,
pub stream: Option<ReadMode>,
pub http: RemoteRead,
pub bucket_object: RemoteRead,
pub bucket_prefix: Option<RemoteRead>,
pub many_files: bool,
pub lines: Option<Lines>,
pub tables: Option<Tables>,
pub summary: Summary,
pub declares_types: bool,
pub conversion: Option<Conversion>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Lines {
Delimited(u8),
Json,
Text,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compressed {
Refused,
Decompressed,
ReadThrough,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Summary {
Tab(&'static str),
None(&'static str),
}
const TEXT_ONLY: Summary = Summary::None("text holds its rows and nothing else");
#[derive(Debug)]
pub struct Tables {
pub opens: Option<&'static str>,
pub by_name: bool,
pub help: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Conversion {
pub label: &'static str,
pub status: &'static str,
}
const BASE: Descriptor = Descriptor {
name: "",
title: "",
extensions: &[],
name_endings: &[],
read: ReadMode::InMemory,
compressed: Compressed::Refused,
stream: None,
http: RemoteRead::Downloaded,
bucket_object: RemoteRead::Downloaded,
bucket_prefix: None,
many_files: false,
lines: None,
tables: None,
summary: TEXT_ONLY,
declares_types: false,
conversion: None,
};
const DELIMITED: Descriptor = Descriptor {
read: ReadMode::Lazy,
compressed: Compressed::Decompressed,
lines: Some(Lines::Delimited(b',')),
..BASE
};
const READ_INTO: Descriptor = Descriptor {
read: ReadMode::Converted,
compressed: Compressed::ReadThrough,
..BASE
};
const MODEL: Descriptor = Descriptor {
http: RemoteRead::InPlace,
bucket_object: RemoteRead::InPlace,
bucket_prefix: Some(RemoteRead::InPlace),
many_files: true,
..BASE
};
const PARQUET: Descriptor = Descriptor {
name: "parquet",
title: "Parquet",
extensions: &["parquet"],
read: ReadMode::Lazy,
bucket_object: RemoteRead::InPlace,
bucket_prefix: Some(RemoteRead::InPlace),
many_files: true,
declares_types: true,
summary: Summary::Tab("Parquet"),
..BASE
};
const CSV: Descriptor = Descriptor {
name: "csv",
title: "CSV",
extensions: &["csv"],
bucket_prefix: Some(RemoteRead::InPlace),
many_files: true,
..DELIMITED
};
const TSV: Descriptor = Descriptor {
name: "tsv",
title: "TSV",
extensions: &["tsv"],
lines: Some(Lines::Delimited(b'\t')),
..DELIMITED
};
const PSV: Descriptor = Descriptor {
name: "psv",
title: "PSV",
extensions: &["psv"],
lines: Some(Lines::Delimited(b'|')),
..DELIMITED
};
const JSON: Descriptor = Descriptor {
name: "json",
title: "JSON",
extensions: &["json"],
many_files: true,
..BASE
};
const JSONL: Descriptor = Descriptor {
name: "jsonl",
title: "NDJSON",
extensions: &["jsonl", "ndjson"],
bucket_prefix: Some(RemoteRead::InPlace),
many_files: true,
lines: Some(Lines::Json),
..BASE
};
const ARROW: Descriptor = Descriptor {
name: "arrow",
title: "Arrow IPC",
extensions: &["arrow", "arrows", "ipc", "feather"],
read: ReadMode::Lazy,
stream: Some(ReadMode::Converted),
bucket_object: RemoteRead::InPlace,
bucket_prefix: Some(RemoteRead::InPlace),
many_files: true,
declares_types: true,
summary: Summary::Tab("Arrow"),
..BASE
};
const AVRO: Descriptor = Descriptor {
name: "avro",
title: "Avro",
extensions: &["avro"],
many_files: true,
declares_types: true,
summary: Summary::Tab("Avro"),
..BASE
};
const ORC: Descriptor = Descriptor {
name: "orc",
title: "ORC",
extensions: &["orc"],
many_files: true,
declares_types: true,
summary: Summary::Tab("ORC"),
..BASE
};
const EXCEL: Descriptor = Descriptor {
name: "excel",
title: "Excel",
extensions: &["xls", "xlsx", "xlsm", "xlsb"],
tables: Some(Tables {
opens: Some("its first table"),
by_name: false,
help: "a worksheet by name, or by 0-based index when no worksheet is so named",
}),
summary: Summary::Tab("Excel"),
..BASE
};
const SAFETENSORS: Descriptor = Descriptor {
name: "safetensors",
title: "SafeTensors",
extensions: &["safetensors"],
name_endings: &[".safetensors.index.json"],
summary: Summary::Tab("Model"),
..MODEL
};
const GGUF: Descriptor = Descriptor {
name: "gguf",
title: "GGUF",
extensions: &["gguf"],
summary: Summary::Tab("Model"),
..MODEL
};
const NMEA: Descriptor = Descriptor {
name: "nmea",
title: "NMEA",
extensions: &["nmea"],
many_files: true,
tables: Some(Tables {
opens: Some("its fixes"),
by_name: false,
help: "fixes (default), GGA, RMC, VTG, GSA, GSV, GLL, ZDA or sentences",
}),
conversion: Some(Conversion {
label: "Reading NMEA log",
status: "Reading NMEA...",
}),
summary: Summary::Tab("GPS"),
..READ_INTO
};
const GPX: Descriptor = Descriptor {
name: "gpx",
title: "GPX",
extensions: &["gpx"],
many_files: true,
conversion: Some(Conversion {
label: "Reading GPX file",
status: "Reading GPX...",
}),
summary: Summary::Tab("GPS"),
..READ_INTO
};
const AUDIO: Descriptor = Descriptor {
name: "audio",
title: "audio",
extensions: &["wav", "wave", "bwf", "rf64", "aif", "aiff", "aifc"],
read: ReadMode::Lazy,
summary: Summary::Tab("Audio"),
..BASE
};
const MIDI: Descriptor = Descriptor {
name: "midi",
title: "MIDI",
extensions: &["mid", "midi", "smf", "kar", "rmi"],
many_files: true,
summary: Summary::Tab("MIDI"),
..BASE
};
const SQLITE: Descriptor = Descriptor {
name: "sqlite",
title: "SQLite",
extensions: &["db", "db3", "sqlite", "sqlite3"],
read: ReadMode::Lazy,
tables: Some(Tables {
opens: None,
by_name: true,
help: "a table or view by name",
}),
summary: Summary::Tab("SQLite"),
..BASE
};
const VCD: Descriptor = Descriptor {
name: "vcd",
title: "VCD",
extensions: &["vcd"],
conversion: Some(Conversion {
label: "Reading value change dump",
status: "Reading VCD...",
}),
summary: Summary::Tab("VCD"),
..READ_INTO
};
const FIX: Descriptor = Descriptor {
name: "fix",
title: "FIX",
conversion: Some(Conversion {
label: "Reading FIX log",
status: "Reading FIX log...",
}),
summary: Summary::Tab("FIX"),
..READ_INTO
};
const SDF: Descriptor = Descriptor {
name: "sdf",
title: "SDF",
extensions: &["sdf", "sd"],
conversion: Some(Conversion {
label: "Reading SDF records",
status: "Reading SDF...",
}),
summary: Summary::Tab("SDF"),
..READ_INTO
};
const NUMPY: Descriptor = Descriptor {
name: "numpy",
title: "NumPy",
extensions: &["npy", "npz"],
read: ReadMode::Lazy,
tables: Some(Tables {
opens: None,
by_name: false,
help: "an array of an archive (.npz) by name",
}),
conversion: Some(Conversion {
label: "Decompressing NumPy array",
status: "Decompressing...",
}),
summary: Summary::Tab("NumPy"),
..BASE
};
const ELF: Descriptor = Descriptor {
name: "elf",
title: "ELF",
extensions: &["elf", "axf"],
tables: Some(Tables {
opens: Some("its symbols"),
by_name: false,
help: "symbols (default) or sections",
}),
summary: Summary::Tab("ELF"),
..BASE
};
const ULOG: Descriptor = Descriptor {
name: "ulog",
title: "ULog",
extensions: &["ulg"],
read: ReadMode::Lazy,
tables: Some(Tables {
opens: None,
by_name: false,
help: "a topic",
}),
summary: Summary::Tab("ULog"),
..BASE
};
const DATAFLASH: Descriptor = Descriptor {
name: "dataflash",
title: "DataFlash",
read: ReadMode::Lazy,
tables: Some(Tables {
opens: None,
by_name: false,
help: "a message type",
}),
summary: Summary::Tab("DataFlash"),
..BASE
};
const CANDUMP: Descriptor = Descriptor {
name: "candump",
title: "candump",
read: ReadMode::Lazy,
tables: Some(Tables {
opens: None,
by_name: false,
help: "frames (default), signals, or a message a dictionary names",
}),
summary: Summary::Tab("CAN"),
..BASE
};
const TEXT: Descriptor = Descriptor {
name: "text",
title: "text",
extensions: &["log", "txt"],
read: ReadMode::Lazy,
compressed: Compressed::Decompressed,
many_files: true,
lines: Some(Lines::Text),
..BASE
};
const JOURNAL: Descriptor = Descriptor {
name: "journal",
title: "systemd journal",
many_files: true,
lines: Some(Lines::Json),
summary: Summary::Tab("Journal"),
..BASE
};
impl FileFormat {
pub const ALL: [Self; 27] = [
Self::Parquet,
Self::Csv,
Self::Tsv,
Self::Psv,
Self::Json,
Self::Jsonl,
Self::Arrow,
Self::Avro,
Self::Orc,
Self::Excel,
Self::Safetensors,
Self::Gguf,
Self::Nmea,
Self::Gpx,
Self::Audio,
Self::Midi,
Self::Sqlite,
Self::Vcd,
Self::Fix,
Self::Sdf,
Self::Numpy,
Self::Elf,
Self::Ulog,
Self::Dataflash,
Self::Candump,
Self::Text,
Self::Journal,
];
pub const TEXT: Self = Self::Text;
pub const fn descriptor(self) -> &'static Descriptor {
match self {
Self::Parquet => &PARQUET,
Self::Csv => &CSV,
Self::Tsv => &TSV,
Self::Psv => &PSV,
Self::Json => &JSON,
Self::Jsonl => &JSONL,
Self::Arrow => &ARROW,
Self::Avro => &AVRO,
Self::Orc => &ORC,
Self::Excel => &EXCEL,
Self::Safetensors => &SAFETENSORS,
Self::Gguf => &GGUF,
Self::Nmea => &NMEA,
Self::Gpx => &GPX,
Self::Audio => &AUDIO,
Self::Midi => &MIDI,
Self::Sqlite => &SQLITE,
Self::Vcd => &VCD,
Self::Fix => &FIX,
Self::Sdf => &SDF,
Self::Numpy => &NUMPY,
Self::Elf => &ELF,
Self::Ulog => &ULOG,
Self::Dataflash => &DATAFLASH,
Self::Candump => &CANDUMP,
Self::Text => &TEXT,
Self::Journal => &JOURNAL,
}
}
pub fn from_path(path: &Path) -> Option<Self> {
Self::from_name_ending(path).or_else(|| {
path.extension()
.and_then(|e| e.to_str())
.and_then(Self::from_extension)
})
}
pub fn from_name_ending(path: &Path) -> Option<Self> {
let name = path.file_name()?.to_str()?.to_ascii_lowercase();
Self::ALL.into_iter().find(|f| {
f.descriptor()
.name_endings
.iter()
.any(|end| name.ends_with(end))
})
}
pub fn name(self) -> &'static str {
self.descriptor().name
}
pub fn title(self) -> &'static str {
self.descriptor().title
}
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|f| f.name() == name)
}
pub fn reads_many_files(self) -> bool {
self.descriptor().many_files
}
pub fn holds_tables(self) -> bool {
self.descriptor().tables.is_some()
}
pub fn opens_one_table(self) -> bool {
self.descriptor()
.tables
.as_ref()
.is_some_and(|t| t.opens.is_some())
}
pub fn summary_tab(self) -> Option<&'static str> {
match self.descriptor().summary {
Summary::Tab(tab) => Some(tab),
Summary::None(_) => None,
}
}
pub fn takes_table(self) -> bool {
self.descriptor().tables.is_some()
}
pub fn reads_into(self) -> bool {
self.descriptor().compressed == Compressed::ReadThrough
}
pub fn read_mode(self, stored: Stored) -> Option<ReadMode> {
let d = self.descriptor();
match stored {
Stored::Plain => Some(d.read),
Stored::Stream => d.stream,
Stored::Compressed { in_memory } => match d.compressed {
Compressed::Refused => None,
Compressed::ReadThrough => Some(ReadMode::Converted),
Compressed::Decompressed if in_memory => Some(ReadMode::InMemory),
Compressed::Decompressed => Some(ReadMode::Decompressed),
},
}
}
pub fn bucket_object(self, stored: Stored) -> RemoteRead {
match stored {
Stored::Plain => self.descriptor().bucket_object,
_ => RemoteRead::Downloaded,
}
}
pub fn http_file(self) -> RemoteRead {
self.descriptor().http
}
pub fn bucket_prefix(self, stored: Stored) -> Option<RemoteRead> {
let d = self.descriptor();
match stored {
Stored::Plain => d.bucket_prefix,
Stored::Stream => d.stream.map(|_| RemoteRead::Downloaded),
Stored::Compressed { .. } => None,
}
}
pub fn reads_bucket_prefix(self) -> bool {
self.bucket_prefix(Stored::Plain) == Some(RemoteRead::InPlace)
}
pub fn separator(self) -> Option<u8> {
match self.descriptor().lines {
Some(Lines::Delimited(separator)) => Some(separator),
_ => None,
}
}
pub fn follows(self) -> bool {
self.descriptor().lines.is_some()
}
pub fn is_lines(self) -> bool {
self.descriptor().lines == Some(Lines::Text)
}
pub fn decompressed_once(self) -> bool {
self.descriptor().compressed == Compressed::Decompressed
}
pub fn conversion(self) -> Conversion {
self.descriptor().conversion.unwrap_or(Conversion {
label: "Converting",
status: "Converting...",
})
}
pub fn from_extension(ext: &str) -> Option<Self> {
let ext = ext.to_ascii_lowercase();
Self::ALL
.into_iter()
.find(|f| f.descriptor().extensions.contains(&ext.as_str()))
}
}
pub fn format_help() -> String {
let names: Vec<&str> = FileFormat::ALL.iter().map(|f| f.name()).collect();
format!(
"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)",
names.join(", ")
)
}
pub fn table_help() -> String {
let mut help = String::from("Table to open from a file that holds several.");
for format in FileFormat::ALL {
if let Some(tables) = &format.descriptor().tables {
help.push_str(&format!(" {}: {}.", format.title(), tables.help));
}
}
help.push_str(" Hugging Face cache and DatasetDict directories: a split (default train)");
help
}
pub fn one_table(format: Option<FileFormat>) -> String {
let what = format.map_or("This file".to_string(), |f| format!("A {} file", f.name()));
let titles: Vec<&str> = FileFormat::ALL
.into_iter()
.filter(|f| f.takes_table())
.map(FileFormat::title)
.collect();
format!(
"{what} holds one table. --table picks one from {} files, or a Hugging Face dataset's split.",
titles.join(", ")
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReadMode {
Lazy,
Decompressed,
Converted,
InMemory,
}
impl ReadMode {
pub fn label(self) -> &'static str {
match self {
Self::Lazy => "lazy scan",
Self::Decompressed => "decompressed copy",
Self::Converted => "converted to Arrow",
Self::InMemory => "in memory",
}
}
pub fn marker(self) -> Option<&'static str> {
match self {
Self::Lazy => None,
Self::Decompressed => Some("decompresses"),
Self::Converted => Some("converts"),
Self::InMemory => Some("in memory"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Stored {
#[default]
Plain,
Compressed { in_memory: bool },
Stream,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemoteRead {
InPlace,
Downloaded,
}
impl RemoteRead {
pub fn label(self) -> &'static str {
match self {
Self::InPlace => "in place",
Self::Downloaded => "downloaded",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_and_extensions_say_one_format() {
let mut seen: Vec<&str> = Vec::new();
for format in FileFormat::ALL {
let d = format.descriptor();
assert!(!d.title.is_empty(), "{format:?}");
for ext in d.extensions {
assert!(!seen.contains(ext), ".{ext} names two formats");
assert_eq!(*ext, ext.to_ascii_lowercase());
seen.push(ext);
assert_eq!(FileFormat::from_extension(ext), Some(format));
}
}
let names: Vec<&str> = FileFormat::ALL.map(FileFormat::name).to_vec();
for name in &names {
assert_eq!(names.iter().filter(|n| *n == name).count(), 1, "{name}");
}
}
#[test]
fn help_and_refusals_come_from_the_descriptors() {
let format = format_help();
let table = table_help();
let refused = one_table(Some(FileFormat::Csv));
for f in FileFormat::ALL {
assert!(format.contains(f.name()), "{}", f.name());
let picked = format!("{}: ", f.title());
assert_eq!(table.contains(&picked), f.takes_table(), "{}", f.name());
if f.takes_table() {
assert!(refused.contains(f.title()), "{refused}");
}
}
assert!(
refused.starts_with("A csv file holds one table"),
"{refused}"
);
assert!(one_table(None).starts_with("This file holds one table"));
assert!(FileFormat::Excel.takes_table(), "a sheet is a table");
assert!(!format.contains("acme"), "{format}");
}
#[test]
fn conversions_say_what_they_read() {
let mut seen: Vec<Conversion> = Vec::new();
for f in FileFormat::ALL.into_iter().filter(|f| f.reads_into()) {
let c = f.descriptor().conversion.expect("a conversion's words");
assert!(c.status.ends_with("..."), "{}", c.status);
assert!(
!seen
.iter()
.any(|s| s.label == c.label || s.status == c.status),
"{c:?}"
);
seen.push(c);
}
}
}