Skip to main content

dnx_core/
integrity.rs

1use base64::{engine::general_purpose::STANDARD, Engine as _};
2use sha2::{Digest, Sha512};
3
4use crate::errors::{DnxError, Result};
5
6/// Computes the SHA-512 hash of the given data and returns it in the format "sha512-<base64>".
7///
8/// # Arguments
9///
10/// * `data` - The data to hash
11///
12/// # Returns
13///
14/// A string in the format "sha512-<base64>" where <base64> is the standard base64 encoding
15/// of the SHA-512 hash.
16///
17/// # Example
18///
19/// ```
20/// use dnx_core::integrity::compute_sha512;
21///
22/// let data = b"hello world";
23/// let integrity = compute_sha512(data);
24/// assert!(integrity.starts_with("sha512-"));
25/// ```
26pub fn compute_sha512(data: &[u8]) -> String {
27    let mut hasher = Sha512::new();
28    hasher.update(data);
29    let hash = hasher.finalize();
30    let encoded = STANDARD.encode(hash);
31    format!("sha512-{}", encoded)
32}
33
34/// Verifies that the given data matches the expected integrity hash.
35///
36/// # Arguments
37///
38/// * `data` - The data to verify
39/// * `expected` - The expected integrity hash in the format "sha512-<base64>"
40///
41/// # Returns
42///
43/// `Ok(true)` if the data matches the expected hash, `Ok(false)` if it doesn't match,
44/// or an error if the expected hash is malformed.
45///
46/// # Errors
47///
48/// Returns an error if:
49/// - The expected hash doesn't start with "sha512-"
50/// - The base64 encoding is invalid
51/// - The decoded hash has an unexpected length
52///
53/// # Example
54///
55/// ```
56/// use dnx_core::integrity::{compute_sha512, verify_integrity};
57///
58/// let data = b"hello world";
59/// let integrity = compute_sha512(data);
60/// assert!(verify_integrity(data, &integrity).unwrap());
61/// ```
62pub fn verify_integrity(data: &[u8], expected: &str) -> Result<bool> {
63    // Strip the "sha512-" prefix
64    let prefix = "sha512-";
65    if !expected.starts_with(prefix) {
66        return Err(DnxError::InvalidIntegrity(
67            "Integrity hash must start with 'sha512-'".to_string(),
68        ));
69    }
70
71    let base64_hash = &expected[prefix.len()..];
72
73    // Decode the base64 hash
74    let expected_hash = STANDARD
75        .decode(base64_hash)
76        .map_err(|e| DnxError::InvalidIntegrity(format!("Invalid base64 encoding: {}", e)))?;
77
78    // Compute the actual hash
79    let mut hasher = Sha512::new();
80    hasher.update(data);
81    let actual_hash = hasher.finalize();
82
83    // Compare the hashes
84    Ok(expected_hash.as_slice() == actual_hash.as_slice())
85}
86
87/// Converts an integrity string to a path-safe string suitable for use as a directory name.
88///
89/// This function converts standard base64 to URL-safe base64 by replacing `/` with `_` and
90/// `+` with `-`. This ensures the resulting string is safe to use as a directory name on
91/// NTFS and other file systems.
92///
93/// # Arguments
94///
95/// * `integrity` - The integrity string (e.g., "sha512-<base64>")
96///
97/// # Returns
98///
99/// A path-safe version of the integrity string
100///
101/// # Example
102///
103/// ```
104/// use dnx_core::integrity::integrity_to_path_safe;
105///
106/// let integrity = "sha512-abc+def/ghi==";
107/// let safe = integrity_to_path_safe(integrity);
108/// assert_eq!(safe, "sha512-abc-def_ghi==");
109/// ```
110pub fn integrity_to_path_safe(integrity: &str) -> String {
111    integrity.replace('/', "_").replace('+', "-")
112}
113
114/// A streaming hash calculator that can process data in chunks.
115///
116/// This is useful for computing integrity hashes of large files or streams without
117/// loading all the data into memory at once.
118///
119/// # Example
120///
121/// ```
122/// use dnx_core::integrity::StreamingHash;
123///
124/// let mut hasher = StreamingHash::new();
125/// hasher.update(b"hello ");
126/// hasher.update(b"world");
127/// let integrity = hasher.finalize();
128/// assert!(integrity.starts_with("sha512-"));
129/// ```
130pub struct StreamingHash {
131    hasher: Sha512,
132}
133
134impl StreamingHash {
135    /// Creates a new streaming hash calculator.
136    pub fn new() -> Self {
137        Self {
138            hasher: Sha512::new(),
139        }
140    }
141
142    /// Updates the hash with the given data chunk.
143    ///
144    /// # Arguments
145    ///
146    /// * `data` - The data chunk to add to the hash
147    pub fn update(&mut self, data: &[u8]) {
148        self.hasher.update(data);
149    }
150
151    /// Finalizes the hash and returns it in the format "sha512-<base64>".
152    ///
153    /// This consumes the `StreamingHash` instance.
154    ///
155    /// # Returns
156    ///
157    /// A string in the format "sha512-<base64>"
158    pub fn finalize(self) -> String {
159        let hash = self.hasher.finalize();
160        let encoded = STANDARD.encode(hash);
161        format!("sha512-{}", encoded)
162    }
163}
164
165impl Default for StreamingHash {
166    fn default() -> Self {
167        Self::new()
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_compute_sha512() {
177        let data = b"hello world";
178        let integrity = compute_sha512(data);
179        assert!(integrity.starts_with("sha512-"));
180        assert!(integrity.len() > 7);
181    }
182
183    #[test]
184    fn test_verify_integrity_valid() {
185        let data = b"hello world";
186        let integrity = compute_sha512(data);
187        assert!(verify_integrity(data, &integrity).unwrap());
188    }
189
190    #[test]
191    fn test_verify_integrity_invalid() {
192        let data = b"hello world";
193        let wrong_data = b"goodbye world";
194        let integrity = compute_sha512(data);
195        assert!(!verify_integrity(wrong_data, &integrity).unwrap());
196    }
197
198    #[test]
199    fn test_verify_integrity_invalid_prefix() {
200        let data = b"hello world";
201        let result = verify_integrity(data, "sha256-abc123");
202        assert!(result.is_err());
203    }
204
205    #[test]
206    fn test_verify_integrity_invalid_base64() {
207        let data = b"hello world";
208        let result = verify_integrity(data, "sha512-@@@invalid@@@");
209        assert!(result.is_err());
210    }
211
212    #[test]
213    fn test_integrity_to_path_safe() {
214        let integrity = "sha512-abc+def/ghi==";
215        let safe = integrity_to_path_safe(integrity);
216        assert_eq!(safe, "sha512-abc-def_ghi==");
217    }
218
219    #[test]
220    fn test_integrity_to_path_safe_no_special_chars() {
221        let integrity = "sha512-abcdefghi==";
222        let safe = integrity_to_path_safe(integrity);
223        assert_eq!(safe, "sha512-abcdefghi==");
224    }
225
226    #[test]
227    fn test_streaming_hash() {
228        let mut hasher = StreamingHash::new();
229        hasher.update(b"hello ");
230        hasher.update(b"world");
231        let integrity = hasher.finalize();
232
233        let direct = compute_sha512(b"hello world");
234        assert_eq!(integrity, direct);
235    }
236
237    #[test]
238    fn test_streaming_hash_empty() {
239        let hasher = StreamingHash::new();
240        let integrity = hasher.finalize();
241        let direct = compute_sha512(b"");
242        assert_eq!(integrity, direct);
243    }
244
245    #[test]
246    fn test_streaming_hash_single_update() {
247        let mut hasher = StreamingHash::new();
248        hasher.update(b"test data");
249        let integrity = hasher.finalize();
250        let direct = compute_sha512(b"test data");
251        assert_eq!(integrity, direct);
252    }
253}