docker_image_pusher/
digest.rs1use sha2::Digest;
7use crate::error::{Result, PusherError};
8
9pub const EMPTY_LAYER_DIGEST: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
11
12pub const EMPTY_LAYER_DIGEST_FULL: &str = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
14
15pub struct DigestUtils;
17
18impl DigestUtils {
19 pub fn compute_sha256(data: &[u8]) -> String {
21 let mut hasher = sha2::Sha256::new();
22 hasher.update(data);
23 format!("{:x}", hasher.finalize())
24 }
25
26 pub fn compute_sha256_str(data: &str) -> String {
28 Self::compute_sha256(data.as_bytes())
29 }
30
31 pub fn compute_docker_digest(data: &[u8]) -> String {
33 format!("sha256:{}", Self::compute_sha256(data))
34 }
35
36 pub fn compute_docker_digest_str(data: &str) -> String {
38 format!("sha256:{}", Self::compute_sha256_str(data))
39 }
40
41 pub fn is_valid_sha256_hex(digest: &str) -> bool {
43 digest.len() == 64 && digest.chars().all(|c| c.is_ascii_hexdigit())
44 }
45
46 pub fn is_valid_docker_digest(digest: &str) -> bool {
48 if let Some(hex_part) = digest.strip_prefix("sha256:") {
49 Self::is_valid_sha256_hex(hex_part)
50 } else {
51 false
52 }
53 }
54
55 pub fn normalize_digest(digest: &str) -> Result<String> {
57 if digest.starts_with("sha256:") {
58 if digest.len() != 71 {
60 return Err(PusherError::Validation(format!(
61 "Invalid SHA256 digest length: expected 71 characters, got {}", digest.len()
62 )));
63 }
64 let hex_part = &digest[7..];
65 if !Self::is_valid_sha256_hex(hex_part) {
66 return Err(PusherError::Validation(format!(
67 "Invalid SHA256 digest format: contains non-hex characters"
68 )));
69 }
70 Ok(digest.to_string())
71 } else {
72 if !Self::is_valid_sha256_hex(digest) {
74 return Err(PusherError::Validation(format!(
75 "Invalid SHA256 digest: expected 64 hex characters, got '{}'", digest
76 )));
77 }
78 Ok(format!("sha256:{}", digest))
79 }
80 }
81
82 pub fn extract_hex_part(digest: &str) -> Result<&str> {
84 if let Some(hex_part) = digest.strip_prefix("sha256:") {
85 if Self::is_valid_sha256_hex(hex_part) {
86 Ok(hex_part)
87 } else {
88 Err(PusherError::Validation(format!(
89 "Invalid SHA256 hex part in digest: {}", digest
90 )))
91 }
92 } else {
93 Err(PusherError::Validation(format!(
94 "Digest missing sha256: prefix: {}", digest
95 )))
96 }
97 }
98
99 pub fn is_empty_layer_digest(digest: &str) -> bool {
101 digest == EMPTY_LAYER_DIGEST_FULL || digest == EMPTY_LAYER_DIGEST
102 }
103
104 pub fn empty_layer_digest() -> String {
106 EMPTY_LAYER_DIGEST_FULL.to_string()
107 }
108
109 pub fn verify_data_integrity(data: &[u8], expected_digest: &str) -> Result<()> {
111 let computed = Self::compute_sha256(data);
112 let expected_hex = Self::extract_hex_part(expected_digest)?;
113
114 if computed != expected_hex {
115 return Err(PusherError::Validation(format!(
116 "Data integrity check failed: expected {}, computed sha256:{}",
117 expected_digest, computed
118 )));
119 }
120
121 Ok(())
122 }
123
124 pub fn extract_digest_from_layer_path(layer_path: &str) -> Option<String> {
126 if let Some(slash_pos) = layer_path.find('/') {
133 let digest_part = &layer_path[..slash_pos];
134 if Self::is_valid_sha256_hex(digest_part) {
135 return Some(digest_part.to_string());
136 }
137 }
138 if layer_path.contains("blobs/sha256/") {
140 if let Some(start) = layer_path.find("blobs/sha256/") {
141 let after_prefix = &layer_path[start + 13..];
142 let end = after_prefix.find('/').unwrap_or(after_prefix.len());
143 let digest_part = &after_prefix[..end];
144 if Self::is_valid_sha256_hex(digest_part) {
145 return Some(digest_part.to_string());
146 }
147 }
148 }
149
150 if let Some(dot_pos) = layer_path.rfind('.') {
152 let digest_part = &layer_path[..dot_pos];
153 if Self::is_valid_sha256_hex(digest_part) {
154 return Some(digest_part.to_string());
155 }
156 }
157
158 if Self::is_valid_sha256_hex(layer_path) {
160 return Some(layer_path.to_string());
161 }
162
163 None
164 }
165
166 pub fn generate_path_based_digest(layer_path: &str) -> String {
168 let mut hasher = sha2::Sha256::new();
169 hasher.update(layer_path.as_bytes());
170 format!("sha256:{:x}", hasher.finalize())
171 }
172
173 pub fn format_digest_short(digest: &str) -> String {
175 if digest.len() > 23 {
176 format!("{}...", &digest[..23])
177 } else {
178 digest.to_string()
179 }
180 }
181
182 pub fn validate_digests(digests: &[&str]) -> Result<()> {
184 for (i, digest) in digests.iter().enumerate() {
185 if !Self::is_valid_docker_digest(digest) {
186 return Err(PusherError::Validation(format!(
187 "Invalid digest format at index {}: {}", i, digest
188 )));
189 }
190 }
191 Ok(())
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_compute_sha256() {
201 let data = b"hello world";
202 let digest = DigestUtils::compute_sha256(data);
203 assert_eq!(digest, "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
204 }
205
206 #[test]
207 fn test_compute_docker_digest() {
208 let data = b"hello world";
209 let digest = DigestUtils::compute_docker_digest(data);
210 assert_eq!(digest, "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
211 }
212
213 #[test]
214 fn test_empty_layer_digest() {
215 let empty_data = b"";
216 let computed = DigestUtils::compute_sha256(empty_data);
217 assert_eq!(computed, EMPTY_LAYER_DIGEST);
218 }
219
220 #[test]
221 fn test_validate_digest() {
222 assert!(DigestUtils::is_valid_docker_digest("sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"));
223 assert!(!DigestUtils::is_valid_docker_digest("sha256:invalid"));
224 assert!(!DigestUtils::is_valid_docker_digest("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"));
225 }
226
227 #[test]
228 fn test_normalize_digest() {
229 let hex_only = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
230 let normalized = DigestUtils::normalize_digest(hex_only).unwrap();
231 assert_eq!(normalized, "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
232 }
233
234 #[test]
235 fn test_extract_digest_from_layer_path() {
236 let paths = vec![
237 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9/layer.tar",
238 "blobs/sha256/b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
239 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9.tar",
240 ];
241
242 for path in paths {
243 let digest = DigestUtils::extract_digest_from_layer_path(path);
244 assert_eq!(digest, Some("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9".to_string()));
245 }
246 }
247
248 #[test]
249 fn test_verify_data_integrity() {
250 let data = b"hello world";
251 let digest = "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
252 assert!(DigestUtils::verify_data_integrity(data, digest).is_ok());
253
254 let wrong_digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
255 assert!(DigestUtils::verify_data_integrity(data, wrong_digest).is_err());
256 }
257}