1use 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
20const HASH_BUFFER_SIZE_BYTES: usize = 64 * 1024;
22
23pub 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#[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#[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
124pub 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#[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 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}