confium_publish/
output.rs1use std::fs;
17use std::io::{self, Read};
18use std::path::{Path, PathBuf};
19
20use sha2::{Digest, Sha256};
21use snafu::{ResultExt, Snafu};
22
23#[derive(Snafu, Debug)]
24pub enum OutputError {
25 #[snafu(display("failed to read artifact at '{}'", path.display()))]
26 ReadArtifact { path: Box<Path>, source: io::Error },
27
28 #[snafu(display("failed to create output directory '{}'", path.display()))]
29 Mkdir { path: Box<Path>, source: io::Error },
30
31 #[snafu(display("failed to write '{}'", path.display()))]
32 WriteFile { path: Box<Path>, source: io::Error },
33}
34
35pub type Result<T> = std::result::Result<T, OutputError>;
36
37pub fn sha256_of_file(path: &Path) -> Result<String> {
40 let path_boxed: Box<Path> = Box::from(path);
41 let mut file = fs::File::open(path_boxed.as_ref()).context(ReadArtifactSnafu {
42 path: path_boxed.clone(),
43 })?;
44 let mut hasher = Sha256::new();
45 let mut buf = [0u8; 64 * 1024];
46 loop {
47 let n = file.read(&mut buf).context(ReadArtifactSnafu {
48 path: path_boxed.clone(),
49 })?;
50 if n == 0 {
51 break;
52 }
53 hasher.update(&buf[..n]);
54 }
55 Ok(hex_encode(&hasher.finalize()))
56}
57
58fn hex_encode(bytes: &[u8]) -> String {
59 const HEX: &[u8; 16] = b"0123456789abcdef";
60 let mut out = String::with_capacity(bytes.len() * 2);
61 for &b in bytes {
62 out.push(HEX[(b >> 4) as usize] as char);
63 out.push(HEX[(b & 0x0f) as usize] as char);
64 }
65 out
66}
67
68pub struct OutputPaths {
70 pub dir: PathBuf,
71 pub manifest: PathBuf,
72 pub artifact_sha256: PathBuf,
73 pub sigs_dir: PathBuf,
74 pub signature: PathBuf,
75}
76
77pub fn paths_for(root: &Path, plugin: &str, version: &str, publisher: &str) -> OutputPaths {
79 let dir = root.join(plugin).join(version);
80 let sigs_dir = dir.join("sigs");
81 OutputPaths {
82 manifest: dir.join("manifest.toml"),
83 artifact_sha256: dir.join("artifact.sha256"),
84 signature: sigs_dir.join(format!("{publisher}.asc")),
85 dir,
86 sigs_dir,
87 }
88}
89
90pub fn write_tree(
94 paths: &OutputPaths,
95 manifest_toml: &str,
96 artifact_path: &Path,
97 sha256_hex: &str,
98 signature: &[u8],
99 dry_run: bool,
100) -> Result<Vec<u8>> {
101 let manifest_bytes = manifest_toml.as_bytes();
102
103 if dry_run {
104 return Ok(Vec::new());
105 }
106
107 let dir_boxed: Box<Path> = Box::from(paths.dir.as_path());
108 fs::create_dir_all(dir_boxed.as_ref()).context(MkdirSnafu {
109 path: dir_boxed.clone(),
110 })?;
111 let sigs_boxed: Box<Path> = Box::from(paths.sigs_dir.as_path());
112 fs::create_dir_all(sigs_boxed.as_ref()).context(MkdirSnafu {
113 path: sigs_boxed.clone(),
114 })?;
115
116 let manifest_boxed: Box<Path> = Box::from(paths.manifest.as_path());
117 fs::write(manifest_boxed.as_ref(), manifest_bytes).context(WriteFileSnafu {
118 path: manifest_boxed.clone(),
119 })?;
120
121 let basename = artifact_path
122 .file_name()
123 .map(|n| n.to_string_lossy().into_owned())
124 .unwrap_or_else(|| "artifact".to_string());
125 let sha_line = format!("{sha256_hex} {basename}\n");
126 let sha_boxed: Box<Path> = Box::from(paths.artifact_sha256.as_path());
127 fs::write(sha_boxed.as_ref(), sha_line).context(WriteFileSnafu {
128 path: sha_boxed.clone(),
129 })?;
130
131 let sig_boxed: Box<Path> = Box::from(paths.signature.as_path());
132 fs::write(sig_boxed.as_ref(), signature).context(WriteFileSnafu {
133 path: sig_boxed.clone(),
134 })?;
135
136 Ok(manifest_bytes.to_vec())
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn sha256_of_empty_file_matches_known_digest() {
145 let tmp = std::env::temp_dir().join("cfm_publish_test_empty");
146 fs::write(&tmp, b"").unwrap();
147 let got = sha256_of_file(&tmp).unwrap();
148 let _ = fs::remove_file(&tmp);
149 assert_eq!(
151 got,
152 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
153 );
154 }
155
156 #[test]
157 fn sha256_of_known_bytes_matches_shasum() {
158 let tmp = std::env::temp_dir().join("cfm_publish_test_abc");
160 fs::write(&tmp, b"abc").unwrap();
161 let got = sha256_of_file(&tmp).unwrap();
162 let _ = fs::remove_file(&tmp);
163 assert_eq!(
164 got,
165 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
166 );
167 }
168
169 #[test]
170 fn write_tree_creates_all_files() {
171 let root = std::env::temp_dir().join("cfm_publish_tree_test");
172 let _ = fs::remove_dir_all(&root);
173 let paths = paths_for(&root, "plug", "1.0.0", "pub");
174 let artifact = std::env::temp_dir().join("cfm_publish_tree_artifact");
175 fs::write(&artifact, b"payload").unwrap();
176 write_tree(
177 &paths,
178 "[plugin]\nname=\"x\"\n",
179 &artifact,
180 "deadbeef",
181 b"SIG",
182 false,
183 )
184 .unwrap();
185 assert!(paths.dir.is_dir());
186 assert!(paths.manifest.is_file());
187 assert!(paths.artifact_sha256.is_file());
188 assert!(paths.signature.is_file());
189 let sha_content = fs::read_to_string(&paths.artifact_sha256).unwrap();
190 assert!(sha_content.contains("deadbeef"));
191 let _ = fs::remove_dir_all(&root);
192 let _ = fs::remove_file(&artifact);
193 }
194
195 #[test]
196 fn dry_run_writes_nothing() {
197 let root = std::env::temp_dir().join("cfm_publish_dryrun_test");
198 let _ = fs::remove_dir_all(&root);
199 let paths = paths_for(&root, "plug", "1.0.0", "pub");
200 let artifact = std::env::temp_dir().join("cfm_publish_dryrun_art");
201 fs::write(&artifact, b"x").unwrap();
202 let bytes = write_tree(&paths, "[plugin]\n", &artifact, "aa", b"s", true).unwrap();
203 assert!(bytes.is_empty());
204 assert!(!paths.dir.exists());
205 let _ = fs::remove_file(&artifact);
206 }
207}