cuttlefish_host/bundle.rs
1//! Packaging a [`crate::pipeline::Checked`] pipeline into a `.cfbundle`.
2//!
3//! Container: `catalog::BUNDLE_MAGIC` (`b"CFBD"`), then `manifest_len: u64`
4//! little-endian, then the JSON manifest, then the concatenated stage
5//! bytes, with offsets recorded per node in the manifest. `catalog.rs`'s
6//! `read_bundle_signature` is the read side of this exact format; the magic
7//! bytes and header length are shared constants (`BUNDLE_MAGIC`,
8//! `BUNDLE_HEADER_LEN`) rather than independently-defined literals, so the
9//! two sides cannot silently drift apart.
10//!
11//! No timestamps or absolute paths anywhere in the output: that's what
12//! makes two builds of the same spec against the same catalog state
13//! byte-identical, so re-cataloging a rebuild never produces a spurious new
14//! hash for "the same" pipeline.
15
16use crate::catalog::{ArtifactKind, BUNDLE_HEADER_LEN, BUNDLE_MAGIC};
17use crate::pipeline::Checked;
18use serde::Serialize;
19
20#[derive(Serialize)]
21struct Manifest {
22 manifest_version: u32,
23 signature: String,
24 nodes: Vec<Node>,
25}
26
27#[derive(Serialize)]
28struct Node {
29 name: String,
30 kind: ArtifactKind,
31 resolved: Option<String>,
32 signature: String,
33 /// Where this node's bytes start, relative to the *first byte after the
34 /// manifest* — not an absolute offset into the `.cfbundle` file. A
35 /// reader must compute `BUNDLE_HEADER_LEN + manifest_len + offset` to
36 /// find the real file position. Nodes must always be located this way,
37 /// never by scanning for `BUNDLE_MAGIC`: a node whose bytes are
38 /// themselves a nested `.cfbundle` starts with that exact magic, so
39 /// scanning for it would misidentify the inner bundle's own header as a
40 /// fresh top-level one.
41 offset: u64,
42 len: u64,
43}
44
45/// Serialize a checked pipeline into `.cfbundle` bytes.
46pub fn build(checked: &Checked) -> Vec<u8> {
47 let mut offset = 0u64;
48 let nodes: Vec<Node> = checked
49 .stages()
50 .iter()
51 .map(|s| {
52 let len = s.module_bytes.len() as u64;
53 let node = Node {
54 name: s.name.clone(),
55 kind: s.kind,
56 resolved: s.resolved.clone(),
57 signature: s.signature.to_string(),
58 offset,
59 len,
60 };
61 offset += len;
62 node
63 })
64 .collect();
65
66 let manifest = Manifest {
67 manifest_version: 1,
68 signature: format!("{} -> {}", checked.input(), checked.output()),
69 nodes,
70 };
71 let manifest_bytes = serde_json::to_vec(&manifest)
72 .expect("Manifest always serializes: no non-finite floats, no non-string map keys");
73
74 let mut out = Vec::with_capacity(BUNDLE_HEADER_LEN + manifest_bytes.len() + offset as usize);
75 out.extend_from_slice(BUNDLE_MAGIC);
76 out.extend_from_slice(&(manifest_bytes.len() as u64).to_le_bytes());
77 out.extend_from_slice(&manifest_bytes);
78 for stage in checked.stages() {
79 out.extend_from_slice(&stage.module_bytes);
80 }
81 out
82}