1use std::fmt::Display;
2use std::io::Read;
3use std::path::Path;
4
5use anyhow::Result;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash)]
9#[serde(rename_all = "lowercase")]
10pub enum HashAlg {
11 Blake3,
12 Md5,
13}
14
15#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
16pub struct Hashes {
17 pub blake3: String,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub md5: Option<String>,
20}
21
22impl Hashes {
23 pub fn compute_from_path(path: &Path, extra: &[HashAlg]) -> Result<(Self, u64)> {
27 let mut file = fs_err::File::open(path)?;
28 let mut size: u64 = 0;
29 let mut buf = [0u8; 64 * 1024];
30
31 let mut blake3_hasher = blake3::Hasher::new();
32 let mut md5_context = if extra.contains(&HashAlg::Md5) {
33 Some(md5::Context::new())
34 } else {
35 None
36 };
37
38 loop {
39 let n = file.read(&mut buf)?;
40 if n == 0 {
41 break;
42 }
43 size += n as u64;
44 blake3_hasher.update(&buf[..n]);
45 if let Some(c) = &mut md5_context {
46 c.consume(&buf[..n]);
47 }
48 }
49
50 Ok((
51 Hashes {
52 blake3: blake3_hasher.finalize().to_string(),
53 md5: md5_context.map(|c| format!("{:x}", c.finalize())),
54 },
55 size,
56 ))
57 }
58
59 pub fn get_blake3(&self) -> &str {
60 self.blake3.as_str()
61 }
62
63 pub fn get_by_alg(&self, alg: HashAlg) -> Option<&str> {
64 match alg {
65 HashAlg::Blake3 => Some(&self.blake3),
66 HashAlg::Md5 => self.md5.as_deref(),
67 }
68 }
69}
70
71impl Display for Hashes {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match &self.md5 {
74 Some(m) => write!(f, "Hashes(md5={}, blake3={})", m, self.blake3),
75 None => write!(f, "Hashes(blake3={})", self.blake3),
76 }
77 }
78}