use std::collections::HashMap;
use super::{constants, guid::VhdxGuid, parent_locator::VhdxParentLocator};
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VhdxDiskType {
Fixed,
Dynamic,
Differential,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VhdxMetadata {
pub disk_type: VhdxDiskType,
pub block_size: u32,
pub virtual_disk_size: u64,
pub logical_sector_size: u32,
pub physical_sector_size: u32,
pub virtual_disk_identifier: VhdxGuid,
pub parent_locator: Option<VhdxParentLocator>,
}
#[derive(Debug, Clone, Copy)]
struct MetadataEntry {
item_offset: u32,
item_length: u32,
flags: u32,
}
impl VhdxMetadata {
pub fn from_region(region_data: &[u8]) -> Result<Self> {
if region_data.len() < constants::METADATA_TABLE_SIZE {
return Err(Error::InvalidFormat(
"vhdx metadata region is smaller than the metadata table".to_string(),
));
}
if ®ion_data[0..8] != constants::METADATA_TABLE_SIGNATURE {
return Err(Error::InvalidFormat(
"invalid vhdx metadata table signature".to_string(),
));
}
if region_data[8..10] != [0, 0] {
return Err(Error::InvalidFormat(
"vhdx metadata table reserved field is not zero".to_string(),
));
}
if region_data[12..32].iter().any(|&byte| byte != 0) {
return Err(Error::InvalidFormat(
"vhdx metadata table reserved bytes are not zero".to_string(),
));
}
let entry_count = usize::from(u16::from_le_bytes([region_data[10], region_data[11]]));
if entry_count > constants::VHDX_MAX_TABLE_ENTRIES {
return Err(Error::InvalidFormat(format!(
"vhdx metadata table contains too many entries: {entry_count}"
)));
}
let mut entries = HashMap::with_capacity(entry_count);
for index in 0..entry_count {
let entry_offset =
32usize
.checked_add(index.checked_mul(32).ok_or_else(|| {
Error::InvalidRange("vhdx metadata entry offset overflow".to_string())
})?)
.ok_or_else(|| Error::InvalidRange("vhdx metadata entry offset overflow".to_string()))?;
let entry_end = entry_offset
.checked_add(32)
.ok_or_else(|| Error::InvalidRange("vhdx metadata entry end overflow".to_string()))?;
let entry_data = region_data.get(entry_offset..entry_end).ok_or_else(|| {
Error::InvalidFormat("vhdx metadata table ends inside an entry".to_string())
})?;
let item_id = VhdxGuid::from_le_bytes(&entry_data[0..16])?;
let item_offset = u32::from_le_bytes([
entry_data[16],
entry_data[17],
entry_data[18],
entry_data[19],
]);
let item_length = u32::from_le_bytes([
entry_data[20],
entry_data[21],
entry_data[22],
entry_data[23],
]);
let flags = u32::from_le_bytes([
entry_data[24],
entry_data[25],
entry_data[26],
entry_data[27],
]);
let reserved = u32::from_le_bytes([
entry_data[28],
entry_data[29],
entry_data[30],
entry_data[31],
]);
if flags & !0x7 != 0 {
return Err(Error::InvalidFormat(format!(
"vhdx metadata entry {item_id} contains unsupported flags: 0x{flags:08x}"
)));
}
if reserved != 0 {
return Err(Error::InvalidFormat(format!(
"vhdx metadata entry {item_id} reserved field is not zero"
)));
}
if usize::try_from(item_offset)
.map_err(|_| Error::InvalidRange("vhdx metadata item offset is too large".to_string()))?
< constants::METADATA_TABLE_SIZE
{
return Err(Error::InvalidFormat(format!(
"vhdx metadata entry {item_id} overlaps the metadata table"
)));
}
read_metadata_item(region_data, item_offset, item_length, item_id)?;
if entries
.insert(
item_id,
MetadataEntry {
item_offset,
item_length,
flags,
},
)
.is_some()
{
return Err(Error::InvalidFormat(format!(
"duplicate vhdx metadata entry: {item_id}"
)));
}
}
let file_parameters = read_metadata_item(
region_data,
required_entry(&entries, constants::FILE_PARAMETERS_GUID)?.item_offset,
required_entry(&entries, constants::FILE_PARAMETERS_GUID)?.item_length,
constants::FILE_PARAMETERS_GUID,
)?;
if file_parameters.len() != 8 {
return Err(Error::InvalidFormat(
"vhdx file parameters item must be 8 bytes".to_string(),
));
}
let block_size = u32::from_le_bytes([
file_parameters[0],
file_parameters[1],
file_parameters[2],
file_parameters[3],
]);
let file_parameters_flags = u32::from_le_bytes([
file_parameters[4],
file_parameters[5],
file_parameters[6],
file_parameters[7],
]);
if file_parameters_flags & !0x3 != 0 {
return Err(Error::InvalidFormat(format!(
"vhdx file parameters contain unsupported flags: 0x{file_parameters_flags:08x}"
)));
}
if !block_size.is_power_of_two()
|| !(constants::VHDX_MIN_BLOCK_SIZE..=constants::VHDX_MAX_BLOCK_SIZE).contains(&block_size)
{
return Err(Error::InvalidFormat(format!(
"invalid vhdx block size: {block_size}"
)));
}
let disk_type = match file_parameters_flags & 0x3 {
0 => VhdxDiskType::Dynamic,
1 => VhdxDiskType::Fixed,
2 => VhdxDiskType::Differential,
_ => {
return Err(Error::InvalidFormat(format!(
"unsupported vhdx disk type flags: 0x{file_parameters_flags:08x}"
)));
}
};
let virtual_disk_size_entry = required_entry(&entries, constants::VIRTUAL_DISK_SIZE_GUID)?;
if virtual_disk_size_entry.flags & 0x4 == 0 {
return Err(Error::InvalidFormat(
"vhdx virtual disk size metadata item is not marked required".to_string(),
));
}
let virtual_disk_size = {
let bytes = read_metadata_item(
region_data,
virtual_disk_size_entry.item_offset,
virtual_disk_size_entry.item_length,
constants::VIRTUAL_DISK_SIZE_GUID,
)?;
if bytes.len() != 8 {
return Err(Error::InvalidFormat(
"vhdx virtual disk size item must be 8 bytes".to_string(),
));
}
u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])
};
if virtual_disk_size == 0 {
return Err(Error::InvalidFormat(
"vhdx virtual disk size must be non-zero".to_string(),
));
}
let logical_sector_size = read_sector_size(
region_data,
required_entry(&entries, constants::LOGICAL_SECTOR_SIZE_GUID)?,
constants::LOGICAL_SECTOR_SIZE_GUID,
"logical",
)?;
let physical_sector_size = read_sector_size(
region_data,
required_entry(&entries, constants::PHYSICAL_SECTOR_SIZE_GUID)?,
constants::PHYSICAL_SECTOR_SIZE_GUID,
"physical",
)?;
if !block_size.is_multiple_of(logical_sector_size) {
return Err(Error::InvalidFormat(
"vhdx block size is not aligned to the logical sector size".to_string(),
));
}
let virtual_disk_identifier = {
let entry = required_entry(&entries, constants::VIRTUAL_DISK_IDENTIFIER_GUID)?;
let bytes = read_metadata_item(
region_data,
entry.item_offset,
entry.item_length,
constants::VIRTUAL_DISK_IDENTIFIER_GUID,
)?;
VhdxGuid::from_le_bytes(bytes)?
};
let parent_locator = match entries.get(&constants::PARENT_LOCATOR_GUID) {
Some(entry) => Some(VhdxParentLocator::from_bytes(read_metadata_item(
region_data,
entry.item_offset,
entry.item_length,
constants::PARENT_LOCATOR_GUID,
)?)?),
None => None,
};
if matches!(disk_type, VhdxDiskType::Differential) && parent_locator.is_none() {
return Err(Error::InvalidFormat(
"differential vhdx images must provide a parent locator".to_string(),
));
}
Ok(Self {
disk_type,
block_size,
virtual_disk_size,
logical_sector_size,
physical_sector_size,
virtual_disk_identifier,
parent_locator,
})
}
}
fn required_entry(
entries: &HashMap<VhdxGuid, MetadataEntry>, guid: VhdxGuid,
) -> Result<&MetadataEntry> {
entries
.get(&guid)
.ok_or_else(|| Error::InvalidFormat(format!("missing required vhdx metadata item: {guid}")))
}
fn read_sector_size(
region_data: &[u8], entry: &MetadataEntry, item_id: VhdxGuid, label: &str,
) -> Result<u32> {
let bytes = read_metadata_item(region_data, entry.item_offset, entry.item_length, item_id)?;
if bytes.len() != 4 {
return Err(Error::InvalidFormat(format!(
"vhdx {label} sector size item must be 4 bytes"
)));
}
let value = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
if value != 512 && value != 4096 {
return Err(Error::InvalidFormat(format!(
"invalid vhdx {label} sector size: {value}"
)));
}
Ok(value)
}
fn read_metadata_item(
region_data: &[u8], item_offset: u32, item_length: u32, item_id: VhdxGuid,
) -> Result<&[u8]> {
let start = usize::try_from(item_offset).map_err(|_| {
Error::InvalidRange(format!("vhdx metadata item offset is too large: {item_id}"))
})?;
let length = usize::try_from(item_length).map_err(|_| {
Error::InvalidRange(format!("vhdx metadata item length is too large: {item_id}"))
})?;
let end = start
.checked_add(length)
.ok_or_else(|| Error::InvalidRange(format!("vhdx metadata item range overflow: {item_id}")))?;
region_data.get(start..end).ok_or_else(|| {
Error::InvalidFormat(format!(
"vhdx metadata item range exceeds the metadata region: {item_id}"
))
})
}