Skip to main content

hfile/
hash.rs

1use crate::command::Algorithm;
2use anyhow::{Context, Result, anyhow};
3use ring::digest::{Context as DigestContext, SHA1_FOR_LEGACY_USE_ONLY, SHA256, SHA384, SHA512};
4use std::{fs, io::Read, path::Path};
5
6const BUFFER_SIZE: usize = 1024 * 1024;
7const BLAKE3_RAYON_MIN_BYTES: u64 = 128 * 1024;
8const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
9
10#[derive(Copy, Clone)]
11enum Blake3Mode {
12    AlwaysRayon,
13    Adaptive,
14}
15
16fn read_file_chunks(file_path: &Path, mut on_chunk: impl FnMut(&[u8])) -> Result<()> {
17    let mut file = std::fs::File::open(file_path)?;
18    let mut buf = vec![0_u8; BUFFER_SIZE].into_boxed_slice();
19
20    loop {
21        let size = file.read(&mut buf)?;
22        if size == 0 {
23            break;
24        }
25
26        let chunk = buf
27            .get(..size)
28            .ok_or_else(|| anyhow!("read size exceeded buffer length"))?;
29        on_chunk(chunk);
30    }
31
32    Ok(())
33}
34
35fn digest_file(algo: Algorithm, file_path: &Path, blake3_mode: Blake3Mode) -> Result<Vec<u8>> {
36    let hash = match algo {
37        Algorithm::Md5 => md5_digest(file_path)?.to_vec(),
38        Algorithm::Sha1 => sha1_digest(file_path)?,
39        Algorithm::Sha256 => sha256_digest(file_path)?,
40        Algorithm::Sha384 => sha384_digest(file_path)?,
41        Algorithm::Sha512 => sha512_digest(file_path)?,
42        Algorithm::Blake => blake3_digest(file_path, blake3_mode)?.to_vec(),
43    };
44
45    Ok(hash)
46}
47
48/// # Errors
49/// Returns an error if the file cannot be opened or read.
50pub fn hash_file(algo: Algorithm, file_path: &Path) -> Result<String> {
51    let hash = digest_file(algo, file_path, Blake3Mode::AlwaysRayon)
52        .with_context(|| format!("failed to hash {}", file_path.display()))?;
53    Ok(write_hex_bytes(&hash))
54}
55
56/// # Errors
57/// Returns an error if the file cannot be opened or read.
58pub(crate) fn hash_file_for_walk(algo: Algorithm, file_path: &Path) -> Result<String> {
59    let hash = digest_file(algo, file_path, Blake3Mode::Adaptive)
60        .with_context(|| format!("failed to hash {}", file_path.display()))?;
61    Ok(write_hex_bytes(&hash))
62}
63
64/// # Errors
65/// Returns an error if the file cannot be opened or read.
66pub(crate) fn hash_file_bytes_for_walk(algo: Algorithm, file_path: &Path) -> Result<Vec<u8>> {
67    digest_file(algo, file_path, Blake3Mode::Adaptive)
68        .with_context(|| format!("failed to hash {}", file_path.display()))
69}
70
71fn blake3_digest(file_path: &Path, mode: Blake3Mode) -> Result<[u8; blake3::OUT_LEN]> {
72    let mut hasher = blake3::Hasher::new();
73
74    match mode {
75        Blake3Mode::AlwaysRayon => {
76            hasher.update_mmap_rayon(file_path)?;
77        }
78        Blake3Mode::Adaptive => {
79            let metadata = fs::metadata(file_path)?;
80            if metadata.len() >= BLAKE3_RAYON_MIN_BYTES {
81                hasher.update_mmap_rayon(file_path)?;
82            } else {
83                hasher.update_mmap(file_path)?;
84            }
85        }
86    }
87
88    Ok(*hasher.finalize().as_bytes())
89}
90
91fn md5_digest(file_path: &Path) -> Result<[u8; 16]> {
92    let mut context = md5::Context::new();
93    read_file_chunks(file_path, |chunk| context.consume(chunk))?;
94    Ok(context.compute().0)
95}
96
97fn sha1_digest(file_path: &Path) -> Result<Vec<u8>> {
98    let mut context = DigestContext::new(&SHA1_FOR_LEGACY_USE_ONLY);
99    read_file_chunks(file_path, |chunk| context.update(chunk))?;
100    Ok(context.finish().as_ref().to_vec())
101}
102
103fn sha256_digest(file_path: &Path) -> Result<Vec<u8>> {
104    let mut context = DigestContext::new(&SHA256);
105    read_file_chunks(file_path, |chunk| context.update(chunk))?;
106    Ok(context.finish().as_ref().to_vec())
107}
108
109fn sha384_digest(file_path: &Path) -> Result<Vec<u8>> {
110    let mut context = DigestContext::new(&SHA384);
111    read_file_chunks(file_path, |chunk| context.update(chunk))?;
112    Ok(context.finish().as_ref().to_vec())
113}
114
115fn sha512_digest(file_path: &Path) -> Result<Vec<u8>> {
116    let mut context = DigestContext::new(&SHA512);
117    read_file_chunks(file_path, |chunk| context.update(chunk))?;
118    Ok(context.finish().as_ref().to_vec())
119}
120
121/// # Errors
122/// Returns an error if the file cannot be opened or read.
123pub fn blake3(file_path: &Path) -> Result<String> {
124    let hash = blake3_digest(file_path, Blake3Mode::AlwaysRayon)?;
125    Ok(write_hex_bytes(&hash))
126}
127
128/// # Errors
129/// Returns an error if the file cannot be opened or read.
130pub fn md5(file_path: &Path) -> Result<String> {
131    let hash = md5_digest(file_path)?;
132    Ok(write_hex_bytes(&hash))
133}
134
135/// # Errors
136/// Returns an error if the file cannot be opened or read.
137pub fn sha1(file_path: &Path) -> Result<String> {
138    let hash = sha1_digest(file_path)?;
139    Ok(write_hex_bytes(&hash))
140}
141
142/// # Errors
143/// Returns an error if the file cannot be opened or read.
144pub fn sha256(file_path: &Path) -> Result<String> {
145    let hash = sha256_digest(file_path)?;
146    Ok(write_hex_bytes(&hash))
147}
148
149/// # Errors
150/// Returns an error if the file cannot be opened or read.
151pub fn sha384(file_path: &Path) -> Result<String> {
152    let hash = sha384_digest(file_path)?;
153    Ok(write_hex_bytes(&hash))
154}
155
156/// # Errors
157/// Returns an error if the file cannot be opened or read.
158pub fn sha512(file_path: &Path) -> Result<String> {
159    let hash = sha512_digest(file_path)?;
160    Ok(write_hex_bytes(&hash))
161}
162
163#[must_use]
164pub fn write_hex_bytes(bytes: &[u8]) -> String {
165    let mut s = String::with_capacity(bytes.len() * 2);
166    for &byte in bytes {
167        let high = HEX_DIGITS
168            .get(usize::from(byte >> 4))
169            .copied()
170            .unwrap_or(b'0');
171        let low = HEX_DIGITS
172            .get(usize::from(byte & 0x0f))
173            .copied()
174            .unwrap_or(b'0');
175        s.push(char::from(high));
176        s.push(char::from(low));
177    }
178    s
179}