Skip to main content

datui_lib/formats/
matching.rs

1//! A spec read from text or a file, which files it matches, and its docs.
2
3use super::*;
4
5impl Spec {
6    /// Read a spec from its text. `path` names it in errors.
7    pub fn parse(text: &str, path: Option<&Path>) -> Result<Self, SpecError> {
8        let reader = Reader { text, path };
9        let document = DeTable::parse(text).map_err(|e| {
10            let (line, column) = e
11                .span()
12                .map_or((0, 0), |span| line_column(text, span.start));
13            SpecError {
14                path: path.map(Path::to_path_buf),
15                line,
16                column,
17                message: e.message().to_string(),
18            }
19        })?;
20        let kind = document
21            .get_ref()
22            .iter()
23            .find(|(key, _)| {
24                let key: &str = key.get_ref();
25                key == "kind"
26            })
27            .map(|(_, v)| v);
28        if let Some(v) = kind {
29            match reader.string(v, "kind")?.as_str() {
30                "binary" => {}
31                "delimited" => return Self::parse_delimited(&reader, document.get_ref(), path),
32                _ => return Err(reader.error(&v.span(), "kind: expected binary or delimited")),
33            }
34        }
35        let top = reader.entries(
36            document.get_ref(),
37            "the spec",
38            &[
39                "name",
40                "description",
41                "documentation",
42                "kind",
43                "match",
44                "endian",
45                "layout",
46                "header",
47                "records",
48                "footer",
49                "variants",
50                "blocks",
51                "capture",
52                "files",
53                "sections",
54            ],
55        )?;
56        let whole = 0..0;
57        let name = reader.spec_name(&top)?;
58        let description = top
59            .get("description")
60            .map(|v| reader.prose(v, "description"))
61            .transpose()?;
62        let documentation = top
63            .get("documentation")
64            .map(|v| reader.documentation(v))
65            .transpose()?;
66        let (endian, endian_auto) = match top.get("endian") {
67            None => (Endian::Little, false),
68            Some(v) => match reader.string(v, "endian")?.as_str() {
69                "le" => (Endian::Little, false),
70                "be" => (Endian::Big, false),
71                "auto" => (Endian::Little, true),
72                _ => return Err(reader.error(&v.span(), "endian: expected le, be or auto")),
73            },
74        };
75        let layout = match top.get("layout") {
76            None => Layout::Rows,
77            Some(v) => match reader.string(v, "layout")?.as_str() {
78                "rows" => Layout::Rows,
79                "columns" => Layout::Columns,
80                _ => return Err(reader.error(&v.span(), "layout: expected rows or columns")),
81            },
82        };
83
84        let mut header = Header::default();
85        if let Some(v) = top.get("header") {
86            let table = reader.table(v, "[header]")?;
87            let keys = reader.entries(table, "[header]", &["fields", "size", "size_adjust"])?;
88            if let Some(f) = keys.get("fields") {
89                header.fields = reader.fields(f, Part::Header, layout, &Scopes::default(), &[])?;
90            }
91            if let Some(s) = keys.get("size") {
92                let scopes = Scopes {
93                    header: &header.fields,
94                    footer: &[],
95                };
96                header.size = Some(reader.amount(
97                    s,
98                    keys.get("size_adjust").copied(),
99                    "size",
100                    Part::Header,
101                    &header.fields,
102                    &scopes,
103                )?);
104            }
105        }
106        let footer = top
107            .get("footer")
108            .map(|v| reader.footer(v, &header.fields))
109            .transpose()?;
110        let footer_fields: &[Field] = footer.as_ref().map_or(&[], |f| &f.fields);
111        let scopes = Scopes {
112            header: &header.fields,
113            footer: footer_fields,
114        };
115
116        let MatchRules {
117            globs,
118            glob_set,
119            magic,
120            magic_offset,
121            expect,
122        } = match top.get("match") {
123            Some(v) => reader.match_rules(v, Some(&header.fields))?,
124            None => MatchRules::default(),
125        };
126
127        if endian_auto && magic.len() < 2 {
128            return Err(reader.error(
129                &top["endian"].span(),
130                "endian = \"auto\" reads the byte order from the magic, so it needs a magic of at least two bytes",
131            ));
132        }
133
134        let sections = top
135            .get("sections")
136            .map(|v| reader.sections(v, &scopes))
137            .transpose()?
138            .unwrap_or_default();
139
140        let Some(records_value) = top.get("records") else {
141            return Err(reader.error(&whole, "missing [records], with the fields of one record"));
142        };
143        let records =
144            reader.records(records_value, top.get("variants").copied(), layout, &scopes)?;
145        for field in all_fields(&records) {
146            if let Some(section) = &field.string_at
147                && !sections.iter().any(|s| &s.name == section)
148            {
149                return Err(reader.error(
150                    &records_value.span(),
151                    format!("string_at: no section named `{section}` under [sections]"),
152                ));
153            }
154        }
155        let blocks = top
156            .get("blocks")
157            .map(|v| reader.blocks(v, &scopes))
158            .transpose()?;
159        let capture = top.get("capture").map(|v| reader.capture(v)).transpose()?;
160        let files = top.get("files").map(|v| reader.files(v)).transpose()?;
161
162        if let Some(v) = top.get("capture")
163            && (blocks.is_some() || top.contains_key("header") || footer.is_some())
164        {
165            return Err(reader.error(
166                &v.span(),
167                "[capture]: the capture's own headers frame the payloads; leave out [header], [footer] and [blocks]",
168            ));
169        }
170        let framed = blocks.is_some() || capture.is_some();
171        if layout == Layout::Columns
172            && let Some(v) = top.get("blocks").or(top.get("files"))
173        {
174            return Err(reader.error(
175                &v.span(),
176                "layout = \"columns\" reads values, not blocks or a tree of files",
177            ));
178        }
179        if let Some(ring) = &records.ring {
180            let ring_ok = records.framing == Framing::Fixed
181                && !framed
182                && records.variants.is_empty()
183                && matches!(
184                    ring,
185                    Amount::Header { .. } | Amount::Footer { .. } | Amount::Given(_)
186                );
187            if !ring_ok {
188                return Err(reader.error(
189                    &records_value.span(),
190                    "ring: is for fixed records, the oldest's index read from the header",
191                ));
192            }
193        }
194        if let Some(files) = &files {
195            let columns: std::collections::HashSet<String> =
196                all_fields(&records).flat_map(output_names).collect();
197            if let Some(part) = files.parts.iter().find(|p| columns.contains(&p.name)) {
198                return Err(reader.error(
199                    &top["files"].span(),
200                    format!("path: `{}` is also a field's name", part.name),
201                ));
202            }
203        }
204
205        if layout == Layout::Columns {
206            reader.check_columns(records_value, &records)?;
207            if let Some(v) = top
208                .get("footer")
209                .or(top.get("variants"))
210                .or(top.get("capture"))
211            {
212                return Err(reader.error(
213                    &v.span(),
214                    "layout = \"columns\" holds fixed values: no footer, variants or capture",
215                ));
216            }
217        }
218        // Sizes written down are checked now; one that comes from the file is checked
219        // when the file is read.
220        if let (Some(Amount::Given(size)), Some(sum)) =
221            (&records.size, given_width(&records.fields))
222            && *size < sum
223            && records.variants.is_empty()
224        {
225            return Err(reader.error(
226                &records_value.span(),
227                format!("size: the fields take {sum} bytes, more than {size}"),
228            ));
229        }
230        if let (Some(Amount::Given(size)), Some(sum)) = (&header.size, given_width(&header.fields))
231            && *size < sum
232        {
233            return Err(reader.error(
234                &records_value.span(),
235                format!("[header] size: the fields take {sum} bytes, more than {size}"),
236            ));
237        }
238        if let Some(sum) = given_width(&records.fields) {
239            if sum == 0 && records.variants.is_empty() && records.sync.is_empty() {
240                return Err(reader.error(&records_value.span(), "fields: a record takes no bytes"));
241            }
242            if sum > MAX_SIZE {
243                return Err(reader.error(
244                    &records_value.span(),
245                    format!("fields: a record of {sum} bytes is more than {MAX_SIZE}"),
246                ));
247            }
248        }
249        Ok(Self {
250            name,
251            description,
252            documentation,
253            notes: Vec::new(),
254            path: path.map(Path::to_path_buf),
255            globs,
256            glob_set,
257            magic,
258            magic_offset,
259            expect,
260            endian,
261            endian_auto,
262            layout,
263            header,
264            records,
265            delimited: None,
266            footer,
267            blocks,
268            capture,
269            files,
270            sections,
271            variant: None,
272        })
273    }
274
275    /// A `kind = "delimited"` spec: a CSV-like file's reading options, with no
276    /// header or record fields.
277    fn parse_delimited(
278        reader: &Reader<'_>,
279        document: &DeTable<'_>,
280        path: Option<&Path>,
281    ) -> Result<Self, SpecError> {
282        let keys = delimited_spec_keys();
283        let top = reader.entries(document, "a delimited spec", &keys)?;
284        let name = reader.spec_name(&top)?;
285        let description = top
286            .get("description")
287            .map(|v| reader.prose(v, "description"))
288            .transpose()?;
289        let documentation = top
290            .get("documentation")
291            .map(|v| reader.documentation(v))
292            .transpose()?;
293        let MatchRules {
294            globs,
295            glob_set,
296            magic,
297            magic_offset,
298            expect,
299        } = match top.get("match") {
300            Some(v) => reader.match_rules(v, None)?,
301            None => MatchRules::default(),
302        };
303        let (delimited, notes) = reader.delimited(&top)?;
304        Ok(Self {
305            name,
306            description,
307            documentation,
308            notes,
309            path: path.map(Path::to_path_buf),
310            globs,
311            glob_set,
312            magic,
313            magic_offset,
314            expect,
315            endian: Endian::Little,
316            endian_auto: false,
317            layout: Layout::Rows,
318            header: Header::default(),
319            records: Records::default(),
320            delimited: Some(Arc::new(delimited)),
321            footer: None,
322            blocks: None,
323            capture: None,
324            files: None,
325            sections: Vec::new(),
326            variant: None,
327        })
328    }
329
330    /// Whether the spec is `kind = "delimited"`.
331    pub fn is_delimited(&self) -> bool {
332        self.delimited.is_some()
333    }
334
335    /// Read the spec in `path`, which may be no more than [`MAX_SPEC_BYTES`].
336    pub fn load(path: &Path) -> Result<Self, SpecError> {
337        use std::io::Read;
338        let mut bytes = Vec::new();
339        std::fs::File::open(path)
340            .and_then(|f| f.take(MAX_SPEC_BYTES + 1).read_to_end(&mut bytes))
341            .map_err(|e| SpecError {
342                path: Some(path.to_path_buf()),
343                line: 0,
344                column: 0,
345                message: format!(
346                    "could not read it. {}",
347                    crate::error_display::user_message_from_io(&e, None)
348                ),
349            })?;
350        Self::from_bytes(&bytes, path)
351    }
352
353    /// The spec in `bytes`, read from `from` (a file or a URL), refused past
354    /// [`MAX_SPEC_BYTES`].
355    pub fn from_bytes(bytes: &[u8], from: &Path) -> Result<Self, SpecError> {
356        let refused = |message: String| SpecError {
357            path: Some(from.to_path_buf()),
358            line: 0,
359            column: 0,
360            message,
361        };
362        if bytes.len() as u64 > MAX_SPEC_BYTES {
363            return Err(refused(format!("a format spec is at most {MAX_SPEC_SAID}")));
364        }
365        let text = std::str::from_utf8(bytes)
366            .map_err(|_| refused("not UTF-8 text, which a format spec is".to_string()))?;
367        Self::parse(text, Some(from))
368    }
369
370    /// Whether `path`'s name matches one of the spec's globs. A glob with a `/` is
371    /// matched against the whole path, one without against the name.
372    pub fn glob_matches(&self, path: &Path) -> bool {
373        let Some(set) = &self.glob_set else {
374            return false;
375        };
376        let name = path.file_name().map(Path::new);
377        name.is_some_and(|n| set.is_match(n)) || set.is_match(path)
378    }
379
380    /// Whether `head`, the first bytes of a file, carries the spec's magic. A delimited
381    /// spec's magic is the start of the first line, after any byte-order mark.
382    pub fn magic_matches(&self, head: &[u8]) -> bool {
383        let head = if self.is_delimited() {
384            head.strip_prefix(UTF8_BOM).unwrap_or(head)
385        } else {
386            head
387        };
388        let start = self.magic_offset as usize;
389        let found = head.get(start..start + self.magic.len());
390        !self.magic.is_empty()
391            && (found == Some(&self.magic)
392                || (self.endian_auto
393                    && found.is_some_and(|f| f.iter().eq(self.magic.iter().rev()))))
394    }
395
396    /// Whether `head` holds the header values `match.where` asks for.
397    pub fn header_matches(&self, head: &[u8]) -> bool {
398        if self.expect.is_empty() {
399            return true;
400        }
401        let Ok(header) = read_header(self, head) else {
402            return false;
403        };
404        self.expect.iter().all(|(field, wanted)| match wanted {
405            Expected::Int(v) => header.int(field) == Some(*v),
406            Expected::Text(v) => header.text(field).as_deref() == Some(v.as_str()),
407        })
408    }
409
410    /// Bytes from the front of a file that settle the spec's magic and `where`.
411    pub fn match_reach(&self) -> u64 {
412        let magic = if self.magic.is_empty() {
413            0
414        } else {
415            let bom = if self.is_delimited() {
416                UTF8_BOM.len() as u64
417            } else {
418                0
419            };
420            bom + self.magic_offset + self.magic.len() as u64
421        };
422        let header = if self.expect.is_empty() {
423            0
424        } else {
425            given_width(&self.header.fields).unwrap_or(MAX_MATCH_READ)
426        };
427        magic.max(header).min(MAX_MATCH_READ)
428    }
429
430    /// What the spec says files of it look like, one chip per condition: its magic,
431    /// its header values, then its globs. Empty when only `--format` picks it.
432    pub fn match_chips(&self) -> Vec<MatchChip> {
433        let mut chips = Vec::new();
434        if !self.magic.is_empty() {
435            let ellipsis = crate::glyphs::get().ellipsis;
436            let (value, kind) = if self.magic.iter().all(|b| b.is_ascii_graphic()) {
437                let text = String::from_utf8_lossy(&self.magic);
438                let value = if text.chars().count() > CHIP_MAGIC_CHARS {
439                    let head: String = text.chars().take(CHIP_MAGIC_CHARS).collect();
440                    format!("{head}{ellipsis}")
441                } else {
442                    text.into_owned()
443                };
444                (value, ChipKind::Magic)
445            } else if self.magic.len() > CHIP_MAGIC_BYTES {
446                let head = crate::formats::fixed_records::hex(&self.magic[..CHIP_MAGIC_BYTES]);
447                (format!("{head} {ellipsis}"), ChipKind::Hex)
448            } else {
449                (
450                    crate::formats::fixed_records::hex(&self.magic),
451                    ChipKind::Hex,
452                )
453            };
454            chips.push(MatchChip {
455                name: "magic".to_string(),
456                value,
457                kind,
458                offset: (self.magic_offset > 0).then_some(self.magic_offset),
459            });
460        }
461        for (field, wanted) in &self.expect {
462            let (value, kind) = match wanted {
463                Expected::Int(v) => (v.to_string(), ChipKind::Int),
464                Expected::Text(v) => (v.clone(), ChipKind::Text),
465            };
466            chips.push(MatchChip {
467                name: field.clone(),
468                value,
469                kind,
470                offset: None,
471            });
472        }
473        if !self.globs.is_empty() {
474            chips.push(MatchChip {
475                name: "glob".to_string(),
476                value: self.globs.join(" "),
477                kind: ChipKind::Glob,
478                offset: None,
479            });
480        }
481        chips
482    }
483
484    /// The chips that named `path` on the home screen: the glob alone when it names the
485    /// file, since a listing names a file by its glob without reading its header; else
486    /// the magic and the header values it was checked against. All of them when
487    /// neither does.
488    pub fn match_chips_for(&self, path: &Path) -> Vec<MatchChip> {
489        if self.glob_matches(path) {
490            let mut chips = self.match_chips();
491            chips.retain(|c| c.kind == ChipKind::Glob);
492            return chips;
493        }
494        let by = (!self.magic.is_empty()).then_some(ChipKind::Magic);
495        self.match_chips_by(by)
496    }
497
498    /// The chips of the rule that chose the spec: `Chosen::Glob` or `Chosen::Magic`
499    /// leave the other out; any other choice keeps every chip.
500    pub fn match_chips_chosen(&self, by: Chosen) -> Vec<MatchChip> {
501        match by {
502            Chosen::Glob => self.match_chips_by(Some(ChipKind::Glob)),
503            Chosen::Magic => self.match_chips_by(Some(ChipKind::Magic)),
504            Chosen::SpecFile | Chosen::Named => self.match_chips(),
505        }
506    }
507
508    fn match_chips_by(&self, by: Option<ChipKind>) -> Vec<MatchChip> {
509        let mut chips = self.match_chips();
510        match by {
511            Some(ChipKind::Glob) => {
512                chips.retain(|c| !matches!(c.kind, ChipKind::Magic | ChipKind::Hex));
513            }
514            Some(_) => chips.retain(|c| c.kind != ChipKind::Glob),
515            None => {}
516        }
517        chips
518    }
519
520    /// Whether the spec reads a file's records as several variants, each listed as a
521    /// table inside the file.
522    pub fn lists_variants(&self) -> bool {
523        !self.is_delimited() && self.records.variants.len() > 1 && self.variant.is_none()
524    }
525
526    /// The columns a file of the spec opens with, when the spec alone says them: fixed
527    /// records of one file whose fields take nothing from the file (no sizes, symbols or
528    /// dates from its header). `None` when the open has to read the file to know.
529    pub fn static_columns(&self) -> Option<Vec<(String, polars::prelude::DataType)>> {
530        if self.is_delimited()
531            || self.layout != Layout::Rows
532            || crate::formats::framed_records::needed(self)
533        {
534            return None;
535        }
536        let (columns, _) = self
537            .record_columns(&HeaderValues::default(), 0, Some(1))
538            .ok()?;
539        Some(
540            columns
541                .iter()
542                .map(|c| (c.name.to_string(), c.dtype()))
543                .collect(),
544        )
545    }
546}
547
548/// What a spec says of its files, for the Documentation view: its own words and the
549/// notes its fields carry. None of it changes how a file is read.
550#[derive(Debug, Clone, Default, PartialEq)]
551pub struct SpecDocs {
552    /// The spec's name.
553    pub spec: String,
554    /// The file the spec was read from; none for one built in or parsed from text.
555    pub file: Option<PathBuf>,
556    pub description: String,
557    /// An `https://` link to the format's own documentation.
558    pub documentation: String,
559    /// The variants, each a record type the file holds.
560    pub record_types: Vec<RecordType>,
561    /// What each column means, by its name, in the spec's order: a field's description,
562    /// its unit, and its enum as the value legend.
563    pub columns: Vec<(String, ColumnNote)>,
564    /// The `[header]` fields that say what they hold, in the spec's order.
565    pub header: Vec<(String, ColumnNote)>,
566    /// The `[footer]` fields that say what they hold, in the spec's order.
567    pub footer: Vec<(String, ColumnNote)>,
568}
569
570/// A field's description and unit, when it gives either.
571fn field_note(field: &Field) -> Option<(String, ColumnNote)> {
572    let name = field.name.clone()?;
573    let note = ColumnNote {
574        description: field.description.clone().unwrap_or_default(),
575        unit: field.unit.clone().unwrap_or_default(),
576        values: Vec::new(),
577        ty: String::new(),
578    };
579    (note != ColumnNote::default()).then_some((name, note))
580}
581
582/// The condition on the type field that picks a variant: `msg_type = 1`, or
583/// `kind in ("E", "C")`, text quoted.
584fn picked_by(type_field: Option<&str>, when: &[Expected]) -> String {
585    let field = type_field.unwrap_or("type");
586    let values: Vec<String> = when
587        .iter()
588        .map(|value| match value {
589            Expected::Int(v) => v.to_string(),
590            Expected::Text(v) => format!("\"{v}\""),
591        })
592        .collect();
593    match values.as_slice() {
594        [one] => format!("{field} = {one}"),
595        _ => format!("{field} in ({})", values.join(", ")),
596    }
597}
598
599/// One variant of a spec, as its documentation shows it.
600#[derive(Debug, Clone, Default, PartialEq)]
601pub struct RecordType {
602    pub name: String,
603    /// What picks it, as a condition on the type field: `msg_type = 1`, or
604    /// `kind in ("E", "C")` for several values.
605    pub picked_by: String,
606    pub description: String,
607    /// Its columns: the common ones and its own.
608    pub columns: usize,
609}
610
611impl Spec {
612    /// What the spec documents, or `None` when it says nothing beyond how to read: no
613    /// description or link, no record type described, no column noted.
614    pub fn docs(&self) -> Option<SpecDocs> {
615        let mut columns: Vec<(String, ColumnNote)> = Vec::new();
616        // A name two variants share is one column: the first note of it stands.
617        let mut add = |name: &str, note: ColumnNote| {
618            if note != ColumnNote::default() && !columns.iter().any(|(n, _)| n == name) {
619                columns.push((name.to_string(), note));
620            }
621        };
622        let legend = |labels: &BTreeMap<i64, String>| {
623            labels
624                .iter()
625                .map(|(code, label)| (code.to_string(), label.clone()))
626                .collect::<Vec<_>>()
627        };
628        for field in all_fields(&self.records) {
629            let note = ColumnNote {
630                description: field.description.clone().unwrap_or_default(),
631                unit: field.unit.clone().unwrap_or_default(),
632                values: match &field.meaning {
633                    Meaning::Enum(labels) => legend(labels),
634                    _ => Vec::new(),
635                },
636                ty: String::new(),
637            };
638            // A flattened field is filed under each column it makes.
639            for name in own_names(field) {
640                add(&name, note.clone());
641            }
642            for bit in &field.bits {
643                if let Some(labels) = &bit.labels {
644                    add(
645                        &bit.name,
646                        ColumnNote {
647                            values: legend(labels),
648                            ..ColumnNote::default()
649                        },
650                    );
651                }
652            }
653        }
654        for (name, note) in &self.notes {
655            add(name, note.clone());
656        }
657        let tables = crate::formats::members::variant_tables(self);
658        let record_types: Vec<RecordType> = self
659            .records
660            .variants
661            .iter()
662            .zip(&tables)
663            .map(|(variant, table)| RecordType {
664                name: variant.name.clone(),
665                picked_by: picked_by(self.records.type_field.as_deref(), &variant.when),
666                description: variant.description.clone().unwrap_or_default(),
667                columns: table.columns.len(),
668            })
669            .collect();
670        let header: Vec<(String, ColumnNote)> =
671            self.header.fields.iter().filter_map(field_note).collect();
672        let footer: Vec<(String, ColumnNote)> = self
673            .footer
674            .iter()
675            .flat_map(|f| &f.fields)
676            .filter_map(field_note)
677            .collect();
678        let documented = self.description.is_some()
679            || self.documentation.is_some()
680            || !columns.is_empty()
681            || !header.is_empty()
682            || !footer.is_empty()
683            || record_types.iter().any(|r| !r.description.is_empty());
684        documented.then(|| SpecDocs {
685            spec: self.name.clone(),
686            file: self.path.clone(),
687            description: self.description.clone().unwrap_or_default(),
688            documentation: self.documentation.clone().unwrap_or_default(),
689            record_types,
690            columns,
691            header,
692            footer,
693        })
694    }
695}
696
697/// Bytes one field takes in each record, when nothing about it comes from the file.
698fn field_width(field: &Field) -> Option<u64> {
699    let width = match (&field.size, field.ty.width()) {
700        (_, Some(w)) => w,
701        (Some(Amount::Given(n)), None) => *n,
702        _ => return None,
703    };
704    let count = match &field.count {
705        None => 1,
706        Some(Amount::Given(n)) => *n,
707        Some(_) => return None,
708    };
709    width.checked_mul(count)
710}
711
712/// The bytes the fields take, when none of their sizes comes from the file.
713pub(crate) fn given_width(fields: &[Field]) -> Option<u64> {
714    fields
715        .iter()
716        .try_fold(0u64, |sum, f| sum.checked_add(field_width(f)?))
717}
718
719/// The bytes `fields` take, when none of their sizes comes from the file.
720pub(crate) fn fields_width(fields: &[Field]) -> Option<u64> {
721    given_width(fields)
722}