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