1use std::io::Read;
14use std::path::{Path, PathBuf};
15
16use crate::client::Client;
17use crate::error::{Error, Result};
18use crate::manifest::Manifest;
19use crate::paths::{plugin_install_dir, plugins_dir};
20
21pub trait Downloader {
28 fn download(&self, url: &str) -> Result<Vec<u8>>;
29}
30
31#[derive(Default, Clone)]
33pub struct MemoryDownloader {
34 artifacts: std::collections::HashMap<String, Vec<u8>>,
35}
36
37impl MemoryDownloader {
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn with(mut self, url: impl Into<String>, body: impl Into<Vec<u8>>) -> Self {
43 self.artifacts.insert(url.into(), body.into());
44 self
45 }
46}
47
48impl Downloader for MemoryDownloader {
49 fn download(&self, url: &str) -> Result<Vec<u8>> {
50 self.artifacts
51 .get(url)
52 .cloned()
53 .ok_or_else(|| Error::Download {
54 message: format!("no artifact registered for {url}"),
55 })
56 }
57}
58
59pub struct NoopDownloader;
62
63impl Downloader for NoopDownloader {
64 fn download(&self, url: &str) -> Result<Vec<u8>> {
65 Err(Error::Download {
66 message: format!("network download not yet wired (confium-net pending); URL: {url}"),
67 })
68 }
69}
70
71#[derive(Clone)]
77pub struct HttpDownloader {
78 agent: ureq::Agent,
79}
80
81impl HttpDownloader {
82 pub fn new() -> Self {
85 Self::with_agent(&format!("confium/{}", env!("CARGO_PKG_VERSION")))
86 }
87
88 pub fn with_agent(user_agent: &str) -> Self {
90 let config = ureq::config::Config::builder()
91 .user_agent(user_agent)
92 .timeout_global(Some(std::time::Duration::from_secs(30)))
93 .build();
94 Self {
95 agent: ureq::Agent::new_with_config(config),
96 }
97 }
98}
99
100impl Default for HttpDownloader {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl Downloader for HttpDownloader {
107 fn download(&self, url: &str) -> Result<Vec<u8>> {
108 let mut response = self.agent.get(url).call().map_err(|e| Error::Download {
109 message: format!("HTTP request to {url} failed: {e}"),
110 })?;
111 if !response.status().is_success() {
114 return Err(Error::Download {
115 message: format!("HTTP request to {url} returned {}", response.status()),
116 });
117 }
118
119 let mut bytes = Vec::with_capacity(1024 * 64);
120 response
121 .body_mut()
122 .as_reader()
123 .read_to_end(&mut bytes)
124 .map_err(|e| Error::Download {
125 message: format!("reading body from {url} failed: {e}"),
126 })?;
127
128 Ok(bytes)
129 }
130}
131
132#[derive(Debug, Clone)]
135pub struct InstalledRecord {
136 pub name: String,
137 pub version: String,
138 pub artifact_path: PathBuf,
139 pub manifest: Manifest,
140}
141
142pub fn install<F: crate::client::Fetcher, D: Downloader>(
148 client: &Client<F>,
149 downloader: &D,
150 override_home: Option<&PathBuf>,
151 name: &str,
152 version: Option<&str>,
153) -> Result<InstalledRecord> {
154 let manifest = client.resolve(name, version)?;
155 install_manifest(downloader, override_home, manifest)
156}
157
158pub fn install_manifest<D: Downloader>(
161 downloader: &D,
162 override_home: Option<&PathBuf>,
163 manifest: Manifest,
164) -> Result<InstalledRecord> {
165 let name = manifest.plugin.name.clone();
166 let version = manifest.plugin.version.clone();
167
168 let bytes = downloader.download(&manifest.artifact.url)?;
169 verify_sha256(&name, &version, &bytes, &manifest.artifact.sha256)?;
170
171 let target = plugin_install_dir(override_home, &name, &version)?;
172 ensure_parent(&target)?;
173
174 write_bytes(&target, &bytes, "artifact")?;
175
176 let manifest_path = manifest_path(&target);
179 let manifest_toml =
180 toml::to_string_pretty(&manifest).map_err(|e| Error::TomlSerialize { source: e })?;
181 write_str(&manifest_path, &manifest_toml, "manifest")?;
182
183 Ok(InstalledRecord {
184 name,
185 version,
186 artifact_path: target,
187 manifest,
188 })
189}
190
191pub fn list_installed(override_home: Option<&PathBuf>) -> Result<Vec<InstalledRecord>> {
194 let dir = plugins_dir(override_home)?;
195 if !dir.exists() {
196 return Ok(Vec::new());
197 }
198 let mut records = Vec::new();
199 let entries = std::fs::read_dir(&dir)
200 .map_err(|e| Error::io(e, format!("failed to read {}", dir.display())))?;
201 for entry in entries {
202 let entry = entry.map_err(|e| Error::io(e, "directory iteration error"))?;
203 let path = entry.path();
204 if path.extension().and_then(|e| e.to_str()) != Some("so") {
205 continue;
206 }
207 let manifest_path = manifest_path(&path);
208 if !manifest_path.exists() {
209 continue;
210 }
211 if let Ok(record) = read_record(&path) {
212 records.push(record);
213 }
214 }
215 records.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.version.cmp(&b.version)));
216 Ok(records)
217}
218
219pub fn read_installed(
221 override_home: Option<&PathBuf>,
222 name: &str,
223 version: &str,
224) -> Result<InstalledRecord> {
225 let path = plugin_install_dir(override_home, name, version)?;
226 if !path.exists() {
227 return Err(Error::NotInstalled {
228 name: name.to_string(),
229 });
230 }
231 read_record(&path)
232}
233
234pub fn remove(override_home: Option<&PathBuf>, name: &str) -> Result<()> {
238 let installed = list_installed(override_home)?;
239 let mut removed = 0;
240 for record in installed {
241 if record.name != name {
242 continue;
243 }
244 let manifest_path = manifest_path(&record.artifact_path);
245 let _ = std::fs::remove_file(&manifest_path);
246 std::fs::remove_file(&record.artifact_path).map_err(|e| {
247 Error::io(
248 e,
249 format!("failed to remove {}", record.artifact_path.display()),
250 )
251 })?;
252 removed += 1;
253 }
254 if removed == 0 {
255 return Err(Error::NotInstalled {
256 name: name.to_string(),
257 });
258 }
259 Ok(())
260}
261
262fn read_record(artifact_path: &Path) -> Result<InstalledRecord> {
263 let manifest_path = manifest_path(artifact_path);
264 let body = std::fs::read_to_string(&manifest_path)
265 .map_err(|e| Error::io(e, format!("failed to read {}", manifest_path.display())))?;
266 let manifest: Manifest = toml::from_str(&body).map_err(|e| Error::TomlParse {
267 path: manifest_path.display().to_string(),
268 source: e,
269 })?;
270 let (name, version) = parse_artifact_name(artifact_path).ok_or_else(|| {
271 Error::io(
272 std::io::Error::new(std::io::ErrorKind::InvalidData, "bad artifact filename"),
273 format!(
274 "artifact filename {} is not <name>-<version>.so",
275 artifact_path.display()
276 ),
277 )
278 })?;
279 Ok(InstalledRecord {
280 name,
281 version,
282 artifact_path: artifact_path.to_path_buf(),
283 manifest,
284 })
285}
286
287fn parse_artifact_name(path: &Path) -> Option<(String, String)> {
288 let stem = path.file_stem()?.to_str()?;
289 let idx = stem.rfind('-')?;
290 let name = stem[..idx].to_string();
291 let version = stem[idx + 1..].to_string();
292 Some((name, version))
293}
294
295fn manifest_path(artifact: &Path) -> PathBuf {
296 let mut p = artifact.to_path_buf();
297 p.set_extension("manifest");
298 p
299}
300
301fn ensure_parent(path: &Path) -> Result<()> {
302 if let Some(parent) = path.parent() {
303 std::fs::create_dir_all(parent)
304 .map_err(|e| Error::io(e, format!("failed to create {}", parent.display())))?;
305 }
306 Ok(())
307}
308
309fn write_bytes(path: &Path, bytes: &[u8], what: &str) -> Result<()> {
310 std::fs::write(path, bytes)
311 .map_err(|e| Error::io(e, format!("failed to write {what} to {}", path.display())))
312}
313
314fn write_str(path: &Path, body: &str, what: &str) -> Result<()> {
315 std::fs::write(path, body)
316 .map_err(|e| Error::io(e, format!("failed to write {what} to {}", path.display())))
317}
318
319fn verify_sha256(name: &str, version: &str, bytes: &[u8], expected: &str) -> Result<()> {
320 use std::fmt::Write;
321 let digest = sha256(bytes);
322 let mut got = String::with_capacity(64);
323 for b in digest {
324 let _ = write!(&mut got, "{:02x}", b);
325 }
326 if got.eq_ignore_ascii_case(expected.trim()) {
327 return Ok(());
328 }
329 Err(Error::HashMismatch {
330 name: name.to_string(),
331 version: version.to_string(),
332 expected: expected.to_string(),
333 actual: got,
334 })
335}
336
337fn sha256(data: &[u8]) -> [u8; 32] {
346 const K: [u32; 64] = [
347 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
348 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
349 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
350 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
351 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
352 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
353 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
354 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
355 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
356 0xc67178f2,
357 ];
358
359 let mut h: [u32; 8] = [
360 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
361 0x5be0cd19,
362 ];
363
364 let bit_len = (data.len() as u64).wrapping_mul(8);
365 let mut padded = data.to_vec();
366 padded.push(0x80);
367 while padded.len() % 64 != 56 {
368 padded.push(0);
369 }
370 padded.extend_from_slice(&bit_len.to_be_bytes());
371
372 for chunk in padded.chunks_exact(64) {
373 let mut w = [0u32; 64];
374 for (i, word) in chunk.chunks_exact(4).enumerate() {
375 w[i] = u32::from_be_bytes([word[0], word[1], word[2], word[3]]);
376 }
377 for i in 16..64 {
378 let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
379 let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
380 w[i] = w[i - 16]
381 .wrapping_add(s0)
382 .wrapping_add(w[i - 7])
383 .wrapping_add(s1);
384 }
385 let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh) =
386 (h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]);
387 for i in 0..64 {
388 let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
389 let ch = (e & f) ^ ((!e) & g);
390 let t1 = hh
391 .wrapping_add(s1)
392 .wrapping_add(ch)
393 .wrapping_add(K[i])
394 .wrapping_add(w[i]);
395 let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
396 let maj = (a & b) ^ (a & c) ^ (b & c);
397 let t2 = s0.wrapping_add(maj);
398 hh = g;
399 g = f;
400 f = e;
401 e = d.wrapping_add(t1);
402 d = c;
403 c = b;
404 b = a;
405 a = t1.wrapping_add(t2);
406 }
407 h[0] = h[0].wrapping_add(a);
408 h[1] = h[1].wrapping_add(b);
409 h[2] = h[2].wrapping_add(c);
410 h[3] = h[3].wrapping_add(d);
411 h[4] = h[4].wrapping_add(e);
412 h[5] = h[5].wrapping_add(f);
413 h[6] = h[6].wrapping_add(g);
414 h[7] = h[7].wrapping_add(hh);
415 }
416
417 let mut out = [0u8; 32];
418 for (i, word) in h.iter().enumerate() {
419 out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
420 }
421 out
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use std::path::PathBuf;
428
429 fn empty_body_hash() -> String {
430 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string()
432 }
433
434 fn manifest_for(name: &str, version: &str, sha: &str) -> Manifest {
435 let manifest_toml = format!(
436 r#"
437[plugin]
438name = "{name}"
439version = "{version}"
440publisher = "ribose"
441
442[artifact]
443url = "https://example.test/{name}.so"
444size = 0
445sha256 = "{sha}"
446"#
447 );
448 toml::from_str(&manifest_toml).unwrap()
449 }
450
451 fn hex(bytes: &[u8]) -> String {
452 let mut s = String::with_capacity(bytes.len() * 2);
453 use std::fmt::Write;
454 for b in bytes {
455 let _ = write!(&mut s, "{:02x}", b);
456 }
457 s
458 }
459
460 #[test]
461 fn sha256_of_empty_matches_known_digest() {
462 assert_eq!(
463 hex(&sha256(b"")),
464 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
465 );
466 }
467
468 #[test]
469 fn sha256_of_abc() {
470 assert_eq!(
471 hex(&sha256(b"abc")),
472 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
473 );
474 }
475
476 #[test]
477 fn install_writes_artifact_and_manifest() {
478 let tmp = tempfile::tempdir().unwrap();
479 let home = PathBuf::from(tmp.path());
480 let manifest = manifest_for("botan", "3.2.0", &empty_body_hash());
481 let downloader = MemoryDownloader::new().with("https://example.test/botan.so", Vec::new());
482
483 let record = install_manifest(&downloader, Some(&home), manifest).unwrap();
484 assert!(record.artifact_path.exists());
485 assert!(manifest_path(&record.artifact_path).exists());
486
487 let installed = list_installed(Some(&home)).unwrap();
488 assert_eq!(installed.len(), 1);
489 assert_eq!(installed[0].name, "botan");
490 assert_eq!(installed[0].version, "3.2.0");
491 }
492
493 #[test]
494 fn install_rejects_hash_mismatch() {
495 let tmp = tempfile::tempdir().unwrap();
496 let home = PathBuf::from(tmp.path());
497 let manifest = manifest_for("botan", "3.2.0", "deadbeef");
498 let downloader =
499 MemoryDownloader::new().with("https://example.test/botan.so", vec![1, 2, 3]);
500 let err = install_manifest(&downloader, Some(&home), manifest).unwrap_err();
501 assert!(matches!(err, Error::HashMismatch { .. }));
502 }
503
504 #[test]
505 fn remove_deletes_artifact_and_manifest() {
506 let tmp = tempfile::tempdir().unwrap();
507 let home = PathBuf::from(tmp.path());
508 let manifest = manifest_for("botan", "3.2.0", &empty_body_hash());
509 let downloader = MemoryDownloader::new().with("https://example.test/botan.so", Vec::new());
510 install_manifest(&downloader, Some(&home), manifest).unwrap();
511
512 remove(Some(&home), "botan").unwrap();
513 assert!(list_installed(Some(&home)).unwrap().is_empty());
514 }
515
516 #[test]
517 fn remove_unknown_errors() {
518 let tmp = tempfile::tempdir().unwrap();
519 let home = PathBuf::from(tmp.path());
520 let err = remove(Some(&home), "ghost").unwrap_err();
521 assert!(matches!(err, Error::NotInstalled { .. }));
522 }
523
524 #[test]
525 fn list_on_missing_dir_is_empty() {
526 let tmp = tempfile::tempdir().unwrap();
527 let home = PathBuf::from(tmp.path());
528 let installed = list_installed(Some(&home)).unwrap();
529 assert!(installed.is_empty());
530 }
531
532 #[test]
533 fn noop_downloader_errors() {
534 let err = NoopDownloader
535 .download("https://example.test/x.so")
536 .unwrap_err();
537 assert!(matches!(err, Error::Download { .. }));
538 }
539}