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