Skip to main content

backhopper_core/snapshot/
parser.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! Canonical reader for the snapshot file format.
6//!
7//! Produces `Snapshot<Canonical>` only when the input parses *and* is in
8//! canonical order. Non-canonical input is a hard error so we never
9//! silently accept a hand-edited snapshot that has drifted out of order.
10
11use 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
52/// Parses only the leading `# ` header block, so a caller that needs the
53/// provenance of a snapshot does not pay for its body.
54pub 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                // decode the reason from its first to last quote, preserving internal spacing and escapes
1183                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
1204// reverse the writer's Rust-debug quoting; unknown escapes pass through so no bytes are lost
1205fn 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
1229/// A `name/arity` entry with a canonical order, so one `check_order`
1230/// gates both the function-arity and type-arity streams.
1231trait 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}