Skip to main content

elfpak_core/
hash.rs

1//! Content hashing and a small per-run cache.
2
3use crate::{
4    error::{Error, Result, io},
5    graph::Digest,
6};
7use sha2::{Digest as _, Sha256};
8use std::{
9    collections::HashMap,
10    io::{BufRead, Read},
11    path::{Path, PathBuf},
12};
13
14pub fn sha256_bytes(bytes: &[u8]) -> Digest {
15    let mut hasher = Sha256::new();
16    hasher.update(bytes);
17    Digest(crate::paths::hex(&hasher.finalize()))
18}
19
20/// Read buffer for streamed hashing.
21const HASH_BUFFER_SIZE_BYTES: usize = 64 * 1024;
22
23/// Hash a file without loading it all at once.
24pub fn sha256_file(path: &Path) -> Result<(Digest, u64)> {
25    let file = std::fs::File::open(path).map_err(|e| io(path, e))?;
26    let mut reader = std::io::BufReader::with_capacity(HASH_BUFFER_SIZE_BYTES, file);
27    let mut hasher = Sha256::new();
28    let mut size = 0u64;
29    loop {
30        let chunk = reader.fill_buf().map_err(|e| io(path, e))?;
31        if chunk.is_empty() {
32            break;
33        }
34        let consumed = chunk.len();
35        hasher.update(chunk);
36        size += consumed as u64;
37        reader.consume(consumed);
38    }
39    Ok((Digest(crate::paths::hex(&hasher.finalize())), size))
40}
41
42/// A reader which records the digest and number of bytes it has yielded.
43///
44/// Output backends use this while copying planned source files so the bytes
45/// that were actually written are checked against the immutable plan.
46#[derive(Debug)]
47pub struct HashingReader<R> {
48    inner: R,
49    hasher: Sha256,
50    size: u64,
51}
52
53impl<R> HashingReader<R> {
54    pub fn new(inner: R) -> HashingReader<R> {
55        HashingReader {
56            inner,
57            hasher: Sha256::new(),
58            size: 0,
59        }
60    }
61
62    pub fn finish(self) -> (Digest, u64) {
63        (
64            Digest(crate::paths::hex(&self.hasher.finalize())),
65            self.size,
66        )
67    }
68}
69
70impl<R: Read> Read for HashingReader<R> {
71    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
72        let read = self.inner.read(buffer)?;
73        self.hasher.update(&buffer[..read]);
74        self.size += read as u64;
75        Ok(read)
76    }
77}
78
79/// Fail when bytes copied from a source no longer match the plan.
80pub fn ensure_matches_plan(
81    path: &Path,
82    expected_digest: &Digest,
83    expected_size: u64,
84    actual_digest: Digest,
85    actual_size: u64,
86) -> Result<()> {
87    if actual_digest == *expected_digest && actual_size == expected_size {
88        return Ok(());
89    }
90    Err(Error::SourceChanged {
91        path: path.to_path_buf(),
92        expected_digest: expected_digest.0.clone(),
93        expected_size,
94        actual_digest: actual_digest.0,
95        actual_size,
96    })
97}
98
99/// Hashes a path once per run.
100#[derive(Debug, Default)]
101pub struct DigestCache {
102    entries: HashMap<PathBuf, (Digest, u64)>,
103}
104
105impl DigestCache {
106    pub fn new() -> DigestCache {
107        DigestCache::default()
108    }
109
110    pub fn get(&mut self, path: &Path) -> Result<(Digest, u64)> {
111        if let Some(hit) = self.entries.get(path) {
112            return Ok(hit.clone());
113        }
114        let value = sha256_file(path)?;
115        self.entries.insert(path.to_path_buf(), value.clone());
116        Ok(value)
117    }
118
119    pub fn len(&self) -> usize {
120        self.entries.len()
121    }
122
123    pub fn is_empty(&self) -> bool {
124        self.entries.is_empty()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn streaming_and_in_memory_hashing_agree() {
134        let temp = tempfile::tempdir().unwrap();
135        let path = temp.path().join("blob");
136        // Larger than the read buffer, so more than one chunk is hashed.
137        let bytes: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
138        std::fs::write(&path, &bytes).unwrap();
139
140        let (digest, size) = sha256_file(&path).unwrap();
141        assert_eq!(size, bytes.len() as u64);
142        assert_eq!(digest, sha256_bytes(&bytes));
143
144        let empty = temp.path().join("empty");
145        std::fs::write(&empty, b"").unwrap();
146        assert_eq!(sha256_file(&empty).unwrap(), (sha256_bytes(b""), 0));
147    }
148
149    #[test]
150    fn the_cache_hashes_each_path_once() {
151        let temp = tempfile::tempdir().unwrap();
152        let path = temp.path().join("blob");
153        std::fs::write(&path, b"one").unwrap();
154
155        let mut cache = DigestCache::new();
156        let first = cache.get(&path).unwrap();
157        std::fs::write(&path, b"two").unwrap();
158        assert_eq!(
159            cache.get(&path).unwrap(),
160            first,
161            "the cached digest is reused"
162        );
163    }
164}