1use std::fmt::Display;
4use std::str::FromStr;
5
6#[non_exhaustive]
12#[derive(Clone, Debug, PartialEq, Eq, Hash)]
13pub enum DigestAlgorithm {
14 Sha256,
16 Sha384,
18 Sha512,
20 Other(Box<str>),
25}
26
27impl AsRef<str> for DigestAlgorithm {
28 fn as_ref(&self) -> &str {
29 match self {
30 DigestAlgorithm::Sha256 => "sha256",
31 DigestAlgorithm::Sha384 => "sha384",
32 DigestAlgorithm::Sha512 => "sha512",
33 DigestAlgorithm::Other(o) => o,
34 }
35 }
36}
37
38impl Display for DigestAlgorithm {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.write_str(self.as_ref())
41 }
42}
43
44impl DigestAlgorithm {
45 pub const fn digest_hexlen(&self) -> Option<u32> {
47 match self {
48 DigestAlgorithm::Sha256 => Some(64),
49 DigestAlgorithm::Sha384 => Some(96),
50 DigestAlgorithm::Sha512 => Some(128),
51 DigestAlgorithm::Other(_) => None,
52 }
53 }
54}
55
56impl From<&str> for DigestAlgorithm {
57 fn from(value: &str) -> Self {
58 match value {
59 "sha256" => Self::Sha256,
60 "sha384" => Self::Sha384,
61 "sha512" => Self::Sha512,
62 o => Self::Other(o.into()),
63 }
64 }
65}
66
67fn char_is_lowercase_ascii_hex(c: char) -> bool {
68 matches!(c, '0'..='9' | 'a'..='f')
69}
70
71fn char_is_algorithm_component(c: char) -> bool {
73 matches!(c, 'a'..='z' | '0'..='9')
74}
75
76fn char_is_encoded(c: char) -> bool {
78 char_is_algorithm_component(c) || matches!(c, 'A'..='Z' | '=' | '_' | '-')
79}
80
81#[derive(Clone, Debug, Eq, PartialEq, Hash)]
99pub struct Digest {
100 algorithm: DigestAlgorithm,
106 value: Box<str>,
107 split: usize,
108}
109
110impl AsRef<str> for Digest {
111 fn as_ref(&self) -> &str {
112 &self.value
113 }
114}
115
116impl Display for Digest {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.write_str(self.as_ref())
119 }
120}
121
122impl<'de> serde::Deserialize<'de> for Digest {
123 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124 where
125 D: serde::Deserializer<'de>,
126 {
127 let s = String::deserialize(deserializer)?;
128 Self::from_str(&s).map_err(serde::de::Error::custom)
129 }
130}
131
132impl serde::ser::Serialize for Digest {
133 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
134 where
135 S: serde::Serializer,
136 {
137 let v = self.to_string();
138 serializer.serialize_str(&v)
139 }
140}
141
142impl Digest {
143 const ALGORITHM_SEPARATOR: &'static [char] = &['+', '.', '_', '-'];
144 pub fn algorithm(&self) -> &DigestAlgorithm {
146 &self.algorithm
147 }
148
149 pub fn digest(&self) -> &str {
154 &self.value[self.split + 1..]
155 }
156}
157
158impl FromStr for Digest {
159 type Err = crate::OciSpecError;
160
161 fn from_str(s: &str) -> Result<Self, Self::Err> {
162 Digest::try_from(s)
163 }
164}
165
166impl TryFrom<String> for Digest {
167 type Error = crate::OciSpecError;
168
169 fn try_from(s: String) -> Result<Self, Self::Error> {
170 let s = s.into_boxed_str();
171 let Some(split) = s.find(':') else {
172 return Err(crate::OciSpecError::Other("missing ':' in digest".into()));
173 };
174 let (algorithm, value) = s.split_at(split);
175 let value = &value[1..];
176
177 let algorithm_parts = algorithm.split(Self::ALGORITHM_SEPARATOR);
179 for part in algorithm_parts {
180 if part.is_empty() {
181 return Err(crate::OciSpecError::Other(
182 "Empty algorithm component".into(),
183 ));
184 }
185 if !part.chars().all(char_is_algorithm_component) {
186 return Err(crate::OciSpecError::Other(format!(
187 "Invalid algorithm component: {part}"
188 )));
189 }
190 }
191
192 if value.is_empty() {
193 return Err(crate::OciSpecError::Other("Empty algorithm value".into()));
194 }
195 if !value.chars().all(char_is_encoded) {
196 return Err(crate::OciSpecError::Other(format!(
197 "Invalid encoded value {value}"
198 )));
199 }
200
201 let algorithm = DigestAlgorithm::from(algorithm);
202 if let Some(expected) = algorithm.digest_hexlen() {
203 let found = value.len();
204 if expected as usize != found {
205 return Err(crate::OciSpecError::Other(format!(
206 "Invalid digest length {found} expected {expected}"
207 )));
208 }
209 let is_all_hex = value.chars().all(char_is_lowercase_ascii_hex);
210 if !is_all_hex {
211 return Err(crate::OciSpecError::Other(format!(
212 "Invalid non-hexadecimal character in digest: {value}"
213 )));
214 }
215 }
216 Ok(Self {
217 algorithm,
218 value: s,
219 split,
220 })
221 }
222}
223
224impl TryFrom<&str> for Digest {
225 type Error = crate::OciSpecError;
226
227 fn try_from(string: &str) -> Result<Self, Self::Error> {
228 TryFrom::try_from(string.to_owned())
229 }
230}
231
232#[derive(Clone, Debug, Eq, PartialEq, Hash)]
234pub struct Sha256Digest {
235 digest: Box<str>,
236}
237
238impl From<Sha256Digest> for Digest {
239 fn from(value: Sha256Digest) -> Self {
240 Self {
241 algorithm: DigestAlgorithm::Sha256,
242 value: format!("sha256:{}", value.digest()).into(),
243 split: 6,
244 }
245 }
246}
247
248impl AsRef<str> for Sha256Digest {
249 fn as_ref(&self) -> &str {
250 self.digest()
251 }
252}
253
254impl Display for Sha256Digest {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 f.write_str(self.digest())
257 }
258}
259
260impl FromStr for Sha256Digest {
261 type Err = crate::OciSpecError;
262
263 fn from_str(digest: &str) -> Result<Self, Self::Err> {
264 let alg = DigestAlgorithm::Sha256;
265 let v = format!("{alg}:{digest}");
266 let d = Digest::from_str(&v)?;
267 match d.algorithm {
268 DigestAlgorithm::Sha256 => Ok(Self {
269 digest: d.digest().into(),
270 }),
271 o => Err(crate::OciSpecError::Other(format!(
272 "Expected algorithm sha256 but found {o}",
273 ))),
274 }
275 }
276}
277
278impl Sha256Digest {
279 pub fn digest(&self) -> &str {
281 &self.digest
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_digest_invalid() {
291 let invalid = [
292 "",
293 "foo",
294 ":",
295 "blah+",
296 "_digest:somevalue",
297 ":blah",
298 "blah:",
299 "FooBar:123abc",
300 "^:foo",
301 "bar^baz:blah",
302 "sha256:123456*78",
303 "sha256:6c3c624b58dbbcd3c0dd82b4z53f04194d1247c6eebdaab7c610cf7d66709b3b", "sha384:x",
305 "sha384:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b",
306 "sha512:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b",
307 ];
308 for case in invalid {
309 assert!(
310 Digest::from_str(case).is_err(),
311 "Should have failed to parse: {case}"
312 )
313 }
314 }
315
316 const VALID_DIGEST_SHA256: &str =
317 "sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b";
318 const VALID_DIGEST_SHA384: &str =
319 "sha384:6c3c624b58dbbcd4d1247c6eebdaab7c610cf7d66709b3b3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b";
320 const VALID_DIGEST_SHA512: &str =
321 "sha512:6c3c624b58dbbcd3c0dd826c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3bb4c53f04194d1247c6eebdaab7c610cf7d66709b3b";
322
323 #[test]
324 fn test_digest_valid() {
325 let cases = ["foo:bar", "xxhash:42"];
326 for case in cases {
327 Digest::from_str(case).unwrap();
328 }
329
330 let d = Digest::try_from("multihash+base58:QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8")
331 .unwrap();
332 assert_eq!(d.algorithm(), &DigestAlgorithm::from("multihash+base58"));
333 assert_eq!(d.digest(), "QmRZxt2b1FVZPNqd8hsiykDL3TdBDeTSPX9Kv46HmX4Gx8");
334 }
335
336 #[test]
337 fn test_sha256_valid() {
338 let expected_value = VALID_DIGEST_SHA256.split_once(':').unwrap().1;
339 let d = Digest::from_str(VALID_DIGEST_SHA256).unwrap();
340 assert_eq!(d.algorithm(), &DigestAlgorithm::Sha256);
341 assert_eq!(d.digest(), expected_value);
342 let base_digest = Digest::from(d.clone());
343 assert_eq!(base_digest.digest(), expected_value);
344 }
345
346 #[test]
347 fn test_sha384_valid() {
348 let expected_value = VALID_DIGEST_SHA384.split_once(':').unwrap().1;
349 let d = Digest::from_str(VALID_DIGEST_SHA384).unwrap();
350 assert_eq!(d.algorithm(), &DigestAlgorithm::Sha384);
351 assert_eq!(d.digest(), expected_value);
352 assert_eq!(d.as_ref(), VALID_DIGEST_SHA384);
354 let base_digest = Digest::from(d.clone());
355 assert_eq!(base_digest.digest(), expected_value);
356 }
357
358 #[test]
359 fn test_sha512_valid() {
360 let expected_value = VALID_DIGEST_SHA512.split_once(':').unwrap().1;
361 let d = Digest::from_str(VALID_DIGEST_SHA512).unwrap();
362 assert_eq!(d.algorithm(), &DigestAlgorithm::Sha512);
363 assert_eq!(d.digest(), expected_value);
364 let base_digest = Digest::from(d.clone());
365 assert_eq!(base_digest.digest(), expected_value);
366 }
367
368 #[test]
369 fn test_sha256() {
370 let digest = VALID_DIGEST_SHA256.split_once(':').unwrap().1;
371 let v = Sha256Digest::from_str(digest).unwrap();
372 assert_eq!(v.digest(), digest);
373 }
374}