use std::io::Write;
use std::ops::ControlFlow;
use md5::{Digest, Md5};
use sha1::Sha1;
use sha2::Sha256;
use crate::{
ComputedHashes, EwfError, HashAlgorithm, HashComparison, HashReference, Image, Result,
SingleFileEntry, SingleFileEntryType,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct SingleFileProgress {
pub bytes_processed: u64,
pub bytes_total: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct SingleFileVerification {
pub hashes: ComputedHashes,
pub comparisons: Vec<HashComparison>,
pub bytes_verified: u64,
}
impl SingleFileVerification {
pub fn references_match(&self) -> Option<bool> {
(!self.comparisons.is_empty()).then(|| self.comparisons.iter().all(|c| c.matches))
}
}
impl Image {
pub fn verify_single_file(&self, entry: &SingleFileEntry) -> Result<SingleFileVerification> {
self.verify_single_file_with_progress(entry, |_| ControlFlow::Continue(()))
}
pub fn verify_single_file_with_progress(
&self,
entry: &SingleFileEntry,
progress: impl FnMut(SingleFileProgress) -> ControlFlow<()>,
) -> Result<SingleFileVerification> {
self.copy_single_file_with_progress(entry, &mut std::io::sink(), progress)
}
pub fn copy_single_file_with_progress(
&self,
entry: &SingleFileEntry,
output: &mut impl Write,
mut progress: impl FnMut(SingleFileProgress) -> ControlFlow<()>,
) -> Result<SingleFileVerification> {
if entry.entry_type() != Some(SingleFileEntryType::File) {
return Err(EwfError::Unsupported(
"entry is not a regular logical file".into(),
));
}
self.read_single_file_at_strict(entry, &mut [], 0)?;
let references = [
(HashAlgorithm::Md5, "MD5", entry.md5.as_deref()),
(HashAlgorithm::Sha1, "SHA1", entry.sha1.as_deref()),
];
for (_, name, value) in references {
if let Some(value) = value {
crate::hashes::validate_digest(name, value)?;
}
}
let total = crate::image::single_file_size(entry)?;
let mut offset = 0;
let mut buffer = vec![0; 1024 * 1024];
let mut md5 = Md5::new();
let mut sha1 = Sha1::new();
let mut sha256 = Sha256::new();
loop {
if progress(SingleFileProgress {
bytes_processed: offset,
bytes_total: total,
})
.is_break()
{
return Err(EwfError::Aborted);
}
if offset == total {
break;
}
let length = (total - offset).min(buffer.len() as u64) as usize;
let read = self.read_single_file_at_strict(entry, &mut buffer[..length], offset)?;
if read != length {
return Err(EwfError::Malformed(
"logical file read was truncated".into(),
));
}
output.write_all(&buffer[..read])?;
md5.update(&buffer[..read]);
sha1.update(&buffer[..read]);
sha256.update(&buffer[..read]);
offset += read as u64;
}
let hashes = ComputedHashes {
md5: md5.finalize().into(),
sha1: sha1.finalize().into(),
sha256: sha256.finalize().into(),
};
let mut comparisons = Vec::new();
if let Some(value) = &entry.md5 {
let expected = crate::types::parse_hex_array::<16>(value).expect("validated digest");
comparisons.push(HashComparison {
algorithm: HashAlgorithm::Md5,
reference: HashReference::Stored,
expected: expected.to_vec(),
computed: hashes.md5.to_vec(),
matches: expected == hashes.md5,
});
}
if let Some(value) = &entry.sha1 {
let expected = crate::types::parse_hex_array::<20>(value).expect("validated digest");
comparisons.push(HashComparison {
algorithm: HashAlgorithm::Sha1,
reference: HashReference::Stored,
expected: expected.to_vec(),
computed: hashes.sha1.to_vec(),
matches: expected == hashes.sha1,
});
}
Ok(SingleFileVerification {
hashes,
comparisons,
bytes_verified: offset,
})
}
}