Skip to main content

release_kit/commands/
payload.rs

1//! `rk payload`: what this binary carries, provably, and what another
2//! release carries, through the seam.
3//!
4//! The version alone does not identify a payload — two locally built
5//! binaries can share a Cargo version while embedding different bytes —
6//! so the report carries a digest per artifact and one aggregate over the
7//! ordered list, computed at runtime over the bytes. A landing record, a
8//! bug report, or a comparison between two installs can then name the
9//! payload it actually saw. `--release <version>` answers the same
10//! document for a release this binary does not carry, which is the first
11//! proof that the engine reads a bundle it was not compiled with.
12
13use crate::cli::payload::PayloadArgs;
14use crate::error::RkError;
15use crate::output::Output;
16use crate::release::{CrateReleaseSource, EmbeddedReleaseSource, ReleaseManifest, ReleaseSource};
17
18/// The machine form of the payload report, `rk.payload/1`: the release
19/// manifest itself.
20pub type Report = ReleaseManifest;
21
22/// The report over the embedded payload.
23#[must_use]
24pub fn report() -> Report {
25    EmbeddedReleaseSource::manifest_ref().clone()
26}
27
28/// Print the payload report.
29///
30/// # Errors
31///
32/// Returns [`RkError::Other`] when the report cannot serialize, which is a
33/// defect in this binary rather than anything a caller can correct, and
34/// the crate source's refusals under `--release`.
35pub fn run(args: &PayloadArgs) -> Result<(), RkError> {
36    let out = Output::new(args.json);
37    let (report, source_line) = match args.release.as_deref() {
38        None => (report(), None),
39        Some(selector) => {
40            let source = CrateReleaseSource::new(selector)?;
41            let manifest = source.manifest()?;
42            let resolved = source.resolve()?;
43            let line = format!(
44                "source crates {} ({}, {})",
45                resolved.version,
46                if resolved.index_fetched {
47                    "index fetched"
48                } else {
49                    "index cached"
50                },
51                if resolved.archive_fetched {
52                    "archive fetched and verified"
53                } else {
54                    "archive cached"
55                }
56            );
57            (manifest, Some(line))
58        }
59    };
60    out.result_line(format!("release-kit {}", report.release_kit_version));
61    if let Some(line) = source_line {
62        out.result_line(line);
63    }
64    out.result_line(format!("payload sha256 {}", report.payload_sha256));
65    for root in crate::payload_roots::PAYLOAD_ROOTS {
66        let count = report
67            .artifacts
68            .iter()
69            .filter(|a| a.path == root || a.path.starts_with(&format!("{root}/")))
70            .count();
71        let noun = if count == 1 { "file" } else { "files" };
72        out.result_line(format!("{root}: {count} {noun}"));
73    }
74    out.emit(&report)
75}
76
77#[cfg(test)]
78mod tests {
79    use super::{Report, report};
80    use crate::digest::Digest;
81    use crate::release::{Artifact, aggregate};
82
83    /// The complete `rk.payload/1` shape, held by snapshot against fixture
84    /// values, beside the live test that checks the real digests.
85    #[test]
86    fn the_payload_report_schema_snapshot_holds() {
87        let fixture = Report::new(
88            "0.0.0".into(),
89            1,
90            vec![Artifact {
91                path: "versions.toml".into(),
92                sha256: Digest::of(b""),
93            }],
94        );
95        assert_eq!(
96            fixture.payload_sha256,
97            aggregate(&fixture.artifacts),
98            "the aggregate is computed, never supplied"
99        );
100        let mut fixture = fixture;
101        fixture.payload_sha256 = Digest::of(b"");
102        assert_eq!(
103            serde_json::to_string(&fixture).expect("a report serializes"),
104            r#"{"release_kit_version":"0.0.0","payload_schema":1,"payload_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","artifacts":[{"path":"versions.toml","sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}"#
105        );
106    }
107
108    #[test]
109    fn the_report_names_the_cargo_version_and_every_artifact() {
110        let report = report();
111        assert_eq!(report.release_kit_version, env!("CARGO_PKG_VERSION"));
112        assert_eq!(report.payload_schema, crate::release::PAYLOAD_SCHEMA);
113        assert!(!report.artifacts.is_empty());
114        assert_eq!(report.payload_sha256, aggregate(&report.artifacts));
115    }
116
117    /// The aggregate must see renames and reorders, not only content.
118    #[test]
119    fn the_aggregate_covers_paths_and_order() {
120        let mut artifacts = report().artifacts;
121        let original = aggregate(&artifacts);
122        artifacts.reverse();
123        assert_ne!(original, aggregate(&artifacts));
124    }
125}