reth_cli_commands/download/
verify.rs1use super::{manifest::OutputFileChecksum, progress::ArchiveVerificationProgress};
2use blake3::Hasher;
3use eyre::Result;
4use rayon::prelude::*;
5use reth_fs_util::{self as fs, FsPathError};
6use std::{
7 io::{ErrorKind, Read},
8 mem,
9 path::Path,
10};
11
12const HASH_BUFFER_SIZE: usize = 4 * 1024 * 1024;
14
15pub(crate) struct OutputVerifier<'a> {
17 target_dir: &'a Path,
19 static_files_dir: Option<&'a Path>,
21}
22
23impl<'a> OutputVerifier<'a> {
24 pub(crate) const fn new(target_dir: &'a Path, static_files_dir: Option<&'a Path>) -> Self {
26 Self { target_dir, static_files_dir }
27 }
28
29 pub(crate) fn verify(&self, output_files: &[OutputFileChecksum]) -> Result<bool> {
33 self.verify_with_progress(output_files, None)
34 }
35
36 pub(crate) fn verify_with_progress(
41 &self,
42 output_files: &[OutputFileChecksum],
43 progress: Option<&ArchiveVerificationProgress<'_>>,
44 ) -> Result<bool> {
45 if output_files.is_empty() {
46 return Ok(false);
47 }
48
49 let mut paths = Vec::with_capacity(output_files.len());
50 for expected in output_files {
51 let output_path = self.output_path(&expected.path);
52 match fs::metadata(&output_path) {
53 Ok(meta) if meta.len() == expected.size => paths.push(output_path),
54 Ok(_) => return Ok(false),
55 Err(FsPathError::Metadata { source, .. })
56 if source.kind() == ErrorKind::NotFound =>
57 {
58 return Ok(false)
59 }
60 Err(error) => return Err(error.into()),
61 }
62 }
63
64 output_files
65 .par_iter()
66 .zip(paths)
67 .map(|(expected, path)| {
68 Self::file_blake3_hex(&path, expected.size, progress)
69 .map(|actual| actual.eq_ignore_ascii_case(&expected.blake3))
70 })
71 .find_any(|verified| !matches!(verified, Ok(true)))
72 .unwrap_or(Ok(true))
73 }
74
75 pub(crate) fn cleanup(&self, output_files: &[OutputFileChecksum]) {
77 for output in output_files {
78 let _ = fs::remove_file(self.output_path(&output.path));
79 }
80 }
81
82 pub(crate) fn output_path(&self, path: &str) -> std::path::PathBuf {
84 if let Some(static_files_dir) = self.static_files_dir &&
85 let Some(relative_path) = super::extract::static_file_relative_path(Path::new(path))
86 {
87 return static_files_dir.join(relative_path)
88 }
89 self.target_dir.join(path)
90 }
91
92 fn file_blake3_hex(
97 path: &Path,
98 size: u64,
99 progress: Option<&ArchiveVerificationProgress<'_>>,
100 ) -> Result<String> {
101 let mut file = fs::open(path)?;
102 let mut hasher = Hasher::new();
103 let buf_len = size.min(HASH_BUFFER_SIZE as u64) as usize;
104 let mut current = vec![0_u8; buf_len];
105 let mut next = vec![0_u8; buf_len];
106
107 let mut filled = read_full(&mut file, &mut current)?;
108 while filled > 0 {
109 let (_, read) = rayon::join(
110 || {
111 hasher.update_rayon(¤t[..filled]);
112 },
113 || read_full(&mut file, &mut next),
114 );
115 if let Some(progress) = progress {
116 progress.record_verified(filled as u64);
117 }
118 filled = read?;
119 mem::swap(&mut current, &mut next);
120 }
121
122 Ok(hasher.finalize().to_hex().to_string())
123 }
124}
125
126fn read_full(reader: &mut impl Read, buf: &mut [u8]) -> std::io::Result<usize> {
131 let mut filled = 0;
132 while filled < buf.len() {
133 match reader.read(&mut buf[filled..]) {
134 Ok(0) => break,
135 Ok(n) => filled += n,
136 Err(error) if error.kind() == ErrorKind::Interrupted => {}
137 Err(error) => return Err(error),
138 }
139 }
140 Ok(filled)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 fn checksum(path: &str, data: &[u8]) -> OutputFileChecksum {
148 OutputFileChecksum {
149 path: path.into(),
150 size: data.len() as u64,
151 blake3: blake3::hash(data).to_hex().to_string(),
152 }
153 }
154
155 #[test]
156 fn verify_detects_each_kind_of_output_mismatch() {
157 let dir = tempfile::tempdir().unwrap();
158 let large: Vec<u8> = (0..2 * HASH_BUFFER_SIZE + 123).map(|i| (i % 251) as u8).collect();
159 let small = b"small".to_vec();
160 fs::write(dir.path().join("large"), &large).unwrap();
161 fs::write(dir.path().join("small"), &small).unwrap();
162 let outputs = vec![checksum("large", &large), checksum("small", &small)];
163 let verifier = OutputVerifier::new(dir.path(), None);
164
165 assert!(verifier.verify(&outputs).unwrap(), "matching multi-buffer outputs verify");
166
167 let mut uppercase = outputs.clone();
168 uppercase[0].blake3 = uppercase[0].blake3.to_ascii_uppercase();
169 assert!(verifier.verify(&uppercase).unwrap(), "checksums compare case-insensitively");
170
171 let mut flipped = large.clone();
172 flipped[HASH_BUFFER_SIZE + 7] ^= 1;
173 fs::write(dir.path().join("large"), &flipped).unwrap();
174 assert!(!verifier.verify(&outputs).unwrap(), "one flipped byte fails verification");
175
176 fs::write(dir.path().join("large"), &large[1..]).unwrap();
177 assert!(!verifier.verify(&outputs).unwrap(), "size mismatch fails verification");
178
179 fs::remove_file(dir.path().join("large")).unwrap();
180 assert!(!verifier.verify(&outputs).unwrap(), "missing file fails verification");
181
182 assert!(!verifier.verify(&[]).unwrap(), "archives without outputs never verify");
183 }
184
185 #[cfg(unix)]
186 #[test]
187 fn verify_returns_metadata_errors_other_than_not_found() {
188 let dir = tempfile::tempdir().unwrap();
189 fs::write(dir.path().join("file"), b"data").unwrap();
190 let verifier = OutputVerifier::new(dir.path(), None);
191
192 assert!(!verifier.verify(&[checksum("missing", b"data")]).unwrap());
193 assert!(
194 verifier.verify(&[checksum("file/child", b"data")]).is_err(),
195 "a path through a regular file is an error, not a missing output"
196 );
197 }
198
199 #[test]
200 fn custom_static_files_verification_and_cleanup() {
201 let datadir = tempfile::tempdir().unwrap();
202 let static_dir = tempfile::tempdir().unwrap();
203 let default_file = datadir.path().join("static_files/headers");
204 fs::create_dir_all(default_file.parent().unwrap()).unwrap();
205 fs::write(&default_file, b"headers").unwrap();
206 let outputs = [OutputFileChecksum {
207 path: "./static_files/headers".into(),
208 size: 7,
209 blake3: blake3::hash(b"headers").to_hex().to_string(),
210 }];
211 let verifier = OutputVerifier::new(datadir.path(), Some(static_dir.path()));
212 assert!(!verifier.verify(&outputs).unwrap());
213 let custom_file = static_dir.path().join("headers");
214 fs::write(&custom_file, b"headers").unwrap();
215 assert!(verifier.verify(&outputs).unwrap());
216 verifier.cleanup(&outputs);
217 assert!(!custom_file.exists());
218 assert_eq!(fs::read(default_file).unwrap(), b"headers");
219 }
220}