Skip to main content

datui_lib/formats/
text_formats.rs

1//! Text formats read into a table of their own: VCD value change dumps, FIX logs, SDF
2//! compound files and the GPS logs of [`crate::formats::gps`]; and [`Detail`], every format's
3//! tab of the Info panel.
4//!
5//! None of those formats can be scanned in place, so an open reads the file once,
6//! start to end, a piece at a time through its [`BatchReader`], and writes its rows to
7//! temporary Arrow IPC segments (see `crate::formats::segments`) that the dataset scans
8//! lazily. Memory stays at one batch however long the file.
9
10use std::fs::File;
11use std::io::{BufReader, Read};
12use std::path::Path;
13use std::sync::Arc;
14use std::sync::atomic::{AtomicU64, Ordering};
15
16use color_eyre::Result;
17use color_eyre::eyre::eyre;
18use polars::prelude::{DataFrame, LazyFrame, PolarsResult};
19
20use crate::cloud::download::TempDownload;
21use crate::formats::model_files::MetaValue;
22use crate::formats::readers::{ConvertIn, ConvertOut};
23use crate::formats::segments::{Converted, Segments};
24use crate::loading::unfinished::Writer;
25use crate::notes::Note;
26use crate::numfmt::group_chrome;
27use crate::{CompressionFormat, OpenOptions};
28
29/// How much of the file is read at a time.
30const CHUNK: usize = 1 << 16;
31
32/// What a file says besides its rows, for its tab of the Info panel: a VCD header, the
33/// FIX dictionaries used, a model's totals and metadata, an audio file's format and
34/// markers. Every format's tab is one of these, made by its reader when it opens.
35#[derive(Debug, Clone, Default, PartialEq)]
36pub struct Detail {
37    /// The tab's name: `VCD`, `Model`, `Audio`, as [`tab`] gives it.
38    pub tab: &'static str,
39    /// The lines above the list.
40    pub lines: Vec<String>,
41    /// Lines after them in the warning color: what the file holds that is not shown.
42    pub warnings: Vec<String>,
43    /// The list's title: `Signals`, `Metadata`, `Tracks`.
44    pub list_title: &'static str,
45    /// The list, key and value.
46    pub list: Vec<(String, MetaValue)>,
47    /// Whether `i` opens on this tab rather than the schema: a table whose columns are
48    /// the same for every file, where what is particular to it is here.
49    pub first: bool,
50    /// Whether the columns are datui's own rather than the file's, as the Schema tab
51    /// says of where they came from.
52    pub own_columns: bool,
53    /// The keys of `list` that are tables of the file, which `T` at the table and
54    /// Enter on the tab open: a workbook's worksheets, a database's tables.
55    pub tables: Vec<String>,
56    /// The one of `tables` on screen.
57    pub table: Option<String>,
58}
59
60/// The name of `format`'s tab of the Info panel, as its descriptor says it: a reader
61/// names its [`Detail`] by this, so the descriptor is the one place that says it.
62pub const fn tab(format: crate::FileFormat) -> &'static str {
63    match format.descriptor().summary {
64        crate::Summary::Tab(tab) => tab,
65        // A format said to have no tab has none to name; its title stands in.
66        crate::Summary::None(_) => format.descriptor().title,
67    }
68}
69
70/// A text format's reader: it takes the file a piece at a time and hands back its rows
71/// a batch at a time, keeping no more of the file than the batch.
72pub trait BatchReader {
73    fn push(&mut self, piece: &[u8]) -> Result<()>;
74    /// The rows ready, once a batch's worth is.
75    fn take_batch(&mut self) -> PolarsResult<Option<DataFrame>>;
76    /// The rows left at the end of the file.
77    fn finish(&mut self) -> Result<DataFrame>;
78}
79
80/// A reader that counts the bytes read through it, for the loading screen.
81struct Counted<'a, R> {
82    inner: R,
83    read: &'a AtomicU64,
84}
85
86impl<R: Read> Read for Counted<'_, R> {
87    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
88        let n = self.inner.read(buf)?;
89        self.read.fetch_add(n as u64, Ordering::Relaxed);
90        Ok(n)
91    }
92}
93
94/// The file, decompressed as it is read when its name or `--compression` says so;
95/// `read` counts the bytes of the file as stored.
96pub(crate) fn open_reader<'a>(
97    file: &Path,
98    options: &OpenOptions,
99    read: &'a AtomicU64,
100) -> Result<Box<dyn Read + 'a>> {
101    let f = BufReader::new(Counted {
102        inner: File::open(file)?,
103        read,
104    });
105    Ok(
106        match options
107            .compression
108            .or_else(|| CompressionFormat::from_extension(file))
109        {
110            None => Box::new(f),
111            Some(CompressionFormat::Gzip) => Box::new(flate2::read::MultiGzDecoder::new(f)),
112            Some(CompressionFormat::Zstd) => Box::new(zstd::Decoder::new(f)?),
113            Some(CompressionFormat::Bzip2) => Box::new(bzip2::read::BzDecoder::new(f)),
114            Some(CompressionFormat::Xz) => Box::new(xz2::read::XzDecoder::new(f)),
115        },
116    )
117}
118
119/// Read `file` through `reader` a piece at a time, through its compression, into
120/// temporary segments written through `writer`, counting the bytes read in `read`;
121/// `each` sees every batch first. Gives the frame over the segments, and their files.
122pub(crate) fn read_through<R: BatchReader>(
123    file: &Path,
124    options: &OpenOptions,
125    writer: &Writer,
126    read: &AtomicU64,
127    reader: &mut R,
128    mut each: impl FnMut(&DataFrame),
129) -> Result<(LazyFrame, Vec<TempDownload>)> {
130    let mut source = open_reader(file, options, read)?;
131    let mut segments = Segments::new(options, writer);
132    let mut chunk = vec![0u8; CHUNK];
133    loop {
134        if writer.stopped() {
135            return Err(eyre!("Reading was stopped."));
136        }
137        let n = match source.read(&mut chunk) {
138            Ok(0) => break,
139            Ok(n) => n,
140            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
141            Err(e) => return Err(e.into()),
142        };
143        reader.push(&chunk[..n])?;
144        if let Some(df) = reader.take_batch()? {
145            each(&df);
146            segments.write(&df)?;
147        }
148    }
149    let last = reader.finish()?;
150    each(&last);
151    segments.write(&last)?;
152    segments.finish()
153}
154
155/// The conversion of a format read one file at a time: the file read through
156/// `reader`, then `finish` gives the frame, the notes and the Info panel's tab from the
157/// frame over its segments and what the reader saw.
158pub(crate) fn convert_with<R: BatchReader>(
159    input: &ConvertIn<'_>,
160    mut reader: R,
161    finish: impl FnOnce(&R, LazyFrame) -> Result<(LazyFrame, Vec<Note>, Detail)>,
162) -> ConvertOut {
163    let [file] = input.files else {
164        return Err(eyre!("Open {} files one at a time.", input.format.name()));
165    };
166    let (lf, files) = read_through(
167        file,
168        input.options,
169        input.writer,
170        input.read,
171        &mut reader,
172        |_| {},
173    )?;
174    let (lf, notes, detail) = finish(&reader, lf)?;
175    let converted = Converted {
176        lf,
177        files,
178        notes,
179        other_tables: Vec::new(),
180    };
181    Ok((converted, Some(Arc::new(detail))))
182}
183
184pub(crate) fn note(summary: String, scope: String) -> Note {
185    Note {
186        summary,
187        scope,
188        read_as_text: None,
189        passed_over: None,
190    }
191}
192
193/// `n` and its noun, singular or plural: `1 line`, `2 lines`.
194pub(crate) fn count(n: u64, one: &str, many: &str) -> String {
195    let n = usize::try_from(n).unwrap_or(usize::MAX);
196    format!("{} {}", group_chrome(n), if n == 1 { one } else { many })
197}
198
199/// A list for a [`Detail`], at most `limits.detail_rows` long; one row more says how
200/// many were left out.
201pub(crate) fn capped_list(
202    rows: impl Iterator<Item = (String, MetaValue)>,
203    total: usize,
204) -> Vec<(String, MetaValue)> {
205    let mut list: Vec<_> = rows.take(crate::limits::get().detail_rows).collect();
206    if total > list.len() {
207        let more = total - list.len();
208        list.push((
209            crate::glyphs::get().ellipsis.to_string(),
210            MetaValue::Text(format!(
211                "{} more {} limits.detail_rows raises it",
212                group_chrome(more),
213                crate::glyphs::get().middot
214            )),
215        ));
216    }
217    list
218}