1use std::path::{Path, PathBuf};
28use std::sync::Arc;
29use std::sync::atomic::AtomicU64;
30
31use color_eyre::Result;
32use color_eyre::eyre::eyre;
33
34use crate::export::export_modal::ExportFormat;
35use crate::formats::members::Table;
36use crate::formats::segments::Converted;
37use crate::formats::text_formats::Detail;
38use crate::loading::scan::Scan;
39use crate::loading::unfinished::Writer;
40use crate::{FileFormat, OpenOptions, ReadReport};
41
42pub(crate) mod csv;
43pub(crate) mod facts;
44pub mod hive;
45pub(crate) mod polars;
46#[cfg(test)]
47mod tests;
48
49#[derive(Default)]
51pub(crate) struct Read {
52 pub(crate) lf: ::polars::prelude::LazyFrame,
53 pub(crate) python: Vec<String>,
56 pub(crate) notes: Vec<crate::notes::Note>,
59 pub(crate) units: Option<Vec<(String, String)>>,
62 pub(crate) typing: Typing,
64 pub(crate) temp: Option<Arc<csv::Decompressed>>,
66}
67
68impl From<::polars::prelude::LazyFrame> for Read {
69 fn from(lf: ::polars::prelude::LazyFrame) -> Self {
70 Self {
71 lf,
72 ..Default::default()
73 }
74 }
75}
76
77impl Read {
78 pub(crate) fn typed(mut self, mut typing: Typing) -> Self {
80 self.notes.append(&mut typing.notes);
81 self.typing = typing;
82 self
83 }
84}
85
86#[derive(Clone, Default)]
88pub struct Typing {
89 pub(crate) source: Option<::polars::prelude::LazyFrame>,
90 pub(crate) typed: Vec<crate::formats::column_types::Typed>,
91 pub(crate) notes: Vec<crate::notes::Note>,
92 pub(crate) text: Vec<String>,
95}
96
97impl std::fmt::Debug for Typing {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.debug_struct("Typing")
100 .field("typed", &self.typed)
101 .field("notes", &self.notes)
102 .finish_non_exhaustive()
103 }
104}
105
106pub(crate) type ListTables = fn(&Path) -> Result<Vec<Table>>;
108
109pub(crate) type FactsFn = fn(&Path) -> Result<FormatFacts>;
112
113#[derive(Clone, Copy)]
115pub(crate) struct Facts {
116 pub read: FactsFn,
117 pub footer: bool,
120}
121
122#[derive(Debug, Clone, Default)]
125pub struct FormatFacts {
126 pub detail: Option<Arc<Detail>>,
127 pub footer: Option<crate::formats::parquet_footer::Footer>,
128}
129
130pub(crate) type TableSchema =
133 fn(&Path, Option<&str>) -> Option<crate::home::discover::SchemaPreview>;
134
135pub(crate) struct ScanIn<'a> {
138 pub format: FileFormat,
139 pub paths: &'a [PathBuf],
140 pub options: &'a OpenOptions,
141 pub report: &'a mut ReadReport,
142 pub formats: &'a crate::formats::Registry,
143}
144
145impl ScanIn<'_> {
146 pub fn path(&self) -> &Path {
148 &self.paths[0]
149 }
150}
151
152pub(crate) type ScanFn = fn(ScanIn<'_>) -> Result<Scan>;
154
155pub(crate) struct ConvertIn<'a> {
158 pub files: &'a [PathBuf],
159 pub display: &'a Path,
160 pub format: FileFormat,
161 pub options: &'a OpenOptions,
162 pub formats: &'a crate::formats::Registry,
163 pub writer: &'a Writer,
164 pub read: &'a AtomicU64,
165}
166
167pub(crate) type ConvertOut = Result<(Converted, Option<Arc<Detail>>)>;
169
170pub(crate) type ConvertFn = fn(&ConvertIn<'_>) -> ConvertOut;
172
173#[cfg(feature = "cloud")]
176pub(crate) struct BucketIn<'a> {
177 pub url: &'a str,
178 pub path: ::polars::prelude::PlRefPath,
179 pub cloud: ::polars::io::cloud::CloudOptions,
180 pub glob: bool,
181 pub options: &'a OpenOptions,
182 pub format: FileFormat,
183}
184
185#[cfg(feature = "cloud")]
187pub(crate) type BucketScan = fn(BucketIn<'_>) -> Result<::polars::prelude::LazyFrame>;
188
189pub(crate) struct Reader {
191 pub scan: ScanFn,
193 #[cfg(feature = "cloud")]
198 pub bucket_scan: Option<BucketScan>,
199 pub convert: Option<ConvertFn>,
202 pub signatures: &'static [Signature],
204 pub refines: &'static [FileFormat],
207 pub tables: Option<ListTables>,
211 pub facts: Option<Facts>,
215 pub table_schema: Option<TableSchema>,
218 pub bytes_decide: bool,
221 pub preview: Option<Preview>,
224 pub python: Option<crate::export::python_script::Python>,
226 pub export: Option<ExportFormat>,
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub(crate) enum Preview {
234 Scan,
236 RowGroup,
238}
239
240pub(crate) const BASE: Reader = Reader {
242 scan: |input| {
243 Err(eyre!(
244 "datui has no reader for {} files.",
245 input.format.name()
246 ))
247 },
248 #[cfg(feature = "cloud")]
249 bucket_scan: None,
250 convert: None,
251 signatures: &[],
252 refines: &[],
253 tables: None,
254 facts: None,
255 table_schema: None,
256 bytes_decide: false,
257 preview: None,
258 python: None,
259 export: None,
260};
261
262pub(crate) fn scan(input: ScanIn<'_>) -> Result<Scan> {
265 let one = (input.paths.len() == 1).then(|| input.paths[0].clone());
266 (of(input.format).scan)(input).map_err(|e| match one {
267 Some(path) => crate::error_display::in_file(&path, e),
268 None => e,
269 })
270}
271
272pub(crate) fn of(format: FileFormat) -> &'static Reader {
274 match format {
275 FileFormat::Parquet => &polars::PARQUET,
276 FileFormat::Csv => &polars::CSV,
277 FileFormat::Tsv => &polars::TSV,
278 FileFormat::Psv => &polars::PSV,
279 FileFormat::Json => &polars::JSON,
280 FileFormat::Jsonl => &polars::JSONL,
281 FileFormat::Arrow => &polars::ARROW,
282 FileFormat::Avro => &polars::AVRO,
283 FileFormat::Orc => &polars::ORC,
284 FileFormat::Excel => &polars::EXCEL,
285 FileFormat::Safetensors => &crate::formats::model_files::SAFETENSORS,
286 FileFormat::Gguf => &crate::formats::model_files::GGUF,
287 FileFormat::Nmea => &crate::formats::gps::NMEA,
288 FileFormat::Gpx => &crate::formats::gps::GPX,
289 FileFormat::Audio => &crate::formats::audio::READER,
290 FileFormat::Midi => &crate::formats::midi::READER,
291 FileFormat::Sqlite => &crate::formats::sqlite::READER,
292 FileFormat::Vcd => &crate::formats::vcd::READER,
293 FileFormat::Fix => &crate::formats::fix::READER,
294 FileFormat::Sdf => &crate::formats::sdf::READER,
295 FileFormat::Numpy => &crate::formats::numpy::READER,
296 FileFormat::Elf => &crate::formats::elf::READER,
297 FileFormat::Ulog => &crate::formats::ulog::READER,
298 FileFormat::Dataflash => &crate::formats::dataflash::READER,
299 FileFormat::Candump => &crate::formats::candump::READER,
300 FileFormat::Text => &crate::formats::lines::READER,
301 FileFormat::Journal => &crate::formats::journal::READER,
302 }
303}
304
305pub const HEAD: usize = 4096;
307
308pub(crate) struct Signature {
310 pub says: fn(&[u8], Option<&Path>) -> bool,
315 pub kind: Kind,
318 pub trusted: Trusted,
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub(crate) enum Kind {
325 Magic,
326 Structure,
327 Text,
328}
329
330#[derive(Debug, Clone, Copy)]
333pub(crate) struct Trusted {
334 pub pipe: bool,
336 pub open: Unnamed,
338 pub listing: bool,
341 pub tables: bool,
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub(crate) enum Unnamed {
349 Never,
350 Any,
352 NoExtension,
354}
355
356pub(crate) const EVERYWHERE: Trusted = Trusted {
358 pipe: true,
359 open: Unnamed::Any,
360 listing: true,
361 tables: false,
362};
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
366pub(crate) enum Asked {
367 Pipe,
369 Open { extension: bool },
372 Listing,
374 Tables,
376}
377
378impl Asked {
379 fn believes(self, format: FileFormat, trusted: Trusted) -> bool {
380 match self {
381 Asked::Pipe => trusted.pipe,
382 Asked::Open { extension } => match trusted.open {
383 Unnamed::Never => false,
384 Unnamed::Any => true,
385 Unnamed::NoExtension => !extension,
386 },
387 Asked::Listing => trusted.listing,
388 Asked::Tables => trusted.tables && format.holds_tables(),
389 }
390 }
391}
392
393pub(crate) fn sniff(
398 head: &[u8],
399 file: Option<&Path>,
400 asked: Asked,
401 among: impl Fn(FileFormat) -> bool,
402) -> Option<FileFormat> {
403 [Kind::Magic, Kind::Structure, Kind::Text]
404 .into_iter()
405 .find_map(|kind| {
406 FileFormat::ALL.into_iter().find(|&format| {
407 among(format)
408 && of(format).signatures.iter().any(|sig| {
409 sig.kind == kind
410 && asked.believes(format, sig.trusted)
411 && (sig.says)(head, file)
412 })
413 })
414 })
415}
416
417pub(crate) fn refined(path: &Path, named: FileFormat) -> Option<FileFormat> {
420 if !FileFormat::ALL
421 .into_iter()
422 .any(|f| of(f).refines.contains(&named))
423 {
424 return None;
425 }
426 let head = head_of(path)?;
427 sniff(&head, Some(path), Asked::Open { extension: true }, |f| {
428 of(f).refines.contains(&named)
429 })
430}
431
432pub(crate) fn sniff_file(path: &Path, asked: Asked) -> Option<FileFormat> {
434 let head = head_of(path)?;
435 sniff(&head, Some(path), asked, |_| true)
436}
437
438pub(crate) fn sniff_open(
442 path: &Path,
443 compression: Option<crate::CompressionFormat>,
444) -> Option<FileFormat> {
445 let read_through = |f: FileFormat| f.reads_into();
446 let asked = Asked::Open {
447 extension: path.extension().is_some(),
448 };
449 let is_file = path.is_file();
450 if is_file
451 && let Some(found) =
452 head_of(path).and_then(|head| sniff(&head, Some(path), asked, |_| true))
453 {
454 return Some(found);
455 }
456 let compression = compression.or_else(|| crate::CompressionFormat::from_extension(path))?;
457 if let Some(named) = path
458 .file_stem()
459 .and_then(|stem| FileFormat::from_path(Path::new(stem)))
460 .filter(|f| read_through(*f))
461 {
462 return Some(named);
463 }
464 if !is_file {
465 return None;
466 }
467 let head = crate::formats::head_of(path, Some(compression), HEAD as u64)?;
468 sniff(&head, Some(path), asked, read_through)
469}
470
471pub(crate) fn head_of(path: &Path) -> Option<Vec<u8>> {
473 use std::io::Read;
474 let mut head = Vec::with_capacity(HEAD);
475 std::fs::File::open(path)
476 .ok()?
477 .take(HEAD as u64)
478 .read_to_end(&mut head)
479 .ok()?;
480 Some(head)
481}
482
483pub(crate) fn read_into(input: ScanIn<'_>) -> Result<Scan> {
486 Ok(Scan::ReadInto {
487 files: input.paths.to_vec(),
488 format: input.format,
489 })
490}
491
492pub(crate) fn convert(input: &ConvertIn<'_>) -> ConvertOut {
495 match of(input.format).convert {
496 Some(convert) => convert(input),
497 None => Err(eyre!(
498 "{} files are not read into files of their own.",
499 input.format.name()
500 )),
501 }
502 .map_err(|e| crate::error_display::in_file(input.display, e))
503}
504
505pub(crate) fn many_files_refused() -> String {
507 let (many, one): (Vec<FileFormat>, Vec<FileFormat>) = FileFormat::ALL
508 .into_iter()
509 .partition(|f| f.reads_many_files());
510 let names = |formats: &[FileFormat]| {
511 formats
512 .iter()
513 .map(|f| f.title())
514 .collect::<Vec<_>>()
515 .join(", ")
516 };
517 format!(
518 "Unsupported file type for multiple files: {} files are read as one table; open {} files one at a time.",
519 names(&many),
520 names(&one)
521 )
522}
523
524pub(crate) fn export_default(format: FileFormat) -> Option<ExportFormat> {
526 of(format).export
527}
528
529#[cfg(test)]
531pub(crate) mod bad_input {
532 use std::path::Path;
533 use std::sync::Arc;
534 use std::sync::atomic::{AtomicBool, AtomicU64};
535
536 use super::{ConvertIn, ScanIn};
537 use crate::loading::scan::Scan;
538 use crate::{FileFormat, OpenOptions, ReadReport};
539
540 pub(crate) fn opening(
544 dir: &Path,
545 name: &str,
546 bytes: &[u8],
547 format: FileFormat,
548 options: &OpenOptions,
549 ) -> Option<String> {
550 let path = dir.join(name);
551 std::fs::write(&path, bytes).unwrap();
552 let said =
553 |e: color_eyre::Report| crate::error_display::user_message_from_report(&e, Some(&path));
554 let formats = crate::formats::Registry::of(Vec::new());
555 let mut report = ReadReport::default();
556 let paths = [path.clone()];
557 let scan = super::scan(ScanIn {
558 format,
559 paths: &paths,
560 options,
561 report: &mut report,
562 formats: &formats,
563 });
564 let mut written = Vec::new();
566 let lf = match scan {
567 Err(e) => return Some(said(e)),
568 Ok(Scan::Frame(lf)) => *lf,
569 Ok(Scan::ReadInto { files, format }) => {
570 let unfinished = crate::loading::unfinished::Unfinished::default();
571 let writer = unfinished.writer(Arc::new(AtomicBool::new(false)));
572 let read = AtomicU64::new(0);
573 match super::convert(&ConvertIn {
574 files: &files,
575 display: &path,
576 format,
577 options,
578 formats: &formats,
579 writer: &writer,
580 read: &read,
581 }) {
582 Err(e) => return Some(said(e)),
583 Ok((converted, _)) => {
584 written = converted.files;
585 converted.lf
586 }
587 }
588 }
589 Ok(_) => return None,
590 };
591 let rows = lf.limit(100).collect();
592 drop(written);
593 rows.err().map(|e| said(color_eyre::Report::new(e)))
594 }
595
596 pub(crate) fn assert_shape(message: &str, path: &Path) {
600 let quoted = format!("\"{}\":", path.display());
601 assert!(message.starts_with("ed), "names the file: {message}");
602 let after = &message[quoted.len()..];
603 let what = match after.strip_prefix(' ') {
604 Some(what) => what,
605 None => {
606 let mut parts = after.splitn(3, ':');
608 let (line, column) = (parts.next().unwrap(), parts.next().unwrap_or_default());
609 for n in [line, column] {
610 assert!(
611 n.parse::<usize>().is_ok_and(|n| n > 0),
612 "a place in the file: {message}"
613 );
614 }
615 parts
616 .next()
617 .and_then(|what| what.strip_prefix(' '))
618 .unwrap_or_else(|| panic!("a place, then a space: {message}"))
619 }
620 };
621 let first = message.lines().next().unwrap_or_default();
622 assert!(first.ends_with('.'), "ends with a full stop: {message}");
623 assert!(
624 crate::error_display::starts_with_a_key(what)
625 || what.chars().next().is_some_and(|c| !c.is_lowercase()),
626 "sentence case: {message}"
627 );
628 assert_eq!(
629 what.matches(path.to_string_lossy().as_ref()).count(),
630 0,
631 "named once: {message}"
632 );
633 for rust in [
634 "Some(",
635 "None",
636 "Error {",
637 "Kind(",
638 "PolarsError",
639 "ComputeError",
640 "os error",
641 ] {
642 assert!(!message.contains(rust), "no Rust ({rust}): {message}");
643 }
644 }
645
646 pub(crate) fn each_names_its_file(format: FileFormat, bad: &[(&str, &[u8], &str)]) {
649 let dir = tempfile::tempdir().unwrap();
650 for (name, bytes, says) in bad {
651 let message = opening(dir.path(), name, bytes, format, &OpenOptions::default())
652 .unwrap_or_else(|| panic!("{name} opens"));
653 eprintln!("{message}");
654 assert_shape(&message, &dir.path().join(name));
655 assert!(message.contains(says), "{name}: {message}");
656 }
657 }
658}