containerregistry_image/
digest.rs1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7use sha2::{Digest as Sha2Digest, Sha256, Sha384, Sha512};
8
9use crate::Error;
10
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
15pub struct Digest {
16 algorithm: Algorithm,
17 hex: String,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum Algorithm {
23 Sha256,
24 Sha384,
25 Sha512,
26}
27
28impl Digest {
29 pub fn new(algorithm: Algorithm, hex: impl Into<String>) -> Result<Self, Error> {
33 let hex = hex.into().to_ascii_lowercase();
34 Self::validate_hex(&algorithm, &hex)?;
35 Ok(Self { algorithm, hex })
36 }
37
38 pub fn sha256(data: &[u8]) -> Self {
40 Self::compute(Algorithm::Sha256, data)
41 }
42
43 pub fn sha384(data: &[u8]) -> Self {
45 Self::compute(Algorithm::Sha384, data)
46 }
47
48 pub fn sha512(data: &[u8]) -> Self {
50 Self::compute(Algorithm::Sha512, data)
51 }
52
53 pub fn compute(algorithm: Algorithm, data: &[u8]) -> Self {
55 let hex = match algorithm {
56 Algorithm::Sha256 => {
57 let mut hasher = Sha256::new();
58 hasher.update(data);
59 hex::encode(hasher.finalize())
60 }
61 Algorithm::Sha384 => {
62 let mut hasher = Sha384::new();
63 hasher.update(data);
64 hex::encode(hasher.finalize())
65 }
66 Algorithm::Sha512 => {
67 let mut hasher = Sha512::new();
68 hasher.update(data);
69 hex::encode(hasher.finalize())
70 }
71 };
72 Self { algorithm, hex }
73 }
74
75 pub fn algorithm(&self) -> Algorithm {
77 self.algorithm
78 }
79
80 pub fn hex(&self) -> &str {
82 &self.hex
83 }
84
85 fn validate_hex(algorithm: &Algorithm, hex: &str) -> Result<(), Error> {
86 let expected_len = match algorithm {
87 Algorithm::Sha256 => 64,
88 Algorithm::Sha384 => 96,
89 Algorithm::Sha512 => 128,
90 };
91
92 if hex.len() != expected_len {
93 return Err(Error::InvalidDigest(format!(
94 "expected {} hex characters, got {}",
95 expected_len,
96 hex.len()
97 )));
98 }
99
100 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
101 return Err(Error::InvalidDigest(
102 "digest contains non-hexadecimal characters".to_string(),
103 ));
104 }
105
106 Ok(())
107 }
108}
109
110impl fmt::Display for Digest {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 let alg = match self.algorithm {
113 Algorithm::Sha256 => "sha256",
114 Algorithm::Sha384 => "sha384",
115 Algorithm::Sha512 => "sha512",
116 };
117 write!(f, "{}:{}", alg, self.hex)
118 }
119}
120
121impl FromStr for Digest {
122 type Err = Error;
123
124 fn from_str(s: &str) -> Result<Self, Self::Err> {
125 let (alg, hex) = s
126 .split_once(':')
127 .ok_or_else(|| Error::InvalidDigest("missing ':' separator".to_string()))?;
128
129 let algorithm = match alg.to_ascii_lowercase().as_str() {
130 "sha256" => Algorithm::Sha256,
131 "sha384" => Algorithm::Sha384,
132 "sha512" => Algorithm::Sha512,
133 _ => return Err(Error::UnsupportedAlgorithm(alg.to_string())),
134 };
135
136 Self::new(algorithm, hex)
137 }
138}
139
140impl Serialize for Digest {
141 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
142 where
143 S: serde::Serializer,
144 {
145 serializer.serialize_str(&self.to_string())
146 }
147}
148
149impl<'de> Deserialize<'de> for Digest {
150 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
151 where
152 D: serde::Deserializer<'de>,
153 {
154 let s = String::deserialize(deserializer)?;
155 s.parse().map_err(serde::de::Error::custom)
156 }
157}
158
159impl fmt::Display for Algorithm {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 match self {
162 Algorithm::Sha256 => write!(f, "sha256"),
163 Algorithm::Sha384 => write!(f, "sha384"),
164 Algorithm::Sha512 => write!(f, "sha512"),
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_sha256_computation() {
175 let digest = Digest::sha256(b"hello world");
176 assert_eq!(digest.algorithm(), Algorithm::Sha256);
177 assert_eq!(
178 digest.hex(),
179 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
180 );
181 }
182
183 #[test]
184 fn test_digest_parse() {
185 let s = "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
186 let digest: Digest = s.parse().unwrap();
187 assert_eq!(digest.to_string(), s);
188 }
189
190 #[test]
191 fn test_digest_parse_is_case_insensitive() {
192 let s = "SHA256:B94D27B9934D3E08A52E52D7DA7DABFAC484EFE37A5380EE9088F7ACE2EFCDE9";
193 let digest: Digest = s.parse().unwrap();
194 assert_eq!(
195 digest.to_string(),
196 "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
197 );
198 }
199
200 #[test]
201 fn test_digest_invalid_algorithm() {
202 let s = "md5:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
203 let result: Result<Digest, _> = s.parse();
204 assert!(result.is_err());
205 }
206
207 #[test]
208 fn test_digest_sha384_sha512_lengths() {
209 let data = b"hello";
210 let sha384 = Digest::sha384(data);
211 let sha512 = Digest::sha512(data);
212
213 assert_eq!(sha384.hex().len(), 96);
214 assert_eq!(sha512.hex().len(), 128);
215 assert_eq!(sha384.algorithm(), Algorithm::Sha384);
216 assert_eq!(sha512.algorithm(), Algorithm::Sha512);
217 }
218
219 #[test]
220 fn test_digest_invalid_hex_length() {
221 let s = "sha256:abcd";
222 let result: Result<Digest, _> = s.parse();
223 assert!(result.is_err());
224 }
225
226 #[test]
227 fn test_digest_serde_roundtrip() {
228 let digest = Digest::sha256(b"test");
229 let json = serde_json::to_string(&digest).unwrap();
230 let parsed: Digest = serde_json::from_str(&json).unwrap();
231 assert_eq!(digest, parsed);
232 }
233
234 #[test]
235 fn test_digest_hex_normalized_to_lowercase() {
236 let upper = "sha256:B94D27B9934D3E08A52E52D7DA7DABFAC484EFE37A5380EE9088F7ACE2EFCDE9";
238 let lower = "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
239
240 let digest_upper: Digest = upper.parse().unwrap();
241 let digest_lower: Digest = lower.parse().unwrap();
242
243 assert_eq!(digest_upper, digest_lower);
244 assert_eq!(digest_upper.hex(), digest_lower.hex());
245 assert_eq!(digest_upper.to_string(), lower);
246 }
247
248 #[test]
249 fn test_digest_mixed_case_normalized() {
250 let mixed = "sha256:B94d27B9934D3e08a52E52d7DA7dabFAC484efe37a5380EE9088f7ACE2efcDE9";
251 let digest: Digest = mixed.parse().unwrap();
252
253 assert!(
255 digest
256 .hex()
257 .chars()
258 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
259 );
260 }
261}