use std::io::{self, Read, Seek, SeekFrom};
use crate::tree::TreeNode;
const FOOTER_COOKIE: &[u8; 8] = b"conectix";
const DYN_HEADER_COOKIE: &[u8; 8] = b"cxsparse";
const FOOTER_SIZE: u64 = 512;
const DISK_TYPE_FIXED: u32 = 2;
const DISK_TYPE_DYNAMIC: u32 = 3;
#[cfg(test)]
const BAT_ENTRY_UNUSED: u32 = 0xFFFF_FFFF;
#[derive(Debug)]
pub enum Error {
TooShort,
BadMagic,
BadChecksum,
UnsupportedType(u32),
BadDynamicHeader,
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, "VHD file is shorter than 512 bytes (no footer)"),
Error::BadMagic => write!(f, "VHD footer cookie b\"conectix\" not found"),
Error::BadChecksum => write!(f, "VHD footer checksum mismatch"),
Error::UnsupportedType(t) => write!(
f,
"VHD disk_type {t} is not supported (only Fixed=2, Dynamic=3)"
),
Error::BadDynamicHeader => {
write!(f, "VHD Dynamic Disk Header cookie b\"cxsparse\" not found")
}
Error::Io(e) => write!(f, "VHD 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 Footer {
data_offset: u64,
current_size: u64,
disk_type: u32,
}
fn verify_checksum(buf: &[u8; 512]) -> bool {
let stored = u32::from_be_bytes([buf[64], buf[65], buf[66], buf[67]]);
let mut sum: u32 = 0;
for (i, &b) in buf.iter().enumerate() {
if (64..68).contains(&i) {
continue;
}
sum = sum.wrapping_add(b as u32);
}
let computed = !sum;
computed == stored
}
fn read_footer<R: Read + Seek>(r: &mut R, offset: u64) -> Result<Footer, Error> {
r.seek(SeekFrom::Start(offset))?;
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)
}
})?;
if &buf[0..8] != FOOTER_COOKIE {
return Err(Error::BadMagic);
}
if !verify_checksum(&buf) {
return Err(Error::BadChecksum);
}
let data_offset = u64::from_be_bytes([
buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
]);
let current_size = u64::from_be_bytes([
buf[48], buf[49], buf[50], buf[51], buf[52], buf[53], buf[54], buf[55],
]);
let disk_type = u32::from_be_bytes([buf[60], buf[61], buf[62], buf[63]]);
Ok(Footer {
data_offset,
current_size,
disk_type,
})
}
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 < FOOTER_SIZE {
return Err(Error::TooShort);
}
let footer_offset = file_len - FOOTER_SIZE;
r.seek(SeekFrom::Start(footer_offset))?;
let mut cookie = [0u8; 8];
match r.read_exact(&mut cookie) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Err(Error::TooShort),
Err(e) => return Err(Error::Io(e)),
}
if &cookie != FOOTER_COOKIE {
return Err(Error::BadMagic);
}
Ok(())
}
pub fn detect_and_parse<R: Read + Seek>(r: &mut R) -> Result<TreeNode, Error> {
let file_len = r.seek(SeekFrom::End(0))?;
if file_len < FOOTER_SIZE {
return Err(Error::TooShort);
}
let footer_offset = file_len - FOOTER_SIZE;
let footer = read_footer(r, footer_offset)?;
match footer.disk_type {
DISK_TYPE_FIXED => parse_fixed(footer.current_size, file_len),
DISK_TYPE_DYNAMIC => parse_dynamic(r, &footer, file_len),
other => Err(Error::UnsupportedType(other)),
}
}
fn parse_fixed(current_size: u64, file_len: u64) -> Result<TreeNode, Error> {
let data_region = file_len.saturating_sub(FOOTER_SIZE);
if current_size > data_region {
return Err(Error::TooShort);
}
let mut root = TreeNode::new_directory("/".to_string());
let disk_node =
TreeNode::new_file_with_location("disk.img".to_string(), current_size, 0, current_size);
root.add_child(disk_node);
root.calculate_directory_size();
Ok(root)
}
fn parse_dynamic<R: Read + Seek>(
r: &mut R,
footer: &Footer,
file_len: u64,
) -> Result<TreeNode, Error> {
let dyn_header_offset = footer.data_offset;
if dyn_header_offset > file_len.saturating_sub(1024) {
return Err(Error::TooShort);
}
r.seek(SeekFrom::Start(dyn_header_offset))?;
let mut hdr = [0u8; 1024];
match r.read_exact(&mut hdr) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Err(Error::TooShort),
Err(e) => return Err(Error::Io(e)),
}
if &hdr[0..8] != DYN_HEADER_COOKIE {
return Err(Error::BadDynamicHeader);
}
let current_size = footer.current_size;
let mut root = TreeNode::new_directory("/".to_string());
let mut disk_node = TreeNode::new_file("disk.img".to_string(), current_size);
disk_node.file_length = Some(current_size);
root.add_child(disk_node);
root.calculate_directory_size();
Ok(root)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn build_footer(disk_type: u32, current_size: u64, data_offset: u64) -> [u8; 512] {
let mut buf = [0u8; 512];
buf[0..8].copy_from_slice(FOOTER_COOKIE);
buf[8..12].copy_from_slice(&2u32.to_be_bytes());
buf[12..16].copy_from_slice(&0x0001_0000u32.to_be_bytes());
buf[16..24].copy_from_slice(&data_offset.to_be_bytes());
buf[24..28].copy_from_slice(&0u32.to_be_bytes());
buf[28..32].copy_from_slice(b"test");
buf[32..36].copy_from_slice(&0u32.to_be_bytes());
buf[36..40].copy_from_slice(b"Wi2k");
buf[40..48].copy_from_slice(¤t_size.to_be_bytes());
buf[48..56].copy_from_slice(¤t_size.to_be_bytes());
buf[56..60].copy_from_slice(&0u32.to_be_bytes());
buf[60..64].copy_from_slice(&disk_type.to_be_bytes());
let mut sum: u32 = 0;
for &b in buf.iter() {
sum = sum.wrapping_add(b as u32);
}
let checksum: u32 = !sum;
buf[64..68].copy_from_slice(&checksum.to_be_bytes());
buf
}
fn build_fixed_vhd(data_size: u64) -> Vec<u8> {
let data_offset = 0xFFFF_FFFF_FFFF_FFFFu64; let footer = build_footer(DISK_TYPE_FIXED, data_size, data_offset);
let mut image = vec![0u8; data_size as usize];
image.extend_from_slice(&footer);
image
}
fn build_dynamic_vhd(virtual_size: u64) -> Vec<u8> {
let footer = build_footer(DISK_TYPE_DYNAMIC, virtual_size, 512);
let mut dyn_hdr = [0u8; 1024];
dyn_hdr[0..8].copy_from_slice(DYN_HEADER_COOKIE);
dyn_hdr[8..16].copy_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_be_bytes());
let table_offset: u64 = 512 + 1024;
dyn_hdr[16..24].copy_from_slice(&table_offset.to_be_bytes());
dyn_hdr[24..28].copy_from_slice(&0x0001_0000u32.to_be_bytes());
let block_size: u32 = 0x0020_0000; let max_table_entries = virtual_size.div_ceil(block_size as u64) as u32;
dyn_hdr[28..32].copy_from_slice(&max_table_entries.to_be_bytes());
dyn_hdr[32..36].copy_from_slice(&block_size.to_be_bytes());
let mut sum: u32 = 0;
for &b in dyn_hdr.iter() {
sum = sum.wrapping_add(b as u32);
}
let hdr_checksum: u32 = !sum;
dyn_hdr[36..40].copy_from_slice(&hdr_checksum.to_be_bytes());
let bat_entries = max_table_entries as usize;
let mut bat = vec![0xFFu8; bat_entries * 4];
for i in 0..bat_entries {
bat[i * 4..i * 4 + 4].copy_from_slice(&BAT_ENTRY_UNUSED.to_be_bytes());
}
let mut image: Vec<u8> = Vec::new();
image.extend_from_slice(&footer); image.extend_from_slice(&dyn_hdr);
image.extend_from_slice(&bat);
image.extend_from_slice(&footer); image
}
#[test]
fn detect_fixed_vhd_ok() {
let img = build_fixed_vhd(512);
let mut c = Cursor::new(&img);
assert!(detect(&mut c).is_ok(), "should detect fixed VHD");
}
#[test]
fn detect_dynamic_vhd_ok() {
let img = build_dynamic_vhd(2 * 1024 * 1024);
let mut c = Cursor::new(&img);
assert!(detect(&mut c).is_ok(), "should detect dynamic VHD");
}
#[test]
fn detect_restores_position() {
let img = build_fixed_vhd(512);
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 stream position"
);
}
#[test]
fn detect_rejects_bad_magic() {
let img = vec![0u8; 1024];
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 checksum_valid_footer_passes() {
let img = build_fixed_vhd(512);
let footer_slice: &[u8; 512] = img[img.len() - 512..].try_into().unwrap();
assert!(
verify_checksum(footer_slice),
"freshly built footer checksum should pass"
);
}
#[test]
fn checksum_corrupted_footer_fails() {
let img = build_fixed_vhd(512);
let mut patched = img.clone();
let footer_start = patched.len() - 512;
patched[footer_start + 10] ^= 0xFF; let footer_slice: &[u8; 512] = patched[footer_start..].try_into().unwrap();
assert!(
!verify_checksum(footer_slice),
"corrupted footer should fail checksum"
);
}
#[test]
fn bad_checksum_returns_error() {
let mut img = build_fixed_vhd(512);
let footer_start = img.len() - 512;
img[footer_start + 64] ^= 0xFF;
let mut c = Cursor::new(&img);
let result = detect_and_parse(&mut c);
assert!(
matches!(result, Err(Error::BadChecksum)),
"corrupted checksum should yield BadChecksum"
);
}
#[test]
fn fixed_vhd_tree_shape() {
let img = build_fixed_vhd(512);
let mut c = Cursor::new(&img);
let root = detect_and_parse(&mut c).expect("parse fixed VHD");
assert_eq!(root.name, "/");
assert!(root.is_directory);
assert_eq!(root.children.len(), 1);
let child = &root.children[0];
assert_eq!(child.name, "disk.img");
assert!(!child.is_directory);
}
#[test]
fn fixed_vhd_file_location_is_zero() {
let data_size: u64 = 512;
let img = build_fixed_vhd(data_size);
let mut c = Cursor::new(&img);
let root = detect_and_parse(&mut c).expect("parse fixed VHD");
let disk = &root.children[0];
assert_eq!(
disk.file_location,
Some(0),
"fixed VHD disk.img should have file_location=Some(0)"
);
assert_eq!(
disk.file_length,
Some(data_size),
"fixed VHD disk.img should have file_length=Some(current_size)"
);
assert_eq!(disk.size, data_size);
}
#[test]
fn dynamic_vhd_tree_shape() {
let virtual_size: u64 = 10 * 1024 * 1024; let img = build_dynamic_vhd(virtual_size);
let mut c = Cursor::new(&img);
let root = detect_and_parse(&mut c).expect("parse dynamic VHD");
assert_eq!(root.name, "/");
assert!(root.is_directory);
assert_eq!(root.children.len(), 1);
let child = &root.children[0];
assert_eq!(child.name, "disk.img");
assert!(!child.is_directory);
}
#[test]
fn dynamic_vhd_no_file_location() {
let virtual_size: u64 = 10 * 1024 * 1024;
let img = build_dynamic_vhd(virtual_size);
let mut c = Cursor::new(&img);
let root = detect_and_parse(&mut c).expect("parse dynamic VHD");
let disk = &root.children[0];
assert_eq!(
disk.file_location, None,
"dynamic VHD disk.img should have file_location=None (fragmented BAT)"
);
assert_eq!(
disk.file_length,
Some(virtual_size),
"dynamic VHD disk.img should report virtual size in file_length"
);
}
#[test]
fn unsupported_differencing_type_returns_error() {
const DISK_TYPE_DIFFERENCING: u32 = 4;
let data_offset = 512u64;
let footer = build_footer(DISK_TYPE_DIFFERENCING, 1024 * 1024, data_offset);
let mut img = vec![0u8; 512];
img.extend_from_slice(&footer);
let mut c = Cursor::new(&img);
let result = detect_and_parse(&mut c);
assert!(
matches!(result, Err(Error::UnsupportedType(4))),
"differencing VHD should return UnsupportedType(4)"
);
}
#[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("conectix") || msg.contains("cookie"),
"got: {msg}"
);
}
#[test]
fn error_display_bad_checksum() {
let msg = format!("{}", Error::BadChecksum);
assert!(
msg.contains("checksum") || msg.contains("sum"),
"got: {msg}"
);
}
#[test]
fn error_display_unsupported_type() {
let msg = format!("{}", Error::UnsupportedType(5));
assert!(msg.contains('5'), "got: {msg}");
}
#[test]
fn error_display_bad_dynamic_header() {
let msg = format!("{}", Error::BadDynamicHeader);
assert!(
msg.contains("cxsparse") || msg.contains("dynamic"),
"got: {msg}"
);
}
#[test]
fn error_display_io() {
let io = io::Error::other("disk");
let msg = format!("{}", Error::Io(io));
assert!(msg.contains("disk"), "got: {msg}");
}
#[test]
fn error_source_io() {
use std::error::Error as StdError;
assert!(Error::Io(io::Error::other("s")).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::BadChecksum.source().is_none());
assert!(Error::UnsupportedType(2).source().is_none());
assert!(Error::BadDynamicHeader.source().is_none());
}
#[test]
fn error_from_io_error() {
let io = io::Error::other("disk read failed");
let e = Error::from(io);
assert!(matches!(e, Error::Io(_)));
}
#[test]
fn read_footer_too_short_returns_error() {
let data = vec![0u8; 200];
let mut c = Cursor::new(data);
assert!(matches!(read_footer(&mut c, 0), Err(Error::TooShort)));
}
#[test]
fn read_footer_bad_magic_returns_error() {
let data = vec![0u8; 512];
let mut c = Cursor::new(data);
assert!(matches!(read_footer(&mut c, 0), Err(Error::BadMagic)));
}
#[test]
fn parse_fixed_current_size_exceeds_data_region_returns_error() {
let footer = build_footer(DISK_TYPE_FIXED, 1000, 0xFFFF_FFFF_FFFF_FFFFu64);
let mut img = vec![0u8; 0];
img.extend_from_slice(&footer);
let mut c = Cursor::new(&img);
assert!(matches!(detect_and_parse(&mut c), Err(Error::TooShort)));
}
#[test]
fn parse_dynamic_data_offset_too_large_returns_error() {
let footer = build_footer(DISK_TYPE_DYNAMIC, 1024 * 1024, 0xFFFF_FFFF_FFFF_FFFFu64);
let mut img = vec![0u8; 512];
img.extend_from_slice(&footer);
let mut c = Cursor::new(&img);
assert!(matches!(detect_and_parse(&mut c), Err(Error::TooShort)));
}
#[test]
fn parse_dynamic_bad_dyn_header_cookie_returns_error() {
let footer = build_footer(DISK_TYPE_DYNAMIC, 1024 * 1024, 512);
let dyn_hdr = [0u8; 1024]; let mut img: Vec<u8> = Vec::new();
img.extend_from_slice(&footer); img.extend_from_slice(&dyn_hdr); img.extend_from_slice(&footer); let mut c = Cursor::new(&img);
assert!(matches!(
detect_and_parse(&mut c),
Err(Error::BadDynamicHeader)
));
}
}