Skip to main content

datui_lib/formats/
open.rs

1//! A spec and a file opened as a table.
2
3use super::*;
4
5impl Spec {
6    /// The columns the record fields become, a record `stride` bytes apart (or one
7    /// file per field for the columns layout), and the bytes the fields take.
8    pub(crate) fn record_columns(
9        &self,
10        header: &HeaderValues,
11        header_size: usize,
12        stride: Option<usize>,
13    ) -> Result<(Vec<ColumnLayout>, u64), String> {
14        let mut columns = Vec::new();
15        let mut at = 0u64;
16        for (i, field) in self.records.fields.iter().enumerate() {
17            let (width, count) = sized(field, header)?;
18            if width == 0 {
19                return Err(format!(
20                    "field `{}` takes no bytes",
21                    field.name.as_deref().unwrap_or("pad")
22                ));
23            }
24            let start = match self.layout {
25                Layout::Rows => header_size + at as usize,
26                Layout::Columns => header_size,
27            };
28            if let Some(name) = &field.name {
29                let source = match self.layout {
30                    Layout::Rows => 0,
31                    Layout::Columns => i,
32                };
33                if field.flatten {
34                    for j in 0..count as usize {
35                        let place = Place {
36                            start: start + j * width as usize,
37                            stride,
38                            width: width as usize,
39                            count: 1,
40                        };
41                        let mut layout =
42                            layout_of(self, field, &format!("{name}_{j}"), place, header)?;
43                        layout.source = source;
44                        columns.push(layout);
45                    }
46                } else {
47                    let place = Place {
48                        start,
49                        stride,
50                        width: width as usize,
51                        count: count as usize,
52                    };
53                    let mut layout = layout_of(self, field, name, place, header)?;
54                    layout.source = source;
55                    columns.push(layout);
56                }
57            }
58            at += width * count;
59        }
60        Ok((columns, at))
61    }
62
63    /// Check the magic of `bytes`, the front of one file.
64    fn check_magic(&self, bytes: &[u8], named: &str) -> Result<(), String> {
65        if self.magic.is_empty() || self.magic_matches(bytes) {
66            return Ok(());
67        }
68        let start = (self.magic_offset as usize).min(bytes.len());
69        let found = &bytes[start..(start + self.magic.len()).min(bytes.len())];
70        Err(format!(
71            "{named} is not {}: expected magic {} at byte {}, found {}",
72            self.name,
73            crate::formats::fixed_records::hex(&self.magic),
74            self.magic_offset,
75            if found.is_empty() {
76                "the end of the file".to_string()
77            } else {
78                crate::formats::fixed_records::hex(found)
79            }
80        ))
81    }
82
83    /// Read `bytes`, one file of the rows layout, named `named` in what it says.
84    pub fn open_rows(&self, bytes: Arc<Bytes>, named: &str) -> Result<Opened, String> {
85        if self.is_delimited() {
86            return Err(format!(
87                "{} is a delimited spec; it reads text through the CSV reader",
88                self.name
89            ));
90        }
91        self.open_rows_in(bytes, named, None)
92    }
93
94    /// The spec with its byte order settled by `head`, for `endian = "auto"`: as the
95    /// spec says when the magic reads as written, big-endian when it reads reversed.
96    pub fn for_file(&self, head: &[u8]) -> std::borrow::Cow<'_, Spec> {
97        if !self.endian_auto {
98            return std::borrow::Cow::Borrowed(self);
99        }
100        let reversed: Vec<u8> = self.magic.iter().rev().copied().collect();
101        let start = self.magic_offset as usize;
102        if reversed != self.magic && head.get(start..start + reversed.len()) == Some(&reversed[..])
103        {
104            let mut spec = self.clone();
105            spec.endian = Endian::Big;
106            spec.endian_auto = false;
107            spec.magic = reversed;
108            return std::borrow::Cow::Owned(spec);
109        }
110        std::borrow::Cow::Borrowed(self)
111    }
112
113    /// Whether the spec reads a directory: column files, or a tree of its files.
114    pub fn reads_directory(&self) -> bool {
115        self.files.is_some()
116            || (self.layout == Layout::Columns
117                && !self.records.fields.iter().any(|f| f.at.is_some()))
118    }
119
120    /// The spec reading the variant `name` alone: only its records, and only its columns.
121    pub fn with_variant(&self, name: &str) -> Result<Spec, String> {
122        if !self.records.variants.iter().any(|v| v.name == name) {
123            let names: Vec<&str> = self
124                .records
125                .variants
126                .iter()
127                .map(|v| v.name.as_str())
128                .collect();
129            return Err(if names.is_empty() {
130                format!("{} has no variants", self.name)
131            } else {
132                format!(
133                    "{} has no variant {name}; it has {}",
134                    self.name,
135                    names.join(", ")
136                )
137            });
138        }
139        let mut spec = self.clone();
140        spec.variant = Some(name.to_string());
141        Ok(spec)
142    }
143
144    /// Read `bytes`, one file, named `named` in what it says; a symbol list a field
145    /// names is looked for beside `path`, the file the bytes are, which also keeps the
146    /// walk of its records for the next open of it.
147    pub fn open_rows_in(
148        &self,
149        bytes: Arc<Bytes>,
150        named: &str,
151        path: Option<&Path>,
152    ) -> Result<Opened, String> {
153        let dir = path.and_then(Path::parent);
154        if self.reads_directory() {
155            return Err(format!(
156                "{} reads a directory ({}); open the directory",
157                self.name,
158                if self.files.is_some() {
159                    "a tree of its files"
160                } else {
161                    "column files, layout = \"columns\""
162                }
163            ));
164        }
165        let spec = self.for_file(bytes.as_slice());
166        let spec = spec.as_ref();
167        if spec.layout == Layout::Columns {
168            return spec.open_columns_file(bytes, named, dir);
169        }
170        let data = bytes.as_slice();
171        let mut header = if spec.capture.is_some() {
172            if !crate::formats::framed_records::capture::is_capture(data) {
173                return Err(format!("{named} is not a pcap or pcapng capture"));
174            }
175            HeaderValues::default()
176        } else {
177            spec.check_magic(data, named)?;
178            read_header(spec, data)?
179        };
180        let mut notes = Vec::new();
181        let full = data.len() as u64;
182        if header.size > full {
183            return Err(format!(
184                "{named} is {full} bytes, shorter than its {}-byte header",
185                header.size
186            ));
187        }
188        let (len, footer_note) = read_footer(spec, data, &mut header)?;
189        notes.extend(footer_note);
190        let fields: Vec<Field> = all_fields(&spec.records).cloned().collect();
191        read_lookups(&fields, dir, &mut header)?;
192        if crate::formats::framed_records::needed(spec) {
193            let (records, more) = crate::formats::framed_records::FramedRecords::open(
194                spec,
195                bytes.clone(),
196                &header,
197                header.size as usize..len as usize,
198                named,
199                path,
200            )?;
201            notes.extend(more);
202            return Ok(Opened {
203                records: Arc::new(records),
204                notes,
205                header,
206            });
207        }
208        let data = &data[..len as usize];
209        // The fields' own width first, to check the record size against it.
210        let (_, fields_width) = spec.record_columns(&header, 0, Some(1))?;
211        let record = match &spec.records.size {
212            None => fields_width,
213            Some(amount) => {
214                let size = header.resolve(amount, "record size")?;
215                if size < fields_width {
216                    return Err(format!(
217                        "the record's fields take {fields_width} bytes, more than its size of {size}"
218                    ));
219                }
220                size
221            }
222        };
223        if record == 0 || record > MAX_SIZE {
224            return Err(format!(
225                "a record of {record} bytes is outside 1 to {MAX_SIZE}"
226            ));
227        }
228        let room = len - header.size;
229        let whole = room / record;
230        let rows = match &spec.records.count {
231            None => {
232                let trailing = room % record;
233                if trailing > 0 {
234                    notes.push(trailing_note(named, &data[(len - trailing) as usize..]));
235                }
236                whole
237            }
238            Some(amount) => {
239                let count = header.resolve(amount, "count")?;
240                if count > whole {
241                    notes.push(format!(
242                        "header says {count} records {} {whole} whole ones shown",
243                        crate::glyphs::get().middot
244                    ));
245                    whole
246                } else {
247                    let end = header.size + count * record;
248                    if end < len {
249                        let after = len - end;
250                        notes.push(format!(
251                            "{named} has {after} {} after its {count} records, left out",
252                            if after == 1 { "byte" } else { "bytes" }
253                        ));
254                    }
255                    count
256                }
257            }
258        };
259        let (columns, _) =
260            spec.record_columns(&header, header.size as usize, Some(record as usize))?;
261        let records =
262            FixedRecords::new(vec![bytes], columns, rows as usize).map_err(|e| e.to_string())?;
263        past_limit(&mut notes, rows, &records);
264        Ok(Opened {
265            records: Arc::new(records),
266            notes,
267            header,
268        })
269    }
270
271    /// Read `dir`, a directory of one file per record field.
272    pub fn open_columns(&self, dir: &Path) -> Result<Opened, String> {
273        if self.is_delimited() {
274            return Err(format!(
275                "{} is a delimited spec; it reads text through the CSV reader",
276                self.name
277            ));
278        }
279        if self.layout == Layout::Rows {
280            return Err(format!(
281                "{} reads one file, and {} is a directory",
282                self.name,
283                dir.display()
284            ));
285        }
286        let mut sources = Vec::new();
287        let mut header: Option<HeaderValues> = None;
288        let mut notes = Vec::new();
289        let mut counts: Vec<(String, u64)> = Vec::new();
290        // Each file's own header size: a size read from the header may differ by file.
291        let mut starts = Vec::new();
292        for field in &self.records.fields {
293            let file_name = field
294                .file
295                .clone()
296                .or_else(|| field.name.clone())
297                .expect("a column field is named");
298            let path = dir.join(&file_name);
299            let bytes = Bytes::map(&path).map_err(|e| format!("{}: {e}", path.display()))?;
300            self.check_magic(bytes.as_slice(), &file_name)?;
301            let read = read_header(self, bytes.as_slice())?;
302            let (width, count) = sized(field, header.as_ref().unwrap_or(&read))?;
303            let cell = width * count;
304            let len = bytes.len() as u64;
305            if read.size > len {
306                return Err(format!(
307                    "{file_name} is {len} bytes, shorter than its {}-byte header",
308                    read.size
309                ));
310            }
311            let room = len - read.size;
312            if cell > 0 && !room.is_multiple_of(cell) {
313                let trailing = room % cell;
314                notes.push(trailing_note(
315                    &file_name,
316                    &bytes.as_slice()[(len - trailing) as usize..],
317                ));
318            }
319            counts.push((file_name, room.checked_div(cell).unwrap_or(0)));
320            starts.push(read.size as usize);
321            if header.is_none() {
322                header = Some(read);
323            }
324            sources.push(Arc::new(bytes));
325        }
326        let mut header = header.unwrap_or_default();
327        read_lookups(&self.records.fields, Some(dir), &mut header)?;
328        let fewest = counts.iter().map(|(_, n)| *n).min().unwrap_or(0);
329        if counts.iter().any(|(_, n)| *n != fewest) {
330            let said: Vec<String> = counts
331                .iter()
332                .map(|(name, n)| format!("{name} {n}"))
333                .collect();
334            notes.push(format!(
335                "column files differ in length ({}) {} first {fewest} rows shown",
336                said.join(", "),
337                crate::glyphs::get().middot
338            ));
339        }
340        let mut rows = fewest;
341        if let Some(amount) = &self.records.count {
342            let count = header.resolve(amount, "count")?;
343            if count > fewest {
344                notes.push(format!(
345                    "header says {count} records {} {fewest} shown",
346                    crate::glyphs::get().middot
347                ));
348            }
349            rows = rows.min(count);
350        }
351        let (mut columns, _) = self.record_columns(&header, 0, None)?;
352        for column in &mut columns {
353            column.start = starts[column.source];
354        }
355        let records =
356            FixedRecords::new(sources, columns, rows as usize).map_err(|e| e.to_string())?;
357        past_limit(&mut notes, rows, &records);
358        Ok(Opened {
359            records: Arc::new(records),
360            notes,
361            header,
362        })
363    }
364
365    /// Read `bytes`, one file holding each column's values in a run at its `offset`.
366    fn open_columns_file(
367        &self,
368        bytes: Arc<Bytes>,
369        named: &str,
370        dir: Option<&Path>,
371    ) -> Result<Opened, String> {
372        let data = bytes.as_slice();
373        self.check_magic(data, named)?;
374        let mut header = read_header(self, data)?;
375        let mut notes = Vec::new();
376        let (len, footer_note) = read_footer(self, data, &mut header)?;
377        notes.extend(footer_note);
378        read_lookups(&self.records.fields, dir, &mut header)?;
379        let (mut columns, _) = self.record_columns(&header, 0, None)?;
380        let mut rows = u64::MAX;
381        let mut starts = Vec::new();
382        for field in &self.records.fields {
383            let (width, count) = sized(field, &header)?;
384            let at = field.at.as_ref().expect("checked at parse");
385            let start = header.resolve_any(at, "offset")?;
386            if start < header.size || start > len {
387                return Err(format!(
388                    "column `{}` starts at byte {start}, outside the data from {} to {len}",
389                    field.name.as_deref().unwrap_or("pad"),
390                    header.size
391                ));
392            }
393            rows = rows.min((len - start) / (width * count).max(1));
394            starts.push(start as usize);
395        }
396        if let Some(amount) = &self.records.count {
397            let count = header.resolve_any(amount, "count")?;
398            if count > rows {
399                notes.push(format!(
400                    "header says {count} records {} room for {rows} shown",
401                    crate::glyphs::get().middot
402                ));
403            }
404            rows = rows.min(count);
405        } else {
406            notes.push(format!(
407                "no count is given, so the rows are the {rows} the shortest column has room for"
408            ));
409        }
410        for column in &mut columns {
411            column.start = starts[column.source];
412        }
413        let sources = vec![bytes; self.records.fields.len()];
414        let records = FixedRecords::new(sources, columns, rows.min(usize::MAX as u64) as usize)
415            .map_err(|e| e.to_string())?;
416        Ok(Opened {
417            records: Arc::new(records),
418            notes,
419            header,
420        })
421    }
422
423    /// Open `path`: a file, or a directory of column files or of the spec's files.
424    pub fn open(&self, path: &Path, named: &str) -> Result<Opened, String> {
425        if self.files.is_some() {
426            return crate::formats::files::open(self, path);
427        }
428        if self.reads_directory() {
429            return self.open_columns(path);
430        }
431        if path.is_dir() {
432            return Err(format!(
433                "{} reads one file, and {} is a directory",
434                self.name,
435                path.display()
436            ));
437        }
438        let bytes = Bytes::map(path).map_err(|e| format!("{}: {e}", path.display()))?;
439        self.open_rows_in(Arc::new(bytes), named, Some(path))
440    }
441}