Skip to main content

shell_tunnel/fs/
sha256.rs

1//! SHA-256 over files and over streams.
2//!
3//! Two shapes because the two callers differ: `list` hashes a file that already
4//! exists, while an upload hashes bytes as they arrive and can never hold the
5//! whole file in memory.
6
7use std::io::Read;
8use std::path::Path;
9
10use sha2::{Digest, Sha256};
11
12/// Read size for file hashing. Large enough that syscall overhead disappears,
13/// small enough to stay off the stack pressure of a big buffer.
14const READ_CHUNK: usize = 64 * 1024;
15
16/// Hash a whole file, returning lowercase hex.
17pub fn hash_file(path: &Path) -> std::io::Result<String> {
18    let mut file = std::fs::File::open(path)?;
19    let mut digest = Sha256::new();
20    let mut buffer = vec![0_u8; READ_CHUNK];
21    loop {
22        let read = file.read(&mut buffer)?;
23        if read == 0 {
24            break;
25        }
26        digest.update(&buffer[..read]);
27    }
28    Ok(hex(&digest.finalize()))
29}
30
31/// Incremental hasher for bytes that arrive over time.
32#[derive(Debug, Default)]
33pub struct Hasher {
34    inner: Sha256,
35}
36
37impl Hasher {
38    pub fn new() -> Self {
39        Self {
40            inner: Sha256::new(),
41        }
42    }
43
44    pub fn update(&mut self, bytes: &[u8]) {
45        self.inner.update(bytes);
46    }
47
48    /// Consume the hasher and render the digest as lowercase hex.
49    pub fn finish(self) -> String {
50        hex(&self.inner.finalize())
51    }
52}
53
54fn hex(bytes: &[u8]) -> String {
55    let mut out = String::with_capacity(bytes.len() * 2);
56    for byte in bytes {
57        out.push_str(&format!("{byte:02x}"));
58    }
59    out
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn the_nist_vector_for_abc() {
68        let mut hasher = Hasher::new();
69        hasher.update(b"abc");
70        assert_eq!(
71            hasher.finish(),
72            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
73        );
74    }
75
76    #[test]
77    fn the_empty_input_vector() {
78        assert_eq!(
79            Hasher::new().finish(),
80            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
81        );
82    }
83
84    #[test]
85    fn incremental_updates_match_a_single_update() {
86        let mut split = Hasher::new();
87        split.update(b"ab");
88        split.update(b"c");
89
90        let mut whole = Hasher::new();
91        whole.update(b"abc");
92
93        assert_eq!(split.finish(), whole.finish());
94    }
95
96    #[test]
97    fn hashing_a_file_matches_hashing_its_bytes() {
98        let dir = tempfile::tempdir().expect("tempdir");
99        let path = dir.path().join("f.bin");
100        std::fs::write(&path, b"abc").expect("write");
101        assert_eq!(
102            hash_file(&path).expect("hash"),
103            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
104        );
105    }
106}