Skip to main content

vhdx/
metadata.rs

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