docker_registry/v2/
content_digest.rs1use std::str;
2
3use sha2::{self, Digest};
5
6#[derive(strum::Display, Clone, Debug)]
8pub enum DigestAlgorithm {
9 Sha256(sha2::Sha256),
10}
11
12impl std::str::FromStr for DigestAlgorithm {
13 type Err = ContentDigestError;
14
15 fn from_str(name: &str) -> Result<Self, Self::Err> {
16 match name {
17 "sha256" => Ok(DigestAlgorithm::Sha256(sha2::Sha256::new())),
18 _ => Err(ContentDigestError::AlgorithmUnknown(name.to_string())),
19 }
20 }
21}
22
23#[derive(Debug, thiserror::Error)]
24pub enum ContentDigestError {
25 #[error("digest {0} does not have algorithm prefix")]
26 BadDigest(String),
27 #[error("unknown algorithm: {0}")]
28 AlgorithmUnknown(String),
29 #[error("verification failed: expected '{expected}', got '{got}'")]
30 Verify { expected: String, got: String },
31}
32
33#[derive(Clone, Debug)]
35pub struct ContentDigest {
36 digest: String,
37 algorithm: DigestAlgorithm,
38}
39
40impl ContentDigest {
41 pub fn try_new(digest: &str) -> std::result::Result<Self, ContentDigestError> {
47 let digest_split = digest.split(':').collect::<Vec<&str>>();
48
49 if digest_split.len() != 2 {
50 return Err(ContentDigestError::BadDigest(digest.to_string()));
51 }
52
53 let algorithm = std::str::FromStr::from_str(digest_split[0])?;
54 Ok(ContentDigest {
55 digest: digest.to_string(),
56 algorithm,
57 })
58 }
59
60 pub fn update(&mut self, input: &[u8]) {
61 self.algorithm.update(input)
62 }
63
64 pub fn verify(self) -> std::result::Result<(), ContentDigestError> {
65 let digest = self.algorithm.digest();
66 if digest != self.digest {
67 return Err(ContentDigestError::Verify {
68 expected: self.digest,
69 got: digest,
70 });
71 }
72 Ok(())
73 }
74}
75
76impl std::fmt::Display for ContentDigest {
77 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
78 write!(f, "{}:{}", self.algorithm, self.digest)
79 }
80}
81
82impl DigestAlgorithm {
83 fn update(&mut self, input: &[u8]) {
84 match self {
85 DigestAlgorithm::Sha256(hash) => {
86 hash.update(input);
87 }
88 }
89 }
90
91 fn digest(self) -> String {
92 let (algo, digest) = match self {
93 DigestAlgorithm::Sha256(hash) => ("sha256", hash.finalize()),
94 };
95 format!("{}:{:x}", algo, &digest)
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use sha2;
102
103 use super::*;
104
105 type Fallible<T> = Result<T, crate::Error>;
106
107 #[test]
108 fn try_new_succeeds_with_correct_digest() -> Fallible<()> {
109 let correct_digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
110 ContentDigest::try_new(correct_digest)?;
111
112 Ok(())
113 }
114
115 #[test]
116 fn try_new_fails_with_incorrect_digest() {
117 for incorrect_digest in &[
118 "invalid",
119 "invalid:",
120 "invalid:0000000000000000000000000000000000000000000000000000000000000000",
121 ] {
122 if ContentDigest::try_new(incorrect_digest).is_ok() {
123 panic!("expected try_new to fail for incorrect digest {incorrect_digest}");
124 }
125 }
126 }
127
128 #[test]
129 fn verify_succeeds_with_same_content() -> Fallible<()> {
130 let blob: &[u8] = b"somecontent";
131 let mut content_digest =
132 ContentDigest::try_new("sha256:d5a3477d91583e65a7aba6f6db7a53e2de739bc7bf8f4a08f0df0457b637f1fb")?;
133 content_digest.update(blob);
134 content_digest.verify().map_err(Into::into)
135 }
136
137 #[test]
138 fn verify_chunked_succeeds_with_same_content() -> Fallible<()> {
139 let mut content_digest =
140 ContentDigest::try_new("sha256:d5a3477d91583e65a7aba6f6db7a53e2de739bc7bf8f4a08f0df0457b637f1fb")?;
141 content_digest.update(b"some");
142 content_digest.update(b"content");
143 content_digest.verify().map_err(Into::into)
144 }
145
146 #[test]
147 fn verify_fails_with_different_content() -> Fallible<()> {
148 let blob: &[u8] = b"somecontent";
149 let different_blob: &[u8] = b"someothercontent";
150
151 let mut expected_digest = DigestAlgorithm::Sha256(sha2::Sha256::new());
152 expected_digest.update(different_blob);
153 let expected_digest = expected_digest.digest();
154
155 let mut content_digest = ContentDigest::try_new(&expected_digest)?;
156 content_digest.update(blob);
157 if content_digest.verify().is_ok() {
158 panic!("expected try_verify to fail for a different blob");
159 }
160 Ok(())
161 }
162}