reth_cli_commands/download/
verify.rs1use super::{
2 extract::ensure_no_symlinks, manifest::OutputFileChecksum,
3 progress::ArchiveVerificationProgress,
4};
5use blake3::Hasher;
6use eyre::Result;
7use rayon::prelude::*;
8use reth_fs_util::{self as fs, FsPathError};
9use std::{
10 io::{ErrorKind, Read},
11 mem,
12 path::{Path, PathBuf},
13};
14use tracing::warn;
15
16const HASH_BUFFER_SIZE: usize = 4 * 1024 * 1024;
18
19pub(crate) struct OutputVerifier<'a> {
21 target_dir: &'a Path,
23 static_files_dir: Option<&'a Path>,
25}
26
27impl<'a> OutputVerifier<'a> {
28 pub(crate) const fn new(target_dir: &'a Path, static_files_dir: Option<&'a Path>) -> Self {
30 Self { target_dir, static_files_dir }
31 }
32
33 pub(crate) fn verify(&self, output_files: &[OutputFileChecksum]) -> Result<bool> {
37 self.verify_with_progress(output_files, None)
38 }
39
40 pub(crate) fn verify_with_progress(
45 &self,
46 output_files: &[OutputFileChecksum],
47 progress: Option<&ArchiveVerificationProgress<'_>>,
48 ) -> Result<bool> {
49 if output_files.is_empty() {
50 return Ok(false);
51 }
52
53 let mut paths = Vec::with_capacity(output_files.len());
54 for expected in output_files {
55 let output_path = self.output_path(expected)?;
56 match fs::metadata(&output_path) {
57 Ok(meta) if meta.len() == expected.size => paths.push(output_path),
58 Ok(_) => return Ok(false),
59 Err(FsPathError::Metadata { source, .. })
60 if source.kind() == ErrorKind::NotFound =>
61 {
62 return Ok(false)
63 }
64 Err(error) => return Err(error.into()),
65 }
66 }
67
68 output_files
69 .par_iter()
70 .zip(paths)
71 .map(|(expected, path)| {
72 Self::file_blake3_hex(&path, expected.size, progress)
73 .map(|actual| actual.eq_ignore_ascii_case(&expected.blake3))
74 })
75 .find_any(|verified| !matches!(verified, Ok(true)))
76 .unwrap_or(Ok(true))
77 }
78
79 pub(crate) fn cleanup(&self, output_files: &[OutputFileChecksum]) {
85 for output in output_files {
86 match self.removal_path(output) {
87 Ok(path) => {
88 let _ = fs::remove_file(path);
89 }
90 Err(error) => {
91 warn!(target: "reth::cli", path = %output.path, %error, "Skipping output cleanup");
92 }
93 }
94 }
95 }
96
97 fn removal_path(&self, output: &OutputFileChecksum) -> Result<PathBuf> {
100 let (root, relative) = self.resolve(output)?;
101 if let Some(parent) = relative.parent() {
102 ensure_no_symlinks(root, parent)?;
103 }
104 Ok(root.join(relative))
105 }
106
107 pub(crate) fn output_path(&self, output: &OutputFileChecksum) -> Result<PathBuf> {
109 self.resolve(output).map(|(root, relative)| root.join(relative))
110 }
111
112 fn resolve<'p>(&'p self, output: &'p OutputFileChecksum) -> Result<(&'p Path, &'p Path)> {
114 let path = output.relative_path()?;
115 if let Some(static_files_dir) = self.static_files_dir &&
116 let Some(relative_path) = super::extract::static_file_relative_path(path)
117 {
118 return Ok((static_files_dir, relative_path))
119 }
120 Ok((self.target_dir, path))
121 }
122
123 fn file_blake3_hex(
128 path: &Path,
129 size: u64,
130 progress: Option<&ArchiveVerificationProgress<'_>>,
131 ) -> Result<String> {
132 let mut file = fs::open(path)?;
133 let mut hasher = Hasher::new();
134 let buf_len = size.min(HASH_BUFFER_SIZE as u64) as usize;
135 let mut current = vec![0_u8; buf_len];
136 let mut next = vec![0_u8; buf_len];
137
138 let mut filled = read_full(&mut file, &mut current)?;
139 while filled > 0 {
140 let (_, read) = rayon::join(
141 || {
142 hasher.update_rayon(¤t[..filled]);
143 },
144 || read_full(&mut file, &mut next),
145 );
146 if let Some(progress) = progress {
147 progress.record_verified(filled as u64);
148 }
149 filled = read?;
150 mem::swap(&mut current, &mut next);
151 }
152
153 Ok(hasher.finalize().to_hex().to_string())
154 }
155}
156
157fn read_full(reader: &mut impl Read, buf: &mut [u8]) -> std::io::Result<usize> {
162 let mut filled = 0;
163 while filled < buf.len() {
164 match reader.read(&mut buf[filled..]) {
165 Ok(0) => break,
166 Ok(n) => filled += n,
167 Err(error) if error.kind() == ErrorKind::Interrupted => {}
168 Err(error) => return Err(error),
169 }
170 }
171 Ok(filled)
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn checksum(path: &str, data: &[u8]) -> OutputFileChecksum {
179 OutputFileChecksum {
180 path: path.into(),
181 size: data.len() as u64,
182 blake3: blake3::hash(data).to_hex().to_string(),
183 }
184 }
185
186 #[test]
187 fn verify_detects_each_kind_of_output_mismatch() {
188 let dir = tempfile::tempdir().unwrap();
189 let large: Vec<u8> = (0..2 * HASH_BUFFER_SIZE + 123).map(|i| (i % 251) as u8).collect();
190 let small = b"small".to_vec();
191 fs::write(dir.path().join("large"), &large).unwrap();
192 fs::write(dir.path().join("small"), &small).unwrap();
193 let outputs = vec![checksum("large", &large), checksum("small", &small)];
194 let verifier = OutputVerifier::new(dir.path(), None);
195
196 assert!(verifier.verify(&outputs).unwrap(), "matching multi-buffer outputs verify");
197
198 let mut uppercase = outputs.clone();
199 uppercase[0].blake3 = uppercase[0].blake3.to_ascii_uppercase();
200 assert!(verifier.verify(&uppercase).unwrap(), "checksums compare case-insensitively");
201
202 let mut flipped = large.clone();
203 flipped[HASH_BUFFER_SIZE + 7] ^= 1;
204 fs::write(dir.path().join("large"), &flipped).unwrap();
205 assert!(!verifier.verify(&outputs).unwrap(), "one flipped byte fails verification");
206
207 fs::write(dir.path().join("large"), &large[1..]).unwrap();
208 assert!(!verifier.verify(&outputs).unwrap(), "size mismatch fails verification");
209
210 fs::remove_file(dir.path().join("large")).unwrap();
211 assert!(!verifier.verify(&outputs).unwrap(), "missing file fails verification");
212
213 assert!(!verifier.verify(&[]).unwrap(), "archives without outputs never verify");
214 }
215
216 #[cfg(unix)]
217 #[test]
218 fn verify_returns_metadata_errors_other_than_not_found() {
219 let dir = tempfile::tempdir().unwrap();
220 fs::write(dir.path().join("file"), b"data").unwrap();
221 let verifier = OutputVerifier::new(dir.path(), None);
222
223 assert!(!verifier.verify(&[checksum("missing", b"data")]).unwrap());
224 assert!(
225 verifier.verify(&[checksum("file/child", b"data")]).is_err(),
226 "a path through a regular file is an error, not a missing output"
227 );
228 }
229
230 #[test]
231 fn custom_static_files_verification_and_cleanup() {
232 let datadir = tempfile::tempdir().unwrap();
233 let static_dir = tempfile::tempdir().unwrap();
234 let default_file = datadir.path().join("static_files/headers");
235 fs::create_dir_all(default_file.parent().unwrap()).unwrap();
236 fs::write(&default_file, b"headers").unwrap();
237 let outputs = [OutputFileChecksum {
238 path: "./static_files/headers".into(),
239 size: 7,
240 blake3: blake3::hash(b"headers").to_hex().to_string(),
241 }];
242 let verifier = OutputVerifier::new(datadir.path(), Some(static_dir.path()));
243 assert!(!verifier.verify(&outputs).unwrap());
244 let custom_file = static_dir.path().join("headers");
245 fs::write(&custom_file, b"headers").unwrap();
246 assert!(verifier.verify(&outputs).unwrap());
247 verifier.cleanup(&outputs);
248 assert!(!custom_file.exists());
249 assert_eq!(fs::read(default_file).unwrap(), b"headers");
250 }
251
252 #[test]
253 fn cleanup_ignores_output_paths_outside_target() {
254 let root = tempfile::tempdir().unwrap();
255 let datadir = root.path().join("datadir");
256 fs::create_dir_all(&datadir).unwrap();
257 let outside = root.path().join("outside");
258 fs::write(&outside, b"keep").unwrap();
259 let inside = datadir.join("inside");
260 fs::write(&inside, b"remove").unwrap();
261
262 let outputs = [
263 checksum(outside.to_str().unwrap(), b"keep"),
264 checksum("../outside", b"keep"),
265 checksum("db/../../outside", b"keep"),
266 checksum("inside", b"remove"),
267 ];
268 let verifier = OutputVerifier::new(&datadir, None);
269 assert!(verifier.verify(&outputs).is_err(), "invalid paths are never verified");
270 verifier.cleanup(&outputs);
271
272 assert_eq!(fs::read(&outside).unwrap(), b"keep");
273 assert!(!inside.exists());
274 }
275
276 #[cfg(unix)]
277 #[test]
278 fn cleanup_does_not_follow_symlinked_parents() {
279 let root = tempfile::tempdir().unwrap();
280 let datadir = root.path().join("datadir");
281 let static_dir = root.path().join("static");
282 let outside = root.path().join("outside");
283 fs::create_dir_all(&datadir).unwrap();
284 fs::create_dir_all(&static_dir).unwrap();
285 fs::create_dir_all(outside.join("nested")).unwrap();
286 for path in ["mdbx.dat", "headers", "nested/headers"] {
287 fs::write(outside.join(path), b"keep").unwrap();
288 }
289 std::os::unix::fs::symlink(&outside, datadir.join("db")).unwrap();
290 std::os::unix::fs::symlink(&outside, static_dir.join("link")).unwrap();
291 std::os::unix::fs::symlink(&outside, static_dir.join("nested")).unwrap();
292
293 let outputs = [
294 checksum("db/mdbx.dat", b"keep"),
295 checksum("static_files/link/headers", b"keep"),
296 checksum("static_files/nested/nested/headers", b"keep"),
297 ];
298 for static_files_dir in [None, Some(static_dir.as_path())] {
299 OutputVerifier::new(&datadir, static_files_dir).cleanup(&outputs);
300 }
301
302 for path in ["mdbx.dat", "headers", "nested/headers"] {
303 assert_eq!(fs::read(outside.join(path)).unwrap(), b"keep");
304 }
305
306 std::os::unix::fs::symlink(outside.join("mdbx.dat"), datadir.join("link")).unwrap();
308 OutputVerifier::new(&datadir, None).cleanup(&[checksum("link", b"keep")]);
309 assert!(datadir.join("link").symlink_metadata().is_err());
310 assert_eq!(fs::read(outside.join("mdbx.dat")).unwrap(), b"keep");
311 }
312}