use std::io::{self, Read, Seek, SeekFrom};
use vmdk::header::{self, SparseExtentHeader};
use vmdk::sesparse::{self, SeConstHeader};
pub use vmdk::VmdkReader;
const SECTOR_SIZE: u64 = 512;
const MAX_GD_BYTES: u64 = 16 * 1024 * 1024;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IntegrityReport {
pub grains_checked: u64,
pub out_of_bounds_grains: u64,
pub out_of_bounds_grain_tables: u64,
}
impl IntegrityReport {
#[must_use]
pub fn is_ok(&self) -> bool {
self.out_of_bounds_grains == 0 && self.out_of_bounds_grain_tables == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GdRecoveryReport {
pub has_rgd: bool,
pub total_entries: usize,
pub primary_intact: usize,
pub primary_damaged: usize,
pub recoverable_via_rgd: usize,
pub unrecoverable: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(clippy::struct_excessive_bools)] pub struct HeaderProvenance {
pub version: u32,
pub unclean_shutdown: bool,
pub newline_check_intact: bool,
pub uses_redundant_gd: bool,
pub compressed: bool,
pub has_markers: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Severity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AnomalyKind {
UncleanShutdown,
FtpAsciiMangled,
RedundantGdMismatch,
DanglingGrainTable,
DanglingGrain,
PrimaryGdRecoverableViaRgd,
PrimaryGdUnrecoverable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VmdkAnomaly {
pub severity: Severity,
pub kind: AnomalyKind,
pub detail: String,
}
struct SparseLayout {
grain_dir: Vec<u32>,
rgd_offset: u64,
num_gtes_per_gt: u64,
grain_size_bytes: u64,
gd_entry_count: usize,
file_len: u64,
}
pub struct VmdkIntegrity<R: Read + Seek> {
inner: R,
}
impl<R: Read + Seek> VmdkIntegrity<R> {
pub fn new(reader: R) -> Self {
Self { inner: reader }
}
pub fn into_inner(self) -> R {
self.inner
}
fn file_len(&mut self) -> io::Result<u64> {
self.inner.seek(SeekFrom::End(0))
}
fn read_at(&mut self, offset: u64, len: usize) -> io::Result<Vec<u8>> {
self.inner.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; len];
self.inner.read_exact(&mut buf)?;
Ok(buf)
}
fn sparse_layout(&mut self) -> io::Result<Option<SparseLayout>> {
let file_len = self.file_len()?;
if file_len < SECTOR_SIZE {
return Ok(None);
}
let hdr_bytes = self.read_at(0, SECTOR_SIZE as usize)?;
let Ok(hdr) = SparseExtentHeader::parse(&hdr_bytes) else {
return Ok(None);
};
let num_grains = hdr.capacity.div_ceil(hdr.grain_size);
let num_gtes = u64::from(hdr.num_gtes_per_gt);
let num_gts = num_grains.div_ceil(num_gtes);
let gd_byte_len = num_gts.saturating_mul(4);
if gd_byte_len > MAX_GD_BYTES {
return Ok(None);
}
let gd_byte_len = gd_byte_len as usize;
let gd_offset = if hdr.gd_offset == header::GD_AT_END {
if file_len < 1024 {
return Ok(None);
}
let footer = self.read_at(file_len - 1024, SECTOR_SIZE as usize)?;
match SparseExtentHeader::parse(&footer) {
Ok(fh) => fh.gd_offset,
Err(_) => return Ok(None),
}
} else {
hdr.gd_offset
};
let gd_byte = gd_offset.saturating_mul(SECTOR_SIZE);
if gd_byte.saturating_add(gd_byte_len as u64) > file_len {
return Ok(None);
}
let gd = self.read_at(gd_byte, gd_byte_len)?;
let grain_dir: Vec<u32> = gd
.chunks_exact(4)
.map(|c| u32::from_le_bytes(c.try_into().expect("4 bytes")))
.collect();
Ok(Some(SparseLayout {
grain_dir,
rgd_offset: hdr.rgd_offset,
num_gtes_per_gt: num_gtes,
grain_size_bytes: hdr.grain_size.saturating_mul(SECTOR_SIZE),
gd_entry_count: num_gts as usize,
file_len,
}))
}
fn read_grain_table_bytes(
&mut self,
gt_sector: u32,
gt_byte_len: usize,
file_len: u64,
) -> io::Result<Option<Vec<u8>>> {
let gt_byte = u64::from(gt_sector) * SECTOR_SIZE;
if gt_byte.saturating_add(gt_byte_len as u64) > file_len {
return Ok(None);
}
Ok(Some(self.read_at(gt_byte, gt_byte_len)?))
}
fn read_rgd(&mut self, layout: &SparseLayout) -> io::Result<Option<Vec<u32>>> {
if layout.rgd_offset == 0 || layout.rgd_offset == header::GD_AT_END {
return Ok(None);
}
let rgd_byte = layout.rgd_offset.saturating_mul(SECTOR_SIZE);
let len = layout.gd_entry_count.saturating_mul(4) as u64;
if rgd_byte.saturating_add(len) > layout.file_len {
return Ok(None);
}
let bytes = self.read_at(rgd_byte, len as usize)?;
Ok(Some(
bytes
.chunks_exact(4)
.map(|c| u32::from_le_bytes(c.try_into().expect("4 bytes")))
.collect(),
))
}
pub fn validate_rgd(&mut self) -> io::Result<bool> {
let Some(layout) = self.sparse_layout()? else {
return Ok(false);
};
let Some(rgd) = self.read_rgd(&layout)? else {
return Ok(false);
};
let gt_byte_len = (layout.num_gtes_per_gt * 4) as usize;
for i in 0..layout.gd_entry_count {
let p = layout.grain_dir.get(i).copied().unwrap_or(0);
let r = rgd.get(i).copied().unwrap_or(0);
if p == 0 && r == 0 {
continue;
}
if (p == 0) != (r == 0) {
return Ok(false);
}
let pgt = self.read_grain_table_bytes(p, gt_byte_len, layout.file_len)?;
let rgt = self.read_grain_table_bytes(r, gt_byte_len, layout.file_len)?;
match (pgt, rgt) {
(Some(a), Some(b)) if a == b => {}
_ => return Ok(false),
}
}
Ok(true)
}
pub fn grain_directory_recovery(&mut self) -> io::Result<GdRecoveryReport> {
let Some(layout) = self.sparse_layout()? else {
return Ok(GdRecoveryReport::default());
};
let rgd = self.read_rgd(&layout)?;
let Some(rgd) = rgd else {
if layout.rgd_offset == 0 || layout.rgd_offset == header::GD_AT_END {
return Ok(GdRecoveryReport::default());
}
let mut report = GdRecoveryReport {
has_rgd: true,
total_entries: layout.gd_entry_count,
..GdRecoveryReport::default()
};
for &p in &layout.grain_dir {
if Self::in_bounds(p, layout.num_gtes_per_gt, layout.file_len) || p == 0 {
report.primary_intact += 1;
} else {
report.primary_damaged += 1;
report.unrecoverable += 1;
}
}
return Ok(report);
};
let mut report = GdRecoveryReport {
has_rgd: true,
total_entries: layout.gd_entry_count,
..GdRecoveryReport::default()
};
for i in 0..layout.gd_entry_count {
let p = layout.grain_dir.get(i).copied().unwrap_or(0);
let r = rgd.get(i).copied().unwrap_or(0);
let p_ok = Self::in_bounds(p, layout.num_gtes_per_gt, layout.file_len);
if p_ok || (p == 0 && r == 0) {
report.primary_intact += 1;
} else {
report.primary_damaged += 1;
if Self::in_bounds(r, layout.num_gtes_per_gt, layout.file_len) {
report.recoverable_via_rgd += 1;
} else {
report.unrecoverable += 1;
}
}
}
Ok(report)
}
fn in_bounds(ptr: u32, num_gtes_per_gt: u64, file_len: u64) -> bool {
ptr != 0
&& u64::from(ptr)
.saturating_mul(SECTOR_SIZE)
.saturating_add(num_gtes_per_gt * 4)
<= file_len
}
pub fn check_integrity(&mut self) -> io::Result<IntegrityReport> {
let file_len = self.file_len()?;
if file_len < SECTOR_SIZE {
return Ok(IntegrityReport::default());
}
let head = self.read_at(0, 8)?;
let magic8 = u64::from_le_bytes(head.try_into().expect("8 bytes"));
if magic8 == sesparse::SE_CONST_MAGIC {
return self.check_integrity_sesparse(file_len);
}
let Some(layout) = self.sparse_layout()? else {
return Ok(IntegrityReport::default());
};
let mut report = IntegrityReport::default();
let gt_byte_len = layout.num_gtes_per_gt * 4;
for >_sector in &layout.grain_dir {
if gt_sector == 0 {
continue;
}
let gt_byte = u64::from(gt_sector) * SECTOR_SIZE;
if gt_byte.saturating_add(gt_byte_len) > file_len {
report.out_of_bounds_grain_tables += 1;
continue;
}
let gt = self.read_at(gt_byte, gt_byte_len as usize)?;
for c in gt.chunks_exact(4) {
let gte = u32::from_le_bytes(c.try_into().expect("4 bytes"));
if gte <= 1 {
continue; }
report.grains_checked += 1;
let grain_byte = u64::from(gte) * SECTOR_SIZE;
if grain_byte.saturating_add(layout.grain_size_bytes) > file_len {
report.out_of_bounds_grains += 1;
}
}
}
Ok(report)
}
fn check_integrity_sesparse(&mut self, file_len: u64) -> io::Result<IntegrityReport> {
let mut report = IntegrityReport::default();
let hdr_bytes = self.read_at(0, SECTOR_SIZE as usize)?;
let Ok(hdr) = SeConstHeader::parse(&hdr_bytes) else {
return Ok(report);
};
if hdr.grain_size == 0 {
return Ok(report);
}
let num_grains = hdr.capacity.div_ceil(hdr.grain_size);
let num_gts = num_grains.div_ceil(sesparse::SE_GTES_PER_GT).max(1);
let gd_len = num_gts.saturating_mul(8);
let gd_byte = hdr.gd_offset.saturating_mul(SECTOR_SIZE);
if gd_len > MAX_GD_BYTES || gd_byte.saturating_add(gd_len) > file_len {
return Ok(report);
}
let gd = self.read_at(gd_byte, gd_len as usize)?;
let grain_dir: Vec<u64> = gd
.chunks_exact(8)
.map(|c| u64::from_le_bytes(c.try_into().expect("8 bytes")))
.collect();
let grain_size_bytes = hdr.grain_size.saturating_mul(SECTOR_SIZE);
let grain_sectors = hdr.grain_size;
let gt_byte_len = sesparse::SE_GTES_PER_GT * 8;
for &gd_entry in &grain_dir {
if gd_entry == 0 {
continue;
}
if gd_entry & sesparse::SE_GD_ALLOC_MASK != sesparse::SE_GD_ALLOC_FLAG {
report.out_of_bounds_grain_tables += 1;
continue;
}
let gt_table_idx = gd_entry & sesparse::SE_GD_INDEX_MASK;
let gt_sector = hdr
.gt_offset
.saturating_add(gt_table_idx.saturating_mul(sesparse::SE_GT_SECTORS));
let gt_byte = gt_sector.saturating_mul(SECTOR_SIZE);
if gt_byte.saturating_add(gt_byte_len) > file_len {
report.out_of_bounds_grain_tables += 1;
continue;
}
let gt = self.read_at(gt_byte, gt_byte_len as usize)?;
for c in gt.chunks_exact(8) {
let gte = u64::from_le_bytes(c.try_into().expect("8 bytes"));
if gte & sesparse::SE_GTE_TYPE_MASK != sesparse::SE_GTE_TYPE_ALLOCATED {
continue;
}
report.grains_checked += 1;
let grain_idx = sesparse::se_gte_grain_index(gte);
let grain_byte = hdr
.grains_offset
.saturating_add(grain_idx.saturating_mul(grain_sectors))
.saturating_mul(SECTOR_SIZE);
if grain_byte.saturating_add(grain_size_bytes) > file_len {
report.out_of_bounds_grains += 1;
}
}
}
Ok(report)
}
pub fn header_provenance(&mut self) -> io::Result<Option<HeaderProvenance>> {
let file_len = self.file_len()?;
if file_len < SECTOR_SIZE {
return Ok(None);
}
let hdr = self.read_at(0, SECTOR_SIZE as usize)?;
if u32::from_le_bytes(hdr[0..4].try_into().expect("4 bytes")) != header::MAGIC {
return Ok(None);
}
let version = u32::from_le_bytes(hdr[4..8].try_into().expect("4 bytes"));
let flags = u32::from_le_bytes(hdr[8..12].try_into().expect("4 bytes"));
Ok(Some(HeaderProvenance {
version,
unclean_shutdown: hdr[72] != 0,
newline_check_intact: hdr[73..77] == [0x0A, 0x20, 0x0D, 0x0A],
uses_redundant_gd: flags & 0x0000_0002 != 0,
compressed: flags & 0x0001_0000 != 0,
has_markers: flags & 0x0002_0000 != 0,
}))
}
pub fn analyse(&mut self) -> io::Result<Vec<VmdkAnomaly>> {
let mut out = Vec::new();
if let Some(p) = self.header_provenance()? {
if p.unclean_shutdown {
out.push(VmdkAnomaly {
severity: Severity::Warning,
kind: AnomalyKind::UncleanShutdown,
detail: "uncleanShutdown flag set — the disk was not closed cleanly; \
in-flight writes may be inconsistent"
.to_string(),
});
}
if !p.newline_check_intact {
out.push(VmdkAnomaly {
severity: Severity::Error,
kind: AnomalyKind::FtpAsciiMangled,
detail: "header newline-detection bytes mangled — the image was likely \
corrupted by an ASCII-mode FTP transfer"
.to_string(),
});
}
}
let recovery = self.grain_directory_recovery()?;
if recovery.has_rgd && !self.validate_rgd()? {
out.push(VmdkAnomaly {
severity: Severity::Error,
kind: AnomalyKind::RedundantGdMismatch,
detail: "redundant grain directory diverges from the primary — the grain \
tables they reference hold different contents"
.to_string(),
});
}
if recovery.recoverable_via_rgd > 0 {
out.push(VmdkAnomaly {
severity: Severity::Warning,
kind: AnomalyKind::PrimaryGdRecoverableViaRgd,
detail: format!(
"{} of {} grain-directory entries damaged, {} recoverable via the RGD",
recovery.primary_damaged, recovery.total_entries, recovery.recoverable_via_rgd
),
});
}
if recovery.unrecoverable > 0 {
out.push(VmdkAnomaly {
severity: Severity::Error,
kind: AnomalyKind::PrimaryGdUnrecoverable,
detail: format!(
"{} grain-directory entries damaged with no RGD recovery available",
recovery.unrecoverable
),
});
}
let integrity = self.check_integrity()?;
if integrity.out_of_bounds_grain_tables > 0 {
out.push(VmdkAnomaly {
severity: Severity::Error,
kind: AnomalyKind::DanglingGrainTable,
detail: format!(
"{} grain-table pointer(s) point beyond end-of-file (truncation or tampering)",
integrity.out_of_bounds_grain_tables
),
});
}
if integrity.out_of_bounds_grains > 0 {
out.push(VmdkAnomaly {
severity: Severity::Error,
kind: AnomalyKind::DanglingGrain,
detail: format!(
"{} grain pointer(s) point beyond end-of-file (truncation or tampering)",
integrity.out_of_bounds_grains
),
});
}
out.sort_by_key(|a| std::cmp::Reverse(a.severity));
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use vmdk::testutil::{test_sesparse_vmdk, test_sparse_vmdk};
#[test]
fn header_provenance_clean_image() {
let v = test_sparse_vmdk(&[0u8; 512]);
let mut a = VmdkIntegrity::new(Cursor::new(v));
let p = a.header_provenance().expect("io").expect("VMDK4 header");
assert_eq!(p.version, 1);
assert!(!p.unclean_shutdown);
assert!(p.newline_check_intact);
}
#[test]
fn validate_rgd_true_on_healthy_image() {
let v = test_sparse_vmdk(&[0xAB; 512]);
let mut a = VmdkIntegrity::new(Cursor::new(v));
assert!(a.validate_rgd().expect("io"));
}
#[test]
fn validate_rgd_false_on_redundant_gt_divergence() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[22 * 512] ^= 0xFF;
let mut a = VmdkIntegrity::new(Cursor::new(v));
assert!(!a.validate_rgd().expect("io"));
}
#[test]
fn grain_directory_recovery_flags_recoverable_damage() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[21 * 512..21 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(v));
let r = a.grain_directory_recovery().expect("io");
assert!(r.has_rgd);
assert_eq!(r.primary_damaged, 1);
assert_eq!(r.recoverable_via_rgd, 1);
}
#[test]
fn check_integrity_clean_then_flags_dangling_gt() {
let v = test_sparse_vmdk(&[0xAB; 512]);
let mut a = VmdkIntegrity::new(Cursor::new(v));
assert!(a.check_integrity().expect("io").is_ok());
let mut v2 = test_sparse_vmdk(&[0xAB; 512]);
v2[21 * 512..21 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
let mut a2 = VmdkIntegrity::new(Cursor::new(v2));
let rep = a2.check_integrity().expect("io");
assert!(!rep.is_ok());
assert_eq!(rep.out_of_bounds_grain_tables, 1);
}
#[test]
fn analyse_reports_rgd_mismatch_anomaly() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[22 * 512] ^= 0xFF; let mut a = VmdkIntegrity::new(Cursor::new(v));
let anomalies = a.analyse().expect("io");
assert!(
anomalies
.iter()
.any(|x| matches!(x.kind, AnomalyKind::RedundantGdMismatch)),
"expected an RGD mismatch anomaly, got: {anomalies:?}"
);
}
#[test]
fn into_inner_returns_reader() {
let v = test_sparse_vmdk(&[0u8; 512]);
let a = VmdkIntegrity::new(Cursor::new(v));
let _cursor = a.into_inner();
}
#[test]
fn header_provenance_flags_unclean_shutdown_and_ftp_mangling() {
let mut v = test_sparse_vmdk(&[0u8; 512]);
v[72] = 1; v[73] = 0x20; let mut a = VmdkIntegrity::new(Cursor::new(v));
let p = a.header_provenance().expect("io").expect("vmdk4");
assert!(p.unclean_shutdown);
assert!(!p.newline_check_intact);
}
#[test]
fn header_provenance_none_for_non_vmdk4() {
let mut a = VmdkIntegrity::new(Cursor::new(vec![0u8; 1024]));
assert!(a.header_provenance().expect("io").is_none());
}
#[test]
fn validate_rgd_false_for_sesparse() {
let se = test_sesparse_vmdk(&[0u8; 512]);
let mut a = VmdkIntegrity::new(Cursor::new(se));
assert!(!a.validate_rgd().expect("io")); }
#[test]
fn grain_directory_recovery_default_when_no_rgd() {
let mut v = test_sparse_vmdk(&[0u8; 512]);
v[48..56].copy_from_slice(&0u64.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(v));
let r = a.grain_directory_recovery().expect("io");
assert!(!r.has_rgd);
assert_eq!(r.total_entries, 0);
}
#[test]
fn grain_directory_recovery_counts_unrecoverable() {
let mut v = test_sparse_vmdk(&[0u8; 512]);
v[21 * 512..21 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); v[22 * 512..22 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(v));
let r = a.grain_directory_recovery().expect("io");
assert_eq!(r.primary_damaged, 1);
assert_eq!(r.unrecoverable, 1);
assert_eq!(r.recoverable_via_rgd, 0);
}
#[test]
fn grain_directory_recovery_clean_all_intact() {
let v = test_sparse_vmdk(&[0xAB; 512]);
let mut a = VmdkIntegrity::new(Cursor::new(v));
let r = a.grain_directory_recovery().expect("io");
assert!(r.has_rgd);
assert_eq!(r.primary_intact, r.total_entries);
assert_eq!(r.primary_damaged, 0);
}
#[test]
fn sesparse_integrity_clean_and_flagged() {
let se = test_sesparse_vmdk(&[0xAB; 512]);
let mut a = VmdkIntegrity::new(Cursor::new(se));
assert!(a.check_integrity().expect("io").is_ok());
let mut se2 = test_sesparse_vmdk(&[0xAB; 512]);
let gd = 2 * 512;
se2[gd..gd + 8].copy_from_slice(&0x5000_0000_0000_0000u64.to_le_bytes());
let mut a2 = VmdkIntegrity::new(Cursor::new(se2));
let rep = a2.check_integrity().expect("io");
assert!(!rep.is_ok());
assert_eq!(rep.out_of_bounds_grain_tables, 1);
let mut se3 = test_sesparse_vmdk(&[0xAB; 512]);
se3[gd..gd + 8].copy_from_slice(&(0x1000_0000_0000_0000u64 | 0xFFFF_FFFF).to_le_bytes());
let mut a3 = VmdkIntegrity::new(Cursor::new(se3));
assert!(!a3.check_integrity().expect("io").is_ok());
}
#[test]
fn check_integrity_flags_grain_past_eof() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[23 * 512..23 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
let mut a = VmdkIntegrity::new(Cursor::new(v));
let rep = a.check_integrity().expect("io");
assert_eq!(rep.out_of_bounds_grains, 1);
assert!(!rep.is_ok());
}
#[test]
fn analyse_flags_unclean_shutdown_warning() {
let mut v = test_sparse_vmdk(&[0u8; 512]);
v[72] = 1;
let mut a = VmdkIntegrity::new(Cursor::new(v));
let anomalies = a.analyse().expect("io");
assert!(anomalies
.iter()
.any(|x| matches!(x.kind, AnomalyKind::UncleanShutdown)));
}
#[test]
fn analyse_flags_dangling_and_recoverable_for_corrupt_primary_gd() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[21 * 512..21 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
let mut a = VmdkIntegrity::new(Cursor::new(v));
let k: Vec<_> = a
.analyse()
.expect("io")
.into_iter()
.map(|x| x.kind)
.collect();
assert!(k.contains(&AnomalyKind::DanglingGrainTable));
assert!(k.contains(&AnomalyKind::PrimaryGdRecoverableViaRgd));
}
#[test]
fn analyse_flags_unrecoverable_when_both_directories_damaged() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[21 * 512..21 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
v[22 * 512..22 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
let mut a = VmdkIntegrity::new(Cursor::new(v));
let k: Vec<_> = a
.analyse()
.expect("io")
.into_iter()
.map(|x| x.kind)
.collect();
assert!(k.contains(&AnomalyKind::PrimaryGdUnrecoverable));
}
#[test]
fn tiny_and_garbage_inputs_are_safe() {
for bytes in [vec![0u8; 100], vec![0xFFu8; 512], vec![0u8; 600]] {
let mut a = VmdkIntegrity::new(Cursor::new(bytes));
assert!(!a.validate_rgd().expect("io"));
assert!(a.check_integrity().expect("io").is_ok());
assert!(a.grain_directory_recovery().expect("io").total_entries == 0);
assert!(a.header_provenance().expect("io").is_none());
let _ = a.analyse().expect("io");
}
}
#[test]
fn validate_rgd_false_when_only_one_directory_has_a_gt() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[21 * 512..21 * 512 + 4].copy_from_slice(&0u32.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(v));
assert!(!a.validate_rgd().expect("io"));
}
#[test]
fn grain_directory_recovery_rgd_directory_out_of_bounds_is_unrecoverable() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[21 * 512..21 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); v[48..56].copy_from_slice(&9_999_999u64.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(v));
let r = a.grain_directory_recovery().expect("io");
assert!(r.has_rgd);
assert_eq!(r.unrecoverable, 1);
assert_eq!(r.recoverable_via_rgd, 0);
}
#[test]
fn analyse_flags_ftp_ascii_mangling() {
let mut v = test_sparse_vmdk(&[0u8; 512]);
v[73] = 0x20; let mut a = VmdkIntegrity::new(Cursor::new(v));
let k: Vec<_> = a
.analyse()
.expect("io")
.into_iter()
.map(|x| x.kind)
.collect();
assert!(k.contains(&AnomalyKind::FtpAsciiMangled));
}
#[test]
fn analyse_flags_dangling_grain() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[23 * 512..23 * 512 + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(v));
let k: Vec<_> = a
.analyse()
.expect("io")
.into_iter()
.map(|x| x.kind)
.collect();
assert!(k.contains(&AnomalyKind::DanglingGrain));
}
#[test]
fn sesparse_grain_past_eof_is_flagged() {
let mut se = test_sesparse_vmdk(&[0xAB; 512]);
se[16..24].copy_from_slice(&u64::MAX.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(se));
let _ = a.check_integrity().expect("io"); }
#[test]
fn validate_rgd_false_when_grain_directory_out_of_bounds() {
let mut v = test_sparse_vmdk(&[0u8; 512]);
v[56..64].copy_from_slice(&9_999_999u64.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(v));
assert!(!a.validate_rgd().expect("io"));
assert!(a.check_integrity().expect("io").is_ok()); }
#[test]
fn validate_rgd_false_when_rgd_directory_out_of_bounds() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[48..56].copy_from_slice(&9_999_999u64.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(v));
assert!(!a.validate_rgd().expect("io"));
}
#[test]
fn sesparse_zero_grain_size_and_oob_gd_are_safe() {
let mut se = test_sesparse_vmdk(&[0u8; 512]);
se[24..32].copy_from_slice(&0u64.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(se));
assert!(a.check_integrity().expect("io").is_ok());
let mut se2 = test_sesparse_vmdk(&[0u8; 512]);
se2[128..136].copy_from_slice(&9_999_999u64.to_le_bytes()); let mut a2 = VmdkIntegrity::new(Cursor::new(se2));
assert!(a2.check_integrity().expect("io").is_ok());
}
#[test]
fn streamoptimized_gd_at_end_footer_resolution() {
let mut v = test_sparse_vmdk(&[0xAB; 512]);
v[56..64].copy_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_le_bytes()); let mut footer = v[0..512].to_vec();
footer[56..64].copy_from_slice(&21u64.to_le_bytes()); v.extend_from_slice(&footer);
v.extend_from_slice(&[0u8; 512]); let mut a = VmdkIntegrity::new(Cursor::new(v));
assert!(a.check_integrity().expect("io").is_ok());
assert!(a.validate_rgd().expect("io"));
}
#[test]
fn sesparse_huge_capacity_grain_directory_too_large_is_safe() {
let mut se = test_sesparse_vmdk(&[0u8; 512]);
se[16..24].copy_from_slice(&u64::MAX.to_le_bytes()); let mut a = VmdkIntegrity::new(Cursor::new(se));
assert!(a.check_integrity().expect("io").is_ok()); }
#[test]
fn sesparse_allocated_gte_grain_past_eof_is_flagged() {
let mut se = test_sesparse_vmdk(&[0xAB; 512]);
let gt0 = 3 * 512; se[gt0..gt0 + 8].copy_from_slice(&(0x3000_0000_0000_0000u64 | 0x00FF_FFFF).to_le_bytes());
let mut a = VmdkIntegrity::new(Cursor::new(se));
let _ = a.check_integrity().expect("io"); }
#[test]
fn analyse_clean_image_has_no_error_anomalies() {
let v = test_sparse_vmdk(&[0xAB; 512]);
let mut a = VmdkIntegrity::new(Cursor::new(v));
let anomalies = a.analyse().expect("io");
assert!(anomalies.iter().all(|x| x.severity != Severity::Error));
}
}