Skip to main content

containerregistry_image/
digest.rs

1//! Content-addressable digest type.
2
3use 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/// A content-addressable digest in the format `algorithm:hex`.
12///
13/// Currently only SHA256 is supported as per OCI specification.
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
15pub struct Digest {
16    algorithm: Algorithm,
17    hex: String,
18}
19
20/// Supported digest algorithms.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum Algorithm {
23    Sha256,
24    Sha384,
25    Sha512,
26}
27
28impl Digest {
29    /// Creates a new digest from algorithm and hex string.
30    ///
31    /// The hex string is normalized to lowercase per OCI specification.
32    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    /// Computes the SHA256 digest of the given data.
39    pub fn sha256(data: &[u8]) -> Self {
40        Self::compute(Algorithm::Sha256, data)
41    }
42
43    /// Computes the SHA384 digest of the given data.
44    pub fn sha384(data: &[u8]) -> Self {
45        Self::compute(Algorithm::Sha384, data)
46    }
47
48    /// Computes the SHA512 digest of the given data.
49    pub fn sha512(data: &[u8]) -> Self {
50        Self::compute(Algorithm::Sha512, data)
51    }
52
53    /// Computes a digest using the requested algorithm.
54    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    /// Returns the algorithm component.
76    pub fn algorithm(&self) -> Algorithm {
77        self.algorithm
78    }
79
80    /// Returns the hex component.
81    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        // Uppercase hex should be normalized to lowercase
237        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        // Should be all lowercase
254        assert!(
255            digest
256                .hex()
257                .chars()
258                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
259        );
260    }
261}