Skip to main content

loadsmith_core/
checksum.rs

1use std::{
2    fmt::Display,
3    fs::File,
4    io::{BufReader, Read, Write},
5    path::Path,
6    str::FromStr,
7};
8
9use serde::{Deserialize, Serialize};
10use sha2::Digest;
11use walkdir::WalkDir;
12
13use crate::{Error, Result};
14
15/// A checksum value computed with a recognised algorithm (BLAKE3 or SHA-256).
16///
17/// ```rust
18/// # use loadsmith_core::{Checksum, ChecksumAlgorithm};
19/// # use std::io::Cursor;
20/// let data = Cursor::new(b"BepInExPack_Valheim-5.4.2202.zip contents");
21/// let ck = Checksum::compute(data, ChecksumAlgorithm::Blake3).unwrap();
22/// assert_eq!(ck.algorithm().to_string(), "blake3");
23///
24/// let as_str = ck.to_string();
25/// let parsed: Checksum = as_str.parse().unwrap();
26/// assert_eq!(ck, parsed);
27///
28/// let ck = Checksum::from_value_str(
29///     "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
30///     ChecksumAlgorithm::Sha256,
31/// ).unwrap();
32/// assert_eq!(ck.algorithm().to_string(), "sha256");
33/// ```
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(into = "String", try_from = "String")]
36pub enum Checksum {
37    Blake3(blake3::Hash),
38    Sha256([u8; 32]),
39}
40
41/// The set of checksum algorithms this crate can handle.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
43#[serde(rename_all = "lowercase")]
44pub enum ChecksumAlgorithm {
45    Blake3,
46    Sha256,
47}
48
49impl Checksum {
50    /// Create a `Checksum::Blake3` from an already-computed `blake3::Hash`.
51    pub fn blake3(hash: blake3::Hash) -> Self {
52        Self::from(hash)
53    }
54
55    /// Create a `Checksum::Sha256` from a raw 32-byte array.
56    pub fn sha256(hash: [u8; 32]) -> Self {
57        Self::Sha256(hash)
58    }
59
60    /// Compute a checksum by reading a byte stream with the chosen algorithm.
61    ///
62    /// ```rust
63    /// # use loadsmith_core::{Checksum, ChecksumAlgorithm};
64    /// # use std::io::Cursor;
65    /// let data = Cursor::new(b"some mod archive data");
66    /// let ck = Checksum::compute(data, ChecksumAlgorithm::Sha256).unwrap();
67    /// assert_eq!(ck.algorithm().to_string(), "sha256");
68    /// ```
69    pub fn compute<R>(mut reader: R, algorithm: ChecksumAlgorithm) -> std::io::Result<Self>
70    where
71        R: Read,
72    {
73        match algorithm {
74            ChecksumAlgorithm::Blake3 => {
75                let mut hasher = blake3::Hasher::new();
76                std::io::copy(&mut reader, &mut hasher)?;
77
78                Ok(Checksum::Blake3(hasher.finalize()))
79            }
80            ChecksumAlgorithm::Sha256 => {
81                let mut hasher = digest_io::IoWrapper(sha2::Sha256::new());
82                std::io::copy(&mut reader, &mut hasher)?;
83
84                let array = hasher.0.finalize().into();
85
86                Ok(Checksum::Sha256(array))
87            }
88        }
89    }
90
91    pub fn compute_from_path(
92        path: impl AsRef<Path>,
93        algorithm: ChecksumAlgorithm,
94    ) -> std::io::Result<Self> {
95        fn hash_dir<T: Write>(mut hasher: T, path: &Path) -> std::io::Result<T> {
96            WalkDir::new(path)
97                .sort_by_file_name()
98                .into_iter()
99                .filter_map(|entry| entry.ok())
100                .try_for_each(|entry| {
101                    let file_name = entry.file_name().as_encoded_bytes();
102                    hasher.write_all(file_name)?;
103
104                    if entry.file_type().is_file() {
105                        let mut file = File::open(entry.path()).map(BufReader::new)?;
106                        std::io::copy(&mut file, &mut hasher)?;
107                    }
108
109                    Ok::<(), std::io::Error>(())
110                })?;
111
112            Ok(hasher)
113        }
114
115        let path = path.as_ref();
116        if path.is_dir() {
117            match algorithm {
118                ChecksumAlgorithm::Blake3 => {
119                    let hasher = blake3::Hasher::new();
120
121                    let hasher = hash_dir(hasher, path.as_ref())?;
122
123                    Ok(Checksum::Blake3(hasher.finalize()))
124                }
125                ChecksumAlgorithm::Sha256 => {
126                    let hasher = digest_io::IoWrapper(sha2::Sha256::new());
127
128                    let hasher = hash_dir(hasher, path.as_ref())?;
129
130                    let array = hasher.0.finalize().into();
131                    Ok(Checksum::Sha256(array))
132                }
133            }
134        } else {
135            let file = File::open(path).map(BufReader::new)?;
136            Self::compute(file, algorithm)
137        }
138    }
139
140    /// Return which algorithm this checksum was produced with.
141    pub fn algorithm(&self) -> ChecksumAlgorithm {
142        match self {
143            Checksum::Blake3(_) => ChecksumAlgorithm::Blake3,
144            Checksum::Sha256(_) => ChecksumAlgorithm::Sha256,
145        }
146    }
147
148    /// Return a wrapper that displays only the hex portion (no algorithm prefix).
149    pub fn without_algorithm(&self) -> WithoutAlgorithm<'_> {
150        WithoutAlgorithm(self)
151    }
152
153    /// Parse a checksum from a raw hex string with an explicit algorithm.
154    ///
155    /// Useful when the algorithm and value are stored separately, or when
156    /// you have already split `"<algo>:<hex>"` yourself.
157    ///
158    /// ```rust
159    /// # use loadsmith_core::{Checksum, ChecksumAlgorithm};
160    /// let ck = Checksum::from_value_str(
161    ///     "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
162    ///     ChecksumAlgorithm::Sha256,
163    /// ).unwrap();
164    /// assert_eq!(ck.algorithm().to_string(), "sha256");
165    /// ```
166    pub fn from_value_str(value: &str, algorithm: ChecksumAlgorithm) -> Result<Self> {
167        match algorithm {
168            ChecksumAlgorithm::Blake3 => {
169                let hash = blake3::Hash::from_hex(value).map_err(Error::InvalidBlake3Hex)?;
170                Ok(Checksum::Blake3(hash))
171            }
172            ChecksumAlgorithm::Sha256 => {
173                let mut hash = [0u8; 32];
174                hex::decode_to_slice(value, &mut hash).map_err(Error::InvalidSha256Hex)?;
175                Ok(Checksum::Sha256(hash))
176            }
177        }
178    }
179}
180
181impl From<blake3::Hash> for Checksum {
182    fn from(hash: blake3::Hash) -> Self {
183        Checksum::Blake3(hash)
184    }
185}
186
187impl Display for Checksum {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        write!(f, "{}:{}", self.algorithm(), self.without_algorithm())
190    }
191}
192
193impl FromStr for Checksum {
194    type Err = Error;
195
196    fn from_str(s: &str) -> Result<Self> {
197        let (algorithm, value) = s.split_once(':').ok_or(Error::InvalidChecksumFormat)?;
198        Checksum::from_value_str(value, algorithm.parse()?)
199    }
200}
201
202impl From<Checksum> for String {
203    fn from(checksum: Checksum) -> Self {
204        checksum.to_string()
205    }
206}
207
208impl TryFrom<String> for Checksum {
209    type Error = Error;
210
211    fn try_from(s: String) -> Result<Self> {
212        s.parse()
213    }
214}
215
216impl Display for ChecksumAlgorithm {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        let algorithm = match self {
219            ChecksumAlgorithm::Blake3 => "blake3",
220            ChecksumAlgorithm::Sha256 => "sha256",
221        };
222
223        write!(f, "{algorithm}")
224    }
225}
226
227impl FromStr for ChecksumAlgorithm {
228    type Err = Error;
229
230    fn from_str(s: &str) -> Result<Self> {
231        match s {
232            "blake3" => Ok(ChecksumAlgorithm::Blake3),
233            "sha256" => Ok(ChecksumAlgorithm::Sha256),
234            algo => Err(Error::UnknownAlgorithm(algo.to_string())),
235        }
236    }
237}
238
239/// The hex-only portion of a [`Checksum`] (no algorithm prefix).
240///
241/// Created via [`Checksum::without_algorithm`].
242pub struct WithoutAlgorithm<'a>(&'a Checksum);
243
244impl Display for WithoutAlgorithm<'_> {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        match self.0 {
247            Checksum::Blake3(hash) => write!(f, "{}", hash.to_hex()),
248            Checksum::Sha256(hash) => write!(f, "{}", hex::encode(hash)),
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    // blake3 hash of "Hello, world!"
258    const BLAKE3_HELLO_WORLD: &str =
259        "ede5c0b10f2ec4979c69b52f61e42ff5b413519ce09be0f14d098dcfe5f6f98d";
260
261    #[test]
262    fn checksum_display() {
263        let hash = blake3::hash(b"Hello, world!");
264        let checksum = Checksum::Blake3(hash);
265        assert_eq!(checksum.to_string(), format!("blake3:{BLAKE3_HELLO_WORLD}"));
266    }
267
268    #[test]
269    fn checksum_from_str() {
270        let checksum_str = format!("blake3:{BLAKE3_HELLO_WORLD}");
271        let checksum = Checksum::from_str(&checksum_str).unwrap();
272
273        let hash = blake3::hash(b"Hello, world!");
274        assert_eq!(checksum, Checksum::Blake3(hash));
275    }
276
277    #[test]
278    fn checksum_from_str_invalid_algorithm() {
279        let checksum_str = "unknown:abcdef";
280        let result = Checksum::from_str(&checksum_str);
281        assert!(result.is_err());
282    }
283
284    #[test]
285    fn checksum_from_str_invalid_format() {
286        let checksum_str = "blake3abcdef";
287        let result = Checksum::from_str(&checksum_str);
288        assert!(result.is_err());
289    }
290
291    #[test]
292    fn checksum_serde_json() {
293        let hash = blake3::hash(b"Hello, world!");
294        let checksum = Checksum::Blake3(hash);
295
296        let json = serde_json::to_string(&checksum).unwrap();
297        assert_eq!(json, format!("\"blake3:{BLAKE3_HELLO_WORLD}\""));
298
299        let deserialized: Checksum = serde_json::from_str(&json).unwrap();
300        assert_eq!(deserialized, checksum);
301    }
302}