1use 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
23pub const MAX_SIZE: u64 = 64 << 20;
27
28pub const MAX_FLATTEN: u64 = 1024;
30
31const MAX_MATCH_READ: u64 = 64 << 10;
34
35const DAY_NS: i64 = 86_400_000_000_000;
37
38pub const PATH_VAR: &str = "DATUI_FORMATS_PATH";
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct SpecError {
44 pub path: Option<PathBuf>,
45 pub line: usize,
47 pub column: usize,
48 pub message: String,
49}
50
51impl 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum Endian {
69 Little,
70 Big,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum Layout {
76 Rows,
78 Columns,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum Framing {
87 #[default]
89 Fixed,
90 LengthPrefixed,
92 Variant,
94 Sync,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum Amount {
101 Given(u64),
102 Header {
104 field: String,
105 adjust: i64,
106 },
107 Footer {
109 field: String,
110 adjust: i64,
111 },
112 Record {
115 field: String,
116 adjust: i64,
117 },
118 Rest,
120}
121
122impl Amount {
123 pub fn is_fixed(&self) -> bool {
125 !matches!(self, Self::Record { .. } | Self::Rest)
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum Type {
132 Unsigned(u8),
134 Signed(u8),
135 Float(u8),
137 BFloat16,
139 Bool,
140 Str,
141 Strz,
143 Bytes,
144 Pad,
145 VarU,
147 VarS,
148 Group,
150}
151
152impl Type {
153 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 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#[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 pub fn unit(self) -> usize {
214 match self {
215 Self::Utf16Le | Self::Utf16Be => 2,
216 _ => 1,
217 }
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
223pub enum Delta {
224 #[default]
225 None,
226 All,
228 Block,
230}
231
232#[derive(Debug, Clone, PartialEq)]
234pub struct BitField {
235 pub name: String,
236 pub bit: u32,
238 pub width: u32,
239 pub labels: Option<Arc<BTreeMap<i64, String>>>,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct Lookup {
245 pub file: String,
247 pub format: LookupFormat,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum LookupFormat {
253 Lines,
255 Nul,
257 Fixed(u64),
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct Checksum {
264 pub algo: ChecksumAlgo,
265 pub field: String,
267 pub from: Option<String>,
269 pub to: Option<String>,
271}
272
273#[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 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#[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#[derive(Debug, Clone, PartialEq)]
348pub enum Meaning {
349 Plain,
350 Time {
353 unit: TimeUnitSpec,
354 epoch_ns: i64,
355 },
356 TimeOfDay {
359 unit: TimeUnitSpec,
360 date: Option<String>,
361 },
362 Yyyymmdd,
364 Scale(u32),
366 Linear {
368 factor: f64,
369 offset: f64,
370 },
371 Enum(Arc<BTreeMap<i64, String>>),
373}
374
375#[derive(Debug, Clone, PartialEq)]
377pub struct Field {
378 pub name: Option<String>,
380 pub ty: Type,
381 pub size: Option<Amount>,
383 pub endian: Option<Endian>,
385 pub meaning: Meaning,
386 pub null: Option<Null>,
388 pub count: Option<Amount>,
390 pub flatten: bool,
391 pub file: Option<String>,
394 pub at: Option<Amount>,
396 pub encoding: Encoding,
398 pub delta: Delta,
399 pub bits: Vec<BitField>,
401 pub group: Vec<Field>,
403 pub string_at: Option<String>,
405 pub lookup: Option<Lookup>,
406 pub description: Option<String>,
408 pub unit: Option<String>,
410}
411
412#[derive(Debug, Clone, Default, PartialEq)]
414pub struct Header {
415 pub fields: Vec<Field>,
416 pub size: Option<Amount>,
417}
418
419#[derive(Debug, Clone, Default, PartialEq)]
421pub struct Records {
422 pub framing: Framing,
423 pub fields: Vec<Field>,
425 pub size: Option<Amount>,
428 pub count: Option<Amount>,
430 pub length_suffix: bool,
432 pub align: u64,
434 pub sync: Vec<u8>,
436 pub type_field: Option<String>,
438 pub variants: Vec<Variant>,
439 pub checksum: Option<Checksum>,
440 pub ring: Option<Amount>,
442}
443
444#[derive(Debug, Clone, PartialEq)]
446pub struct Variant {
447 pub name: String,
448 pub when: Vec<Expected>,
450 pub fields: Vec<Field>,
452 pub size: Option<Amount>,
454 pub description: Option<String>,
456}
457
458#[derive(Debug, Clone, Default, PartialEq)]
460pub struct Footer {
461 pub fields: Vec<Field>,
462 pub size: Option<u64>,
463 pub checksum: Option<(ChecksumAlgo, String)>,
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
469pub enum Compression {
470 None,
471 Gzip,
472 Deflate,
473 Zlib,
474 Zstd,
475 Lz4,
477 Lz4Block,
479 Snappy,
481 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#[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#[derive(Debug, Clone, PartialEq)]
531pub struct BlockIndex {
532 pub at: Amount,
534 pub count: Amount,
536 pub fields: Vec<Field>,
538}
539
540#[derive(Debug, Clone, PartialEq)]
542pub struct Blocks {
543 pub header: Vec<Field>,
544 pub size: Amount,
546 pub codec: Codec,
547 pub records: Option<String>,
549 pub uncompressed: Option<String>,
551 pub index: Option<BlockIndex>,
552}
553
554#[derive(Debug, Clone, PartialEq)]
556pub struct Capture {
557 pub header: Vec<Field>,
559 pub count: Option<String>,
561 pub time: Option<String>,
563}
564
565#[derive(Debug, Clone, PartialEq, Eq)]
567pub struct PathPart {
568 pub name: String,
569 pub date: Option<String>,
571}
572
573#[derive(Debug, Clone, PartialEq, Eq)]
575pub struct Files {
576 pub pattern: String,
577 pub parts: Vec<PathPart>,
578}
579
580#[derive(Debug, Clone, PartialEq, Eq)]
582pub struct Section {
583 pub name: String,
584 pub offset: Amount,
585 pub size: Amount,
586}
587
588#[derive(Debug, Clone, PartialEq, Eq)]
590pub enum Expected {
591 Int(i128),
592 Text(String),
593}
594
595#[derive(Debug, Clone, PartialEq, Eq)]
600pub struct MatchChip {
601 pub name: String,
602 pub value: String,
603 pub kind: ChipKind,
604 pub offset: Option<u64>,
606}
607
608#[derive(Debug, Clone, Copy, PartialEq, Eq)]
610pub enum ChipKind {
611 Magic,
613 Hex,
615 Text,
617 Int,
619 Glob,
621}
622
623const CHIP_MAGIC_CHARS: usize = 16;
625const CHIP_MAGIC_BYTES: usize = 8;
626
627impl MatchChip {
628 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 pub fn plain(&self, quote: bool) -> String {
642 format!("{} {}", self.name, self.value_text(quote))
643 }
644}
645
646pub 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
657pub const MAX_SPEC_BYTES: u64 = 1 << 20;
660pub const MAX_SPEC_SAID: &str = "1 MiB";
662
663#[derive(Debug, Clone)]
665pub struct Spec {
666 pub name: String,
667 pub description: Option<String>,
668 pub documentation: Option<String>,
670 pub notes: Vec<(String, ColumnNote)>,
672 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 pub expect: Vec<(String, Expected)>,
680 pub endian: Endian,
681 pub endian_auto: bool,
684 pub layout: Layout,
685 pub header: Header,
686 pub records: Records,
687 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 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
724const UTF8_BOM: &[u8] = b"\xEF\xBB\xBF";
726
727#[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
737fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
755enum Part {
756 Header,
757 Footer,
758 Records,
761}
762
763const 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#[derive(Clone, Copy, Default)]
793struct Scopes<'f> {
794 header: &'f [Field],
795 footer: &'f [Field],
796}
797
798struct 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 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 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 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 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 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 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 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 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 let mut notes: Vec<(usize, String, ColumnNote)> = Vec::new();
1199 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
3027pub 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
3035pub 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
3089fn 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
3096fn 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
3108fn 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
3121fn 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
3130fn 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
3185fn 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
3203pub 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 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 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 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 pub fn is_delimited(&self) -> bool {
3541 self.delimited.is_some()
3542 }
3543
3544 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 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 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 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 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 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 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 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 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 pub fn lists_variants(&self) -> bool {
3729 !self.is_delimited() && self.records.variants.len() > 1 && self.variant.is_none()
3730 }
3731
3732 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#[derive(Debug, Clone, Default, PartialEq)]
3755pub struct SpecDocs {
3756 pub spec: String,
3758 pub file: Option<PathBuf>,
3760 pub description: String,
3761 pub documentation: String,
3763 pub record_types: Vec<RecordType>,
3765 pub columns: Vec<(String, ColumnNote)>,
3768 pub header: Vec<(String, ColumnNote)>,
3770 pub footer: Vec<(String, ColumnNote)>,
3772}
3773
3774fn 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
3786fn 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#[derive(Debug, Clone, Default, PartialEq)]
3805pub struct RecordType {
3806 pub name: String,
3807 pub picked_by: String,
3810 pub description: String,
3811 pub columns: usize,
3813}
3814
3815impl Spec {
3816 pub fn docs(&self) -> Option<SpecDocs> {
3819 let mut columns: Vec<(String, ColumnNote)> = Vec::new();
3820 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 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
3901fn 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
3916fn given_width(fields: &[Field]) -> Option<u64> {
3918 fields
3919 .iter()
3920 .try_fold(0u64, |sum, f| sum.checked_add(field_width(f)?))
3921}
3922
3923pub(crate) fn fields_width(fields: &[Field]) -> Option<u64> {
3925 given_width(fields)
3926}
3927
3928#[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 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 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 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 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#[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
4068pub(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
4154fn 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
4175fn 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
4220fn 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
4239fn 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 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
4281fn 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
4312pub 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 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
4337pub trait SpecRecords: crate::pushdown::Windowed + std::fmt::Debug {
4340 fn rows(&self) -> usize;
4341 fn schema(&self) -> SchemaRef;
4342 fn into_lazy(self: Arc<Self>) -> PolarsResult<LazyFrame>;
4344 fn collect(&self, rows: usize) -> PolarsResult<DataFrame>;
4346 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
4394pub struct Opened {
4396 pub records: Arc<dyn SpecRecords>,
4397 pub notes: Vec<String>,
4399 pub header: HeaderValues,
4400}
4401
4402fn 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
4412const 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
4870pub struct Found {
4871 pub spec: Arc<Spec>,
4872 pub overrides: Vec<PathBuf>,
4873}
4874
4875#[derive(Debug, Clone, Default)]
4877pub struct Registry {
4878 pub specs: Vec<Found>,
4879 pub fix: Vec<FixFound>,
4881 pub dbc: Vec<DbcFound>,
4884 pub errors: Vec<SpecError>,
4886}
4887
4888#[derive(Debug, Clone)]
4890pub struct DbcFound {
4891 pub dbc: Arc<crate::dbc::Dbc>,
4892 pub path: PathBuf,
4894}
4895
4896#[derive(Debug, Clone)]
4898pub struct FixFound {
4899 pub dict: Arc<crate::fix::dict::Dictionary>,
4900 pub overrides: Vec<PathBuf>,
4901}
4902
4903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4905pub enum Chosen {
4906 SpecFile,
4908 Named,
4910 Glob,
4911 Magic,
4912}
4913
4914pub 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#[derive(Debug, Clone)]
4941pub struct Matched {
4942 pub specs: Vec<Arc<Spec>>,
4943 pub by: Chosen,
4944}
4945
4946pub 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
4969pub 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 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 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 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 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 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 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 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 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 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 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 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 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
5300pub 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
5323pub struct Read {
5325 pub spec: Arc<Spec>,
5326 pub by: Chosen,
5327 pub also: Vec<String>,
5329 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#[derive(Debug, Clone, Default)]
5348pub struct Asked {
5349 pub spec_file: Option<PathBuf>,
5351 pub spec_name: Option<String>,
5353 pub spec: Option<Arc<Spec>>,
5355 pub variant: Option<String>,
5358 pub builtin: bool,
5360 pub compression: Option<crate::CompressionFormat>,
5361 pub text_only: bool,
5363}
5364
5365#[derive(Clone)]
5367pub struct Choice {
5368 pub spec: Arc<Spec>,
5369 pub by: Chosen,
5370 pub also: Vec<String>,
5371}
5372
5373pub enum Route {
5375 Elsewhere,
5377 Read(Box<Read>),
5378 Decompress(Choice),
5380 Delimited(Choice),
5382}
5383
5384fn 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
5403fn 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 .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
5426fn 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
5440pub 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 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 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
5540pub 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 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
5612fn 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
5623pub const FORMAT_ONLY: &str = "no match";
5625
5626pub 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(), ®istry, &options) {
5640 Ok(text) => (text, 0),
5641 Err(text) => (text, 1),
5642 }
5643 }
5644 }
5645}
5646
5647const CHECK_ROWS: usize = 10;
5649
5650fn 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 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
5758fn 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
5773fn 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 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
5797fn 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
5851fn 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
5906pub(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
5943fn 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
5961pub 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 const MAX_FILES: usize = 100_000;
5971
5972 pub struct SpecFiles {
5974 parts: Vec<(Arc<dyn SpecRecords>, Vec<AnyValue<'static>>)>,
5975 part_fields: Vec<(PlSmallStr, DataType)>,
5976 starts: Vec<usize>,
5978 rows: usize,
5979 schema: SchemaRef,
5980 sources: Vec<Arc<Bytes>>,
5981 }
5982
5983 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(®ex::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(®ex::escape(rest));
5999 out.push('$');
6000 Ok((regex::Regex::new(&out).map_err(|e| e.to_string())?, names))
6001 }
6002
6003 type PartText = (String, String);
6005
6006 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 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 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 #[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 #[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 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 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 #[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 #[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 #[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 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 #[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 ®istry,
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 ®istry,
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 ®istry,
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 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 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 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, ®istry).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 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, ®istry) else {
7035 panic!("a binary spec does not read several files");
7036 };
7037 assert!(e.contains("reads one file"), "{e}");
7038 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, ®istry).unwrap(),
7043 Route::Delimited(_)
7044 ));
7045 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, ®istry).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 ®istry,
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 #[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 #[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, ®istry, &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, ®istry, &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), ®istry, &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, ®istry, &options).unwrap_err();
7175 assert!(e.starts_with("error: ") && e.contains("bad.dbc\":2"), "{e}");
7176 }
7177
7178 #[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 ®istry,
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 ®istry,
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 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 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 assert_eq!(
7414 note("side").values,
7415 [
7416 ("1".to_string(), "BUY".to_string()),
7417 ("2".into(), "SELL".into())
7418 ]
7419 );
7420 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 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 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 #[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 #[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 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}