vhdx/header/core.rs
1//! Header section parser for VHDX files.
2//!
3//! Implements zero-copy parsing of the 1 MB header section:
4//! - File Type Identifier (offset 0, 64 KB aligned)
5//! - Header 1 (offset 64 KB, 4 KB structure, 64 KB aligned)
6//! - Header 2 (offset 128 KB, 4 KB structure, 64 KB aligned)
7//! - Region Table 1 (offset 192 KB, 64 KB)
8//! - Region Table 2 (offset 256 KB, 64 KB)
9
10use bitvec::prelude::*;
11
12use crate::constants::{
13 CREATOR_SIZE, HEADER_SIGNATURE, HEADER_SIZE, HEADER1_OFFSET, HEADER2_OFFSET,
14 MAX_REGION_ENTRIES, REGION_ENTRY_SIZE, REGION_SIGNATURE, REGION_TABLE_SIZE,
15 REGION_TABLE1_OFFSET, REGION_TABLE2_OFFSET, RT_HEADER_SIZE,
16};
17use crate::error::{Error, Result, SignaturePosition};
18use crate::types::{Crc32c, Guid};
19
20// ---------------------------------------------------------------------------
21// Header (top-level section view)
22// ---------------------------------------------------------------------------
23
24/// View over the 1 MB header section of a VHDX file.
25///
26/// Borrows a slice of the file's header buffer and provides validated access
27/// to the file type identifier, headers, and region tables.
28#[derive(Clone, Copy)]
29pub struct Header<'a> {
30 data: &'a [u8],
31}
32
33impl<'a> Header<'a> {
34 /// Create a new `Header` view over the given buffer.
35 ///
36 /// The buffer must be at least 320 KB (covering through Region Table 2).
37 /// In practice it should be the full 1 MB header section.
38 pub(crate) fn new(data: &'a [u8]) -> Result<Self> {
39 let min_size = (REGION_TABLE2_OFFSET + REGION_TABLE_SIZE) as usize;
40 if data.len() < min_size {
41 return Err(Error::InvalidFile(format!(
42 "header section too small: {} bytes, need at least {min_size}",
43 data.len()
44 )));
45 }
46 Ok(Self { data })
47 }
48
49 /// Return the file type identifier.
50 #[must_use]
51 pub fn file_type(&self) -> FileTypeIdentifier<'a> {
52 FileTypeIdentifier { data: self.data }
53 }
54
55 /// Return a header structure.
56 ///
57 /// - `index = 0`: returns the **current** header (the one with the higher
58 /// sequence number among the two valid headers).
59 /// - `index = 1`: returns Header 1 (physical offset 64 KB).
60 /// - `index = 2`: returns Header 2 (physical offset 128 KB).
61 ///
62 /// # Errors
63 ///
64 /// Returns an error if the index is invalid or the requested header fails
65 /// signature/checksum validation.
66 pub fn header(&self, index: usize) -> Result<HeaderStructure<'a>> {
67 match index {
68 0 => self.current_header(),
69 1 => self.validate_header_at(HEADER1_OFFSET as usize),
70 2 => self.validate_header_at(HEADER2_OFFSET as usize),
71 _ => Err(Error::InvalidParameter(format!(
72 "header index must be 0, 1, or 2, got {index}"
73 ))),
74 }
75 }
76
77 /// Return a region table.
78 ///
79 /// - `index = 0`: returns the region table corresponding to the current header.
80 /// - `index = 1`: returns Region Table 1 (physical offset 192 KB).
81 /// - `index = 2`: returns Region Table 2 (physical offset 256 KB).
82 ///
83 /// # Errors
84 ///
85 /// Returns an error if the index is invalid or the requested table fails
86 /// signature/checksum validation.
87 pub fn region_table(&self, index: usize) -> Result<RegionTable<'a>> {
88 match index {
89 0 => self.current_region_table(),
90 1 => self.validate_region_table_at(REGION_TABLE1_OFFSET as usize),
91 2 => self.validate_region_table_at(REGION_TABLE2_OFFSET as usize),
92 _ => Err(Error::InvalidParameter(format!(
93 "region table index must be 0, 1, or 2, got {index}"
94 ))),
95 }
96 }
97
98 // -- Internal helpers ---------------------------------------------------
99
100 /// Parse and validate the header at the given byte offset.
101 fn validate_header_at(&self, offset: usize) -> Result<HeaderStructure<'a>> {
102 let slice = &self.data[offset..][..HEADER_SIZE as usize];
103
104 // Check signature.
105 if slice[..4].view_bits::<Lsb0>() != *HEADER_SIGNATURE {
106 let mut found: [u8; 8] = [0; 8];
107 found[..4].copy_from_slice(&slice[..4]);
108 let mut expected: [u8; 8] = [0; 8];
109 expected[..4].copy_from_slice(&HEADER_SIGNATURE.into_inner().to_le_bytes());
110 return Err(Error::InvalidSignature {
111 position: SignaturePosition::Header,
112 expected,
113 found,
114 });
115 }
116
117 let stored_crc = u32::from_le_bytes(slice[4..8].try_into().unwrap());
118 let computed_crc = crate::common::crc32c_zeroed_checksum(slice);
119
120 if computed_crc != stored_crc {
121 return Err(Error::InvalidChecksum {
122 expected: stored_crc,
123 actual: computed_crc,
124 });
125 }
126
127 Ok(HeaderStructure { data: slice })
128 }
129
130 /// Determine the current header by comparing sequence numbers.
131 fn current_header(&self) -> Result<HeaderStructure<'a>> {
132 let h1 = self.validate_header_at(HEADER1_OFFSET as usize);
133 let h2 = self.validate_header_at(HEADER2_OFFSET as usize);
134
135 match (h1, h2) {
136 (Ok(h1), Ok(h2)) => {
137 if h1.sequence_number() == h2.sequence_number() {
138 return Err(Error::HeaderSequenceNumberInvalid {
139 sequence_number_1: h1.sequence_number(),
140 sequence_number_2: h2.sequence_number(),
141 });
142 }
143 if h1.sequence_number() > h2.sequence_number() {
144 Ok(h1)
145 } else {
146 Ok(h2)
147 }
148 }
149 (Ok(h1), Err(_)) => Ok(h1),
150 (Err(_), Ok(h2)) => Ok(h2),
151 (Err(e1), Err(_)) => Err(Error::CorruptedHeader(format!(
152 "both headers are invalid: {e1}"
153 ))),
154 }
155 }
156
157 /// Return the index (1 or 2) of the current header.
158 fn current_header_index(&self) -> Result<usize> {
159 let h1 = self.validate_header_at(HEADER1_OFFSET as usize);
160 let h2 = self.validate_header_at(HEADER2_OFFSET as usize);
161
162 match (h1, h2) {
163 (Ok(h1), Ok(h2)) => {
164 if h1.sequence_number() == h2.sequence_number() {
165 return Err(Error::HeaderSequenceNumberInvalid {
166 sequence_number_1: h1.sequence_number(),
167 sequence_number_2: h2.sequence_number(),
168 });
169 }
170 if h1.sequence_number() > h2.sequence_number() {
171 Ok(1)
172 } else {
173 Ok(2)
174 }
175 }
176 (Ok(_), Err(_)) => Ok(1),
177 (Err(_), Ok(_)) => Ok(2),
178 (Err(e1), Err(_)) => Err(Error::CorruptedHeader(format!(
179 "both headers are invalid: {e1}"
180 ))),
181 }
182 }
183
184 /// Parse and validate a region table at the given byte offset.
185 fn validate_region_table_at(&self, offset: usize) -> Result<RegionTable<'a>> {
186 let slice = &self.data[offset..][..REGION_TABLE_SIZE as usize];
187
188 // Check signature.
189 if slice[..4].view_bits::<Lsb0>() != *REGION_SIGNATURE {
190 let mut found: [u8; 8] = [0; 8];
191 found[..4].copy_from_slice(&slice[..4]);
192 let mut expected: [u8; 8] = [0; 8];
193 expected[..4].copy_from_slice(®ION_SIGNATURE.into_inner().to_le_bytes());
194 return Err(Error::InvalidSignature {
195 position: SignaturePosition::RegionTable,
196 expected,
197 found,
198 });
199 }
200
201 let stored_crc = u32::from_le_bytes(slice[4..8].try_into().unwrap());
202 let computed_crc = crate::common::crc32c_zeroed_checksum(slice);
203
204 if computed_crc != stored_crc {
205 return Err(Error::InvalidChecksum {
206 expected: stored_crc,
207 actual: computed_crc,
208 });
209 }
210
211 // Validate entry count.
212 let entry_count = u32::from_le_bytes(slice[8..12].try_into().unwrap());
213 if entry_count > u32::from(MAX_REGION_ENTRIES) {
214 return Err(Error::InvalidRegionTable(format!(
215 "REGION_ENTRY_COUNT_EXCEEDS_MAXIMUM: entry count {entry_count} exceeds maximum of {MAX_REGION_ENTRIES}"
216 )));
217 }
218
219 // Check that entries fit within the 64 KB table.
220 let entries_start = 16; // after the 16-byte region table header
221 let entries_end = entries_start + entry_count as usize * REGION_ENTRY_SIZE as usize;
222 if entries_end > REGION_TABLE_SIZE as usize {
223 return Err(Error::InvalidRegionTable(format!(
224 "entry count {entry_count} overflows region table"
225 )));
226 }
227
228 Ok(RegionTable { data: slice })
229 }
230
231 /// Return the region table corresponding to the current header.
232 ///
233 /// Per spec, region table N corresponds to header N. The current header
234 /// determines the current region table.
235 fn current_region_table(&self) -> Result<RegionTable<'a>> {
236 let idx = self.current_header_index()?;
237 let offset = match idx {
238 1 => REGION_TABLE1_OFFSET as usize,
239 2 => REGION_TABLE2_OFFSET as usize,
240 _ => unreachable!(),
241 };
242 self.validate_region_table_at(offset)
243 }
244}
245
246// ---------------------------------------------------------------------------
247// FileTypeIdentifier
248// ---------------------------------------------------------------------------
249
250/// View over the file type identifier (first 64 KB of the VHDX file).
251///
252/// Contains the 8-byte "vhdxfile" signature and a 512-byte UTF-16 creator string.
253pub struct FileTypeIdentifier<'a> {
254 data: &'a [u8],
255}
256
257impl<'a> FileTypeIdentifier<'a> {
258 /// Return the 8-byte VHDX file signature ("vhdxfile").
259 ///
260 /// # Panics
261 ///
262 /// Cannot panic — the data slice is guaranteed to be at least 320 KB.
263 #[must_use]
264 pub fn signature(&self) -> &'a [u8; 8] {
265 self.data[..8].try_into().unwrap()
266 }
267
268 /// Return the 512-byte creator field as raw bytes (UTF-16 LE, possibly null-terminated).
269 ///
270 /// # Panics
271 ///
272 /// Panics if the header buffer is shorter than the required creator field.
273 #[must_use]
274 pub fn creator(&self) -> &'a [u8; 512] {
275 self.data[8..8 + CREATOR_SIZE as usize].try_into().unwrap()
276 }
277}
278
279// ---------------------------------------------------------------------------
280// HeaderStructure
281// ---------------------------------------------------------------------------
282
283/// View over a single 4 KB VHDX header structure.
284///
285/// Fields are parsed on demand from the underlying byte slice.
286pub struct HeaderStructure<'a> {
287 data: &'a [u8],
288}
289
290impl<'a> HeaderStructure<'a> {
291 /// Return the 4-byte header signature ("head").
292 ///
293 /// # Panics
294 ///
295 /// Panics if the header slice is shorter than 4 bytes.
296 #[must_use]
297 pub fn signature(&self) -> &'a [u8; 4] {
298 self.data[..4].try_into().unwrap()
299 }
300
301 /// Return the stored CRC-32C checksum.
302 ///
303 /// # Panics
304 ///
305 /// Panics if the header slice is shorter than 8 bytes.
306 #[must_use]
307 pub fn checksum(&self) -> Crc32c {
308 Crc32c::from_raw(u32::from_le_bytes(self.data[4..8].try_into().unwrap()))
309 }
310
311 /// Return the sequence number.
312 ///
313 /// The header with the higher sequence number is considered current.
314 ///
315 /// # Panics
316 ///
317 /// Panics if the header slice is shorter than 16 bytes.
318 #[must_use]
319 pub fn sequence_number(&self) -> u64 {
320 u64::from_le_bytes(self.data[8..16].try_into().unwrap())
321 }
322
323 /// Return the file write GUID.
324 ///
325 /// # Panics
326 ///
327 /// Panics if the header slice is shorter than 32 bytes.
328 #[must_use]
329 pub fn file_write_guid(&self) -> Guid {
330 Guid::from_bytes(self.data[16..32].try_into().unwrap())
331 }
332
333 /// Return the data write GUID.
334 ///
335 /// # Panics
336 ///
337 /// Panics if the header slice is shorter than 48 bytes.
338 #[must_use]
339 pub fn data_write_guid(&self) -> Guid {
340 Guid::from_bytes(self.data[32..48].try_into().unwrap())
341 }
342
343 /// Return the log GUID.
344 ///
345 /// # Panics
346 ///
347 /// Panics if the header slice is shorter than 64 bytes.
348 #[must_use]
349 pub fn log_guid(&self) -> Guid {
350 Guid::from_bytes(self.data[48..64].try_into().unwrap())
351 }
352
353 /// Return the log format version.
354 ///
355 /// Per MS-VHDX §2.2.2, this field MUST be 0. The library enforces this
356 /// requirement when a log is active (non-zero [`LogGuid`](Self::log_guid));
357 /// see [`validate_header`](crate::validation::SpecValidator::validate_header).
358 ///
359 /// # Panics
360 ///
361 /// Panics if the header slice is shorter than 66 bytes.
362 #[must_use]
363 pub fn log_version(&self) -> u16 {
364 u16::from_le_bytes(self.data[64..66].try_into().unwrap())
365 }
366
367 /// Return the VHDX format version (must be 1 per spec).
368 ///
369 /// # Panics
370 ///
371 /// Panics if the header slice is shorter than 68 bytes.
372 #[must_use]
373 pub fn version(&self) -> u16 {
374 u16::from_le_bytes(self.data[66..68].try_into().unwrap())
375 }
376
377 /// Return the log length in bytes (must be a multiple of 1 MB).
378 ///
379 /// # Panics
380 ///
381 /// Panics if the header slice is shorter than 72 bytes.
382 #[must_use]
383 pub fn log_length(&self) -> u32 {
384 u32::from_le_bytes(self.data[68..72].try_into().unwrap())
385 }
386
387 /// Return the log offset in the file (must be a multiple of 1 MB).
388 ///
389 /// # Panics
390 ///
391 /// Panics if the header slice is shorter than 80 bytes.
392 #[must_use]
393 pub fn log_offset(&self) -> u64 {
394 u64::from_le_bytes(self.data[72..80].try_into().unwrap())
395 }
396}
397
398// ---------------------------------------------------------------------------
399// RegionTable
400// ---------------------------------------------------------------------------
401
402/// View over a 64 KB region table.
403///
404/// Provides access to the region table header and a zero-copy iterator over entries.
405pub struct RegionTable<'a> {
406 data: &'a [u8],
407}
408
409impl<'a> RegionTable<'a> {
410 /// Return the region table header.
411 #[must_use]
412 pub fn header(&self) -> RegionTableHeader<'a> {
413 RegionTableHeader {
414 data: &self.data[..RT_HEADER_SIZE as usize],
415 }
416 }
417
418 /// Return a zero-copy iterator over region table entries.
419 pub fn entries(&self) -> impl Iterator<Item = RegionTableEntry<'a>> + '_ {
420 let count = self.header().entry_count() as usize;
421 (0..count).map(move |i| {
422 let offset = RT_HEADER_SIZE as usize + i * REGION_ENTRY_SIZE as usize;
423 RegionTableEntry {
424 data: &self.data[offset..][..REGION_ENTRY_SIZE as usize],
425 }
426 })
427 }
428}
429
430// ---------------------------------------------------------------------------
431// RegionTableHeader
432// ---------------------------------------------------------------------------
433
434/// View over the 16-byte region table header.
435pub struct RegionTableHeader<'a> {
436 data: &'a [u8],
437}
438
439impl<'a> RegionTableHeader<'a> {
440 /// Return the 4-byte signature ("regi").
441 ///
442 /// # Panics
443 ///
444 /// Panics if the region table header slice is shorter than 4 bytes.
445 #[must_use]
446 pub fn signature(&self) -> &'a [u8; 4] {
447 self.data[..4].try_into().unwrap()
448 }
449
450 /// Return the stored CRC-32C checksum.
451 ///
452 /// # Panics
453 ///
454 /// Panics if the region table header slice is shorter than 8 bytes.
455 #[must_use]
456 pub fn checksum(&self) -> Crc32c {
457 Crc32c::from_raw(u32::from_le_bytes(self.data[4..8].try_into().unwrap()))
458 }
459
460 /// Return the number of region table entries.
461 ///
462 /// # Panics
463 ///
464 /// Panics if the region table header slice is shorter than 12 bytes.
465 #[must_use]
466 pub fn entry_count(&self) -> u32 {
467 u32::from_le_bytes(self.data[8..12].try_into().unwrap())
468 }
469
470 /// Return the reserved field.
471 ///
472 /// # Panics
473 ///
474 /// Panics if the region table header slice is shorter than 16 bytes.
475 #[must_use]
476 pub fn reserved(&self) -> u32 {
477 u32::from_le_bytes(self.data[12..16].try_into().unwrap())
478 }
479}
480
481// ---------------------------------------------------------------------------
482// RegionTableEntry
483// ---------------------------------------------------------------------------
484
485/// View over a single 32-byte region table entry.
486pub struct RegionTableEntry<'a> {
487 data: &'a [u8],
488}
489
490impl RegionTableEntry<'_> {
491 /// Return the region GUID (16 bytes, mixed-endian RFC 4122 layout).
492 ///
493 /// # Panics
494 ///
495 /// Panics if the entry slice is shorter than 16 bytes.
496 #[must_use]
497 pub fn guid(&self) -> Guid {
498 Guid::from_bytes(self.data[..16].try_into().unwrap())
499 }
500
501 /// Return the byte offset of the region within the file.
502 ///
503 /// # Panics
504 ///
505 /// Panics if the entry slice is shorter than 24 bytes.
506 #[must_use]
507 pub fn file_offset(&self) -> u64 {
508 u64::from_le_bytes(self.data[16..24].try_into().unwrap())
509 }
510
511 /// Return the byte length of the region.
512 ///
513 /// # Panics
514 ///
515 /// Panics if the entry slice is shorter than 28 bytes.
516 #[must_use]
517 pub fn length(&self) -> u32 {
518 u32::from_le_bytes(self.data[24..28].try_into().unwrap())
519 }
520
521 /// Whether this region is required (bit 0 of the Required field per MS-VHDX §2.2.3).
522 ///
523 /// # Panics
524 ///
525 /// Panics if the entry slice is shorter than 32 bytes.
526 #[must_use]
527 pub fn required(&self) -> bool {
528 self.data[28..32].view_bits::<Lsb0>()[0]
529 }
530}