Skip to main content

datui_lib/
formats.rs

1//! Binary formats described by specs: one TOML file per format, found on a search path.
2//!
3//! A spec names its format (`acme.l2feed`), says which files are in it (`match`: a
4//! glob, magic bytes, header values), and lays out its header and its records. Specs
5//! are data: no expressions and no code. A field refers to an earlier one by name
6//! instead, and every size read from a file is bounded.
7//!
8//! What a spec describes is decoded by [`crate::fixed_records`]; this module turns a
9//! spec and a file into that reader's columns.
10
11use crate::catalog::ColumnNote;
12use crate::fixed_records::{Bytes, ColumnLayout, FixedRecords, Logical, Null, Physical};
13use globset::{Glob, GlobSet, GlobSetBuilder};
14use polars::prelude::{
15    AnyValue, DataFrame, LazyFrame, PlSmallStr, PolarsResult, SchemaRef, TimeUnit,
16};
17use std::collections::BTreeMap;
18use std::ops::Range;
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21use toml::de::{DeTable, DeValue};
22
23/// The largest size a spec or a file may give one field, header or record, and the
24/// most values one field may hold. A size read from a file past this is refused
25/// rather than believed.
26pub const MAX_SIZE: u64 = 64 << 20;
27
28/// The most columns one field may be flattened into.
29pub const MAX_FLATTEN: u64 = 1024;
30
31/// The most bytes at the front of a file read to match it: its magic and the header
32/// values `match.where` compares.
33const MAX_MATCH_READ: u64 = 64 << 10;
34
35/// Nanoseconds in a day.
36const DAY_NS: i64 = 86_400_000_000_000;
37
38/// The variable the search path is extended with, after the config directory.
39pub const PATH_VAR: &str = "DATUI_FORMATS_PATH";
40
41/// A problem with a spec: where it is (file, line, column) and what was expected.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct SpecError {
44    pub path: Option<PathBuf>,
45    /// One-based; 0 when the problem is not at one place in the text.
46    pub line: usize,
47    pub column: usize,
48    pub message: String,
49}
50
51/// Said as every reader error is, with the place in the text a compiler gives:
52/// `"spec.toml":3:7: <what went wrong>.`
53impl std::fmt::Display for SpecError {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        let at = (self.line > 0).then_some((self.line, self.column));
56        f.write_str(&crate::error_display::located_message(
57            self.path.as_deref(),
58            at,
59            &self.message,
60        ))
61    }
62}
63
64impl std::error::Error for SpecError {}
65
66/// Byte order.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum Endian {
69    Little,
70    Big,
71}
72
73/// How the records sit in what is opened.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum Layout {
76    /// One file, a record after another.
77    Rows,
78    /// A directory holding one file of fixed-width values per field, as kdb+ splays a
79    /// table; or, when every field gives its `offset`, one file holding each column's
80    /// values in a run of their own.
81    Columns,
82}
83
84/// How one record is told from the next.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum Framing {
87    /// Each record takes the same bytes: `size`, or what its fields take.
88    #[default]
89    Fixed,
90    /// A field of the record gives its size (`size = "len"`).
91    LengthPrefixed,
92    /// The variant the type field picks gives the record's size.
93    Variant,
94    /// Each record starts with the `sync` marker; bytes between records are skipped.
95    Sync,
96}
97
98/// A size or a count: written in the spec, or read from a field.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum Amount {
101    Given(u64),
102    /// The value of header field `field`, plus `adjust`.
103    Header {
104        field: String,
105        adjust: i64,
106    },
107    /// The value of footer field `field`, plus `adjust`.
108    Footer {
109        field: String,
110        adjust: i64,
111    },
112    /// The value of an earlier field of the same record (or block header, or group
113    /// item), plus `adjust`.
114    Record {
115        field: String,
116        adjust: i64,
117    },
118    /// What is left of the record: `size = "rest"`.
119    Rest,
120}
121
122impl Amount {
123    /// Whether the amount is known before a record is read.
124    pub fn is_fixed(&self) -> bool {
125        !matches!(self, Self::Record { .. } | Self::Rest)
126    }
127}
128
129/// What a field's bytes hold.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum Type {
132    /// 1 to 8 bytes.
133    Unsigned(u8),
134    Signed(u8),
135    /// 2 (half), 4 or 8 bytes.
136    Float(u8),
137    /// bfloat16.
138    BFloat16,
139    Bool,
140    Str,
141    /// Text up to its NUL, within `size` when one is given.
142    Strz,
143    Bytes,
144    Pad,
145    /// LEB128: unsigned, and zigzag signed.
146    VarU,
147    VarS,
148    /// A counted run of items, each of the group's fields.
149    Group,
150}
151
152impl Type {
153    /// Bytes one value of the type takes, when the type says.
154    pub fn width(self) -> Option<u64> {
155        match self {
156            Self::Strz | Self::VarU | Self::VarS | Self::Group => None,
157            _ => self.physical().width().map(|w| w as u64),
158        }
159    }
160
161    fn is_integer(self) -> bool {
162        matches!(
163            self,
164            Self::Unsigned(_) | Self::Signed(_) | Self::VarU | Self::VarS
165        )
166    }
167
168    fn is_number(self) -> bool {
169        self.is_integer() || matches!(self, Self::Float(_) | Self::BFloat16)
170    }
171
172    pub fn is_text(self) -> bool {
173        matches!(self, Self::Str | Self::Strz)
174    }
175
176    fn physical(self) -> Physical {
177        match self {
178            Self::Unsigned(n) => Physical::Unsigned(n),
179            Self::Signed(n) => Physical::Signed(n),
180            Self::Float(n) => Physical::Float(n),
181            Self::BFloat16 => Physical::BFloat16,
182            Self::Bool => Physical::Bool,
183            Self::Str | Self::Strz => Physical::Text,
184            // A varint is decoded to eight bytes before its meaning is applied.
185            Self::VarU => Physical::Unsigned(8),
186            Self::VarS => Physical::Signed(8),
187            Self::Bytes | Self::Pad | Self::Group => Physical::Raw,
188        }
189    }
190}
191
192/// How text is stored.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
194pub enum Encoding {
195    #[default]
196    Utf8,
197    Latin1,
198    Utf16Le,
199    Utf16Be,
200}
201
202impl Encoding {
203    pub fn physical(self) -> Physical {
204        match self {
205            Self::Utf8 => Physical::Text,
206            Self::Latin1 => Physical::Latin1,
207            Self::Utf16Le => Physical::Utf16 { big_endian: false },
208            Self::Utf16Be => Physical::Utf16 { big_endian: true },
209        }
210    }
211
212    /// Bytes in one code unit: where a NUL is looked for.
213    pub fn unit(self) -> usize {
214        match self {
215            Self::Utf16Le | Self::Utf16Be => 2,
216            _ => 1,
217        }
218    }
219}
220
221/// A value stored as the change from the previous record's.
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
223pub enum Delta {
224    #[default]
225    None,
226    /// Summed from the first record.
227    All,
228    /// Summed from the first record of each block.
229    Block,
230}
231
232/// One bit field of an integer: its own column.
233#[derive(Debug, Clone, PartialEq)]
234pub struct BitField {
235    pub name: String,
236    /// The lowest bit, 0 the least significant.
237    pub bit: u32,
238    pub width: u32,
239    pub labels: Option<Arc<BTreeMap<i64, String>>>,
240}
241
242/// Where a symbol list is, for a field of indexes into it.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct Lookup {
245    /// Relative to the directory the data is in.
246    pub file: String,
247    pub format: LookupFormat,
248}
249
250/// How a symbol list's entries are told apart.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum LookupFormat {
253    /// One a line.
254    Lines,
255    /// Each ended by a NUL, as kdb+ writes its `sym` file.
256    Nul,
257    /// Each `n` bytes, padded.
258    Fixed(u64),
259}
260
261/// A checksum over part of each record, or of the file before its footer.
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct Checksum {
264    pub algo: ChecksumAlgo,
265    /// The field holding the stored checksum.
266    pub field: String,
267    /// The field the checksummed bytes start at; the record's start when `None`.
268    pub from: Option<String>,
269    /// The field they end before; the checksum field when `None`.
270    pub to: Option<String>,
271}
272
273/// The checksums a spec can name.
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum ChecksumAlgo {
276    Crc16Ccitt,
277    Crc16Xmodem,
278    Crc16Modbus,
279    Crc16Arc,
280    Crc32,
281    Crc32c,
282    Sum8,
283    Xor8,
284}
285
286impl ChecksumAlgo {
287    const NAMES: &[(&str, Self)] = &[
288        ("crc16-ccitt", Self::Crc16Ccitt),
289        ("crc16-xmodem", Self::Crc16Xmodem),
290        ("crc16-modbus", Self::Crc16Modbus),
291        ("crc16-arc", Self::Crc16Arc),
292        ("crc32", Self::Crc32),
293        ("crc32c", Self::Crc32c),
294        ("sum8", Self::Sum8),
295        ("xor8", Self::Xor8),
296    ];
297
298    fn parse(name: &str) -> Option<Self> {
299        Self::NAMES
300            .iter()
301            .find(|(n, _)| *n == name)
302            .map(|(_, a)| *a)
303    }
304
305    /// The checksum of `bytes`.
306    pub fn compute(self, bytes: &[u8]) -> u64 {
307        use crc::{
308            CRC_16_ARC, CRC_16_IBM_3740, CRC_16_MODBUS, CRC_16_XMODEM, CRC_32_ISCSI,
309            CRC_32_ISO_HDLC, Crc,
310        };
311        match self {
312            Self::Crc16Ccitt => u64::from(Crc::<u16>::new(&CRC_16_IBM_3740).checksum(bytes)),
313            Self::Crc16Xmodem => u64::from(Crc::<u16>::new(&CRC_16_XMODEM).checksum(bytes)),
314            Self::Crc16Modbus => u64::from(Crc::<u16>::new(&CRC_16_MODBUS).checksum(bytes)),
315            Self::Crc16Arc => u64::from(Crc::<u16>::new(&CRC_16_ARC).checksum(bytes)),
316            Self::Crc32 => u64::from(Crc::<u32>::new(&CRC_32_ISO_HDLC).checksum(bytes)),
317            Self::Crc32c => u64::from(Crc::<u32>::new(&CRC_32_ISCSI).checksum(bytes)),
318            Self::Sum8 => u64::from(bytes.iter().fold(0u8, |a, b| a.wrapping_add(*b))),
319            Self::Xor8 => u64::from(bytes.iter().fold(0u8, |a, b| a ^ b)),
320        }
321    }
322}
323
324/// The unit a `time` field counts in.
325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326pub enum TimeUnitSpec {
327    Days,
328    Seconds,
329    Millis,
330    Micros,
331    Nanos,
332}
333
334impl TimeUnitSpec {
335    fn nanos(self) -> i64 {
336        match self {
337            Self::Days => DAY_NS,
338            Self::Seconds => 1_000_000_000,
339            Self::Millis => 1_000_000,
340            Self::Micros => 1_000,
341            Self::Nanos => 1,
342        }
343    }
344}
345
346/// What a field means beyond its stored value.
347#[derive(Debug, Clone, PartialEq)]
348pub enum Meaning {
349    Plain,
350    /// A count of `unit` since `epoch_ns` nanoseconds past the Unix epoch: a datetime,
351    /// or a date for whole days.
352    Time {
353        unit: TimeUnitSpec,
354        epoch_ns: i64,
355    },
356    /// A count of `unit` since midnight: a time of day, or a datetime on the date the
357    /// header field `date` holds.
358    TimeOfDay {
359        unit: TimeUnitSpec,
360        date: Option<String>,
361    },
362    /// An integer written as `YYYYMMDD`.
363    Yyyymmdd,
364    /// `scale` implied decimal places.
365    Scale(u32),
366    /// `value * factor + offset`, as a float.
367    Linear {
368        factor: f64,
369        offset: f64,
370    },
371    /// Codes and their labels.
372    Enum(Arc<BTreeMap<i64, String>>),
373}
374
375/// One field of a header or a record.
376#[derive(Debug, Clone, PartialEq)]
377pub struct Field {
378    /// `None` for `pad`.
379    pub name: Option<String>,
380    pub ty: Type,
381    /// For `str`, `bytes` and `pad`.
382    pub size: Option<Amount>,
383    /// The type's own `le` or `be`, over the spec's.
384    pub endian: Option<Endian>,
385    pub meaning: Meaning,
386    /// A stored value that means no value.
387    pub null: Option<Null>,
388    /// Values side by side: an Array column, or `flatten`ed into `name_0`, `name_1`, ...
389    pub count: Option<Amount>,
390    pub flatten: bool,
391    /// In the columns layout, the file in the directory that holds it; its name by
392    /// default.
393    pub file: Option<String>,
394    /// In the columns layout of one file, where the column's values start.
395    pub at: Option<Amount>,
396    /// For text.
397    pub encoding: Encoding,
398    pub delta: Delta,
399    /// Columns of their own from the integer's bits.
400    pub bits: Vec<BitField>,
401    /// The fields of each item of a group; its count is `count`.
402    pub group: Vec<Field>,
403    /// An offset into this section of the file, where NUL-terminated text is.
404    pub string_at: Option<String>,
405    pub lookup: Option<Lookup>,
406    /// What the column means: documentation, never read.
407    pub description: Option<String>,
408    /// The column's unit: documentation, never read.
409    pub unit: Option<String>,
410}
411
412/// A spec's header: its fields, and its size when that is more than they take.
413#[derive(Debug, Clone, Default, PartialEq)]
414pub struct Header {
415    pub fields: Vec<Field>,
416    pub size: Option<Amount>,
417}
418
419/// A spec's records.
420#[derive(Debug, Clone, Default, PartialEq)]
421pub struct Records {
422    pub framing: Framing,
423    /// Every record's fields; with variants, the common ones before the variant's.
424    pub fields: Vec<Field>,
425    /// At least the fields' sizes; the rest of each record is skipped. Read from a
426    /// field of the record for `length_prefixed`.
427    pub size: Option<Amount>,
428    /// How many records there are, when the file says.
429    pub count: Option<Amount>,
430    /// The size is written again after the record, as Fortran writes it.
431    pub length_suffix: bool,
432    /// Each record starts at a multiple of this, counted from the first.
433    pub align: u64,
434    /// The marker each record starts with, for `sync`.
435    pub sync: Vec<u8>,
436    /// The common field whose value picks the variant.
437    pub type_field: Option<String>,
438    pub variants: Vec<Variant>,
439    pub checksum: Option<Checksum>,
440    /// A ring buffer: the oldest record's index, from the header.
441    pub ring: Option<Amount>,
442}
443
444/// One layout a record can take, picked by the type field.
445#[derive(Debug, Clone, PartialEq)]
446pub struct Variant {
447    pub name: String,
448    /// The type values that pick it.
449    pub when: Vec<Expected>,
450    /// The fields after the common ones.
451    pub fields: Vec<Field>,
452    /// The whole record's size, when more than its fields take.
453    pub size: Option<Amount>,
454    /// What a record of this type is: documentation, never read.
455    pub description: Option<String>,
456}
457
458/// Fields at the end of the file.
459#[derive(Debug, Clone, Default, PartialEq)]
460pub struct Footer {
461    pub fields: Vec<Field>,
462    pub size: Option<u64>,
463    /// A checksum of the bytes before the footer.
464    pub checksum: Option<(ChecksumAlgo, String)>,
465}
466
467/// How a block's body is compressed.
468#[derive(Debug, Clone, Copy, PartialEq, Eq)]
469pub enum Compression {
470    None,
471    Gzip,
472    Deflate,
473    Zlib,
474    Zstd,
475    /// The LZ4 frame format.
476    Lz4,
477    /// A raw LZ4 block; its decompressed size comes from `uncompressed`.
478    Lz4Block,
479    /// Raw Snappy.
480    Snappy,
481    /// The Snappy frame format.
482    SnappyFramed,
483    Brotli,
484    Bzip2,
485    Xz,
486}
487
488impl Compression {
489    const NAMES: &[(&str, Self)] = &[
490        ("none", Self::None),
491        ("gzip", Self::Gzip),
492        ("deflate", Self::Deflate),
493        ("zlib", Self::Zlib),
494        ("zstd", Self::Zstd),
495        ("lz4", Self::Lz4),
496        ("lz4_block", Self::Lz4Block),
497        ("snappy", Self::Snappy),
498        ("snappy_framed", Self::SnappyFramed),
499        ("brotli", Self::Brotli),
500        ("bzip2", Self::Bzip2),
501        ("xz", Self::Xz),
502    ];
503
504    fn parse(name: &str) -> Option<Self> {
505        Self::NAMES
506            .iter()
507            .find(|(n, _)| *n == name)
508            .map(|(_, c)| *c)
509    }
510
511    pub fn name(self) -> &'static str {
512        Self::NAMES
513            .iter()
514            .find(|(_, c)| *c == self)
515            .map_or("none", |(n, _)| n)
516    }
517}
518
519/// The codec of each block: one for all, or one a header field names.
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub enum Codec {
522    Fixed(Compression),
523    ByField {
524        field: String,
525        values: BTreeMap<i64, Compression>,
526    },
527}
528
529/// Blocks listed in the file, so their headers are not walked.
530#[derive(Debug, Clone, PartialEq)]
531pub struct BlockIndex {
532    /// Where the index starts.
533    pub at: Amount,
534    /// How many entries it has.
535    pub count: Amount,
536    /// One entry's fields: `offset` is required; `rows` is the records in the block.
537    pub fields: Vec<Field>,
538}
539
540/// Records in blocks, each with a header and, often, compressed on its own.
541#[derive(Debug, Clone, PartialEq)]
542pub struct Blocks {
543    pub header: Vec<Field>,
544    /// The body's bytes after the header.
545    pub size: Amount,
546    pub codec: Codec,
547    /// The header field counting the block's records.
548    pub records: Option<String>,
549    /// The header field giving the body's decompressed size.
550    pub uncompressed: Option<String>,
551    pub index: Option<BlockIndex>,
552}
553
554/// A packet capture whose payloads hold the records.
555#[derive(Debug, Clone, PartialEq)]
556pub struct Capture {
557    /// Fields at the front of each payload, such as a MoldUDP64 header.
558    pub header: Vec<Field>,
559    /// The payload header field counting its records.
560    pub count: Option<String>,
561    /// The name of a column of each packet's capture time.
562    pub time: Option<String>,
563}
564
565/// A part of a file's path that becomes a column.
566#[derive(Debug, Clone, PartialEq, Eq)]
567pub struct PathPart {
568    pub name: String,
569    /// A chrono format for a date, such as `%Y%m%d`; text when `None`.
570    pub date: Option<String>,
571}
572
573/// A directory of one spec's files: each file's path is read by `pattern`.
574#[derive(Debug, Clone, PartialEq, Eq)]
575pub struct Files {
576    pub pattern: String,
577    pub parts: Vec<PathPart>,
578}
579
580/// A named run of the file that fields point into.
581#[derive(Debug, Clone, PartialEq, Eq)]
582pub struct Section {
583    pub name: String,
584    pub offset: Amount,
585    pub size: Amount,
586}
587
588/// A header value a file must hold to match: `match.where`.
589#[derive(Debug, Clone, PartialEq, Eq)]
590pub enum Expected {
591    Int(i128),
592    Text(String),
593}
594
595/// One condition of a spec's `match`, as the home pane, the Info panel and `datui
596/// formats` show it: `magic MKTD`, `version 1`, `glob *.bin *.dat`. Separate chips must
597/// all hold, but for a glob and a magic: a file the glob names is not asked for its
598/// magic. Alternatives live inside one chip.
599#[derive(Debug, Clone, PartialEq, Eq)]
600pub struct MatchChip {
601    pub name: String,
602    pub value: String,
603    pub kind: ChipKind,
604    /// Where the magic sits, when not at the start.
605    pub offset: Option<u64>,
606}
607
608/// What a chip's value is, for its color and whether plain text quotes it.
609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
610pub enum ChipKind {
611    /// Printable magic bytes, as text. Never quoted: a magic is bytes either way.
612    Magic,
613    /// Magic bytes in hex.
614    Hex,
615    /// A header value that is text. Quoted where no color tells it from a number.
616    Text,
617    /// A header value that is a number.
618    Int,
619    /// File name patterns, any of which names the file.
620    Glob,
621}
622
623/// The most of a magic a chip shows before it cuts it.
624const CHIP_MAGIC_CHARS: usize = 16;
625const CHIP_MAGIC_BYTES: usize = 8;
626
627impl MatchChip {
628    /// The value as plain text: text header values quoted when `quote`.
629    pub fn value_text(&self, quote: bool) -> String {
630        let value = match self.kind {
631            ChipKind::Text if quote => format!("\"{}\"", self.value),
632            _ => self.value.clone(),
633        };
634        match self.offset {
635            Some(at) => format!("{value} @ {at}"),
636            None => value,
637        }
638    }
639
640    /// `name value`, as plain text.
641    pub fn plain(&self, quote: bool) -> String {
642        format!("{} {}", self.name, self.value_text(quote))
643    }
644}
645
646/// Chips as one line of plain text, for the command line and the Info panel:
647/// `magic MKTD · version 1 · glob *.bin *.dat`.
648pub fn chips_plain(chips: &[MatchChip]) -> String {
649    let sep = format!(" {} ", crate::glyphs::get().middot);
650    chips
651        .iter()
652        .map(|c| c.plain(true))
653        .collect::<Vec<_>>()
654        .join(&sep)
655}
656
657/// The most a spec file may hold, local or remote: far more than any spec needs, and
658/// a bound on what `--format FILE` reads before it knows what it read.
659pub const MAX_SPEC_BYTES: u64 = 1 << 20;
660/// [`MAX_SPEC_BYTES`], as the user is told it.
661pub const MAX_SPEC_SAID: &str = "1 MiB";
662
663/// One format, as its spec describes it.
664#[derive(Debug, Clone)]
665pub struct Spec {
666    pub name: String,
667    pub description: Option<String>,
668    /// An `https://` link to the format's own documentation.
669    pub documentation: Option<String>,
670    /// A delimited spec's `[columns]` notes: what each column of the file means.
671    pub notes: Vec<(String, ColumnNote)>,
672    /// The file it was read from.
673    pub path: Option<PathBuf>,
674    pub globs: Vec<String>,
675    glob_set: Option<GlobSet>,
676    pub magic: Vec<u8>,
677    pub magic_offset: u64,
678    /// Header fields and the values a file must hold in them to match.
679    pub expect: Vec<(String, Expected)>,
680    pub endian: Endian,
681    /// The byte order is the one the magic reads in: as written, little-endian, and
682    /// reversed, big-endian.
683    pub endian_auto: bool,
684    pub layout: Layout,
685    pub header: Header,
686    pub records: Records,
687    /// For `kind = "delimited"`: the reading options of a CSV-like file. A delimited
688    /// spec has no header or record fields.
689    pub delimited: Option<Arc<crate::delimited_spec::Delimited>>,
690    pub footer: Option<Footer>,
691    pub blocks: Option<Blocks>,
692    pub capture: Option<Capture>,
693    pub files: Option<Files>,
694    pub sections: Vec<Section>,
695    /// The variant read alone (`--table`), when one is.
696    pub variant: Option<String>,
697}
698
699impl PartialEq for Spec {
700    fn eq(&self, other: &Self) -> bool {
701        self.name == other.name
702            && self.description == other.description
703            && self.documentation == other.documentation
704            && self.notes == other.notes
705            && self.path == other.path
706            && self.globs == other.globs
707            && self.magic == other.magic
708            && self.magic_offset == other.magic_offset
709            && self.expect == other.expect
710            && self.endian == other.endian
711            && self.layout == other.layout
712            && self.endian_auto == other.endian_auto
713            && self.header == other.header
714            && self.records == other.records
715            && self.delimited == other.delimited
716            && self.footer == other.footer
717            && self.blocks == other.blocks
718            && self.capture == other.capture
719            && self.files == other.files
720            && self.sections == other.sections
721    }
722}
723
724/// The byte-order mark a text file may start with.
725const UTF8_BOM: &[u8] = b"\xEF\xBB\xBF";
726
727/// What a spec's `match` says files of it look like.
728#[derive(Default)]
729struct MatchRules {
730    globs: Vec<String>,
731    glob_set: Option<GlobSet>,
732    magic: Vec<u8>,
733    magic_offset: u64,
734    expect: Vec<(String, Expected)>,
735}
736
737/// Line and column (one-based) of byte `offset` in `text`.
738fn line_column(text: &str, offset: usize) -> (usize, usize) {
739    let mut offset = offset.min(text.len());
740    while !text.is_char_boundary(offset) {
741        offset -= 1;
742    }
743    let before = &text[..offset];
744    let line = before.matches('\n').count() + 1;
745    let column = before
746        .rsplit('\n')
747        .next()
748        .map_or(0, |last| last.chars().count())
749        + 1;
750    (line, column)
751}
752
753/// Which part of a file a field belongs to.
754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
755enum Part {
756    Header,
757    Footer,
758    /// A record, a block header, a payload header or a group item: read one at a
759    /// time, so a field may size a later one.
760    Records,
761}
762
763/// The keys a field takes.
764const FIELD_KEYS: &[&str] = &[
765    "name",
766    "type",
767    "size",
768    "size_adjust",
769    "count",
770    "flatten",
771    "null",
772    "time",
773    "epoch",
774    "of_day",
775    "date",
776    "scale",
777    "factor",
778    "offset",
779    "enum",
780    "file",
781    "encoding",
782    "delta",
783    "bits",
784    "group",
785    "string_at",
786    "lookup",
787    "description",
788    "unit",
789];
790
791/// The parts a field can refer to by name: `header.NAME`, `footer.NAME`.
792#[derive(Clone, Copy, Default)]
793struct Scopes<'f> {
794    header: &'f [Field],
795    footer: &'f [Field],
796}
797
798/// Reads a spec's TOML, keeping where each value was so a problem can say.
799struct Reader<'a> {
800    text: &'a str,
801    path: Option<&'a Path>,
802}
803
804type Value<'i> = toml::Spanned<DeValue<'i>>;
805
806impl Reader<'_> {
807    fn error(&self, span: &Range<usize>, message: impl Into<String>) -> SpecError {
808        let (line, column) = line_column(self.text, span.start);
809        SpecError {
810            path: self.path.map(Path::to_path_buf),
811            line,
812            column,
813            message: message.into(),
814        }
815    }
816
817    /// The table's entries, after checking every key is one of `known`.
818    fn entries<'t, 'i>(
819        &self,
820        table: &'t DeTable<'i>,
821        what: &str,
822        known: &[&str],
823    ) -> Result<BTreeMap<&'t str, &'t Value<'i>>, SpecError> {
824        let mut out = BTreeMap::new();
825        for (key, value) in table {
826            let name: &str = key.get_ref();
827            if !known.contains(&name) {
828                return Err(self.error(
829                    &key.span(),
830                    format!(
831                        "unknown key `{name}` in {what}; expected one of {}",
832                        known.join(", ")
833                    ),
834                ));
835            }
836            out.insert(name, value);
837        }
838        Ok(out)
839    }
840
841    fn string(&self, value: &Value<'_>, what: &str) -> Result<String, SpecError> {
842        match value.get_ref() {
843            DeValue::String(s) => Ok(s.to_string()),
844            _ => Err(self.error(&value.span(), format!("{what}: expected a string"))),
845        }
846    }
847
848    /// Documentation text: trimmed, and never empty.
849    fn prose(&self, value: &Value<'_>, what: &str) -> Result<String, SpecError> {
850        let text = self.string(value, what)?.trim().to_string();
851        if text.is_empty() {
852            return Err(self.error(&value.span(), format!("{what}: must not be empty")));
853        }
854        Ok(text)
855    }
856
857    /// A link to documentation: an `https://` URL, as a catalog's `documentation` is.
858    fn documentation(&self, value: &Value<'_>) -> Result<String, SpecError> {
859        let link = self.prose(value, "documentation")?;
860        if !link.starts_with("https://") || link.chars().any(char::is_whitespace) {
861            return Err(self.error(
862                &value.span(),
863                format!("documentation: \"{link}\" is not an https:// link"),
864            ));
865        }
866        Ok(link)
867    }
868
869    fn integer(&self, value: &Value<'_>, what: &str) -> Result<i64, SpecError> {
870        match value.get_ref() {
871            DeValue::Integer(i) => {
872                let digits = i.as_str().replace('_', "");
873                i64::from_str_radix(&digits, i.radix())
874                    .map_err(|_| self.error(&value.span(), format!("{what}: too large")))
875            }
876            _ => Err(self.error(&value.span(), format!("{what}: expected an integer"))),
877        }
878    }
879
880    fn number(&self, value: &Value<'_>, what: &str) -> Result<f64, SpecError> {
881        match value.get_ref() {
882            DeValue::Integer(_) => Ok(self.integer(value, what)? as f64),
883            DeValue::Float(f) => f
884                .as_str()
885                .replace('_', "")
886                .parse::<f64>()
887                .ok()
888                .filter(|v| v.is_finite())
889                .ok_or_else(|| {
890                    self.error(&value.span(), format!("{what}: expected a finite number"))
891                }),
892            _ => Err(self.error(&value.span(), format!("{what}: expected a number"))),
893        }
894    }
895
896    fn boolean(&self, value: &Value<'_>, what: &str) -> Result<bool, SpecError> {
897        match value.get_ref() {
898            DeValue::Boolean(b) => Ok(*b),
899            _ => Err(self.error(&value.span(), format!("{what}: expected true or false"))),
900        }
901    }
902
903    fn table<'t, 'i>(
904        &self,
905        value: &'t Value<'i>,
906        what: &str,
907    ) -> Result<&'t DeTable<'i>, SpecError> {
908        match value.get_ref() {
909            DeValue::Table(t) => Ok(t),
910            _ => Err(self.error(&value.span(), format!("{what}: expected a table"))),
911        }
912    }
913
914    /// The earlier field `reference` names: `header.NAME`, `footer.NAME`, or `NAME`
915    /// for an earlier field of the same part.
916    fn earlier<'f>(
917        &self,
918        value: &Value<'_>,
919        reference: &str,
920        what: &str,
921        earlier: &'f [Field],
922        scopes: &Scopes<'f>,
923    ) -> Result<(&'f Field, Option<&'static str>), SpecError> {
924        let (scope, field) = match reference.split_once('.') {
925            Some((scope, field)) => (Some(scope), field),
926            None => (None, reference),
927        };
928        let (fields, scope) = match scope {
929            Some("header") => (scopes.header, Some("header")),
930            Some("footer") => (scopes.footer, Some("footer")),
931            None => (earlier, None),
932            Some(other) => {
933                return Err(self.error(
934                    &value.span(),
935                    format!(
936                        "{what}: `{other}.` is not a part; expected `header.NAME` or `footer.NAME`"
937                    ),
938                ));
939            }
940        };
941        fields
942            .iter()
943            .find(|f| f.name.as_deref() == Some(field))
944            .map(|f| (f, scope))
945            .ok_or_else(|| {
946                let part = scope.unwrap_or("earlier");
947                let hint = if scope.is_none()
948                    && scopes
949                        .header
950                        .iter()
951                        .any(|f| f.name.as_deref() == Some(field))
952                {
953                    format!("; a header field is named `header.{field}`")
954                } else {
955                    String::new()
956                };
957                self.error(
958                    &value.span(),
959                    format!("{what}: no {part} field named `{field}`{hint}"),
960                )
961            })
962    }
963
964    /// A size or a count: a whole number, `rest`, or the name of an earlier plain
965    /// integer field, with an optional `adjust`.
966    fn amount(
967        &self,
968        value: &Value<'_>,
969        adjust: Option<&Value<'_>>,
970        what: &str,
971        part: Part,
972        earlier: &[Field],
973        scopes: &Scopes<'_>,
974    ) -> Result<Amount, SpecError> {
975        let adjust = adjust
976            .map(|a| self.integer(a, &format!("{what}_adjust")))
977            .transpose()?;
978        match value.get_ref() {
979            DeValue::Integer(_) => {
980                let n = self.integer(value, what)?;
981                if adjust.is_some() {
982                    return Err(self.error(
983                        &value.span(),
984                        format!("{what}_adjust goes with a {what} read from a field"),
985                    ));
986                }
987                if n < 0 || n as u64 > MAX_SIZE {
988                    return Err(
989                        self.error(&value.span(), format!("{what}: expected 0 to {MAX_SIZE}"))
990                    );
991                }
992                Ok(Amount::Given(n as u64))
993            }
994            DeValue::String(reference) if reference.as_ref() == "rest" && what == "size" => {
995                if part != Part::Records {
996                    return Err(
997                        self.error(&value.span(), "size = \"rest\" is for a field of a record")
998                    );
999                }
1000                if adjust.is_some() {
1001                    return Err(self.error(
1002                        &value.span(),
1003                        "size_adjust goes with a size read from a field",
1004                    ));
1005                }
1006                Ok(Amount::Rest)
1007            }
1008            DeValue::String(reference) => {
1009                let (target, scope) = self.earlier(value, reference, what, earlier, scopes)?;
1010                if !matches!(
1011                    target.ty,
1012                    Type::Unsigned(_) | Type::Signed(_) | Type::VarU | Type::VarS
1013                ) || target.meaning != Meaning::Plain
1014                    || target.count.is_some()
1015                    || target.delta != Delta::None
1016                {
1017                    return Err(self.error(
1018                        &value.span(),
1019                        format!("{what}: `{reference}` is not a plain integer field"),
1020                    ));
1021                }
1022                let field = target.name.clone().expect("a referenced field is named");
1023                let adjust = adjust.unwrap_or(0);
1024                Ok(match (scope, part) {
1025                    (Some("footer"), _) | (None, Part::Footer) => Amount::Footer { field, adjust },
1026                    (Some(_), _) | (None, Part::Header) => Amount::Header { field, adjust },
1027                    (None, Part::Records) => Amount::Record { field, adjust },
1028                })
1029            }
1030            _ => Err(self.error(
1031                &value.span(),
1032                format!("{what}: expected a whole number or the name of an earlier field"),
1033            )),
1034        }
1035    }
1036
1037    /// A spec's `name`: namespaced, such as `acme.l2feed`.
1038    fn spec_name(&self, top: &BTreeMap<&str, &Value<'_>>) -> Result<String, SpecError> {
1039        match top.get("name") {
1040            Some(v) => {
1041                let name = self.string(v, "name")?;
1042                if !is_spec_name(&name) {
1043                    return Err(self.error(
1044                        &v.span(),
1045                        "name: expected a namespaced name of letters, digits, `_` and `-`, such as acme.l2feed",
1046                    ));
1047                }
1048                Ok(name)
1049            }
1050            None => Err(self.error(&(0..0), "missing `name`, such as name = \"acme.l2feed\"")),
1051        }
1052    }
1053
1054    /// A spec's `match`: its globs, magic and, given a binary header's fields, the
1055    /// header values a file must hold.
1056    fn match_rules(
1057        &self,
1058        v: &Value<'_>,
1059        header: Option<&[Field]>,
1060    ) -> Result<MatchRules, SpecError> {
1061        let (mut globs, mut magic, mut magic_offset) = (Vec::new(), Vec::new(), 0u64);
1062        let mut glob_set = None;
1063        let mut expect = Vec::new();
1064        let table = self.table(v, "match")?;
1065        let keys = self.entries(table, "match", &["glob", "magic", "magic_offset", "where"])?;
1066        if let Some(g) = keys.get("glob") {
1067            globs = match g.get_ref() {
1068                DeValue::String(s) => vec![s.to_string()],
1069                DeValue::Array(items) => items
1070                    .iter()
1071                    .map(|item| self.string(item, "glob"))
1072                    .collect::<Result<_, _>>()?,
1073                _ => {
1074                    return Err(self.error(&g.span(), "glob: expected a string or a list of them"));
1075                }
1076            };
1077            let mut builder = GlobSetBuilder::new();
1078            for glob in &globs {
1079                builder.add(
1080                    Glob::new(glob).map_err(|e| {
1081                        self.error(&g.span(), format!("glob `{glob}`: {}", e.kind()))
1082                    })?,
1083                );
1084            }
1085            glob_set = Some(
1086                builder
1087                    .build()
1088                    .map_err(|e| self.error(&g.span(), format!("glob: {e}")))?,
1089            );
1090        }
1091        if let Some(m) = keys.get("magic") {
1092            magic = match m.get_ref() {
1093                DeValue::String(s) => s.as_bytes().to_vec(),
1094                DeValue::Array(items) => items
1095                    .iter()
1096                    .map(|item| {
1097                        let byte = self.integer(item, "magic")?;
1098                        u8::try_from(byte)
1099                            .map_err(|_| self.error(&item.span(), "magic: a byte is 0 to 255"))
1100                    })
1101                    .collect::<Result<_, _>>()?,
1102                _ => {
1103                    return Err(
1104                        self.error(&m.span(), "magic: expected a string or a list of bytes")
1105                    );
1106                }
1107            };
1108            if magic.is_empty() || magic.len() as u64 > MAX_MATCH_READ {
1109                return Err(self.error(&m.span(), "magic: expected 1 to 65536 bytes"));
1110            }
1111        }
1112        if let Some(o) = keys.get("magic_offset") {
1113            let offset = self.integer(o, "magic_offset")?;
1114            let room = MAX_MATCH_READ - magic.len() as u64;
1115            if offset < 0 || offset as u64 > room {
1116                return Err(self.error(&o.span(), format!("magic_offset: expected 0 to {room}")));
1117            }
1118            magic_offset = offset as u64;
1119        }
1120        if let Some(w) = keys.get("where") {
1121            let Some(header_fields) = header else {
1122                return Err(self.error(
1123                    &w.span(),
1124                    "`where` compares a binary header's fields; a delimited spec matches by glob and magic",
1125                ));
1126            };
1127            let table = self.table(w, "where")?;
1128            for (key, value) in table {
1129                let reference: &str = key.get_ref();
1130                let Some(field) = reference.strip_prefix("header.") else {
1131                    return Err(self.error(
1132                        &key.span(),
1133                        "where: expected header fields, such as \"header.version\" = 3",
1134                    ));
1135                };
1136                let Some(target) = header_fields
1137                    .iter()
1138                    .find(|f| f.name.as_deref() == Some(field))
1139                else {
1140                    return Err(self.error(
1141                        &key.span(),
1142                        format!("where: no header field named `{field}`"),
1143                    ));
1144                };
1145                let wanted = self.expected(value, target, "where")?;
1146                expect.push((field.to_string(), wanted));
1147            }
1148        }
1149        Ok(MatchRules {
1150            globs,
1151            glob_set,
1152            magic,
1153            magic_offset,
1154            expect,
1155        })
1156    }
1157
1158    /// The reading options of a `kind = "delimited"` spec, from its top-level keys.
1159    fn delimited(
1160        &self,
1161        top: &BTreeMap<&str, &Value<'_>>,
1162    ) -> Result<(crate::delimited_spec::Delimited, Vec<(String, ColumnNote)>), SpecError> {
1163        use crate::delimited_spec::{Delimited, Derived, DerivedKind, HeaderRows, MAX_HEAD_LINE};
1164        let line = |value: &Value<'_>, what: &str| -> Result<usize, SpecError> {
1165            let n = self.integer(value, what)?;
1166            if n < 1 || n as usize > MAX_HEAD_LINE {
1167                return Err(self.error(
1168                    &value.span(),
1169                    format!("{what}: expected a line from 1 to {MAX_HEAD_LINE}"),
1170                ));
1171            }
1172            Ok(n as usize)
1173        };
1174        let lines = |value: &Value<'_>, what: &str| -> Result<Vec<usize>, SpecError> {
1175            match value.get_ref() {
1176                DeValue::Integer(_) => Ok(vec![line(value, what)?]),
1177                DeValue::Array(items) if !items.is_empty() => {
1178                    let mut rows = Vec::with_capacity(items.len());
1179                    for item in items {
1180                        let n = line(item, what)?;
1181                        if rows.contains(&n) {
1182                            return Err(self
1183                                .error(&item.span(), format!("{what}: line {n} is named twice")));
1184                        }
1185                        rows.push(n);
1186                    }
1187                    Ok(rows)
1188                }
1189                _ => Err(self.error(
1190                    &value.span(),
1191                    format!("{what}: expected a line number or a list of them"),
1192                )),
1193            }
1194        };
1195        let mut spec = Delimited::default();
1196        // Each note with where its key sits, to keep the file's order: the table reads
1197        // back sorted by name.
1198        let mut notes: Vec<(usize, String, ColumnNote)> = Vec::new();
1199        // The `[csv]` keys and `--delimiter`'s words, read by the same rules.
1200        if let Some(v) = top.get("delimiter") {
1201            let text = self.string(v, "delimiter")?;
1202            let byte = datui_cli::parse_delimiter(&text)
1203                .map_err(|e| self.error(&v.span(), format!("delimiter: {e}")))?;
1204            spec.delimiter = Some(byte);
1205        }
1206        if let Some(v) = top.get("comment") {
1207            let text = self.string(v, "comment")?;
1208            crate::csv_dialect::check_comment_char(&text)
1209                .map_err(|e| self.error(&v.span(), format!("comment: {e}")))?;
1210            spec.comment_char = Some(text);
1211        }
1212        if let Some(v) = top.get("skip_initial_space") {
1213            spec.skip_initial_space = Some(self.boolean(v, "skip_initial_space")?);
1214        }
1215        if let Some(v) = top.get("header_join") {
1216            spec.header_join = Some(self.string(v, "header_join")?);
1217        }
1218        if let Some(v) = top.get("skip_lines") {
1219            let n = self.integer(v, "skip_lines")?;
1220            if n < 0 || n > i64::from(u32::MAX) {
1221                return Err(self.error(&v.span(), "skip_lines: expected 0 or more"));
1222            }
1223            spec.skip_lines = Some(n as usize);
1224        }
1225        if let Some(v) = top.get("null_values") {
1226            spec.null_values = match v.get_ref() {
1227                DeValue::String(s) => vec![s.to_string()],
1228                DeValue::Array(items) => items
1229                    .iter()
1230                    .map(|item| self.string(item, "null_values"))
1231                    .collect::<Result<_, _>>()?,
1232                _ => {
1233                    return Err(self.error(
1234                        &v.span(),
1235                        "null_values: expected a string or a list of them, such as \"NA\" or \"COL=-999\"",
1236                    ));
1237                }
1238            };
1239        }
1240        if let Some(v) = top.get("header_rows") {
1241            let rows = match v.get_ref() {
1242                DeValue::Table(table) => {
1243                    let keys =
1244                        self.entries(table, "header_rows", &["name", "unit", "description"])?;
1245                    if let Some(d) = keys.get("description") {
1246                        return Err(
1247                            self.error(&d.span(), "header_rows.description is not yet supported")
1248                        );
1249                    }
1250                    let Some(name) = keys.get("name") else {
1251                        return Err(self.error(
1252                            &v.span(),
1253                            "header_rows: missing `name`, the line that names the columns",
1254                        ));
1255                    };
1256                    let name = lines(name, "header_rows.name")?;
1257                    let unit = keys
1258                        .get("unit")
1259                        .map(|u| {
1260                            let n = line(u, "header_rows.unit")?;
1261                            if name.contains(&n) {
1262                                return Err(self.error(
1263                                    &u.span(),
1264                                    format!("header_rows.unit: line {n} is also a name line"),
1265                                ));
1266                            }
1267                            Ok(n)
1268                        })
1269                        .transpose()?;
1270                    HeaderRows { name, unit }
1271                }
1272                _ => HeaderRows {
1273                    name: lines(v, "header_rows")?,
1274                    unit: None,
1275                },
1276            };
1277            spec.header_rows = Some(rows);
1278        }
1279        if let Some(v) = top.get("metadata_line") {
1280            let n = line(v, "metadata_line")?;
1281            let header = spec.header_rows.as_ref();
1282            if header.is_some_and(|h| h.name.contains(&n) || h.unit == Some(n)) {
1283                return Err(self.error(
1284                    &v.span(),
1285                    format!("metadata_line: line {n} is a header line"),
1286                ));
1287            }
1288            let before_data = header
1289                .map_or(0, HeaderRows::last)
1290                .max(spec.skip_lines.unwrap_or(0));
1291            if n > before_data && spec.comment_char.is_none() {
1292                return Err(self.error(
1293                    &v.span(),
1294                    format!(
1295                        "metadata_line: line {n} would be read as data; put it above header_rows, or set skip_lines or comment"
1296                    ),
1297                ));
1298            }
1299            spec.metadata_line = Some(n);
1300        }
1301        if let Some(v) = top.get("columns") {
1302            let table = self.table(v, "[columns]")?;
1303            for (key, value) in table {
1304                let name: &str = key.get_ref();
1305                let what = format!("columns.{name}");
1306                let entry = self.table(value, &what)?;
1307                let keys = self.entries(
1308                    entry,
1309                    &what,
1310                    &["from", "as", "format", "description", "unit", "type"],
1311                )?;
1312                if name.trim().is_empty() {
1313                    return Err(self.error(&key.span(), "columns: a column needs a name"));
1314                }
1315                // A column of the file, or a derived one, may say what it means.
1316                let said = |k: &str| {
1317                    keys.get(k)
1318                        .map(|v| self.prose(v, &format!("{what}.{k}")))
1319                        .transpose()
1320                        .map(Option::unwrap_or_default)
1321                };
1322                // A column of the file read as a type: `{ type = "date", format = ... }`.
1323                let typed = match keys.get("type") {
1324                    Some(t) => {
1325                        if let Some(k) = ["from", "as"].iter().find(|k| keys.contains_key(*k)) {
1326                            return Err(self.error(
1327                                &keys[k].span(),
1328                                format!(
1329                                    "{what}: a derived column takes `as` for its type, not `type`"
1330                                ),
1331                            ));
1332                        }
1333                        let type_name = self.string(t, &format!("{what}.type"))?;
1334                        let format = keys
1335                            .get("format")
1336                            .map(|f| self.string(f, &format!("{what}.format")))
1337                            .transpose()?;
1338                        let ty = crate::column_types::ColumnType::named(&type_name, format)
1339                            .map_err(|e| self.error(&t.span(), format!("{what}.type: {e}")))?;
1340                        Some(ty)
1341                    }
1342                    None => None,
1343                };
1344                let note = ColumnNote {
1345                    description: said("description")?,
1346                    unit: said("unit")?,
1347                    values: Vec::new(),
1348                    ty: typed.as_ref().map(|t| t.name()).unwrap_or_default(),
1349                };
1350                let documented =
1351                    !(note.description.is_empty() && note.unit.is_empty() && note.ty.is_empty());
1352                if documented {
1353                    notes.push((key.span().start, name.to_string(), note));
1354                }
1355                if let Some(ty) = typed {
1356                    spec.types.push((name.to_string(), ty));
1357                    continue;
1358                }
1359                let derived = ["from", "as", "format"]
1360                    .iter()
1361                    .any(|k| keys.contains_key(k));
1362                if documented && !derived {
1363                    continue;
1364                }
1365                let Some(from) = keys.get("from") else {
1366                    return Err(self.error(
1367                        &value.span(),
1368                        format!(
1369                            "{what}: missing `from`, the columns it is made from (a column of the file takes description or unit alone)"
1370                        ),
1371                    ));
1372                };
1373                let from_columns: Vec<String> = match from.get_ref() {
1374                    DeValue::String(s) => vec![s.to_string()],
1375                    DeValue::Array(items) => items
1376                        .iter()
1377                        .map(|item| self.string(item, &format!("{what}.from")))
1378                        .collect::<Result<_, _>>()?,
1379                    _ => {
1380                        return Err(self.error(
1381                            &from.span(),
1382                            format!("{what}.from: expected a column name or a list of them"),
1383                        ));
1384                    }
1385                };
1386                let Some(kind) = keys.get("as") else {
1387                    return Err(self.error(
1388                        &value.span(),
1389                        format!("{what}: missing `as`; expected datetime, date or time"),
1390                    ));
1391                };
1392                let kind = match self.string(kind, &format!("{what}.as"))?.as_str() {
1393                    "datetime" => DerivedKind::Datetime,
1394                    "date" => DerivedKind::Date,
1395                    "time" => DerivedKind::Time,
1396                    _ => {
1397                        return Err(self.error(
1398                            &kind.span(),
1399                            format!("{what}.as: expected datetime, date or time"),
1400                        ));
1401                    }
1402                };
1403                let most = if kind == DerivedKind::Datetime { 3 } else { 1 };
1404                if from_columns.is_empty() || from_columns.len() > most {
1405                    let expected = if most == 3 {
1406                        "1 to 3 columns: a date, a time and a UTC offset"
1407                    } else {
1408                        "one column"
1409                    };
1410                    return Err(self.error(
1411                        &from.span(),
1412                        format!("{what}.from: as = \"{}\" takes {expected}", kind.name()),
1413                    ));
1414                }
1415                let format = keys
1416                    .get("format")
1417                    .map(|f| self.string(f, &format!("{what}.format")))
1418                    .transpose()?;
1419                spec.columns.push(Derived {
1420                    name: name.to_string(),
1421                    from: from_columns,
1422                    kind,
1423                    format,
1424                });
1425            }
1426        }
1427        notes.sort_by_key(|(at, ..)| *at);
1428        let notes = notes
1429            .into_iter()
1430            .map(|(_, name, note)| (name, note))
1431            .collect();
1432        Ok((spec, notes))
1433    }
1434
1435    fn fields(
1436        &self,
1437        value: &Value<'_>,
1438        part: Part,
1439        layout: Layout,
1440        scopes: &Scopes<'_>,
1441        prefix: &[Field],
1442    ) -> Result<Vec<Field>, SpecError> {
1443        let DeValue::Array(items) = value.get_ref() else {
1444            return Err(self.error(
1445                &value.span(),
1446                "fields: expected an array of tables, such as [{ name = \"ts\", type = \"u8\" }]",
1447            ));
1448        };
1449        let mut fields: Vec<Field> = prefix.to_vec();
1450        let mut names: std::collections::HashSet<String> =
1451            prefix.iter().flat_map(output_names).collect();
1452        for item in items {
1453            let field = self.field(item, part, layout, &fields, scopes)?;
1454            for name in output_names(&field) {
1455                if !names.insert(name.clone()) {
1456                    return Err(self.error(&item.span(), format!("a second field named `{name}`")));
1457                }
1458            }
1459            fields.push(field);
1460        }
1461        Ok(fields.split_off(prefix.len()))
1462    }
1463
1464    fn field(
1465        &self,
1466        value: &Value<'_>,
1467        part: Part,
1468        layout: Layout,
1469        earlier: &[Field],
1470        scopes: &Scopes<'_>,
1471    ) -> Result<Field, SpecError> {
1472        let table = self.table(value, "field")?;
1473        let mut keys = self.entries(table, "a field", FIELD_KEYS)?;
1474        let at = value.span();
1475        let (ty, endian) = match (keys.get("type"), keys.get("group")) {
1476            (Some(_), Some(g)) => {
1477                return Err(self.error(&g.span(), "group: a group has no type of its own"));
1478            }
1479            (None, Some(_)) => (Type::Group, None),
1480            (None, None) => return Err(self.error(&at, "field: missing `type`")),
1481            (Some(ty_value), None) => parse_type(&self.string(ty_value, "type")?).ok_or_else(|| {
1482                self.error(
1483                    &ty_value.span(),
1484                    "type: expected u1 to u8, s1 to s8, f2, f4, f8 (each with an optional le or be), bf2, vu, vs, bool, str, strz, bytes or pad",
1485                )
1486            })?,
1487        };
1488        let name = keys
1489            .get("name")
1490            .map(|v| {
1491                let name = self.string(v, "name")?;
1492                if name.trim().is_empty() {
1493                    return Err(self.error(&v.span(), "name: must not be empty"));
1494                }
1495                if name.contains('.') {
1496                    return Err(self.error(&v.span(), "name: must not contain `.`"));
1497                }
1498                Ok(name)
1499            })
1500            .transpose()?;
1501        if ty == Type::Pad {
1502            if let Some(key) = keys
1503                .keys()
1504                .find(|k| !matches!(**k, "type" | "size" | "size_adjust"))
1505            {
1506                return Err(self.error(
1507                    &keys[key].span(),
1508                    format!("pad: skipped bytes take only a size, not `{key}`"),
1509                ));
1510            }
1511        } else if name.is_none() {
1512            return Err(self.error(&at, "field: missing `name` (only pad goes without)"));
1513        }
1514        // A string offset is where a column starts (`offset = "header.px_off"`); a
1515        // number is the linear conversion's.
1516        let column_at = match keys.get("offset") {
1517            Some(v)
1518                if matches!(v.get_ref(), DeValue::String(_))
1519                    && layout == Layout::Columns
1520                    && part == Part::Records =>
1521            {
1522                let v = keys.remove("offset").expect("just read");
1523                Some(self.amount(v, None, "offset", Part::Header, &[], scopes)?)
1524            }
1525            _ => None,
1526        };
1527        if part != Part::Records
1528            && let Some(key) = ["delta", "bits", "group", "string_at", "lookup"]
1529                .iter()
1530                .find(|k| keys.contains_key(**k))
1531        {
1532            return Err(self.error(
1533                &keys[*key].span(),
1534                format!("{key}: is for a field of the records"),
1535            ));
1536        }
1537        if part != Part::Records && matches!(ty, Type::VarU | Type::VarS | Type::Strz) {
1538            return Err(self.error(
1539                &at,
1540                format!("{}: is for a field of the records", type_name(ty)),
1541            ));
1542        }
1543        let size = match (ty.width(), keys.get("size")) {
1544            (Some(width), Some(v)) => {
1545                return Err(self.error(
1546                    &v.span(),
1547                    format!(
1548                        "size: a {} is {width} bytes; size is for str, strz, bytes and pad",
1549                        type_name(ty)
1550                    ),
1551                ));
1552            }
1553            (None, Some(v)) if matches!(ty, Type::VarU | Type::VarS | Type::Group) => {
1554                return Err(
1555                    self.error(&v.span(), format!("size: a {} sizes itself", type_name(ty)))
1556                );
1557            }
1558            (Some(_), None) => None,
1559            (None, Some(v)) => Some(self.amount(
1560                v,
1561                keys.get("size_adjust").copied(),
1562                "size",
1563                part,
1564                earlier,
1565                scopes,
1566            )?),
1567            (None, None) if matches!(ty, Type::Strz | Type::VarU | Type::VarS | Type::Group) => {
1568                None
1569            }
1570            (None, None) => {
1571                return Err(self.error(&at, format!("{}: missing `size`", type_name(ty))));
1572            }
1573        };
1574        if !size.as_ref().is_some_and(|s| {
1575            matches!(
1576                s,
1577                Amount::Header { .. } | Amount::Footer { .. } | Amount::Record { .. }
1578            )
1579        }) && let Some(v) = keys.get("size_adjust")
1580        {
1581            return Err(self.error(&v.span(), "size_adjust goes with a size read from a field"));
1582        }
1583        if layout == Layout::Columns
1584            && part == Part::Records
1585            && size.as_ref().is_some_and(|s| !s.is_fixed())
1586        {
1587            return Err(self.error(
1588                &keys["size"].span(),
1589                "size: a column file's values are all one size",
1590            ));
1591        }
1592        let mut group = Vec::new();
1593        let mut count = keys
1594            .get("count")
1595            .map(|v| {
1596                if ty == Type::Group {
1597                    return Err(self.error(
1598                        &v.span(),
1599                        "count: a group's count goes inside group = { count = ... }",
1600                    ));
1601                }
1602                let count = self.amount(v, None, "count", part, earlier, scopes)?;
1603                if count == Amount::Given(0) {
1604                    return Err(self.error(&v.span(), "count: expected at least 1"));
1605                }
1606                Ok(count)
1607            })
1608            .transpose()?;
1609        if ty == Type::Group {
1610            let v = keys["group"];
1611            if layout == Layout::Columns {
1612                return Err(self.error(&v.span(), "group: a column file holds one value a row"));
1613            }
1614            let table = self.table(v, "group")?;
1615            let inner = self.entries(table, "group", &["count", "fields"])?;
1616            let Some(c) = inner.get("count") else {
1617                return Err(self.error(
1618                    &v.span(),
1619                    "group: missing `count`, such as count = \"n_levels\"",
1620                ));
1621            };
1622            count = Some(self.amount(c, None, "count", part, earlier, scopes)?);
1623            let Some(f) = inner.get("fields") else {
1624                return Err(self.error(&v.span(), "group: missing `fields`"));
1625            };
1626            group = self.fields(f, Part::Records, Layout::Rows, scopes, &[])?;
1627            if !group.iter().any(|f| f.name.is_some()) {
1628                return Err(self.error(&f.span(), "fields: a group item needs a named field"));
1629            }
1630        }
1631        let flatten = keys
1632            .get("flatten")
1633            .map(|v| self.boolean(v, "flatten"))
1634            .transpose()?
1635            .unwrap_or(false);
1636        if flatten {
1637            let v = keys["flatten"];
1638            match &count {
1639                Some(Amount::Given(n)) if *n > MAX_FLATTEN => {
1640                    return Err(self.error(
1641                        &v.span(),
1642                        format!(
1643                            "flatten: at most {MAX_FLATTEN} columns; leave {n} values an Array"
1644                        ),
1645                    ));
1646                }
1647                Some(Amount::Given(_)) if ty != Type::Group => {}
1648                _ => {
1649                    return Err(self.error(
1650                        &v.span(),
1651                        "flatten: goes with a count written in the spec, such as count = 10",
1652                    ));
1653                }
1654            }
1655        }
1656        if matches!(count, Some(Amount::Rest)) {
1657            return Err(self.error(&keys["count"].span(), "count: expected a number or a field"));
1658        }
1659
1660        let null = keys
1661            .get("null")
1662            .map(|v| {
1663                let null = match v.get_ref() {
1664                    DeValue::String(s) => match s.as_ref() {
1665                        "min" => Null::Min,
1666                        "max" => Null::Max,
1667                        "nan" => Null::NaN,
1668                        _ => {
1669                            return Err(self.error(
1670                                &v.span(),
1671                                "null: expected \"min\", \"max\", \"nan\" or an integer",
1672                            ));
1673                        }
1674                    },
1675                    DeValue::Integer(_) => Null::Value(i128::from(self.integer(v, "null")?)),
1676                    _ => {
1677                        return Err(self.error(
1678                            &v.span(),
1679                            "null: expected \"min\", \"max\", \"nan\" or an integer",
1680                        ));
1681                    }
1682                };
1683                let fits = match (null, ty) {
1684                    (Null::Min | Null::Max, t) => matches!(t, Type::Unsigned(_) | Type::Signed(_)),
1685                    (Null::NaN, t) => matches!(t, Type::Float(_) | Type::BFloat16),
1686                    (Null::Value(_), t) => t.is_number() || t == Type::Bool,
1687                };
1688                if !fits {
1689                    return Err(
1690                        self.error(&v.span(), format!("null: does not fit a {}", type_name(ty)))
1691                    );
1692                }
1693                // A sentinel the type cannot hold would never match.
1694                if let (Null::Value(value), Some((low, high))) = (null, integer_range(ty))
1695                    && !(low..=high).contains(&value)
1696                {
1697                    return Err(self.error(
1698                        &v.span(),
1699                        format!(
1700                            "null: a {} holds {low} to {high}, not {value}",
1701                            type_name(ty)
1702                        ),
1703                    ));
1704                }
1705                Ok(null)
1706            })
1707            .transpose()?;
1708
1709        let meaning = self.meaning(&keys, ty)?;
1710        let file = keys
1711            .get("file")
1712            .map(|v| {
1713                if part != Part::Records || layout == Layout::Rows {
1714                    return Err(self.error(
1715                        &v.span(),
1716                        "file: only a record field of layout = \"columns\" has a file of its own",
1717                    ));
1718                }
1719                let file = self.string(v, "file")?;
1720                if !is_file_name(&file) {
1721                    return Err(self.error(
1722                        &v.span(),
1723                        "file: expected the name of a file in the directory, such as px.dat",
1724                    ));
1725                }
1726                Ok(file)
1727            })
1728            .transpose()?;
1729        if let (Some(_), Some(v)) = (&column_at, keys.get("file")) {
1730            return Err(self.error(&v.span(), "file: a column at an offset is in the one file"));
1731        }
1732        // Its name names its file.
1733        if layout == Layout::Columns
1734            && part == Part::Records
1735            && file.is_none()
1736            && column_at.is_none()
1737            && let Some(name) = &name
1738            && !is_file_name(name)
1739        {
1740            return Err(self.error(
1741                &keys["name"].span(),
1742                "name: names the column's file, so it cannot hold a path; give the file with file = \"...\"",
1743            ));
1744        }
1745        let encoding = keys
1746            .get("encoding")
1747            .map(|v| {
1748                if !ty.is_text() {
1749                    return Err(self.error(&v.span(), "encoding: is for str and strz"));
1750                }
1751                match self.string(v, "encoding")?.as_str() {
1752                    "utf8" | "utf-8" | "ascii" => Ok(Encoding::Utf8),
1753                    "latin1" | "iso-8859-1" => Ok(Encoding::Latin1),
1754                    "utf16le" | "utf-16le" => Ok(Encoding::Utf16Le),
1755                    "utf16be" | "utf-16be" => Ok(Encoding::Utf16Be),
1756                    _ => Err(self.error(
1757                        &v.span(),
1758                        "encoding: expected utf8, latin1, utf16le or utf16be",
1759                    )),
1760                }
1761            })
1762            .transpose()?
1763            .unwrap_or_default();
1764        let delta = keys
1765            .get("delta")
1766            .map(|v| {
1767                if !ty.is_integer() || count.is_some() {
1768                    return Err(self.error(&v.span(), "delta: is for one integer a record"));
1769                }
1770                match v.get_ref() {
1771                    DeValue::Boolean(true) => Ok(Delta::All),
1772                    DeValue::Boolean(false) => Ok(Delta::None),
1773                    DeValue::String(s) if s.as_ref() == "block" => Ok(Delta::Block),
1774                    _ => Err(self.error(&v.span(), "delta: expected true or \"block\"")),
1775                }
1776            })
1777            .transpose()?
1778            .unwrap_or_default();
1779        if delta != Delta::None && layout == Layout::Columns {
1780            return Err(self.error(
1781                &keys["delta"].span(),
1782                "delta: is for records, not column files",
1783            ));
1784        }
1785        let bits = match keys.get("bits") {
1786            None => Vec::new(),
1787            Some(v) => self.bits(v, ty, count.is_some())?,
1788        };
1789        let string_at = keys
1790            .get("string_at")
1791            .map(|v| {
1792                if !matches!(ty, Type::Unsigned(_)) || meaning != Meaning::Plain || count.is_some()
1793                {
1794                    return Err(self.error(
1795                        &v.span(),
1796                        "string_at: is for an unsigned offset, one a record",
1797                    ));
1798                }
1799                self.string(v, "string_at")
1800            })
1801            .transpose()?;
1802        let lookup = keys
1803            .get("lookup")
1804            .map(|v| {
1805                if !matches!(ty, Type::Unsigned(_) | Type::Signed(_)) || meaning != Meaning::Plain {
1806                    return Err(self.error(&v.span(), "lookup: is for a plain integer index"));
1807                }
1808                let table = self.table(v, "lookup")?;
1809                let inner = self.entries(table, "lookup", &["file", "format"])?;
1810                let Some(f) = inner.get("file") else {
1811                    return Err(self.error(&v.span(), "lookup: missing `file`"));
1812                };
1813                let file = self.string(f, "file")?;
1814                if file.trim().is_empty() || Path::new(&file).is_absolute() {
1815                    return Err(self.error(
1816                        &f.span(),
1817                        "file: expected a path relative to the data, such as ../sym",
1818                    ));
1819                }
1820                let format = match inner.get("format") {
1821                    None => LookupFormat::Lines,
1822                    Some(fv) => {
1823                        let text = self.string(fv, "format")?;
1824                        match text.as_str() {
1825                            "lines" => LookupFormat::Lines,
1826                            "nul" => LookupFormat::Nul,
1827                            _ => match text
1828                                .strip_prefix("str:")
1829                                .and_then(|n| n.parse::<u64>().ok())
1830                            {
1831                                Some(n) if (1..=MAX_SIZE).contains(&n) => LookupFormat::Fixed(n),
1832                                _ => {
1833                                    return Err(self.error(
1834                                        &fv.span(),
1835                                        "format: expected lines, nul or str:N",
1836                                    ));
1837                                }
1838                            },
1839                        }
1840                    }
1841                };
1842                Ok(Lookup { file, format })
1843            })
1844            .transpose()?;
1845        if lookup.is_some() && string_at.is_some() {
1846            return Err(self.error(
1847                &keys["lookup"].span(),
1848                "lookup: a field takes one of lookup and string_at",
1849            ));
1850        }
1851        let description = keys
1852            .get("description")
1853            .map(|v| self.prose(v, "description"))
1854            .transpose()?;
1855        let unit = keys
1856            .get("unit")
1857            .map(|v| self.prose(v, "unit"))
1858            .transpose()?;
1859        Ok(Field {
1860            name,
1861            ty,
1862            size,
1863            endian,
1864            meaning,
1865            null,
1866            count,
1867            flatten,
1868            file,
1869            at: column_at,
1870            encoding,
1871            delta,
1872            bits,
1873            group,
1874            string_at,
1875            lookup,
1876            description,
1877            unit,
1878        })
1879    }
1880
1881    /// A value a field must hold: an integer for an integer field, text for text.
1882    fn expected(
1883        &self,
1884        value: &Value<'_>,
1885        target: &Field,
1886        what: &str,
1887    ) -> Result<Expected, SpecError> {
1888        match value.get_ref() {
1889            DeValue::Integer(_) if target.ty.is_integer() && target.meaning == Meaning::Plain => {
1890                Ok(Expected::Int(i128::from(self.integer(value, what)?)))
1891            }
1892            DeValue::String(s) if target.ty.is_text() => Ok(Expected::Text(s.to_string())),
1893            _ => Err(self.error(
1894                &value.span(),
1895                format!(
1896                    "{what}: `{}` is a {}; expected a value of that type",
1897                    target.name.as_deref().unwrap_or("pad"),
1898                    type_name(target.ty)
1899                ),
1900            )),
1901        }
1902    }
1903
1904    /// Bytes written as hex (`"1ACFFC1D"`, `"0x1a cf"`) or as a list of bytes.
1905    fn hex_bytes(&self, value: &Value<'_>, what: &str) -> Result<Vec<u8>, SpecError> {
1906        let bad = || {
1907            self.error(
1908                &value.span(),
1909                format!("{what}: expected hex such as \"1ACFFC1D\", or a list of bytes"),
1910            )
1911        };
1912        let bytes = match value.get_ref() {
1913            DeValue::String(s) => {
1914                let digits: String = s
1915                    .trim()
1916                    .trim_start_matches("0x")
1917                    .chars()
1918                    .filter(|c| !c.is_whitespace())
1919                    .collect();
1920                // Hex digits only, so every pair is two bytes of the string.
1921                if digits.is_empty()
1922                    || !digits.len().is_multiple_of(2)
1923                    || !digits.bytes().all(|b| b.is_ascii_hexdigit())
1924                {
1925                    return Err(bad());
1926                }
1927                (0..digits.len())
1928                    .step_by(2)
1929                    .map(|i| u8::from_str_radix(&digits[i..i + 2], 16).map_err(|_| bad()))
1930                    .collect::<Result<Vec<u8>, _>>()?
1931            }
1932            DeValue::Array(items) => items
1933                .iter()
1934                .map(|item| {
1935                    let byte = self.integer(item, what)?;
1936                    u8::try_from(byte).map_err(|_| {
1937                        self.error(&item.span(), format!("{what}: a byte is 0 to 255"))
1938                    })
1939                })
1940                .collect::<Result<_, _>>()?,
1941            _ => return Err(bad()),
1942        };
1943        if bytes.is_empty() || bytes.len() > 64 {
1944            return Err(self.error(&value.span(), format!("{what}: expected 1 to 64 bytes")));
1945        }
1946        Ok(bytes)
1947    }
1948
1949    fn footer(&self, value: &Value<'_>, header: &[Field]) -> Result<Footer, SpecError> {
1950        let table = self.table(value, "[footer]")?;
1951        let keys = self.entries(table, "[footer]", &["fields", "size", "checksum"])?;
1952        let scopes = Scopes {
1953            header,
1954            footer: &[],
1955        };
1956        let fields = match keys.get("fields") {
1957            Some(f) => self.fields(f, Part::Footer, Layout::Rows, &scopes, &[])?,
1958            None => Vec::new(),
1959        };
1960        let width = given_width(&fields);
1961        let size = match keys.get("size") {
1962            None => None,
1963            Some(v) => {
1964                let n = self.integer(v, "size")?;
1965                if n < 0 || n as u64 > MAX_SIZE {
1966                    return Err(self.error(&v.span(), format!("size: expected 0 to {MAX_SIZE}")));
1967                }
1968                if width.is_some_and(|w| w > n as u64) {
1969                    return Err(self.error(
1970                        &v.span(),
1971                        format!(
1972                            "size: the fields take {} bytes, more than {n}",
1973                            width.unwrap_or(0)
1974                        ),
1975                    ));
1976                }
1977                Some(n as u64)
1978            }
1979        };
1980        if size.is_none() && width.is_none() {
1981            return Err(self.error(
1982                &value.span(),
1983                "[footer]: read from the end of the file, so its fields' sizes are written in the spec, or give its size",
1984            ));
1985        }
1986        let checksum = keys
1987            .get("checksum")
1988            .map(|v| {
1989                let table = self.table(v, "checksum")?;
1990                let inner = self.entries(table, "checksum", &["algo", "field"])?;
1991                let algo = self.algo(inner.get("algo").copied(), v)?;
1992                let Some(f) = inner.get("field") else {
1993                    return Err(self.error(
1994                        &v.span(),
1995                        "checksum: missing `field`, the footer field holding it",
1996                    ));
1997                };
1998                let field = self.string(f, "field")?;
1999                let field = field.strip_prefix("footer.").unwrap_or(&field).to_string();
2000                if !fields
2001                    .iter()
2002                    .any(|x| x.name.as_deref() == Some(field.as_str()) && x.ty.is_integer())
2003                {
2004                    return Err(self.error(
2005                        &f.span(),
2006                        format!("field: no integer footer field named `{field}`"),
2007                    ));
2008                }
2009                Ok((algo, field))
2010            })
2011            .transpose()?;
2012        Ok(Footer {
2013            fields,
2014            size,
2015            checksum,
2016        })
2017    }
2018
2019    fn algo(&self, value: Option<&Value<'_>>, at: &Value<'_>) -> Result<ChecksumAlgo, SpecError> {
2020        let Some(v) = value else {
2021            return Err(self.error(&at.span(), "checksum: missing `algo`"));
2022        };
2023        ChecksumAlgo::parse(&self.string(v, "algo")?).ok_or_else(|| {
2024            let names: Vec<&str> = ChecksumAlgo::NAMES.iter().map(|(n, _)| *n).collect();
2025            self.error(
2026                &v.span(),
2027                format!("algo: expected one of {}", names.join(", ")),
2028            )
2029        })
2030    }
2031
2032    fn sections(&self, value: &Value<'_>, scopes: &Scopes<'_>) -> Result<Vec<Section>, SpecError> {
2033        let table = self.table(value, "[sections]")?;
2034        let mut out = Vec::new();
2035        for (key, v) in table {
2036            let name: &str = key.get_ref();
2037            let inner = self.table(v, "section")?;
2038            let keys = self.entries(inner, "a section", &["offset", "size"])?;
2039            let (Some(o), Some(s)) = (keys.get("offset"), keys.get("size")) else {
2040                return Err(self.error(
2041                    &v.span(),
2042                    format!("[sections.{name}]: needs offset and size"),
2043                ));
2044            };
2045            let offset = self.amount(o, None, "offset", Part::Header, &[], scopes)?;
2046            let size = self.amount(s, None, "size", Part::Header, &[], scopes)?;
2047            out.push(Section {
2048                name: name.to_string(),
2049                offset,
2050                size,
2051            });
2052        }
2053        Ok(out)
2054    }
2055
2056    fn records(
2057        &self,
2058        value: &Value<'_>,
2059        variants_value: Option<&Value<'_>>,
2060        layout: Layout,
2061        scopes: &Scopes<'_>,
2062    ) -> Result<Records, SpecError> {
2063        let table = self.table(value, "[records]")?;
2064        let keys = self.entries(
2065            table,
2066            "[records]",
2067            &[
2068                "framing",
2069                "fields",
2070                "size",
2071                "size_adjust",
2072                "count",
2073                "length_suffix",
2074                "align",
2075                "sync",
2076                "type",
2077                "checksum",
2078                "ring",
2079                "common",
2080            ],
2081        )?;
2082        let framing = match keys.get("framing") {
2083            None => Framing::Fixed,
2084            Some(v) => match self.string(v, "framing")?.as_str() {
2085                "fixed" => Framing::Fixed,
2086                "length_prefixed" => Framing::LengthPrefixed,
2087                "variant" => Framing::Variant,
2088                "sync" => Framing::Sync,
2089                "blocks" => {
2090                    return Err(self.error(
2091                        &v.span(),
2092                        "framing: blocks are described under [blocks]; framing is how records sit inside each block",
2093                    ));
2094                }
2095                _ => {
2096                    return Err(self.error(
2097                        &v.span(),
2098                        "framing: expected fixed, length_prefixed, variant or sync",
2099                    ));
2100                }
2101            },
2102        };
2103        let common_value = match (keys.get("fields"), keys.get("common")) {
2104            (Some(_), Some(c)) => {
2105                return Err(self.error(
2106                    &c.span(),
2107                    "[records.common]: give the shared fields here or as `fields`, not both",
2108                ));
2109            }
2110            (Some(f), None) => Some(*f),
2111            (None, Some(c)) => {
2112                let t = self.table(c, "[records.common]")?;
2113                let k = self.entries(t, "[records.common]", &["fields"])?;
2114                k.get("fields").copied()
2115            }
2116            (None, None) => None,
2117        };
2118        let mut fields = match common_value {
2119            Some(f) => self.fields(f, Part::Records, layout, scopes, &[])?,
2120            None => Vec::new(),
2121        };
2122        let mut type_field = None;
2123        if let Some(t) = keys.get("type") {
2124            let name =
2125                match t.get_ref() {
2126                    DeValue::String(s) => s.to_string(),
2127                    DeValue::Table(inner) => {
2128                        let k = self.entries(inner, "type", &["field", "type"])?;
2129                        let Some(f) = k.get("field") else {
2130                            return Err(self.error(&t.span(), "type: missing `field`"));
2131                        };
2132                        let name = self.string(f, "field")?;
2133                        if let Some(ty) = k.get("type") {
2134                            if fields
2135                                .iter()
2136                                .any(|x| x.name.as_deref() == Some(name.as_str()))
2137                            {
2138                                return Err(self.error(
2139                                    &ty.span(),
2140                                    format!("type: `{name}` is already a field; name it alone"),
2141                                ));
2142                            }
2143                            let (ty, endian) = parse_type(&self.string(ty, "type")?)
2144                                .filter(|(t, _)| {
2145                                    matches!(t, Type::Unsigned(_) | Type::Signed(_) | Type::Str)
2146                                })
2147                                .ok_or_else(|| {
2148                                    self.error(
2149                                        &ty.span(),
2150                                        "type: a type field is an integer, or str with a size",
2151                                    )
2152                                })?;
2153                            if ty == Type::Str {
2154                                return Err(self.error(
2155                                    &t.span(),
2156                                    "type: a str type field goes in the fields, with its size",
2157                                ));
2158                            }
2159                            fields.push(Field::plain(&name, ty, endian));
2160                        }
2161                        name
2162                    }
2163                    _ => return Err(self.error(
2164                        &t.span(),
2165                        "type: expected the name of a common field, or { field = ..., type = ... }",
2166                    )),
2167                };
2168            let Some(target) = fields
2169                .iter()
2170                .find(|f| f.name.as_deref() == Some(name.as_str()))
2171            else {
2172                return Err(self.error(&t.span(), format!("type: no common field named `{name}`")));
2173            };
2174            if !(target.ty.is_integer() || target.ty.is_text()) || target.count.is_some() {
2175                return Err(self.error(
2176                    &t.span(),
2177                    format!("type: `{name}` is not an integer or text field"),
2178                ));
2179            }
2180            type_field = Some(name);
2181        }
2182        let mut variants = Vec::new();
2183        if let Some(v) = variants_value {
2184            let Some(type_name) = &type_field else {
2185                return Err(self.error(
2186                    &v.span(),
2187                    "[[variants]]: needs [records] type, the field that picks one",
2188                ));
2189            };
2190            let target = fields
2191                .iter()
2192                .find(|f| f.name.as_deref() == Some(type_name.as_str()))
2193                .expect("checked above")
2194                .clone();
2195            let DeValue::Array(items) = v.get_ref() else {
2196                return Err(self.error(&v.span(), "variants: expected [[variants]] tables"));
2197            };
2198            let mut seen = std::collections::HashSet::new();
2199            for item in items {
2200                let t = self.table(item, "variant")?;
2201                let k = self.entries(
2202                    t,
2203                    "a variant",
2204                    &[
2205                        "name",
2206                        "when",
2207                        "fields",
2208                        "size",
2209                        "size_adjust",
2210                        "description",
2211                    ],
2212                )?;
2213                let Some(n) = k.get("name") else {
2214                    return Err(self.error(&item.span(), "variant: missing `name`"));
2215                };
2216                let name = self.string(n, "name")?;
2217                if name.trim().is_empty() || !seen.insert(name.clone()) {
2218                    return Err(self.error(
2219                        &n.span(),
2220                        format!("name: `{name}` is empty or a second variant's"),
2221                    ));
2222                }
2223                let Some(w) = k.get("when") else {
2224                    return Err(self.error(
2225                        &item.span(),
2226                        "variant: missing `when`, the type value that picks it",
2227                    ));
2228                };
2229                let when = match w.get_ref() {
2230                    DeValue::Array(values) if values.is_empty() => {
2231                        return Err(self.error(
2232                            &w.span(),
2233                            "when: an empty list picks no record; give the type value",
2234                        ));
2235                    }
2236                    DeValue::Array(values) => values
2237                        .iter()
2238                        .map(|x| self.expected(x, &target, "when"))
2239                        .collect::<Result<Vec<_>, _>>()?,
2240                    _ => vec![self.expected(w, &target, "when")?],
2241                };
2242                let vfields = match k.get("fields") {
2243                    Some(f) => self.fields(f, Part::Records, layout, scopes, &fields)?,
2244                    None => Vec::new(),
2245                };
2246                let mut every = fields.clone();
2247                every.extend(vfields.iter().cloned());
2248                let size = k
2249                    .get("size")
2250                    .map(|s| {
2251                        self.amount(
2252                            s,
2253                            k.get("size_adjust").copied(),
2254                            "size",
2255                            Part::Records,
2256                            &every,
2257                            scopes,
2258                        )
2259                    })
2260                    .transpose()?;
2261                if let (Some(Amount::Given(size)), Some(sum)) = (&size, given_width(&every))
2262                    && *size < sum
2263                {
2264                    return Err(self.error(
2265                        &k["size"].span(),
2266                        format!("size: the fields take {sum} bytes, more than {size}"),
2267                    ));
2268                }
2269                let description = k
2270                    .get("description")
2271                    .map(|d| self.prose(d, "description"))
2272                    .transpose()?;
2273                variants.push(Variant {
2274                    name,
2275                    when,
2276                    fields: vfields,
2277                    size,
2278                    description,
2279                });
2280            }
2281            // A name two variants share is one column, so it has one type.
2282            let mut by_name: BTreeMap<String, &Field> = BTreeMap::new();
2283            for variant in &variants {
2284                for f in &variant.fields {
2285                    let Some(n) = &f.name else { continue };
2286                    if let Some(other) = by_name.get(n)
2287                        && (other.ty != f.ty
2288                            || other.meaning != f.meaning
2289                            || other.count != f.count
2290                            || other.flatten != f.flatten
2291                            || other.bits != f.bits
2292                            || other.group != f.group
2293                            || other.encoding != f.encoding)
2294                    {
2295                        return Err(self.error(
2296                            &v.span(),
2297                            format!("variants: `{n}` is a different field in two variants; one column has one type, so name them apart"),
2298                        ));
2299                    }
2300                    by_name.insert(n.clone(), f);
2301                }
2302            }
2303            if fields
2304                .iter()
2305                .filter_map(|f| f.name.as_deref())
2306                .any(|n| n == "type")
2307                || by_name.contains_key("type")
2308            {
2309                return Err(self.error(&v.span(), "variants: the variant's name is shown in a column named `type`, so no field may be named that"));
2310            }
2311        } else if type_field.is_some() {
2312            return Err(self.error(
2313                &keys["type"].span(),
2314                "type: picks one of the [[variants]], and there are none",
2315            ));
2316        }
2317        if fields.is_empty() && variants.is_empty() {
2318            return Err(self.error(&value.span(), "[records]: missing `fields`"));
2319        }
2320        if !fields.iter().any(|f| f.name.is_some()) && variants.is_empty() {
2321            return Err(self.error(&value.span(), "fields: a record needs a named field"));
2322        }
2323        let size = keys
2324            .get("size")
2325            .map(|s| {
2326                self.amount(
2327                    s,
2328                    keys.get("size_adjust").copied(),
2329                    "size",
2330                    Part::Records,
2331                    &fields,
2332                    scopes,
2333                )
2334            })
2335            .transpose()?;
2336        if matches!(size, Some(Amount::Rest)) {
2337            return Err(self.error(&keys["size"].span(), "size: expected a number or a field"));
2338        }
2339        let count = keys
2340            .get("count")
2341            .map(|c| self.amount(c, None, "count", Part::Header, &[], scopes))
2342            .transpose()?;
2343        let length_suffix = keys
2344            .get("length_suffix")
2345            .map(|v| self.boolean(v, "length_suffix"))
2346            .transpose()?
2347            .unwrap_or(false);
2348        let align = match keys.get("align") {
2349            None => 1,
2350            Some(v) => {
2351                let n = self.integer(v, "align")?;
2352                if !(1..=65536).contains(&n) {
2353                    return Err(self.error(&v.span(), "align: expected 1 to 65536"));
2354                }
2355                n as u64
2356            }
2357        };
2358        let sync = keys
2359            .get("sync")
2360            .map(|v| self.hex_bytes(v, "sync"))
2361            .transpose()?
2362            .unwrap_or_default();
2363        let ring = keys
2364            .get("ring")
2365            .map(|v| self.amount(v, None, "ring", Part::Header, &[], scopes))
2366            .transpose()?;
2367        let record_size = matches!(size, Some(Amount::Record { .. }));
2368        let at = |key: &str| keys.get(key).map_or(value.span(), |v| v.span());
2369        match framing {
2370            Framing::LengthPrefixed if !record_size => {
2371                return Err(self.error(&at("size"), "framing = \"length_prefixed\": size names the field holding each record's length, such as size = \"len\""));
2372            }
2373            Framing::Variant if variants.is_empty() => {
2374                return Err(self.error(
2375                    &at("framing"),
2376                    "framing = \"variant\": needs [[variants]] and a type field",
2377                ));
2378            }
2379            Framing::Sync if sync.is_empty() => {
2380                return Err(self.error(&at("framing"), "framing = \"sync\": needs sync, the marker each record starts with, such as sync = \"1ACFFC1D\""));
2381            }
2382            Framing::Fixed | Framing::Variant if record_size => {
2383                return Err(self.error(&at("size"), "size: a record whose size is in its own field needs framing = \"length_prefixed\""));
2384            }
2385            _ => {}
2386        }
2387        if framing != Framing::Sync && !sync.is_empty() {
2388            return Err(self.error(&at("sync"), "sync: goes with framing = \"sync\""));
2389        }
2390        if length_suffix && !record_size {
2391            return Err(self.error(
2392                &at("length_suffix"),
2393                "length_suffix: repeats a length read from the record; give size = \"len\"",
2394            ));
2395        }
2396        let checksum = keys
2397            .get("checksum")
2398            .map(|v| {
2399                let table = self.table(v, "checksum")?;
2400                let inner = self.entries(table, "checksum", &["algo", "field", "from", "to"])?;
2401                let algo = self.algo(inner.get("algo").copied(), v)?;
2402                let named = |key: &str| -> Result<Option<String>, SpecError> {
2403                    let Some(x) = inner.get(key) else {
2404                        return Ok(None);
2405                    };
2406                    let name = self.string(x, key)?;
2407                    let known = fields
2408                        .iter()
2409                        .chain(variants.iter().flat_map(|v| &v.fields))
2410                        .any(|f| f.name.as_deref() == Some(name.as_str()));
2411                    if !known {
2412                        return Err(
2413                            self.error(&x.span(), format!("{key}: no record field named `{name}`"))
2414                        );
2415                    }
2416                    Ok(Some(name))
2417                };
2418                let Some(field) = named("field")? else {
2419                    return Err(
2420                        self.error(&v.span(), "checksum: missing `field`, the field holding it")
2421                    );
2422                };
2423                let holder = fields
2424                    .iter()
2425                    .chain(variants.iter().flat_map(|v| &v.fields))
2426                    .find(|f| f.name.as_deref() == Some(field.as_str()))
2427                    .expect("checked");
2428                if !matches!(holder.ty, Type::Unsigned(_) | Type::Signed(_)) {
2429                    return Err(self.error(&v.span(), "checksum: its field is an integer"));
2430                }
2431                Ok(Checksum {
2432                    algo,
2433                    field,
2434                    from: named("from")?,
2435                    to: named("to")?,
2436                })
2437            })
2438            .transpose()?;
2439        if checksum.is_some()
2440            && fields
2441                .iter()
2442                .chain(variants.iter().flat_map(|v| &v.fields))
2443                .any(|f| f.name.as_deref() == Some("checksum_ok"))
2444        {
2445            return Err(self.error(&value.span(), "checksum: its result is a column named `checksum_ok`, so no field may be named that"));
2446        }
2447        Ok(Records {
2448            framing,
2449            fields,
2450            size,
2451            count,
2452            length_suffix,
2453            align,
2454            sync,
2455            type_field,
2456            variants,
2457            checksum,
2458            ring,
2459        })
2460    }
2461
2462    fn blocks(&self, value: &Value<'_>, scopes: &Scopes<'_>) -> Result<Blocks, SpecError> {
2463        let table = self.table(value, "[blocks]")?;
2464        let keys = self.entries(
2465            table,
2466            "[blocks]",
2467            &[
2468                "header",
2469                "size",
2470                "size_adjust",
2471                "compression",
2472                "records",
2473                "uncompressed",
2474                "index",
2475            ],
2476        )?;
2477        let header = match keys.get("header") {
2478            Some(h) => self.fields(h, Part::Header, Layout::Rows, scopes, &[])?,
2479            None => Vec::new(),
2480        };
2481        let Some(s) = keys.get("size") else {
2482            return Err(self.error(&value.span(), "[blocks]: missing `size`, the bytes after each block's header, such as size = \"clen\""));
2483        };
2484        let size = self.amount(
2485            s,
2486            keys.get("size_adjust").copied(),
2487            "size",
2488            Part::Records,
2489            &header,
2490            scopes,
2491        )?;
2492        if matches!(size, Amount::Rest) {
2493            return Err(self.error(&s.span(), "size: expected a number or a block header field"));
2494        }
2495        let header_field = |key: &str| -> Result<Option<String>, SpecError> {
2496            let Some(v) = keys.get(key) else {
2497                return Ok(None);
2498            };
2499            let name = self.string(v, key)?;
2500            if !header.iter().any(|f| {
2501                f.name.as_deref() == Some(name.as_str())
2502                    && f.ty.is_integer()
2503                    && f.meaning == Meaning::Plain
2504            }) {
2505                return Err(self.error(
2506                    &v.span(),
2507                    format!("{key}: no plain integer block header field named `{name}`"),
2508                ));
2509            }
2510            Ok(Some(name))
2511        };
2512        let records = header_field("records")?;
2513        let uncompressed = header_field("uncompressed")?;
2514        let codec = match keys.get("compression") {
2515            None => Codec::Fixed(Compression::None),
2516            Some(v) => match v.get_ref() {
2517                DeValue::String(name) => Codec::Fixed(self.compression(v, name)?),
2518                DeValue::Table(inner) => {
2519                    let k = self.entries(inner, "compression", &["field", "values"])?;
2520                    let (Some(f), Some(vals)) = (k.get("field"), k.get("values")) else {
2521                        return Err(self.error(&v.span(), "compression: expected { field = \"codec\", values = { 0 = \"none\", 1 = \"zstd\" } }"));
2522                    };
2523                    let field = self.string(f, "field")?;
2524                    if !header
2525                        .iter()
2526                        .any(|x| x.name.as_deref() == Some(field.as_str()) && x.ty.is_integer())
2527                    {
2528                        return Err(self.error(
2529                            &f.span(),
2530                            format!("field: no integer block header field named `{field}`"),
2531                        ));
2532                    }
2533                    let mut values = BTreeMap::new();
2534                    for (code, name) in self.table(vals, "values")? {
2535                        let text: &str = code.get_ref();
2536                        let code_value: i64 = text.parse().map_err(|_| {
2537                            self.error(
2538                                &code.span(),
2539                                format!("values: `{text}` is not a whole number"),
2540                            )
2541                        })?;
2542                        let DeValue::String(n) = name.get_ref() else {
2543                            return Err(self.error(&name.span(), "values: expected a codec's name"));
2544                        };
2545                        values.insert(code_value, self.compression(name, n)?);
2546                    }
2547                    Codec::ByField { field, values }
2548                }
2549                _ => {
2550                    return Err(self.error(
2551                        &v.span(),
2552                        "compression: expected a codec's name or { field, values }",
2553                    ));
2554                }
2555            },
2556        };
2557        let lz4_block = match &codec {
2558            Codec::Fixed(c) => *c == Compression::Lz4Block,
2559            Codec::ByField { values, .. } => values.values().any(|c| *c == Compression::Lz4Block),
2560        };
2561        if lz4_block && uncompressed.is_none() {
2562            return Err(self.error(&value.span(), "compression: lz4_block needs uncompressed, the header field with each block's decompressed size"));
2563        }
2564        let index = keys
2565            .get("index")
2566            .map(|v| {
2567                let t = self.table(v, "index")?;
2568                let k = self.entries(t, "index", &["at", "count", "fields"])?;
2569                let (Some(a), Some(c), Some(f)) = (k.get("at"), k.get("count"), k.get("fields"))
2570                else {
2571                    return Err(self.error(&v.span(), "index: needs at, count and fields"));
2572                };
2573                let at = self.amount(a, None, "at", Part::Header, &[], scopes)?;
2574                let count = self.amount(c, None, "count", Part::Header, &[], scopes)?;
2575                let fields = self.fields(f, Part::Header, Layout::Rows, scopes, &[])?;
2576                if given_width(&fields).is_none() {
2577                    return Err(self.error(
2578                        &f.span(),
2579                        "fields: an index entry's sizes are written in the spec",
2580                    ));
2581                }
2582                if !fields
2583                    .iter()
2584                    .any(|x| x.name.as_deref() == Some("offset") && x.ty.is_integer())
2585                {
2586                    return Err(self.error(
2587                        &f.span(),
2588                        "fields: an index entry needs an integer `offset`, where its block starts",
2589                    ));
2590                }
2591                Ok(BlockIndex { at, count, fields })
2592            })
2593            .transpose()?;
2594        Ok(Blocks {
2595            header,
2596            size,
2597            codec,
2598            records,
2599            uncompressed,
2600            index,
2601        })
2602    }
2603
2604    fn compression(&self, at: &Value<'_>, name: &str) -> Result<Compression, SpecError> {
2605        Compression::parse(name).ok_or_else(|| {
2606            let names: Vec<&str> = Compression::NAMES.iter().map(|(n, _)| *n).collect();
2607            self.error(
2608                &at.span(),
2609                format!("compression: expected one of {}", names.join(", ")),
2610            )
2611        })
2612    }
2613
2614    fn capture(&self, value: &Value<'_>) -> Result<Capture, SpecError> {
2615        let table = self.table(value, "[capture]")?;
2616        // pcap or pcapng is told by the file's magic, so there is no key for it.
2617        let keys = self.entries(table, "[capture]", &["header", "count", "time"])?;
2618        let header = match keys.get("header") {
2619            Some(h) => self.fields(h, Part::Records, Layout::Rows, &Scopes::default(), &[])?,
2620            None => Vec::new(),
2621        };
2622        let count = keys
2623            .get("count")
2624            .map(|v| {
2625                let name = self.string(v, "count")?;
2626                if !header
2627                    .iter()
2628                    .any(|f| f.name.as_deref() == Some(name.as_str()) && f.ty.is_integer())
2629                {
2630                    return Err(self.error(
2631                        &v.span(),
2632                        format!("count: no integer payload header field named `{name}`"),
2633                    ));
2634                }
2635                Ok(name)
2636            })
2637            .transpose()?;
2638        let time = keys
2639            .get("time")
2640            .map(|v| {
2641                let name = self.string(v, "time")?;
2642                if name.trim().is_empty() || name.contains('.') {
2643                    return Err(self.error(&v.span(), "time: expected a column name"));
2644                }
2645                Ok(name)
2646            })
2647            .transpose()?;
2648        Ok(Capture {
2649            header,
2650            count,
2651            time,
2652        })
2653    }
2654
2655    fn files(&self, value: &Value<'_>) -> Result<Files, SpecError> {
2656        let table = self.table(value, "[files]")?;
2657        let keys = self.entries(table, "[files]", &["path"])?;
2658        let Some(p) = keys.get("path") else {
2659            return Err(self.error(
2660                &value.span(),
2661                "[files]: missing `path`, such as path = \"{date:%Y%m%d}/{venue}/trades.bin\"",
2662            ));
2663        };
2664        let pattern = self.string(p, "path")?;
2665        let parts =
2666            path_parts(&pattern).map_err(|e| self.error(&p.span(), format!("path: {e}")))?;
2667        Ok(Files { pattern, parts })
2668    }
2669
2670    /// What the columns layout allows: fixed values, one file each or all at offsets in
2671    /// one file.
2672    fn check_columns(&self, value: &Value<'_>, records: &Records) -> Result<(), SpecError> {
2673        if records.size.is_some() {
2674            return Err(self.error(
2675                &value.span(),
2676                "size: each column holds one field, so layout = \"columns\" takes no record size",
2677            ));
2678        }
2679        if records.framing != Framing::Fixed || records.checksum.is_some() || records.ring.is_some()
2680        {
2681            return Err(self.error(
2682                &value.span(),
2683                "layout = \"columns\": values are fixed, with no framing, checksum or ring",
2684            ));
2685        }
2686        let at = records.fields.iter().filter(|f| f.at.is_some()).count();
2687        if at != 0 && at != records.fields.len() {
2688            return Err(self.error(
2689                &value.span(),
2690                "offset: in one file, every column gives where it starts",
2691            ));
2692        }
2693        for field in &records.fields {
2694            let said = if field.ty == Type::Pad {
2695                Some("pad: layout = \"columns\" has no bytes between fields to skip")
2696            } else if field.flatten {
2697                Some("flatten: a column file holds one column; leave the values an Array")
2698            } else if matches!(field.ty, Type::Strz | Type::VarU | Type::VarS | Type::Group)
2699                || !field.bits.is_empty()
2700                || field.string_at.is_some()
2701            {
2702                Some("layout = \"columns\": a column's values are fixed-width")
2703            } else if field.size.as_ref().is_some_and(|s| !s.is_fixed())
2704                || field.count.as_ref().is_some_and(|s| !s.is_fixed())
2705            {
2706                Some("layout = \"columns\": a column's values are all one size")
2707            } else {
2708                None
2709            };
2710            if let Some(said) = said {
2711                return Err(self.error(&value.span(), said));
2712            }
2713        }
2714        Ok(())
2715    }
2716
2717    /// An integer's bit fields.
2718    fn bits(&self, value: &Value<'_>, ty: Type, counted: bool) -> Result<Vec<BitField>, SpecError> {
2719        let total = match ty {
2720            Type::Unsigned(n) | Type::Signed(n) => u32::from(n) * 8,
2721            Type::VarU | Type::VarS => 64,
2722            _ => return Err(self.error(&value.span(), "bits: are for an integer field")),
2723        };
2724        if counted {
2725            return Err(self.error(&value.span(), "bits: are for one integer a record"));
2726        }
2727        let DeValue::Array(items) = value.get_ref() else {
2728            return Err(self.error(
2729                &value.span(),
2730                "bits: expected a list, such as [{ name = \"valid\", bit = 0 }]",
2731            ));
2732        };
2733        let mut out = Vec::new();
2734        for item in items {
2735            let table = self.table(item, "bit")?;
2736            let keys = self.entries(table, "a bit field", &["name", "bit", "width", "enum"])?;
2737            let Some(n) = keys.get("name") else {
2738                return Err(self.error(&item.span(), "bit: missing `name`"));
2739            };
2740            let name = self.string(n, "name")?;
2741            if name.trim().is_empty() || name.contains('.') {
2742                return Err(self.error(&n.span(), "name: must not be empty or contain `.`"));
2743            }
2744            let Some(b) = keys.get("bit") else {
2745                return Err(self.error(
2746                    &item.span(),
2747                    "bit: missing `bit`, the lowest bit, 0 the least significant",
2748                ));
2749            };
2750            let bit = self.integer(b, "bit")?;
2751            let width = keys
2752                .get("width")
2753                .map(|w| self.integer(w, "width"))
2754                .transpose()?
2755                .unwrap_or(1);
2756            if bit < 0 || width < 1 || bit + width > i64::from(total) {
2757                return Err(self.error(
2758                    &item.span(),
2759                    format!(
2760                        "bit: bits {bit} to {} are outside the field's {total}",
2761                        bit + width - 1
2762                    ),
2763                ));
2764            }
2765            let labels = keys
2766                .get("enum")
2767                .map(|e| {
2768                    let table = self.table(e, "enum")?;
2769                    let mut labels = BTreeMap::new();
2770                    for (code, label) in table {
2771                        let text: &str = code.get_ref();
2772                        let code_value: i64 = text.parse().map_err(|_| {
2773                            self.error(
2774                                &code.span(),
2775                                format!("enum: `{text}` is not a whole number"),
2776                            )
2777                        })?;
2778                        labels.insert(code_value, self.string(label, "enum label")?);
2779                    }
2780                    Ok::<_, SpecError>(Arc::new(labels))
2781                })
2782                .transpose()?;
2783            out.push(BitField {
2784                name,
2785                bit: bit as u32,
2786                width: width as u32,
2787                labels,
2788            });
2789        }
2790        Ok(out)
2791    }
2792
2793    /// What a field means: at most one of a time, a scale, a linear conversion and an
2794    /// enum.
2795    fn meaning(&self, keys: &BTreeMap<&str, &Value<'_>>, ty: Type) -> Result<Meaning, SpecError> {
2796        let groups: [(&[&str], &str); 4] = [
2797            (&["time", "of_day", "date", "epoch"], "time"),
2798            (&["scale"], "scale"),
2799            (&["factor", "offset"], "factor"),
2800            (&["enum"], "enum"),
2801        ];
2802        let used: Vec<(&str, Range<usize>)> = groups
2803            .iter()
2804            .filter_map(|(group, said)| {
2805                group
2806                    .iter()
2807                    .find_map(|k| keys.get(k))
2808                    .map(|v| (*said, v.span()))
2809            })
2810            .collect();
2811        if used.len() > 1 {
2812            return Err(self.error(
2813                &used[1].1,
2814                format!(
2815                    "a field takes one of time (or date), scale, factor and enum, not {} and {}",
2816                    used[0].0, used[1].0
2817                ),
2818            ));
2819        }
2820        let Some((kind, span)) = used.into_iter().next() else {
2821            return Ok(Meaning::Plain);
2822        };
2823        let integers_only = |what: &str| {
2824            if ty.is_integer() {
2825                Ok(())
2826            } else {
2827                Err(self.error(
2828                    &span,
2829                    format!("{what} is for integer types, not {}", type_name(ty)),
2830                ))
2831            }
2832        };
2833        match kind {
2834            "scale" => {
2835                integers_only("scale")?;
2836                let v = keys["scale"];
2837                let scale = self.integer(v, "scale")?;
2838                if !(0..=38).contains(&scale) {
2839                    return Err(self.error(&v.span(), "scale: expected 0 to 38"));
2840                }
2841                Ok(Meaning::Scale(scale as u32))
2842            }
2843            "factor" => {
2844                if !ty.is_number() {
2845                    return Err(self.error(
2846                        &span,
2847                        format!(
2848                            "`factor` and `offset` are for numbers, not {}",
2849                            type_name(ty)
2850                        ),
2851                    ));
2852                }
2853                let factor = keys
2854                    .get("factor")
2855                    .map(|v| self.number(v, "factor"))
2856                    .transpose()?
2857                    .unwrap_or(1.0);
2858                let offset = keys
2859                    .get("offset")
2860                    .map(|v| self.number(v, "offset"))
2861                    .transpose()?
2862                    .unwrap_or(0.0);
2863                Ok(Meaning::Linear { factor, offset })
2864            }
2865            "enum" => {
2866                integers_only("enum")?;
2867                let v = keys["enum"];
2868                let table = self.table(v, "enum")?;
2869                let mut labels = BTreeMap::new();
2870                for (code, label) in table {
2871                    let text: &str = code.get_ref();
2872                    let code_value: i64 = text.parse().map_err(|_| {
2873                        self.error(
2874                            &code.span(),
2875                            format!("enum: `{text}` is not a whole number"),
2876                        )
2877                    })?;
2878                    labels.insert(code_value, self.string(label, "enum label")?);
2879                }
2880                Ok(Meaning::Enum(Arc::new(labels)))
2881            }
2882            _ => self.time_meaning(keys, ty, &span),
2883        }
2884    }
2885
2886    fn time_meaning(
2887        &self,
2888        keys: &BTreeMap<&str, &Value<'_>>,
2889        ty: Type,
2890        span: &Range<usize>,
2891    ) -> Result<Meaning, SpecError> {
2892        let unit = keys
2893            .get("time")
2894            .map(|v| match self.string(v, "time")?.as_str() {
2895                "days" => Ok(TimeUnitSpec::Days),
2896                "s" => Ok(TimeUnitSpec::Seconds),
2897                "ms" => Ok(TimeUnitSpec::Millis),
2898                "us" => Ok(TimeUnitSpec::Micros),
2899                "ns" => Ok(TimeUnitSpec::Nanos),
2900                _ => Err(self.error(&v.span(), "time: expected days, s, ms, us or ns")),
2901            })
2902            .transpose()?;
2903        let of_day = keys
2904            .get("of_day")
2905            .map(|v| self.boolean(v, "of_day"))
2906            .transpose()?
2907            .unwrap_or(false);
2908        let date = keys
2909            .get("date")
2910            .map(|v| Ok::<_, SpecError>((self.string(v, "date")?, v.span())))
2911            .transpose()?;
2912        if let Some(v) = keys.get("epoch")
2913            && (unit.is_none() || of_day)
2914        {
2915            return Err(self.error(&v.span(), "`epoch` goes with time, and not with of_day"));
2916        }
2917        match (unit, of_day, date) {
2918            (None, false, Some((date, at))) => {
2919                if date != "yyyymmdd" {
2920                    return Err(self.error(
2921                        &at,
2922                        "date: expected \"yyyymmdd\" (or, with of_day, a header field)",
2923                    ));
2924                }
2925                if !ty.is_integer() {
2926                    return Err(self.error(
2927                        &at,
2928                        format!("`date` is for integer types, not {}", type_name(ty)),
2929                    ));
2930                }
2931                Ok(Meaning::Yyyymmdd)
2932            }
2933            (None, _, _) => Err(self.error(
2934                span,
2935                "of_day goes with time = \"s\", \"ms\", \"us\" or \"ns\"",
2936            )),
2937            (Some(unit), true, date) => {
2938                if !ty.is_integer() {
2939                    return Err(self.error(
2940                        span,
2941                        format!("of_day is for integer types, not {}", type_name(ty)),
2942                    ));
2943                }
2944                if unit == TimeUnitSpec::Days {
2945                    return Err(self.error(span, "of_day counts s, ms, us or ns since midnight"));
2946                }
2947                let date = date
2948                    .map(|(date, at)| {
2949                        let field = date.strip_prefix("header.").ok_or_else(|| {
2950                            self.error(&at, "date: with of_day, expected a header field such as header.trade_date")
2951                        })?;
2952                        Ok::<_, SpecError>(field.to_string())
2953                    })
2954                    .transpose()?;
2955                Ok(Meaning::TimeOfDay { unit, date })
2956            }
2957            (Some(unit), false, date) => {
2958                if let Some((_, at)) = date {
2959                    return Err(self.error(&at, "date: goes with of_day, or alone as \"yyyymmdd\""));
2960                }
2961                if !ty.is_number() {
2962                    return Err(self.error(
2963                        span,
2964                        format!("`time` is for numbers, not {}", type_name(ty)),
2965                    ));
2966                }
2967                let epoch_ns = keys
2968                    .get("epoch")
2969                    .map(|e| self.epoch(e))
2970                    .transpose()?
2971                    .unwrap_or(0);
2972                Ok(Meaning::Time { unit, epoch_ns })
2973            }
2974        }
2975    }
2976
2977    /// An epoch: a TOML date or date-time, or one written as a string.
2978    fn epoch(&self, value: &Value<'_>) -> Result<i64, SpecError> {
2979        let text = match value.get_ref() {
2980            DeValue::Datetime(dt) => dt.to_string(),
2981            DeValue::String(s) => s.to_string(),
2982            _ => {
2983                return Err(self.error(&value.span(), "epoch: expected a date such as 2000-01-01"));
2984            }
2985        };
2986        parse_epoch(&text).ok_or_else(|| {
2987            self.error(
2988                &value.span(),
2989                "epoch: expected a date such as 2000-01-01 or a date-time such as 2000-01-01T00:00:00Z",
2990            )
2991        })
2992    }
2993}
2994
2995impl Field {
2996    /// A named field of `ty`, as stored.
2997    pub fn plain(name: &str, ty: Type, endian: Option<Endian>) -> Self {
2998        Self {
2999            name: Some(name.to_string()),
3000            ty,
3001            size: None,
3002            endian,
3003            meaning: Meaning::Plain,
3004            null: None,
3005            count: None,
3006            flatten: false,
3007            file: None,
3008            at: None,
3009            encoding: Encoding::Utf8,
3010            delta: Delta::None,
3011            bits: Vec::new(),
3012            group: Vec::new(),
3013            string_at: None,
3014            lookup: None,
3015            description: None,
3016            unit: None,
3017        }
3018    }
3019
3020    /// Whether the reader of fixed records reads it as it is: one value or an Array
3021    /// of them, of a size known before a record is read, and nothing derived.
3022    pub fn is_fixed_width(&self) -> bool {
3023        self.ty.width().is_some() || matches!(self.ty, Type::Str | Type::Bytes | Type::Pad)
3024    }
3025}
3026
3027/// Every field of the records: the common ones, then each variant's.
3028pub fn all_fields(records: &Records) -> impl Iterator<Item = &Field> {
3029    records
3030        .fields
3031        .iter()
3032        .chain(records.variants.iter().flat_map(|v| &v.fields))
3033}
3034
3035/// The parts `{name}` and `{name:%Y%m%d}` of a `[files]` path, in order.
3036pub fn path_parts(pattern: &str) -> Result<Vec<PathPart>, String> {
3037    let mut parts: Vec<PathPart> = Vec::new();
3038    let mut rest = pattern;
3039    if pattern.trim().is_empty()
3040        || Path::new(pattern).is_absolute()
3041        || pattern.split('/').any(|c| c == ".." || c.is_empty())
3042    {
3043        return Err(
3044            "expected a path relative to the directory, such as {date:%Y%m%d}/trades.bin".into(),
3045        );
3046    }
3047    while let Some(open) = rest.find('{') {
3048        let after = &rest[open + 1..];
3049        let close = after.find('}').ok_or("a `{` without its `}`")?;
3050        let inside = &after[..close];
3051        let (name, date) = match inside.split_once(':') {
3052            Some((name, format)) => (name, Some(format.to_string())),
3053            None => (inside, None),
3054        };
3055        if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
3056            return Err(format!(
3057                "`{{{inside}}}`: a part is named with letters, digits and `_`"
3058            ));
3059        }
3060        if inside.contains('/') {
3061            return Err(format!(
3062                "`{{{inside}}}`: a part stays inside one directory's name"
3063            ));
3064        }
3065        if let Some(format) = &date
3066            && (format.is_empty()
3067                || chrono::format::StrftimeItems::new(format)
3068                    .any(|i| matches!(i, chrono::format::Item::Error)))
3069        {
3070            return Err(format!(
3071                "`{{{inside}}}`: `{format}` is not a date format such as %Y%m%d"
3072            ));
3073        }
3074        if parts.iter().any(|p| p.name == name) {
3075            return Err(format!("`{name}` is named twice"));
3076        }
3077        parts.push(PathPart {
3078            name: name.to_string(),
3079            date,
3080        });
3081        rest = &after[close + 1..];
3082    }
3083    if rest.contains('}') {
3084        return Err("a `}` without its `{`".into());
3085    }
3086    Ok(parts)
3087}
3088
3089/// The columns a field shows as: none for `pad`, `name_0`... when flattened.
3090fn output_names(field: &Field) -> Vec<String> {
3091    let mut names = own_names(field);
3092    names.extend(field.bits.iter().map(|b| b.name.clone()));
3093    names
3094}
3095
3096/// The columns of a field's own value, without its bits: `name`, or `name_0`... when
3097/// flattened.
3098fn own_names(field: &Field) -> Vec<String> {
3099    let Some(name) = &field.name else {
3100        return Vec::new();
3101    };
3102    match (&field.count, field.flatten) {
3103        (Some(Amount::Given(n)), true) => (0..*n).map(|i| format!("{name}_{i}")).collect(),
3104        _ => vec![name.clone()],
3105    }
3106}
3107
3108/// The smallest and largest value an integer (or bool) type holds.
3109fn integer_range(ty: Type) -> Option<(i128, i128)> {
3110    let bits = match ty {
3111        Type::Unsigned(n) | Type::Signed(n) => u32::from(n) * 8,
3112        Type::Bool => 8,
3113        _ => return None,
3114    };
3115    Some(match ty {
3116        Type::Signed(_) => (-(1i128 << (bits - 1)), (1i128 << (bits - 1)) - 1),
3117        _ => (0, (1i128 << bits) - 1),
3118    })
3119}
3120
3121/// Whether `name` is one file's name, with no directory in it.
3122fn is_file_name(name: &str) -> bool {
3123    let mut parts = Path::new(name).components();
3124    matches!(
3125        (parts.next(), parts.next()),
3126        (Some(std::path::Component::Normal(_)), None)
3127    ) && !name.contains(['/', '\\'])
3128}
3129
3130/// A type name and the byte order its suffix asks for.
3131fn parse_type(text: &str) -> Option<(Type, Option<Endian>)> {
3132    match text {
3133        "str" => return Some((Type::Str, None)),
3134        "strz" => return Some((Type::Strz, None)),
3135        "vu" => return Some((Type::VarU, None)),
3136        "vs" => return Some((Type::VarS, None)),
3137        "bf2" => return Some((Type::BFloat16, None)),
3138        "bf2le" => return Some((Type::BFloat16, Some(Endian::Little))),
3139        "bf2be" => return Some((Type::BFloat16, Some(Endian::Big))),
3140        "bytes" => return Some((Type::Bytes, None)),
3141        "pad" => return Some((Type::Pad, None)),
3142        "bool" => return Some((Type::Bool, None)),
3143        _ => {}
3144    }
3145    let (base, endian) = if let Some(base) = text.strip_suffix("le") {
3146        (base, Some(Endian::Little))
3147    } else if let Some(base) = text.strip_suffix("be") {
3148        (base, Some(Endian::Big))
3149    } else {
3150        (text, None)
3151    };
3152    let mut chars = base.chars();
3153    let kind = chars.next()?;
3154    let digits = chars.as_str();
3155    if digits.len() != 1 {
3156        return None;
3157    }
3158    let width: u8 = digits.parse().ok()?;
3159    let ty = match (kind, width) {
3160        ('u', 1..=8) => Type::Unsigned(width),
3161        ('s', 1..=8) => Type::Signed(width),
3162        ('f', 2 | 4 | 8) => Type::Float(width),
3163        _ => return None,
3164    };
3165    Some((ty, endian))
3166}
3167
3168fn type_name(ty: Type) -> String {
3169    match ty {
3170        Type::Unsigned(n) => format!("u{n}"),
3171        Type::Signed(n) => format!("s{n}"),
3172        Type::Float(n) => format!("f{n}"),
3173        Type::Bool => "bool".into(),
3174        Type::BFloat16 => "bf2".into(),
3175        Type::Str => "str".into(),
3176        Type::Strz => "strz".into(),
3177        Type::Bytes => "bytes".into(),
3178        Type::Pad => "pad".into(),
3179        Type::VarU => "vu".into(),
3180        Type::VarS => "vs".into(),
3181        Type::Group => "group".into(),
3182    }
3183}
3184
3185/// Nanoseconds from the Unix epoch to `text`: a date, or an RFC 3339 date-time (a
3186/// date-time without an offset is UTC).
3187fn parse_epoch(text: &str) -> Option<i64> {
3188    use chrono::{DateTime, NaiveDate, NaiveDateTime};
3189    let at = if let Ok(dt) = DateTime::parse_from_rfc3339(text) {
3190        dt.naive_utc()
3191    } else if let Ok(dt) = NaiveDateTime::parse_from_str(text, "%Y-%m-%dT%H:%M:%S%.f") {
3192        dt
3193    } else if let Ok(dt) = NaiveDateTime::parse_from_str(text, "%Y-%m-%d %H:%M:%S%.f") {
3194        dt
3195    } else {
3196        NaiveDate::parse_from_str(text, "%Y-%m-%d")
3197            .ok()?
3198            .and_hms_opt(0, 0, 0)?
3199    };
3200    at.and_utc().timestamp_nanos_opt()
3201}
3202
3203/// What a spec name may be: namespaced, as `acme.l2feed` is, so it cannot be taken for
3204/// a built-in format.
3205pub fn is_spec_name(name: &str) -> bool {
3206    name.contains('.')
3207        && !name.starts_with('.')
3208        && !name.ends_with('.')
3209        && name
3210            .chars()
3211            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
3212}
3213
3214impl Spec {
3215    /// Read a spec from its text. `path` names it in errors.
3216    pub fn parse(text: &str, path: Option<&Path>) -> Result<Self, SpecError> {
3217        let reader = Reader { text, path };
3218        let document = DeTable::parse(text).map_err(|e| {
3219            let (line, column) = e
3220                .span()
3221                .map_or((0, 0), |span| line_column(text, span.start));
3222            SpecError {
3223                path: path.map(Path::to_path_buf),
3224                line,
3225                column,
3226                message: e.message().to_string(),
3227            }
3228        })?;
3229        let kind = document
3230            .get_ref()
3231            .iter()
3232            .find(|(key, _)| {
3233                let key: &str = key.get_ref();
3234                key == "kind"
3235            })
3236            .map(|(_, v)| v);
3237        if let Some(v) = kind {
3238            match reader.string(v, "kind")?.as_str() {
3239                "binary" => {}
3240                "delimited" => return Self::parse_delimited(&reader, document.get_ref(), path),
3241                _ => return Err(reader.error(&v.span(), "kind: expected binary or delimited")),
3242            }
3243        }
3244        let top = reader.entries(
3245            document.get_ref(),
3246            "the spec",
3247            &[
3248                "name",
3249                "description",
3250                "documentation",
3251                "kind",
3252                "match",
3253                "endian",
3254                "layout",
3255                "header",
3256                "records",
3257                "footer",
3258                "variants",
3259                "blocks",
3260                "capture",
3261                "files",
3262                "sections",
3263            ],
3264        )?;
3265        let whole = 0..0;
3266        let name = reader.spec_name(&top)?;
3267        let description = top
3268            .get("description")
3269            .map(|v| reader.prose(v, "description"))
3270            .transpose()?;
3271        let documentation = top
3272            .get("documentation")
3273            .map(|v| reader.documentation(v))
3274            .transpose()?;
3275        let (endian, endian_auto) = match top.get("endian") {
3276            None => (Endian::Little, false),
3277            Some(v) => match reader.string(v, "endian")?.as_str() {
3278                "le" => (Endian::Little, false),
3279                "be" => (Endian::Big, false),
3280                "auto" => (Endian::Little, true),
3281                _ => return Err(reader.error(&v.span(), "endian: expected le, be or auto")),
3282            },
3283        };
3284        let layout = match top.get("layout") {
3285            None => Layout::Rows,
3286            Some(v) => match reader.string(v, "layout")?.as_str() {
3287                "rows" => Layout::Rows,
3288                "columns" => Layout::Columns,
3289                _ => return Err(reader.error(&v.span(), "layout: expected rows or columns")),
3290            },
3291        };
3292
3293        let mut header = Header::default();
3294        if let Some(v) = top.get("header") {
3295            let table = reader.table(v, "[header]")?;
3296            let keys = reader.entries(table, "[header]", &["fields", "size", "size_adjust"])?;
3297            if let Some(f) = keys.get("fields") {
3298                header.fields = reader.fields(f, Part::Header, layout, &Scopes::default(), &[])?;
3299            }
3300            if let Some(s) = keys.get("size") {
3301                let scopes = Scopes {
3302                    header: &header.fields,
3303                    footer: &[],
3304                };
3305                header.size = Some(reader.amount(
3306                    s,
3307                    keys.get("size_adjust").copied(),
3308                    "size",
3309                    Part::Header,
3310                    &header.fields,
3311                    &scopes,
3312                )?);
3313            }
3314        }
3315        let footer = top
3316            .get("footer")
3317            .map(|v| reader.footer(v, &header.fields))
3318            .transpose()?;
3319        let footer_fields: &[Field] = footer.as_ref().map_or(&[], |f| &f.fields);
3320        let scopes = Scopes {
3321            header: &header.fields,
3322            footer: footer_fields,
3323        };
3324
3325        let MatchRules {
3326            globs,
3327            glob_set,
3328            magic,
3329            magic_offset,
3330            expect,
3331        } = match top.get("match") {
3332            Some(v) => reader.match_rules(v, Some(&header.fields))?,
3333            None => MatchRules::default(),
3334        };
3335
3336        if endian_auto && magic.len() < 2 {
3337            return Err(reader.error(
3338                &top["endian"].span(),
3339                "endian = \"auto\" reads the byte order from the magic, so it needs a magic of at least two bytes",
3340            ));
3341        }
3342
3343        let sections = top
3344            .get("sections")
3345            .map(|v| reader.sections(v, &scopes))
3346            .transpose()?
3347            .unwrap_or_default();
3348
3349        let Some(records_value) = top.get("records") else {
3350            return Err(reader.error(&whole, "missing [records], with the fields of one record"));
3351        };
3352        let records =
3353            reader.records(records_value, top.get("variants").copied(), layout, &scopes)?;
3354        for field in all_fields(&records) {
3355            if let Some(section) = &field.string_at
3356                && !sections.iter().any(|s| &s.name == section)
3357            {
3358                return Err(reader.error(
3359                    &records_value.span(),
3360                    format!("string_at: no section named `{section}` under [sections]"),
3361                ));
3362            }
3363        }
3364        let blocks = top
3365            .get("blocks")
3366            .map(|v| reader.blocks(v, &scopes))
3367            .transpose()?;
3368        let capture = top.get("capture").map(|v| reader.capture(v)).transpose()?;
3369        let files = top.get("files").map(|v| reader.files(v)).transpose()?;
3370
3371        if let Some(v) = top.get("capture")
3372            && (blocks.is_some() || top.contains_key("header") || footer.is_some())
3373        {
3374            return Err(reader.error(
3375                &v.span(),
3376                "[capture]: the capture's own headers frame the payloads; leave out [header], [footer] and [blocks]",
3377            ));
3378        }
3379        let framed = blocks.is_some() || capture.is_some();
3380        if layout == Layout::Columns
3381            && let Some(v) = top.get("blocks").or(top.get("files"))
3382        {
3383            return Err(reader.error(
3384                &v.span(),
3385                "layout = \"columns\" reads values, not blocks or a tree of files",
3386            ));
3387        }
3388        if let Some(ring) = &records.ring {
3389            let ring_ok = records.framing == Framing::Fixed
3390                && !framed
3391                && records.variants.is_empty()
3392                && matches!(
3393                    ring,
3394                    Amount::Header { .. } | Amount::Footer { .. } | Amount::Given(_)
3395                );
3396            if !ring_ok {
3397                return Err(reader.error(
3398                    &records_value.span(),
3399                    "ring: is for fixed records, the oldest's index read from the header",
3400                ));
3401            }
3402        }
3403        if let Some(files) = &files {
3404            let columns: std::collections::HashSet<String> =
3405                all_fields(&records).flat_map(output_names).collect();
3406            if let Some(part) = files.parts.iter().find(|p| columns.contains(&p.name)) {
3407                return Err(reader.error(
3408                    &top["files"].span(),
3409                    format!("path: `{}` is also a field's name", part.name),
3410                ));
3411            }
3412        }
3413
3414        if layout == Layout::Columns {
3415            reader.check_columns(records_value, &records)?;
3416            if let Some(v) = top
3417                .get("footer")
3418                .or(top.get("variants"))
3419                .or(top.get("capture"))
3420            {
3421                return Err(reader.error(
3422                    &v.span(),
3423                    "layout = \"columns\" holds fixed values: no footer, variants or capture",
3424                ));
3425            }
3426        }
3427        // Sizes written down are checked now; one that comes from the file is checked
3428        // when the file is read.
3429        if let (Some(Amount::Given(size)), Some(sum)) =
3430            (&records.size, given_width(&records.fields))
3431            && *size < sum
3432            && records.variants.is_empty()
3433        {
3434            return Err(reader.error(
3435                &records_value.span(),
3436                format!("size: the fields take {sum} bytes, more than {size}"),
3437            ));
3438        }
3439        if let (Some(Amount::Given(size)), Some(sum)) = (&header.size, given_width(&header.fields))
3440            && *size < sum
3441        {
3442            return Err(reader.error(
3443                &records_value.span(),
3444                format!("[header] size: the fields take {sum} bytes, more than {size}"),
3445            ));
3446        }
3447        if let Some(sum) = given_width(&records.fields) {
3448            if sum == 0 && records.variants.is_empty() && records.sync.is_empty() {
3449                return Err(reader.error(&records_value.span(), "fields: a record takes no bytes"));
3450            }
3451            if sum > MAX_SIZE {
3452                return Err(reader.error(
3453                    &records_value.span(),
3454                    format!("fields: a record of {sum} bytes is more than {MAX_SIZE}"),
3455                ));
3456            }
3457        }
3458        Ok(Self {
3459            name,
3460            description,
3461            documentation,
3462            notes: Vec::new(),
3463            path: path.map(Path::to_path_buf),
3464            globs,
3465            glob_set,
3466            magic,
3467            magic_offset,
3468            expect,
3469            endian,
3470            endian_auto,
3471            layout,
3472            header,
3473            records,
3474            delimited: None,
3475            footer,
3476            blocks,
3477            capture,
3478            files,
3479            sections,
3480            variant: None,
3481        })
3482    }
3483
3484    /// A `kind = "delimited"` spec: a CSV-like file's reading options, with no
3485    /// header or record fields.
3486    fn parse_delimited(
3487        reader: &Reader<'_>,
3488        document: &DeTable<'_>,
3489        path: Option<&Path>,
3490    ) -> Result<Self, SpecError> {
3491        let keys = delimited_spec_keys();
3492        let top = reader.entries(document, "a delimited spec", &keys)?;
3493        let name = reader.spec_name(&top)?;
3494        let description = top
3495            .get("description")
3496            .map(|v| reader.prose(v, "description"))
3497            .transpose()?;
3498        let documentation = top
3499            .get("documentation")
3500            .map(|v| reader.documentation(v))
3501            .transpose()?;
3502        let MatchRules {
3503            globs,
3504            glob_set,
3505            magic,
3506            magic_offset,
3507            expect,
3508        } = match top.get("match") {
3509            Some(v) => reader.match_rules(v, None)?,
3510            None => MatchRules::default(),
3511        };
3512        let (delimited, notes) = reader.delimited(&top)?;
3513        Ok(Self {
3514            name,
3515            description,
3516            documentation,
3517            notes,
3518            path: path.map(Path::to_path_buf),
3519            globs,
3520            glob_set,
3521            magic,
3522            magic_offset,
3523            expect,
3524            endian: Endian::Little,
3525            endian_auto: false,
3526            layout: Layout::Rows,
3527            header: Header::default(),
3528            records: Records::default(),
3529            delimited: Some(Arc::new(delimited)),
3530            footer: None,
3531            blocks: None,
3532            capture: None,
3533            files: None,
3534            sections: Vec::new(),
3535            variant: None,
3536        })
3537    }
3538
3539    /// Whether the spec is `kind = "delimited"`.
3540    pub fn is_delimited(&self) -> bool {
3541        self.delimited.is_some()
3542    }
3543
3544    /// Read the spec in `path`, which may be no more than [`MAX_SPEC_BYTES`].
3545    pub fn load(path: &Path) -> Result<Self, SpecError> {
3546        use std::io::Read;
3547        let mut bytes = Vec::new();
3548        std::fs::File::open(path)
3549            .and_then(|f| f.take(MAX_SPEC_BYTES + 1).read_to_end(&mut bytes))
3550            .map_err(|e| SpecError {
3551                path: Some(path.to_path_buf()),
3552                line: 0,
3553                column: 0,
3554                message: format!(
3555                    "could not read it. {}",
3556                    crate::error_display::user_message_from_io(&e, None)
3557                ),
3558            })?;
3559        Self::from_bytes(&bytes, path)
3560    }
3561
3562    /// The spec in `bytes`, read from `from` (a file or a URL), refused past
3563    /// [`MAX_SPEC_BYTES`].
3564    pub fn from_bytes(bytes: &[u8], from: &Path) -> Result<Self, SpecError> {
3565        let refused = |message: String| SpecError {
3566            path: Some(from.to_path_buf()),
3567            line: 0,
3568            column: 0,
3569            message,
3570        };
3571        if bytes.len() as u64 > MAX_SPEC_BYTES {
3572            return Err(refused(format!("a format spec is at most {MAX_SPEC_SAID}")));
3573        }
3574        let text = std::str::from_utf8(bytes)
3575            .map_err(|_| refused("not UTF-8 text, which a format spec is".to_string()))?;
3576        Self::parse(text, Some(from))
3577    }
3578
3579    /// Whether `path`'s name matches one of the spec's globs. A glob with a `/` is
3580    /// matched against the whole path, one without against the name.
3581    pub fn glob_matches(&self, path: &Path) -> bool {
3582        let Some(set) = &self.glob_set else {
3583            return false;
3584        };
3585        let name = path.file_name().map(Path::new);
3586        name.is_some_and(|n| set.is_match(n)) || set.is_match(path)
3587    }
3588
3589    /// Whether `head`, the first bytes of a file, carries the spec's magic. A delimited
3590    /// spec's magic is the start of the first line, after any byte-order mark.
3591    pub fn magic_matches(&self, head: &[u8]) -> bool {
3592        let head = if self.is_delimited() {
3593            head.strip_prefix(UTF8_BOM).unwrap_or(head)
3594        } else {
3595            head
3596        };
3597        let start = self.magic_offset as usize;
3598        let found = head.get(start..start + self.magic.len());
3599        !self.magic.is_empty()
3600            && (found == Some(&self.magic)
3601                || (self.endian_auto
3602                    && found.is_some_and(|f| f.iter().eq(self.magic.iter().rev()))))
3603    }
3604
3605    /// Whether `head` holds the header values `match.where` asks for.
3606    pub fn header_matches(&self, head: &[u8]) -> bool {
3607        if self.expect.is_empty() {
3608            return true;
3609        }
3610        let Ok(header) = read_header(self, head) else {
3611            return false;
3612        };
3613        self.expect.iter().all(|(field, wanted)| match wanted {
3614            Expected::Int(v) => header.int(field) == Some(*v),
3615            Expected::Text(v) => header.text(field).as_deref() == Some(v.as_str()),
3616        })
3617    }
3618
3619    /// Bytes from the front of a file that settle the spec's magic and `where`.
3620    pub fn match_reach(&self) -> u64 {
3621        let magic = if self.magic.is_empty() {
3622            0
3623        } else {
3624            let bom = if self.is_delimited() {
3625                UTF8_BOM.len() as u64
3626            } else {
3627                0
3628            };
3629            bom + self.magic_offset + self.magic.len() as u64
3630        };
3631        let header = if self.expect.is_empty() {
3632            0
3633        } else {
3634            given_width(&self.header.fields).unwrap_or(MAX_MATCH_READ)
3635        };
3636        magic.max(header).min(MAX_MATCH_READ)
3637    }
3638
3639    /// What the spec says files of it look like, one chip per condition: its magic,
3640    /// its header values, then its globs. Empty when only `--format` picks it.
3641    pub fn match_chips(&self) -> Vec<MatchChip> {
3642        let mut chips = Vec::new();
3643        if !self.magic.is_empty() {
3644            let ellipsis = crate::glyphs::get().ellipsis;
3645            let (value, kind) = if self.magic.iter().all(|b| b.is_ascii_graphic()) {
3646                let text = String::from_utf8_lossy(&self.magic);
3647                let value = if text.chars().count() > CHIP_MAGIC_CHARS {
3648                    let head: String = text.chars().take(CHIP_MAGIC_CHARS).collect();
3649                    format!("{head}{ellipsis}")
3650                } else {
3651                    text.into_owned()
3652                };
3653                (value, ChipKind::Magic)
3654            } else if self.magic.len() > CHIP_MAGIC_BYTES {
3655                let head = crate::fixed_records::hex(&self.magic[..CHIP_MAGIC_BYTES]);
3656                (format!("{head} {ellipsis}"), ChipKind::Hex)
3657            } else {
3658                (crate::fixed_records::hex(&self.magic), ChipKind::Hex)
3659            };
3660            chips.push(MatchChip {
3661                name: "magic".to_string(),
3662                value,
3663                kind,
3664                offset: (self.magic_offset > 0).then_some(self.magic_offset),
3665            });
3666        }
3667        for (field, wanted) in &self.expect {
3668            let (value, kind) = match wanted {
3669                Expected::Int(v) => (v.to_string(), ChipKind::Int),
3670                Expected::Text(v) => (v.clone(), ChipKind::Text),
3671            };
3672            chips.push(MatchChip {
3673                name: field.clone(),
3674                value,
3675                kind,
3676                offset: None,
3677            });
3678        }
3679        if !self.globs.is_empty() {
3680            chips.push(MatchChip {
3681                name: "glob".to_string(),
3682                value: self.globs.join(" "),
3683                kind: ChipKind::Glob,
3684                offset: None,
3685            });
3686        }
3687        chips
3688    }
3689
3690    /// The chips that named `path` on the home screen: the glob alone when it names the
3691    /// file, since a listing names a file by its glob without reading its header; else
3692    /// the magic and the header values it was checked against. All of them when
3693    /// neither does.
3694    pub fn match_chips_for(&self, path: &Path) -> Vec<MatchChip> {
3695        if self.glob_matches(path) {
3696            let mut chips = self.match_chips();
3697            chips.retain(|c| c.kind == ChipKind::Glob);
3698            return chips;
3699        }
3700        let by = (!self.magic.is_empty()).then_some(ChipKind::Magic);
3701        self.match_chips_by(by)
3702    }
3703
3704    /// The chips of the rule that chose the spec: `Chosen::Glob` or `Chosen::Magic`
3705    /// leave the other out; any other choice keeps every chip.
3706    pub fn match_chips_chosen(&self, by: Chosen) -> Vec<MatchChip> {
3707        match by {
3708            Chosen::Glob => self.match_chips_by(Some(ChipKind::Glob)),
3709            Chosen::Magic => self.match_chips_by(Some(ChipKind::Magic)),
3710            Chosen::SpecFile | Chosen::Named => self.match_chips(),
3711        }
3712    }
3713
3714    fn match_chips_by(&self, by: Option<ChipKind>) -> Vec<MatchChip> {
3715        let mut chips = self.match_chips();
3716        match by {
3717            Some(ChipKind::Glob) => {
3718                chips.retain(|c| !matches!(c.kind, ChipKind::Magic | ChipKind::Hex));
3719            }
3720            Some(_) => chips.retain(|c| c.kind != ChipKind::Glob),
3721            None => {}
3722        }
3723        chips
3724    }
3725
3726    /// Whether the spec reads a file's records as several variants, each listed as a
3727    /// table inside the file.
3728    pub fn lists_variants(&self) -> bool {
3729        !self.is_delimited() && self.records.variants.len() > 1 && self.variant.is_none()
3730    }
3731
3732    /// The columns a file of the spec opens with, when the spec alone says them: fixed
3733    /// records of one file whose fields take nothing from the file (no sizes, symbols or
3734    /// dates from its header). `None` when the open has to read the file to know.
3735    pub fn static_columns(&self) -> Option<Vec<(String, polars::prelude::DataType)>> {
3736        if self.is_delimited() || self.layout != Layout::Rows || crate::framed_records::needed(self)
3737        {
3738            return None;
3739        }
3740        let (columns, _) = self
3741            .record_columns(&HeaderValues::default(), 0, Some(1))
3742            .ok()?;
3743        Some(
3744            columns
3745                .iter()
3746                .map(|c| (c.name.to_string(), c.dtype()))
3747                .collect(),
3748        )
3749    }
3750}
3751
3752/// What a spec says of its files, for the Documentation view: its own words and the
3753/// notes its fields carry. None of it changes how a file is read.
3754#[derive(Debug, Clone, Default, PartialEq)]
3755pub struct SpecDocs {
3756    /// The spec's name.
3757    pub spec: String,
3758    /// The file the spec was read from; none for one built in or parsed from text.
3759    pub file: Option<PathBuf>,
3760    pub description: String,
3761    /// An `https://` link to the format's own documentation.
3762    pub documentation: String,
3763    /// The variants, each a record type the file holds.
3764    pub record_types: Vec<RecordType>,
3765    /// What each column means, by its name, in the spec's order: a field's description,
3766    /// its unit, and its enum as the value legend.
3767    pub columns: Vec<(String, ColumnNote)>,
3768    /// The `[header]` fields that say what they hold, in the spec's order.
3769    pub header: Vec<(String, ColumnNote)>,
3770    /// The `[footer]` fields that say what they hold, in the spec's order.
3771    pub footer: Vec<(String, ColumnNote)>,
3772}
3773
3774/// A field's description and unit, when it gives either.
3775fn field_note(field: &Field) -> Option<(String, ColumnNote)> {
3776    let name = field.name.clone()?;
3777    let note = ColumnNote {
3778        description: field.description.clone().unwrap_or_default(),
3779        unit: field.unit.clone().unwrap_or_default(),
3780        values: Vec::new(),
3781        ty: String::new(),
3782    };
3783    (note != ColumnNote::default()).then_some((name, note))
3784}
3785
3786/// The condition on the type field that picks a variant: `msg_type = 1`, or
3787/// `kind in ("E", "C")`, text quoted.
3788fn picked_by(type_field: Option<&str>, when: &[Expected]) -> String {
3789    let field = type_field.unwrap_or("type");
3790    let values: Vec<String> = when
3791        .iter()
3792        .map(|value| match value {
3793            Expected::Int(v) => v.to_string(),
3794            Expected::Text(v) => format!("\"{v}\""),
3795        })
3796        .collect();
3797    match values.as_slice() {
3798        [one] => format!("{field} = {one}"),
3799        _ => format!("{field} in ({})", values.join(", ")),
3800    }
3801}
3802
3803/// One variant of a spec, as its documentation shows it.
3804#[derive(Debug, Clone, Default, PartialEq)]
3805pub struct RecordType {
3806    pub name: String,
3807    /// What picks it, as a condition on the type field: `msg_type = 1`, or
3808    /// `kind in ("E", "C")` for several values.
3809    pub picked_by: String,
3810    pub description: String,
3811    /// Its columns: the common ones and its own.
3812    pub columns: usize,
3813}
3814
3815impl Spec {
3816    /// What the spec documents, or `None` when it says nothing beyond how to read: no
3817    /// description or link, no record type described, no column noted.
3818    pub fn docs(&self) -> Option<SpecDocs> {
3819        let mut columns: Vec<(String, ColumnNote)> = Vec::new();
3820        // A name two variants share is one column: the first note of it stands.
3821        let mut add = |name: &str, note: ColumnNote| {
3822            if note != ColumnNote::default() && !columns.iter().any(|(n, _)| n == name) {
3823                columns.push((name.to_string(), note));
3824            }
3825        };
3826        let legend = |labels: &BTreeMap<i64, String>| {
3827            labels
3828                .iter()
3829                .map(|(code, label)| (code.to_string(), label.clone()))
3830                .collect::<Vec<_>>()
3831        };
3832        for field in all_fields(&self.records) {
3833            let note = ColumnNote {
3834                description: field.description.clone().unwrap_or_default(),
3835                unit: field.unit.clone().unwrap_or_default(),
3836                values: match &field.meaning {
3837                    Meaning::Enum(labels) => legend(labels),
3838                    _ => Vec::new(),
3839                },
3840                ty: String::new(),
3841            };
3842            // A flattened field is filed under each column it makes.
3843            for name in own_names(field) {
3844                add(&name, note.clone());
3845            }
3846            for bit in &field.bits {
3847                if let Some(labels) = &bit.labels {
3848                    add(
3849                        &bit.name,
3850                        ColumnNote {
3851                            values: legend(labels),
3852                            ..ColumnNote::default()
3853                        },
3854                    );
3855                }
3856            }
3857        }
3858        for (name, note) in &self.notes {
3859            add(name, note.clone());
3860        }
3861        let tables = crate::members::variant_tables(self);
3862        let record_types: Vec<RecordType> = self
3863            .records
3864            .variants
3865            .iter()
3866            .zip(&tables)
3867            .map(|(variant, table)| RecordType {
3868                name: variant.name.clone(),
3869                picked_by: picked_by(self.records.type_field.as_deref(), &variant.when),
3870                description: variant.description.clone().unwrap_or_default(),
3871                columns: table.columns.len(),
3872            })
3873            .collect();
3874        let header: Vec<(String, ColumnNote)> =
3875            self.header.fields.iter().filter_map(field_note).collect();
3876        let footer: Vec<(String, ColumnNote)> = self
3877            .footer
3878            .iter()
3879            .flat_map(|f| &f.fields)
3880            .filter_map(field_note)
3881            .collect();
3882        let documented = self.description.is_some()
3883            || self.documentation.is_some()
3884            || !columns.is_empty()
3885            || !header.is_empty()
3886            || !footer.is_empty()
3887            || record_types.iter().any(|r| !r.description.is_empty());
3888        documented.then(|| SpecDocs {
3889            spec: self.name.clone(),
3890            file: self.path.clone(),
3891            description: self.description.clone().unwrap_or_default(),
3892            documentation: self.documentation.clone().unwrap_or_default(),
3893            record_types,
3894            columns,
3895            header,
3896            footer,
3897        })
3898    }
3899}
3900
3901/// Bytes one field takes in each record, when nothing about it comes from the file.
3902fn field_width(field: &Field) -> Option<u64> {
3903    let width = match (&field.size, field.ty.width()) {
3904        (_, Some(w)) => w,
3905        (Some(Amount::Given(n)), None) => *n,
3906        _ => return None,
3907    };
3908    let count = match &field.count {
3909        None => 1,
3910        Some(Amount::Given(n)) => *n,
3911        Some(_) => return None,
3912    };
3913    width.checked_mul(count)
3914}
3915
3916/// The bytes the fields take, when none of their sizes comes from the file.
3917fn given_width(fields: &[Field]) -> Option<u64> {
3918    fields
3919        .iter()
3920        .try_fold(0u64, |sum, f| sum.checked_add(field_width(f)?))
3921}
3922
3923/// The bytes `fields` take, when none of their sizes comes from the file.
3924pub(crate) fn fields_width(fields: &[Field]) -> Option<u64> {
3925    given_width(fields)
3926}
3927
3928/// A header, read: each named field's value, and how many bytes the header takes;
3929/// and the footer's values, and the symbol lists fields index into.
3930#[derive(Debug, Default, Clone)]
3931pub struct HeaderValues {
3932    pub values: Vec<(String, AnyValue<'static>)>,
3933    pub size: u64,
3934    pub footer: Vec<(String, AnyValue<'static>)>,
3935    pub lookups: BTreeMap<String, Arc<Vec<String>>>,
3936}
3937
3938fn int_value(value: &AnyValue<'_>) -> Option<i128> {
3939    match value {
3940        AnyValue::UInt8(v) => Some(i128::from(*v)),
3941        AnyValue::UInt16(v) => Some(i128::from(*v)),
3942        AnyValue::UInt32(v) => Some(i128::from(*v)),
3943        AnyValue::UInt64(v) => Some(i128::from(*v)),
3944        AnyValue::Int8(v) => Some(i128::from(*v)),
3945        AnyValue::Int16(v) => Some(i128::from(*v)),
3946        AnyValue::Int32(v) => Some(i128::from(*v)),
3947        AnyValue::Int64(v) => Some(i128::from(*v)),
3948        _ => None,
3949    }
3950}
3951
3952impl HeaderValues {
3953    fn get(&self, name: &str) -> Option<&AnyValue<'static>> {
3954        self.values.iter().find(|(n, _)| n == name).map(|(_, v)| v)
3955    }
3956
3957    fn footer_int(&self, name: &str) -> Option<i128> {
3958        self.footer
3959            .iter()
3960            .find(|(n, _)| n == name)
3961            .and_then(|(_, v)| int_value(v))
3962    }
3963
3964    /// `amount`'s value with no bound but its sign: an offset or a count, which the
3965    /// file's length bounds where it is used.
3966    pub(crate) fn resolve_any(&self, amount: &Amount, what: &str) -> Result<u64, String> {
3967        let (value, field) = match amount {
3968            Amount::Given(n) => return Ok(*n),
3969            Amount::Header { field, adjust } => (
3970                self.int(field).map(|v| v + i128::from(*adjust)),
3971                format!("header's `{field}`"),
3972            ),
3973            Amount::Footer { field, adjust } => (
3974                self.footer_int(field).map(|v| v + i128::from(*adjust)),
3975                format!("footer's `{field}`"),
3976            ),
3977            Amount::Record { .. } | Amount::Rest => {
3978                return Err(format!("{what}: comes from each record"));
3979            }
3980        };
3981        let value = value.ok_or_else(|| format!("{what}: the {field} has no value"))?;
3982        u64::try_from(value).map_err(|_| format!("{what}: the {field} gives {value}, below 0"))
3983    }
3984
3985    fn int(&self, name: &str) -> Option<i128> {
3986        int_value(self.get(name)?)
3987    }
3988
3989    fn text(&self, name: &str) -> Option<String> {
3990        match self.get(name)? {
3991            AnyValue::String(s) => Some(s.to_string()),
3992            AnyValue::StringOwned(s) => Some(s.to_string()),
3993            _ => None,
3994        }
3995    }
3996
3997    /// Nanoseconds from the Unix epoch to midnight of the date header field `name`
3998    /// holds: a date, a datetime, or text such as 2024-01-02 or 20240102.
3999    fn midnight_ns(&self, name: &str) -> Option<i64> {
4000        let days = match self.get(name)? {
4001            AnyValue::Date(days) => i64::from(*days),
4002            AnyValue::Datetime(v, unit, _) | AnyValue::DatetimeOwned(v, unit, _) => {
4003                let per_day = DAY_NS
4004                    / match unit {
4005                        TimeUnit::Nanoseconds => 1,
4006                        TimeUnit::Microseconds => 1_000,
4007                        TimeUnit::Milliseconds => 1_000_000,
4008                    };
4009                v.div_euclid(per_day)
4010            }
4011            other => {
4012                let text = match other {
4013                    AnyValue::String(s) => s.to_string(),
4014                    AnyValue::StringOwned(s) => s.to_string(),
4015                    _ => return None,
4016                };
4017                let date = chrono::NaiveDate::parse_from_str(&text, "%Y-%m-%d")
4018                    .or_else(|_| chrono::NaiveDate::parse_from_str(&text, "%Y%m%d"))
4019                    .ok()?;
4020                (date - chrono::NaiveDate::from_ymd_opt(1970, 1, 1)?).num_days()
4021            }
4022        };
4023        days.checked_mul(DAY_NS)
4024    }
4025
4026    /// `amount`'s value: given, or read from a header or footer field and bounded.
4027    pub(crate) fn resolve(&self, amount: &Amount, what: &str) -> Result<u64, String> {
4028        match amount {
4029            Amount::Given(n) => Ok(*n),
4030            Amount::Record { .. } | Amount::Rest => Err(format!(
4031                "{what}: comes from each record, which needs the records walked"
4032            )),
4033            Amount::Header { field, adjust } | Amount::Footer { field, adjust } => {
4034                let (part, value) = match amount {
4035                    Amount::Footer { .. } => ("footer", self.footer_int(field)),
4036                    _ => ("header", self.int(field)),
4037                };
4038                let value = value
4039                    .ok_or_else(|| format!("{what}: the {part} has no value for `{field}`"))?
4040                    + i128::from(*adjust);
4041                // A record count is bounded by the file instead.
4042                let bound = if what == "count" {
4043                    i128::from(u64::MAX)
4044                } else {
4045                    i128::from(MAX_SIZE)
4046                };
4047                if !(0..=bound).contains(&value) {
4048                    return Err(format!(
4049                        "{what}: the header's `{field}` gives {value}, outside 0 to {bound}"
4050                    ));
4051                }
4052                Ok(value as u64)
4053            }
4054        }
4055    }
4056}
4057
4058/// Where a column's cells are: the first at `start`, `stride` apart (a cell's own
4059/// width when `None`), each `count` values of `width` bytes.
4060#[derive(Debug, Clone, Copy)]
4061pub(crate) struct Place {
4062    pub start: usize,
4063    pub stride: Option<usize>,
4064    pub width: usize,
4065    pub count: usize,
4066}
4067
4068/// The decoder's view of `field`, named `name`, its cells at `place`.
4069pub(crate) fn layout_of(
4070    spec: &Spec,
4071    field: &Field,
4072    name: &str,
4073    place: Place,
4074    header: &HeaderValues,
4075) -> Result<ColumnLayout, String> {
4076    let Place {
4077        start,
4078        stride,
4079        width,
4080        count,
4081    } = place;
4082    let logical = match &field.meaning {
4083        Meaning::Plain if field.lookup.is_some() => Logical::Lookup(
4084            field
4085                .name
4086                .as_ref()
4087                .and_then(|n| header.lookups.get(n))
4088                .cloned()
4089                .ok_or_else(|| format!("{name}: its symbol list was not read"))?,
4090        ),
4091        Meaning::Plain => Logical::Plain,
4092        Meaning::Scale(scale) => Logical::Decimal {
4093            scale: *scale as usize,
4094        },
4095        Meaning::Linear { factor, offset } => Logical::Linear {
4096            factor: *factor,
4097            offset: *offset,
4098        },
4099        Meaning::Enum(labels) => Logical::Enum(labels.clone()),
4100        Meaning::Yyyymmdd => Logical::Yyyymmdd,
4101        Meaning::Time { unit, epoch_ns } => match (field.ty, unit) {
4102            (Type::Float(_), unit) => Logical::FloatTimestamp {
4103                ns_per_unit: unit.nanos() as f64,
4104                epoch_ns: *epoch_ns,
4105            },
4106            (_, TimeUnitSpec::Days) => Logical::Days {
4107                epoch_days: i32::try_from(epoch_ns.div_euclid(DAY_NS))
4108                    .map_err(|_| format!("{name}: the epoch is out of range"))?,
4109            },
4110            (_, unit) => {
4111                let (unit, multiplier, per) = match unit {
4112                    TimeUnitSpec::Seconds => (TimeUnit::Milliseconds, 1000, 1_000_000),
4113                    TimeUnitSpec::Millis => (TimeUnit::Milliseconds, 1, 1_000_000),
4114                    TimeUnitSpec::Micros => (TimeUnit::Microseconds, 1, 1_000),
4115                    _ => (TimeUnit::Nanoseconds, 1, 1),
4116                };
4117                Logical::Timestamp {
4118                    unit,
4119                    multiplier,
4120                    epoch: epoch_ns / per,
4121                }
4122            }
4123        },
4124        Meaning::TimeOfDay { unit, date } => Logical::TimeOfDay {
4125            ns_per_unit: unit.nanos(),
4126            date_ns: match date {
4127                None => None,
4128                Some(field) => Some(
4129                    header
4130                        .midnight_ns(field)
4131                        .ok_or_else(|| format!("{name}: the header's `{field}` holds no date"))?,
4132                ),
4133            },
4134        },
4135    };
4136    Ok(ColumnLayout {
4137        name: PlSmallStr::from(name),
4138        source: 0,
4139        start,
4140        stride: stride.unwrap_or(width * count),
4141        width,
4142        count,
4143        physical: if field.ty == Type::Str {
4144            field.encoding.physical()
4145        } else {
4146            field.ty.physical()
4147        },
4148        big_endian: field.endian.unwrap_or(spec.endian) == Endian::Big,
4149        null: field.null,
4150        logical,
4151    })
4152}
4153
4154/// How many bytes one value of `field` takes and how many values it holds, now that
4155/// the header is read.
4156fn sized(field: &Field, header: &HeaderValues) -> Result<(u64, u64), String> {
4157    let width = match (field.ty.width(), &field.size) {
4158        (Some(w), _) => w,
4159        (None, Some(amount)) => header.resolve(amount, "size")?,
4160        (None, None) => 0,
4161    };
4162    let count = match &field.count {
4163        None => 1,
4164        Some(amount) => header.resolve(amount, "count")?,
4165    };
4166    if count > MAX_SIZE || width.saturating_mul(count) > MAX_SIZE {
4167        return Err(format!(
4168            "field `{}` would take {count} values of {width} bytes, more than {MAX_SIZE}",
4169            field.name.as_deref().unwrap_or("pad")
4170        ));
4171    }
4172    Ok((width, count))
4173}
4174
4175/// Read `fields` from `bytes` at `at`, sizing each as it goes, into `read`'s header
4176/// values or (for `footer`) its footer values. Gives where they end.
4177fn read_fields(
4178    spec: &Spec,
4179    fields: &[Field],
4180    bytes: &[u8],
4181    mut at: u64,
4182    read: &mut HeaderValues,
4183    footer: bool,
4184) -> Result<u64, String> {
4185    for field in fields {
4186        let (width, count) = sized(field, read)?;
4187        let end = at + width * count;
4188        if end > bytes.len() as u64 {
4189            return Err(format!(
4190                "the file is {} bytes, too short for its {} (field `{}` ends at byte {end})",
4191                bytes.len(),
4192                if footer { "footer" } else { "header" },
4193                field.name.as_deref().unwrap_or("pad")
4194            ));
4195        }
4196        if let Some(name) = &field.name
4197            && width > 0
4198        {
4199            let place = Place {
4200                start: at as usize,
4201                stride: None,
4202                width: width as usize,
4203                count: count as usize,
4204            };
4205            let layout = layout_of(spec, field, name, place, read)?;
4206            let column =
4207                crate::fixed_records::decode(bytes, &layout, 1).map_err(|e| e.to_string())?;
4208            let value = column.get(0).map_err(|e| e.to_string())?.into_static();
4209            if footer {
4210                read.footer.push((name.clone(), value));
4211            } else {
4212                read.values.push((name.clone(), value));
4213            }
4214        }
4215        at = end;
4216    }
4217    Ok(at)
4218}
4219
4220/// Read the header from the front of `bytes`, sizing each field as it goes.
4221fn read_header(spec: &Spec, bytes: &[u8]) -> Result<HeaderValues, String> {
4222    let mut read = HeaderValues::default();
4223    let at = read_fields(spec, &spec.header.fields, bytes, 0, &mut read, false)?;
4224    read.size = match &spec.header.size {
4225        None => at,
4226        Some(amount) => {
4227            let size = read.resolve(amount, "header size")?;
4228            if size < at {
4229                return Err(format!(
4230                    "the header's fields take {at} bytes, more than its size of {size}"
4231                ));
4232            }
4233            size
4234        }
4235    };
4236    Ok(read)
4237}
4238
4239/// Read the footer at the end of `bytes` into `read`: where it starts, and a note on
4240/// its checksum.
4241fn read_footer(
4242    spec: &Spec,
4243    bytes: &[u8],
4244    read: &mut HeaderValues,
4245) -> Result<(u64, Option<String>), String> {
4246    let len = bytes.len() as u64;
4247    let Some(footer) = &spec.footer else {
4248        return Ok((len, None));
4249    };
4250    let size = footer
4251        .size
4252        .or_else(|| given_width(&footer.fields))
4253        .unwrap_or(0);
4254    let start = len
4255        .checked_sub(size)
4256        .filter(|s| *s >= read.size)
4257        .ok_or_else(|| {
4258            format!(
4259                "the file is {len} bytes, too short for its {}-byte header and {size}-byte footer",
4260                read.size
4261            )
4262        })?;
4263    read_fields(spec, &footer.fields, bytes, start, read, true)?;
4264    // Notes are warnings: a checksum that matches says nothing.
4265    let note = footer.checksum.as_ref().and_then(|(algo, field)| {
4266        let stored = read.footer_int(field);
4267        let computed = algo.compute(&bytes[..start as usize]);
4268        match stored {
4269            Some(v) if v == i128::from(computed) => None,
4270            Some(v) => Some(format!(
4271                "the footer's checksum `{field}` is {v:#x}; the file's is {computed:#x}"
4272            )),
4273            None => Some(format!(
4274                "the footer has no value for its checksum `{field}`"
4275            )),
4276        }
4277    });
4278    Ok((start, note))
4279}
4280
4281/// The symbol lists `fields` index into, read from beside `dir`.
4282fn read_lookups(
4283    fields: &[Field],
4284    dir: Option<&Path>,
4285    read: &mut HeaderValues,
4286) -> Result<(), String> {
4287    for field in fields {
4288        let (Some(name), Some(lookup)) = (&field.name, &field.lookup) else {
4289            continue;
4290        };
4291        let dir = dir.ok_or_else(|| {
4292            format!("{name}: its symbol list {} is beside the data, which was not opened from a directory", lookup.file)
4293        })?;
4294        let path = dir.join(&lookup.file);
4295        let size = std::fs::metadata(&path)
4296            .map_err(|e| format!("{name}: the symbol list {}: {e}", path.display()))?
4297            .len();
4298        if size > MAX_SIZE {
4299            return Err(format!(
4300                "{name}: the symbol list {} is {size} bytes, more than {MAX_SIZE}",
4301                path.display()
4302            ));
4303        }
4304        let bytes = std::fs::read(&path)
4305            .map_err(|e| format!("{name}: the symbol list {}: {e}", path.display()))?;
4306        read.lookups
4307            .insert(name.clone(), Arc::new(symbols(&bytes, lookup.format)));
4308    }
4309    Ok(())
4310}
4311
4312/// The entries of a symbol list.
4313pub fn symbols(bytes: &[u8], format: LookupFormat) -> Vec<String> {
4314    match format {
4315        LookupFormat::Lines => String::from_utf8_lossy(bytes)
4316            .lines()
4317            .map(|l| l.trim_end_matches('\r').to_string())
4318            .collect(),
4319        LookupFormat::Nul => {
4320            let mut out: Vec<String> = bytes
4321                .split(|b| *b == 0)
4322                .map(|s| String::from_utf8_lossy(s).into_owned())
4323                .collect();
4324            // The list ends with a NUL, which leaves an empty piece after it.
4325            if bytes.last() == Some(&0) {
4326                out.pop();
4327            }
4328            out
4329        }
4330        LookupFormat::Fixed(n) => bytes
4331            .chunks(n as usize)
4332            .map(crate::fixed_records::text)
4333            .collect(),
4334    }
4335}
4336
4337/// The rows a spec reads, however they are framed: what the table, a window of it,
4338/// `formats check` and the fuzz target read through.
4339pub trait SpecRecords: crate::pushdown::Windowed + std::fmt::Debug {
4340    fn rows(&self) -> usize;
4341    fn schema(&self) -> SchemaRef;
4342    /// The frame: a scan that decodes only what a query asks for.
4343    fn into_lazy(self: Arc<Self>) -> PolarsResult<LazyFrame>;
4344    /// The first `rows` rows, decoded now.
4345    fn collect(&self, rows: usize) -> PolarsResult<DataFrame>;
4346    /// The bytes the rows are read from, for a reader of the raw bytes.
4347    fn sources(&self) -> &[Arc<Bytes>];
4348}
4349
4350impl std::fmt::Debug for FixedRecords {
4351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4352        f.debug_struct("FixedRecords")
4353            .field("rows", &self.rows())
4354            .finish()
4355    }
4356}
4357
4358impl SpecRecords for FixedRecords {
4359    fn rows(&self) -> usize {
4360        FixedRecords::rows(self)
4361    }
4362    fn schema(&self) -> SchemaRef {
4363        FixedRecords::schema(self)
4364    }
4365    fn into_lazy(self: Arc<Self>) -> PolarsResult<LazyFrame> {
4366        Ok(FixedRecords::lazy(&self))
4367    }
4368    fn collect(&self, rows: usize) -> PolarsResult<DataFrame> {
4369        FixedRecords::collect(self, rows)
4370    }
4371    fn sources(&self) -> &[Arc<Bytes>] {
4372        FixedRecords::sources(self)
4373    }
4374}
4375
4376impl SpecRecords for crate::framed_records::FramedRecords {
4377    fn rows(&self) -> usize {
4378        crate::framed_records::FramedRecords::rows(self)
4379    }
4380    fn schema(&self) -> SchemaRef {
4381        crate::framed_records::FramedRecords::schema(self)
4382    }
4383    fn into_lazy(self: Arc<Self>) -> PolarsResult<LazyFrame> {
4384        Ok(crate::framed_records::FramedRecords::lazy(&self))
4385    }
4386    fn collect(&self, rows: usize) -> PolarsResult<DataFrame> {
4387        crate::framed_records::FramedRecords::collect(self, rows)
4388    }
4389    fn sources(&self) -> &[Arc<Bytes>] {
4390        crate::framed_records::FramedRecords::sources(self)
4391    }
4392}
4393
4394/// A file read through a spec: its columns, and what the read had to say.
4395pub struct Opened {
4396    pub records: Arc<dyn SpecRecords>,
4397    /// Warnings for the dataset's notes, one sentence each.
4398    pub notes: Vec<String>,
4399    pub header: HeaderValues,
4400}
4401
4402/// A note for the records of `rows` past the most a table holds, which are not shown.
4403fn past_limit(notes: &mut Vec<String>, rows: u64, records: &FixedRecords) {
4404    let past = rows.saturating_sub(records.rows() as u64);
4405    if past > 0 {
4406        notes.push(format!(
4407            "last {past} records not shown: past the table limit"
4408        ));
4409    }
4410}
4411
4412/// Up to this many trailing bytes are shown in the warning about them.
4413const TRAILING_SHOWN: usize = 32;
4414
4415pub(crate) fn trailing_note(what: &str, bytes: &[u8]) -> String {
4416    let shown = &bytes[..bytes.len().min(TRAILING_SHOWN)];
4417    let more = if bytes.len() > shown.len() {
4418        " ..."
4419    } else {
4420        ""
4421    };
4422    format!(
4423        "{what}: {} trailing {} left out, not a whole record: {}{more}",
4424        bytes.len(),
4425        if bytes.len() == 1 { "byte" } else { "bytes" },
4426        crate::fixed_records::hex(shown),
4427    )
4428}
4429
4430impl Spec {
4431    /// The columns the record fields become, a record `stride` bytes apart (or one
4432    /// file per field for the columns layout), and the bytes the fields take.
4433    fn record_columns(
4434        &self,
4435        header: &HeaderValues,
4436        header_size: usize,
4437        stride: Option<usize>,
4438    ) -> Result<(Vec<ColumnLayout>, u64), String> {
4439        let mut columns = Vec::new();
4440        let mut at = 0u64;
4441        for (i, field) in self.records.fields.iter().enumerate() {
4442            let (width, count) = sized(field, header)?;
4443            if width == 0 {
4444                return Err(format!(
4445                    "field `{}` takes no bytes",
4446                    field.name.as_deref().unwrap_or("pad")
4447                ));
4448            }
4449            let start = match self.layout {
4450                Layout::Rows => header_size + at as usize,
4451                Layout::Columns => header_size,
4452            };
4453            if let Some(name) = &field.name {
4454                let source = match self.layout {
4455                    Layout::Rows => 0,
4456                    Layout::Columns => i,
4457                };
4458                if field.flatten {
4459                    for j in 0..count as usize {
4460                        let place = Place {
4461                            start: start + j * width as usize,
4462                            stride,
4463                            width: width as usize,
4464                            count: 1,
4465                        };
4466                        let mut layout =
4467                            layout_of(self, field, &format!("{name}_{j}"), place, header)?;
4468                        layout.source = source;
4469                        columns.push(layout);
4470                    }
4471                } else {
4472                    let place = Place {
4473                        start,
4474                        stride,
4475                        width: width as usize,
4476                        count: count as usize,
4477                    };
4478                    let mut layout = layout_of(self, field, name, place, header)?;
4479                    layout.source = source;
4480                    columns.push(layout);
4481                }
4482            }
4483            at += width * count;
4484        }
4485        Ok((columns, at))
4486    }
4487
4488    /// Check the magic of `bytes`, the front of one file.
4489    fn check_magic(&self, bytes: &[u8], named: &str) -> Result<(), String> {
4490        if self.magic.is_empty() || self.magic_matches(bytes) {
4491            return Ok(());
4492        }
4493        let start = (self.magic_offset as usize).min(bytes.len());
4494        let found = &bytes[start..(start + self.magic.len()).min(bytes.len())];
4495        Err(format!(
4496            "{named} is not {}: expected magic {} at byte {}, found {}",
4497            self.name,
4498            crate::fixed_records::hex(&self.magic),
4499            self.magic_offset,
4500            if found.is_empty() {
4501                "the end of the file".to_string()
4502            } else {
4503                crate::fixed_records::hex(found)
4504            }
4505        ))
4506    }
4507
4508    /// Read `bytes`, one file of the rows layout, named `named` in what it says.
4509    pub fn open_rows(&self, bytes: Arc<Bytes>, named: &str) -> Result<Opened, String> {
4510        if self.is_delimited() {
4511            return Err(format!(
4512                "{} is a delimited spec; it reads text through the CSV reader",
4513                self.name
4514            ));
4515        }
4516        self.open_rows_in(bytes, named, None)
4517    }
4518
4519    /// The spec with its byte order settled by `head`, for `endian = "auto"`: as the
4520    /// spec says when the magic reads as written, big-endian when it reads reversed.
4521    pub fn for_file(&self, head: &[u8]) -> std::borrow::Cow<'_, Spec> {
4522        if !self.endian_auto {
4523            return std::borrow::Cow::Borrowed(self);
4524        }
4525        let reversed: Vec<u8> = self.magic.iter().rev().copied().collect();
4526        let start = self.magic_offset as usize;
4527        if reversed != self.magic && head.get(start..start + reversed.len()) == Some(&reversed[..])
4528        {
4529            let mut spec = self.clone();
4530            spec.endian = Endian::Big;
4531            spec.endian_auto = false;
4532            spec.magic = reversed;
4533            return std::borrow::Cow::Owned(spec);
4534        }
4535        std::borrow::Cow::Borrowed(self)
4536    }
4537
4538    /// Whether the spec reads a directory: column files, or a tree of its files.
4539    pub fn reads_directory(&self) -> bool {
4540        self.files.is_some()
4541            || (self.layout == Layout::Columns
4542                && !self.records.fields.iter().any(|f| f.at.is_some()))
4543    }
4544
4545    /// The spec reading the variant `name` alone: only its records, and only its columns.
4546    pub fn with_variant(&self, name: &str) -> Result<Spec, String> {
4547        if !self.records.variants.iter().any(|v| v.name == name) {
4548            let names: Vec<&str> = self
4549                .records
4550                .variants
4551                .iter()
4552                .map(|v| v.name.as_str())
4553                .collect();
4554            return Err(if names.is_empty() {
4555                format!("{} has no variants", self.name)
4556            } else {
4557                format!(
4558                    "{} has no variant {name}; it has {}",
4559                    self.name,
4560                    names.join(", ")
4561                )
4562            });
4563        }
4564        let mut spec = self.clone();
4565        spec.variant = Some(name.to_string());
4566        Ok(spec)
4567    }
4568
4569    /// Read `bytes`, one file, named `named` in what it says; a symbol list a field
4570    /// names is looked for beside `path`, the file the bytes are, which also keeps the
4571    /// walk of its records for the next open of it.
4572    pub fn open_rows_in(
4573        &self,
4574        bytes: Arc<Bytes>,
4575        named: &str,
4576        path: Option<&Path>,
4577    ) -> Result<Opened, String> {
4578        let dir = path.and_then(Path::parent);
4579        if self.reads_directory() {
4580            return Err(format!(
4581                "{} reads a directory ({}); open the directory",
4582                self.name,
4583                if self.files.is_some() {
4584                    "a tree of its files"
4585                } else {
4586                    "column files, layout = \"columns\""
4587                }
4588            ));
4589        }
4590        let spec = self.for_file(bytes.as_slice());
4591        let spec = spec.as_ref();
4592        if spec.layout == Layout::Columns {
4593            return spec.open_columns_file(bytes, named, dir);
4594        }
4595        let data = bytes.as_slice();
4596        let mut header = if spec.capture.is_some() {
4597            if !crate::framed_records::capture::is_capture(data) {
4598                return Err(format!("{named} is not a pcap or pcapng capture"));
4599            }
4600            HeaderValues::default()
4601        } else {
4602            spec.check_magic(data, named)?;
4603            read_header(spec, data)?
4604        };
4605        let mut notes = Vec::new();
4606        let full = data.len() as u64;
4607        if header.size > full {
4608            return Err(format!(
4609                "{named} is {full} bytes, shorter than its {}-byte header",
4610                header.size
4611            ));
4612        }
4613        let (len, footer_note) = read_footer(spec, data, &mut header)?;
4614        notes.extend(footer_note);
4615        let fields: Vec<Field> = all_fields(&spec.records).cloned().collect();
4616        read_lookups(&fields, dir, &mut header)?;
4617        if crate::framed_records::needed(spec) {
4618            let (records, more) = crate::framed_records::FramedRecords::open(
4619                spec,
4620                bytes.clone(),
4621                &header,
4622                header.size as usize..len as usize,
4623                named,
4624                path,
4625            )?;
4626            notes.extend(more);
4627            return Ok(Opened {
4628                records: Arc::new(records),
4629                notes,
4630                header,
4631            });
4632        }
4633        let data = &data[..len as usize];
4634        // The fields' own width first, to check the record size against it.
4635        let (_, fields_width) = spec.record_columns(&header, 0, Some(1))?;
4636        let record = match &spec.records.size {
4637            None => fields_width,
4638            Some(amount) => {
4639                let size = header.resolve(amount, "record size")?;
4640                if size < fields_width {
4641                    return Err(format!(
4642                        "the record's fields take {fields_width} bytes, more than its size of {size}"
4643                    ));
4644                }
4645                size
4646            }
4647        };
4648        if record == 0 || record > MAX_SIZE {
4649            return Err(format!(
4650                "a record of {record} bytes is outside 1 to {MAX_SIZE}"
4651            ));
4652        }
4653        let room = len - header.size;
4654        let whole = room / record;
4655        let rows = match &spec.records.count {
4656            None => {
4657                let trailing = room % record;
4658                if trailing > 0 {
4659                    notes.push(trailing_note(named, &data[(len - trailing) as usize..]));
4660                }
4661                whole
4662            }
4663            Some(amount) => {
4664                let count = header.resolve(amount, "count")?;
4665                if count > whole {
4666                    notes.push(format!(
4667                        "header says {count} records {} {whole} whole ones shown",
4668                        crate::glyphs::get().middot
4669                    ));
4670                    whole
4671                } else {
4672                    let end = header.size + count * record;
4673                    if end < len {
4674                        let after = len - end;
4675                        notes.push(format!(
4676                            "{named} has {after} {} after its {count} records, left out",
4677                            if after == 1 { "byte" } else { "bytes" }
4678                        ));
4679                    }
4680                    count
4681                }
4682            }
4683        };
4684        let (columns, _) =
4685            spec.record_columns(&header, header.size as usize, Some(record as usize))?;
4686        let records =
4687            FixedRecords::new(vec![bytes], columns, rows as usize).map_err(|e| e.to_string())?;
4688        past_limit(&mut notes, rows, &records);
4689        Ok(Opened {
4690            records: Arc::new(records),
4691            notes,
4692            header,
4693        })
4694    }
4695
4696    /// Read `dir`, a directory of one file per record field.
4697    pub fn open_columns(&self, dir: &Path) -> Result<Opened, String> {
4698        if self.is_delimited() {
4699            return Err(format!(
4700                "{} is a delimited spec; it reads text through the CSV reader",
4701                self.name
4702            ));
4703        }
4704        if self.layout == Layout::Rows {
4705            return Err(format!(
4706                "{} reads one file, and {} is a directory",
4707                self.name,
4708                dir.display()
4709            ));
4710        }
4711        let mut sources = Vec::new();
4712        let mut header: Option<HeaderValues> = None;
4713        let mut notes = Vec::new();
4714        let mut counts: Vec<(String, u64)> = Vec::new();
4715        // Each file's own header size: a size read from the header may differ by file.
4716        let mut starts = Vec::new();
4717        for field in &self.records.fields {
4718            let file_name = field
4719                .file
4720                .clone()
4721                .or_else(|| field.name.clone())
4722                .expect("a column field is named");
4723            let path = dir.join(&file_name);
4724            let bytes = Bytes::map(&path).map_err(|e| format!("{}: {e}", path.display()))?;
4725            self.check_magic(bytes.as_slice(), &file_name)?;
4726            let read = read_header(self, bytes.as_slice())?;
4727            let (width, count) = sized(field, header.as_ref().unwrap_or(&read))?;
4728            let cell = width * count;
4729            let len = bytes.len() as u64;
4730            if read.size > len {
4731                return Err(format!(
4732                    "{file_name} is {len} bytes, shorter than its {}-byte header",
4733                    read.size
4734                ));
4735            }
4736            let room = len - read.size;
4737            if cell > 0 && !room.is_multiple_of(cell) {
4738                let trailing = room % cell;
4739                notes.push(trailing_note(
4740                    &file_name,
4741                    &bytes.as_slice()[(len - trailing) as usize..],
4742                ));
4743            }
4744            counts.push((file_name, room.checked_div(cell).unwrap_or(0)));
4745            starts.push(read.size as usize);
4746            if header.is_none() {
4747                header = Some(read);
4748            }
4749            sources.push(Arc::new(bytes));
4750        }
4751        let mut header = header.unwrap_or_default();
4752        read_lookups(&self.records.fields, Some(dir), &mut header)?;
4753        let fewest = counts.iter().map(|(_, n)| *n).min().unwrap_or(0);
4754        if counts.iter().any(|(_, n)| *n != fewest) {
4755            let said: Vec<String> = counts
4756                .iter()
4757                .map(|(name, n)| format!("{name} {n}"))
4758                .collect();
4759            notes.push(format!(
4760                "column files differ in length ({}) {} first {fewest} rows shown",
4761                said.join(", "),
4762                crate::glyphs::get().middot
4763            ));
4764        }
4765        let mut rows = fewest;
4766        if let Some(amount) = &self.records.count {
4767            let count = header.resolve(amount, "count")?;
4768            if count > fewest {
4769                notes.push(format!(
4770                    "header says {count} records {} {fewest} shown",
4771                    crate::glyphs::get().middot
4772                ));
4773            }
4774            rows = rows.min(count);
4775        }
4776        let (mut columns, _) = self.record_columns(&header, 0, None)?;
4777        for column in &mut columns {
4778            column.start = starts[column.source];
4779        }
4780        let records =
4781            FixedRecords::new(sources, columns, rows as usize).map_err(|e| e.to_string())?;
4782        past_limit(&mut notes, rows, &records);
4783        Ok(Opened {
4784            records: Arc::new(records),
4785            notes,
4786            header,
4787        })
4788    }
4789
4790    /// Read `bytes`, one file holding each column's values in a run at its `offset`.
4791    fn open_columns_file(
4792        &self,
4793        bytes: Arc<Bytes>,
4794        named: &str,
4795        dir: Option<&Path>,
4796    ) -> Result<Opened, String> {
4797        let data = bytes.as_slice();
4798        self.check_magic(data, named)?;
4799        let mut header = read_header(self, data)?;
4800        let mut notes = Vec::new();
4801        let (len, footer_note) = read_footer(self, data, &mut header)?;
4802        notes.extend(footer_note);
4803        read_lookups(&self.records.fields, dir, &mut header)?;
4804        let (mut columns, _) = self.record_columns(&header, 0, None)?;
4805        let mut rows = u64::MAX;
4806        let mut starts = Vec::new();
4807        for field in &self.records.fields {
4808            let (width, count) = sized(field, &header)?;
4809            let at = field.at.as_ref().expect("checked at parse");
4810            let start = header.resolve_any(at, "offset")?;
4811            if start < header.size || start > len {
4812                return Err(format!(
4813                    "column `{}` starts at byte {start}, outside the data from {} to {len}",
4814                    field.name.as_deref().unwrap_or("pad"),
4815                    header.size
4816                ));
4817            }
4818            rows = rows.min((len - start) / (width * count).max(1));
4819            starts.push(start as usize);
4820        }
4821        if let Some(amount) = &self.records.count {
4822            let count = header.resolve_any(amount, "count")?;
4823            if count > rows {
4824                notes.push(format!(
4825                    "header says {count} records {} room for {rows} shown",
4826                    crate::glyphs::get().middot
4827                ));
4828            }
4829            rows = rows.min(count);
4830        } else {
4831            notes.push(format!(
4832                "no count is given, so the rows are the {rows} the shortest column has room for"
4833            ));
4834        }
4835        for column in &mut columns {
4836            column.start = starts[column.source];
4837        }
4838        let sources = vec![bytes; self.records.fields.len()];
4839        let records = FixedRecords::new(sources, columns, rows.min(usize::MAX as u64) as usize)
4840            .map_err(|e| e.to_string())?;
4841        Ok(Opened {
4842            records: Arc::new(records),
4843            notes,
4844            header,
4845        })
4846    }
4847
4848    /// Open `path`: a file, or a directory of column files or of the spec's files.
4849    pub fn open(&self, path: &Path, named: &str) -> Result<Opened, String> {
4850        if self.files.is_some() {
4851            return crate::formats::files::open(self, path);
4852        }
4853        if self.reads_directory() {
4854            return self.open_columns(path);
4855        }
4856        if path.is_dir() {
4857            return Err(format!(
4858                "{} reads one file, and {} is a directory",
4859                self.name,
4860                path.display()
4861            ));
4862        }
4863        let bytes = Bytes::map(path).map_err(|e| format!("{}: {e}", path.display()))?;
4864        self.open_rows_in(Arc::new(bytes), named, Some(path))
4865    }
4866}
4867
4868/// A spec found on the search path, and the copies of the same name it hides.
4869#[derive(Debug, Clone)]
4870pub struct Found {
4871    pub spec: Arc<Spec>,
4872    pub overrides: Vec<PathBuf>,
4873}
4874
4875/// Every spec on the search path, first of each name first.
4876#[derive(Debug, Clone, Default)]
4877pub struct Registry {
4878    pub specs: Vec<Found>,
4879    /// FIX dictionaries: QuickFIX XML files and `kind = "fix"` TOML files.
4880    pub fix: Vec<FixFound>,
4881    /// DBC files for CAN logs: `.dbc` files and `kind = "dbc"` TOML files, in the order
4882    /// they are read.
4883    pub dbc: Vec<DbcFound>,
4884    /// Spec files that could not be read, each with why.
4885    pub errors: Vec<SpecError>,
4886}
4887
4888/// A DBC file found on the search path.
4889#[derive(Debug, Clone)]
4890pub struct DbcFound {
4891    pub dbc: Arc<crate::dbc::Dbc>,
4892    /// The file it was found as: the `.dbc`, or the TOML that names it.
4893    pub path: PathBuf,
4894}
4895
4896/// A FIX dictionary found on the search path, and the copies of the same name it hides.
4897#[derive(Debug, Clone)]
4898pub struct FixFound {
4899    pub dict: Arc<crate::fix::dict::Dictionary>,
4900    pub overrides: Vec<PathBuf>,
4901}
4902
4903/// How a spec was chosen for a file.
4904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4905pub enum Chosen {
4906    /// `--format FILE`.
4907    SpecFile,
4908    /// `--format NAME`, or picked in the view.
4909    Named,
4910    Glob,
4911    Magic,
4912}
4913
4914/// Why `spec` read a file, for the Notes tab: `matched by magic MKTD · version 1`
4915/// when its glob or its magic chose it, `chosen by --format FILE` otherwise.
4916pub fn chosen_words(spec: &Spec, by: Chosen) -> String {
4917    let chips = match by {
4918        Chosen::Glob | Chosen::Magic => spec.match_chips_chosen(by),
4919        Chosen::SpecFile | Chosen::Named => Vec::new(),
4920    };
4921    if chips.is_empty() {
4922        format!("chosen by {}", by.words())
4923    } else {
4924        format!("matched by {}", chips_plain(&chips))
4925    }
4926}
4927
4928impl Chosen {
4929    pub fn words(self) -> &'static str {
4930        match self {
4931            Self::SpecFile => "--format FILE",
4932            Self::Named => "its name",
4933            Self::Glob => "its glob",
4934            Self::Magic => "its magic",
4935        }
4936    }
4937}
4938
4939/// The specs a file matches, by the first rule that matched any.
4940#[derive(Debug, Clone)]
4941pub struct Matched {
4942    pub specs: Vec<Arc<Spec>>,
4943    pub by: Chosen,
4944}
4945
4946/// The directories and files searched for specs, in order: the config directory's
4947/// `formats`, then `$DATUI_FORMATS_PATH`, then `[formats] path` from the config.
4948pub fn search_path(
4949    config_dir: Option<&Path>,
4950    env: Option<std::ffi::OsString>,
4951    configured: &[String],
4952) -> Vec<PathBuf> {
4953    let mut path = Vec::new();
4954    if let Some(dir) = config_dir {
4955        path.push(dir.join("formats"));
4956    }
4957    if let Some(env) = env {
4958        path.extend(std::env::split_paths(&env).filter(|p| !p.as_os_str().is_empty()));
4959    }
4960    path.extend(
4961        configured
4962            .iter()
4963            .filter(|p| !p.trim().is_empty())
4964            .map(|p| crate::config::expand_path(p)),
4965    );
4966    path
4967}
4968
4969/// The search path `config` asks for: the config directory's `formats`, then
4970/// `$DATUI_FORMATS_PATH`, then its `[formats] path`.
4971pub fn search_path_for(config: &crate::config::AppConfig) -> Vec<PathBuf> {
4972    let config_dir = crate::config::ConfigManager::new(crate::APP_NAME)
4973        .ok()
4974        .map(|m| m.config_dir().to_path_buf());
4975    search_path(
4976        config_dir.as_deref(),
4977        std::env::var_os(PATH_VAR),
4978        &config.formats.path,
4979    )
4980}
4981
4982impl Registry {
4983    /// Read every spec on `path`. A directory gives its `*.toml` files in name order; a
4984    /// file gives itself. What cannot be read is kept as an error, not fatal. A TOML
4985    /// file of `kind = "fix"`, or a QuickFIX XML file, is a FIX dictionary; a `.dbc`
4986    /// file, or a TOML file of `kind = "dbc"`, is a DBC file.
4987    pub fn load(path: &[PathBuf]) -> Self {
4988        let mut registry = Self::default();
4989        for entry in path {
4990            let files: Vec<PathBuf> = if entry.is_dir() {
4991                let Ok(listing) = std::fs::read_dir(entry) else {
4992                    continue;
4993                };
4994                let mut files: Vec<PathBuf> = listing
4995                    .flatten()
4996                    .map(|e| e.path())
4997                    .filter(|p| {
4998                        p.is_file()
4999                            && p.extension().is_some_and(|e| {
5000                                e.eq_ignore_ascii_case("toml")
5001                                    || e.eq_ignore_ascii_case("xml")
5002                                    || e.eq_ignore_ascii_case("dbc")
5003                            })
5004                    })
5005                    .collect();
5006                files.sort();
5007                files
5008            } else if entry.is_file() {
5009                vec![entry.clone()]
5010            } else {
5011                continue;
5012            };
5013            for file in files {
5014                // A DBC file, or a TOML file of `kind = "dbc"` that names one.
5015                let dbc_like = file.extension().is_some_and(|e| {
5016                    e.eq_ignore_ascii_case("dbc") || e.eq_ignore_ascii_case("toml")
5017                });
5018                if dbc_like {
5019                    match crate::dbc::load(&file) {
5020                        Ok(Some(dbc)) => {
5021                            registry.dbc.push(DbcFound {
5022                                dbc: Arc::new(dbc),
5023                                path: file,
5024                            });
5025                            continue;
5026                        }
5027                        Err(e) => {
5028                            registry.errors.push(e);
5029                            continue;
5030                        }
5031                        Ok(None) => {}
5032                    }
5033                }
5034                match crate::fix::dict::Dictionary::load(&file) {
5035                    Ok(Some(dict)) => {
5036                        registry.add_fix(dict, file);
5037                        continue;
5038                    }
5039                    Err(e) => {
5040                        registry.errors.push(e);
5041                        continue;
5042                    }
5043                    // An XML file that is not a FIX dictionary is not a spec either.
5044                    Ok(None)
5045                        if !file
5046                            .extension()
5047                            .is_some_and(|e| e.eq_ignore_ascii_case("toml")) =>
5048                    {
5049                        continue;
5050                    }
5051                    Ok(None) => {}
5052                }
5053                match Spec::load(&file) {
5054                    Ok(spec) => registry.add(spec, file),
5055                    Err(e) => registry.errors.push(e),
5056                }
5057            }
5058        }
5059        registry
5060    }
5061
5062    fn add(&mut self, spec: Spec, file: PathBuf) {
5063        if let Some(found) = self.specs.iter_mut().find(|f| f.spec.name == spec.name) {
5064            found.overrides.push(file);
5065        } else {
5066            self.specs.push(Found {
5067                spec: Arc::new(spec),
5068                overrides: Vec::new(),
5069            });
5070        }
5071    }
5072
5073    fn add_fix(&mut self, dict: crate::fix::dict::Dictionary, file: PathBuf) {
5074        if let Some(found) = self.fix.iter_mut().find(|f| f.dict.name == dict.name) {
5075            found.overrides.push(file);
5076        } else {
5077            self.fix.push(FixFound {
5078                dict: Arc::new(dict),
5079                overrides: Vec::new(),
5080            });
5081        }
5082    }
5083
5084    /// The FIX dictionary named `name`.
5085    pub fn fix_dict(&self, name: &str) -> Option<&Arc<crate::fix::dict::Dictionary>> {
5086        self.fix
5087            .iter()
5088            .find(|f| f.dict.name == name)
5089            .map(|f| &f.dict)
5090    }
5091
5092    /// The registry of `specs`, for tests and hosts that have their specs in hand.
5093    pub fn of(specs: Vec<Spec>) -> Self {
5094        let mut registry = Self::default();
5095        for spec in specs {
5096            let file = spec.path.clone().unwrap_or_default();
5097            registry.add(spec, file);
5098        }
5099        registry
5100    }
5101
5102    pub fn get(&self, name: &str) -> Option<&Arc<Spec>> {
5103        self.specs
5104            .iter()
5105            .find(|f| f.spec.name == name)
5106            .map(|f| &f.spec)
5107    }
5108
5109    pub fn is_empty(&self) -> bool {
5110        self.specs.is_empty()
5111    }
5112
5113    /// The spec that reads the file `file`, by its glob or else by its magic as an open
5114    /// picks it, when it reads the file's records as several variants: the home screen
5115    /// lists them inside the file. Its first bytes are read only when no glob names it.
5116    pub fn variants_of(&self, file: &Path) -> Option<Arc<Spec>> {
5117        let globbed = self.by_glob(file, false);
5118        let spec = if globbed.is_empty() {
5119            let wanted = unnamed_may(file, false, false)?;
5120            let compression = crate::CompressionFormat::from_extension(file);
5121            if compression.is_some() || !self.specs.iter().any(|f| !f.spec.magic.is_empty()) {
5122                return None;
5123            }
5124            self.matching_among(file, false, wanted, |reach| spec_head(file, None, reach))?
5125                .specs
5126                .into_iter()
5127                .next()?
5128        } else {
5129            globbed.into_iter().find(|s| !s.is_delimited())?
5130        };
5131        spec.lists_variants().then_some(spec)
5132    }
5133
5134    /// The spec that reads a local file a listing looked inside, as an open with
5135    /// nothing asked picks it: by glob, else by magic and `match.where`, compared
5136    /// against `head`, the bytes the listing already read from its front (all of it
5137    /// when `whole`). A spec whose match needs more than `head` holds is not asked.
5138    pub fn listed(&self, path: &Path, head: &[u8], whole: bool) -> Option<Arc<Spec>> {
5139        if self.is_empty() || crate::CompressionFormat::from_extension(path).is_some() {
5140            return None;
5141        }
5142        let wanted = unnamed_may(path, false, false)?;
5143        let held = head.len() as u64;
5144        let within = |s: &Spec| wanted(s) && (whole || s.match_reach() <= held);
5145        self.matching_among(path, false, within, |_| Some(head.to_vec()))?
5146            .specs
5147            .into_iter()
5148            .next()
5149    }
5150
5151    /// The specs whose globs match `path`, a file or (for the columns layout) a
5152    /// directory, by name alone.
5153    pub fn by_glob(&self, path: &Path, is_dir: bool) -> Vec<Arc<Spec>> {
5154        self.specs
5155            .iter()
5156            .map(|f| &f.spec)
5157            .filter(|s| s.reads_directory() == is_dir && s.glob_matches(path))
5158            .cloned()
5159            .collect()
5160    }
5161
5162    /// The specs `path` matches: by glob, else by magic. A spec with `match.where`
5163    /// matches only a file whose header holds those values. The front of the file is
5164    /// read through `head`, once, and only when a magic or a header is to be compared.
5165    pub fn matching(
5166        &self,
5167        path: &Path,
5168        is_dir: bool,
5169        head: impl FnOnce(u64) -> Option<Vec<u8>>,
5170    ) -> Option<Matched> {
5171        self.matching_among(path, is_dir, |_| true, head)
5172    }
5173
5174    /// [`Self::matching`] among the specs `wanted` says may read `path`.
5175    pub fn matching_among(
5176        &self,
5177        path: &Path,
5178        is_dir: bool,
5179        wanted: impl Fn(&Spec) -> bool,
5180        head: impl FnOnce(u64) -> Option<Vec<u8>>,
5181    ) -> Option<Matched> {
5182        let mut globbed = self.by_glob(path, is_dir);
5183        globbed.retain(|s| wanted(s));
5184        let (candidates, by) = if !globbed.is_empty() {
5185            (globbed, Chosen::Glob)
5186        } else if is_dir {
5187            return None;
5188        } else {
5189            let magic: Vec<Arc<Spec>> = self
5190                .specs
5191                .iter()
5192                .map(|f| &f.spec)
5193                .filter(|s| !s.reads_directory() && !s.magic.is_empty() && wanted(s))
5194                .cloned()
5195                .collect();
5196            (magic, Chosen::Magic)
5197        };
5198        let reach = candidates
5199            .iter()
5200            .map(|s| match by {
5201                Chosen::Magic => s.match_reach(),
5202                _ if s.expect.is_empty() => 0,
5203                _ => s.match_reach(),
5204            })
5205            .max()
5206            .unwrap_or(0);
5207        let head = if reach > 0 && !is_dir {
5208            head(reach)
5209        } else {
5210            None
5211        };
5212        let specs: Vec<Arc<Spec>> = candidates
5213            .into_iter()
5214            .filter(|s| {
5215                let Some(head) = &head else {
5216                    // Nothing read: a glob match stands unless it asked about the header.
5217                    return by == Chosen::Glob && s.expect.is_empty();
5218                };
5219                (by != Chosen::Magic || s.magic_matches(head)) && s.header_matches(head)
5220            })
5221            .collect();
5222        (!specs.is_empty()).then_some(Matched { specs, by })
5223    }
5224
5225    /// Text for `datui formats`: each spec, the file it came from, the copies it hides,
5226    /// and the spec files that could not be read.
5227    pub fn listing(&self, path: &[PathBuf]) -> String {
5228        let mut out = String::new();
5229        if self.specs.is_empty() {
5230            out.push_str("No format specs found.\n");
5231        }
5232        for found in &self.specs {
5233            let spec = &found.spec;
5234            out.push_str(&spec.name);
5235            let said = match_words(spec);
5236            if spec.is_delimited() {
5237                out.push_str(&format!("  (delimited; {said})"));
5238            } else {
5239                out.push_str(&format!("  ({said})"));
5240            }
5241            out.push('\n');
5242            if let Some(description) = &spec.description {
5243                out.push_str(&format!("  {description}\n"));
5244            }
5245            if let Some(file) = &spec.path {
5246                out.push_str(&format!("  {}\n", file.display()));
5247            }
5248            for hidden in &found.overrides {
5249                out.push_str(&format!("  overrides {}\n", hidden.display()));
5250            }
5251        }
5252        if !self.fix.is_empty() {
5253            out.push_str("\nDictionaries (FIX):\n");
5254        }
5255        for found in &self.fix {
5256            let dict = &found.dict;
5257            out.push_str(&dict.name);
5258            let summary = dict.matcher.summary();
5259            if !summary.is_empty() {
5260                out.push_str(&format!("  ({summary})"));
5261            }
5262            out.push('\n');
5263            if let Some(file) = &dict.path {
5264                out.push_str(&format!("  {}\n", file.display()));
5265            }
5266            for hidden in &found.overrides {
5267                out.push_str(&format!("  overrides {}\n", hidden.display()));
5268            }
5269        }
5270        if !self.dbc.is_empty() {
5271            out.push_str("\nDictionaries (DBC):\n");
5272        }
5273        for found in &self.dbc {
5274            let dbc = &found.dbc;
5275            out.push_str(&format!(
5276                "{}  ({}{})\n  {}\n",
5277                dbc.name,
5278                crate::text_formats::count(dbc.messages.len() as u64, "message", "messages"),
5279                dbc.interface
5280                    .as_ref()
5281                    .map(|i| format!(", interface {i}"))
5282                    .unwrap_or_default(),
5283                found.path.display()
5284            ));
5285        }
5286        if !self.errors.is_empty() {
5287            out.push_str("\nCould not read:\n");
5288            for e in &self.errors {
5289                out.push_str(&format!("  {e}\n"));
5290            }
5291        }
5292        out.push_str("\nSearched, in order:\n");
5293        for entry in path {
5294            out.push_str(&format!("  {}\n", entry.display()));
5295        }
5296        out
5297    }
5298}
5299
5300/// The first `reach` bytes of `path`, through its decompressor when it has one.
5301pub fn head_of(
5302    path: &Path,
5303    compression: Option<crate::CompressionFormat>,
5304    reach: u64,
5305) -> Option<Vec<u8>> {
5306    use std::io::Read;
5307    let file = std::fs::File::open(path).ok()?;
5308    let reader: Box<dyn Read> = match compression {
5309        None => Box::new(file),
5310        Some(crate::CompressionFormat::Gzip) => Box::new(flate2::read::GzDecoder::new(file)),
5311        Some(crate::CompressionFormat::Zstd) => Box::new(zstd::Decoder::new(file).ok()?),
5312        Some(crate::CompressionFormat::Bzip2) => Box::new(bzip2::read::BzDecoder::new(file)),
5313        Some(crate::CompressionFormat::Xz) => Box::new(xz2::read::XzDecoder::new(file)),
5314    };
5315    let mut head = Vec::new();
5316    reader
5317        .take(reach.min(MAX_MATCH_READ))
5318        .read_to_end(&mut head)
5319        .ok()?;
5320    Some(head)
5321}
5322
5323/// A file read through a spec, as the open carries it to the dataset.
5324pub struct Read {
5325    pub spec: Arc<Spec>,
5326    pub by: Chosen,
5327    /// The other specs that matched as well as `spec`, by the same rule.
5328    pub also: Vec<String>,
5329    /// Warnings from the read: trailing bytes, a short count.
5330    pub notes: Vec<String>,
5331    pub header: HeaderValues,
5332    pub records: Arc<dyn SpecRecords>,
5333}
5334
5335impl std::fmt::Debug for Read {
5336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5337        f.debug_struct("Read")
5338            .field("spec", &self.spec.name)
5339            .field("by", &self.by)
5340            .field("also", &self.also)
5341            .field("rows", &self.records.rows())
5342            .finish()
5343    }
5344}
5345
5346/// What a request for a format says, besides the path.
5347#[derive(Debug, Clone, Default)]
5348pub struct Asked {
5349    /// `--format FILE`.
5350    pub spec_file: Option<PathBuf>,
5351    /// `--format NAME`, or the spec picked in the view.
5352    pub spec_name: Option<String>,
5353    /// The spec `spec_file` names, read already: fetched, when it is remote.
5354    pub spec: Option<Arc<Spec>>,
5355    /// `--table NAME`: one variant of the spec's records, read alone; any other
5356    /// reader takes the name as its own table.
5357    pub variant: Option<String>,
5358    /// A built-in format from `--format`, which no spec overrides.
5359    pub builtin: bool,
5360    pub compression: Option<crate::CompressionFormat>,
5361    /// Several files are read as one: only a delimited spec reads them.
5362    pub text_only: bool,
5363}
5364
5365/// Where a spec was chosen from, carried to a decompressed copy's read.
5366#[derive(Clone)]
5367pub struct Choice {
5368    pub spec: Arc<Spec>,
5369    pub by: Chosen,
5370    pub also: Vec<String>,
5371}
5372
5373/// What [`route`] decided about one local path.
5374pub enum Route {
5375    /// Not a spec's: the path opens as it does without specs.
5376    Elsewhere,
5377    Read(Box<Read>),
5378    /// Compressed: decompress it, then read the copy with `choice.spec`.
5379    Decompress(Choice),
5380    /// A delimited spec's: read with the CSV reader, in the spec's dialect.
5381    Delimited(Choice),
5382}
5383
5384/// The keys a delimited spec takes: its own, and the dialect the option registry
5385/// gives a spec key (`[csv]`'s keys and the layout flags'), so a spec reads as a
5386/// `[csv]` block.
5387fn delimited_spec_keys() -> Vec<&'static str> {
5388    use datui_cli::settings::{OPEN, SETTINGS};
5389    let mut keys = vec![
5390        "name",
5391        "description",
5392        "documentation",
5393        "kind",
5394        "match",
5395        "metadata_line",
5396        "columns",
5397    ];
5398    keys.extend(SETTINGS.iter().filter_map(|s| s.spec));
5399    keys.extend(OPEN.iter().filter_map(|o| o.spec));
5400    keys
5401}
5402
5403/// Which specs may read `path` when nothing names one, or `None` when none may. What
5404/// the name already says is read as it says, compressed or not: a spec of records takes
5405/// only a name that says no format datui reads, and a delimited spec also one that says
5406/// delimited text.
5407fn unnamed_may(path: &Path, is_dir: bool, text_only: bool) -> Option<impl Fn(&Spec) -> bool> {
5408    let said = (!is_dir)
5409        .then(|| crate::discover::data_format(path))
5410        .flatten()
5411        // Text by its name (`.log`, `.txt`) says no more than no name does.
5412        .filter(|f| !f.is_lines());
5413    let parquet_key = crate::discover::is_parquet_key(&crate::discover::directory_and_name(path));
5414    let records_may = said.is_none() && !parquet_key && !text_only;
5415    let text_may =
5416        !is_dir && !parquet_key && said.is_none_or(|f| crate::FileFormat::separator(f).is_some());
5417    (records_may || text_may).then_some(move |s: &Spec| {
5418        if s.is_delimited() {
5419            text_may
5420        } else {
5421            records_may
5422        }
5423    })
5424}
5425
5426/// The first `reach` bytes of `path` for comparing specs' magic, or `None` when its
5427/// bytes say a format datui reads already: a file with no extension may be Parquet,
5428/// Arrow, Avro or ORC by its bytes, which it stays.
5429fn spec_head(
5430    path: &Path,
5431    compression: Option<crate::CompressionFormat>,
5432    reach: u64,
5433) -> Option<Vec<u8>> {
5434    if compression.is_none() && crate::discover::sniff_format(path).is_some() {
5435        return None;
5436    }
5437    head_of(path, compression, reach)
5438}
5439
5440/// Whether, and with which spec, `path` is read. In order: `--format FILE`, then
5441/// `--format NAME`, then a glob, then magic. A file whose name or bytes say it is a
5442/// format datui reads already keeps opening that way.
5443pub fn route(path: &Path, asked: &Asked, registry: &Registry) -> Result<Route, String> {
5444    let compression = asked.compression.or_else(|| {
5445        path.is_file()
5446            .then(|| crate::CompressionFormat::from_extension(path))
5447            .flatten()
5448    });
5449    let named = path.file_name().map_or_else(
5450        || path.display().to_string(),
5451        |n| n.to_string_lossy().into_owned(),
5452    );
5453    let explicit = if let Some(spec) = &asked.spec {
5454        Some(Choice {
5455            spec: spec.clone(),
5456            by: Chosen::SpecFile,
5457            also: Vec::new(),
5458        })
5459    } else if let Some(file) = &asked.spec_file {
5460        if crate::source::is_remote_url(file) {
5461            return Err(format!(
5462                "{}: a remote spec is fetched by the open, and was not",
5463                file.display()
5464            ));
5465        }
5466        let spec = Spec::load(file).map_err(|e| e.to_string())?;
5467        Some(Choice {
5468            spec: Arc::new(spec),
5469            by: Chosen::SpecFile,
5470            also: Vec::new(),
5471        })
5472    } else if let Some(name) = &asked.spec_name {
5473        let spec = registry.get(name).ok_or_else(|| {
5474            format!("no format named {name} on the search path; `datui formats` lists them")
5475        })?;
5476        Some(Choice {
5477            spec: spec.clone(),
5478            by: Chosen::Named,
5479            also: Vec::new(),
5480        })
5481    } else {
5482        None
5483    };
5484    if asked.text_only
5485        && let Some(choice) = &explicit
5486        && !choice.spec.is_delimited()
5487    {
5488        return Err(format!(
5489            "{} reads one file, or one directory of column files",
5490            choice.spec.name
5491        ));
5492    }
5493    let choice = match explicit {
5494        Some(choice) => choice,
5495        None => {
5496            if asked.builtin || registry.is_empty() {
5497                return Ok(Route::Elsewhere);
5498            }
5499            let is_dir = path.is_dir();
5500            let Some(wanted) = unnamed_may(path, is_dir, asked.text_only) else {
5501                return Ok(Route::Elsewhere);
5502            };
5503            // A glob names the file as it is stored uncompressed: `day.l2.zst` is an `*.l2`.
5504            let inner = match compression {
5505                Some(_) => path.with_extension(""),
5506                None => path.to_path_buf(),
5507            };
5508            let matched = registry.matching_among(&inner, is_dir, wanted, |reach| {
5509                spec_head(path, compression, reach)
5510            });
5511            let Some(matched) = matched else {
5512                return Ok(Route::Elsewhere);
5513            };
5514            let mut specs = matched.specs.into_iter();
5515            let spec = specs.next().expect("a match has a spec");
5516            Choice {
5517                spec,
5518                by: matched.by,
5519                also: specs.map(|s| s.name.clone()).collect(),
5520            }
5521        }
5522    };
5523    // The CSV reader reads a delimited spec's files, compressed or not.
5524    if choice.spec.is_delimited() {
5525        return Ok(Route::Delimited(choice));
5526    }
5527    let choice = match &asked.variant {
5528        Some(variant) => Choice {
5529            spec: Arc::new(choice.spec.with_variant(variant)?),
5530            ..choice
5531        },
5532        None => choice,
5533    };
5534    if compression.is_some() && path.is_file() {
5535        return Ok(Route::Decompress(choice));
5536    }
5537    read(path, &named, choice).map(|r| Route::Read(Box::new(r)))
5538}
5539
5540/// Read `path` with the spec `choice` holds, naming it `named` in what it says.
5541pub fn read(path: &Path, named: &str, choice: Choice) -> Result<Read, String> {
5542    let opened = choice.spec.open(path, named)?;
5543    Ok(Read {
5544        spec: choice.spec,
5545        by: choice.by,
5546        also: choice.also,
5547        notes: opened.notes,
5548        header: opened.header,
5549        records: opened.records,
5550    })
5551}
5552
5553impl Read {
5554    /// The dataset's notes about the read: which format, why, what else matched, the
5555    /// header's values, and the warnings.
5556    pub fn notes(&self) -> Vec<crate::notes::Note> {
5557        let note = |summary: String, scope: String| crate::notes::Note {
5558            summary,
5559            scope,
5560            read_as_text: None,
5561            passed_over: None,
5562        };
5563        let from = self
5564            .spec
5565            .path
5566            .as_ref()
5567            .map_or_else(|| "the spec".to_string(), |p| p.display().to_string());
5568        let mut notes = vec![note(
5569            format!(
5570                "read as {}, {}",
5571                self.spec.name,
5572                chosen_words(&self.spec, self.by)
5573            ),
5574            format!("from {from}"),
5575        )];
5576        if !self.also.is_empty() {
5577            notes.push(note(
5578                format!(
5579                    "{} also {} this file; press b to pick another",
5580                    self.also.join(", "),
5581                    if self.also.len() == 1 {
5582                        "matches"
5583                    } else {
5584                        "match"
5585                    }
5586                ),
5587                format!("by {}", self.by.words()),
5588            ));
5589        }
5590        if !self.header.values.is_empty() {
5591            let said: Vec<String> = self
5592                .header
5593                .values
5594                .iter()
5595                .map(|(name, value)| format!("{name} = {value}"))
5596                .collect();
5597            notes.push(note(
5598                format!("header: {}", said.join(", ")),
5599                "from the file's header".to_string(),
5600            ));
5601        }
5602        for warning in &self.notes {
5603            notes.push(note(
5604                warning.clone(),
5605                "from the file's length and the spec".to_string(),
5606            ));
5607        }
5608        notes
5609    }
5610}
5611
5612/// A spec's match conditions as the command line prints them: its chips, or
5613/// `no match` when only a choice (`--format`, b, B) picks it.
5614fn match_words(spec: &Spec) -> String {
5615    let chips = spec.match_chips();
5616    if chips.is_empty() {
5617        FORMAT_ONLY.to_string()
5618    } else {
5619        chips_plain(&chips)
5620    }
5621}
5622
5623/// What a spec with no `match` says in place of its conditions.
5624pub const FORMAT_ONLY: &str = "no match";
5625
5626/// `datui formats`, or `datui formats check SPEC [FILE]`: what to print, and the exit
5627/// code (non-zero when the check finds an error).
5628pub fn command(
5629    action: Option<&crate::cli::FormatsAction>,
5630    args: &crate::cli::Args,
5631    config: &crate::config::AppConfig,
5632) -> (String, i32) {
5633    let path = search_path_for(config);
5634    let registry = Registry::load(&path);
5635    match action {
5636        None => (registry.listing(&path), 0),
5637        Some(crate::cli::FormatsAction::Check { spec, file }) => {
5638            let options = crate::OpenOptions::from_args_and_config(args, config);
5639            match check(spec, file.as_deref(), &registry, &options) {
5640                Ok(text) => (text, 0),
5641                Err(text) => (text, 1),
5642            }
5643        }
5644    }
5645}
5646
5647/// Rows `formats check` prints from a file.
5648const CHECK_ROWS: usize = 10;
5649
5650/// Check the spec `named` (a file, or a name on the search path) and, given `file`,
5651/// read its first rows.
5652fn check(
5653    named: &str,
5654    file: Option<&Path>,
5655    registry: &Registry,
5656    options: &crate::OpenOptions,
5657) -> Result<String, String> {
5658    let as_file = Path::new(named);
5659    if let Some(dict) = fix_dict_named(named, registry)? {
5660        return check_fix(&dict, file);
5661    }
5662    if let Some(dbc) = dbc_named(named, registry)? {
5663        return check_dbc(&dbc, file);
5664    }
5665    let spec = if as_file.is_file() {
5666        Arc::new(Spec::load(as_file).map_err(|e| format!("error: {e}\n"))?)
5667    } else if let Some(spec) = registry.get(named) {
5668        spec.clone()
5669    } else {
5670        let mut said = format!("error: no spec file or format named {named}\n");
5671        if let Some(e) = registry.errors.iter().find(|e| {
5672            e.path
5673                .as_ref()
5674                .is_some_and(|p| p.file_stem() == as_file.file_stem())
5675        }) {
5676            said.push_str(&format!("error: {e}\n"));
5677        }
5678        return Err(said);
5679    };
5680    let mut out = format!("{}: ok\n", spec.name);
5681    if let Some(from) = &spec.path {
5682        out.push_str(&format!("  from {}\n", from.display()));
5683    }
5684    out.push_str(&format!("  matches {}\n", match_words(&spec)));
5685    if spec.is_delimited() {
5686        return crate::delimited_spec::check(&spec, file, CHECK_ROWS, options)
5687            .map(|rest| out.clone() + &rest)
5688            .map_err(|rest| out.clone() + &rest);
5689    }
5690    let named_fields = spec
5691        .records
5692        .fields
5693        .iter()
5694        .filter(|f| f.name.is_some())
5695        .count();
5696    out.push_str(&format!("  {named_fields} record fields"));
5697    if let Some(width) = given_width(&spec.records.fields) {
5698        let size = match spec.records.size {
5699            Some(Amount::Given(size)) => size,
5700            _ => width,
5701        };
5702        if spec.layout == Layout::Rows
5703            && spec
5704                .records
5705                .size
5706                .as_ref()
5707                .is_none_or(|s| matches!(s, Amount::Given(_)))
5708        {
5709            out.push_str(&format!(", {size} bytes a record"));
5710        }
5711    }
5712    out.push('\n');
5713    let Some(file) = file else {
5714        return Ok(out);
5715    };
5716    let compression = crate::CompressionFormat::from_extension(file).filter(|_| file.is_file());
5717    let shown = file.file_name().map_or_else(
5718        || file.display().to_string(),
5719        |n| n.to_string_lossy().into_owned(),
5720    );
5721    let choice = Choice {
5722        spec: spec.clone(),
5723        by: Chosen::SpecFile,
5724        also: Vec::new(),
5725    };
5726    // A compressed file is read from a copy, as the open reads it.
5727    let copy;
5728    let readable = match compression {
5729        None => file,
5730        Some(compression) => {
5731            copy = decompressed_copy(file, compression)
5732                .map_err(|e| format!("{out}error: {shown}: {e}\n"))?;
5733            copy.path()
5734        }
5735    };
5736    let read = read(readable, &shown, choice).map_err(|e| format!("{out}error: {e}\n"))?;
5737    for note in &read.notes {
5738        out.push_str(&format!("warning: {note}\n"));
5739    }
5740    if !read.header.values.is_empty() {
5741        let said: Vec<String> = read
5742            .header
5743            .values
5744            .iter()
5745            .map(|(name, value)| format!("{name} = {value}"))
5746            .collect();
5747        out.push_str(&format!("header: {}\n", said.join(", ")));
5748    }
5749    out.push_str(&format!("{} records\n", read.records.rows()));
5750    let df = read
5751        .records
5752        .collect(CHECK_ROWS)
5753        .map_err(|e| format!("{out}error: {e}\n"))?;
5754    out.push_str(&text_table(&df));
5755    Ok(out)
5756}
5757
5758/// The FIX dictionary `named` names: a dictionary file, or one on the search path.
5759fn fix_dict_named(
5760    named: &str,
5761    registry: &Registry,
5762) -> Result<Option<Arc<crate::fix::dict::Dictionary>>, String> {
5763    let as_file = Path::new(named);
5764    if as_file.is_file() {
5765        return match crate::fix::dict::Dictionary::load(as_file) {
5766            Ok(dict) => Ok(dict.map(Arc::new)),
5767            Err(e) => Err(format!("error: {e}\n")),
5768        };
5769    }
5770    Ok(registry.fix_dict(named).cloned())
5771}
5772
5773/// The DBC file `named` names: a `.dbc` file, a `kind = "dbc"` TOML file, or one on the
5774/// search path by its name.
5775fn dbc_named(named: &str, registry: &Registry) -> Result<Option<Arc<crate::dbc::Dbc>>, String> {
5776    let as_file = Path::new(named);
5777    if as_file.is_file() {
5778        // Any other file would parse as an empty DBC: only these two kinds are asked.
5779        let dbc_like = as_file
5780            .extension()
5781            .is_some_and(|e| e.eq_ignore_ascii_case("dbc") || e.eq_ignore_ascii_case("toml"));
5782        if !dbc_like {
5783            return Ok(None);
5784        }
5785        return match crate::dbc::load(as_file) {
5786            Ok(dbc) => Ok(dbc.map(Arc::new)),
5787            Err(e) => Err(format!("error: {e}\n")),
5788        };
5789    }
5790    Ok(registry
5791        .dbc
5792        .iter()
5793        .find(|found| found.dbc.name == named)
5794        .map(|found| found.dbc.clone()))
5795}
5796
5797/// `formats check` of a DBC file: its messages and signals, what it passed over and
5798/// the interface it applies to; with `file`, a candump log, how many of its frames it
5799/// names and which messages.
5800fn check_dbc(dbc: &Arc<crate::dbc::Dbc>, file: Option<&Path>) -> Result<String, String> {
5801    use std::io::Read;
5802    let mut out = format!("{}: ok\n", dbc.name);
5803    if let Some(from) = &dbc.path {
5804        out.push_str(&format!("  from {}\n", from.display()));
5805    }
5806    if let Some(interface) = &dbc.interface {
5807        out.push_str(&format!("  matches interface {interface}\n"));
5808    }
5809    let signals: usize = dbc.messages.iter().map(|m| m.signals.len()).sum();
5810    out.push_str(&format!(
5811        "  {}, {}\n",
5812        crate::text_formats::count(dbc.messages.len() as u64, "message", "messages"),
5813        crate::text_formats::count(signals as u64, "signal", "signals"),
5814    ));
5815    for note in &dbc.notes {
5816        out.push_str(&format!("warning: {note}\n"));
5817    }
5818    let Some(file) = file else {
5819        return Ok(out);
5820    };
5821    let failed =
5822        |out: &str, e: &dyn std::fmt::Display| format!("{out}error: {}: {e}\n", file.display());
5823    let read = std::sync::atomic::AtomicU64::new(0);
5824    let mut bytes = Vec::new();
5825    crate::gps::open_reader(file, &crate::OpenOptions::default(), &read)
5826        .and_then(|mut reader| reader.read_to_end(&mut bytes).map_err(Into::into))
5827        .map_err(|e| failed(&out, &e))?;
5828    let index = crate::candump::index(&bytes).map_err(|e| failed(&out, &e))?;
5829    let layers = crate::candump::Layers {
5830        dbcs: vec![dbc.clone()],
5831    };
5832    let listing = crate::candump::Listing::resolve(&index, layers);
5833    let frames = index.keys.len();
5834    out.push_str(&format!(
5835        "{}, {} of them named by {}\n",
5836        crate::text_formats::count(frames as u64, "frame", "frames"),
5837        frames - listing.unknown,
5838        dbc.name
5839    ));
5840    let named: Vec<String> = listing
5841        .messages
5842        .iter()
5843        .map(|(name, (_, rows))| format!("{name} ({})", rows.len()))
5844        .collect();
5845    if !named.is_empty() {
5846        out.push_str(&format!("messages in the log: {}\n", named.join(", ")));
5847    }
5848    Ok(out)
5849}
5850
5851/// `formats check` of a FIX dictionary: what it names and matches; with `file`, how
5852/// many of the log's messages it applies to and the tags it names there.
5853fn check_fix(
5854    dict: &Arc<crate::fix::dict::Dictionary>,
5855    file: Option<&Path>,
5856) -> Result<String, String> {
5857    use std::io::Read;
5858    let mut out = format!("{}: ok\n", dict.name);
5859    if let Some(from) = &dict.path {
5860        out.push_str(&format!("  from {}\n", from.display()));
5861    }
5862    let summary = dict.matcher.summary();
5863    if !summary.is_empty() {
5864        out.push_str(&format!("  matches {summary}\n"));
5865    }
5866    let enums = dict.tags.values().filter(|t| !t.enums.is_empty()).count();
5867    out.push_str(&format!("  {} tags, {enums} with enums\n", dict.tags.len()));
5868    let Some(file) = file else {
5869        return Ok(out);
5870    };
5871    let read = std::sync::atomic::AtomicU64::new(0);
5872    let mut reader = crate::gps::open_reader(file, &crate::OpenOptions::default(), &read)
5873        .map_err(|e| format!("{out}error: {}: {e}\n", file.display()))?;
5874    let mut log = crate::fix::FixReader::new(crate::fix::dict::Layers::new(vec![dict.clone()]));
5875    let mut chunk = vec![0u8; 1 << 16];
5876    loop {
5877        let n = reader
5878            .read(&mut chunk)
5879            .map_err(|e| format!("{out}error: {}: {e}\n", file.display()))?;
5880        if n == 0 {
5881            break;
5882        }
5883        log.push(&chunk[..n]);
5884        let _ = log.take_batch();
5885    }
5886    let _ = log.finish();
5887    let stats = log.stats();
5888    out.push_str(&format!(
5889        "{} messages, {} of them matched by {}\n",
5890        stats.messages,
5891        stats.applied.get(1).copied().unwrap_or(0),
5892        dict.name
5893    ));
5894    let named: Vec<String> = log
5895        .tag_names()
5896        .into_iter()
5897        .filter(|(_, _, by)| *by == 1)
5898        .map(|(tag, name, _)| format!("{tag} {name}"))
5899        .collect();
5900    if !named.is_empty() {
5901        out.push_str(&format!("names in the log: {}\n", named.join(", ")));
5902    }
5903    Ok(out)
5904}
5905
5906/// `df` as plain text: a row of names, then a row per record, columns aligned.
5907pub(crate) fn text_table(df: &polars::prelude::DataFrame) -> String {
5908    let mut rows: Vec<Vec<String>> = vec![
5909        df.get_column_names()
5910            .iter()
5911            .map(|n| n.to_string())
5912            .collect(),
5913    ];
5914    for i in 0..df.height() {
5915        rows.push(
5916            df.columns()
5917                .iter()
5918                .map(|c| match c.get(i) {
5919                    Ok(polars::prelude::AnyValue::String(s)) => s.to_string(),
5920                    Ok(polars::prelude::AnyValue::StringOwned(s)) => s.to_string(),
5921                    Ok(v) => v.to_string(),
5922                    Err(_) => String::new(),
5923                })
5924                .collect(),
5925        );
5926    }
5927    let widths: Vec<usize> = (0..rows[0].len())
5928        .map(|c| rows.iter().map(|r| r[c].chars().count()).max().unwrap_or(0))
5929        .collect();
5930    let mut out = String::new();
5931    for row in rows {
5932        let cells: Vec<String> = row
5933            .iter()
5934            .zip(&widths)
5935            .map(|(cell, width)| format!("{cell:<width$}"))
5936            .collect();
5937        out.push_str(cells.join("  ").trim_end());
5938        out.push('\n');
5939    }
5940    out
5941}
5942
5943/// `path` decompressed into a temporary file.
5944fn decompressed_copy(
5945    path: &Path,
5946    compression: crate::CompressionFormat,
5947) -> std::io::Result<tempfile::NamedTempFile> {
5948    use std::io::Read as _;
5949    let file = std::fs::File::open(path)?;
5950    let mut reader: Box<dyn std::io::Read> = match compression {
5951        crate::CompressionFormat::Gzip => Box::new(flate2::read::GzDecoder::new(file)),
5952        crate::CompressionFormat::Zstd => Box::new(zstd::Decoder::new(file)?),
5953        crate::CompressionFormat::Bzip2 => Box::new(bzip2::read::BzDecoder::new(file)),
5954        crate::CompressionFormat::Xz => Box::new(xz2::read::XzDecoder::new(file)),
5955    };
5956    let mut copy = tempfile::NamedTempFile::new()?;
5957    std::io::copy(&mut reader.by_ref(), copy.as_file_mut())?;
5958    Ok(copy)
5959}
5960
5961/// A directory of one spec's files, `{date}/{venue}/trades.bin`: one table, the parts
5962/// of each file's path as columns.
5963pub mod files {
5964    use super::{Bytes, Opened, Spec, SpecRecords};
5965    use polars::prelude::*;
5966    use std::path::{Path, PathBuf};
5967    use std::sync::Arc;
5968
5969    /// The most files one tree is read from.
5970    const MAX_FILES: usize = 100_000;
5971
5972    /// Each file's records and the values of its path's parts.
5973    pub struct SpecFiles {
5974        parts: Vec<(Arc<dyn SpecRecords>, Vec<AnyValue<'static>>)>,
5975        part_fields: Vec<(PlSmallStr, DataType)>,
5976        /// The first row of each file.
5977        starts: Vec<usize>,
5978        rows: usize,
5979        schema: SchemaRef,
5980        sources: Vec<Arc<Bytes>>,
5981    }
5982
5983    /// One part's pattern, compiled: its regex, and the part names it captures.
5984    fn component_regex(component: &str) -> Result<(regex::Regex, Vec<String>), String> {
5985        let mut out = String::from("^");
5986        let mut names = Vec::new();
5987        let mut rest = component;
5988        while let Some(open) = rest.find('{') {
5989            out.push_str(&regex::escape(&rest[..open]));
5990            let after = &rest[open + 1..];
5991            let close = after.find('}').ok_or("a `{` without its `}`")?;
5992            let inside = &after[..close];
5993            let name = inside.split_once(':').map_or(inside, |(n, _)| n);
5994            out.push_str("(.+?)");
5995            names.push(name.to_string());
5996            rest = &after[close + 1..];
5997        }
5998        out.push_str(&regex::escape(rest));
5999        out.push('$');
6000        Ok((regex::Regex::new(&out).map_err(|e| e.to_string())?, names))
6001    }
6002
6003    /// A part's name and the text it matched in a path.
6004    type PartText = (String, String);
6005
6006    /// The files under `dir` the pattern names, each with its parts' text.
6007    fn matching(dir: &Path, pattern: &str) -> Result<Vec<(PathBuf, Vec<PartText>)>, String> {
6008        let components: Vec<(regex::Regex, Vec<String>)> = pattern
6009            .split('/')
6010            .map(component_regex)
6011            .collect::<Result<_, _>>()?;
6012        let mut found = Vec::new();
6013        let mut stack = vec![(dir.to_path_buf(), 0usize, Vec::<PartText>::new())];
6014        while let Some((at, depth, parts)) = stack.pop() {
6015            let Ok(listing) = std::fs::read_dir(&at) else {
6016                continue;
6017            };
6018            let (re, names) = &components[depth];
6019            let last = depth + 1 == components.len();
6020            for entry in listing.flatten() {
6021                let name = entry.file_name().to_string_lossy().into_owned();
6022                let Some(caps) = re.captures(&name) else {
6023                    continue;
6024                };
6025                let mut parts = parts.clone();
6026                for (i, part) in names.iter().enumerate() {
6027                    parts.push((
6028                        part.clone(),
6029                        caps.get(i + 1).map_or("", |m| m.as_str()).to_string(),
6030                    ));
6031                }
6032                let path = entry.path();
6033                if last {
6034                    if path.is_file() {
6035                        found.push((path, parts));
6036                        if found.len() > MAX_FILES {
6037                            return Err(format!("more than {MAX_FILES} files match"));
6038                        }
6039                    }
6040                } else if path.is_dir() {
6041                    stack.push((path, depth + 1, parts));
6042                }
6043            }
6044        }
6045        found.sort_by(|a, b| a.0.cmp(&b.0));
6046        Ok(found)
6047    }
6048
6049    /// Open the tree of `spec`'s files under `dir`.
6050    pub fn open(spec: &Spec, dir: &Path) -> Result<Opened, String> {
6051        let files = spec.files.as_ref().expect("a spec of files");
6052        if !dir.is_dir() {
6053            return Err(format!(
6054                "{} reads a directory of its files ({}), and {} is not one",
6055                spec.name,
6056                files.pattern,
6057                dir.display()
6058            ));
6059        }
6060        let mut one = spec.clone();
6061        one.files = None;
6062        let mut parts = Vec::new();
6063        let mut notes = Vec::new();
6064        let mut header = None;
6065        let mut sources = Vec::new();
6066        let mut schema: Option<SchemaRef> = None;
6067        let found = matching(dir, &files.pattern)?;
6068        if found.is_empty() {
6069            return Err(format!(
6070                "no files under {} match {}",
6071                dir.display(),
6072                files.pattern
6073            ));
6074        }
6075        for (path, texts) in found {
6076            let shown = path
6077                .strip_prefix(dir)
6078                .unwrap_or(&path)
6079                .display()
6080                .to_string();
6081            let mut values = Vec::new();
6082            let mut ok = true;
6083            for part in &files.parts {
6084                let text = texts
6085                    .iter()
6086                    .find(|(n, _)| *n == part.name)
6087                    .map_or("", |(_, t)| t.as_str());
6088                match &part.date {
6089                    Some(format) => match chrono::NaiveDate::parse_from_str(text, format) {
6090                        Ok(date) => {
6091                            let days = (date
6092                                - chrono::NaiveDate::from_ymd_opt(1970, 1, 1).expect("a date"))
6093                            .num_days();
6094                            values.push(AnyValue::Date(days as i32));
6095                        }
6096                        Err(_) => {
6097                            notes.push(format!(
6098                                "{shown}: `{text}` is not a date as {format}; left out"
6099                            ));
6100                            ok = false;
6101                            break;
6102                        }
6103                    },
6104                    None => values.push(AnyValue::StringOwned(text.into())),
6105                }
6106            }
6107            if !ok {
6108                continue;
6109            }
6110            let opened = match one.open(&path, &shown) {
6111                Ok(o) => o,
6112                Err(e) => {
6113                    notes.push(format!("{shown}: {e}; left out"));
6114                    continue;
6115                }
6116            };
6117            let theirs = opened.records.schema();
6118            match &schema {
6119                Some(s) if *s != theirs => {
6120                    notes.push(format!(
6121                        "{shown}: its columns differ from the first file's; left out"
6122                    ));
6123                    continue;
6124                }
6125                Some(_) => {}
6126                None => schema = Some(theirs),
6127            }
6128            notes.extend(opened.notes.into_iter().map(|n| format!("{shown}: {n}")));
6129            sources.extend(opened.records.sources().iter().cloned());
6130            if header.is_none() {
6131                header = Some(opened.header);
6132            }
6133            parts.push((opened.records, values));
6134        }
6135        let Some(record_schema) = schema else {
6136            return Err(format!(
6137                "none of the files under {} could be read",
6138                dir.display()
6139            ));
6140        };
6141        let part_fields: Vec<(PlSmallStr, DataType)> = files
6142            .parts
6143            .iter()
6144            .map(|p| {
6145                (
6146                    PlSmallStr::from(p.name.as_str()),
6147                    if p.date.is_some() {
6148                        DataType::Date
6149                    } else {
6150                        DataType::String
6151                    },
6152                )
6153            })
6154            .collect();
6155        let mut fields: Vec<Field> = part_fields
6156            .iter()
6157            .map(|(n, d)| Field::new(n.clone(), d.clone()))
6158            .collect();
6159        fields.extend(record_schema.iter_fields());
6160        let mut starts = Vec::with_capacity(parts.len());
6161        let mut rows = 0usize;
6162        for (records, _) in &parts {
6163            starts.push(rows);
6164            rows = rows.saturating_add(records.rows());
6165        }
6166        let records = SpecFiles {
6167            parts,
6168            part_fields,
6169            starts,
6170            rows: rows.min(IdxSize::MAX as usize),
6171            schema: Arc::new(Schema::from_iter(fields)),
6172            sources,
6173        };
6174        Ok(Opened {
6175            records: Arc::new(records),
6176            notes,
6177            header: header.unwrap_or_default(),
6178        })
6179    }
6180
6181    impl std::fmt::Debug for SpecFiles {
6182        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6183            f.debug_struct("SpecFiles")
6184                .field("files", &self.parts.len())
6185                .field("rows", &self.rows)
6186                .finish()
6187        }
6188    }
6189
6190    impl SpecFiles {
6191        /// `lf`, the rows of part `i`, with the part's path values in front.
6192        fn dressed(&self, i: usize, lf: LazyFrame) -> LazyFrame {
6193            let mut exprs: Vec<Expr> = self
6194                .part_fields
6195                .iter()
6196                .zip(&self.parts[i].1)
6197                .map(|((name, dtype), value)| {
6198                    let value = match value {
6199                        AnyValue::Date(d) => lit(*d).cast(DataType::Date),
6200                        AnyValue::StringOwned(s) => lit(s.as_str()),
6201                        _ => lit(NULL).cast(dtype.clone()),
6202                    };
6203                    value.alias(name.clone())
6204                })
6205                .collect();
6206            exprs.push(all().as_expr());
6207            lf.select(exprs)
6208        }
6209    }
6210
6211    impl crate::pushdown::Windowed for SpecFiles {
6212        fn window(&self, start: usize, len: usize) -> PolarsResult<LazyFrame> {
6213            let end = start.saturating_add(len).min(self.rows);
6214            let mut frames = Vec::new();
6215            let first = self
6216                .starts
6217                .partition_point(|s| *s <= start)
6218                .saturating_sub(1);
6219            for i in first..self.parts.len() {
6220                let begin = self.starts[i];
6221                if begin >= end {
6222                    break;
6223                }
6224                let rows = self.parts[i].0.rows();
6225                let from = start.saturating_sub(begin).min(rows);
6226                let take = (end - begin).min(rows) - from;
6227                if take == 0 {
6228                    continue;
6229                }
6230                frames.push(self.dressed(i, self.parts[i].0.window(from, take)?));
6231            }
6232            if frames.is_empty() {
6233                return Ok(DataFrame::empty_with_schema(&self.schema).lazy());
6234            }
6235            concat(frames, UnionArgs::default())
6236        }
6237    }
6238
6239    impl SpecRecords for SpecFiles {
6240        fn rows(&self) -> usize {
6241            self.rows
6242        }
6243        fn schema(&self) -> SchemaRef {
6244            self.schema.clone()
6245        }
6246        fn into_lazy(self: Arc<Self>) -> PolarsResult<LazyFrame> {
6247            let frames = (0..self.parts.len())
6248                .map(|i| Ok(self.dressed(i, self.parts[i].0.clone().into_lazy()?)))
6249                .collect::<PolarsResult<Vec<_>>>()?;
6250            concat(frames, UnionArgs::default())
6251        }
6252        fn collect(&self, rows: usize) -> PolarsResult<DataFrame> {
6253            crate::pushdown::Windowed::window(self, 0, rows)?.collect()
6254        }
6255        fn sources(&self) -> &[Arc<Bytes>] {
6256            &self.sources
6257        }
6258    }
6259}
6260
6261#[cfg(test)]
6262mod tests {
6263    use super::*;
6264    use polars::prelude::*;
6265
6266    const L2: &str = r#"
6267name = "acme.l2feed"
6268match = { glob = ["*.l2"], magic = "L2FD" }
6269endian = "le"
6270
6271[header]
6272fields = [{ name = "magic", type = "str", size = 4 }, { name = "count", type = "u8" }]
6273
6274[records]
6275count = "header.count"
6276fields = [
6277  { name = "ts",     type = "u8", time = "ns" },
6278  { name = "symbol", type = "str", size = 8 },
6279  { name = "side",   type = "u1", enum = { 1 = "BUY", 2 = "SELL" } },
6280  { name = "price",  type = "u4", scale = 4 },
6281]
6282"#;
6283
6284    fn l2_file(records: &[(u64, &str, u8, u32)], count: u64, trailing: &[u8]) -> Vec<u8> {
6285        let mut out = b"L2FD".to_vec();
6286        out.extend(count.to_le_bytes());
6287        for (ts, symbol, side, price) in records {
6288            out.extend(ts.to_le_bytes());
6289            let mut sym = symbol.as_bytes().to_vec();
6290            sym.resize(8, b' ');
6291            out.extend(sym);
6292            out.push(*side);
6293            out.extend(price.to_le_bytes());
6294        }
6295        out.extend(trailing);
6296        out
6297    }
6298
6299    fn open(spec: &Spec, bytes: Vec<u8>) -> Opened {
6300        spec.open_rows(Arc::new(Bytes::Owned(bytes)), "f").unwrap()
6301    }
6302
6303    /// Records past the most a table holds are not shown, and a note says so.
6304    #[test]
6305    fn records_past_the_table_limit_are_noted() {
6306        let records = FixedRecords::new(
6307            vec![Arc::new(Bytes::Owned(vec![0; 4]))],
6308            vec![ColumnLayout::new("a", 0, 1, Physical::Unsigned(1), 1)],
6309            usize::MAX,
6310        )
6311        .unwrap();
6312        let mut notes = Vec::new();
6313        past_limit(&mut notes, 4, &records);
6314        assert!(notes.is_empty());
6315        past_limit(&mut notes, 9, &records);
6316        assert_eq!(notes, ["last 5 records not shown: past the table limit"]);
6317    }
6318
6319    fn collect(opened: &Opened) -> DataFrame {
6320        Arc::clone(&opened.records)
6321            .into_lazy()
6322            .unwrap()
6323            .collect()
6324            .unwrap()
6325    }
6326
6327    fn cell(df: &DataFrame, column: &str, row: usize) -> String {
6328        df.column(column).unwrap().get(row).unwrap().to_string()
6329    }
6330
6331    #[test]
6332    fn the_issue_s_example_reads() {
6333        let spec = Spec::parse(L2, None).unwrap();
6334        assert_eq!(spec.name, "acme.l2feed");
6335        assert!(spec.glob_matches(Path::new("/x/day.l2")));
6336        let bytes = l2_file(&[(5, "AAPL", 1, 1_234_500), (6, "MSFT", 2, 7)], 2, &[]);
6337        assert!(spec.magic_matches(&bytes));
6338        let opened = open(&spec, bytes);
6339        assert!(opened.notes.is_empty(), "{:?}", opened.notes);
6340        let df = collect(&opened);
6341        assert_eq!(df.height(), 2);
6342        assert_eq!(cell(&df, "symbol", 0), "\"AAPL\"");
6343        assert_eq!(cell(&df, "side", 1), "\"SELL\"");
6344        assert_eq!(
6345            df.column("price").unwrap().dtype(),
6346            &DataType::Decimal(38, 4)
6347        );
6348        assert_eq!(cell(&df, "price", 0), "123.4500");
6349        assert_eq!(
6350            df.column("ts").unwrap().dtype(),
6351            &DataType::Datetime(TimeUnit::Nanoseconds, None)
6352        );
6353    }
6354
6355    #[test]
6356    fn errors_point_at_the_line_and_column() {
6357        let text = "name = \"a.b\"\n[records]\nfields = [{ name = \"x\", type = \"u9\" }]\n";
6358        let e = Spec::parse(text, Some(Path::new("a.toml"))).unwrap_err();
6359        assert_eq!((e.line, e.column), (3, 32), "{e}");
6360        assert!(
6361            e.to_string()
6362                .starts_with("\"a.toml\":3:32: type: expected u1"),
6363            "{e}"
6364        );
6365        let e = Spec::parse(
6366            "name = \"a.b\"\n[records]\nfields = [{ name = \"x\", type = \"u4\", colour = 1 }]",
6367            None,
6368        )
6369        .unwrap_err();
6370        assert!(e.message.contains("unknown key `colour`"), "{e}");
6371        assert_eq!(e.line, 3);
6372        let e = Spec::parse("name = \"a.b\"\n[records\n", None).unwrap_err();
6373        assert_eq!(e.line, 2, "{e}");
6374    }
6375
6376    #[test]
6377    fn bad_specs_are_refused_by_name() {
6378        let record = |fields: &str| format!("name = \"a.b\"\n[records]\nfields = [{fields}]");
6379        for (text, said) in [
6380            ("name = \"a.b\"\n[records]\nframing = \"length_prefixed\"\nfields = [{ name = \"x\", type = \"u1\" }]".to_string(), "size names the field"),
6381            ("name = \"a.b\"\n[records]\nframing = \"sync\"\nfields = [{ name = \"x\", type = \"u1\" }]".to_string(), "needs sync"),
6382            ("name = \"a.b\"\n[blocks]\nheader = [{ name = \"n\", type = \"u4\" }]\nsize = \"n\"\ncompression = \"lzma9\"\n[records]\nfields = [{ name = \"x\", type = \"u1\" }]".to_string(), "expected one of none"),
6383            ("name = \"a.b\"\n[footer]\nfields = [{ name = \"n\", type = \"u4\" }]\n[records]\ncount = \"footer.m\"\nfields = [{ name = \"x\", type = \"u1\" }]".to_string(), "no footer field named `m`"),
6384            ("name = \"a.b\"\n[records]\ntype = \"k\"\nfields = [{ name = \"k\", type = \"u1\" }]\n[[variants]]\nname = \"a\"\nwhen = \"x\"\nfields = []".to_string(), "`k` is a u1"),
6385            ("name = \"a.b\"\n[records]\ntype = \"k\"\nfields = [{ name = \"k\", type = \"u1\" }]\n[[variants]]\nname = \"a\"\nwhen = []\nfields = []".to_string(), "empty list picks no record"),
6386            ("name = \"a.b\"\n[records]\nframing = \"sync\"\nsync = \"\u{1bb}a\"\nfields = [{ name = \"x\", type = \"u1\" }]".to_string(), "expected hex"),
6387            ("name = \"ab\"\n[records]\nfields = [{ name = \"x\", type = \"u1\" }]".to_string(), "namespaced"),
6388            ("name = \"a.b\"\n[records]\nsize = 2\nfields = [{ name = \"x\", type = \"u4\" }]".to_string(), "more than 2"),
6389            (record("{ name = \"x\", type = \"f4\", scale = 2 }"), "integer types"),
6390            (record("{ name = \"x\", type = \"u4\", scale = 2, enum = { 1 = \"a\" } }"), "one of time"),
6391            (record("{ name = \"x\", type = \"f4\", null = \"min\" }"), "does not fit"),
6392            (record("{ name = \"x\", type = \"u4\", flatten = true }"), "flatten"),
6393            (record("{ name = \"x\", type = \"u4\", of_day = true }"), "of_day goes with time"),
6394            (record("{ name = \"x\", type = \"u4\", date = \"ddmmyy\" }"), "yyyymmdd"),
6395            (record("{ name = \"x\", type = \"u4\", time = \"ns\", of_day = true, date = \"trade\" }"), "header field"),
6396            (record("{ type = \"pad\", size = 2, name = \"p\" }"), "only a size"),
6397            (record("{ name = \"x\", type = \"u1\", count = 67108864, flatten = true }"), "at most 1024"),
6398            (record("{ name = \"x\", type = \"u4\", null = -1 }"), "holds 0 to 4294967295"),
6399            (record("{ name = \"x\", type = \"s1\", null = 128 }"), "holds -128 to 127"),
6400            ("name = \"a.b\"\nlayout = \"columns\"\n[records]\nfields = [{ name = \"x\", type = \"u1\", file = \"../x\" }]".to_string(), "name of a file"),
6401            ("name = \"a.b\"\nlayout = \"columns\"\n[records]\nfields = [{ name = \"/etc/x\", type = \"u1\" }]".to_string(), "cannot hold a path"),
6402        ] {
6403            let e = Spec::parse(&text, None).unwrap_err();
6404            assert!(e.message.contains(said), "{text}: {e}");
6405        }
6406    }
6407
6408    /// Every type and meaning, in both byte orders, from one record.
6409    #[test]
6410    fn every_field_type_in_both_byte_orders() {
6411        for (endian, big) in [("le", false), ("be", true)] {
6412            let text = format!(
6413                r#"name = "t.all"
6414endian = "{endian}"
6415[records]
6416fields = [
6417  {{ name = "u1", type = "u1" }}, {{ name = "u2", type = "u2" }}, {{ name = "u3", type = "u3" }},
6418  {{ name = "u4", type = "u4" }}, {{ name = "u5", type = "u5" }}, {{ name = "u8", type = "u8" }},
6419  {{ name = "s1", type = "s1" }}, {{ name = "s2", type = "s2" }}, {{ name = "s3", type = "s3" }},
6420  {{ name = "s4", type = "s4" }}, {{ name = "s6", type = "s6" }}, {{ name = "s8", type = "s8" }},
6421  {{ name = "f4", type = "f4" }}, {{ name = "f8", type = "f8" }},
6422  {{ name = "flag", type = "bool" }},
6423  {{ name = "s", type = "str", size = 3 }}, {{ name = "b", type = "bytes", size = 2 }},
6424  {{ type = "pad", size = 1 }},
6425  {{ name = "t", type = "s4", time = "s", epoch = 2000-01-01 }},
6426  {{ name = "day", type = "u2", time = "days", epoch = "2000-01-01" }},
6427  {{ name = "ymd", type = "u4", date = "yyyymmdd" }},
6428  {{ name = "tod", type = "u4", time = "ms", of_day = true }},
6429  {{ name = "serial", type = "f8", time = "days", epoch = "1899-12-30" }},
6430  {{ name = "d", type = "s2", scale = 2 }},
6431  {{ name = "c", type = "s2", factor = 0.5, offset = -40.0 }},
6432  {{ name = "e", type = "u1", enum = {{ 7 = "seven" }} }},
6433  {{ name = "n", type = "s4", null = "min" }},
6434  {{ name = "le", type = "u2le" }}, {{ name = "be", type = "u2be" }},
6435  {{ name = "arr", type = "u1", count = 3 }},
6436  {{ name = "lv", type = "u1", count = 2, flatten = true }},
6437]"#
6438            );
6439            let spec = Spec::parse(&text, None).unwrap();
6440            let mut bytes = Vec::new();
6441            let put = |bytes: &mut Vec<u8>, le: &[u8]| {
6442                if big {
6443                    bytes.extend(le.iter().rev());
6444                } else {
6445                    bytes.extend(le);
6446                }
6447            };
6448            bytes.push(200);
6449            put(&mut bytes, &60_000u16.to_le_bytes());
6450            put(&mut bytes, &16_000_000u32.to_le_bytes()[..3]);
6451            put(&mut bytes, &4_000_000_000u32.to_le_bytes());
6452            put(&mut bytes, &1_099_511_627_775u64.to_le_bytes()[..5]);
6453            put(&mut bytes, &u64::MAX.to_le_bytes());
6454            bytes.push(-5i8 as u8);
6455            put(&mut bytes, &(-300i16).to_le_bytes());
6456            put(&mut bytes, &(-70_000i32).to_le_bytes()[..3]);
6457            put(&mut bytes, &(-70_000i32).to_le_bytes());
6458            put(&mut bytes, &(-1i64).to_le_bytes()[..6]);
6459            put(&mut bytes, &i64::MIN.to_le_bytes());
6460            put(&mut bytes, &1.5f32.to_le_bytes());
6461            put(&mut bytes, &(-2.25f64).to_le_bytes());
6462            bytes.push(9);
6463            bytes.extend(b"hi\0");
6464            bytes.extend([0xde, 0xad]);
6465            bytes.push(0xff);
6466            put(&mut bytes, &86_400i32.to_le_bytes());
6467            put(&mut bytes, &2u16.to_le_bytes());
6468            put(&mut bytes, &20240229u32.to_le_bytes());
6469            put(&mut bytes, &34_200_000u32.to_le_bytes());
6470            put(&mut bytes, &2.5f64.to_le_bytes());
6471            put(&mut bytes, &(-1234i16).to_le_bytes());
6472            put(&mut bytes, &100i16.to_le_bytes());
6473            bytes.push(7);
6474            put(&mut bytes, &i32::MIN.to_le_bytes());
6475            bytes.extend(513u16.to_le_bytes());
6476            bytes.extend(513u16.to_be_bytes());
6477            bytes.extend([1, 2, 3]);
6478            bytes.extend([4, 5]);
6479            let df = collect(&open(&spec, bytes));
6480            let row: Vec<String> = df
6481                .columns()
6482                .iter()
6483                .map(|c| c.get(0).unwrap().to_string())
6484                .collect();
6485            assert_eq!(
6486                row,
6487                [
6488                    "200",
6489                    "60000",
6490                    "16000000",
6491                    "4000000000",
6492                    "1099511627775",
6493                    "18446744073709551615",
6494                    "-5",
6495                    "-300",
6496                    "-70000",
6497                    "-70000",
6498                    "-1",
6499                    "-9223372036854775808",
6500                    "1.5",
6501                    "-2.25",
6502                    "true",
6503                    "\"hi\"",
6504                    "b\"\\xde\\xad\"",
6505                    "2000-01-02 00:00:00",
6506                    "2000-01-03",
6507                    "2024-02-29",
6508                    "09:30:00",
6509                    "1900-01-01 12:00:00",
6510                    "-12.34",
6511                    "10.0",
6512                    "\"seven\"",
6513                    "null",
6514                    "513",
6515                    "513",
6516                    "[1, 2, 3]",
6517                    "4",
6518                    "5",
6519                ],
6520                "{endian}"
6521            );
6522            assert_eq!(
6523                df.get_column_names(),
6524                [
6525                    "u1", "u2", "u3", "u4", "u5", "u8", "s1", "s2", "s3", "s4", "s6", "s8", "f4",
6526                    "f8", "flag", "s", "b", "t", "day", "ymd", "tod", "serial", "d", "c", "e", "n",
6527                    "le", "be", "arr", "lv_0", "lv_1"
6528                ]
6529            );
6530        }
6531    }
6532
6533    #[test]
6534    fn a_time_of_day_takes_its_date_from_the_header() {
6535        let text = r#"name = "t.tod"
6536[header]
6537fields = [{ name = "trade_date", type = "u4", date = "yyyymmdd" }]
6538[records]
6539fields = [{ name = "ts", type = "u6be", time = "ns", of_day = true, date = "header.trade_date" }]"#;
6540        let spec = Spec::parse(text, None).unwrap();
6541        let mut bytes = 20240102u32.to_le_bytes().to_vec();
6542        bytes.extend(&34_200_000_000_123u64.to_be_bytes()[2..]);
6543        let df = collect(&open(&spec, bytes));
6544        assert_eq!(cell(&df, "ts", 0), "2024-01-02 09:30:00.000000123");
6545    }
6546
6547    #[test]
6548    fn a_bad_magic_is_refused_saying_what_was_found() {
6549        let spec = Spec::parse(L2, None).unwrap();
6550        let mut bytes = l2_file(&[(1, "A", 1, 1)], 1, &[]);
6551        bytes[..4].copy_from_slice(b"NOPE");
6552        let e = spec
6553            .open_rows(Arc::new(Bytes::Owned(bytes)), "x.l2")
6554            .err()
6555            .unwrap();
6556        assert!(
6557            e.contains("expected magic 4c 32 46 44 at byte 0, found 4e 4f 50 45"),
6558            "{e}"
6559        );
6560    }
6561
6562    #[test]
6563    fn a_truncated_last_record_is_left_out_and_shown() {
6564        let text = "name = \"t.x\"\n[records]\nfields = [{ name = \"a\", type = \"u4\" }]";
6565        let spec = Spec::parse(text, None).unwrap();
6566        let bytes = vec![1, 0, 0, 0, 2, 0, 0, 0, 0xab, 0xcd];
6567        let opened = spec
6568            .open_rows(Arc::new(Bytes::Owned(bytes)), "x.bin")
6569            .unwrap();
6570        assert_eq!(collect(&opened).height(), 2);
6571        assert_eq!(
6572            opened.notes,
6573            ["x.bin: 2 trailing bytes left out, not a whole record: ab cd"]
6574        );
6575        // The header's count says more than is there: the whole records are shown.
6576        let spec = Spec::parse(L2, None).unwrap();
6577        let opened = open(&spec, l2_file(&[(1, "A", 1, 1)], 3, &[9]));
6578        assert_eq!(collect(&opened).height(), 1);
6579        assert!(
6580            opened.notes[0].contains("says 3 records"),
6581            "{:?}",
6582            opened.notes
6583        );
6584    }
6585
6586    #[test]
6587    fn sizes_come_from_the_header_and_are_bounded() {
6588        let text = r#"name = "t.h"
6589[header]
6590fields = [{ name = "len", type = "u1" }, { name = "title", type = "str", size = "len" }, { name = "rec", type = "u2" }]
6591[records]
6592size = "header.rec"
6593size_adjust = 1
6594fields = [{ name = "a", type = "u1" }, { name = "b", type = "bytes", size = "header.len" }]"#;
6595        let spec = Spec::parse(text, None).unwrap();
6596        let mut bytes = vec![2, b'h', b'i', 3, 0];
6597        bytes.extend([1, 0xa, 0xb, 0xff, 2, 0xc, 0xd, 0xff]);
6598        let opened = open(&spec, bytes);
6599        let df = collect(&opened);
6600        assert_eq!(df.height(), 2);
6601        assert_eq!(
6602            df.column("a").unwrap().u8().unwrap().to_vec(),
6603            [Some(1), Some(2)]
6604        );
6605        assert_eq!(opened.header.text("title").as_deref(), Some("hi"));
6606        // A size the file gives past the bound is refused, not believed.
6607        let text = r#"name = "t.h"
6608[header]
6609fields = [{ name = "len", type = "u8" }]
6610[records]
6611fields = [{ name = "b", type = "bytes", size = "header.len" }]"#;
6612        let spec = Spec::parse(text, None).unwrap();
6613        let e = spec
6614            .open_rows(Arc::new(Bytes::Owned(u64::MAX.to_le_bytes().to_vec())), "h")
6615            .err()
6616            .unwrap();
6617        assert!(e.contains("outside 0 to"), "{e}");
6618    }
6619
6620    #[test]
6621    fn a_columns_layout_reads_one_file_per_field() {
6622        let dir = tempfile::tempdir().unwrap();
6623        let text = r#"name = "kdb.trades"
6624layout = "columns"
6625match = { glob = "trades" }
6626[header]
6627size = 8
6628[records]
6629fields = [{ name = "price", type = "f8" }, { name = "size", type = "s4", file = "qty" }]"#;
6630        let spec = Spec::parse(text, None).unwrap();
6631        let mut price = vec![0u8; 8];
6632        for p in [1.5f64, 2.5, 3.5] {
6633            price.extend(p.to_le_bytes());
6634        }
6635        let mut qty = vec![0u8; 8];
6636        for q in [10i32, 20] {
6637            qty.extend(q.to_le_bytes());
6638        }
6639        std::fs::write(dir.path().join("price"), price).unwrap();
6640        std::fs::write(dir.path().join("qty"), qty).unwrap();
6641        let opened = spec.open(dir.path(), "trades").unwrap();
6642        let df = collect(&opened);
6643        assert_eq!(df.height(), 2);
6644        assert_eq!(
6645            df.column("size").unwrap().i32().unwrap().to_vec(),
6646            [Some(10), Some(20)]
6647        );
6648        assert!(
6649            opened.notes[0].contains("price 3, qty 2"),
6650            "{:?}",
6651            opened.notes
6652        );
6653        let registry = Registry::of(vec![spec]);
6654        assert_eq!(registry.by_glob(Path::new("/db/trades"), true).len(), 1);
6655        assert!(registry.by_glob(Path::new("/db/trades"), false).is_empty());
6656    }
6657
6658    /// A header sized by its own field may differ from file to file; each column
6659    /// starts after its own file's header.
6660    #[test]
6661    fn each_column_file_starts_after_its_own_header() {
6662        let dir = tempfile::tempdir().unwrap();
6663        let text = r#"name = "t.cols"
6664layout = "columns"
6665[header]
6666fields = [{ name = "len", type = "u1" }]
6667size = "len"
6668[records]
6669fields = [{ name = "a", type = "u1" }, { name = "b", type = "u1" }]"#;
6670        let spec = Spec::parse(text, None).unwrap();
6671        std::fs::write(dir.path().join("a"), [2, 0, 7, 8]).unwrap();
6672        std::fs::write(dir.path().join("b"), [4, 0, 0, 0, 9, 10]).unwrap();
6673        let df = collect(&spec.open(dir.path(), "t").unwrap());
6674        assert_eq!(
6675            df.column("a").unwrap().u8().unwrap().to_vec(),
6676            [Some(7), Some(8)]
6677        );
6678        assert_eq!(
6679            df.column("b").unwrap().u8().unwrap().to_vec(),
6680            [Some(9), Some(10)]
6681        );
6682    }
6683
6684    #[test]
6685    fn the_search_path_keeps_the_first_of_each_name() {
6686        let a = tempfile::tempdir().unwrap();
6687        let b = tempfile::tempdir().unwrap();
6688        std::fs::write(a.path().join("l2.toml"), L2).unwrap();
6689        std::fs::write(b.path().join("l2.toml"), L2.replace("*.l2", "*.lvl2")).unwrap();
6690        std::fs::write(b.path().join("bad.toml"), "name = 3").unwrap();
6691        std::fs::write(b.path().join("notes.txt"), "not a spec").unwrap();
6692        let path = vec![a.path().to_path_buf(), b.path().to_path_buf()];
6693        let registry = Registry::load(&path);
6694        assert_eq!(registry.specs.len(), 1);
6695        assert_eq!(registry.specs[0].spec.globs, ["*.l2"]);
6696        assert_eq!(registry.specs[0].overrides, [b.path().join("l2.toml")]);
6697        assert_eq!(registry.errors.len(), 1);
6698        let listing = registry.listing(&path);
6699        assert!(listing.contains("overrides"), "{listing}");
6700        assert!(
6701            listing.contains("bad.toml\":1:8: name: expected a string."),
6702            "{listing}"
6703        );
6704    }
6705
6706    #[test]
6707    fn glob_comes_before_magic_and_ties_are_kept() {
6708        let one = Spec::parse(L2, None).unwrap();
6709        let two = Spec::parse(&L2.replace("acme.l2feed", "acme.other"), None).unwrap();
6710        let magic_only = Spec::parse(
6711            &L2.replace("acme.l2feed", "acme.magic")
6712                .replace("glob = [\"*.l2\"], ", ""),
6713            None,
6714        )
6715        .unwrap();
6716        let registry = Registry::of(vec![one, two, magic_only]);
6717        let matched = registry
6718            .matching(Path::new("a.l2"), false, |_| {
6719                panic!("no read for a glob match")
6720            })
6721            .unwrap();
6722        assert_eq!(matched.by, Chosen::Glob);
6723        assert_eq!(matched.specs.len(), 2);
6724        let matched = registry
6725            .matching(Path::new("a.dat"), false, |n| {
6726                assert_eq!(n, 4);
6727                Some(b"L2FD".to_vec())
6728            })
6729            .unwrap();
6730        assert_eq!(matched.by, Chosen::Magic);
6731        assert_eq!(matched.specs.len(), 3);
6732        assert!(
6733            registry
6734                .matching(Path::new("a.dat"), false, |_| Some(b"nope".to_vec()))
6735                .is_none()
6736        );
6737    }
6738
6739    /// Fixed records say their columns from the spec alone, as the open finds them;
6740    /// framed records and a size from the header leave it to the open.
6741    #[test]
6742    fn fixed_records_say_their_columns_without_a_file() {
6743        let spec = Spec::parse(L2, None).unwrap();
6744        let opened = open(&spec, l2_file(&[(1, "A", 1, 1)], 1, &[]));
6745        let schema: Vec<(String, DataType)> = opened
6746            .records
6747            .schema()
6748            .iter()
6749            .map(|(n, t)| (n.to_string(), t.clone()))
6750            .collect();
6751        assert_eq!(spec.static_columns(), Some(schema));
6752        let sized = L2.replace("size = 8 }", "size = \"header.count\" }");
6753        assert_eq!(Spec::parse(&sized, None).unwrap().static_columns(), None);
6754        let framed = r#"name = "acme.f"
6755[records]
6756framing = "length_prefixed"
6757size = "len"
6758fields = [{ name = "len", type = "u2" }, { name = "x", type = "u1" }]"#;
6759        assert_eq!(Spec::parse(framed, None).unwrap().static_columns(), None);
6760    }
6761
6762    /// A listing names a file by a spec's magic and `where` from the bytes it read
6763    /// already, never more; a glob still comes first, and a name that says a format
6764    /// datui reads keeps it.
6765    #[test]
6766    fn a_listing_names_a_file_from_the_head_it_read() {
6767        let versioned = r#"name = "acme.v1"
6768match = { magic = "L2FD", where = { "header.version" = 1 } }
6769[header]
6770fields = [{ type = "pad", size = 4 }, { name = "version", type = "u1" }]
6771[records]
6772fields = [{ name = "x", type = "u1" }]"#;
6773        let globbed = r#"name = "acme.named"
6774match = { glob = "named.bin" }
6775[records]
6776fields = [{ name = "x", type = "u1" }]"#;
6777        let registry = Registry::of(vec![
6778            Spec::parse(versioned, None).unwrap(),
6779            Spec::parse(globbed, None).unwrap(),
6780        ]);
6781        let name = |path: &str, head: &[u8], whole: bool| {
6782            registry
6783                .listed(Path::new(path), head, whole)
6784                .map(|s| s.name.clone())
6785        };
6786        let v1 = b"L2FD\x01rest";
6787        assert_eq!(name("data.bin", v1, true).as_deref(), Some("acme.v1"));
6788        assert_eq!(name("data", v1, false).as_deref(), Some("acme.v1"));
6789        assert_eq!(name("data.bin", b"L2FD\x02rest", true), None);
6790        assert_eq!(name("data.bin", b"nope", true), None);
6791        // Shorter than the header, and the file goes on: not settled, not named.
6792        assert_eq!(name("data.bin", b"L2FD", false), None);
6793        assert_eq!(name("named.bin", v1, true).as_deref(), Some("acme.named"));
6794        assert_eq!(name("data.csv", v1, true), None);
6795        assert_eq!(name("data.bin.gz", v1, true), None);
6796    }
6797
6798    /// One spec per version, told apart by a header field after the magic.
6799    #[test]
6800    fn a_header_version_picks_the_spec() {
6801        let version = |v: u8| {
6802            format!(
6803                r#"name = "acme.v{v}"
6804match = {{ glob = "*.l2", magic = "L2FD", where = {{ "header.version" = {v} }} }}
6805[header]
6806fields = [{{ type = "pad", size = 4 }}, {{ name = "version", type = "u1" }}]
6807[records]
6808fields = [{{ name = "x", type = "u1" }}]"#
6809            )
6810        };
6811        let registry = Registry::of(vec![
6812            Spec::parse(&version(2), None).unwrap(),
6813            Spec::parse(&version(3), None).unwrap(),
6814        ]);
6815        for v in [2u8, 3] {
6816            let matched = registry
6817                .matching(Path::new("a.l2"), false, |n| {
6818                    assert_eq!(n, 5);
6819                    Some([b"L2FD".as_slice(), &[v]].concat())
6820                })
6821                .unwrap();
6822            let names: Vec<&str> = matched.specs.iter().map(|s| s.name.as_str()).collect();
6823            assert_eq!(names, [format!("acme.v{v}")]);
6824        }
6825        let e = Spec::parse(
6826            &version(2).replace("\"header.version\"", "\"header.nope\""),
6827            None,
6828        )
6829        .unwrap_err();
6830        assert!(e.message.contains("no header field named `nope`"), "{e}");
6831    }
6832
6833    #[test]
6834    fn the_search_path_is_config_dir_then_env_then_config() {
6835        let env = std::env::join_paths(["/org/a", "/org/b"]).unwrap();
6836        let path = search_path(Some(Path::new("/cfg")), Some(env), &["/c".to_string()]);
6837        assert_eq!(
6838            path,
6839            [
6840                PathBuf::from("/cfg/formats"),
6841                PathBuf::from("/org/a"),
6842                PathBuf::from("/org/b"),
6843                PathBuf::from("/c")
6844            ]
6845        );
6846    }
6847
6848    #[test]
6849    fn formats_check_validates_and_prints_the_first_rows() {
6850        let dir = tempfile::tempdir().unwrap();
6851        let spec = dir.path().join("l2.toml");
6852        std::fs::write(&spec, L2).unwrap();
6853        let data = dir.path().join("day.l2");
6854        std::fs::write(&data, l2_file(&[(1, "AAPL", 1, 10_000)], 1, &[7])).unwrap();
6855        let registry = Registry::default();
6856        let text = check(
6857            &spec.to_string_lossy(),
6858            Some(&data),
6859            &registry,
6860            &crate::OpenOptions::default(),
6861        )
6862        .unwrap();
6863        assert!(text.starts_with("acme.l2feed: ok"), "{text}");
6864        assert!(text.contains("warning: day.l2 has 1 byte after"), "{text}");
6865        assert!(
6866            text.contains("AAPL") && text.contains("1 records"),
6867            "{text}"
6868        );
6869        std::fs::write(&spec, "name = \"a.b\"\n[records]\nfields = 1").unwrap();
6870        let e = check(
6871            &spec.to_string_lossy(),
6872            None,
6873            &registry,
6874            &crate::OpenOptions::default(),
6875        )
6876        .unwrap_err();
6877        assert!(
6878            e.contains("l2.toml\":3:10: fields: expected an array"),
6879            "{e}"
6880        );
6881        assert!(
6882            check(
6883                "acme.nothing",
6884                None,
6885                &registry,
6886                &crate::OpenOptions::default()
6887            )
6888            .is_err()
6889        );
6890    }
6891
6892    const LOG: &str = r##"
6893name = "acme.instrument-log"
6894kind = "delimited"
6895match = { magic = "#device_info" }
6896
6897comment = "#"
6898skip_initial_space = true
6899header_rows = { name = 3, unit = 2 }
6900metadata_line = 1
6901
6902[columns]
6903time = { from = ["Lcl Date", "Lcl Time", "UTCOfst"], as = "datetime" }
6904"##;
6905
6906    const LOG_TEXT: &str = "#device_info, log_version=\"1.03\", model=\"X\"\n#yyyy-mm-dd, hh:mm:ss, hh:mm, deg F\n  Lcl Date, Lcl Time, UTCOfst, E1 CHT1\n          ,         ,       ,   187.2\n2024-03-01, 10:00:00, -05:00,   180.0\n";
6907
6908    #[test]
6909    fn the_delimited_example_parses() {
6910        use crate::delimited_spec::{DerivedKind, HeaderRows};
6911        let spec = Spec::parse(LOG, None).unwrap();
6912        assert!(spec.is_delimited());
6913        assert_eq!(spec.magic, b"#device_info");
6914        let d = spec.delimited.as_deref().unwrap();
6915        assert_eq!(d.comment_char.as_deref(), Some("#"));
6916        assert_eq!(d.skip_initial_space, Some(true));
6917        assert_eq!(
6918            d.header_rows,
6919            Some(HeaderRows {
6920                name: vec![3],
6921                unit: Some(2)
6922            })
6923        );
6924        assert_eq!(d.metadata_line, Some(1));
6925        assert_eq!(d.columns[0].name, "time");
6926        assert_eq!(d.columns[0].kind, DerivedKind::Datetime);
6927        assert_eq!(d.columns[0].from.len(), 3);
6928        // A list of name lines joins them, as Frictionless does.
6929        let joined = Spec::parse(
6930            "name = \"a.b\"\nkind = \"delimited\"\nheader_rows = [1, 2]\ndelimiter = \"\\t\"\nnull_values = [\"NA\", \"x=-1\"]",
6931            None,
6932        )
6933        .unwrap();
6934        let d = joined.delimited.as_deref().unwrap();
6935        assert_eq!(d.header_rows.as_ref().unwrap().name, [1, 2]);
6936        assert_eq!(d.delimiter, Some(b'\t'));
6937        assert_eq!(d.null_values, ["NA", "x=-1"]);
6938        // A binary spec is not one.
6939        assert!(!Spec::parse(L2, None).unwrap().is_delimited());
6940    }
6941
6942    #[test]
6943    fn delimited_spec_errors_point_at_the_line_and_column() {
6944        let head = "name = \"a.b\"\nkind = \"delimited\"\n";
6945        for (rest, said) in [
6946            (
6947                "records = 1",
6948                "3:1: Unknown key `records` in a delimited spec",
6949            ),
6950            ("header_rows = { unit = 2 }", "header_rows: missing `name`"),
6951            (
6952                "header_rows = { name = 2, unit = 2 }",
6953                "header_rows.unit: line 2 is also a name line",
6954            ),
6955            (
6956                "header_rows = { name = 2, description = 1 }",
6957                "header_rows.description is not yet supported",
6958            ),
6959            (
6960                "header_rows = 0",
6961                "header_rows: expected a line from 1 to 1000",
6962            ),
6963            ("header_rows = [2, 2]", "header_rows: line 2 is named twice"),
6964            (
6965                "header_rows = 2\nmetadata_line = 5",
6966                "4:17: metadata_line: line 5 would be read as data",
6967            ),
6968            (
6969                "header_rows = 2\nmetadata_line = 2",
6970                "metadata_line: line 2 is a header line",
6971            ),
6972            (
6973                "delimiter = \"ab\"",
6974                "delimiter: \"ab\" is not one ASCII character",
6975            ),
6976            ("comment_char = \"#\"", "comment_char"),
6977            ("comment = \"\"", "comment: must not be empty"),
6978            (
6979                "match = { where = { \"header.v\" = 1 } }",
6980                "`where` compares a binary header's fields",
6981            ),
6982            (
6983                "[columns]\nt = { from = [\"a\", \"b\"], as = \"date\" }",
6984                "columns.t.from: as = \"date\" takes one column",
6985            ),
6986            (
6987                "[columns]\nt = { from = \"a\", as = \"instant\" }",
6988                "columns.t.as: expected datetime, date or time",
6989            ),
6990            (
6991                "[columns]\nt = { as = \"date\" }",
6992                "columns.t: missing `from`",
6993            ),
6994            ("kind = \"text\"", "kind: expected binary or delimited"),
6995        ] {
6996            let text = format!("{head}{rest}");
6997            let text = text.replacen(
6998                "kind = \"delimited\"\nkind = \"text\"",
6999                "kind = \"text\"",
7000                1,
7001            );
7002            let e = Spec::parse(&text, None).unwrap_err().to_string();
7003            assert!(e.contains(said), "{rest}: {e}");
7004        }
7005        // A metadata line below the header lines is fine when it is a comment line.
7006        let text = format!("{head}header_rows = 2\ncomment = \"#\"\nmetadata_line = 5");
7007        assert!(Spec::parse(&text, None).is_ok());
7008    }
7009
7010    #[test]
7011    fn a_delimited_spec_matches_csv_names_and_a_binary_spec_does_not() {
7012        let dir = tempfile::tempdir().unwrap();
7013        let csv = dir.path().join("flight.csv");
7014        std::fs::write(&csv, LOG_TEXT).unwrap();
7015        let binary = "name = \"acme.raw\"\nmatch = { glob = \"*.csv\", magic = \"#dev\" }\n[records]\nfields = [{ name = \"b\", type = \"u1\" }]";
7016        let registry = Registry::of(vec![
7017            Spec::parse(binary, None).unwrap(),
7018            Spec::parse(LOG, None).unwrap(),
7019        ]);
7020        let asked = Asked::default();
7021        match route(&csv, &asked, &registry).unwrap() {
7022            Route::Delimited(choice) => {
7023                assert_eq!(choice.spec.name, "acme.instrument-log");
7024                assert_eq!(choice.by, Chosen::Magic);
7025            }
7026            _ => panic!("the delimited spec reads the CSV"),
7027        }
7028        // Several files: only a delimited spec is asked about.
7029        let several = Asked {
7030            spec_name: Some("acme.raw".into()),
7031            text_only: true,
7032            ..Asked::default()
7033        };
7034        let Err(e) = route(&csv, &several, &registry) else {
7035            panic!("a binary spec does not read several files");
7036        };
7037        assert!(e.contains("reads one file"), "{e}");
7038        // The magic is the start of the first line, after a byte-order mark.
7039        let bom = dir.path().join("bom.csv");
7040        std::fs::write(&bom, format!("\u{feff}{LOG_TEXT}")).unwrap();
7041        assert!(matches!(
7042            route(&bom, &asked, &registry).unwrap(),
7043            Route::Delimited(_)
7044        ));
7045        // A CSV whose first line is not the magic is read as it always was.
7046        let plain = dir.path().join("plain.csv");
7047        std::fs::write(&plain, "a,b\n1,2\n").unwrap();
7048        assert!(matches!(
7049            route(&plain, &asked, &registry).unwrap(),
7050            Route::Elsewhere
7051        ));
7052    }
7053
7054    #[test]
7055    fn formats_check_prints_a_delimited_file_s_metadata_units_and_rows() {
7056        let dir = tempfile::tempdir().unwrap();
7057        let spec = dir.path().join("log.toml");
7058        std::fs::write(&spec, LOG).unwrap();
7059        let data = dir.path().join("flight.csv");
7060        std::fs::write(&data, LOG_TEXT).unwrap();
7061        let registry = Registry::default();
7062        let text = check(
7063            &spec.to_string_lossy(),
7064            Some(&data),
7065            &registry,
7066            &crate::OpenOptions::default(),
7067        )
7068        .unwrap();
7069        assert!(text.starts_with("acme.instrument-log: ok"), "{text}");
7070        assert!(
7071            text.contains("delimited: names on line 3, units on line 2, metadata on line 1"),
7072            "{text}"
7073        );
7074        assert!(
7075            text.contains("time = datetime from Lcl Date, Lcl Time, UTCOfst"),
7076            "{text}"
7077        );
7078        assert!(
7079            text.contains("metadata: device_info: log_version = 1.03, model = X"),
7080            "{text}"
7081        );
7082        assert!(text.contains("E1 CHT1 = deg F"), "{text}");
7083        assert!(text.contains("2024-03-01 15:00:00 UTC"), "{text}");
7084        let listing = Registry::of(vec![Spec::parse(LOG, None).unwrap()]).listing(&[]);
7085        assert!(
7086            listing.contains("acme.instrument-log  (delimited; magic #device_info)"),
7087            "{listing}"
7088        );
7089    }
7090
7091    /// DBC files share the search path: a `.dbc` file for every interface, a
7092    /// `kind = "dbc"` TOML file that names one for an interface, and one that does not
7093    /// parse is an error with its line.
7094    #[test]
7095    fn dbc_files_on_the_search_path() {
7096        let dir = tempfile::tempdir().unwrap();
7097        std::fs::write(dir.path().join("l2.toml"), L2).unwrap();
7098        std::fs::write(
7099            dir.path().join("car.dbc"),
7100            "BO_ 291 ENGINE: 8 ECU\n SG_ Speed : 0|16@1+ (0.125,0) [0|8191] \"rpm\" GW\n",
7101        )
7102        .unwrap();
7103        std::fs::write(
7104            dir.path().join("body.toml"),
7105            "kind = \"dbc\"\nfile = \"body/body.dbc\"\n[match]\ninterface = \"can1\"\n",
7106        )
7107        .unwrap();
7108        std::fs::create_dir(dir.path().join("body")).unwrap();
7109        std::fs::write(
7110            dir.path().join("body/body.dbc"),
7111            "BO_ 512 DOORS: 1 GW\n SG_ Open : 0|1@1+ (1,0) [0|1] \"\" ECU\n",
7112        )
7113        .unwrap();
7114        std::fs::write(dir.path().join("bad.dbc"), "BO_ 1 A: 8 X\n SG_ nope\n").unwrap();
7115        let path = vec![dir.path().to_path_buf()];
7116        let registry = Registry::load(&path);
7117        assert_eq!(registry.specs.len(), 1);
7118        let names: Vec<(&str, Option<&str>)> = registry
7119            .dbc
7120            .iter()
7121            .map(|f| (f.dbc.name.as_str(), f.dbc.interface.as_deref()))
7122            .collect();
7123        assert_eq!(names, [("body", Some("can1")), ("car", None)]);
7124        assert_eq!(registry.errors.len(), 1, "{:?}", registry.errors);
7125        assert_eq!(registry.errors[0].line, 2);
7126        let listing = registry.listing(&path);
7127        assert!(listing.contains("Dictionaries (DBC):"), "{listing}");
7128        assert!(
7129            listing.contains("body  (1 message, interface can1)"),
7130            "{listing}"
7131        );
7132    }
7133
7134    /// `formats check` takes a DBC file by name, as a `.dbc` file or as the `kind = "dbc"`
7135    /// TOML file that names one; with a candump log, it says how many frames it names.
7136    /// One that does not parse is refused at its line.
7137    #[test]
7138    fn formats_check_reads_dbc_files() {
7139        let dir = tempfile::tempdir().unwrap();
7140        std::fs::write(
7141            dir.path().join("car.dbc"),
7142            "BO_ 291 ENGINE: 8 ECU\n SG_ Speed : 0|16@1+ (0.125,0) [0|8191] \"rpm\" GW\n SG_ Temp : 16|8@1+ (1,-40) [-40|215] \"C\" GW\n",
7143        )
7144        .unwrap();
7145        std::fs::write(
7146            dir.path().join("body.toml"),
7147            "kind = \"dbc\"\nfile = \"car.dbc\"\n[match]\ninterface = \"can1\"\n",
7148        )
7149        .unwrap();
7150        let bad = dir.path().join("bad.dbc");
7151        std::fs::write(&bad, "BO_ 1 A: 8 X\n SG_ nope\n").unwrap();
7152        let registry = Registry::load(&[dir.path().to_path_buf()]);
7153        let options = crate::OpenOptions::default();
7154
7155        let text = check("car", None, &registry, &options).unwrap();
7156        assert!(text.starts_with("car: ok\n"), "{text}");
7157        assert!(text.contains("car.dbc"), "{text}");
7158        assert!(text.contains("1 message, 2 signals"), "{text}");
7159
7160        let toml = dir.path().join("body.toml");
7161        let text = check(&toml.to_string_lossy(), None, &registry, &options).unwrap();
7162        assert!(text.contains("matches interface can1"), "{text}");
7163
7164        let log = dir.path().join("drive.log");
7165        std::fs::write(
7166            &log,
7167            "(1706689000.100000) can0 123#B80B280000000000\n(1706689000.200000) can0 456#00\n(1706689000.300000) can0 123#C00B290000000000\n",
7168        )
7169        .unwrap();
7170        let text = check("car", Some(&log), &registry, &options).unwrap();
7171        assert!(text.contains("3 frames, 2 of them named by car"), "{text}");
7172        assert!(text.contains("messages in the log: ENGINE (2)"), "{text}");
7173
7174        let e = check(&bad.to_string_lossy(), None, &registry, &options).unwrap_err();
7175        assert!(e.starts_with("error: ") && e.contains("bad.dbc\":2"), "{e}");
7176    }
7177
7178    /// FIX dictionaries share the search path: a `kind = "fix"` TOML file and a
7179    /// QuickFIX XML file are listed apart from the specs, an XML file that is not one is
7180    /// passed over, and `formats check` reads a log with one.
7181    #[test]
7182    fn fix_dictionaries_on_the_search_path() {
7183        let dir = tempfile::tempdir().unwrap();
7184        std::fs::write(dir.path().join("l2.toml"), L2).unwrap();
7185        std::fs::write(
7186            dir.path().join("broker.toml"),
7187            "name = \"acme.fix.broker-x\"\nkind = \"fix\"\nmatch = { sender = \"BROKERX\" }\ntags = { 9001 = \"AlgoName\" }\n",
7188        )
7189        .unwrap();
7190        std::fs::write(
7191            dir.path().join("FIX44-custom.xml"),
7192            "<fix major='4' minor='4'><fields><field number='5001' name='Desk' type='STRING'/></fields></fix>",
7193        )
7194        .unwrap();
7195        std::fs::write(dir.path().join("other.xml"), "<gpx/>").unwrap();
7196        std::fs::write(
7197            dir.path().join("broken.toml"),
7198            "name = \"acme.fix.bad\"\nkind = \"fix\"\ntags = { nine = \"X\" }\n",
7199        )
7200        .unwrap();
7201        let path = vec![dir.path().to_path_buf()];
7202        let registry = Registry::load(&path);
7203        assert_eq!(registry.specs.len(), 1);
7204        let names: Vec<&str> = registry.fix.iter().map(|f| f.dict.name.as_str()).collect();
7205        assert_eq!(names, ["FIX44-custom", "acme.fix.broker-x"]);
7206        assert_eq!(registry.errors.len(), 1, "{:?}", registry.errors);
7207        assert!(registry.errors[0].to_string().contains("tags.nine"));
7208        let listing = registry.listing(&path);
7209        assert!(listing.contains("Dictionaries (FIX):"), "{listing}");
7210        assert!(
7211            listing.contains("acme.fix.broker-x  (sender BROKERX)"),
7212            "{listing}"
7213        );
7214        assert!(
7215            listing.contains("FIX44-custom  (begin string FIX.4.4)"),
7216            "{listing}"
7217        );
7218
7219        let log = dir.path().join("session.log");
7220        std::fs::write(
7221            &log,
7222            "8=FIX.4.4|9=20|35=0|49=BROKERX|9001=x|10=000|\n8=FIX.4.4|9=5|35=0|49=OTHER|10=000|\n",
7223        )
7224        .unwrap();
7225        let text = check(
7226            "acme.fix.broker-x",
7227            Some(&log),
7228            &registry,
7229            &crate::OpenOptions::default(),
7230        )
7231        .unwrap();
7232        assert!(text.starts_with("acme.fix.broker-x: ok"), "{text}");
7233        assert!(text.contains("matches sender BROKERX"), "{text}");
7234        assert!(text.contains("2 messages, 1 of them matched"), "{text}");
7235        assert!(text.contains("names in the log: 9001 AlgoName"), "{text}");
7236        let e = check(
7237            &dir.path().join("broken.toml").to_string_lossy(),
7238            None,
7239            &registry,
7240            &crate::OpenOptions::default(),
7241        )
7242        .unwrap_err();
7243        assert!(e.contains("broken.toml\":3:1: tags.nine"), "{e}");
7244    }
7245}
7246
7247#[cfg(test)]
7248mod docs_tests {
7249    use super::*;
7250
7251    const ORDERS: &str = r#"
7252name = "acme.orders"
7253description = "  Order entry capture  "
7254documentation = "https://example.com/orders.pdf"
7255match = { glob = "*.ord" }
7256
7257[records]
7258framing = "length_prefixed"
7259size = "len"
7260size_adjust = 2
7261type = "kind"
7262fields = [
7263  { name = "len", type = "u2" },
7264  { name = "kind", type = "str", size = 1, description = "Message type" },
7265]
7266
7267[[variants]]
7268name = "add"
7269when = "A"
7270description = "An order added to the book"
7271fields = [
7272  { name = "ref", type = "u8", description = "Order reference" },
7273  { name = "price", type = "u4", scale = 4, unit = "USD" },
7274  { name = "side", type = "u1", enum = { 1 = "BUY", 2 = "SELL" } },
7275]
7276
7277[[variants]]
7278name = "exec"
7279when = ["E", "C"]
7280fields = [{ name = "ref", type = "u8" }, { name = "shares", type = "u4", unit = "shares" }]
7281"#;
7282
7283    fn error_of(text: &str) -> String {
7284        Spec::parse(text, None).unwrap_err().message
7285    }
7286
7287    #[test]
7288    fn a_spec_takes_the_catalogs_documentation_keys() {
7289        let spec = Spec::parse(ORDERS, None).unwrap();
7290        assert_eq!(spec.description.as_deref(), Some("Order entry capture"));
7291        assert_eq!(
7292            spec.documentation.as_deref(),
7293            Some("https://example.com/orders.pdf")
7294        );
7295        let add = &spec.records.variants[0];
7296        assert_eq!(
7297            add.description.as_deref(),
7298            Some("An order added to the book")
7299        );
7300        assert_eq!(add.fields[1].unit.as_deref(), Some("USD"));
7301        assert_eq!(
7302            spec.records.fields[1].description.as_deref(),
7303            Some("Message type")
7304        );
7305        // Documentation only: the same spec without it reads the same columns.
7306        let plain = Spec::parse(
7307            &ORDERS
7308                .replace(", description = \"Message type\"", "")
7309                .replace(", unit = \"USD\"", ""),
7310            None,
7311        )
7312        .unwrap();
7313        assert_eq!(
7314            plain.static_columns(),
7315            spec.static_columns(),
7316            "nothing documented changes the columns"
7317        );
7318    }
7319
7320    #[test]
7321    fn empty_documentation_and_unknown_keys_are_refused() {
7322        for (from, to, said) in [
7323            (
7324                "description = \"  Order entry capture  \"",
7325                "description = \"  \"",
7326                "description: must not be empty",
7327            ),
7328            (
7329                "documentation = \"https://example.com/orders.pdf\"",
7330                "documentation = \"ftp://example.com/x\"",
7331                "documentation: \"ftp://example.com/x\" is not an https:// link",
7332            ),
7333            (
7334                "description = \"Order reference\"",
7335                "description = \"\"",
7336                "description: must not be empty",
7337            ),
7338            ("unit = \"USD\"", "unit = \" \"", "unit: must not be empty"),
7339            (
7340                "description = \"An order added to the book\"",
7341                "description = \"\"",
7342                "description: must not be empty",
7343            ),
7344            (
7345                "unit = \"USD\"",
7346                "units = \"USD\"",
7347                "unknown key `units` in a field",
7348            ),
7349            (
7350                "description = \"An order added to the book\"",
7351                "about = \"x\"",
7352                "unknown key `about` in a variant",
7353            ),
7354            (
7355                "documentation = \"https://example.com/orders.pdf\"",
7356                "url = \"https://example.com/orders.pdf\"",
7357                "unknown key `url` in the spec",
7358            ),
7359            (
7360                "unit = \"USD\"",
7361                "values = { 1 = \"x\" }",
7362                "unknown key `values` in a field",
7363            ),
7364        ] {
7365            assert!(ORDERS.contains(from), "{from}");
7366            let e = error_of(&ORDERS.replacen(from, to, 1));
7367            assert!(e.contains(said), "{to}: {e}");
7368        }
7369        // Skipped bytes have no column to document.
7370        let e = error_of(&ORDERS.replace(
7371            "{ name = \"len\", type = \"u2\" },",
7372            "{ name = \"len\", type = \"u2\" }, { type = \"pad\", size = 1, description = \"x\" },",
7373        ));
7374        assert!(e.contains("pad: skipped bytes take only a size"), "{e}");
7375    }
7376
7377    #[test]
7378    fn a_variant_spec_documents_its_record_types_and_columns() {
7379        let docs = Spec::parse(ORDERS, None).unwrap().docs().unwrap();
7380        assert_eq!(docs.spec, "acme.orders");
7381        assert_eq!(docs.description, "Order entry capture");
7382        assert_eq!(docs.documentation, "https://example.com/orders.pdf");
7383        assert_eq!(
7384            docs.record_types,
7385            [
7386                RecordType {
7387                    name: "add".into(),
7388                    picked_by: "kind = \"A\"".into(),
7389                    description: "An order added to the book".into(),
7390                    columns: 5,
7391                },
7392                RecordType {
7393                    name: "exec".into(),
7394                    picked_by: "kind in (\"E\", \"C\")".into(),
7395                    description: String::new(),
7396                    columns: 4,
7397                },
7398            ]
7399        );
7400        let names: Vec<&str> = docs.columns.iter().map(|(n, _)| n.as_str()).collect();
7401        assert_eq!(names, ["kind", "ref", "price", "side", "shares"]);
7402        let note = |name: &str| {
7403            docs.columns
7404                .iter()
7405                .find(|(n, _)| n == name)
7406                .unwrap()
7407                .1
7408                .clone()
7409        };
7410        assert_eq!(note("ref").description, "Order reference");
7411        assert_eq!(note("price").unit, "USD");
7412        // The enum is the column's value legend.
7413        assert_eq!(
7414            note("side").values,
7415            [
7416                ("1".to_string(), "BUY".to_string()),
7417                ("2".into(), "SELL".into())
7418            ]
7419        );
7420        // A spec that says nothing beyond how to read has no page.
7421        let bare = "name = \"a.b\"\nmatch = { glob = \"*.b\" }\n[records]\nfields = [{ name = \"x\", type = \"u1\" }]\n";
7422        assert_eq!(Spec::parse(bare, None).unwrap().docs(), None);
7423    }
7424
7425    #[test]
7426    fn a_flattened_fields_note_is_filed_under_each_of_its_columns() {
7427        let text = "name = \"a.b\"\n[records]\nfields = [{ name = \"bid\", type = \"u4\", count = 3, flatten = true, description = \"Bid level\", unit = \"USD\" }]\n";
7428        let spec = Spec::parse(text, None).unwrap();
7429        let docs = spec.docs().unwrap();
7430        let names: Vec<&str> = docs.columns.iter().map(|(n, _)| n.as_str()).collect();
7431        assert_eq!(names, ["bid_0", "bid_1", "bid_2"]);
7432        assert!(
7433            docs.columns
7434                .iter()
7435                .all(|(_, note)| note.description == "Bid level" && note.unit == "USD")
7436        );
7437        let schema: Vec<String> = spec
7438            .static_columns()
7439            .unwrap()
7440            .into_iter()
7441            .map(|(name, _)| name)
7442            .collect();
7443        assert_eq!(schema, names);
7444    }
7445
7446    #[test]
7447    fn a_delimited_specs_columns_take_a_description_and_unit() {
7448        let text = r#"
7449name = "acme.log"
7450kind = "delimited"
7451documentation = "https://example.com/log"
7452header_rows = { name = 2, unit = 1 }
7453
7454[columns]
7455time = { from = ["date", "clock"], as = "datetime", description = "When it was read" }
7456temp = { description = "Air temperature", unit = "deg F" }
7457volts = { unit = "V" }
7458"#;
7459        let spec = Spec::parse(text, None).unwrap();
7460        let delimited = spec.delimited.as_ref().unwrap();
7461        // A note alone is no derived column; the units line still works.
7462        assert_eq!(delimited.columns.len(), 1);
7463        assert_eq!(delimited.header_rows.as_ref().unwrap().unit, Some(1));
7464        let docs = spec.docs().unwrap();
7465        assert!(docs.record_types.is_empty());
7466        assert_eq!(
7467            docs.columns,
7468            [
7469                (
7470                    "time".to_string(),
7471                    ColumnNote {
7472                        description: "When it was read".into(),
7473                        ..Default::default()
7474                    }
7475                ),
7476                (
7477                    "temp".to_string(),
7478                    ColumnNote {
7479                        description: "Air temperature".into(),
7480                        unit: "deg F".into(),
7481                        values: Vec::new(),
7482                        ty: String::new(),
7483                    }
7484                ),
7485                (
7486                    "volts".to_string(),
7487                    ColumnNote {
7488                        unit: "V".into(),
7489                        ..Default::default()
7490                    }
7491                ),
7492            ]
7493        );
7494        for (entry, said) in [
7495            ("t = {}", "columns.t: missing `from`"),
7496            (
7497                "t = { description = \"\" }",
7498                "columns.t.description: must not be empty",
7499            ),
7500            (
7501                "t = { values = { a = \"b\" } }",
7502                "unknown key `values` in columns.t",
7503            ),
7504            ("t = { format = \"%Y\" }", "columns.t: missing `from`"),
7505            (
7506                "t = { from = \"d\", as = \"date\", type = \"date\" }",
7507                "columns.t: a derived column takes `as` for its type, not `type`",
7508            ),
7509            (
7510                "t = { type = \"int\" }",
7511                "columns.t.type: unknown type \"int\"; expected one of str, bool, i8",
7512            ),
7513            (
7514                "t = { type = \"i64\", format = \"%Y\" }",
7515                "columns.t.type: format is for date, time and datetime, not i64",
7516            ),
7517        ] {
7518            let e = error_of(&format!(
7519                "name = \"a.b\"\nkind = \"delimited\"\n[columns]\n{entry}\n"
7520            ));
7521            assert!(e.contains(said), "{entry}: {e}");
7522        }
7523        let e = error_of("name = \"a.b\"\nkind = \"delimited\"\nurl = \"https://x\"\n");
7524        assert!(e.contains("unknown key `url`"), "{e}");
7525    }
7526}
7527
7528#[cfg(test)]
7529mod chip_tests {
7530    use super::*;
7531
7532    /// A spec of one header and one record field, with `matches` as its `match`.
7533    fn spec(matches: &str) -> Spec {
7534        let text = format!(
7535            r#"name = "acme.chips"
7536{matches}
7537[header]
7538fields = [
7539  {{ name = "magic", type = "str", size = 4 }},
7540  {{ name = "version", type = "u2" }},
7541  {{ name = "kind", type = "str", size = 1 }},
7542]
7543
7544[records]
7545fields = [{{ name = "x", type = "u1" }}]
7546"#
7547        );
7548        Spec::parse(&text, None).unwrap()
7549    }
7550
7551    fn plain(spec: &Spec) -> Vec<String> {
7552        spec.match_chips().iter().map(|c| c.plain(true)).collect()
7553    }
7554
7555    #[test]
7556    fn printable_magic_is_text_and_where_drops_the_header_prefix() {
7557        let s = spec(r#"match = { magic = "MKTD", where = { "header.version" = 1 } }"#);
7558        let chips = s.match_chips();
7559        assert_eq!(chips[0].kind, ChipKind::Magic);
7560        assert_eq!(chips[1].kind, ChipKind::Int);
7561        assert_eq!(plain(&s), ["magic MKTD", "version 1"]);
7562    }
7563
7564    #[test]
7565    fn unprintable_magic_is_hex_and_a_long_one_is_cut() {
7566        let s = spec("match = { magic = [127, 69, 76, 70] }");
7567        assert_eq!(s.match_chips()[0].kind, ChipKind::Hex);
7568        assert_eq!(plain(&s), ["magic 7f 45 4c 46"]);
7569        let s = spec("match = { magic = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }");
7570        let ellipsis = crate::glyphs::get().ellipsis;
7571        assert_eq!(
7572            plain(&s),
7573            [format!("magic 00 01 02 03 04 05 06 07 {ellipsis}")]
7574        );
7575        let s = spec(r#"match = { magic = "ABCDEFGHIJKLMNOPQRST" }"#);
7576        assert_eq!(plain(&s), [format!("magic ABCDEFGHIJKLMNOP{ellipsis}")]);
7577    }
7578
7579    #[test]
7580    fn a_magic_offset_folds_into_its_chip_when_not_zero() {
7581        let s = spec(r#"match = { magic = "MKTD", magic_offset = 8 }"#);
7582        assert_eq!(plain(&s), ["magic MKTD @ 8"]);
7583        let s = spec(r#"match = { magic = "MKTD", magic_offset = 0 }"#);
7584        assert_eq!(plain(&s), ["magic MKTD"]);
7585    }
7586
7587    #[test]
7588    fn a_glob_list_is_one_chip_of_alternatives() {
7589        let s = spec(r#"match = { glob = ["*.bin", "*.dat"] }"#);
7590        assert_eq!(plain(&s), ["glob *.bin *.dat"]);
7591        assert_eq!(s.match_chips()[0].kind, ChipKind::Glob);
7592    }
7593
7594    #[test]
7595    fn a_text_value_is_quoted_only_in_plain_text() {
7596        let s = spec(
7597            r#"match = { magic = "MKTD", where = { "header.kind" = "A", "header.version" = 2 } }"#,
7598        );
7599        let chips = s.match_chips();
7600        let kind = chips.iter().find(|c| c.name == "kind").unwrap();
7601        assert_eq!(kind.kind, ChipKind::Text);
7602        assert_eq!(kind.plain(true), "kind \"A\"");
7603        assert_eq!(kind.plain(false), "kind A");
7604        let version = chips.iter().find(|c| c.name == "version").unwrap();
7605        assert_eq!(version.plain(true), "version 2");
7606    }
7607
7608    #[test]
7609    fn a_spec_with_no_match_is_for_format_only() {
7610        let s = spec("");
7611        assert!(s.match_chips().is_empty());
7612        assert_eq!(match_words(&s), FORMAT_ONLY);
7613    }
7614
7615    /// A glob that names the file stands without the magic, and the magic is asked only
7616    /// of a file no glob names, so the pane and the Notes show the one that chose it.
7617    #[test]
7618    fn the_rule_that_chose_the_spec_keeps_its_chips() {
7619        let s =
7620            spec(r#"match = { glob = "*.bin", magic = "MKTD", where = { "header.version" = 1 } }"#);
7621        let names = |chips: Vec<MatchChip>| chips.into_iter().map(|c| c.name).collect::<Vec<_>>();
7622        assert_eq!(names(s.match_chips()), ["magic", "version", "glob"]);
7623        assert_eq!(
7624            names(s.match_chips_chosen(Chosen::Glob)),
7625            ["version", "glob"]
7626        );
7627        assert_eq!(
7628            names(s.match_chips_chosen(Chosen::Magic)),
7629            ["magic", "version"]
7630        );
7631        assert_eq!(
7632            names(s.match_chips_for(Path::new("x/day.bin"))),
7633            ["glob"],
7634            "a listing names it by the glob alone, its header unread"
7635        );
7636        assert_eq!(
7637            names(s.match_chips_for(Path::new("x/day"))),
7638            ["magic", "version"]
7639        );
7640        let middot = crate::glyphs::get().middot;
7641        assert_eq!(
7642            chips_plain(&s.match_chips()),
7643            format!("magic MKTD {middot} version 1 {middot} glob *.bin")
7644        );
7645        assert_eq!(
7646            chosen_words(&s, Chosen::Magic),
7647            format!("matched by magic MKTD {middot} version 1")
7648        );
7649        assert_eq!(chosen_words(&s, Chosen::Named), "chosen by its name");
7650    }
7651
7652    /// `datui formats` lists each spec with its conditions, `no match` without.
7653    #[test]
7654    fn the_listing_shows_each_specs_chips() {
7655        let mut one = spec(r#"match = { magic = "MKTD", where = { "header.version" = 1 } }"#);
7656        one.name = "acme.mktd".to_string();
7657        let two = spec("");
7658        let listing = Registry::of(vec![one, two]).listing(&[]);
7659        let middot = crate::glyphs::get().middot;
7660        assert!(
7661            listing.contains(&format!("acme.mktd  (magic MKTD {middot} version 1)")),
7662            "{listing}"
7663        );
7664        assert!(listing.contains("acme.chips  (no match)"), "{listing}");
7665
7666        // `datui formats check` says the same.
7667        let dir = tempfile::tempdir().unwrap();
7668        let file = dir.path().join("spec-chips-mktd.toml");
7669        std::fs::write(
7670            &file,
7671            "name = \"acme.mktd\"\nmatch = { magic = \"MKTD\", where = { \"header.version\" = 1 } }\n\
7672             [header]\nfields = [{ name = \"magic\", type = \"str\", size = 4 }, \
7673             { name = \"version\", type = \"u2\" }]\n\
7674             [records]\nfields = [{ name = \"x\", type = \"u1\" }]\n",
7675        )
7676        .unwrap();
7677        let text = check(
7678            &file.to_string_lossy(),
7679            None,
7680            &Registry::default(),
7681            &crate::OpenOptions::default(),
7682        )
7683        .unwrap();
7684        assert!(
7685            text.contains(&format!("  matches magic MKTD {middot} version 1\n")),
7686            "{text}"
7687        );
7688    }
7689}