1use std::fmt;
10
11const HEX: [u8; 16] = *b"0123456789abcdef";
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
16pub struct Digest(String);
17
18impl Digest {
19 #[must_use]
21 pub fn of(bytes: &[u8]) -> Self {
22 use sha2::Digest as _;
23 let mut hex = String::with_capacity(64);
24 for byte in sha2::Sha256::digest(bytes) {
25 hex.push(char::from(HEX[usize::from(byte >> 4)]));
26 hex.push(char::from(HEX[usize::from(byte & 0x0f)]));
27 }
28 Self(hex)
29 }
30
31 #[must_use]
33 pub fn parse(text: &str) -> Option<Self> {
34 let hex = text.len() == 64
35 && text
36 .bytes()
37 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
38 hex.then(|| Self(text.to_owned()))
39 }
40}
41
42impl fmt::Display for Digest {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.write_str(&self.0)
45 }
46}
47
48impl serde::Serialize for Digest {
49 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
50 serializer.serialize_str(&self.0)
51 }
52}
53
54impl<'de> serde::Deserialize<'de> for Digest {
55 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
56 let text = String::deserialize(deserializer)?;
57 Self::parse(&text).ok_or_else(|| {
58 serde::de::Error::custom(format!("'{text}' is not a 64-character hex sha256"))
59 })
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 #![allow(clippy::expect_used)]
66
67 use super::Digest;
68
69 #[test]
70 fn a_digest_round_trips_through_its_hex_form() {
71 let empty = Digest::of(b"");
74 assert_eq!(
75 empty.to_string(),
76 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
77 );
78 assert_eq!(Digest::parse(&empty.to_string()), Some(empty));
79 }
80
81 #[test]
82 fn a_malformed_digest_is_rejected() {
83 for text in ["", "abc", &"g".repeat(64), &"A".repeat(64), &"a".repeat(63)] {
84 assert!(Digest::parse(text).is_none(), "'{text}' parsed as a digest");
85 }
86 }
87}