Skip to main content

vhdx/validation/
parent.rs

1use super::{
2    BAT_REGION_GUID, Error, Guid, Header, METADATA_REGION_GUID, Result, SpecValidator,
3    ValidationIssue,
4};
5
6impl SpecValidator {
7    /// Validate the parent locator for differencing disks.
8    ///
9    /// # Errors
10    ///
11    /// Returns an error when parent locator keys, paths, or linkage are invalid.
12    pub fn validate_parent_locator(&self) -> Result<Vec<ValidationIssue>> {
13        let mut issues = Vec::new();
14        let Some(meta_data) = self.metadata_region() else {
15            return Ok(issues);
16        };
17
18        let meta = crate::metadata::Metadata::new(meta_data)?;
19        let Ok(locator) = meta.items().parent_locator() else {
20            return Ok(issues);
21        };
22        Self::validate_parent_locator_keys(&locator, &mut issues)?;
23
24        Ok(issues)
25    }
26
27    fn validate_parent_locator_keys(
28        locator: &crate::metadata::ParentLocator<'_>, issues: &mut Vec<ValidationIssue>,
29    ) -> Result<Option<Guid>> {
30        let kv_data = locator.key_value_data();
31        let mut has_parent_linkage = false;
32        let mut has_path = false;
33        let mut parent_linkage_guid: Option<Guid> = None;
34        for kv in locator.entries() {
35            let key = kv.key(kv_data)?;
36            match key.as_str() {
37                "parent_linkage" => {
38                    has_parent_linkage = true;
39                    if let Ok(value) = kv.value(kv_data) {
40                        parent_linkage_guid = parse_guid_from_braced_string(&value);
41                    }
42                }
43                "parent_linkage2" => {
44                    Self::push_issue(
45                        issues,
46                        ValidationIssue::new(
47                            "parent_locator",
48                            "PARENT_LOCATOR_LINKAGE2_CONFLICT",
49                            "parent_linkage2 present",
50                            "MS-VHDX/2.6.2.6.3",
51                        ),
52                    );
53                    return Err(Error::ParentLocatorLinkage2Conflict);
54                }
55                "relative_path" | "volume_path" | "absolute_win32_path" => {
56                    has_path = true;
57                }
58                _ => {}
59            }
60        }
61        if !has_parent_linkage {
62            Self::push_issue(
63                issues,
64                ValidationIssue::new(
65                    "parent_locator",
66                    "PARENT_LOCATOR_MISSING_LINKAGE",
67                    "parent_linkage key not found",
68                    "MS-VHDX/2.6.2.6.3",
69                ),
70            );
71            return Err(Error::ParentLocatorMissingLinkage);
72        }
73        if parent_linkage_guid.is_none() {
74            Self::push_issue(
75                issues,
76                ValidationIssue::new(
77                    "parent_locator",
78                    "PARENT_LOCATOR_FORMAT_ERROR",
79                    "parent_linkage value is not a valid GUID format",
80                    "VALEXT",
81                ),
82            );
83            return Err(Error::InvalidParentLocator(
84                "parent_linkage value is not a valid GUID format".into(),
85            ));
86        }
87        if !has_path {
88            Self::push_issue(
89                issues,
90                ValidationIssue::new(
91                    "parent_locator",
92                    "PARENT_LOCATOR_NO_VALID_PATH",
93                    "no valid parent path (relative_path/volume_path/absolute_win32_path)",
94                    "MS-VHDX/2.6.2.6.3",
95                ),
96            );
97            return Err(Error::ParentNotFound);
98        }
99        Ok(parent_linkage_guid)
100    }
101
102    // -----------------------------------------------------------------------
103    // Internal helpers
104    // -----------------------------------------------------------------------
105
106    /// Parse the header section from the data buffer.
107    pub(super) fn parse_header(&self) -> Result<Header<'_>> {
108        Header::new(&self.data)
109    }
110
111    /// Resolve the log region slice from the data buffer.
112    pub(super) fn log_region(&self) -> Option<&[u8]> {
113        let header = self.parse_header().ok()?;
114        let current = header.header(0).ok()?;
115        let log_offset = usize::try_from(current.log_offset()).ok()?;
116        let log_length = usize::try_from(current.log_length()).ok()?;
117
118        if log_offset == 0 && log_length == 0 {
119            return None;
120        }
121
122        let end = log_offset.checked_add(log_length)?;
123        if end > self.data.len() {
124            return None;
125        }
126
127        Some(&self.data[log_offset..end])
128    }
129
130    /// Resolve a region by GUID from the current region table.
131    pub(super) fn region_for_guid(&self, guid: &Guid) -> Option<&[u8]> {
132        let header = self.parse_header().ok()?;
133        let rt = header.region_table(0).ok()?;
134        for entry in rt.entries() {
135            if entry.guid() == *guid {
136                let offset = usize::try_from(entry.file_offset()).ok()?;
137                let length = usize::try_from(entry.length()).ok()?;
138                let end = offset.checked_add(length)?;
139                if end <= self.data.len() {
140                    return Some(&self.data[offset..end]);
141                }
142            }
143        }
144        None
145    }
146
147    /// Resolve the BAT region data.
148    pub(super) fn bat_region(&self) -> Option<&[u8]> {
149        self.region_for_guid(&BAT_REGION_GUID)
150    }
151
152    /// Resolve the metadata region data.
153    pub(super) fn metadata_region(&self) -> Option<&[u8]> {
154        self.region_for_guid(&METADATA_REGION_GUID)
155    }
156
157    /// Determine whether this is a differencing disk (`has_parent` flag).
158    pub(super) fn has_parent(&self) -> bool {
159        if let Some(meta_data) = self.metadata_region()
160            && let Ok(meta) = crate::metadata::Metadata::new(meta_data)
161            && let Ok(fp) = meta.items().file_parameters()
162        {
163            return fp.has_parent();
164        }
165        false
166    }
167
168    /// Extract the current header's `LogGuid`.
169    pub(super) fn current_log_guid(header: &Header<'_>) -> Result<Guid> {
170        let current = header.header(0)?;
171        Ok(current.log_guid())
172    }
173
174    /// Compute the chunk ratio for BAT interpretation.
175    pub(super) fn chunk_ratio(&self) -> u64 {
176        let block_size = u64::from(self.block_size());
177        let logical_sector_size = u64::from(self.logical_sector_size());
178        if block_size == 0 || logical_sector_size == 0 {
179            return 0;
180        }
181        crate::common::compute_chunk_ratio(block_size, logical_sector_size)
182    }
183
184    /// Get block size from metadata.
185    pub(super) fn block_size(&self) -> u32 {
186        if let Some(meta_data) = self.metadata_region()
187            && let Ok(meta) = crate::metadata::Metadata::new(meta_data)
188            && let Ok(fp) = meta.items().file_parameters()
189        {
190            return fp.block_size();
191        }
192        0
193    }
194
195    /// Get logical sector size from metadata.
196    pub(super) fn logical_sector_size(&self) -> u32 {
197        if let Some(meta_data) = self.metadata_region()
198            && let Ok(meta) = crate::metadata::Metadata::new(meta_data)
199            && let Ok(lss) = meta.items().logical_sector_size()
200        {
201            return lss;
202        }
203        0
204    }
205
206    /// Get virtual disk size from metadata.
207    pub(super) fn virtual_disk_size(&self) -> u64 {
208        if let Some(meta_data) = self.metadata_region()
209            && let Ok(meta) = crate::metadata::Metadata::new(meta_data)
210            && let Ok(vds) = meta.items().virtual_disk_size()
211        {
212            return vds;
213        }
214        0
215    }
216}
217
218// ---------------------------------------------------------------------------
219// Helpers
220// ---------------------------------------------------------------------------
221
222/// Parse a GUID from a braced lowercase hex string like
223/// `{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}`.
224///
225/// Returns `None` if the string is not in the expected format.
226fn parse_guid_from_braced_string(s: &str) -> Option<Guid> {
227    let s = s.trim();
228    // Strip enclosing braces
229    let inner = s.strip_prefix('{').and_then(|s| s.strip_suffix('}'))?;
230    // Remove hyphens and parse as 32 hex digits
231    let hex: String = inner.chars().filter(|c| *c != '-').collect();
232    if hex.len() != 32 {
233        return None;
234    }
235    let mut bytes = [0u8; 16];
236    for i in 0..16 {
237        let byte_str = &hex[i * 2..i * 2 + 2];
238        bytes[i] = u8::from_str_radix(byte_str, 16).ok()?;
239    }
240    Some(Guid::from_bytes(bytes))
241}