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, Write},
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/// A writer which records the digest and number of bytes accepted by its
80/// inner writer.
81#[derive(Debug)]
82pub struct HashingWriter<W> {
83    inner: W,
84    hasher: Sha256,
85    size: u64,
86}
87
88impl<W> HashingWriter<W> {
89    pub fn new(inner: W) -> HashingWriter<W> {
90        HashingWriter {
91            inner,
92            hasher: Sha256::new(),
93            size: 0,
94        }
95    }
96
97    pub fn finish(self) -> (W, Digest, u64) {
98        (
99            self.inner,
100            Digest(crate::paths::hex(&self.hasher.finalize())),
101            self.size,
102        )
103    }
104}
105
106impl<W: Write> Write for HashingWriter<W> {
107    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
108        let written = self.inner.write(buffer)?;
109        let written_u64 = u64::try_from(written)
110            .map_err(|_| std::io::Error::other("written byte count exceeds u64"))?;
111        self.size = self
112            .size
113            .checked_add(written_u64)
114            .ok_or_else(|| std::io::Error::other("written byte count exceeds u64"))?;
115        self.hasher.update(&buffer[..written]);
116        Ok(written)
117    }
118
119    fn flush(&mut self) -> std::io::Result<()> {
120        self.inner.flush()
121    }
122}
123
124/// Fail when bytes copied from a source no longer match the plan.
125pub fn ensure_matches_plan(
126    path: &Path,
127    expected_digest: &Digest,
128    expected_size: u64,
129    actual_digest: Digest,
130    actual_size: u64,
131) -> Result<()> {
132    if actual_digest == *expected_digest && actual_size == expected_size {
133        return Ok(());
134    }
135    Err(Error::SourceChanged {
136        path: path.to_path_buf(),
137        expected_digest: expected_digest.0.clone(),
138        expected_size,
139        actual_digest: actual_digest.0,
140        actual_size,
141    })
142}
143
144/// Hashes a path once per run.
145#[derive(Debug, Default)]
146pub struct DigestCache {
147    entries: HashMap<PathBuf, (Digest, u64)>,
148}
149
150impl DigestCache {
151    pub fn new() -> DigestCache {
152        DigestCache::default()
153    }
154
155    pub fn get(&mut self, path: &Path) -> Result<(Digest, u64)> {
156        if let Some(hit) = self.entries.get(path) {
157            return Ok(hit.clone());
158        }
159        let value = sha256_file(path)?;
160        self.entries.insert(path.to_path_buf(), value.clone());
161        Ok(value)
162    }
163
164    pub fn len(&self) -> usize {
165        self.entries.len()
166    }
167
168    pub fn is_empty(&self) -> bool {
169        self.entries.is_empty()
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn hashing_writer_returns_the_written_bytes_digest_and_size() {
179        let mut writer = HashingWriter::new(Vec::new());
180        writer.write_all(b"first").unwrap();
181        writer.write_all(b"-second").unwrap();
182        writer.flush().unwrap();
183
184        let (bytes, digest, size) = writer.finish();
185        assert_eq!(bytes, b"first-second");
186        assert_eq!(size, 12);
187        assert_eq!(digest, sha256_bytes(b"first-second"));
188    }
189
190    #[test]
191    fn streaming_and_in_memory_hashing_agree() {
192        let temp = tempfile::tempdir().unwrap();
193        let path = temp.path().join("blob");
194        // Larger than the read buffer, so more than one chunk is hashed.
195        let bytes: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
196        std::fs::write(&path, &bytes).unwrap();
197
198        let (digest, size) = sha256_file(&path).unwrap();
199        assert_eq!(size, bytes.len() as u64);
200        assert_eq!(digest, sha256_bytes(&bytes));
201
202        let empty = temp.path().join("empty");
203        std::fs::write(&empty, b"").unwrap();
204        assert_eq!(sha256_file(&empty).unwrap(), (sha256_bytes(b""), 0));
205    }
206
207    #[test]
208    fn the_cache_hashes_each_path_once() {
209        let temp = tempfile::tempdir().unwrap();
210        let path = temp.path().join("blob");
211        std::fs::write(&path, b"one").unwrap();
212
213        let mut cache = DigestCache::new();
214        let first = cache.get(&path).unwrap();
215        std::fs::write(&path, b"two").unwrap();
216        assert_eq!(
217            cache.get(&path).unwrap(),
218            first,
219            "the cached digest is reused"
220        );
221    }
222}