Skip to main content

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}