Skip to main content

loadsmith_core/
checksum.rs

1use std::{fmt::Display, io::Read, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4use sha2::Digest;
5
6use crate::{Error, Result};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(into = "String", try_from = "String")]
10pub enum Checksum {
11    Blake3(blake3::Hash),
12    Sha256([u8; 32]),
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
16#[serde(rename_all = "lowercase")]
17pub enum ChecksumAlgorithm {
18    Blake3,
19    Sha256,
20}
21
22impl Checksum {
23    pub fn blake3(hash: blake3::Hash) -> Self {
24        Self::from(hash)
25    }
26
27    pub fn sha256(hash: [u8; 32]) -> Self {
28        Self::Sha256(hash)
29    }
30
31    pub fn compute<R>(mut reader: R, algorithm: ChecksumAlgorithm) -> Result<Self>
32    where
33        R: Read,
34    {
35        match algorithm {
36            ChecksumAlgorithm::Blake3 => {
37                let mut hasher = blake3::Hasher::new();
38                std::io::copy(&mut reader, &mut hasher)?;
39                Ok(Checksum::Blake3(hasher.finalize()))
40            }
41            ChecksumAlgorithm::Sha256 => {
42                let mut hasher = digest_io::IoWrapper(sha2::Sha256::new());
43                std::io::copy(&mut reader, &mut hasher)?;
44
45                let array = hasher.0.finalize().into();
46
47                Ok(Checksum::Sha256(array))
48            }
49        }
50    }
51
52    pub fn algorithm(&self) -> ChecksumAlgorithm {
53        match self {
54            Checksum::Blake3(_) => ChecksumAlgorithm::Blake3,
55            Checksum::Sha256(_) => ChecksumAlgorithm::Sha256,
56        }
57    }
58
59    pub fn without_algorithm(&self) -> WithoutAlgorithm<'_> {
60        WithoutAlgorithm(self)
61    }
62
63    pub fn from_value_str(value: &str, algorithm: ChecksumAlgorithm) -> Result<Self> {
64        match algorithm {
65            ChecksumAlgorithm::Blake3 => {
66                let hash = blake3::Hash::from_hex(value).map_err(Error::InvalidBlake3Hex)?;
67                Ok(Checksum::Blake3(hash))
68            }
69            ChecksumAlgorithm::Sha256 => {
70                let mut hash = [0u8; 32];
71                hex::decode_to_slice(value, &mut hash).map_err(Error::InvalidSha256Hex)?;
72                Ok(Checksum::Sha256(hash))
73            }
74        }
75    }
76}
77
78impl From<blake3::Hash> for Checksum {
79    fn from(hash: blake3::Hash) -> Self {
80        Checksum::Blake3(hash)
81    }
82}
83
84impl Display for Checksum {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        write!(f, "{}:{}", self.algorithm(), self.without_algorithm())
87    }
88}
89
90impl FromStr for Checksum {
91    type Err = Error;
92
93    fn from_str(s: &str) -> Result<Self> {
94        let (algorithm, value) = s.split_once(':').ok_or(Error::InvalidChecksumFormat)?;
95        Checksum::from_value_str(value, algorithm.parse()?)
96    }
97}
98
99impl From<Checksum> for String {
100    fn from(checksum: Checksum) -> Self {
101        checksum.to_string()
102    }
103}
104
105impl TryFrom<String> for Checksum {
106    type Error = Error;
107
108    fn try_from(s: String) -> Result<Self> {
109        s.parse()
110    }
111}
112
113impl Display for ChecksumAlgorithm {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        let algorithm = match self {
116            ChecksumAlgorithm::Blake3 => "blake3",
117            ChecksumAlgorithm::Sha256 => "sha256",
118        };
119
120        write!(f, "{algorithm}")
121    }
122}
123
124impl FromStr for ChecksumAlgorithm {
125    type Err = Error;
126
127    fn from_str(s: &str) -> Result<Self> {
128        match s {
129            "blake3" => Ok(ChecksumAlgorithm::Blake3),
130            "sha256" => Ok(ChecksumAlgorithm::Sha256),
131            algo => Err(Error::UnknownAlgorithm(algo.to_string())),
132        }
133    }
134}
135
136pub struct WithoutAlgorithm<'a>(&'a Checksum);
137
138impl Display for WithoutAlgorithm<'_> {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self.0 {
141            Checksum::Blake3(hash) => write!(f, "{}", hash.to_hex()),
142            Checksum::Sha256(hash) => write!(f, "{}", hex::encode(hash)),
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    // blake3 hash of "Hello, world!"
152    const BLAKE3_HELLO_WORLD: &str =
153        "ede5c0b10f2ec4979c69b52f61e42ff5b413519ce09be0f14d098dcfe5f6f98d";
154
155    #[test]
156    fn checksum_display() {
157        let hash = blake3::hash(b"Hello, world!");
158        let checksum = Checksum::Blake3(hash);
159        assert_eq!(checksum.to_string(), format!("blake3:{BLAKE3_HELLO_WORLD}"));
160    }
161
162    #[test]
163    fn checksum_from_str() {
164        let checksum_str = format!("blake3:{BLAKE3_HELLO_WORLD}");
165        let checksum = Checksum::from_str(&checksum_str).unwrap();
166
167        let hash = blake3::hash(b"Hello, world!");
168        assert_eq!(checksum, Checksum::Blake3(hash));
169    }
170
171    #[test]
172    fn checksum_from_str_invalid_algorithm() {
173        let checksum_str = "unknown:abcdef";
174        let result = Checksum::from_str(&checksum_str);
175        assert!(result.is_err());
176    }
177
178    #[test]
179    fn checksum_from_str_invalid_format() {
180        let checksum_str = "blake3abcdef";
181        let result = Checksum::from_str(&checksum_str);
182        assert!(result.is_err());
183    }
184
185    #[test]
186    fn checksum_serde_json() {
187        let hash = blake3::hash(b"Hello, world!");
188        let checksum = Checksum::Blake3(hash);
189
190        let json = serde_json::to_string(&checksum).unwrap();
191        assert_eq!(json, format!("\"blake3:{BLAKE3_HELLO_WORLD}\""));
192
193        let deserialized: Checksum = serde_json::from_str(&json).unwrap();
194        assert_eq!(deserialized, checksum);
195    }
196}