Skip to main content

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(&REGION_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}