Skip to main content

vhdx/
metadata.rs

1use crate::error::{Result, VhdxError};
2
3pub const METADATA_TABLE_SIGNATURE: &[u8; 8] = b"metadata";
4
5const BLOCK_SIZE_MIN: u32 = 1 << 20; // 1 MB
6const BLOCK_SIZE_MAX: u32 = 256 << 20; // 256 MB
7const VALID_SECTOR_SIZES: [u32; 2] = [512, 4096];
8const VIRTUAL_DISK_SIZE_MAX: u64 = 64 * (1u64 << 40); // 64 TiB
9
10// Well-known metadata item GUIDs (MS-VHDX §2.5.5).
11pub const GUID_FILE_PARAMETERS: [u8; 16] = [
12    0x37, 0x67, 0xA1, 0xCA, 0x36, 0xFA, 0x43, 0x4D, 0xB3, 0xB6, 0x33, 0xF0, 0xAA, 0x44, 0xE7, 0x6B,
13];
14pub const GUID_VIRTUAL_DISK_SIZE: [u8; 16] = [
15    0x24, 0x42, 0xA5, 0x2F, 0x1B, 0xCD, 0x76, 0x48, 0xB2, 0x11, 0x5B, 0xE0, 0x7A, 0x6C, 0xE3, 0x2C,
16];
17// QEMU ≤ v5.2 wrote a non-spec VirtualDiskSize GUID; files created by that version
18// are otherwise valid and common in the wild.
19pub const GUID_VIRTUAL_DISK_SIZE_QEMU_COMPAT: [u8; 16] = [
20    0x24, 0x42, 0xA5, 0x2F, 0x1B, 0xCD, 0x76, 0x48, 0xB2, 0x11, 0x5D, 0xBE, 0xD8, 0x3B, 0xF4, 0xB8,
21];
22pub const GUID_LOGICAL_SECTOR_SIZE: [u8; 16] = [
23    0x1D, 0xBF, 0x41, 0x81, 0x6F, 0xA9, 0x09, 0x47, 0xBA, 0x47, 0xF2, 0x33, 0xA8, 0xFA, 0xAB, 0x5F,
24];
25pub const GUID_PHYSICAL_SECTOR_SIZE: [u8; 16] = [
26    0xC7, 0x48, 0xA3, 0xCD, 0x5D, 0x44, 0x71, 0x44, 0x9C, 0xC9, 0xE9, 0x88, 0x52, 0x51, 0xC5, 0x56,
27];
28pub const GUID_VIRTUAL_DISK_ID: [u8; 16] = [
29    0xAB, 0x12, 0xCA, 0xBE, 0xE6, 0xB2, 0x23, 0x45, 0x93, 0xEF, 0xC3, 0x09, 0xE0, 0x00, 0xC7, 0x46,
30];
31pub const GUID_PARENT_LOCATOR: [u8; 16] = [
32    0x2D, 0x5F, 0xD3, 0xA8, 0x0B, 0xB3, 0x4D, 0x45, 0xAB, 0xF7, 0xD3, 0xD8, 0x48, 0x34, 0xAB, 0x0C,
33];
34
35#[derive(Debug, Clone)]
36pub struct VhdxMetadata {
37    /// Data block size in bytes (default 32 MB).
38    pub block_size: u32,
39    /// True if this is a differencing disk (not supported for forensics).
40    pub has_parent: bool,
41    /// Total virtual disk size in bytes.
42    pub virtual_disk_size: u64,
43    /// Logical sector size (typically 512).
44    pub logical_sector_size: u32,
45}
46
47impl VhdxMetadata {
48    /// Chunk ratio: how many data block BAT entries precede each sector bitmap entry.
49    /// Formula from MS-VHDX §2.3.5: `(2^23 * LogicalSectorSize) / BlockSize`.
50    pub fn chunk_ratio(&self) -> u64 {
51        (1u64 << 23) * u64::from(self.logical_sector_size) / u64::from(self.block_size)
52    }
53
54    pub fn validate(&self) -> Result<()> {
55        if self.block_size < BLOCK_SIZE_MIN || self.block_size > BLOCK_SIZE_MAX {
56            return Err(VhdxError::InvalidMetadata(
57                "BlockSize must be in [1 MB, 256 MB]",
58            ));
59        }
60        if self.block_size.count_ones() != 1 {
61            return Err(VhdxError::InvalidMetadata(
62                "BlockSize must be a power of two",
63            ));
64        }
65        if !VALID_SECTOR_SIZES.contains(&self.logical_sector_size) {
66            return Err(VhdxError::InvalidMetadata(
67                "LogicalSectorSize must be 512 or 4096",
68            ));
69        }
70        if self.virtual_disk_size == 0 {
71            return Err(VhdxError::InvalidMetadata("VirtualDiskSize cannot be zero"));
72        }
73        if self.virtual_disk_size > VIRTUAL_DISK_SIZE_MAX {
74            return Err(VhdxError::InvalidMetadata(
75                "VirtualDiskSize exceeds the 64 TiB spec limit",
76            ));
77        }
78        if self.virtual_disk_size % u64::from(self.logical_sector_size) != 0 {
79            return Err(VhdxError::InvalidMetadata(
80                "VirtualDiskSize must be a multiple of LogicalSectorSize",
81            ));
82        }
83        Ok(())
84    }
85}
86
87pub fn parse_metadata(data: &[u8], region_offset: u64, region_len: u32) -> Result<VhdxMetadata> {
88    let start = region_offset as usize;
89    let end = start + region_len as usize;
90    if data.len() < end || end < start + 8 {
91        return Err(VhdxError::MetadataMissing("region out of bounds"));
92    }
93    let region = &data[start..end];
94    if &region[0..8] != METADATA_TABLE_SIGNATURE {
95        return Err(VhdxError::MetadataMissing("bad metadata signature"));
96    }
97    let entry_count = u16::from_le_bytes(region[10..12].try_into().unwrap()) as usize;
98
99    let mut block_size: Option<u32> = None;
100    let mut has_parent = false;
101    let mut virtual_disk_size: Option<u64> = None;
102    let mut logical_sector_size: Option<u32> = None;
103
104    for i in 0..entry_count {
105        let base = 32 + i * 32;
106        if base + 32 > region.len() {
107            break;
108        }
109        let mut guid = [0u8; 16];
110        guid.copy_from_slice(&region[base..base + 16]);
111        let item_offset =
112            u32::from_le_bytes(region[base + 16..base + 20].try_into().unwrap()) as usize;
113        let item_len =
114            u32::from_le_bytes(region[base + 20..base + 24].try_into().unwrap()) as usize;
115
116        let data_start = start + item_offset;
117        let data_end = data_start + item_len;
118        if data.len() < data_end {
119            continue;
120        }
121        let item_data = &data[data_start..data_end];
122
123        if guid == GUID_FILE_PARAMETERS && item_data.len() >= 8 {
124            block_size = Some(u32::from_le_bytes(item_data[0..4].try_into().unwrap()));
125            let flags = u32::from_le_bytes(item_data[4..8].try_into().unwrap());
126            has_parent = flags & 2 != 0;
127        } else if (guid == GUID_VIRTUAL_DISK_SIZE || guid == GUID_VIRTUAL_DISK_SIZE_QEMU_COMPAT)
128            && item_data.len() >= 8
129        {
130            virtual_disk_size = Some(u64::from_le_bytes(item_data[0..8].try_into().unwrap()));
131        } else if guid == GUID_LOGICAL_SECTOR_SIZE && item_data.len() >= 4 {
132            logical_sector_size = Some(u32::from_le_bytes(item_data[0..4].try_into().unwrap()));
133        }
134    }
135
136    Ok(VhdxMetadata {
137        block_size: block_size.ok_or(VhdxError::MetadataMissing("BlockSize"))?,
138        has_parent,
139        virtual_disk_size: virtual_disk_size
140            .ok_or(VhdxError::MetadataMissing("VirtualDiskSize"))?,
141        logical_sector_size: logical_sector_size.unwrap_or(512),
142    })
143}