loadsmith_core/
checksum.rs1use 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 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}