1use 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
67pub 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
130pub 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}