1use std::path::Path;
10use std::process::Command;
11
12use snafu::{ResultExt, Snafu};
13
14#[derive(Snafu, Debug)]
15pub enum SignError {
16 #[snafu(display("gpg not found on PATH; install GnuPG to sign releases"))]
17 GpgMissing { source: std::io::Error },
18
19 #[snafu(display(
20 "gpg exited with status {exit_code} while signing {}\nstderr: {stderr}",
21 manifest_path.display()
22 ))]
23 GpgFailed {
24 exit_code: i32,
25 stderr: String,
26 manifest_path: Box<Path>,
27 },
28}
29
30pub type Result<T> = std::result::Result<T, SignError>;
31
32pub fn sign_manifest(manifest_path: &Path, signing_key: &str, dry_run: bool) -> Result<Vec<u8>> {
40 if dry_run {
41 return Ok(
42 b"-----BEGIN PGP SIGNATURE-----\n[dry-run placeholder]\n-----END PGP SIGNATURE-----\n"
43 .to_vec(),
44 );
45 }
46
47 let output = Command::new("gpg")
48 .args([
49 "--detach-sign",
50 "--armor",
51 "--batch",
52 "--yes",
53 "--local-user",
54 signing_key,
55 "--output",
56 "-", ])
58 .arg(manifest_path)
59 .output()
60 .context(GpgMissingSnafu)?;
61
62 if !output.status.success() {
63 let manifest_path_boxed: Box<Path> = Box::from(manifest_path);
64 return GpgFailedSnafu {
65 exit_code: output.status.code().unwrap_or(-1),
66 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
67 manifest_path: manifest_path_boxed,
68 }
69 .fail();
70 }
71 Ok(output.stdout)
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn dry_run_returns_placeholder_without_gpg() {
80 let got = sign_manifest(std::path::Path::new("/nonexistent"), "key", true).unwrap();
81 let s = String::from_utf8(got).unwrap();
82 assert!(s.contains("BEGIN PGP SIGNATURE"));
83 assert!(s.contains("dry-run placeholder"));
84 }
85}