Skip to main content

ewf_image/
logical_verify.rs

1//! Strict logical-file streaming and digest comparisons.
2
3use std::io::Write;
4use std::ops::ControlFlow;
5
6use md5::{Digest, Md5};
7use sha1::Sha1;
8use sha2::Sha256;
9
10use crate::{
11    ComputedHashes, EwfError, HashAlgorithm, HashComparison, HashReference, Image, Result,
12    SingleFileEntry, SingleFileEntryType,
13};
14
15/// Progress over one file's bytes, including sparse zeroes.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize))]
18#[non_exhaustive]
19pub struct SingleFileProgress {
20    /// Bytes successfully read and written.
21    pub bytes_processed: u64,
22    /// File size; excludes container padding.
23    pub bytes_total: u64,
24}
25
26/// Completed file-level verification; never certifies the whole container.
27#[derive(Debug, Clone, PartialEq, Eq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize))]
29#[non_exhaustive]
30pub struct SingleFileVerification {
31    /// Hashes of exactly the reconstructed file bytes.
32    pub hashes: ComputedHashes,
33    /// Comparisons against available entry MD5/SHA1 references.
34    pub comparisons: Vec<HashComparison>,
35    /// File bytes verified, excluding container padding.
36    pub bytes_verified: u64,
37}
38
39impl SingleFileVerification {
40    /// `None` means no file reference was stored; it does not mean a match.
41    pub fn references_match(&self) -> Option<bool> {
42        (!self.comparisons.is_empty()).then(|| self.comparisons.iter().all(|c| c.matches))
43    }
44}
45
46impl Image {
47    /// Strictly verifies one regular file against its stored MD5/SHA1 hashes.
48    /// Mismatches are results; malformed digests and unreadable data are errors.
49    /// The caller should supply an entry from this image's catalog.
50    pub fn verify_single_file(&self, entry: &SingleFileEntry) -> Result<SingleFileVerification> {
51        self.verify_single_file_with_progress(entry, |_| ControlFlow::Continue(()))
52    }
53
54    /// Verifies one file, with cooperative cancellation before and after each buffer.
55    pub fn verify_single_file_with_progress(
56        &self,
57        entry: &SingleFileEntry,
58        progress: impl FnMut(SingleFileProgress) -> ControlFlow<()>,
59    ) -> Result<SingleFileVerification> {
60        self.copy_single_file_with_progress(entry, &mut std::io::sink(), progress)
61    }
62
63    /// Copies strictly decoded file bytes and compares stored file hashes.
64    /// Uses bounded buffers and bypasses decoded caches and recovery policies.
65    /// The caller owns output publication: errors, cancellation, or mismatches
66    /// can leave partial or unverified bytes in `output`. No output is flushed
67    /// or synchronized here. Missing references return `None`, not success.
68    pub fn copy_single_file_with_progress(
69        &self,
70        entry: &SingleFileEntry,
71        output: &mut impl Write,
72        mut progress: impl FnMut(SingleFileProgress) -> ControlFlow<()>,
73    ) -> Result<SingleFileVerification> {
74        if entry.entry_type() != Some(SingleFileEntryType::File) {
75            return Err(EwfError::Unsupported(
76                "entry is not a regular logical file".into(),
77            ));
78        }
79        // Honor the image-wide abort flag even for an empty file.
80        self.read_single_file_at_strict(entry, &mut [], 0)?;
81        let references = [
82            (HashAlgorithm::Md5, "MD5", entry.md5.as_deref()),
83            (HashAlgorithm::Sha1, "SHA1", entry.sha1.as_deref()),
84        ];
85        for (_, name, value) in references {
86            if let Some(value) = value {
87                crate::hashes::validate_digest(name, value)?;
88            }
89        }
90        let total = crate::image::single_file_size(entry)?;
91        let mut offset = 0;
92        let mut buffer = vec![0; 1024 * 1024];
93        let mut md5 = Md5::new();
94        let mut sha1 = Sha1::new();
95        let mut sha256 = Sha256::new();
96        loop {
97            if progress(SingleFileProgress {
98                bytes_processed: offset,
99                bytes_total: total,
100            })
101            .is_break()
102            {
103                return Err(EwfError::Aborted);
104            }
105            if offset == total {
106                break;
107            }
108            let length = (total - offset).min(buffer.len() as u64) as usize;
109            let read = self.read_single_file_at_strict(entry, &mut buffer[..length], offset)?;
110            if read != length {
111                return Err(EwfError::Malformed(
112                    "logical file read was truncated".into(),
113                ));
114            }
115            output.write_all(&buffer[..read])?;
116            md5.update(&buffer[..read]);
117            sha1.update(&buffer[..read]);
118            sha256.update(&buffer[..read]);
119            offset += read as u64;
120        }
121        let hashes = ComputedHashes {
122            md5: md5.finalize().into(),
123            sha1: sha1.finalize().into(),
124            sha256: sha256.finalize().into(),
125        };
126        let mut comparisons = Vec::new();
127        if let Some(value) = &entry.md5 {
128            let expected = crate::types::parse_hex_array::<16>(value).expect("validated digest");
129            comparisons.push(HashComparison {
130                algorithm: HashAlgorithm::Md5,
131                reference: HashReference::Stored,
132                expected: expected.to_vec(),
133                computed: hashes.md5.to_vec(),
134                matches: expected == hashes.md5,
135            });
136        }
137        if let Some(value) = &entry.sha1 {
138            let expected = crate::types::parse_hex_array::<20>(value).expect("validated digest");
139            comparisons.push(HashComparison {
140                algorithm: HashAlgorithm::Sha1,
141                reference: HashReference::Stored,
142                expected: expected.to_vec(),
143                computed: hashes.sha1.to_vec(),
144                matches: expected == hashes.sha1,
145            });
146        }
147        Ok(SingleFileVerification {
148            hashes,
149            comparisons,
150            bytes_verified: offset,
151        })
152    }
153}