Skip to main content

vhdx/validation/
metadata.rs

1use super::{
2    Error, Guid, Result, SpecValidator, StandardItems, ValidationIssue, is_known_metadata_guid,
3};
4
5impl SpecValidator {
6    /// Validate the metadata table and item structure.
7    ///
8    /// Checks:
9    /// - Table signature "metadata"
10    /// - Entry count <= 2047
11    /// - Entry offset/length bounds (within metadata region)
12    /// - No items extend beyond the region
13    ///
14    /// # Errors
15    ///
16    /// Returns an error when metadata table or item constraints are violated.
17    ///
18    /// # Panics
19    ///
20    /// Panics if checked integer conversions for metadata range bookkeeping are
21    /// violated unexpectedly.
22    pub fn validate_metadata(&self) -> Result<Vec<ValidationIssue>> {
23        let mut issues = Vec::new();
24        let Some(meta_data) = self.metadata_region() else {
25            return Ok(issues);
26        };
27
28        let meta = crate::metadata::Metadata::new(meta_data)?;
29        let table = meta.table();
30
31        Self::validate_metadata_header_checks(&table, &mut issues)?;
32        let mut ranges: Vec<(u32, u32, Guid)> = Vec::new();
33        for entry in table.entries() {
34            self.validate_metadata_entry(&entry, meta_data.len(), &mut ranges, &mut issues)?;
35        }
36        Self::validate_metadata_ranges_overlap(&ranges, &mut issues)?;
37        Self::push_corrupted_known_metadata_items(&table, &mut issues);
38
39        Ok(issues)
40    }
41
42    fn validate_metadata_header_checks(
43        table: &crate::metadata::MetadataTable<'_>, issues: &mut Vec<ValidationIssue>,
44    ) -> Result<()> {
45        if let Err(e) = table.header().validate_signature() {
46            Self::push_issue(
47                issues,
48                ValidationIssue::new(
49                    "metadata",
50                    "METADATA_TABLE_SIGNATURE_INVALID",
51                    format!("{e}"),
52                    "MS-VHDX/2.6.1.1",
53                ),
54            );
55            return Err(e);
56        }
57        let entry_count = table.header().entry_count();
58        if entry_count > 2047 {
59            Self::push_issue(
60                issues,
61                ValidationIssue::new(
62                    "metadata",
63                    "METADATA_ENTRY_INVALID",
64                    format!("entry count {entry_count} > 2047"),
65                    "MS-VHDX/2.6.1.2",
66                ),
67            );
68            return Err(Error::InvalidMetadata(format!(
69                "METADATA_ENTRY_INVALID: entry count {entry_count} > 2047"
70            )));
71        }
72        Ok(())
73    }
74
75    fn validate_metadata_entry(
76        &self, entry: &crate::metadata::TableEntry<'_>, region_len: usize,
77        ranges: &mut Vec<(u32, u32, Guid)>, issues: &mut Vec<ValidationIssue>,
78    ) -> Result<()> {
79        let offset = entry.offset() as usize;
80        let length = entry.length() as usize;
81        Self::validate_metadata_offset_and_length(
82            entry, offset, length, region_len, ranges, issues,
83        )?;
84        Self::validate_metadata_entry_reserved_flags(entry, issues)?;
85        Self::validate_metadata_entry_reserved_field(entry, issues)?;
86        self.validate_metadata_unknown_guid_policy(entry, issues)
87    }
88
89    fn validate_metadata_offset_and_length(
90        entry: &crate::metadata::TableEntry<'_>, offset: usize, length: usize, region_len: usize,
91        ranges: &mut Vec<(u32, u32, Guid)>, issues: &mut Vec<ValidationIssue>,
92    ) -> Result<()> {
93        if length == 0 && offset != 0 {
94            Self::push_issue(
95                issues,
96                ValidationIssue::new(
97                    "metadata",
98                    "METADATA_ENTRY_INVALID",
99                    format!("length=0 but offset={offset} (expected 0)"),
100                    "MS-VHDX/2.6.1.2",
101                ),
102            );
103            return Err(Error::InvalidMetadata(format!(
104                "METADATA_ENTRY_INVALID: length=0 but offset={offset} (expected 0)"
105            )));
106        }
107        if length > 0 {
108            if offset < 65536 {
109                Self::push_issue(
110                    issues,
111                    ValidationIssue::new(
112                        "metadata",
113                        "METADATA_ENTRY_OFFSET_MINIMUM",
114                        format!("metadata entry offset {offset} < 64KB minimum"),
115                        "MS-VHDX/2.6.1.2",
116                    ),
117                );
118                return Err(Error::InvalidMetadata(format!(
119                    "METADATA_ENTRY_OFFSET_MINIMUM: metadata entry offset {offset} < 64KB minimum"
120                )));
121            }
122            let Some(end) = offset.checked_add(length) else {
123                Self::push_issue(
124                    issues,
125                    ValidationIssue::new(
126                        "metadata",
127                        "METADATA_ENTRY_INVALID",
128                        "offset+length overflow",
129                        "MS-VHDX/2.6.1.2",
130                    ),
131                );
132                return Err(Error::InvalidMetadata(
133                    "METADATA_ENTRY_INVALID: offset+length overflow".into(),
134                ));
135            };
136            if end > region_len {
137                Self::push_issue(
138                    issues,
139                    ValidationIssue::new(
140                        "metadata",
141                        "METADATA_ENTRY_INVALID",
142                        format!("item extent [{offset}..{end}] exceeds region ({region_len})"),
143                        "MS-VHDX/2.6.1.2",
144                    ),
145                );
146                return Err(Error::InvalidMetadata(format!(
147                    "METADATA_ENTRY_INVALID: item extent [{offset}..{end}] exceeds region ({region_len})"
148                )));
149            }
150            ranges.push((
151                u32::try_from(offset).expect("metadata item offset fits u32"),
152                u32::try_from(offset + length).expect("metadata item end fits u32"),
153                entry.item_id(),
154            ));
155        }
156        Ok(())
157    }
158
159    fn validate_metadata_entry_reserved_flags(
160        entry: &crate::metadata::TableEntry<'_>, issues: &mut Vec<ValidationIssue>,
161    ) -> Result<()> {
162        if entry.flags().has_reserved_bits() {
163            Self::push_issue(
164                issues,
165                ValidationIssue::new(
166                    "metadata",
167                    "METADATA_RESERVED_FLAGS_SET",
168                    format!(
169                        "metadata entry GUID {} has reserved flags bits set: {:#010x}",
170                        entry.item_id(),
171                        entry.flags_bits()
172                    ),
173                    "MS-VHDX/2.6.1.2",
174                ),
175            );
176            return Err(Error::MetadataReservedFlagsSet {
177                flags: entry.flags_bits(),
178            });
179        }
180        Ok(())
181    }
182
183    fn validate_metadata_entry_reserved_field(
184        entry: &crate::metadata::TableEntry<'_>, issues: &mut Vec<ValidationIssue>,
185    ) -> Result<()> {
186        if entry.reserved() != 0 {
187            Self::push_issue(
188                issues,
189                ValidationIssue::new(
190                    "metadata",
191                    "METADATA_ENTRY_RESERVED_NONZERO",
192                    format!(
193                        "metadata entry GUID {} has reserved field set to {:#010x}",
194                        entry.item_id(),
195                        entry.reserved()
196                    ),
197                    "MS-VHDX/2.6.1.2",
198                ),
199            );
200            return Err(Error::MetadataEntryReservedNonzero {
201                reserved: entry.reserved(),
202            });
203        }
204        Ok(())
205    }
206
207    fn validate_metadata_unknown_guid_policy(
208        &self, entry: &crate::metadata::TableEntry<'_>, issues: &mut Vec<ValidationIssue>,
209    ) -> Result<()> {
210        if is_known_metadata_guid(&entry.item_id()) {
211            return Ok(());
212        }
213        Self::push_issue(
214            issues,
215            ValidationIssue::new(
216                "metadata",
217                "METADATA_GUID_UNKNOWN",
218                format!("unknown metadata GUID {}", entry.item_id()),
219                "MS-VHDX/2.6.2",
220            ),
221        );
222        if entry.flags().is_required() {
223            Self::push_issue(
224                issues,
225                ValidationIssue::new(
226                    "metadata",
227                    "METADATA_REQUIRED_UNKNOWN",
228                    format!("required unknown metadata GUID {}", entry.item_id()),
229                    "RELAX",
230                ),
231            );
232            return Err(Error::MetadataRequiredUnknown {
233                guid: entry.item_id(),
234            });
235        }
236        if self.strict {
237            Self::push_issue(
238                issues,
239                ValidationIssue::new(
240                    "metadata",
241                    "METADATA_OPTIONAL_UNKNOWN",
242                    format!(
243                        "optional unknown metadata GUID {} in strict mode",
244                        entry.item_id()
245                    ),
246                    "RELAX",
247                ),
248            );
249            return Err(Error::MetadataOptionalUnknown {
250                guid: entry.item_id(),
251            });
252        }
253        Self::push_issue(
254            issues,
255            ValidationIssue::new(
256                "metadata",
257                "METADATA_OPTIONAL_UNKNOWN",
258                format!(
259                    "optional unknown metadata GUID {} tolerated in non-strict mode",
260                    entry.item_id()
261                ),
262                "RELAX",
263            ),
264        );
265        Ok(())
266    }
267
268    fn validate_metadata_ranges_overlap(
269        ranges: &[(u32, u32, Guid)], issues: &mut Vec<ValidationIssue>,
270    ) -> Result<()> {
271        for i in 0..ranges.len() {
272            for j in (i + 1)..ranges.len() {
273                let (s1, e1, g1) = &ranges[i];
274                let (s2, e2, g2) = &ranges[j];
275                if *s1 < *e2 && *s2 < *e1 {
276                    Self::push_issue(
277                        issues,
278                        ValidationIssue::new(
279                            "metadata",
280                            "METADATA_ITEMS_OVERLAP",
281                            format!("metadata items overlap: {g1} and {g2}"),
282                            "MS-VHDX/2.6.2",
283                        ),
284                    );
285                    return Err(Error::InvalidMetadata(format!(
286                        "METADATA_ITEMS_OVERLAP: metadata items overlap: {g1} and {g2}"
287                    )));
288                }
289            }
290        }
291        Ok(())
292    }
293
294    fn push_corrupted_known_metadata_items(
295        table: &crate::metadata::MetadataTable<'_>, issues: &mut Vec<ValidationIssue>,
296    ) {
297        let known_items: &[(&Guid, &str, u32)] = &[
298            (&StandardItems::FILE_PARAMETERS, "FileParameters", 8),
299            (&StandardItems::VIRTUAL_DISK_SIZE, "VirtualDiskSize", 8),
300            (&StandardItems::VIRTUAL_DISK_ID, "VirtualDiskId", 16),
301            (&StandardItems::LOGICAL_SECTOR_SIZE, "LogicalSectorSize", 4),
302            (
303                &StandardItems::PHYSICAL_SECTOR_SIZE,
304                "PhysicalSectorSize",
305                4,
306            ),
307        ];
308        for &(guid, name, min_len) in known_items {
309            if let Ok(entry) = table.entry(guid)
310                && entry.length() > 0
311                && entry.length() < min_len
312            {
313                Self::push_issue(
314                    issues,
315                    ValidationIssue::new(
316                        "metadata",
317                        "METADATA_ITEM_CORRUPTED",
318                        format!(
319                            "{name}: data length {} < expected minimum {} bytes",
320                            entry.length(),
321                            min_len
322                        ),
323                        "MS-VHDX/2.6.2",
324                    ),
325                );
326            }
327        }
328    }
329
330    /// Validate that all required metadata items are present.
331    ///
332    /// Required items (MS-VHDX ยง2.6.2):
333    /// - `FileParameters`
334    /// - `VirtualDiskSize`
335    /// - `VirtualDiskId`
336    /// - `LogicalSectorSize`
337    /// - `PhysicalSectorSize`
338    /// - `ParentLocator` (if differencing disk)
339    ///
340    /// # Errors
341    ///
342    /// Returns an error when required metadata entries or required payloads are
343    /// missing.
344    pub fn validate_required_metadata_items(&self) -> Result<Vec<ValidationIssue>> {
345        let mut issues = Vec::new();
346        let Some(meta_data) = self.metadata_region() else {
347            return Ok(issues);
348        };
349
350        let meta = crate::metadata::Metadata::new(meta_data)?;
351        let items = meta.items();
352
353        Self::validate_required_metadata_core(&meta, &items, &mut issues)?;
354        self.validate_required_parent_locator_item(&meta, &items, &mut issues)?;
355
356        Ok(issues)
357    }
358
359    fn validate_required_metadata_core(
360        meta: &crate::metadata::Metadata<'_>, items: &crate::metadata::MetadataItems<'_>,
361        issues: &mut Vec<ValidationIssue>,
362    ) -> Result<()> {
363        let required_items: &[(&Guid, &str)] = &[
364            (&StandardItems::FILE_PARAMETERS, "FileParameters"),
365            (&StandardItems::VIRTUAL_DISK_SIZE, "VirtualDiskSize"),
366            (&StandardItems::VIRTUAL_DISK_ID, "VirtualDiskId"),
367            (&StandardItems::LOGICAL_SECTOR_SIZE, "LogicalSectorSize"),
368            (&StandardItems::PHYSICAL_SECTOR_SIZE, "PhysicalSectorSize"),
369        ];
370        for (guid, name) in required_items {
371            Self::ensure_required_metadata_entry_present(meta, guid, name, issues)?;
372            Self::ensure_required_metadata_item_data_present(items, guid, name, issues)?;
373        }
374        Ok(())
375    }
376
377    fn ensure_required_metadata_entry_present(
378        meta: &crate::metadata::Metadata<'_>, guid: &Guid, name: &str,
379        issues: &mut Vec<ValidationIssue>,
380    ) -> Result<()> {
381        if meta.table().entry(guid).is_err() {
382            Self::push_issue(
383                issues,
384                ValidationIssue::new(
385                    "metadata_required",
386                    "METADATA_REQUIRED_MISSING",
387                    format!("{name} entry not found in metadata table"),
388                    "RELAX",
389                ),
390            );
391            return Err(Error::MetadataRequiredMissing { guid: *guid });
392        }
393        Ok(())
394    }
395
396    fn ensure_required_metadata_item_data_present(
397        items: &crate::metadata::MetadataItems<'_>, guid: &Guid, name: &str,
398        issues: &mut Vec<ValidationIssue>,
399    ) -> Result<()> {
400        match name {
401            "FileParameters" => Self::ensure_file_parameters_data(items, issues),
402            "VirtualDiskSize" if items.virtual_disk_size().is_err() => {
403                Self::push_required_data_missing(issues, name, *guid)
404            }
405            "VirtualDiskId" if items.virtual_disk_id().is_err() => {
406                Self::push_required_data_missing(issues, name, *guid)
407            }
408            "LogicalSectorSize" if items.logical_sector_size().is_err() => {
409                Self::push_required_data_missing(issues, name, *guid)
410            }
411            "PhysicalSectorSize" if items.physical_sector_size().is_err() => {
412                Self::push_required_data_missing(issues, name, *guid)
413            }
414            _ => Ok(()),
415        }
416    }
417
418    fn ensure_file_parameters_data(
419        items: &crate::metadata::MetadataItems<'_>, issues: &mut Vec<ValidationIssue>,
420    ) -> Result<()> {
421        let Ok(fp) = items.file_parameters() else {
422            Self::push_issue(
423                issues,
424                ValidationIssue::new(
425                    "metadata_required",
426                    "METADATA_REQUIRED_MISSING",
427                    "FileParameters data not present",
428                    "RELAX",
429                ),
430            );
431            return Err(Error::MetadataRequiredMissing {
432                guid: StandardItems::FILE_PARAMETERS,
433            });
434        };
435        if fp.has_reserved_bits_set() {
436            let fp_flags = fp.flags();
437            Self::push_issue(
438                issues,
439                ValidationIssue::new(
440                    "metadata_required",
441                    "METADATA_FILE_PARAMETERS_RESERVED_FLAGS",
442                    format!("FileParameters reserved flags (bits 2-31) are set: {fp_flags:#010x}"),
443                    "MS-VHDX/2.6.2.1",
444                ),
445            );
446            return Err(Error::FileParametersReservedFlags { flags: fp_flags });
447        }
448        Ok(())
449    }
450
451    fn push_required_data_missing(
452        issues: &mut Vec<ValidationIssue>, name: &str, guid: Guid,
453    ) -> Result<()> {
454        Self::push_issue(
455            issues,
456            ValidationIssue::new(
457                "metadata_required",
458                "METADATA_REQUIRED_MISSING",
459                format!("{name} data not present"),
460                "RELAX",
461            ),
462        );
463        Err(Error::MetadataRequiredMissing { guid })
464    }
465
466    fn validate_required_parent_locator_item(
467        &self, meta: &crate::metadata::Metadata<'_>, items: &crate::metadata::MetadataItems<'_>,
468        issues: &mut Vec<ValidationIssue>,
469    ) -> Result<()> {
470        if !self.has_parent() {
471            return Ok(());
472        }
473        if meta.table().entry(&StandardItems::PARENT_LOCATOR).is_err() {
474            Self::push_issue(
475                issues,
476                ValidationIssue::new(
477                    "metadata_required",
478                    "METADATA_REQUIRED_MISSING",
479                    "ParentLocator entry not found for differencing disk",
480                    "RELAX",
481                ),
482            );
483            return Err(Error::MetadataRequiredMissing {
484                guid: StandardItems::PARENT_LOCATOR,
485            });
486        }
487        if items.parent_locator().is_err() {
488            Self::push_issue(
489                issues,
490                ValidationIssue::new(
491                    "metadata_required",
492                    "METADATA_REQUIRED_MISSING",
493                    "ParentLocator data not present for differencing disk",
494                    "RELAX",
495                ),
496            );
497            return Err(Error::MetadataRequiredMissing {
498                guid: StandardItems::PARENT_LOCATOR,
499            });
500        }
501        Ok(())
502    }
503}