use vhdx::header::{
crc32c, HEADER1_OFFSET, HEADER2_OFFSET, HEADER_SIZE, REGION_TABLE1_OFFSET, REGION_TABLE2_OFFSET,
};
use crate::integrity::{VhdxIntegrity, VhdxIntegrityAnomaly};
use vhdx::region::REGION_TABLE_CRC_COVERAGE;
#[derive(Debug, Clone)]
pub struct RepairAction {
pub description: String,
pub byte_offset: u64,
pub bytes_changed: usize,
pub disclaimer: &'static str,
}
#[derive(Debug, Clone)]
pub struct CannotRepair {
pub anomaly: VhdxIntegrityAnomaly,
pub reason: &'static str,
}
#[derive(Debug)]
pub struct RepairReport {
pub repaired: Vec<RepairAction>,
pub cannot_repair: Vec<CannotRepair>,
}
impl RepairReport {
pub const DISCLAIMER: &'static str = concat!(
"FORENSIC REPAIR DISCLAIMER: ",
"This image has been modified by an automated repair process. ",
"Repaired bytes no longer reflect the original evidence state. ",
"The original unmodified image MUST be preserved and should be ",
"used as the primary evidence artefact. This repaired copy is ",
"provided solely to enable further analysis of recoverable content. ",
"All repair actions are documented in RepairReport::repaired. ",
"Do NOT submit a repaired image as unmodified forensic evidence.",
);
pub fn any_repaired(&self) -> bool {
!self.repaired.is_empty()
}
pub fn any_unresolved(&self) -> bool {
!self.cannot_repair.is_empty()
}
}
pub struct VhdxRepair {
data: Vec<u8>,
}
impl VhdxRepair {
pub fn new(data: Vec<u8>) -> Self {
Self { data }
}
pub fn attempt_repair(&mut self) -> RepairReport {
let mut repaired = Vec::new();
let mut cannot_repair = Vec::new();
let integrity = VhdxIntegrity::new(&self.data);
let mut issues = integrity.analyse();
issues.extend(integrity.check_bat_ghost_data());
let bat_offset = VhdxIntegrity::new(&self.data).bat_region_offset();
for issue in issues {
let was_repaired = match &issue {
VhdxIntegrityAnomaly::HeaderChecksumMismatch { copy: 1, .. } => {
repaired.push(self.copy_header(2, 1));
true
}
VhdxIntegrityAnomaly::HeaderChecksumMismatch { copy: 2, .. } => {
repaired.push(self.copy_header(1, 2));
true
}
VhdxIntegrityAnomaly::RegionTableChecksumMismatch { copy: 1, .. } => {
repaired.push(self.copy_region_table(2, 1));
true
}
VhdxIntegrityAnomaly::RegionTableChecksumMismatch { copy: 2, .. } => {
repaired.push(self.copy_region_table(1, 2));
true
}
VhdxIntegrityAnomaly::BatEntryBeyondContainer { bat_index, .. } => {
let idx = *bat_index;
if let Some(off) = bat_offset {
repaired.push(self.zero_bat_entry(off, idx));
true
} else {
false
}
}
VhdxIntegrityAnomaly::BatEntryInStructuralRegion { bat_index, .. } => {
let idx = *bat_index;
if let Some(off) = bat_offset {
repaired.push(self.zero_bat_entry(off, idx));
true
} else {
false
}
}
VhdxIntegrityAnomaly::UndefinedBlockState { bat_index } => {
let idx = *bat_index;
if let Some(off) = bat_offset {
repaired.push(self.zero_bat_entry(off, idx));
true
} else {
false
}
}
VhdxIntegrityAnomaly::BatEntryUnaligned { bat_index, .. } => {
let idx = *bat_index;
if let Some(off) = bat_offset {
repaired.push(self.clear_bat_reserved_bits(off, idx));
true
} else {
false
}
}
_ => false,
};
if !was_repaired {
let reason: &'static str = match &issue {
VhdxIntegrityAnomaly::BothHeaderCopiesInvalid => {
"Both header copies are invalid; no valid reference copy exists to restore from"
}
VhdxIntegrityAnomaly::BothRegionTableCopiesInvalid => {
"Both region table copies are invalid; region layout cannot be determined"
}
VhdxIntegrityAnomaly::DirtyLog { .. } => {
"Log replay is required to reach a consistent image state; \
log replay is out of scope for offline forensic analysis — \
mount the image on a running Hyper-V host for automatic replay"
}
VhdxIntegrityAnomaly::BatSizeMetadataMismatch { .. } => {
"Cannot determine which field (VirtualDiskSize or BlockSize) was \
altered without an external reference; altering either would destroy evidence"
}
VhdxIntegrityAnomaly::LogEntryGuidMismatch { .. } => {
"Log was transplanted from a different image; replay would overwrite \
this image's metadata with data from the source image"
}
VhdxIntegrityAnomaly::GhostDataInAbsentBlock { .. } => {
"Absent-block data cannot be zeroed without destroying evidence; \
use a carver to extract the content before repair"
}
VhdxIntegrityAnomaly::MetadataItemsOverlap { .. } => {
"Overlapping metadata items are ambiguous; cannot determine which \
item's data is authoritative without external reference"
}
VhdxIntegrityAnomaly::LogGuidAllZerosWithDirtyLog { .. } => {
"Log structure is internally contradictory; replay is unsafe and \
clearing the log would destroy evidence of the anomaly"
}
VhdxIntegrityAnomaly::BatEntriesOverlap { .. } => {
"Overlapping BAT entries are ambiguous; cannot determine which \
logical block mapping is authoritative without external reference data"
}
VhdxIntegrityAnomaly::HeaderCopyMismatch { .. } => {
"Header field mismatch between copies requires manual forensic \
analysis to determine which copy reflects the intended state"
}
VhdxIntegrityAnomaly::RegionTableCopyMismatch { .. } => {
"Region table field mismatch between copies requires manual forensic \
analysis to determine which copy reflects the intended state"
}
VhdxIntegrityAnomaly::DifferencingDisk => {
"Differencing disk repair requires access to the full parent chain"
}
VhdxIntegrityAnomaly::BatEntryBeyondContainer { .. } => {
"BAT region offset cannot be determined from region tables"
}
_ => "No automated repair strategy is available for this anomaly type",
};
cannot_repair.push(CannotRepair {
anomaly: issue,
reason,
});
}
}
RepairReport {
repaired,
cannot_repair,
}
}
pub fn into_bytes(self) -> Vec<u8> {
self.data
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
}
impl VhdxRepair {
fn copy_block_with_crc(&mut self, src_off: usize, dst_off: usize, size: usize) {
self.data.copy_within(src_off..src_off + size, dst_off);
self.data[dst_off + 4..dst_off + 8].fill(0);
let crc = crc32c(&self.data[dst_off..dst_off + size]);
self.data[dst_off + 4..dst_off + 8].copy_from_slice(&crc.to_le_bytes());
}
fn copy_header(&mut self, src_copy: u8, dst_copy: u8) -> RepairAction {
let src_off = header_offset(src_copy);
let dst_off = header_offset(dst_copy);
self.copy_block_with_crc(src_off, dst_off, HEADER_SIZE);
RepairAction {
description: format!(
"Replaced corrupt header copy {dst_copy} with copy {src_copy} and recomputed CRC32C"
),
byte_offset: dst_off as u64,
bytes_changed: HEADER_SIZE,
disclaimer: "Header copy was replaced with the other valid copy. \
The original corrupt copy may have contained forensically significant modifications.",
}
}
fn copy_region_table(&mut self, src_copy: u8, dst_copy: u8) -> RepairAction {
let src_off = rt_offset(src_copy);
let dst_off = rt_offset(dst_copy);
self.copy_block_with_crc(src_off, dst_off, REGION_TABLE_CRC_COVERAGE);
RepairAction {
description: format!(
"Replaced corrupt region table copy {dst_copy} with copy {src_copy} and recomputed CRC32C"
),
byte_offset: dst_off as u64,
bytes_changed: REGION_TABLE_CRC_COVERAGE,
disclaimer: "Region table copy was replaced with the other valid copy. \
The original corrupt copy may have contained forensically significant modifications.",
}
}
fn clear_bat_reserved_bits(&mut self, bat_offset: u64, bat_index: usize) -> RepairAction {
let byte_pos = bat_offset as usize + bat_index * 8;
let raw = u64::from_le_bytes(self.data[byte_pos..byte_pos + 8].try_into().unwrap());
let cleaned = raw & !0x000F_FFF8u64;
self.data[byte_pos..byte_pos + 8].copy_from_slice(&cleaned.to_le_bytes());
RepairAction {
description: format!(
"BAT entry {bat_index} had non-zero reserved bits — bits [3..19] cleared, \
offset and state preserved"
),
byte_offset: byte_pos as u64,
bytes_changed: 8,
disclaimer: "Reserved bits cleared; payload offset preserved. \
The original reserved-bit pattern may have been forensically significant.",
}
}
fn zero_bat_entry(&mut self, bat_offset: u64, bat_index: usize) -> RepairAction {
let byte_pos = bat_offset as usize + bat_index * 8;
self.data[byte_pos..byte_pos + 8].fill(0);
RepairAction {
description: format!(
"BAT entry {bat_index} pointed outside container — zeroed to NOT_PRESENT"
),
byte_offset: byte_pos as u64,
bytes_changed: 8,
disclaimer: "BAT entry replaced with NOT_PRESENT (0). \
Data the entry supposedly referenced cannot be recovered from this image.",
}
}
}
#[inline]
fn header_offset(copy: u8) -> usize {
if copy == 1 { HEADER1_OFFSET as usize } else { HEADER2_OFFSET as usize }
}
#[inline]
fn rt_offset(copy: u8) -> usize {
if copy == 1 { REGION_TABLE1_OFFSET as usize } else { REGION_TABLE2_OFFSET as usize }
}