provenant/models/
digest.rs1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5macro_rules! define_digest {
6 (
7 $(#[$meta:meta])*
8 $name:ident, $byte_len:literal
9 ) => {
10 $(#[$meta])*
11 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12 pub struct $name([u8; $byte_len]);
13
14 impl $name {
15 #[allow(dead_code)]
16 pub const EMPTY: Self = Self([0u8; $byte_len]);
17
18 #[allow(dead_code)]
19 pub const fn from_bytes(bytes: [u8; $byte_len]) -> Self {
20 Self(bytes)
21 }
22
23 pub fn from_hex(s: &str) -> Result<Self, ParseDigestError> {
24 let bytes = hex::decode(s).map_err(|_| ParseDigestError::InvalidHex)?;
25 let array: [u8; $byte_len] = bytes
26 .try_into()
27 .map_err(|_: Vec<u8>| ParseDigestError::InvalidLength)?;
28 Ok(Self(array))
29 }
30
31 #[allow(dead_code)]
32 pub fn as_bytes(&self) -> &[u8; $byte_len] {
33 &self.0
34 }
35
36 pub fn as_hex(&self) -> String {
37 hex::encode(self.0)
38 }
39 }
40
41 impl fmt::Debug for $name {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 f.debug_tuple(stringify!($name))
44 .field(&self.as_hex())
45 .finish()
46 }
47 }
48
49 impl fmt::Display for $name {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.write_str(&self.as_hex())
52 }
53 }
54
55 impl Serialize for $name {
56 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
57 where
58 S: Serializer,
59 {
60 serializer.serialize_str(&self.as_hex())
61 }
62 }
63
64 impl<'de> Deserialize<'de> for $name {
65 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66 where
67 D: Deserializer<'de>,
68 {
69 let s = String::deserialize(deserializer)?;
70 Self::from_hex(&s).map_err(serde::de::Error::custom)
71 }
72 }
73 };
74}
75
76define_digest!(
77 Sha1Digest,
79 20
80);
81
82define_digest!(
83 Md5Digest,
85 16
86);
87
88define_digest!(
89 Sha256Digest,
91 32
92);
93
94define_digest!(
95 Sha512Digest,
97 64
98);
99
100define_digest!(
101 GitSha1,
103 20
104);
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum ParseDigestError {
108 InvalidHex,
109 InvalidLength,
110}
111
112impl std::fmt::Display for ParseDigestError {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match self {
115 ParseDigestError::InvalidHex => write!(f, "invalid hex encoding"),
116 ParseDigestError::InvalidLength => write!(f, "invalid digest length"),
117 }
118 }
119}
120
121impl std::error::Error for ParseDigestError {}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn sha1_roundtrip() {
129 let hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
130 let digest = Sha1Digest::from_hex(hex).unwrap();
131 assert_eq!(digest.as_hex(), hex);
132 }
133
134 #[test]
135 fn sha256_roundtrip() {
136 let hex = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
137 let digest = Sha256Digest::from_hex(hex).unwrap();
138 assert_eq!(digest.as_hex(), hex);
139 }
140
141 #[test]
142 fn sha512_roundtrip() {
143 let hex = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e";
144 let digest = Sha512Digest::from_hex(hex).unwrap();
145 assert_eq!(digest.as_hex(), hex);
146 }
147
148 #[test]
149 fn md5_roundtrip() {
150 let hex = "d41d8cd98f00b204e9800998ecf8427e";
151 let digest = Md5Digest::from_hex(hex).unwrap();
152 assert_eq!(digest.as_hex(), hex);
153 }
154
155 #[test]
156 fn git_sha1_roundtrip() {
157 let hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
158 let digest = GitSha1::from_hex(hex).unwrap();
159 assert_eq!(digest.as_hex(), hex);
160 }
161
162 #[test]
163 fn invalid_hex_rejected() {
164 assert!(Sha1Digest::from_hex("not-hex!").is_err());
165 }
166
167 #[test]
168 fn invalid_length_rejected() {
169 assert!(Sha1Digest::from_hex("abcd").is_err());
170 assert!(Sha256Digest::from_hex("da39a3ee5e6b4b0d3255bfef95601890afd80709").is_err());
171 }
172
173 #[test]
174 fn serde_roundtrip() {
175 let hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
176 let digest = Sha1Digest::from_hex(hex).unwrap();
177 let json = serde_json::to_string(&digest).unwrap();
178 assert_eq!(json, format!("\"{}\"", hex));
179 let back: Sha1Digest = serde_json::from_str(&json).unwrap();
180 assert_eq!(back, digest);
181 }
182
183 #[test]
184 fn optional_serde_roundtrip() {
185 let some: Option<Sha1Digest> =
186 Some(Sha1Digest::from_hex("da39a3ee5e6b4b0d3255bfef95601890afd80709").unwrap());
187 let json = serde_json::to_string(&some).unwrap();
188 let back: Option<Sha1Digest> = serde_json::from_str(&json).unwrap();
189 assert_eq!(back, some);
190
191 let none: Option<Sha1Digest> = None;
192 let json = serde_json::to_string(&none).unwrap();
193 let back: Option<Sha1Digest> = serde_json::from_str(&json).unwrap();
194 assert_eq!(back, none);
195 }
196
197 #[test]
198 fn empty_constant_is_all_zeros() {
199 assert_eq!(Sha1Digest::EMPTY.0, [0u8; 20]);
200 assert_eq!(Md5Digest::EMPTY.0, [0u8; 16]);
201 assert_eq!(Sha256Digest::EMPTY.0, [0u8; 32]);
202 assert_eq!(Sha512Digest::EMPTY.0, [0u8; 64]);
203 assert_eq!(GitSha1::EMPTY.0, [0u8; 20]);
204 }
205
206 #[test]
207 fn display_shows_hex() {
208 let hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
209 let digest = Sha1Digest::from_hex(hex).unwrap();
210 assert_eq!(format!("{}", digest), hex);
211 }
212}