1use std::fmt;
12use std::str::FromStr;
13
14use serde::{Deserialize, Serialize};
15
16pub const DIGEST_HEX_LEN: usize = 64;
18
19const HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";
20
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
26#[serde(try_from = "String", into = "String")]
27pub struct Digest(String);
28
29impl Digest {
30 pub fn parse(s: impl Into<String>) -> Result<Self, DigestError> {
32 let s = s.into();
33 if s.len() != DIGEST_HEX_LEN {
34 return Err(DigestError::Length(s.len()));
35 }
36 if !s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
37 return Err(DigestError::NotLowercaseHex);
38 }
39 Ok(Self(s))
40 }
41
42 pub fn from_sha256(bytes: [u8; 32]) -> Self {
45 let mut hex = String::with_capacity(DIGEST_HEX_LEN);
46 for b in bytes {
47 hex.push(HEX_DIGITS[(b >> 4) as usize] as char);
48 hex.push(HEX_DIGITS[(b & 0x0f) as usize] as char);
49 }
50 Self(hex)
51 }
52
53 pub fn as_str(&self) -> &str {
54 &self.0
55 }
56}
57
58impl fmt::Display for Digest {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str(&self.0)
61 }
62}
63
64impl AsRef<str> for Digest {
65 fn as_ref(&self) -> &str {
66 &self.0
67 }
68}
69
70impl From<Digest> for String {
71 fn from(d: Digest) -> Self {
72 d.0
73 }
74}
75
76impl TryFrom<String> for Digest {
77 type Error = DigestError;
78
79 fn try_from(s: String) -> Result<Self, Self::Error> {
80 Digest::parse(s)
81 }
82}
83
84impl FromStr for Digest {
85 type Err = DigestError;
86
87 fn from_str(s: &str) -> Result<Self, Self::Err> {
88 Digest::parse(s)
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum DigestError {
96 Length(usize),
98 NotLowercaseHex,
101}
102
103impl fmt::Display for DigestError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 DigestError::Length(len) => {
107 write!(f, "expected {DIGEST_HEX_LEN} hex characters, got {len}")
108 }
109 DigestError::NotLowercaseHex => {
110 f.write_str("expected lowercase hex characters (0-9, a-f)")
111 }
112 }
113 }
114}
115
116impl std::error::Error for DigestError {}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 const VALID: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
123
124 #[test]
125 fn parse_accepts_a_lowercase_hex_sha256() {
126 let digest = Digest::parse(VALID).unwrap();
127 assert_eq!(digest.as_str(), VALID);
128 assert_eq!(digest.to_string(), VALID);
129 }
130
131 #[test]
132 fn parse_rejects_wrong_length() {
133 assert_eq!(Digest::parse("abc"), Err(DigestError::Length(3)));
134 assert_eq!(
135 Digest::parse(format!("{VALID}0")),
136 Err(DigestError::Length(DIGEST_HEX_LEN + 1))
137 );
138 assert_eq!(
139 Digest::parse(&VALID[..DIGEST_HEX_LEN - 1]),
140 Err(DigestError::Length(DIGEST_HEX_LEN - 1))
141 );
142 }
143
144 #[test]
145 fn parse_rejects_uppercase() {
146 assert_eq!(
147 Digest::parse(VALID.to_uppercase()),
148 Err(DigestError::NotLowercaseHex)
149 );
150 }
151
152 #[test]
153 fn parse_rejects_non_hex() {
154 let non_hex = format!("z{}", &VALID[1..]);
155 assert_eq!(Digest::parse(non_hex), Err(DigestError::NotLowercaseHex));
156 }
157
158 #[test]
159 fn from_sha256_hex_encodes_all_nibbles() {
160 let mut bytes = [0u8; 32];
161 bytes[0] = 0x0f;
162 bytes[1] = 0xa0;
163 bytes[31] = 0xff;
164
165 let digest = Digest::from_sha256(bytes);
166
167 assert_eq!(digest.as_str().len(), DIGEST_HEX_LEN);
168 assert!(digest.as_str().starts_with("0fa0"));
169 assert!(digest.as_str().ends_with("ff"));
170 assert_eq!(Digest::parse(digest.as_str()).unwrap(), digest);
171 }
172
173 #[test]
174 fn serializes_as_a_plain_string() {
175 let digest = Digest::parse(VALID).unwrap();
176 let json = serde_json::to_string(&digest).unwrap();
177 assert_eq!(json, format!("\"{VALID}\""));
178 assert_eq!(serde_json::from_str::<Digest>(&json).unwrap(), digest);
179 }
180
181 #[test]
182 fn deserialization_rejects_an_invalid_digest() {
183 let err = serde_json::from_str::<Digest>("\"NOPE\"").unwrap_err();
184 assert!(err.to_string().contains("64 hex characters"), "{err}");
185 }
186
187 #[test]
188 fn from_str_matches_parse() {
189 assert_eq!(
190 VALID.parse::<Digest>().unwrap(),
191 Digest::parse(VALID).unwrap()
192 );
193 assert!("".parse::<Digest>().is_err());
194 }
195}