use super::{constants, guid::VhdxGuid};
use crate::{ByteSource, Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VhdxImageHeader {
pub sequence_number: u64,
pub file_write_identifier: VhdxGuid,
pub data_write_identifier: VhdxGuid,
pub log_identifier: VhdxGuid,
pub log_version: u16,
pub format_version: u16,
pub log_length: u32,
pub log_offset: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VhdxRegionTableEntry {
pub type_identifier: VhdxGuid,
pub file_offset: u64,
pub length: u32,
pub is_required: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VhdxRegionTable {
entries: Vec<VhdxRegionTableEntry>,
}
impl VhdxImageHeader {
pub fn read(source: &dyn ByteSource, offset: u64) -> Result<Self> {
let data = source.read_bytes_at(offset, constants::IMAGE_HEADER_SIZE)?;
Self::from_bytes(&data)
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() != constants::IMAGE_HEADER_SIZE {
return Err(Error::invalid_format(format!(
"vhdx image header must be {} bytes, got {}",
constants::IMAGE_HEADER_SIZE,
data.len()
)));
}
if &data[0..4] != constants::IMAGE_HEADER_SIGNATURE {
return Err(Error::invalid_format(
"invalid vhdx image header signature".to_string(),
));
}
verify_crc32c(data, 4, "vhdx image header")?;
let format_version = u16::from_le_bytes([data[66], data[67]]);
if format_version != 1 {
return Err(Error::invalid_format(format!(
"unsupported vhdx format version: {format_version}"
)));
}
let log_length = u32::from_le_bytes([data[68], data[69], data[70], data[71]]);
if log_length == 0 || !u64::from(log_length).is_multiple_of(constants::VHDX_ALIGNMENT) {
return Err(Error::invalid_format(format!(
"invalid vhdx log length: {log_length}"
)));
}
let log_offset = u64::from_le_bytes([
data[72], data[73], data[74], data[75], data[76], data[77], data[78], data[79],
]);
if log_offset < constants::VHDX_ALIGNMENT
|| !log_offset.is_multiple_of(constants::VHDX_ALIGNMENT)
{
return Err(Error::invalid_format(format!(
"invalid vhdx log offset: {log_offset}"
)));
}
if data[80..].iter().any(|&byte| byte != 0) {
return Err(Error::invalid_format(
"vhdx image header reserved bytes are not zero".to_string(),
));
}
Ok(Self {
sequence_number: u64::from_le_bytes([
data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15],
]),
file_write_identifier: VhdxGuid::from_le_bytes(&data[16..32])?,
data_write_identifier: VhdxGuid::from_le_bytes(&data[32..48])?,
log_identifier: VhdxGuid::from_le_bytes(&data[48..64])?,
log_version: u16::from_le_bytes([data[64], data[65]]),
format_version,
log_length,
log_offset,
})
}
}
impl VhdxRegionTable {
pub fn read(source: &dyn ByteSource, offset: u64) -> Result<Self> {
let data = source.read_bytes_at(offset, constants::REGION_TABLE_SIZE)?;
Self::from_bytes(&data)
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() != constants::REGION_TABLE_SIZE {
return Err(Error::invalid_format(format!(
"vhdx region table must be {} bytes, got {}",
constants::REGION_TABLE_SIZE,
data.len()
)));
}
if &data[0..4] != constants::REGION_TABLE_SIGNATURE {
return Err(Error::invalid_format(
"invalid vhdx region table signature".to_string(),
));
}
verify_crc32c(data, 4, "vhdx region table")?;
let entry_count = usize::try_from(u32::from_le_bytes([data[8], data[9], data[10], data[11]]))
.map_err(|_| Error::invalid_range("vhdx region entry count is too large"))?;
if entry_count > constants::VHDX_MAX_TABLE_ENTRIES {
return Err(Error::invalid_format(format!(
"vhdx region table contains too many entries: {entry_count}"
)));
}
if data[12..16] != [0, 0, 0, 0] {
return Err(Error::invalid_format(
"vhdx region table reserved field is not zero".to_string(),
));
}
let mut entries: Vec<VhdxRegionTableEntry> = Vec::with_capacity(entry_count);
for index in 0..entry_count {
let entry_offset = 16usize
.checked_add(
index
.checked_mul(32)
.ok_or_else(|| Error::invalid_range("vhdx region entry offset overflow"))?,
)
.ok_or_else(|| Error::invalid_range("vhdx region entry offset overflow"))?;
let entry_end = entry_offset
.checked_add(32)
.ok_or_else(|| Error::invalid_range("vhdx region entry end overflow"))?;
let entry_data = data
.get(entry_offset..entry_end)
.ok_or_else(|| Error::invalid_format("vhdx region table ends inside an entry"))?;
let type_identifier = VhdxGuid::from_le_bytes(&entry_data[0..16])?;
let file_offset = u64::from_le_bytes([
entry_data[16],
entry_data[17],
entry_data[18],
entry_data[19],
entry_data[20],
entry_data[21],
entry_data[22],
entry_data[23],
]);
let length = u32::from_le_bytes([
entry_data[24],
entry_data[25],
entry_data[26],
entry_data[27],
]);
let required_raw = u32::from_le_bytes([
entry_data[28],
entry_data[29],
entry_data[30],
entry_data[31],
]);
if file_offset < constants::VHDX_ALIGNMENT
|| !file_offset.is_multiple_of(constants::VHDX_ALIGNMENT)
{
return Err(Error::invalid_format(format!(
"vhdx region {type_identifier} has an invalid offset: {file_offset}"
)));
}
if length == 0 || !u64::from(length).is_multiple_of(constants::VHDX_ALIGNMENT) {
return Err(Error::invalid_format(format!(
"vhdx region {type_identifier} has an invalid size: {length}"
)));
}
let is_required = match required_raw {
0 => false,
1 => true,
_ => {
return Err(Error::invalid_format(format!(
"vhdx region {type_identifier} has an invalid required flag: {required_raw}"
)));
}
};
let entry = VhdxRegionTableEntry {
type_identifier,
file_offset,
length,
is_required,
};
if entries
.iter()
.any(|existing| existing.type_identifier == type_identifier)
{
return Err(Error::invalid_format(format!(
"duplicate vhdx region table entry: {type_identifier}"
)));
}
entries.push(entry);
}
Ok(Self { entries })
}
pub fn entry(&self, type_identifier: VhdxGuid) -> Option<&VhdxRegionTableEntry> {
self
.entries
.iter()
.find(|entry| entry.type_identifier == type_identifier)
}
pub fn entries(&self) -> &[VhdxRegionTableEntry] {
&self.entries
}
}
pub(super) fn validate_file_identifier(source: &dyn ByteSource) -> Result<()> {
let data = source.read_bytes_at(
constants::FILE_IDENTIFIER_OFFSET,
constants::FILE_IDENTIFIER_SIZE,
)?;
if &data[0..8] != constants::FILE_IDENTIFIER_SIGNATURE {
return Err(Error::invalid_format(
"invalid vhdx file identifier signature".to_string(),
));
}
Ok(())
}
fn verify_crc32c(data: &[u8], checksum_offset: usize, label: &str) -> Result<()> {
let checksum_end = checksum_offset
.checked_add(4)
.ok_or_else(|| Error::invalid_range(format!("{label} checksum offset overflow")))?;
let stored_checksum = u32::from_le_bytes([
data[checksum_offset],
data[checksum_offset + 1],
data[checksum_offset + 2],
data[checksum_offset + 3],
]);
let mut checksum = crc32c::crc32c_append(0, &data[..checksum_offset]);
checksum = crc32c::crc32c_append(checksum, &[0; 4]);
checksum = crc32c::crc32c_append(checksum, &data[checksum_end..]);
if stored_checksum != 0 && stored_checksum != checksum {
return Err(Error::invalid_format(format!(
"mismatch between stored and calculated {label} checksum: 0x{stored_checksum:08x} != 0x{checksum:08x}"
)));
}
Ok(())
}