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},
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
79pub 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#[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 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}