1use std::collections::BTreeSet;
12use std::fmt::{Debug, Display};
13use std::num::ParseIntError;
14use std::str::FromStr;
15
16use time::OffsetDateTime;
17use time::format_description::well_known::Rfc3339;
18
19use crate::errors::{NameError, SnapshotError, UnknownLabel};
20use crate::model::app_manifest::{AppManifest, ManifestDeps, ManifestSource, ManifestUnreadable};
21use crate::model::macro_decl::{MacroBody, MacroDecl, MacroKey};
22use crate::model::names::{
23 ApplicationName, Arity, CommitSha, ConfKey, DependencyName, DependencyVersion,
24 ExtractionSettingsDigest, FieldName, FunctionName, HostFactsDigest, MacroName,
25 MakeVariableName, ModuleName, PartialName, ProjectName, RecordName, RelativePath, TagName,
26 TypeName, ValidatorName,
27};
28use crate::model::non_empty::NonEmpty;
29use crate::model::normalised::{NormalisedDefault, NormalisedMacroBody};
30use crate::model::schema_files::{PartialFile, SchemaFile};
31use crate::model::snapshot::{
32 ArityMatch, CallbackSig, Deprecation, DeprecationReplacement, FORMAT_VERSION, FunArity,
33 HeaderRef, HostReleaseFacts, HrlFile, IfdefGuardKind, IfdefMacro, Module, Provenance,
34 RecordDecl, RecordField, SUPPORTED_FORMAT_VERSIONS, Snapshot, SnapshotHeader, SnapshotParts,
35 SpecSig, TestExportVariant, TestOnlyExport, TypeArity, TypeDecl, VariantCBlock, VendoredDep,
36 VendoredDepSource, VersionedMachineVersion, Visibility, WireConstantBinding, WireValue, state,
37};
38use crate::snapshot::SNAPSHOT_SIZE_LIMIT;
39use crate::snapshot::sort::breaks_order;
40
41pub fn parse(input: &str) -> Result<Snapshot<state::Canonical>, SnapshotError> {
42 if input.len() > SNAPSHOT_SIZE_LIMIT {
43 return Err(SnapshotError::SizeLimit {
44 size: input.len(),
45 limit: SNAPSHOT_SIZE_LIMIT,
46 });
47 }
48 let mut parser = Parser::new(input);
49 parser.parse()
50}
51
52pub fn parse_header(input: &str) -> Result<SnapshotHeader, SnapshotError> {
55 Parser::new(header_block(input)).parse_header()
56}
57
58fn header_block(input: &str) -> &str {
59 let mut end = 0;
60 for line in input.split_inclusive('\n') {
61 if !line.starts_with("# ") {
62 break;
63 }
64 end += line.len();
65 }
66 &input[..end]
67}
68
69struct Parser<'a> {
70 lines: Vec<(usize, &'a str)>,
71 cursor: usize,
72 format_version: u32,
73}
74
75impl<'a> Parser<'a> {
76 fn new(input: &'a str) -> Self {
77 let lines = input.lines().enumerate().map(|(i, l)| (i + 1, l)).collect();
78 Self {
79 lines,
80 cursor: 0,
81 format_version: 0,
82 }
83 }
84
85 fn parse(&mut self) -> Result<Snapshot<state::Canonical>, SnapshotError> {
86 let header = self.parse_header()?;
87 self.format_version = header.format_version;
88 let mut apps: Vec<AppManifest> = Vec::new();
89 let mut modules: Vec<Module> = Vec::new();
90 let mut headers: Vec<HrlFile> = Vec::new();
91 let mut schemas: Vec<SchemaFile> = Vec::new();
92 let mut partials: Vec<PartialFile> = Vec::new();
93 let mut section = ClassOrder::new("section order");
94 loop {
95 self.skip_blank();
96 let Some((lineno, line)) = self.peek() else {
97 break;
98 };
99 if let Some(rest) = line.strip_prefix("app ") {
100 self.require_format_5(lineno, "app section")?;
101 section.advance(SectionClass::App, lineno)?;
102 self.advance();
103 let name = ApplicationName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
104 if breaks_order(apps.last().map(|a| &a.name), &name) {
105 return Err(not_canonical(
106 lineno,
107 format!("apps out of order at {name}"),
108 ));
109 }
110 apps.push(self.parse_app_body(name)?);
111 } else if let Some(rest) = line.strip_prefix("module ") {
112 section.advance(SectionClass::Module, lineno)?;
113 self.advance();
114 let name = ModuleName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
115 if breaks_order(modules.last().map(|m| &m.name), &name) {
116 return Err(not_canonical(
117 lineno,
118 format!("modules out of order at {name}"),
119 ));
120 }
121 let module = self.parse_module_body(name)?;
122 if self.format_version >= 5 && module.app.is_none() {
123 return Err(missing_app(lineno, "module", &module.name));
124 }
125 modules.push(module);
126 } else if let Some(rest) = line.strip_prefix("header ") {
127 section.advance(SectionClass::Header, lineno)?;
128 self.advance();
129 let path = RelativePath::from_str(rest.trim()).map_err(SnapshotError::Name)?;
130 if breaks_order(headers.last().map(|h| &h.path), &path) {
131 return Err(not_canonical(
132 lineno,
133 format!("headers out of order at {path}"),
134 ));
135 }
136 let hrl = self.parse_header_body(path)?;
137 if self.format_version >= 5 && hrl.app.is_none() {
138 return Err(missing_app(lineno, "header", &hrl.path));
139 }
140 headers.push(hrl);
141 } else if let Some(rest) = line.strip_prefix("schema ") {
142 self.require_format_5(lineno, "schema section")?;
143 section.advance(SectionClass::Schema, lineno)?;
144 self.advance();
145 let path = RelativePath::from_str(rest.trim()).map_err(SnapshotError::Name)?;
146 if breaks_order(schemas.last().map(|s| &s.path), &path) {
147 return Err(not_canonical(
148 lineno,
149 format!("schemas out of order at {path}"),
150 ));
151 }
152 schemas.push(self.parse_schema_body(lineno, path)?);
153 } else if let Some(rest) = line.strip_prefix("partial ") {
154 self.require_format_5(lineno, "partial section")?;
155 section.advance(SectionClass::Partial, lineno)?;
156 self.advance();
157 let path = RelativePath::from_str(rest.trim()).map_err(SnapshotError::Name)?;
158 if breaks_order(partials.last().map(|p| &p.path), &path) {
159 return Err(not_canonical(
160 lineno,
161 format!("partials out of order at {path}"),
162 ));
163 }
164 partials.push(self.parse_partial_body(lineno, path)?);
165 } else {
166 return Err(SnapshotError::UnexpectedToken {
167 line: lineno,
168 detail: format!(
169 "expected 'app', 'module', 'header', 'schema' or 'partial', got {line:?}"
170 ),
171 });
172 }
173 }
174 Ok(Snapshot::from_canonical_parts(SnapshotParts {
175 header,
176 apps,
177 modules,
178 headers,
179 schemas,
180 partials,
181 }))
182 }
183
184 fn require_format_5(&self, line: usize, what: &str) -> Result<(), SnapshotError> {
185 if self.format_version >= 5 {
186 return Ok(());
187 }
188 Err(SnapshotError::UnexpectedToken {
189 line,
190 detail: format!(
191 "{what} needs format-version 5, this file is format-version {}",
192 self.format_version
193 ),
194 })
195 }
196
197 fn parse_header(&mut self) -> Result<SnapshotHeader, SnapshotError> {
198 let mut project: Option<ProjectName> = None;
199 let mut tag: Option<TagName> = None;
200 let mut branch: Option<String> = None;
201 let mut commit: Option<CommitSha> = None;
202 let mut scanned_paths: Vec<String> = Vec::new();
203 let mut apps_scanned: Vec<ApplicationName> = Vec::new();
204 let mut generated_by: Option<String> = None;
205 let mut generated_at: Option<OffsetDateTime> = None;
206 let mut extractor_version: String = String::new();
207 let mut dep_pins: Vec<VendoredDep> = Vec::new();
208 let mut release_roots: Vec<ApplicationName> = Vec::new();
209 let mut host_facts_digest: Option<HostFactsDigest> = None;
210 let mut extraction_settings: Option<ExtractionSettingsDigest> = None;
211 let mut format_version: u32 = 0;
212 let mut format_version_seen = false;
213 loop {
214 let Some((lineno, line)) = self.peek() else {
215 break;
216 };
217 let Some(content) = line.strip_prefix("# ") else {
218 break;
219 };
220 self.advance();
221 if content == "backhopper snapshot" {
222 continue;
223 }
224 let (key, value) =
225 content
226 .split_once(": ")
227 .ok_or_else(|| SnapshotError::MalformedHeader {
228 line: lineno,
229 detail: format!("missing ': ' in header line {line:?}"),
230 })?;
231 match key {
232 "format-version" => {
233 let parsed: u32 =
234 value.parse().map_err(|_| SnapshotError::MalformedHeader {
235 line: lineno,
236 detail: format!("non-integer format-version {value:?}"),
237 })?;
238 if !SUPPORTED_FORMAT_VERSIONS.contains(&parsed) {
239 return Err(SnapshotError::UnknownFormatVersion {
240 found: value.to_owned(),
241 expected: FORMAT_VERSION,
242 });
243 }
244 format_version = parsed;
245 format_version_seen = true;
246 }
247 "project" => {
248 project = Some(ProjectName::from_str(value).map_err(SnapshotError::Name)?);
249 }
250 "tag" => {
251 tag = Some(TagName::from_str(value).map_err(SnapshotError::Name)?);
252 }
253 "branch" => {
254 branch = Some(value.to_owned());
255 }
256 "commit" => {
257 commit = Some(CommitSha::from_str(value).map_err(SnapshotError::Name)?);
258 }
259 "scanned-paths" => {
260 scanned_paths = value
261 .split(',')
262 .map(|p| p.trim().to_owned())
263 .filter(|p| !p.is_empty())
264 .collect();
265 }
266 "apps-scanned" => {
267 apps_scanned = value
268 .split(',')
269 .map(str::trim)
270 .filter(|s| !s.is_empty())
271 .map(ApplicationName::from_str)
272 .collect::<Result<_, _>>()
273 .map_err(SnapshotError::Name)?;
274 }
275 "generated-by" => generated_by = Some(value.to_owned()),
276 "extractor-version" => value.clone_into(&mut extractor_version),
277 "dep-pin" => {
278 dep_pins.push(parse_dep_pin_line(lineno, value)?);
279 }
280 "release-root" => {
281 require_header_format_5(format_version, lineno, key)?;
282 let root = ApplicationName::from_str(value).map_err(SnapshotError::Name)?;
283 if breaks_order(release_roots.last(), &root) {
284 return Err(not_canonical(
285 lineno,
286 format!("release-root {root} out of order"),
287 ));
288 }
289 release_roots.push(root);
290 }
291 "host-facts" => {
292 require_header_format_5(format_version, lineno, key)?;
293 host_facts_digest =
294 Some(HostFactsDigest::from_str(value).map_err(SnapshotError::Name)?);
295 }
296 "extraction-settings" => {
297 require_header_format_5(format_version, lineno, key)?;
298 extraction_settings = Some(
299 ExtractionSettingsDigest::from_str(value).map_err(SnapshotError::Name)?,
300 );
301 }
302 "generated-at" => {
303 generated_at = Some(OffsetDateTime::parse(value, &Rfc3339).map_err(|_| {
304 SnapshotError::MalformedHeader {
305 line: lineno,
306 detail: format!("invalid timestamp {value:?}"),
307 }
308 })?);
309 }
310 other => {
311 return Err(SnapshotError::UnknownHeaderKey {
312 line: lineno,
313 key: other.to_owned(),
314 });
315 }
316 }
317 }
318 if !format_version_seen {
319 return Err(SnapshotError::MissingHeaderKey {
320 key: "format-version",
321 });
322 }
323 let host_facts = match (NonEmpty::try_from(release_roots), host_facts_digest) {
324 (Ok(release_roots), Some(digest)) => Some(HostReleaseFacts {
325 release_roots,
326 digest,
327 }),
328 (Err(_), None) => None,
329 (Ok(_), None) => return Err(SnapshotError::MissingHeaderKey { key: "host-facts" }),
330 (Err(_), Some(_)) => {
331 return Err(SnapshotError::MissingHeaderKey {
332 key: "release-root",
333 });
334 }
335 };
336 Ok(SnapshotHeader {
337 project: project.ok_or(SnapshotError::MissingHeaderKey { key: "project" })?,
338 tag: tag.ok_or(SnapshotError::MissingHeaderKey { key: "tag" })?,
339 branch,
340 commit: commit.ok_or(SnapshotError::MissingHeaderKey { key: "commit" })?,
341 scanned_paths,
342 apps_scanned,
343 generated_by: generated_by.ok_or(SnapshotError::MissingHeaderKey {
344 key: "generated-by",
345 })?,
346 generated_at: generated_at.ok_or(SnapshotError::MissingHeaderKey {
347 key: "generated-at",
348 })?,
349 extractor_version,
350 format_version,
351 dep_pins,
352 host_facts,
353 extraction_settings,
354 })
355 }
356
357 fn parse_module_body(&mut self, name: ModuleName) -> Result<Module, SnapshotError> {
358 let mut module = Module::new(name);
359 let mut state = ClassOrder::new("entry-class order");
360 loop {
361 let Some((lineno, line)) = self.peek() else {
362 break;
363 };
364 if !line.starts_with(" ") {
365 break;
366 }
367 let trimmed = &line[2..];
368 self.advance();
369 if let Some(rest) = trimmed.strip_prefix("visibility ") {
370 state.advance(EntryClass::Visibility, lineno)?;
371 let kw = rest.trim();
372 module.visibility =
373 Visibility::from_keyword(kw).ok_or_else(|| SnapshotError::UnexpectedToken {
374 line: lineno,
375 detail: format!("unknown visibility {kw:?}"),
376 })?;
377 } else if let Some(rest) = trimmed.strip_prefix("path ") {
378 state.advance(EntryClass::Path, lineno)?;
379 module.path =
380 Some(RelativePath::from_str(rest.trim()).map_err(SnapshotError::Name)?);
381 } else if let Some(rest) = trimmed.strip_prefix("app ") {
382 state.advance(EntryClass::App, lineno)?;
383 let app = ApplicationName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
384 module.app = Some(app);
385 } else if let Some(rest) = trimmed.strip_prefix("behaviour ") {
386 state.advance(EntryClass::Behaviour, lineno)?;
387 let m = ModuleName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
388 if let Some(prev) = module.behaviours.last()
389 && &m <= prev
390 {
391 return Err(SnapshotError::NotCanonical {
392 line: lineno,
393 detail: format!("behaviour {m} out of order"),
394 });
395 }
396 module.behaviours.push(m);
397 } else if let Some(rest) = trimmed.strip_prefix("export ") {
398 state.advance(EntryClass::Export, lineno)?;
399 let fa = parse_fun_arity(rest.trim()).map_err(SnapshotError::Name)?;
400 check_order(&module.exports, &fa, lineno, "export")?;
401 module.exports.push(fa);
402 } else if let Some(rest) = trimmed.strip_prefix("export_type ") {
403 state.advance(EntryClass::ExportType, lineno)?;
404 let ta = parse_type_arity(rest.trim()).map_err(SnapshotError::Name)?;
405 check_order(&module.export_types, &ta, lineno, "export_type")?;
406 module.export_types.push(ta);
407 } else if let Some(rest) = trimmed.strip_prefix("callback ") {
408 state.advance(EntryClass::Callback, lineno)?;
409 let (fa, sig) = parse_fun_arity_and_signature(rest).map_err(|e| {
410 SnapshotError::UnexpectedToken {
411 line: lineno,
412 detail: e,
413 }
414 })?;
415 let signature = self.collect_continuation(sig);
416 module.callbacks.push(CallbackSig {
417 name: fa.name,
418 arity: fa.arity,
419 signature,
420 });
421 } else if let Some(rest) = trimmed.strip_prefix("optional_callback ") {
422 state.advance(EntryClass::OptionalCallback, lineno)?;
423 let fa = parse_fun_arity(rest.trim()).map_err(SnapshotError::Name)?;
424 check_order(&module.optional_callbacks, &fa, lineno, "optional_callback")?;
425 module.optional_callbacks.push(fa);
426 } else if let Some(rest) = trimmed.strip_prefix("spec ") {
427 state.advance(EntryClass::Spec, lineno)?;
428 let (fa, sig) = parse_fun_arity_and_signature(rest).map_err(|e| {
429 SnapshotError::UnexpectedToken {
430 line: lineno,
431 detail: e,
432 }
433 })?;
434 let signature = self.collect_continuation(sig);
435 module.specs.push(SpecSig {
436 name: fa.name,
437 arity: fa.arity,
438 signature,
439 });
440 } else if let Some(rest) = trimmed.strip_prefix("type ") {
441 state.advance(EntryClass::Type, lineno)?;
442 let (ta, rhs) =
443 parse_type_decl_line(rest).map_err(|e| SnapshotError::UnexpectedToken {
444 line: lineno,
445 detail: e,
446 })?;
447 let rhs_full = self.collect_continuation(rhs);
448 module.types.push(TypeDecl {
449 name: ta.name,
450 arity: ta.arity,
451 rhs: rhs_full,
452 });
453 } else if let Some(rest) = trimmed.strip_prefix("opaque ") {
454 state.advance(EntryClass::Opaque, lineno)?;
455 let ta = parse_type_arity(rest.trim()).map_err(SnapshotError::Name)?;
456 check_order(&module.opaques, &ta, lineno, "opaque")?;
457 module.opaques.push(ta);
458 } else if let Some(rest) = trimmed.strip_prefix("deprecated ") {
459 state.advance(EntryClass::Deprecated, lineno)?;
460 let dep = parse_deprecation(rest).map_err(|e| SnapshotError::UnexpectedToken {
461 line: lineno,
462 detail: e,
463 })?;
464 module.deprecations.push(dep);
465 } else if let Some(rest) = trimmed.strip_prefix("record ") {
466 state.advance(EntryClass::Record, lineno)?;
467 let name = RecordName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
468 let fields = self.parse_record_fields()?;
469 module.records.push(RecordDecl { name, fields });
470 } else if let Some(rest) = trimmed.strip_prefix("test_only_export ") {
471 state.advance(EntryClass::TestOnlyExport, lineno)?;
472 let entry = parse_test_only_export(rest).map_err(|detail| {
473 SnapshotError::UnexpectedToken {
474 line: lineno,
475 detail,
476 }
477 })?;
478 module.test_only_exports.push(entry);
479 } else if let Some(rest) = trimmed.strip_prefix("ifdef_macro ") {
480 state.advance(EntryClass::IfdefMacro, lineno)?;
481 let entry =
482 parse_ifdef_macro(rest).map_err(|detail| SnapshotError::UnexpectedToken {
483 line: lineno,
484 detail,
485 })?;
486 module.ifdef_macros.push(entry);
487 } else if let Some(rest) = trimmed.strip_prefix("variant_c_block ") {
488 state.advance(EntryClass::VariantCBlock, lineno)?;
489 let entry = parse_variant_c_block(rest).map_err(|detail| {
490 SnapshotError::UnexpectedToken {
491 line: lineno,
492 detail,
493 }
494 })?;
495 module.variant_c_blocks.push(entry);
496 } else if let Some(rest) = trimmed.strip_prefix("versioned_machine ") {
497 state.advance(EntryClass::VersionedMachine, lineno)?;
498 if module.versioned_machine_version.is_some() {
499 return Err(SnapshotError::UnexpectedToken {
500 line: lineno,
501 detail: "duplicate versioned_machine entry".to_owned(),
502 });
503 }
504 let entry = parse_versioned_machine(rest).map_err(|detail| {
505 SnapshotError::UnexpectedToken {
506 line: lineno,
507 detail,
508 }
509 })?;
510 module.versioned_machine_version = Some(entry);
511 } else if let Some(rest) = trimmed.strip_prefix("wire_constant ") {
512 state.advance(EntryClass::WireConstant, lineno)?;
513 let entry =
514 parse_wire_constant(rest).map_err(|detail| SnapshotError::UnexpectedToken {
515 line: lineno,
516 detail,
517 })?;
518 if let Some(prev) = module.wire_constants.last()
519 && entry.macro_name <= prev.macro_name
520 {
521 return Err(SnapshotError::NotCanonical {
522 line: lineno,
523 detail: format!("wire_constant {} out of order", entry.macro_name),
524 });
525 }
526 module.wire_constants.push(entry);
527 } else {
528 return Err(SnapshotError::UnexpectedToken {
529 line: lineno,
530 detail: format!("unknown module entry {trimmed:?}"),
531 });
532 }
533 }
534 Ok(module)
535 }
536
537 fn parse_app_body(&mut self, name: ApplicationName) -> Result<AppManifest, SnapshotError> {
538 let mut source: Option<ManifestSource> = None;
539 let mut listed: Vec<ApplicationName> = Vec::new();
540 let mut unreadable: Option<ManifestUnreadable> = None;
541 let mut state = ClassOrder::new("app-entry class order");
542 while let Some((lineno, trimmed)) = self.next_entry() {
543 if let Some(rest) = trimmed.strip_prefix("manifest ") {
544 state.advance(AppEntryClass::Manifest, lineno)?;
545 if source.is_some() {
546 return Err(unexpected(lineno, "duplicate manifest line".into()));
547 }
548 source = Some(parse_label(lineno, rest)?);
549 } else if let Some(rest) = trimmed.strip_prefix("requires ") {
550 state.advance(AppEntryClass::Requires, lineno)?;
551 let app = ApplicationName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
552 push_in_order(&mut listed, app, lineno, "requires")?;
553 } else if let Some(rest) = trimmed.strip_prefix("requires_unreadable ") {
554 state.advance(AppEntryClass::RequiresUnreadable, lineno)?;
555 if unreadable.is_some() {
556 return Err(unexpected(
557 lineno,
558 "duplicate requires_unreadable line".into(),
559 ));
560 }
561 unreadable = Some(parse_unreadable(lineno, rest)?);
562 } else {
563 return Err(unexpected(lineno, format!("unknown app entry {trimmed:?}")));
564 }
565 }
566 let source = source.ok_or(SnapshotError::MissingHeaderKey { key: "manifest" })?;
567 let requires = match unreadable {
568 Some(reason) if listed.is_empty() => ManifestDeps::Unreadable(reason),
569 Some(_) => {
570 return Err(unexpected(
571 self.cursor,
572 format!("app {name} lists requirements and marks them unreadable"),
573 ));
574 }
575 None => ManifestDeps::Listed(listed.into_iter().collect()),
576 };
577 Ok(AppManifest {
578 name,
579 source,
580 requires,
581 })
582 }
583
584 fn parse_schema_body(
585 &mut self,
586 start: usize,
587 path: RelativePath,
588 ) -> Result<SchemaFile, SnapshotError> {
589 let (app, _, cuttlefish) = self.parse_cuttlefish_body(start, false)?;
590 Ok(SchemaFile {
591 path,
592 app,
593 mappings: cuttlefish.mappings,
594 validators: cuttlefish.validators,
595 })
596 }
597
598 fn parse_partial_body(
599 &mut self,
600 start: usize,
601 path: RelativePath,
602 ) -> Result<PartialFile, SnapshotError> {
603 let (app, name, cuttlefish) = self.parse_cuttlefish_body(start, true)?;
604 let name = name.ok_or_else(|| missing_line(start, "partial", &path, "name"))?;
605 Ok(PartialFile {
606 path,
607 app,
608 name,
609 mappings: cuttlefish.mappings,
610 validators: cuttlefish.validators,
611 })
612 }
613
614 fn parse_cuttlefish_body(
615 &mut self,
616 start: usize,
617 named: bool,
618 ) -> Result<(ApplicationName, Option<PartialName>, CuttlefishEntries), SnapshotError> {
619 let mut state = ClassOrder::new("cuttlefish-entry class order");
620 let mut app: Option<ApplicationName> = None;
621 let mut name: Option<PartialName> = None;
622 let mut mappings: Vec<ConfKey> = Vec::new();
623 let mut validators: Vec<ValidatorName> = Vec::new();
624 while let Some((lineno, trimmed)) = self.next_entry() {
625 if let Some(rest) = trimmed.strip_prefix("app ") {
626 state.advance(CuttlefishEntryClass::App, lineno)?;
627 app = Some(ApplicationName::from_str(rest.trim()).map_err(SnapshotError::Name)?);
628 } else if named && let Some(rest) = trimmed.strip_prefix("name ") {
629 state.advance(CuttlefishEntryClass::Name, lineno)?;
630 name = Some(PartialName::from_str(rest.trim()).map_err(SnapshotError::Name)?);
631 } else if let Some(rest) = trimmed.strip_prefix("mapping ") {
632 state.advance(CuttlefishEntryClass::Mapping, lineno)?;
633 let key = ConfKey::from_str(rest.trim()).map_err(SnapshotError::Name)?;
634 push_in_order(&mut mappings, key, lineno, "mapping")?;
635 } else if let Some(rest) = trimmed.strip_prefix("validator ") {
636 state.advance(CuttlefishEntryClass::Validator, lineno)?;
637 let validator =
638 ValidatorName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
639 push_in_order(&mut validators, validator, lineno, "validator")?;
640 } else {
641 return Err(unexpected(
642 lineno,
643 format!("unknown cuttlefish entry {trimmed:?}"),
644 ));
645 }
646 }
647 let app =
648 app.ok_or_else(|| unexpected(start, "cuttlefish section has no app line".to_owned()))?;
649 let entries = CuttlefishEntries {
650 mappings: mappings.into_iter().collect(),
651 validators: validators.into_iter().collect(),
652 };
653 Ok((app, name, entries))
654 }
655
656 fn parse_header_body(&mut self, path: RelativePath) -> Result<HrlFile, SnapshotError> {
657 let mut hrl = HrlFile::new(path);
658 let mut state = ClassOrder::new("hrl-entry-class order");
659 let mut defines: Vec<MacroDecl> = Vec::new();
660 while let Some((lineno, trimmed)) = self.next_entry() {
661 if let Some(rest) = trimmed.strip_prefix("app ") {
662 self.require_format_5(lineno, "header app line")?;
663 state.advance(HrlEntryClass::App, lineno)?;
664 hrl.app =
665 Some(ApplicationName::from_str(rest.trim()).map_err(SnapshotError::Name)?);
666 } else if let Some(rest) = trimmed.strip_prefix("include ") {
667 self.require_format_5(lineno, "include line")?;
668 state.advance(HrlEntryClass::Include, lineno)?;
669 let include = parse_header_ref(lineno, rest)?;
670 push_in_order(&mut hrl.includes, include, lineno, "include")?;
671 } else if let Some(rest) = trimmed.strip_prefix("type ") {
672 state.advance(HrlEntryClass::Type, lineno)?;
673 let (ta, rhs) =
674 parse_type_decl_line(rest).map_err(|detail| unexpected(lineno, detail))?;
675 let rhs_full = self.collect_continuation(rhs);
676 hrl.types.push(TypeDecl {
677 name: ta.name,
678 arity: ta.arity,
679 rhs: rhs_full,
680 });
681 } else if let Some(rest) = trimmed.strip_prefix("opaque ") {
682 state.advance(HrlEntryClass::Opaque, lineno)?;
683 let ta = parse_type_arity(rest.trim()).map_err(SnapshotError::Name)?;
684 hrl.opaques.push(ta);
685 } else if let Some(rest) = trimmed.strip_prefix("record ") {
686 state.advance(HrlEntryClass::Record, lineno)?;
687 let name = RecordName::from_str(rest.trim()).map_err(SnapshotError::Name)?;
688 let fields = self.parse_record_fields()?;
689 hrl.records.push(RecordDecl { name, fields });
690 } else if let Some(rest) = trimmed.strip_prefix("define ") {
691 self.require_format_5(lineno, "define line")?;
692 state.advance(HrlEntryClass::Define, lineno)?;
693 let decl = parse_define_line(lineno, rest)?;
694 if breaks_order(defines.last().map(|d| &d.key), &decl.key) {
695 return Err(not_canonical(
696 lineno,
697 format!("define {} out of order", decl.key),
698 ));
699 }
700 defines.push(decl);
701 } else {
702 return Err(unexpected(
703 lineno,
704 format!("unknown header entry {trimmed:?}"),
705 ));
706 }
707 }
708 hrl.defines = defines;
709 Ok(hrl)
710 }
711
712 fn parse_record_fields(&mut self) -> Result<Vec<RecordField>, SnapshotError> {
713 let mut raw: Vec<RawField> = Vec::new();
714 while let Some((flineno, fline)) = self.peek() {
715 if !fline.starts_with(" ") {
716 break;
717 }
718 let ftrimmed = &fline[4..];
719 if let Some(fbody) = ftrimmed.strip_prefix("field ") {
720 self.advance();
721 raw.push(self.parse_field_line(flineno, fbody)?);
722 } else if fline.starts_with(" ") {
723 let Some(last) = raw.last_mut() else {
724 return Err(unexpected(
725 flineno,
726 format!("expected 'field', got {ftrimmed:?}"),
727 ));
728 };
729 let Some(repr) = last.type_text.as_mut() else {
730 return Err(unexpected(
731 flineno,
732 format!(
733 "type continuation but the previous field has no '::' type: {fline:?}",
734 ),
735 ));
736 };
737 self.advance();
738 repr.push('\n');
739 repr.push_str(fline);
740 } else {
741 return Err(unexpected(
742 flineno,
743 format!("expected 'field', got {ftrimmed:?}"),
744 ));
745 }
746 }
747 raw.into_iter().map(|f| self.finish_field(f)).collect()
748 }
749
750 fn parse_field_line(&self, line: usize, fbody: &str) -> Result<RawField, SnapshotError> {
751 let (name, tail) = match fbody.split_once(' ') {
752 Some((name, tail)) => (name, Some(tail)),
753 None => (fbody, None),
754 };
755 let name = FieldName::from_str(name).map_err(SnapshotError::Name)?;
756 let (type_text, default_text) = match tail {
757 None => (None, None),
758 Some(tail) => match (tail.strip_prefix(":: "), tail.strip_prefix("= ")) {
759 (Some(typed), _) => (Some(typed.trim().to_owned()), None),
760 (None, Some(default)) => (None, Some(default.to_owned())),
761 (None, None) => {
762 return Err(unexpected(
763 line,
764 format!("expected '::' or '=' after field {name}, got {tail:?}"),
765 ));
766 }
767 },
768 };
769 Ok(RawField {
770 line,
771 name,
772 type_text,
773 default_text,
774 })
775 }
776
777 fn finish_field(&self, raw: RawField) -> Result<RecordField, SnapshotError> {
778 let RawField {
779 line,
780 name,
781 type_text,
782 default_text,
783 } = raw;
784 let split = type_text
785 .as_deref()
786 .and_then(|t| t.split_once(" = "))
787 .map(|(t, d)| (t.to_owned(), d.to_owned()));
788 let (type_repr, default_text) = match split {
789 Some((t, d)) => (Some(t), Some(d)),
790 None => (type_text, default_text),
791 };
792 let default = match default_text {
793 Some(text) => {
794 self.require_format_5(line, "record field default")?;
795 Some(
796 NormalisedDefault::from_stored(&text)
797 .map_err(|e| unexpected(line, e.to_string()))?,
798 )
799 }
800 None => None,
801 };
802 Ok(RecordField {
803 name,
804 type_repr,
805 default,
806 })
807 }
808
809 fn next_entry(&mut self) -> Option<(usize, &'a str)> {
810 let (lineno, line) = self.peek()?;
811 let trimmed = line.strip_prefix(" ")?;
812 self.advance();
813 Some((lineno, trimmed))
814 }
815
816 fn collect_continuation(&mut self, first: String) -> String {
817 let mut s = first;
818 while let Some((_, line)) = self.peek()
819 && line.starts_with(" ")
820 {
821 self.advance();
822 s.push('\n');
823 s.push_str(line);
824 }
825 s
826 }
827
828 fn peek(&self) -> Option<(usize, &'a str)> {
829 self.lines.get(self.cursor).copied()
830 }
831
832 fn advance(&mut self) {
833 self.cursor += 1;
834 }
835
836 fn skip_blank(&mut self) {
837 while let Some((_, line)) = self.peek() {
838 if line.is_empty() {
839 self.advance();
840 } else {
841 break;
842 }
843 }
844 }
845}
846
847struct RawField {
848 line: usize,
849 name: FieldName,
850 type_text: Option<String>,
851 default_text: Option<String>,
852}
853
854struct CuttlefishEntries {
855 mappings: BTreeSet<ConfKey>,
856 validators: BTreeSet<ValidatorName>,
857}
858
859fn unexpected(line: usize, detail: String) -> SnapshotError {
860 SnapshotError::UnexpectedToken { line, detail }
861}
862
863fn not_canonical(line: usize, detail: String) -> SnapshotError {
864 SnapshotError::NotCanonical { line, detail }
865}
866
867fn missing_app(line: usize, noun: &str, name: &impl Display) -> SnapshotError {
868 unexpected(line, format!("{noun} {name} has no app line"))
869}
870
871fn missing_line(line: usize, noun: &str, name: &impl Display, entry: &str) -> SnapshotError {
872 unexpected(line, format!("{noun} {name} has no {entry} line"))
873}
874
875fn require_header_format_5(
876 format_version: u32,
877 line: usize,
878 key: &str,
879) -> Result<(), SnapshotError> {
880 if format_version >= 5 {
881 return Ok(());
882 }
883 Err(SnapshotError::MalformedHeader {
884 line,
885 detail: format!(
886 "{key} needs format-version 5, this file is format-version {format_version}"
887 ),
888 })
889}
890
891fn push_in_order<T: Ord + Display>(
892 list: &mut Vec<T>,
893 item: T,
894 line: usize,
895 label: &str,
896) -> Result<(), SnapshotError> {
897 if breaks_order(list.last(), &item) {
898 return Err(not_canonical(line, format!("{label} {item} out of order")));
899 }
900 list.push(item);
901 Ok(())
902}
903
904fn parse_label<T: FromStr<Err = UnknownLabel>>(
905 line: usize,
906 text: &str,
907) -> Result<T, SnapshotError> {
908 text.trim()
909 .parse()
910 .map_err(|e: UnknownLabel| unexpected(line, e.to_string()))
911}
912
913fn parse_unreadable(line: usize, rest: &str) -> Result<ManifestUnreadable, SnapshotError> {
914 let (kind, argument) = match rest.trim().split_once(' ') {
915 Some((kind, argument)) => (kind, Some(argument.trim())),
916 None => (rest.trim(), None),
917 };
918 match (kind, argument) {
919 ("missing", None) => Ok(ManifestUnreadable::Missing),
920 ("parse_failed", None) => Ok(ManifestUnreadable::ParseFailed),
921 ("unexpanded_variable", Some(name)) => Ok(ManifestUnreadable::UnexpandedVariable(
922 MakeVariableName::from_str(name).map_err(SnapshotError::Name)?,
923 )),
924 _ => Err(unexpected(
925 line,
926 format!("unknown requires_unreadable reason {rest:?}"),
927 )),
928 }
929}
930
931fn parse_header_ref(line: usize, rest: &str) -> Result<HeaderRef, SnapshotError> {
932 let (app, app_path) = rest
933 .trim()
934 .split_once(' ')
935 .ok_or_else(|| unexpected(line, format!("expected 'app path' in include {rest:?}")))?;
936 Ok(HeaderRef {
937 app: ApplicationName::from_str(app).map_err(SnapshotError::Name)?,
938 app_path: RelativePath::from_str(app_path.trim()).map_err(SnapshotError::Name)?,
939 })
940}
941
942fn parse_define_line(line: usize, rest: &str) -> Result<MacroDecl, SnapshotError> {
943 let (key, body) = rest
944 .split_once(' ')
945 .ok_or_else(|| unexpected(line, format!("expected 'NAME body' in define {rest:?}")))?;
946 let key = MacroKey::from_str(key).map_err(SnapshotError::Name)?;
947 let body = if body == "?" {
948 MacroBody::Conditional
949 } else {
950 MacroBody::Text(
951 NormalisedMacroBody::from_stored(body).map_err(|e| unexpected(line, e.to_string()))?,
952 )
953 };
954 Ok(MacroDecl { key, body })
955}
956
957fn parse_fun_arity(s: &str) -> Result<FunArity, NameError> {
958 let (n, a) = s
959 .split_once('/')
960 .ok_or_else(|| NameError::PatternMismatch {
961 kind: "function/arity",
962 value: s.to_owned(),
963 pattern: "name/arity",
964 })?;
965 Ok(FunArity {
966 name: FunctionName::from_str(n)?,
967 arity: Arity::from_str(a)?,
968 })
969}
970
971fn parse_type_arity(s: &str) -> Result<TypeArity, NameError> {
972 let (n, a) = s
973 .split_once('/')
974 .ok_or_else(|| NameError::PatternMismatch {
975 kind: "type/arity",
976 value: s.to_owned(),
977 pattern: "name/arity",
978 })?;
979 Ok(TypeArity {
980 name: TypeName::from_str(n)?,
981 arity: Arity::from_str(a)?,
982 })
983}
984
985fn parse_fun_arity_and_signature(s: &str) -> Result<(FunArity, String), String> {
986 let (head, sig) = s
987 .split_once(' ')
988 .ok_or_else(|| format!("expected 'name/arity body' in {s:?}"))?;
989 let fa = parse_fun_arity(head).map_err(|e| e.to_string())?;
990 Ok((fa, sig.to_owned()))
991}
992
993fn parse_test_only_export(s: &str) -> Result<TestOnlyExport, String> {
994 let mut parts = s.split_whitespace();
995 let variant_token = parts
996 .next()
997 .ok_or_else(|| format!("missing variant in test_only_export {s:?}"))?;
998 let variant = TestExportVariant::from_label(variant_token)
999 .ok_or_else(|| format!("unknown test_only_export variant {variant_token:?}"))?;
1000 let fa_token = parts
1001 .next()
1002 .ok_or_else(|| format!("missing function/arity in test_only_export {s:?}"))?;
1003 let fa = parse_fun_arity(fa_token).map_err(|e| e.to_string())?;
1004 let mut export_line: Option<usize> = None;
1005 let mut body_line: Option<usize> = None;
1006 for token in parts {
1007 let (key, value) = token
1008 .split_once('=')
1009 .ok_or_else(|| format!("expected key=value in test_only_export, got {token:?}"))?;
1010 match key {
1011 "export_line" => {
1012 export_line = Some(
1013 value
1014 .parse()
1015 .map_err(|e: ParseIntError| format!("invalid export_line: {e}"))?,
1016 );
1017 }
1018 "body_line" => {
1019 body_line = Some(
1020 value
1021 .parse()
1022 .map_err(|e: ParseIntError| format!("invalid body_line: {e}"))?,
1023 );
1024 }
1025 other => return Err(format!("unknown test_only_export key {other:?}")),
1026 }
1027 }
1028 let export_line = export_line.ok_or_else(|| "missing export_line".to_owned())?;
1029 if matches!(variant, TestExportVariant::B) && body_line.is_none() {
1030 return Err("variant=b requires body_line".into());
1031 }
1032 Ok(TestOnlyExport {
1033 function: fa.name,
1034 arity: fa.arity,
1035 export_line,
1036 body_line,
1037 variant,
1038 })
1039}
1040
1041fn parse_ifdef_macro(s: &str) -> Result<IfdefMacro, String> {
1042 let mut parts = s.split_whitespace();
1043 let name_token = parts
1044 .next()
1045 .ok_or_else(|| format!("missing macro name in ifdef_macro {s:?}"))?;
1046 let name = name_token.to_owned();
1047 let mut guard_kind: Option<IfdefGuardKind> = None;
1048 let mut line: Option<usize> = None;
1049 for token in parts {
1050 let (key, value) = token
1051 .split_once('=')
1052 .ok_or_else(|| format!("expected key=value in ifdef_macro, got {token:?}"))?;
1053 match key {
1054 "guard" => {
1055 guard_kind = Some(
1056 IfdefGuardKind::from_label(value)
1057 .ok_or_else(|| format!("unknown ifdef_macro guard {value:?}"))?,
1058 );
1059 }
1060 "line" => {
1061 line = Some(
1062 value
1063 .parse()
1064 .map_err(|e: ParseIntError| format!("invalid line: {e}"))?,
1065 );
1066 }
1067 other => return Err(format!("unknown ifdef_macro key {other:?}")),
1068 }
1069 }
1070 let guard_kind = guard_kind.ok_or_else(|| "missing guard".to_owned())?;
1071 let line = line.ok_or_else(|| "missing line".to_owned())?;
1072 Ok(IfdefMacro {
1073 name,
1074 line,
1075 guard_kind,
1076 })
1077}
1078
1079fn parse_variant_c_block(s: &str) -> Result<VariantCBlock, String> {
1080 let mut guard: Option<String> = None;
1081 let mut start_line: Option<usize> = None;
1082 let mut else_line: Option<usize> = None;
1083 let mut end_line: Option<usize> = None;
1084 for token in s.split_whitespace() {
1085 let (key, value) = token
1086 .split_once('=')
1087 .ok_or_else(|| format!("expected key=value in variant_c_block, got {token:?}"))?;
1088 match key {
1089 "guard" => guard = Some(value.to_owned()),
1090 "start_line" => {
1091 start_line = Some(
1092 value
1093 .parse()
1094 .map_err(|e: ParseIntError| format!("invalid start_line: {e}"))?,
1095 );
1096 }
1097 "else_line" => {
1098 else_line = Some(
1099 value
1100 .parse()
1101 .map_err(|e: ParseIntError| format!("invalid else_line: {e}"))?,
1102 );
1103 }
1104 "end_line" => {
1105 end_line = Some(
1106 value
1107 .parse()
1108 .map_err(|e: ParseIntError| format!("invalid end_line: {e}"))?,
1109 );
1110 }
1111 other => return Err(format!("unknown variant_c_block key {other:?}")),
1112 }
1113 }
1114 let guard = guard.ok_or_else(|| "missing guard".to_owned())?;
1115 let start_line = start_line.ok_or_else(|| "missing start_line".to_owned())?;
1116 let end_line = end_line.ok_or_else(|| "missing end_line".to_owned())?;
1117 Ok(VariantCBlock {
1118 guard,
1119 start_line,
1120 end_line,
1121 else_line,
1122 })
1123}
1124
1125fn parse_type_decl_line(s: &str) -> Result<(TypeArity, String), String> {
1126 let (head, rhs) = s
1127 .split_once(" :: ")
1128 .ok_or_else(|| format!("expected 'name/arity :: rhs' in {s:?}"))?;
1129 let ta = parse_type_arity(head).map_err(|e| e.to_string())?;
1130 Ok((ta, rhs.to_owned()))
1131}
1132
1133fn parse_deprecation(rest: &str) -> Result<Deprecation, String> {
1134 if rest.trim_start().starts_with("module") {
1135 return Ok(Deprecation {
1136 function: None,
1137 arity_match: ArityMatch::Any,
1138 since: None,
1139 replacement: None,
1140 reason: None,
1141 module_wide: true,
1142 });
1143 }
1144 let mut tokens = rest.split_whitespace().peekable();
1145 let head = tokens.next().ok_or("empty deprecation")?;
1146 let (function, arity_match) = if head == "*" {
1147 (None, ArityMatch::Any)
1148 } else {
1149 let (n, a) = head
1150 .split_once('/')
1151 .ok_or_else(|| format!("expected name/arity got {head:?}"))?;
1152 let arity_match = if a == "*" {
1153 ArityMatch::Any
1154 } else {
1155 ArityMatch::Exact {
1156 arity: Arity::from_str(a).map_err(|e| e.to_string())?,
1157 }
1158 };
1159 let function = FunctionName::from_str(n).map_err(|e| e.to_string())?;
1160 (Some(function), arity_match)
1161 };
1162 let mut since: Option<TagName> = None;
1163 let mut replacement: Option<DeprecationReplacement> = None;
1164 let mut reason: Option<String> = None;
1165 while let Some(tok) = tokens.next() {
1166 match tok {
1167 "since" => {
1168 let value = tokens.next().ok_or("missing 'since' value")?;
1169 since = Some(TagName::from_str(value).map_err(|e| e.to_string())?);
1170 }
1171 "use" => {
1172 let target = tokens.next().ok_or("missing 'use' target")?;
1173 let (n, a) = target
1174 .split_once('/')
1175 .ok_or_else(|| format!("expected use name/arity got {target:?}"))?;
1176 replacement = Some(DeprecationReplacement {
1177 function: FunctionName::from_str(n).map_err(|e| e.to_string())?,
1178 arity: Arity::from_str(a).map_err(|e| e.to_string())?,
1179 });
1180 }
1181 "reason" => {
1182 let open = rest.find('"').ok_or("reason must be a quoted string")?;
1184 let close = rest.rfind('"').ok_or("reason must be a quoted string")?;
1185 if close <= open {
1186 return Err("reason must be a quoted string".into());
1187 }
1188 reason = Some(unescape_reason(&rest[open + 1..close]));
1189 break;
1190 }
1191 other => return Err(format!("unexpected deprecation token {other:?}")),
1192 }
1193 }
1194 Ok(Deprecation {
1195 function,
1196 arity_match,
1197 since,
1198 replacement,
1199 reason,
1200 module_wide: false,
1201 })
1202}
1203
1204fn unescape_reason(s: &str) -> String {
1206 let mut out = String::with_capacity(s.len());
1207 let mut chars = s.chars();
1208 while let Some(c) = chars.next() {
1209 if c != '\\' {
1210 out.push(c);
1211 continue;
1212 }
1213 match chars.next() {
1214 Some('n') => out.push('\n'),
1215 Some('t') => out.push('\t'),
1216 Some('r') => out.push('\r'),
1217 Some('"') => out.push('"'),
1218 Some('\\') => out.push('\\'),
1219 Some(other) => {
1220 out.push('\\');
1221 out.push(other);
1222 }
1223 None => out.push('\\'),
1224 }
1225 }
1226 out
1227}
1228
1229trait ArityOrdered {
1232 type Key: Ord;
1233 fn order_key(&self) -> Self::Key;
1234 fn describe(&self) -> String;
1235}
1236
1237impl ArityOrdered for FunArity {
1238 type Key = (FunctionName, Arity);
1239 fn order_key(&self) -> Self::Key {
1240 (self.name.clone(), self.arity)
1241 }
1242 fn describe(&self) -> String {
1243 format!("{}/{}", self.name, self.arity)
1244 }
1245}
1246
1247impl ArityOrdered for TypeArity {
1248 type Key = (TypeName, Arity);
1249 fn order_key(&self) -> Self::Key {
1250 (self.name.clone(), self.arity)
1251 }
1252 fn describe(&self) -> String {
1253 format!("{}/{}", self.name, self.arity)
1254 }
1255}
1256
1257fn check_order<T: ArityOrdered>(
1258 existing: &[T],
1259 item: &T,
1260 lineno: usize,
1261 label: &str,
1262) -> Result<(), SnapshotError> {
1263 if let Some(prev) = existing.last() {
1264 if item.order_key() <= prev.order_key() {
1265 return Err(SnapshotError::NotCanonical {
1266 line: lineno,
1267 detail: format!("{label} {} out of order", item.describe()),
1268 });
1269 }
1270 }
1271 Ok(())
1272}
1273
1274fn parse_dep_pin_line(line: usize, value: &str) -> Result<VendoredDep, SnapshotError> {
1275 let (name_part, after_eq) =
1276 value
1277 .split_once('=')
1278 .ok_or_else(|| SnapshotError::MalformedHeader {
1279 line,
1280 detail: format!("dep-pin missing '=': {value:?}"),
1281 })?;
1282 let name_str = name_part.trim();
1283 let source_and_version = after_eq.trim();
1284 let (source_label, version_str) =
1285 source_and_version
1286 .split_once(' ')
1287 .ok_or_else(|| SnapshotError::MalformedHeader {
1288 line,
1289 detail: format!("dep-pin missing version after source: {value:?}"),
1290 })?;
1291 let source_label = source_label.trim();
1292 let source = VendoredDepSource::from_label(source_label).ok_or_else(|| {
1293 SnapshotError::MalformedHeader {
1294 line,
1295 detail: format!("unknown dep-pin source {source_label:?}"),
1296 }
1297 })?;
1298 let name = DependencyName::from_str(name_str).map_err(SnapshotError::Name)?;
1299 let version = DependencyVersion::from_str(version_str.trim()).map_err(SnapshotError::Name)?;
1300 Ok(VendoredDep {
1301 name,
1302 version,
1303 source,
1304 })
1305}
1306
1307fn parse_versioned_machine(rest: &str) -> Result<VersionedMachineVersion, String> {
1308 let (fa_part, after_fa) = rest
1309 .split_once('=')
1310 .ok_or_else(|| format!("versioned_machine missing '=': {rest:?}"))?;
1311 let fa = parse_fun_arity(fa_part.trim()).map_err(|e| e.to_string())?;
1312 let after_fa = after_fa.trim();
1313 let (value_part, provenance_part) = match after_fa.split_once(' ') {
1314 Some((v, p)) => (v.trim(), p.trim()),
1315 None => (after_fa, ""),
1316 };
1317 let value = match value_part {
1318 "-" => None,
1319 s => Some(
1320 s.parse::<u64>()
1321 .map_err(|e| format!("versioned_machine value: {e}"))?,
1322 ),
1323 };
1324 let provenance = parse_provenance(provenance_part)?;
1325 Ok(VersionedMachineVersion {
1326 function: fa.name,
1327 arity: fa.arity,
1328 value,
1329 provenance,
1330 })
1331}
1332
1333fn parse_provenance(rest: &str) -> Result<Provenance, String> {
1334 if rest.is_empty() || rest == "literal" {
1335 return Ok(Provenance::Literal);
1336 }
1337 if let Some(after) = rest.strip_prefix("macro_body ") {
1338 let (name_str, at_tail) = match after.split_once(" at ") {
1339 Some((n, p)) => (n.trim(), Some(p.trim().to_owned())),
1340 None => (after.trim(), None),
1341 };
1342 let macro_name = MacroName::from_str(name_str).map_err(|e| e.to_string())?;
1343 return Ok(Provenance::MacroBody {
1344 macro_name,
1345 defined_in: at_tail,
1346 });
1347 }
1348 Err(format!("unknown provenance {rest:?}"))
1349}
1350
1351fn parse_wire_constant(rest: &str) -> Result<WireConstantBinding, String> {
1352 let (name_part, after_eq) = rest
1353 .split_once('=')
1354 .ok_or_else(|| format!("wire_constant missing '=': {rest:?}"))?;
1355 let macro_name = MacroName::from_str(name_part.trim()).map_err(|e| e.to_string())?;
1356 let body = after_eq.trim();
1357 let (body_no_at, defined_in) = match body.rsplit_once(" at ") {
1358 Some((b, p)) => (b.trim(), Some(p.trim().to_owned())),
1359 None => (body, None),
1360 };
1361 let (kind, value_str) = body_no_at
1362 .split_once(' ')
1363 .ok_or_else(|| format!("wire_constant missing type/value: {rest:?}"))?;
1364 let value = match kind.trim() {
1365 "u64" => WireValue::U64(
1366 value_str
1367 .trim()
1368 .parse::<u64>()
1369 .map_err(|e| format!("wire_constant u64 value: {e}"))?,
1370 ),
1371 "bytes" => WireValue::Bytes(value_str.trim().as_bytes().to_vec()),
1372 "opaque" => WireValue::Opaque(value_str.trim().to_owned()),
1373 other => return Err(format!("unknown wire_constant kind {other:?}")),
1374 };
1375 Ok(WireConstantBinding {
1376 macro_name,
1377 value,
1378 defined_in,
1379 })
1380}
1381
1382#[derive(Debug)]
1383struct ClassOrder<T> {
1384 label: &'static str,
1385 last: Option<T>,
1386}
1387
1388impl<T: Copy + Ord + Debug> ClassOrder<T> {
1389 fn new(label: &'static str) -> Self {
1390 Self { label, last: None }
1391 }
1392
1393 fn advance(&mut self, c: T, line: usize) -> Result<(), SnapshotError> {
1394 if let Some(prev) = self.last
1395 && c < prev
1396 {
1397 return Err(SnapshotError::NotCanonical {
1398 line,
1399 detail: format!("{}: {c:?} after {prev:?}", self.label),
1400 });
1401 }
1402 self.last = Some(c);
1403 Ok(())
1404 }
1405}
1406
1407#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1408enum EntryClass {
1409 Visibility,
1410 Path,
1411 App,
1412 Behaviour,
1413 Export,
1414 ExportType,
1415 Callback,
1416 OptionalCallback,
1417 Spec,
1418 Type,
1419 Opaque,
1420 Deprecated,
1421 Record,
1422 TestOnlyExport,
1423 IfdefMacro,
1424 VariantCBlock,
1425 VersionedMachine,
1426 WireConstant,
1427}
1428
1429#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1430enum HrlEntryClass {
1431 App,
1432 Include,
1433 Type,
1434 Opaque,
1435 Record,
1436 Define,
1437}
1438
1439#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1440enum SectionClass {
1441 App,
1442 Module,
1443 Header,
1444 Schema,
1445 Partial,
1446}
1447
1448#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1449enum AppEntryClass {
1450 Manifest,
1451 Requires,
1452 RequiresUnreadable,
1453}
1454
1455#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1456enum CuttlefishEntryClass {
1457 App,
1458 Name,
1459 Mapping,
1460 Validator,
1461}