Skip to main content

vhdx/validation/
bat.rs

1use super::{Error, PayloadBlockState, Result, SectorBitmapState, SpecValidator, ValidationIssue};
2
3impl SpecValidator {
4    /// Validate the Block Allocation Table.
5    ///
6    /// Checks:
7    /// - Entry states are valid values
8    /// - State matches disk type (e.g., fixed disk has no Unmapped)
9    /// - Sector bitmap entries in non-differencing disks are `NotPresent`
10    /// - File offsets are aligned
11    ///
12    /// # Errors
13    ///
14    /// Returns an error when BAT structure or state rules are violated.
15    ///
16    /// # Panics
17    ///
18    /// Panics if integer conversion for minimum BAT entry count overflows `usize`
19    /// (should not occur with valid metadata).
20    pub fn validate_bat(&self) -> Result<Vec<ValidationIssue>> {
21        let mut issues = Vec::new();
22        let Some(bat_data) = self.bat_region() else {
23            return Ok(issues);
24        };
25
26        let chunk_ratio = self.chunk_ratio();
27        if chunk_ratio == 0 {
28            return Ok(issues); // Cannot validate without chunk ratio
29        }
30
31        let bat = crate::bat::Bat::new(bat_data, chunk_ratio);
32        let has_parent = self.has_parent();
33        let block_size = u64::from(self.block_size());
34
35        self.validate_bat_entry_count(&bat, block_size, &mut issues)?;
36        let mut seen_offsets = std::collections::HashSet::new();
37        for entry in bat.entries() {
38            Self::validate_bat_entry(
39                entry,
40                has_parent,
41                block_size,
42                &mut seen_offsets,
43                &mut issues,
44            )?;
45        }
46        Self::validate_bat_sector_bitmap_consistency(&bat, has_parent, chunk_ratio, &mut issues);
47
48        Ok(issues)
49    }
50
51    fn validate_bat_entry_count(
52        &self, bat: &crate::bat::Bat<'_>, block_size: u64, issues: &mut Vec<ValidationIssue>,
53    ) -> Result<()> {
54        let virtual_disk_size = self.virtual_disk_size();
55        if virtual_disk_size > 0 && block_size > 0 {
56            let min_entries = virtual_disk_size.div_ceil(block_size);
57            if bat.len() < usize::try_from(min_entries).expect("minimum BAT entries fit usize") {
58                Self::push_issue(
59                    issues,
60                    ValidationIssue::new(
61                        "bat",
62                        "BAT_ENTRY_COUNT_INSUFFICIENT",
63                        format!(
64                            "BAT has {} entries but virtual disk requires at least {}",
65                            bat.len(),
66                            min_entries
67                        ),
68                        "MS-VHDX/2.5",
69                    ),
70                );
71                return Err(Error::BatEntryCountInsufficient {
72                    actual: bat.len() as u64,
73                    expected: min_entries,
74                });
75            }
76        }
77        Ok(())
78    }
79
80    fn validate_bat_entry(
81        entry: crate::bat::BatEntry<'_>, has_parent: bool, block_size: u64,
82        seen_offsets: &mut std::collections::HashSet<u64>, issues: &mut Vec<ValidationIssue>,
83    ) -> Result<()> {
84        let raw_state = entry.raw_state();
85        if entry.is_sector_bitmap() {
86            return Self::validate_bat_sector_bitmap_entry(raw_state, entry, has_parent, issues);
87        }
88        Self::validate_bat_payload_entry(
89            raw_state,
90            entry,
91            has_parent,
92            block_size,
93            seen_offsets,
94            issues,
95        )
96    }
97
98    fn validate_bat_sector_bitmap_entry(
99        raw_state: u8, entry: crate::bat::BatEntry<'_>, has_parent: bool,
100        issues: &mut Vec<ValidationIssue>,
101    ) -> Result<()> {
102        let Some(sb_state) = entry.sector_bitmap_state() else {
103            Self::push_issue(
104                issues,
105                ValidationIssue::new(
106                    "bat",
107                    "BAT_SECTOR_BITMAP_INVALID_STATE",
108                    format!("invalid sector bitmap state: {raw_state}"),
109                    "MS-VHDX/2.5.1.2",
110                ),
111            );
112            return Err(Error::InvalidSectorBitmapState(raw_state));
113        };
114        if !has_parent && sb_state != SectorBitmapState::NotPresent {
115            Self::push_issue(
116                issues,
117                ValidationIssue::new(
118                    "bat",
119                    "BAT_ENTRY_STATE_MISMATCH",
120                    "sector bitmap state not NotPresent on non-differencing disk".to_string(),
121                    "MS-VHDX/2.5.1.1",
122                ),
123            );
124            return Err(Error::StateMismatch {
125                state: raw_state,
126                description: "sector bitmap state not NotPresent on non-differencing disk".into(),
127            });
128        }
129        Ok(())
130    }
131
132    fn validate_bat_payload_entry(
133        raw_state: u8, entry: crate::bat::BatEntry<'_>, has_parent: bool, block_size: u64,
134        seen_offsets: &mut std::collections::HashSet<u64>, issues: &mut Vec<ValidationIssue>,
135    ) -> Result<()> {
136        let Some(p_state) = entry.payload_state() else {
137            Self::push_issue(
138                issues,
139                ValidationIssue::new(
140                    "bat",
141                    "BAT_ENTRY_INVALID_STATE",
142                    format!("invalid payload block state: {raw_state}"),
143                    "MS-VHDX/2.5.1.1",
144                ),
145            );
146            return Err(Error::InvalidBlockState(raw_state));
147        };
148        Self::validate_bat_payload_state_for_disk_type(raw_state, p_state, has_parent, issues)?;
149        Self::validate_bat_payload_offset_alignment(entry, p_state, block_size, issues)?;
150        Self::validate_bat_payload_offset_uniqueness(entry, p_state, seen_offsets, issues)
151    }
152
153    fn validate_bat_payload_offset_alignment(
154        entry: crate::bat::BatEntry<'_>, p_state: PayloadBlockState, block_size: u64,
155        issues: &mut Vec<ValidationIssue>,
156    ) -> Result<()> {
157        match p_state {
158            PayloadBlockState::FullyPresent | PayloadBlockState::PartiallyPresent => {
159                let offset_mb = entry.file_offset_mb();
160                if block_size > 0 && offset_mb != 0 {
161                    let offset_bytes = offset_mb * 1024 * 1024;
162                    if !offset_bytes.is_multiple_of(block_size) {
163                        Self::push_issue(
164                            issues,
165                            ValidationIssue::new(
166                                "bat",
167                                "BAT_ENTRY_FILE_OFFSET_UNALIGNED",
168                                format!(
169                                    "payload block file offset {offset_mb} MB ({offset_bytes} bytes) not aligned to block size {block_size}"
170                                ),
171                                "MS-VHDX/2.5",
172                            ),
173                        );
174                        return Err(Error::BatFileOffsetUnaligned {
175                            offset_mb,
176                            block_size: u32::try_from(block_size).unwrap_or(u32::MAX),
177                        });
178                    }
179                }
180            }
181            _ => {}
182        }
183        Ok(())
184    }
185
186    fn validate_bat_payload_state_for_disk_type(
187        raw_state: u8, p_state: PayloadBlockState, has_parent: bool,
188        issues: &mut Vec<ValidationIssue>,
189    ) -> Result<()> {
190        if !has_parent {
191            match p_state {
192                PayloadBlockState::Unmapped | PayloadBlockState::PartiallyPresent => {
193                    Self::push_issue(
194                        issues,
195                        ValidationIssue::new(
196                            "bat",
197                            "BAT_ENTRY_STATE_MISMATCH",
198                            "payload state Unmapped/PartiallyPresent on non-differencing disk"
199                                .to_string(),
200                            "MS-VHDX/2.5.1.1",
201                        ),
202                    );
203                    return Err(Error::StateMismatch {
204                        state: raw_state,
205                        description:
206                            "payload state Unmapped/PartiallyPresent on non-differencing disk"
207                                .into(),
208                    });
209                }
210                _ => {}
211            }
212        }
213        Ok(())
214    }
215
216    fn validate_bat_payload_offset_uniqueness(
217        entry: crate::bat::BatEntry<'_>, p_state: PayloadBlockState,
218        seen_offsets: &mut std::collections::HashSet<u64>, issues: &mut Vec<ValidationIssue>,
219    ) -> Result<()> {
220        match p_state {
221            PayloadBlockState::FullyPresent | PayloadBlockState::PartiallyPresent => {
222                let offset_mb = entry.file_offset_mb();
223                if offset_mb != 0 && !seen_offsets.insert(offset_mb) {
224                    Self::push_issue(
225                        issues,
226                        ValidationIssue::new(
227                            "bat",
228                            "BAT_FILE_OFFSET_DUPLICATE",
229                            format!("duplicate file_offset_mb {offset_mb} in BAT"),
230                            "MS-VHDX/2.5",
231                        ),
232                    );
233                    return Err(Error::BatFileOffsetDuplicate { offset_mb });
234                }
235            }
236            _ => {}
237        }
238        Ok(())
239    }
240
241    fn validate_bat_sector_bitmap_consistency(
242        bat: &crate::bat::Bat<'_>, has_parent: bool, chunk_ratio: u64,
243        issues: &mut Vec<ValidationIssue>,
244    ) {
245        if !has_parent {
246            return;
247        }
248        let stride = chunk_ratio + 1;
249        let total_entries = bat.len() as u64;
250        let num_chunks = total_entries / stride;
251        for chunk_idx in 0..num_chunks {
252            if !Self::chunk_has_partially_present_payload(
253                bat,
254                chunk_idx,
255                stride,
256                chunk_ratio,
257                total_entries,
258            ) {
259                continue;
260            }
261            let sb_bat_idx = chunk_idx * stride + chunk_ratio;
262            if sb_bat_idx >= total_entries {
263                break;
264            }
265            let Ok(sb_entry) = bat.entry(sb_bat_idx) else {
266                break;
267            };
268            let sb_state = sb_entry.sector_bitmap_state();
269            if !matches!(sb_state, Some(crate::bat::SectorBitmapState::Present)) {
270                Self::push_issue(
271                    issues,
272                    ValidationIssue::new(
273                        "bat",
274                        "BAT_SECTOR_BITMAP_INVALID_STATE",
275                        format!(
276                            "chunk {chunk_idx}: payload entry is PartiallyPresent but sector bitmap state is {sb_state:?}"
277                        ),
278                        "MS-VHDX/2.5.1.2",
279                    ),
280                );
281            }
282        }
283    }
284
285    fn chunk_has_partially_present_payload(
286        bat: &crate::bat::Bat<'_>, chunk_idx: u64, stride: u64, chunk_ratio: u64,
287        total_entries: u64,
288    ) -> bool {
289        for payload_offset_in_chunk in 0..chunk_ratio {
290            let payload_bat_idx = chunk_idx * stride + payload_offset_in_chunk;
291            if payload_bat_idx >= total_entries {
292                break;
293            }
294            let Ok(payload_entry) = bat.entry(payload_bat_idx) else {
295                continue;
296            };
297            if !payload_entry.is_sector_bitmap()
298                && let Some(crate::bat::PayloadBlockState::PartiallyPresent) =
299                    payload_entry.payload_state()
300            {
301                return true;
302            }
303        }
304        false
305    }
306}