docker_image_pusher/
digest.rs

1//! SHA256 digest utilities for Docker image processing
2//! 
3//! This module provides centralized functionality for computing, validating,
4//! and formatting SHA256 digests used throughout the Docker image pusher.
5
6use sha2::Digest;
7use crate::error::{Result, PusherError};
8
9/// Standard SHA256 digest for empty files/layers
10pub const EMPTY_LAYER_DIGEST: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
11
12/// Docker digest with sha256: prefix for empty layers
13pub const EMPTY_LAYER_DIGEST_FULL: &str = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
14
15/// Utilities for working with SHA256 digests in Docker context
16pub struct DigestUtils;
17
18impl DigestUtils {
19    /// Compute SHA256 digest from byte data
20    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    /// Compute SHA256 digest from string data
27    pub fn compute_sha256_str(data: &str) -> String {
28        Self::compute_sha256(data.as_bytes())
29    }
30
31    /// Compute full Docker digest (with sha256: prefix) from byte data
32    pub fn compute_docker_digest(data: &[u8]) -> String {
33        format!("sha256:{}", Self::compute_sha256(data))
34    }
35
36    /// Compute full Docker digest (with sha256: prefix) from string data
37    pub fn compute_docker_digest_str(data: &str) -> String {
38        format!("sha256:{}", Self::compute_sha256_str(data))
39    }
40
41    /// Validate SHA256 hex string (64 characters, all hex)
42    pub fn is_valid_sha256_hex(digest: &str) -> bool {
43        digest.len() == 64 && digest.chars().all(|c| c.is_ascii_hexdigit())
44    }
45
46    /// Validate full Docker digest format (sha256:xxxxx)
47    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    /// Normalize digest to full Docker format (add sha256: prefix if missing)
56    pub fn normalize_digest(digest: &str) -> Result<String> {
57        if digest.starts_with("sha256:") {
58            // Validate existing format
59            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            // Add prefix and validate
73            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    /// Extract SHA256 hex part from full Docker digest
83    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    /// Check if a digest represents an empty layer
100    pub fn is_empty_layer_digest(digest: &str) -> bool {
101        digest == EMPTY_LAYER_DIGEST_FULL || digest == EMPTY_LAYER_DIGEST
102    }
103
104    /// Get the standard empty layer digest with full Docker format
105    pub fn empty_layer_digest() -> String {
106        EMPTY_LAYER_DIGEST_FULL.to_string()
107    }
108
109    /// Verify data matches expected digest
110    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    /// Extract digest from Docker layer path (various formats)
125    pub fn extract_digest_from_layer_path(layer_path: &str) -> Option<String> {
126        // Docker tar文件中的层路径通常是这样的格式:
127        // "abc123def456.../layer.tar"
128        // "blobs/sha256/abc123def456..."
129        // "abc123def456.tar"
130        
131        // 首先尝试目录名格式 (最常见的格式)
132        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          // 尝试blobs格式
139        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        // 尝试文件名格式
151        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        // 尝试完整路径作为digest (某些特殊情况)
159        if Self::is_valid_sha256_hex(layer_path) {
160            return Some(layer_path.to_string());
161        }
162        
163        None
164    }
165
166    /// Generate a fallback digest from path when real digest cannot be extracted
167    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    /// Format digest for display (truncated for readability)
174    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    /// Batch validate multiple digests
183    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}