use std::ops::ControlFlow;
use std::path::Path;
use crate::{
ComputedHashes, EwfError, EwfPassword, HashAlgorithm, HashComparison, HashReference, Image,
OpenStrictness, Result, SectionInfo, SectionKind, VerifyOptions, VerifyProgress,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum IntegritySeverity {
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum IntegrityFindingKind {
InvalidStructure,
InvalidSignature,
UnsupportedFeature,
PasswordRequired,
DecryptionFailed,
ChunkUnreadable,
RedundantTableMismatch,
TableCheckUnavailable,
MissingStoredHashes,
AcquisitionErrors,
IncompleteAcquisition,
LenientOpen,
HashMismatch {
algorithm: HashAlgorithm,
reference: HashReference,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct IntegrityFinding {
pub kind: IntegrityFindingKind,
pub severity: IntegritySeverity,
pub message: String,
pub segment_index: Option<usize>,
pub segment_offset: Option<u64>,
pub chunk_index: Option<u64>,
pub logical_offset: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum MediaScanStatus {
Complete,
Incomplete,
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct IntegrityReport {
pub media_status: MediaScanStatus,
pub bytes_verified: u64,
pub bytes_total: Option<u64>,
pub hashes: Option<ComputedHashes>,
pub comparisons: Vec<HashComparison>,
pub findings: Vec<IntegrityFinding>,
pub suppressed_findings: u64,
pub error_count: u64,
pub warning_count: u64,
}
impl IntegrityReport {
fn new() -> Self {
Self {
media_status: MediaScanStatus::Unavailable,
bytes_verified: 0,
bytes_total: None,
hashes: None,
comparisons: Vec::new(),
findings: Vec::new(),
suppressed_findings: 0,
error_count: 0,
warning_count: 0,
}
}
fn push(&mut self, finding: IntegrityFinding, limit: usize) {
match finding.severity {
IntegritySeverity::Error => self.error_count += 1,
IntegritySeverity::Warning => self.warning_count += 1,
}
if self.findings.len() < limit {
self.findings.push(finding);
} else {
self.suppressed_findings += 1;
}
}
}
fn finding(
kind: IntegrityFindingKind,
severity: IntegritySeverity,
message: impl Into<String>,
) -> IntegrityFinding {
IntegrityFinding {
kind,
severity,
message: message.into(),
segment_index: None,
segment_offset: None,
chunk_index: None,
logical_offset: None,
}
}
pub fn analyze_path(path: impl AsRef<Path>, options: &VerifyOptions) -> Result<IntegrityReport> {
options.validate()?;
analyze_open_result(Image::open(path), options)
}
pub fn analyze_path_with_progress(
path: impl AsRef<Path>,
options: &VerifyOptions,
progress: impl FnMut(VerifyProgress) -> ControlFlow<()>,
) -> Result<IntegrityReport> {
options.validate()?;
let opened = Image::open_with_options(
path,
crate::OpenOptions::default()
.with_chunk_cache_size(0)
.with_maximum_open_handles(Some(16)),
);
analyze_open_with_progress(opened, options, progress)
}
pub fn analyze_path_with_password(
path: impl AsRef<Path>,
options: &VerifyOptions,
password: &EwfPassword,
) -> Result<IntegrityReport> {
options.validate()?;
analyze_open_result(Image::open_with_password(path, password), options)
}
pub fn analyze_path_with_progress_and_password(
path: impl AsRef<Path>,
options: &VerifyOptions,
password: &EwfPassword,
progress: impl FnMut(VerifyProgress) -> ControlFlow<()>,
) -> Result<IntegrityReport> {
options.validate()?;
let opened = Image::open_with_options_and_password(
path,
crate::OpenOptions::default()
.with_chunk_cache_size(0)
.with_maximum_open_handles(Some(16)),
password,
);
analyze_open_with_progress(opened, options, progress)
}
fn analyze_open_result(opened: Result<Image>, options: &VerifyOptions) -> Result<IntegrityReport> {
analyze_open_with_progress(opened, options, |_| ControlFlow::Continue(()))
}
fn analyze_open_with_progress(
opened: Result<Image>,
options: &VerifyOptions,
progress: impl FnMut(VerifyProgress) -> ControlFlow<()>,
) -> Result<IntegrityReport> {
match opened {
Ok(image) => image.analyze_with_progress(options, progress),
Err(error) => {
let kind = match &error {
EwfError::Malformed(_) | EwfError::BufferTooShort { .. } => {
IntegrityFindingKind::InvalidStructure
}
EwfError::InvalidSignature => IntegrityFindingKind::InvalidSignature,
EwfError::Unsupported(_) => IntegrityFindingKind::UnsupportedFeature,
EwfError::PasswordRequired => IntegrityFindingKind::PasswordRequired,
EwfError::PasswordRejected | EwfError::DecryptionValidationFailed => {
IntegrityFindingKind::DecryptionFailed
}
EwfError::Io(io) if io.kind() == std::io::ErrorKind::UnexpectedEof => {
IntegrityFindingKind::InvalidStructure
}
_ => return Err(error),
};
let mut report = IntegrityReport::new();
report.push(
finding(kind, IntegritySeverity::Error, error.to_string()),
options.maximum_findings(),
);
Ok(report)
}
}
}
impl Image {
pub fn analyze(&self, options: &VerifyOptions) -> Result<IntegrityReport> {
self.analyze_with_progress(options, |_| ControlFlow::Continue(()))
}
pub fn analyze_with_progress(
&self,
options: &VerifyOptions,
progress: impl FnMut(VerifyProgress) -> ControlFlow<()>,
) -> Result<IntegrityReport> {
options.validate()?;
self.ensure_not_aborted()?;
let limit = options.maximum_findings();
let mut report = IntegrityReport::new();
report.bytes_total = Some(self.media_size());
if self.open_strictness() == OpenStrictness::Lenient {
report.push(
finding(
IntegrityFindingKind::LenientOpen,
IntegritySeverity::Warning,
"image was opened with lenient structural validation",
),
limit,
);
}
if !self.info().acquisition_complete {
report.push(
finding(
IntegrityFindingKind::IncompleteAcquisition,
IntegritySeverity::Warning,
"acquisition is incomplete",
),
limit,
);
}
if !self.acquisition_errors().is_empty() {
report.push(
finding(
IntegrityFindingKind::AcquisitionErrors,
IntegritySeverity::Warning,
format!(
"{} acquisition error ranges are recorded",
self.acquisition_errors().len()
),
),
limit,
);
}
if self.md5_hash().is_none()
&& self.sha1_hash().is_none()
&& self.hash_value("SHA256").is_none()
{
report.push(
finding(
IntegrityFindingKind::MissingStoredHashes,
IntegritySeverity::Warning,
"no embedded MD5, SHA1, or SHA256 reference is available",
),
limit,
);
}
self.analyze_redundant_tables(&mut report, limit)?;
let scan = self.scan_media(options, progress, |id, error| {
let mut item = finding(
IntegrityFindingKind::ChunkUnreadable,
IntegritySeverity::Error,
error.to_string(),
);
item.chunk_index = Some(id);
item.logical_offset = Some(id * self.chunk_size());
item.segment_index = self.segment_filename_for_chunk(id).ok().and_then(|path| {
self.segment_filenames()
.iter()
.position(|candidate| candidate == path)
});
report.push(item, limit);
Ok(())
})?;
report.bytes_verified = scan.progress.bytes_verified;
report.media_status = if scan.hashes.is_some() {
MediaScanStatus::Complete
} else {
MediaScanStatus::Incomplete
};
if let Some(hashes) = &scan.hashes {
report.comparisons = self.compare_hashes(hashes, options);
let mismatches: Vec<_> = report
.comparisons
.iter()
.filter(|item| !item.matches)
.map(|item| {
finding(
IntegrityFindingKind::HashMismatch {
algorithm: item.algorithm,
reference: item.reference,
},
IntegritySeverity::Error,
format!(
"{:?} differs from {:?} reference",
item.algorithm, item.reference
),
)
})
.collect();
for item in mismatches {
report.push(item, limit);
}
}
report.hashes = scan.hashes;
Ok(report)
}
fn analyze_redundant_tables(&self, report: &mut IntegrityReport, limit: usize) -> Result<()> {
let mut primary: Option<&SectionInfo> = None;
for section in self.sections() {
self.ensure_not_aborted()?;
match §ion.kind {
SectionKind::Ewf1(name) if name == "table" => primary = Some(section),
SectionKind::Ewf1(name) if name == "table2" => {
if let Some(table) = primary
.take()
.filter(|table| table.segment_index == section.segment_index)
{
match self.compare_table_entries(table, section) {
Ok(Some(offset)) => {
let mut item = finding(
IntegrityFindingKind::RedundantTableMismatch,
IntegritySeverity::Error,
"primary and redundant chunk table entries differ",
);
item.segment_index = Some(section.segment_index);
item.segment_offset = Some(offset);
report.push(item, limit);
}
Err(EwfError::Aborted) => return Err(EwfError::Aborted),
Err(error) => {
let mut item = finding(
IntegrityFindingKind::TableCheckUnavailable,
IntegritySeverity::Error,
error.to_string(),
);
item.segment_index = Some(section.segment_index);
item.segment_offset = Some(section.descriptor_offset);
report.push(item, limit);
}
Ok(None) => {}
}
}
}
_ => {}
}
}
Ok(())
}
fn compare_table_entries(
&self,
primary: &SectionInfo,
redundant: &SectionInfo,
) -> Result<Option<u64>> {
let left =
self.read_segment_range(primary.segment_index, primary.data_offset, 24, false)?;
let right =
self.read_segment_range(redundant.segment_index, redundant.data_offset, 24, false)?;
if left[..4] != right[..4] || left[8..16] != right[8..16] {
return Ok(None);
}
let size = u64::from(u32::from_le_bytes(
left[..4].try_into().expect("header read"),
)) * 4;
if size > primary.data_size.saturating_sub(24)
|| size > redundant.data_size.saturating_sub(24)
{
return Err(EwfError::Malformed(
"redundant table entries exceed section payload".into(),
));
}
let mut offset = 0;
while offset < size {
self.ensure_not_aborted()?;
let take = (size - offset).min(64 * 1024);
let a = self.read_segment_range(
primary.segment_index,
primary.data_offset + 24 + offset,
take,
false,
)?;
let b = self.read_segment_range(
redundant.segment_index,
redundant.data_offset + 24 + offset,
take,
false,
)?;
if let Some(difference) = a.iter().zip(&b).position(|(a, b)| a != b) {
return Ok(Some(
redundant.data_offset + 24 + offset + difference as u64,
));
}
offset += take;
}
Ok(None)
}
}