Skip to main content

provenant/utils/
hash.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use md5::{Digest as Md5Digest, Md5};
5use sha1::Sha1;
6use sha2::Sha256;
7use std::fs::File;
8use std::io::{self, BufReader, Read};
9use std::path::Path;
10
11use crate::models::{GitSha1, Md5Digest as Md5DigestType, Sha1Digest, Sha256Digest};
12
13pub fn calculate_sha1(content: &[u8]) -> Sha1Digest {
14    let digest = Sha1::digest(content);
15    Sha1Digest::from_bytes(digest.into())
16}
17
18pub fn calculate_md5(content: &[u8]) -> Md5DigestType {
19    let digest = Md5::digest(content);
20    Md5DigestType::from_bytes(digest.into())
21}
22
23pub fn calculate_sha256(content: &[u8]) -> Sha256Digest {
24    let digest = Sha256::digest(content);
25    Sha256Digest::from_bytes(digest.into())
26}
27
28pub fn calculate_sha1_git(content: &[u8]) -> GitSha1 {
29    let mut payload = Vec::with_capacity(content.len() + 32);
30    payload.extend_from_slice(format!("blob {}\0", content.len()).as_bytes());
31    payload.extend_from_slice(content);
32    let digest = Sha1::digest(&payload);
33    GitSha1::from_bytes(digest.into())
34}
35
36/// Computes sha1, md5, sha256 and git-sha1 for an in-memory buffer in a single
37/// chunked pass that updates every hasher per chunk, improving cache locality
38/// over four independent full passes over the same bytes.
39pub fn calculate_buffer_hashes(
40    content: &[u8],
41) -> (Sha1Digest, Md5DigestType, Sha256Digest, GitSha1) {
42    let mut sha1 = Sha1::new();
43    let mut md5 = Md5::new();
44    let mut sha256 = Sha256::new();
45    let mut git_sha1 = Sha1::new();
46
47    git_sha1.update(format!("blob {}\0", content.len()).as_bytes());
48
49    const CHUNK: usize = 64 * 1024;
50    for chunk in content.chunks(CHUNK) {
51        sha1.update(chunk);
52        md5.update(chunk);
53        sha256.update(chunk);
54        git_sha1.update(chunk);
55    }
56
57    (
58        Sha1Digest::from_bytes(sha1.finalize().into()),
59        Md5DigestType::from_bytes(md5.finalize().into()),
60        Sha256Digest::from_bytes(sha256.finalize().into()),
61        GitSha1::from_bytes(git_sha1.finalize().into()),
62    )
63}
64
65pub fn calculate_file_hashes(
66    path: &Path,
67    size: u64,
68) -> io::Result<(Sha1Digest, Md5DigestType, Sha256Digest, GitSha1)> {
69    let file = File::open(path)?;
70    let mut reader = BufReader::new(file);
71    let mut sha1 = Sha1::new();
72    let mut md5 = Md5::new();
73    let mut sha256 = Sha256::new();
74    let mut git_sha1 = Sha1::new();
75    let mut buffer = [0_u8; 64 * 1024];
76
77    git_sha1.update(format!("blob {}\0", size).as_bytes());
78
79    loop {
80        let read = reader.read(&mut buffer)?;
81        if read == 0 {
82            break;
83        }
84
85        let chunk = &buffer[..read];
86        sha1.update(chunk);
87        md5.update(chunk);
88        sha256.update(chunk);
89        git_sha1.update(chunk);
90    }
91
92    Ok((
93        Sha1Digest::from_bytes(sha1.finalize().into()),
94        Md5DigestType::from_bytes(md5.finalize().into()),
95        Sha256Digest::from_bytes(sha256.finalize().into()),
96        GitSha1::from_bytes(git_sha1.finalize().into()),
97    ))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    /// Pins the fused buffer pass to the individual single-hash helpers so a
105    /// future change to either path cannot silently diverge the `--info` output.
106    #[test]
107    fn fused_buffer_hashes_match_individual_helpers() {
108        for content in [
109            b"".as_slice(),
110            b"hello world\n".as_slice(),
111            &vec![0xABu8; 200 * 1024],
112        ] {
113            let (sha1, md5, sha256, sha1_git) = calculate_buffer_hashes(content);
114            assert_eq!(sha1, calculate_sha1(content));
115            assert_eq!(md5, calculate_md5(content));
116            assert_eq!(sha256, calculate_sha256(content));
117            assert_eq!(sha1_git, calculate_sha1_git(content));
118        }
119    }
120}