Skip to main content

reth_cli_commands/download/
verify.rs

1use super::{manifest::OutputFileChecksum, progress::ArchiveVerificationProgress};
2use blake3::Hasher;
3use eyre::Result;
4use reth_fs_util as fs;
5use std::{io::Read, path::Path};
6
7/// Verifies and cleans up extracted output files in one target directory.
8pub(crate) struct OutputVerifier<'a> {
9    /// Directory containing the output files declared by the manifest.
10    target_dir: &'a Path,
11    /// Custom location of static file outputs.
12    static_files_dir: Option<&'a Path>,
13}
14
15impl<'a> OutputVerifier<'a> {
16    /// Creates a verifier for one extraction target directory.
17    pub(crate) const fn new(target_dir: &'a Path, static_files_dir: Option<&'a Path>) -> Self {
18        Self { target_dir, static_files_dir }
19    }
20
21    /// Returns `true` only when every declared output file exists and matches size and BLAKE3.
22    /// Returns `false` if any file is missing, mismatched, or no outputs were declared.
23    pub(crate) fn verify(&self, output_files: &[OutputFileChecksum]) -> Result<bool> {
24        self.verify_with_progress(output_files, None)
25    }
26
27    /// Returns `true` only when every declared output file exists and matches size and BLAKE3,
28    /// updating the optional verification progress as file bytes are hashed.
29    pub(crate) fn verify_with_progress(
30        &self,
31        output_files: &[OutputFileChecksum],
32        mut progress: Option<&mut ArchiveVerificationProgress<'_>>,
33    ) -> Result<bool> {
34        if output_files.is_empty() {
35            return Ok(false);
36        }
37
38        for expected in output_files {
39            let output_path = self.output_path(&expected.path);
40            let meta = match fs::metadata(&output_path) {
41                Ok(meta) => meta,
42                Err(_) => return Ok(false),
43            };
44            if meta.len() != expected.size {
45                return Ok(false);
46            }
47
48            let actual = Self::file_blake3_hex(&output_path, progress.as_deref_mut())?;
49            if !actual.eq_ignore_ascii_case(&expected.blake3) {
50                return Ok(false);
51            }
52        }
53
54        Ok(true)
55    }
56
57    /// Removes any declared output files so a fresh archive attempt can restart cleanly.
58    pub(crate) fn cleanup(&self, output_files: &[OutputFileChecksum]) {
59        for output in output_files {
60            let _ = fs::remove_file(self.output_path(&output.path));
61        }
62    }
63
64    /// Resolves archive paths consistently for verification and retry cleanup.
65    fn output_path(&self, path: &str) -> std::path::PathBuf {
66        if let Some(static_files_dir) = self.static_files_dir &&
67            let Some(relative_path) = super::extract::static_file_relative_path(Path::new(path))
68        {
69            return static_files_dir.join(relative_path)
70        }
71        self.target_dir.join(path)
72    }
73
74    /// Computes the hex-encoded BLAKE3 checksum for one plain output file.
75    fn file_blake3_hex(
76        path: &Path,
77        mut progress: Option<&mut ArchiveVerificationProgress<'_>>,
78    ) -> Result<String> {
79        let mut file = fs::open(path)?;
80        let mut hasher = Hasher::new();
81        let mut buf = [0_u8; 64 * 1024];
82
83        loop {
84            let n = file.read(&mut buf)?;
85            if n == 0 {
86                break;
87            }
88            hasher.update(&buf[..n]);
89            if let Some(progress) = progress.as_deref_mut() {
90                progress.record_verified(n as u64);
91            }
92        }
93
94        Ok(hasher.finalize().to_hex().to_string())
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn custom_static_files_verification_and_cleanup() {
104        let datadir = tempfile::tempdir().unwrap();
105        let static_dir = tempfile::tempdir().unwrap();
106        let default_file = datadir.path().join("static_files/headers");
107        fs::create_dir_all(default_file.parent().unwrap()).unwrap();
108        fs::write(&default_file, b"headers").unwrap();
109        let outputs = [OutputFileChecksum {
110            path: "./static_files/headers".into(),
111            size: 7,
112            blake3: blake3::hash(b"headers").to_hex().to_string(),
113        }];
114        let verifier = OutputVerifier::new(datadir.path(), Some(static_dir.path()));
115        assert!(!verifier.verify(&outputs).unwrap());
116        let custom_file = static_dir.path().join("headers");
117        fs::write(&custom_file, b"headers").unwrap();
118        assert!(verifier.verify(&outputs).unwrap());
119        verifier.cleanup(&outputs);
120        assert!(!custom_file.exists());
121        assert_eq!(fs::read(default_file).unwrap(), b"headers");
122    }
123}