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