Skip to main content

backhopper_core/snapshot/
format.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 writer for the snapshot file format.
6//!
7//! Only `Snapshot<state::Canonical>` is writable; the type system ensures
8//! a non-canonical snapshot can never reach this code.
9
10use std::io::{self, Write};
11
12use time::format_description::well_known::Rfc3339;
13
14use crate::errors::SnapshotError;
15use crate::model::app_manifest::{AppManifest, ManifestDeps, ManifestUnreadable};
16use crate::model::macro_decl::{MacroBody, MacroDecl};
17use crate::model::names::{ConfKey, ModuleName, ValidatorName};
18use crate::model::schema_files::{PartialFile, SchemaFile};
19use crate::model::snapshot::{
20    ArityMatch, Deprecation, HrlFile, Module, Provenance, RecordDecl, Snapshot, SnapshotHeader,
21    VendoredDep, VersionedMachineVersion, Visibility, WireConstantBinding, WireValue, state,
22};
23
24pub const HEADER_PREFIX: &str = "# ";
25
26fn write_section<T, W: Write>(
27    w: &mut W,
28    first: &mut bool,
29    items: &[T],
30    mut write_item: impl FnMut(&T, &mut W) -> io::Result<()>,
31) -> io::Result<()> {
32    for item in items {
33        if !*first {
34            writeln!(w)?;
35        }
36        *first = false;
37        write_item(item, w)?;
38    }
39    Ok(())
40}
41
42pub fn write<W: Write>(
43    snapshot: &Snapshot<state::Canonical>,
44    w: &mut W,
45) -> Result<(), SnapshotError> {
46    if let Some(detail) = snapshot.unwritable_reason() {
47        return Err(SnapshotError::Unwritable { detail });
48    }
49    write_header(snapshot.header(), snapshot.written_format_version(), w)?;
50    let mut first = true;
51    write_section(w, &mut first, snapshot.apps(), write_app)?;
52    write_section(w, &mut first, snapshot.modules(), write_module)?;
53    write_section(w, &mut first, snapshot.headers(), write_hrl)?;
54    write_section(w, &mut first, snapshot.schemas(), write_schema)?;
55    write_section(w, &mut first, snapshot.partials(), write_partial)?;
56    Ok(())
57}
58
59pub fn to_string(snapshot: &Snapshot<state::Canonical>) -> Result<String, SnapshotError> {
60    let mut buf = Vec::new();
61    write(snapshot, &mut buf)?;
62    String::from_utf8(buf).map_err(|e| SnapshotError::InvalidUtf8 {
63        offset: e.utf8_error().valid_up_to(),
64    })
65}
66
67/// Write the canonical header followed by the single named module's section.
68/// Returns `Ok(false)` if no such module is present in the snapshot.
69pub fn write_module_filtered<W: Write>(
70    snapshot: &Snapshot<state::Canonical>,
71    module: &ModuleName,
72    w: &mut W,
73) -> Result<bool, SnapshotError> {
74    write_header(snapshot.header(), snapshot.written_format_version(), w)?;
75    let Some(m) = snapshot.module_named(module) else {
76        return Ok(false);
77    };
78    write_module(m, w)?;
79    Ok(true)
80}
81
82fn write_header<W: Write>(
83    header: &SnapshotHeader,
84    format_version: u32,
85    w: &mut W,
86) -> io::Result<()> {
87    writeln!(w, "# backhopper snapshot")?;
88    writeln!(w, "# format-version: {format_version}")?;
89    writeln!(w, "# project: {}", header.project)?;
90    writeln!(w, "# tag: {}", header.tag)?;
91    if let Some(branch) = &header.branch {
92        writeln!(w, "# branch: {branch}")?;
93    }
94    writeln!(w, "# commit: {}", header.commit)?;
95    writeln!(w, "# scanned-paths: {}", header.scanned_paths.join(", "))?;
96    if !header.apps_scanned.is_empty() {
97        let names: Vec<&str> = header.apps_scanned.iter().map(|a| a.as_str()).collect();
98        writeln!(w, "# apps-scanned: {}", names.join(", "))?;
99    }
100    writeln!(w, "# generated-by: {}", header.generated_by)?;
101    let formatted = header
102        .generated_at
103        .format(&Rfc3339)
104        .unwrap_or_else(|_| String::from("0"));
105    writeln!(w, "# generated-at: {formatted}")?;
106    if !header.extractor_version.is_empty() {
107        writeln!(w, "# extractor-version: {}", header.extractor_version)?;
108    }
109    if let Some(digest) = &header.extraction_settings {
110        writeln!(w, "# extraction-settings: {digest}")?;
111    }
112    for pin in &header.dep_pins {
113        write_dep_pin(w, pin)?;
114    }
115    if let Some(host) = &header.host_facts {
116        for root in &host.release_roots {
117            writeln!(w, "# release-root: {root}")?;
118        }
119        writeln!(w, "# host-facts: {}", host.digest)?;
120    }
121    writeln!(w)?;
122    Ok(())
123}
124
125fn write_dep_pin<W: Write>(w: &mut W, pin: &VendoredDep) -> io::Result<()> {
126    let label = pin.source.as_label();
127    writeln!(w, "# dep-pin: {} = {} {}", pin.name, label, pin.version)
128}
129
130/// One module's canonical section text, without any snapshot header:
131/// the shape `tree show` prints for a live tree, where no snapshot
132/// exists to head it.
133pub fn module_to_string(m: &Module) -> Result<String, SnapshotError> {
134    let mut buf = Vec::new();
135    write_module(m, &mut buf)?;
136    String::from_utf8(buf).map_err(|e| SnapshotError::InvalidUtf8 {
137        offset: e.utf8_error().valid_up_to(),
138    })
139}
140
141fn write_module<W: Write>(m: &Module, w: &mut W) -> io::Result<()> {
142    writeln!(w, "module {}", m.name)?;
143    if m.visibility != Visibility::Public {
144        writeln!(w, "  visibility {}", m.visibility.keyword())?;
145    }
146    if let Some(path) = &m.path {
147        writeln!(w, "  path {path}")?;
148    }
149    if let Some(app) = &m.app {
150        writeln!(w, "  app {app}")?;
151    }
152    for b in &m.behaviours {
153        writeln!(w, "  behaviour {b}")?;
154    }
155    for e in &m.exports {
156        writeln!(w, "  export {}/{}", e.name, e.arity)?;
157    }
158    for et in &m.export_types {
159        writeln!(w, "  export_type {}/{}", et.name, et.arity)?;
160    }
161    for c in &m.callbacks {
162        write_keyword_signature(
163            w,
164            "callback",
165            &format!("{}/{}", c.name, c.arity),
166            &c.signature,
167        )?;
168    }
169    for oc in &m.optional_callbacks {
170        writeln!(w, "  optional_callback {}/{}", oc.name, oc.arity)?;
171    }
172    for s in &m.specs {
173        write_keyword_signature(w, "spec", &format!("{}/{}", s.name, s.arity), &s.signature)?;
174    }
175    for t in &m.types {
176        let body = format!("{}/{} :: {}", t.name, t.arity, t.rhs);
177        write_keyword_signature(w, "type", &body, "")?;
178    }
179    for o in &m.opaques {
180        writeln!(w, "  opaque {}/{}", o.name, o.arity)?;
181    }
182    for d in &m.deprecations {
183        write_deprecation(d, w)?;
184    }
185    for r in &m.records {
186        write_record(r, w)?;
187    }
188    for t in &m.test_only_exports {
189        let variant = t.variant.as_label();
190        match t.body_line {
191            Some(body) => writeln!(
192                w,
193                "  test_only_export {} {}/{} export_line={} body_line={}",
194                variant, t.function, t.arity, t.export_line, body
195            )?,
196            None => writeln!(
197                w,
198                "  test_only_export {} {}/{} export_line={}",
199                variant, t.function, t.arity, t.export_line
200            )?,
201        }
202    }
203    for im in &m.ifdef_macros {
204        let guard = im.guard_kind.as_label();
205        writeln!(
206            w,
207            "  ifdef_macro {} guard={} line={}",
208            im.name, guard, im.line
209        )?;
210    }
211    for vc in &m.variant_c_blocks {
212        match vc.else_line {
213            Some(else_line) => writeln!(
214                w,
215                "  variant_c_block guard={} start_line={} else_line={} end_line={}",
216                vc.guard, vc.start_line, else_line, vc.end_line
217            )?,
218            None => writeln!(
219                w,
220                "  variant_c_block guard={} start_line={} end_line={}",
221                vc.guard, vc.start_line, vc.end_line
222            )?,
223        }
224    }
225    if let Some(v) = &m.versioned_machine_version {
226        write_versioned_machine(w, v)?;
227    }
228    for wc in &m.wire_constants {
229        write_wire_constant(w, wc)?;
230    }
231    Ok(())
232}
233
234fn write_versioned_machine<W: Write>(w: &mut W, v: &VersionedMachineVersion) -> io::Result<()> {
235    let value = match v.value {
236        Some(n) => n.to_string(),
237        None => "-".to_owned(),
238    };
239    match &v.provenance {
240        Provenance::Literal => writeln!(
241            w,
242            "  versioned_machine {}/{} = {} literal",
243            v.function, v.arity, value
244        ),
245        Provenance::MacroBody {
246            macro_name,
247            defined_in: Some(path),
248        } => writeln!(
249            w,
250            "  versioned_machine {}/{} = {} macro_body {} at {}",
251            v.function, v.arity, value, macro_name, path
252        ),
253        Provenance::MacroBody {
254            macro_name,
255            defined_in: None,
256        } => writeln!(
257            w,
258            "  versioned_machine {}/{} = {} macro_body {}",
259            v.function, v.arity, value, macro_name
260        ),
261    }
262}
263
264fn write_wire_constant<W: Write>(w: &mut W, wc: &WireConstantBinding) -> io::Result<()> {
265    let (kind, body) = match &wc.value {
266        WireValue::U64(n) => ("u64", n.to_string()),
267        WireValue::Bytes(b) => ("bytes", String::from_utf8_lossy(b).into_owned()),
268        WireValue::Opaque(s) => ("opaque", s.clone()),
269    };
270    match &wc.defined_in {
271        Some(path) => writeln!(
272            w,
273            "  wire_constant {} = {} {} at {}",
274            wc.macro_name, kind, body, path
275        ),
276        None => writeln!(w, "  wire_constant {} = {} {}", wc.macro_name, kind, body),
277    }
278}
279
280fn write_record<W: Write>(r: &RecordDecl, w: &mut W) -> io::Result<()> {
281    writeln!(w, "  record {}", r.name)?;
282    for f in &r.fields {
283        write!(w, "    field {}", f.name)?;
284        if let Some(t) = &f.type_repr {
285            write!(w, " :: {t}")?;
286        }
287        if let Some(d) = &f.default {
288            write!(w, " = {d}")?;
289        }
290        writeln!(w)?;
291    }
292    Ok(())
293}
294
295fn write_hrl<W: Write>(h: &HrlFile, w: &mut W) -> io::Result<()> {
296    writeln!(w, "header {}", h.path)?;
297    if let Some(app) = &h.app {
298        writeln!(w, "  app {app}")?;
299    }
300    for i in &h.includes {
301        writeln!(w, "  include {} {}", i.app, i.app_path)?;
302    }
303    for t in &h.types {
304        let body = format!("{}/{} :: {}", t.name, t.arity, t.rhs);
305        write_keyword_signature(w, "type", &body, "")?;
306    }
307    for o in &h.opaques {
308        writeln!(w, "  opaque {}/{}", o.name, o.arity)?;
309    }
310    for r in &h.records {
311        write_record(r, w)?;
312    }
313    for d in &h.defines {
314        write_define(d, w)?;
315    }
316    Ok(())
317}
318
319fn write_define<W: Write>(d: &MacroDecl, w: &mut W) -> io::Result<()> {
320    match &d.body {
321        MacroBody::Text(body) => writeln!(w, "  define {} {body}", d.key),
322        MacroBody::Conditional => writeln!(w, "  define {} ?", d.key),
323    }
324}
325
326fn write_app<W: Write>(a: &AppManifest, w: &mut W) -> io::Result<()> {
327    writeln!(w, "app {}", a.name)?;
328    writeln!(w, "  manifest {}", a.source)?;
329    match &a.requires {
330        ManifestDeps::Listed(apps) => {
331            for app in apps {
332                writeln!(w, "  requires {app}")?;
333            }
334            Ok(())
335        }
336        ManifestDeps::Unreadable(ManifestUnreadable::Missing) => {
337            writeln!(w, "  requires_unreadable missing")
338        }
339        ManifestDeps::Unreadable(ManifestUnreadable::UnexpandedVariable(name)) => {
340            writeln!(w, "  requires_unreadable unexpanded_variable {name}")
341        }
342        ManifestDeps::Unreadable(ManifestUnreadable::ParseFailed) => {
343            writeln!(w, "  requires_unreadable parse_failed")
344        }
345    }
346}
347
348fn write_schema<W: Write>(s: &SchemaFile, w: &mut W) -> io::Result<()> {
349    writeln!(w, "schema {}", s.path)?;
350    writeln!(w, "  app {}", s.app)?;
351    write_cuttlefish_keys(w, s.mappings.iter(), s.validators.iter())
352}
353
354fn write_partial<W: Write>(p: &PartialFile, w: &mut W) -> io::Result<()> {
355    writeln!(w, "partial {}", p.path)?;
356    writeln!(w, "  app {}", p.app)?;
357    writeln!(w, "  name {}", p.name)?;
358    write_cuttlefish_keys(w, p.mappings.iter(), p.validators.iter())
359}
360
361fn write_cuttlefish_keys<'a, W: Write>(
362    w: &mut W,
363    mappings: impl Iterator<Item = &'a ConfKey>,
364    validators: impl Iterator<Item = &'a ValidatorName>,
365) -> io::Result<()> {
366    for m in mappings {
367        writeln!(w, "  mapping {m}")?;
368    }
369    for v in validators {
370        writeln!(w, "  validator {v}")?;
371    }
372    Ok(())
373}
374
375fn write_keyword_signature<W: Write>(
376    w: &mut W,
377    keyword: &str,
378    head: &str,
379    body: &str,
380) -> io::Result<()> {
381    if body.is_empty() {
382        writeln!(w, "  {keyword} {head}")?;
383        return Ok(());
384    }
385    let mut lines = body.split('\n');
386    let first = lines.next().unwrap_or("");
387    writeln!(w, "  {keyword} {head} {first}")?;
388    for cont in lines {
389        writeln!(w, "{cont}")?;
390    }
391    Ok(())
392}
393
394fn write_deprecation<W: Write>(d: &Deprecation, w: &mut W) -> io::Result<()> {
395    write!(w, "  ")?;
396    match (&d.function, &d.arity_match, d.module_wide) {
397        (_, _, true) => write!(w, "deprecated module")?,
398        (Some(f), ArityMatch::Exact { arity }, _) => write!(w, "deprecated {f}/{arity}")?,
399        (Some(f), ArityMatch::Any, _) => write!(w, "deprecated {f}/*")?,
400        (None, _, _) => write!(w, "deprecated *")?,
401    }
402    if let Some(since) = &d.since {
403        write!(w, " since {since}")?;
404    }
405    if let Some(rep) = &d.replacement {
406        write!(w, " use {}/{}", rep.function, rep.arity)?;
407    }
408    if let Some(reason) = &d.reason {
409        write!(w, " reason {reason:?}")?;
410    }
411    writeln!(w)?;
412    Ok(())
413}