Skip to main content

datui_lib/formats/
mod.rs

1//! Binary formats described by specs: one TOML file per format, on a search path. A
2//! spec names its format (`acme.l2feed`), says which files match (glob, magic, header
3//! values), and lays out header and records. Specs are data (no expressions or code:
4//! fields refer to earlier ones by name), and every size read from a file is bounded.
5//! Decoding is [`crate::formats::fixed_records`]' for fixed-size records and
6//! [`crate::formats::framed_records`]' otherwise; [`files`] reads a directory of one spec's
7//! files. This module turns a spec and a file into that reader's columns.
8
9pub mod audio;
10pub mod candump;
11pub mod column_types;
12pub mod columns;
13pub mod csv_dialect;
14pub mod dataflash;
15pub(crate) mod dataset_files;
16pub mod dbc;
17pub mod delimited_spec;
18pub mod elf;
19pub mod excel;
20pub mod fix;
21pub mod fixed_records;
22pub mod framed_records;
23pub mod gps;
24pub mod hf_splits;
25pub mod indexed;
26pub mod ipc_stream;
27pub mod journal;
28pub mod lines;
29pub mod members;
30pub mod midi;
31pub mod model_files;
32pub mod nul_tail;
33pub mod numpy;
34pub mod parquet_footer;
35pub mod pushdown;
36pub mod readers;
37pub mod row_index;
38pub mod schema_union;
39pub mod sdf;
40pub(crate) mod segments;
41pub(crate) mod spec_union;
42pub mod sqlite;
43pub mod text_formats;
44pub mod ulog;
45pub mod vcd;
46
47use crate::formats::fixed_records::{Bytes, ColumnLayout, FixedRecords, Logical, Null, Physical};
48use crate::home::catalog::ColumnNote;
49use globset::{Glob, GlobSet, GlobSetBuilder};
50use polars::prelude::{
51    AnyValue, DataFrame, LazyFrame, PlSmallStr, PolarsResult, SchemaRef, TimeUnit,
52};
53use std::collections::BTreeMap;
54use std::ops::Range;
55use std::path::{Path, PathBuf};
56use std::sync::Arc;
57use toml::de::{DeTable, DeValue};
58
59/// The largest size a spec or a file may give one field, header or record, and the
60/// most values one field may hold. A size read from a file past this is refused
61/// rather than believed.
62pub const MAX_SIZE: u64 = 64 << 20;
63
64/// The most columns one field may be flattened into.
65pub const MAX_FLATTEN: u64 = 1024;
66
67/// The most bytes at the front of a file read to match it: its magic and the header
68/// values `match.where` compares.
69const MAX_MATCH_READ: u64 = 64 << 10;
70
71/// Nanoseconds in a day.
72const DAY_NS: i64 = 86_400_000_000_000;
73
74/// The variable the search path is extended with, after the config directory.
75pub const PATH_VAR: &str = "DATUI_FORMATS_PATH";
76
77/// A problem with a spec: where it is (file, line, column) and what was expected.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct SpecError {
80    pub path: Option<PathBuf>,
81    /// One-based; 0 when the problem is not at one place in the text.
82    pub line: usize,
83    pub column: usize,
84    pub message: String,
85}
86
87/// Said as every reader error is, with the place in the text a compiler gives:
88/// `"spec.toml":3:7: <what went wrong>.`
89impl std::fmt::Display for SpecError {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        let at = (self.line > 0).then_some((self.line, self.column));
92        f.write_str(&crate::error_display::located_message(
93            self.path.as_deref(),
94            at,
95            &self.message,
96        ))
97    }
98}
99
100impl std::error::Error for SpecError {}
101
102/// Byte order.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum Endian {
105    Little,
106    Big,
107}
108
109/// How the records sit in what is opened.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum Layout {
112    /// One file, a record after another.
113    Rows,
114    /// A directory holding one file of fixed-width values per field, as kdb+ splays a
115    /// table; or, when every field gives its `offset`, one file holding each column's
116    /// values in a run of their own.
117    Columns,
118}
119
120/// How one record is told from the next.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub enum Framing {
123    /// Each record takes the same bytes: `size`, or what its fields take.
124    #[default]
125    Fixed,
126    /// A field of the record gives its size (`size = "len"`).
127    LengthPrefixed,
128    /// The variant the type field picks gives the record's size.
129    Variant,
130    /// Each record starts with the `sync` marker; bytes between records are skipped.
131    Sync,
132}
133
134/// A size or a count: written in the spec, or read from a field.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub enum Amount {
137    Given(u64),
138    /// The value of header field `field`, plus `adjust`.
139    Header {
140        field: String,
141        adjust: i64,
142    },
143    /// The value of footer field `field`, plus `adjust`.
144    Footer {
145        field: String,
146        adjust: i64,
147    },
148    /// The value of an earlier field of the same record (or block header, or group
149    /// item), plus `adjust`.
150    Record {
151        field: String,
152        adjust: i64,
153    },
154    /// What is left of the record: `size = "rest"`.
155    Rest,
156}
157
158impl Amount {
159    /// Whether the amount is known before a record is read.
160    pub fn is_fixed(&self) -> bool {
161        !matches!(self, Self::Record { .. } | Self::Rest)
162    }
163}
164
165/// What a field's bytes hold.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum Type {
168    /// 1 to 8 bytes.
169    Unsigned(u8),
170    Signed(u8),
171    /// 2 (half), 4 or 8 bytes.
172    Float(u8),
173    /// bfloat16.
174    BFloat16,
175    Bool,
176    Str,
177    /// Text up to its NUL, within `size` when one is given.
178    Strz,
179    Bytes,
180    Pad,
181    /// LEB128: unsigned, and zigzag signed.
182    VarU,
183    VarS,
184    /// A counted run of items, each of the group's fields.
185    Group,
186}
187
188impl Type {
189    /// Bytes one value of the type takes, when the type says.
190    pub fn width(self) -> Option<u64> {
191        match self {
192            Self::Strz | Self::VarU | Self::VarS | Self::Group => None,
193            _ => self.physical().width().map(|w| w as u64),
194        }
195    }
196
197    fn is_integer(self) -> bool {
198        matches!(
199            self,
200            Self::Unsigned(_) | Self::Signed(_) | Self::VarU | Self::VarS
201        )
202    }
203
204    fn is_number(self) -> bool {
205        self.is_integer() || matches!(self, Self::Float(_) | Self::BFloat16)
206    }
207
208    pub fn is_text(self) -> bool {
209        matches!(self, Self::Str | Self::Strz)
210    }
211
212    fn physical(self) -> Physical {
213        match self {
214            Self::Unsigned(n) => Physical::Unsigned(n),
215            Self::Signed(n) => Physical::Signed(n),
216            Self::Float(n) => Physical::Float(n),
217            Self::BFloat16 => Physical::BFloat16,
218            Self::Bool => Physical::Bool,
219            Self::Str | Self::Strz => Physical::Text,
220            // A varint is decoded to eight bytes before its meaning is applied.
221            Self::VarU => Physical::Unsigned(8),
222            Self::VarS => Physical::Signed(8),
223            Self::Bytes | Self::Pad | Self::Group => Physical::Raw,
224        }
225    }
226}
227
228/// How text is stored.
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
230pub enum Encoding {
231    #[default]
232    Utf8,
233    Latin1,
234    Utf16Le,
235    Utf16Be,
236}
237
238impl Encoding {
239    pub fn physical(self) -> Physical {
240        match self {
241            Self::Utf8 => Physical::Text,
242            Self::Latin1 => Physical::Latin1,
243            Self::Utf16Le => Physical::Utf16 { big_endian: false },
244            Self::Utf16Be => Physical::Utf16 { big_endian: true },
245        }
246    }
247
248    /// Bytes in one code unit: where a NUL is looked for.
249    pub fn unit(self) -> usize {
250        match self {
251            Self::Utf16Le | Self::Utf16Be => 2,
252            _ => 1,
253        }
254    }
255}
256
257/// A value stored as the change from the previous record's.
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
259pub enum Delta {
260    #[default]
261    None,
262    /// Summed from the first record.
263    All,
264    /// Summed from the first record of each block.
265    Block,
266}
267
268/// One bit field of an integer: its own column.
269#[derive(Debug, Clone, PartialEq)]
270pub struct BitField {
271    pub name: String,
272    /// The lowest bit, 0 the least significant.
273    pub bit: u32,
274    pub width: u32,
275    pub labels: Option<Arc<BTreeMap<i64, String>>>,
276}
277
278/// Where a symbol list is, for a field of indexes into it.
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct Lookup {
281    /// Relative to the directory the data is in.
282    pub file: String,
283    pub format: LookupFormat,
284}
285
286/// How a symbol list's entries are told apart.
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum LookupFormat {
289    /// One a line.
290    Lines,
291    /// Each ended by a NUL, as kdb+ writes its `sym` file.
292    Nul,
293    /// Each `n` bytes, padded.
294    Fixed(u64),
295}
296
297/// A checksum over part of each record, or of the file before its footer.
298#[derive(Debug, Clone, PartialEq, Eq)]
299pub struct Checksum {
300    pub algo: ChecksumAlgo,
301    /// The field holding the stored checksum.
302    pub field: String,
303    /// The field the checksummed bytes start at; the record's start when `None`.
304    pub from: Option<String>,
305    /// The field they end before; the checksum field when `None`.
306    pub to: Option<String>,
307}
308
309/// The checksums a spec can name.
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum ChecksumAlgo {
312    Crc16Ccitt,
313    Crc16Xmodem,
314    Crc16Modbus,
315    Crc16Arc,
316    Crc32,
317    Crc32c,
318    Sum8,
319    Xor8,
320}
321
322impl ChecksumAlgo {
323    const NAMES: &[(&str, Self)] = &[
324        ("crc16-ccitt", Self::Crc16Ccitt),
325        ("crc16-xmodem", Self::Crc16Xmodem),
326        ("crc16-modbus", Self::Crc16Modbus),
327        ("crc16-arc", Self::Crc16Arc),
328        ("crc32", Self::Crc32),
329        ("crc32c", Self::Crc32c),
330        ("sum8", Self::Sum8),
331        ("xor8", Self::Xor8),
332    ];
333
334    fn parse(name: &str) -> Option<Self> {
335        Self::NAMES
336            .iter()
337            .find(|(n, _)| *n == name)
338            .map(|(_, a)| *a)
339    }
340
341    /// The checksum of `bytes`.
342    pub fn compute(self, bytes: &[u8]) -> u64 {
343        use crc::{
344            CRC_16_ARC, CRC_16_IBM_3740, CRC_16_MODBUS, CRC_16_XMODEM, CRC_32_ISCSI,
345            CRC_32_ISO_HDLC, Crc,
346        };
347        match self {
348            Self::Crc16Ccitt => u64::from(Crc::<u16>::new(&CRC_16_IBM_3740).checksum(bytes)),
349            Self::Crc16Xmodem => u64::from(Crc::<u16>::new(&CRC_16_XMODEM).checksum(bytes)),
350            Self::Crc16Modbus => u64::from(Crc::<u16>::new(&CRC_16_MODBUS).checksum(bytes)),
351            Self::Crc16Arc => u64::from(Crc::<u16>::new(&CRC_16_ARC).checksum(bytes)),
352            Self::Crc32 => u64::from(Crc::<u32>::new(&CRC_32_ISO_HDLC).checksum(bytes)),
353            Self::Crc32c => u64::from(Crc::<u32>::new(&CRC_32_ISCSI).checksum(bytes)),
354            Self::Sum8 => u64::from(bytes.iter().fold(0u8, |a, b| a.wrapping_add(*b))),
355            Self::Xor8 => u64::from(bytes.iter().fold(0u8, |a, b| a ^ b)),
356        }
357    }
358}
359
360/// The unit a `time` field counts in.
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub enum TimeUnitSpec {
363    Days,
364    Seconds,
365    Millis,
366    Micros,
367    Nanos,
368}
369
370impl TimeUnitSpec {
371    fn nanos(self) -> i64 {
372        match self {
373            Self::Days => DAY_NS,
374            Self::Seconds => 1_000_000_000,
375            Self::Millis => 1_000_000,
376            Self::Micros => 1_000,
377            Self::Nanos => 1,
378        }
379    }
380}
381
382/// What a field means beyond its stored value.
383#[derive(Debug, Clone, PartialEq)]
384pub enum Meaning {
385    Plain,
386    /// A count of `unit` since `epoch_ns` nanoseconds past the Unix epoch: a datetime,
387    /// or a date for whole days.
388    Time {
389        unit: TimeUnitSpec,
390        epoch_ns: i64,
391    },
392    /// A count of `unit` since midnight: a time of day, or a datetime on the date the
393    /// header field `date` holds.
394    TimeOfDay {
395        unit: TimeUnitSpec,
396        date: Option<String>,
397    },
398    /// An integer written as `YYYYMMDD`.
399    Yyyymmdd,
400    /// `scale` implied decimal places.
401    Scale(u32),
402    /// `value * factor + offset`, as a float.
403    Linear {
404        factor: f64,
405        offset: f64,
406    },
407    /// Codes and their labels.
408    Enum(Arc<BTreeMap<i64, String>>),
409}
410
411/// One field of a header or a record.
412#[derive(Debug, Clone, PartialEq)]
413pub struct Field {
414    /// `None` for `pad`.
415    pub name: Option<String>,
416    pub ty: Type,
417    /// For `str`, `bytes` and `pad`.
418    pub size: Option<Amount>,
419    /// The type's own `le` or `be`, over the spec's.
420    pub endian: Option<Endian>,
421    pub meaning: Meaning,
422    /// A stored value that means no value.
423    pub null: Option<Null>,
424    /// Values side by side: an Array column, or `flatten`ed into `name_0`, `name_1`, ...
425    pub count: Option<Amount>,
426    pub flatten: bool,
427    /// In the columns layout, the file in the directory that holds it; its name by
428    /// default.
429    pub file: Option<String>,
430    /// In the columns layout of one file, where the column's values start.
431    pub at: Option<Amount>,
432    /// For text.
433    pub encoding: Encoding,
434    pub delta: Delta,
435    /// Columns of their own from the integer's bits.
436    pub bits: Vec<BitField>,
437    /// The fields of each item of a group; its count is `count`.
438    pub group: Vec<Field>,
439    /// An offset into this section of the file, where NUL-terminated text is.
440    pub string_at: Option<String>,
441    pub lookup: Option<Lookup>,
442    /// What the column means: documentation, never read.
443    pub description: Option<String>,
444    /// The column's unit: documentation, never read.
445    pub unit: Option<String>,
446}
447
448/// A spec's header: its fields, and its size when that is more than they take.
449#[derive(Debug, Clone, Default, PartialEq)]
450pub struct Header {
451    pub fields: Vec<Field>,
452    pub size: Option<Amount>,
453}
454
455/// A spec's records.
456#[derive(Debug, Clone, Default, PartialEq)]
457pub struct Records {
458    pub framing: Framing,
459    /// Every record's fields; with variants, the common ones before the variant's.
460    pub fields: Vec<Field>,
461    /// At least the fields' sizes; the rest of each record is skipped. Read from a
462    /// field of the record for `length_prefixed`.
463    pub size: Option<Amount>,
464    /// How many records there are, when the file says.
465    pub count: Option<Amount>,
466    /// The size is written again after the record, as Fortran writes it.
467    pub length_suffix: bool,
468    /// Each record starts at a multiple of this, counted from the first.
469    pub align: u64,
470    /// The marker each record starts with, for `sync`.
471    pub sync: Vec<u8>,
472    /// The common field whose value picks the variant.
473    pub type_field: Option<String>,
474    pub variants: Vec<Variant>,
475    pub checksum: Option<Checksum>,
476    /// A ring buffer: the oldest record's index, from the header.
477    pub ring: Option<Amount>,
478}
479
480/// One layout a record can take, picked by the type field.
481#[derive(Debug, Clone, PartialEq)]
482pub struct Variant {
483    pub name: String,
484    /// The type values that pick it.
485    pub when: Vec<Expected>,
486    /// The fields after the common ones.
487    pub fields: Vec<Field>,
488    /// The whole record's size, when more than its fields take.
489    pub size: Option<Amount>,
490    /// What a record of this type is: documentation, never read.
491    pub description: Option<String>,
492}
493
494/// Fields at the end of the file.
495#[derive(Debug, Clone, Default, PartialEq)]
496pub struct Footer {
497    pub fields: Vec<Field>,
498    pub size: Option<u64>,
499    /// A checksum of the bytes before the footer.
500    pub checksum: Option<(ChecksumAlgo, String)>,
501}
502
503/// How a block's body is compressed.
504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
505pub enum Compression {
506    None,
507    Gzip,
508    Deflate,
509    Zlib,
510    Zstd,
511    /// The LZ4 frame format.
512    Lz4,
513    /// A raw LZ4 block; its decompressed size comes from `uncompressed`.
514    Lz4Block,
515    /// Raw Snappy.
516    Snappy,
517    /// The Snappy frame format.
518    SnappyFramed,
519    Brotli,
520    Bzip2,
521    Xz,
522}
523
524impl Compression {
525    const NAMES: &[(&str, Self)] = &[
526        ("none", Self::None),
527        ("gzip", Self::Gzip),
528        ("deflate", Self::Deflate),
529        ("zlib", Self::Zlib),
530        ("zstd", Self::Zstd),
531        ("lz4", Self::Lz4),
532        ("lz4_block", Self::Lz4Block),
533        ("snappy", Self::Snappy),
534        ("snappy_framed", Self::SnappyFramed),
535        ("brotli", Self::Brotli),
536        ("bzip2", Self::Bzip2),
537        ("xz", Self::Xz),
538    ];
539
540    fn parse(name: &str) -> Option<Self> {
541        Self::NAMES
542            .iter()
543            .find(|(n, _)| *n == name)
544            .map(|(_, c)| *c)
545    }
546
547    pub fn name(self) -> &'static str {
548        Self::NAMES
549            .iter()
550            .find(|(_, c)| *c == self)
551            .map_or("none", |(n, _)| n)
552    }
553}
554
555/// The codec of each block: one for all, or one a header field names.
556#[derive(Debug, Clone, PartialEq, Eq)]
557pub enum Codec {
558    Fixed(Compression),
559    ByField {
560        field: String,
561        values: BTreeMap<i64, Compression>,
562    },
563}
564
565/// Blocks listed in the file, so their headers are not walked.
566#[derive(Debug, Clone, PartialEq)]
567pub struct BlockIndex {
568    /// Where the index starts.
569    pub at: Amount,
570    /// How many entries it has.
571    pub count: Amount,
572    /// One entry's fields: `offset` is required; `rows` is the records in the block.
573    pub fields: Vec<Field>,
574}
575
576/// Records in blocks, each with a header and, often, compressed on its own.
577#[derive(Debug, Clone, PartialEq)]
578pub struct Blocks {
579    pub header: Vec<Field>,
580    /// The body's bytes after the header.
581    pub size: Amount,
582    pub codec: Codec,
583    /// The header field counting the block's records.
584    pub records: Option<String>,
585    /// The header field giving the body's decompressed size.
586    pub uncompressed: Option<String>,
587    pub index: Option<BlockIndex>,
588}
589
590/// A packet capture whose payloads hold the records.
591#[derive(Debug, Clone, PartialEq)]
592pub struct Capture {
593    /// Fields at the front of each payload, such as a MoldUDP64 header.
594    pub header: Vec<Field>,
595    /// The payload header field counting its records.
596    pub count: Option<String>,
597    /// The name of a column of each packet's capture time.
598    pub time: Option<String>,
599}
600
601/// A part of a file's path that becomes a column.
602#[derive(Debug, Clone, PartialEq, Eq)]
603pub struct PathPart {
604    pub name: String,
605    /// A chrono format for a date, such as `%Y%m%d`; text when `None`.
606    pub date: Option<String>,
607}
608
609/// A directory of one spec's files: each file's path is read by `pattern`.
610#[derive(Debug, Clone, PartialEq, Eq)]
611pub struct Files {
612    pub pattern: String,
613    pub parts: Vec<PathPart>,
614}
615
616/// A named run of the file that fields point into.
617#[derive(Debug, Clone, PartialEq, Eq)]
618pub struct Section {
619    pub name: String,
620    pub offset: Amount,
621    pub size: Amount,
622}
623
624/// A header value a file must hold to match: `match.where`.
625#[derive(Debug, Clone, PartialEq, Eq)]
626pub enum Expected {
627    Int(i128),
628    Text(String),
629}
630
631/// One condition of a spec's `match`, as home, Info and `datui formats` show it (`magic
632/// MKTD`, `version 1`, `glob *.bin *.dat`). Chips must all hold, except that a file the
633/// glob names is not asked its magic; alternatives live inside one chip.
634#[derive(Debug, Clone, PartialEq, Eq)]
635pub struct MatchChip {
636    pub name: String,
637    pub value: String,
638    pub kind: ChipKind,
639    /// Where the magic sits, when not at the start.
640    pub offset: Option<u64>,
641}
642
643/// What a chip's value is, for its color and whether plain text quotes it.
644#[derive(Debug, Clone, Copy, PartialEq, Eq)]
645pub enum ChipKind {
646    /// Printable magic bytes, as text. Never quoted: a magic is bytes either way.
647    Magic,
648    /// Magic bytes in hex.
649    Hex,
650    /// A header value that is text. Quoted where no color tells it from a number.
651    Text,
652    /// A header value that is a number.
653    Int,
654    /// File name patterns, any of which names the file.
655    Glob,
656}
657
658/// The most of a magic a chip shows before it cuts it.
659const CHIP_MAGIC_CHARS: usize = 16;
660const CHIP_MAGIC_BYTES: usize = 8;
661
662impl MatchChip {
663    /// The value as plain text: text header values quoted when `quote`.
664    pub fn value_text(&self, quote: bool) -> String {
665        let value = match self.kind {
666            ChipKind::Text if quote => format!("\"{}\"", self.value),
667            _ => self.value.clone(),
668        };
669        match self.offset {
670            Some(at) => format!("{value} @ {at}"),
671            None => value,
672        }
673    }
674
675    /// `name value`, as plain text.
676    pub fn plain(&self, quote: bool) -> String {
677        format!("{} {}", self.name, self.value_text(quote))
678    }
679}
680
681/// Chips as one line of plain text, for the command line and the Info panel:
682/// `magic MKTD · version 1 · glob *.bin *.dat`.
683pub fn chips_plain(chips: &[MatchChip]) -> String {
684    let sep = format!(" {} ", crate::glyphs::get().middot);
685    chips
686        .iter()
687        .map(|c| c.plain(true))
688        .collect::<Vec<_>>()
689        .join(&sep)
690}
691
692/// The most a spec file may hold, local or remote: far more than any spec needs, and
693/// a bound on what `--format FILE` reads before it knows what it read.
694pub const MAX_SPEC_BYTES: u64 = 1 << 20;
695/// [`MAX_SPEC_BYTES`], as the user is told it.
696pub const MAX_SPEC_SAID: &str = "1 MiB";
697
698/// One format, as its spec describes it.
699#[derive(Debug, Clone)]
700pub struct Spec {
701    pub name: String,
702    pub description: Option<String>,
703    /// An `https://` link to the format's own documentation.
704    pub documentation: Option<String>,
705    /// A delimited spec's `[columns]` notes: what each column of the file means.
706    pub notes: Vec<(String, ColumnNote)>,
707    /// The file it was read from.
708    pub path: Option<PathBuf>,
709    pub globs: Vec<String>,
710    glob_set: Option<GlobSet>,
711    pub magic: Vec<u8>,
712    pub magic_offset: u64,
713    /// Header fields and the values a file must hold in them to match.
714    pub expect: Vec<(String, Expected)>,
715    pub endian: Endian,
716    /// The byte order is the one the magic reads in: as written, little-endian, and
717    /// reversed, big-endian.
718    pub endian_auto: bool,
719    pub layout: Layout,
720    pub header: Header,
721    pub records: Records,
722    /// For `kind = "delimited"`: the reading options of a CSV-like file. A delimited
723    /// spec has no header or record fields.
724    pub delimited: Option<Arc<crate::formats::delimited_spec::Delimited>>,
725    pub footer: Option<Footer>,
726    pub blocks: Option<Blocks>,
727    pub capture: Option<Capture>,
728    pub files: Option<Files>,
729    pub sections: Vec<Section>,
730    /// The variant read alone (`--table`), when one is.
731    pub variant: Option<String>,
732}
733
734impl PartialEq for Spec {
735    fn eq(&self, other: &Self) -> bool {
736        self.name == other.name
737            && self.description == other.description
738            && self.documentation == other.documentation
739            && self.notes == other.notes
740            && self.path == other.path
741            && self.globs == other.globs
742            && self.magic == other.magic
743            && self.magic_offset == other.magic_offset
744            && self.expect == other.expect
745            && self.endian == other.endian
746            && self.layout == other.layout
747            && self.endian_auto == other.endian_auto
748            && self.header == other.header
749            && self.records == other.records
750            && self.delimited == other.delimited
751            && self.footer == other.footer
752            && self.blocks == other.blocks
753            && self.capture == other.capture
754            && self.files == other.files
755            && self.sections == other.sections
756    }
757}
758
759mod command;
760pub mod files;
761mod layout;
762mod matching;
763mod open;
764mod parse;
765mod registry;
766mod types;
767
768pub use command::*;
769pub use layout::*;
770pub use matching::*;
771pub(crate) use parse::*;
772pub use registry::*;
773pub use types::*;
774
775#[cfg(test)]
776mod tests;
777
778#[cfg(test)]
779mod docs_tests;
780
781#[cfg(test)]
782mod chip_tests;