use std::io::{self, Read, Seek, SeekFrom};
use crate::tree::TreeNode;
const VMDK_MAGIC: u32 = 0x564d_444b;
const HEADER_SIZE: u64 = 512;
const SECTOR_SIZE: u64 = 512;
#[derive(Debug)]
pub enum Error {
TooShort,
BadMagic,
UnsupportedVersion(u32),
Compressed,
Io(io::Error),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::TooShort => write!(f, "VMDK file is shorter than 512 bytes (no header)"),
Error::BadMagic => write!(
f,
"VMDK sparse extent magic 0x564d444b not found at offset 0"
),
Error::UnsupportedVersion(v) => write!(
f,
"VMDK header version {v} is not supported (only 1 and 3 are)"
),
Error::Compressed => write!(
f,
"VMDK streamOptimized (deflate-compressed) images are not supported"
),
Error::Io(e) => write!(f, "VMDK I/O error: {e}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
struct Header {
capacity: u64,
}
fn read_header<R: Read + Seek>(r: &mut R) -> Result<Header, Error> {
r.seek(SeekFrom::Start(0))?;
let mut buf = [0u8; 512];
r.read_exact(&mut buf).map_err(|e| {
if e.kind() == io::ErrorKind::UnexpectedEof {
Error::TooShort
} else {
Error::Io(e)
}
})?;
let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
if magic != VMDK_MAGIC {
return Err(Error::BadMagic);
}
let version = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
if version != 1 && version != 3 {
return Err(Error::UnsupportedVersion(version));
}
let capacity = u64::from_le_bytes([
buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19],
]);
if buf[77] == 1 {
return Err(Error::Compressed);
}
Ok(Header { capacity })
}
pub fn detect<R: Read + Seek>(r: &mut R) -> Result<(), Error> {
let pos = r.stream_position()?;
let result = detect_inner(r);
let _ = r.seek(SeekFrom::Start(pos));
result
}
fn detect_inner<R: Read + Seek>(r: &mut R) -> Result<(), Error> {
let file_len = r.seek(SeekFrom::End(0))?;
if file_len < HEADER_SIZE {
return Err(Error::TooShort);
}
r.seek(SeekFrom::Start(0))?;
let mut magic_bytes = [0u8; 4];
match r.read_exact(&mut magic_bytes) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Err(Error::TooShort),
Err(e) => return Err(Error::Io(e)),
}
let magic = u32::from_le_bytes(magic_bytes);
if magic != VMDK_MAGIC {
return Err(Error::BadMagic);
}
Ok(())
}
pub fn detect_and_parse<R: Read + Seek>(r: &mut R) -> Result<TreeNode, Error> {
let header = read_header(r)?;
let virtual_size = header
.capacity
.checked_mul(SECTOR_SIZE)
.ok_or(Error::BadMagic)?;
let mut root = TreeNode::new_directory("/".to_string());
let mut disk_node = TreeNode::new_file("disk.vmdk".to_string(), virtual_size);
disk_node.file_length = Some(virtual_size);
root.add_child(disk_node);
root.calculate_directory_size();
Ok(root)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn build_sparse_header(version: u32, capacity: u64, compress_algorithm: u8) -> [u8; 512] {
let mut buf = [0u8; 512];
buf[0..4].copy_from_slice(&VMDK_MAGIC.to_le_bytes());
buf[4..8].copy_from_slice(&version.to_le_bytes());
buf[8..12].copy_from_slice(&0u32.to_le_bytes());
buf[12..20].copy_from_slice(&capacity.to_le_bytes());
buf[20..28].copy_from_slice(&128u64.to_le_bytes());
buf[28..36].copy_from_slice(&0u64.to_le_bytes());
buf[36..44].copy_from_slice(&0u64.to_le_bytes());
buf[44..48].copy_from_slice(&512u32.to_le_bytes());
buf[48..56].copy_from_slice(&1u64.to_le_bytes());
buf[56..64].copy_from_slice(&2u64.to_le_bytes());
buf[64..72].copy_from_slice(&128u64.to_le_bytes());
buf[72] = 0;
buf[73] = b'\n';
buf[74] = b' ';
buf[75] = b'\r';
buf[76] = b'\n';
buf[77] = compress_algorithm;
buf
}
fn build_vmdk(version: u32, capacity_sectors: u64, compress_algorithm: u8) -> Vec<u8> {
let header = build_sparse_header(version, capacity_sectors, compress_algorithm);
header.to_vec()
}
#[test]
fn detect_sparse_vmdk_ok() {
let img = build_vmdk(1, 2048, 0);
let mut c = Cursor::new(&img);
assert!(detect(&mut c).is_ok(), "should detect sparse VMDK");
}
#[test]
fn detect_restores_position() {
let img = build_vmdk(1, 2048, 0);
let mut c = Cursor::new(&img);
c.seek(SeekFrom::Start(7)).unwrap();
detect(&mut c).unwrap();
assert_eq!(
c.stream_position().unwrap(),
7,
"detect() must restore the stream position"
);
}
#[test]
fn detect_rejects_bad_magic() {
let img = vec![0u8; 512];
let mut c = Cursor::new(&img);
assert!(
matches!(detect(&mut c), Err(Error::BadMagic)),
"all-zeros should fail with BadMagic"
);
}
#[test]
fn detect_rejects_too_short() {
let img = vec![0u8; 256];
let mut c = Cursor::new(&img);
assert!(
matches!(detect(&mut c), Err(Error::TooShort)),
"256-byte image should fail with TooShort"
);
}
#[test]
fn version_3_accepted() {
let img = build_vmdk(3, 2048, 0);
let mut c = Cursor::new(&img);
assert!(
detect_and_parse(&mut c).is_ok(),
"version=3 should be accepted"
);
}
#[test]
fn version_2_rejected() {
let img = build_vmdk(2, 2048, 0);
let mut c = Cursor::new(&img);
assert!(
matches!(detect_and_parse(&mut c), Err(Error::UnsupportedVersion(2))),
"version=2 should return UnsupportedVersion(2)"
);
}
#[test]
fn compressed_vmdk_rejected() {
let img = build_vmdk(1, 2048, 1);
let mut c = Cursor::new(&img);
assert!(
matches!(detect_and_parse(&mut c), Err(Error::Compressed)),
"compress_algorithm=1 should return Error::Compressed"
);
}
#[test]
fn parse_tree_shape() {
let img = build_vmdk(1, 2048, 0);
let mut c = Cursor::new(&img);
let root = detect_and_parse(&mut c).expect("parse sparse VMDK");
assert_eq!(root.name, "/");
assert!(root.is_directory);
assert_eq!(root.children.len(), 1);
let child = &root.children[0];
assert_eq!(child.name, "disk.vmdk");
assert!(!child.is_directory);
assert!(child.children.is_empty());
}
#[test]
fn parse_virtual_size() {
let capacity_sectors: u64 = 4096;
let expected_size = capacity_sectors * 512;
let img = build_vmdk(1, capacity_sectors, 0);
let mut c = Cursor::new(&img);
let root = detect_and_parse(&mut c).expect("parse sparse VMDK");
let disk = &root.children[0];
assert_eq!(
disk.size, expected_size,
"disk.vmdk size should equal capacity_sectors * 512"
);
assert_eq!(
disk.file_length,
Some(expected_size),
"disk.vmdk file_length should equal capacity_sectors * 512"
);
}
#[test]
fn parse_file_location_is_none() {
let img = build_vmdk(1, 2048, 0);
let mut c = Cursor::new(&img);
let root = detect_and_parse(&mut c).expect("parse sparse VMDK");
let disk = &root.children[0];
assert_eq!(
disk.file_location, None,
"sparse VMDK disk.vmdk should have file_location=None (grain directory indirection)"
);
}
#[test]
fn error_display_too_short() {
let msg = format!("{}", Error::TooShort);
assert!(msg.contains("512") || msg.contains("short"), "got: {msg}");
}
#[test]
fn error_display_bad_magic() {
let msg = format!("{}", Error::BadMagic);
assert!(
msg.contains("564d444b") || msg.contains("magic"),
"got: {msg}"
);
}
#[test]
fn error_display_unsupported_version() {
let msg = format!("{}", Error::UnsupportedVersion(42));
assert!(msg.contains("42"), "got: {msg}");
}
#[test]
fn error_display_compressed() {
let msg = format!("{}", Error::Compressed);
assert!(
msg.contains("deflate") || msg.contains("compress"),
"got: {msg}"
);
}
#[test]
fn error_display_io() {
let io = io::Error::other("disk fail");
let msg = format!("{}", Error::Io(io));
assert!(msg.contains("disk fail"), "got: {msg}");
}
#[test]
fn error_source_io() {
use std::error::Error as StdError;
let io = io::Error::other("src");
assert!(Error::Io(io).source().is_some());
}
#[test]
fn error_source_non_io() {
use std::error::Error as StdError;
assert!(Error::TooShort.source().is_none());
assert!(Error::BadMagic.source().is_none());
assert!(Error::Compressed.source().is_none());
assert!(Error::UnsupportedVersion(1).source().is_none());
}
#[test]
fn detect_restores_position_on_failure() {
let img = vec![0u8; 512]; let mut c = Cursor::new(&img);
c.seek(SeekFrom::Start(10)).unwrap();
let _ = detect(&mut c);
assert_eq!(
c.stream_position().unwrap(),
10,
"cursor should be restored on failure"
);
}
#[test]
fn error_from_io_error() {
let io = std::io::Error::other("vmdk read failed");
let e = Error::from(io);
assert!(matches!(e, Error::Io(_)));
}
#[test]
fn read_header_too_short_returns_error() {
let data = vec![0u8; 200];
let mut c = Cursor::new(data);
assert!(matches!(read_header(&mut c), Err(Error::TooShort)));
}
#[test]
fn read_header_bad_magic_returns_error() {
let data = vec![0u8; 512];
let mut c = Cursor::new(data);
assert!(matches!(read_header(&mut c), Err(Error::BadMagic)));
}
}