harmont_cli/plugin/
install.rs1use std::path::PathBuf;
4
5use anyhow::{Context, Result, bail};
6use sha2::{Digest, Sha256};
7
8use super::host::LoadedPlugin;
9use super::paths;
10
11pub async fn install(source: &str, pin: Option<&str>) -> Result<PathBuf> {
25 let bytes = if source.starts_with("https://") {
26 let pin = pin.context("--pin <sha256> is required for HTTPS sources")?;
27 let body = reqwest::get(source)
28 .await
29 .with_context(|| format!("GET {source}"))?
30 .error_for_status()?
31 .bytes()
32 .await
33 .context("read response body")?;
34 verify_pin(&body, pin)?;
35 body.to_vec()
36 } else if source.starts_with("http://") {
37 bail!("plain http:// is not allowed; use https:// or a local file path");
38 } else {
39 let path = PathBuf::from(source);
40 if !path.is_file() {
41 bail!("no file at {}", path.display());
42 }
43 let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
44 if let Some(pin) = pin {
45 verify_pin(&bytes, pin)?;
46 }
47 bytes
48 };
49
50 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
54 let plugin =
55 LoadedPlugin::from_bytes(leaked, 1).context("validate plugin before installing")?;
56 let install_dir = paths::install_dir().context("resolve install dir")?;
57 std::fs::create_dir_all(&install_dir)
58 .with_context(|| format!("create {}", install_dir.display()))?;
59 let target = install_dir.join(format!("{}.wasm", plugin.manifest.name));
60 std::fs::write(&target, &bytes).with_context(|| format!("write {}", target.display()))?;
61 Ok(target)
62}
63
64fn verify_pin(bytes: &[u8], expected_hex: &str) -> Result<()> {
65 let mut h = Sha256::new();
66 h.update(bytes);
67 let got = h.finalize();
68 let got_hex = hex::encode(got);
69 if !got_hex.eq_ignore_ascii_case(expected_hex.trim()) {
70 bail!(
71 "SHA-256 mismatch: expected {expected_hex}, downloaded {got_hex}\n\
72 fix: re-fetch the source or correct the --pin value"
73 );
74 }
75 Ok(())
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use sha2::{Digest, Sha256};
82
83 #[test]
84 fn pin_verification_round_trip() {
85 let body = b"hello plugin";
86 let mut h = Sha256::new();
87 h.update(body);
88 let hex_digest = hex::encode(h.finalize());
89 assert!(verify_pin(body, &hex_digest).is_ok());
90 assert!(verify_pin(body, "deadbeef").is_err());
91 }
92}