Skip to main content

vhdx/
bat.rs

1//! BAT (Block Allocation Table) section parser.
2//!
3//! The BAT is an array of 64-bit entries mapping virtual disk blocks to file offsets.
4//! Payload block entries and sector bitmap block entries are interleaved based on the
5//! chunk ratio: every `chunk_ratio` payload entries is followed by 1 sector bitmap entry.
6
7use bitvec::prelude::*;
8use std::borrow::Cow;
9
10use crate::error::{Error, Result};
11
12/// Payload block state (MS-VHDX §2.5.1.1).
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum PayloadBlockState {
15    /// Block not present; contents undefined. `FileOffsetMB` reserved.
16    NotPresent = 0,
17    /// Block contents undefined; may contain arbitrary data.
18    Undefined = 1,
19    /// Block contents are zero.
20    Zero = 2,
21    /// Block was unmapped; contents no longer relied upon.
22    Unmapped = 3,
23    /// Block fully present at `FileOffsetMB`.
24    FullyPresent = 6,
25    /// Block partially present (differencing only); sector bitmap required.
26    PartiallyPresent = 7,
27}
28
29impl PayloadBlockState {
30    /// Interpret a raw 3-bit state value as a payload block state.
31    ///
32    /// Returns `None` for reserved values (4, 5).
33    pub(crate) fn from_raw(raw: u8) -> Option<Self> {
34        match raw {
35            0 => Some(Self::NotPresent),
36            1 => Some(Self::Undefined),
37            2 => Some(Self::Zero),
38            3 => Some(Self::Unmapped),
39            6 => Some(Self::FullyPresent),
40            7 => Some(Self::PartiallyPresent),
41            _ => None,
42        }
43    }
44}
45
46/// Sector bitmap block state (MS-VHDX §2.5.1.2).
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum SectorBitmapState {
49    /// Block not allocated; contents undefined.
50    NotPresent = 0,
51    /// Block present at `FileOffsetMB` (differencing only).
52    Present = 6,
53}
54
55impl SectorBitmapState {
56    /// Interpret a raw 3-bit state value as a sector bitmap state.
57    ///
58    /// Returns `None` for reserved values (1-5, 7).
59    pub(crate) fn from_raw(raw: u8) -> Option<Self> {
60        match raw {
61            0 => Some(Self::NotPresent),
62            6 => Some(Self::Present),
63            _ => None,
64        }
65    }
66}
67
68/// Interpreted BAT entry state, distinguishing payload vs sector bitmap.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum BatState {
71    /// Entry describes a payload block.
72    Payload(PayloadBlockState),
73    /// Entry describes a sector bitmap block.
74    SectorBitmap(SectorBitmapState),
75}
76
77/// Zero-copy view into a single 64-bit BAT entry.
78///
79/// Each entry encodes a 3-bit state field and a 44-bit file offset (in MB).
80/// The entry also carries whether it represents a payload block or sector bitmap
81/// block, determined by its position and the chunk ratio.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct BatEntry<'a> {
84    /// Raw 8 bytes from the BAT buffer.
85    bytes: &'a [u8; 8],
86    /// Whether this entry is a sector bitmap block entry (vs payload).
87    is_sector_bitmap: bool,
88}
89
90impl BatEntry<'_> {
91    /// Raw 3-bit state value (bits 0-2).
92    #[inline]
93    pub(crate) fn raw_state(&self) -> u8 {
94        self.bytes.view_bits::<Lsb0>()[0..3].load::<u8>()
95    }
96
97    /// File offset in MB (bits 20-63, 44-bit field).
98    #[inline]
99    #[must_use]
100    pub fn file_offset_mb(&self) -> u64 {
101        self.bytes.view_bits::<Lsb0>()[20..64].load::<u64>()
102    }
103
104    /// Interpret the raw state as a payload block state.
105    ///
106    /// Returns `None` for reserved values (4, 5).
107    pub(crate) fn payload_state(&self) -> Option<PayloadBlockState> {
108        PayloadBlockState::from_raw(self.raw_state())
109    }
110
111    /// Interpret the raw state as a sector bitmap state.
112    ///
113    /// Returns `None` for reserved values (1-5, 7).
114    pub(crate) fn sector_bitmap_state(&self) -> Option<SectorBitmapState> {
115        SectorBitmapState::from_raw(self.raw_state())
116    }
117
118    /// Typed state, resolved using the entry type determined by chunk-ratio interleaving.
119    ///
120    /// Returns an error for reserved/invalid raw state values (4, 5 for payload;
121    /// 1-5, 7 for sector bitmap).
122    ///
123    /// # Errors
124    ///
125    /// Returns [`Error::InvalidBlockState`] for reserved/invalid payload states,
126    /// or [`Error::InvalidSectorBitmapState`] for reserved/invalid sector bitmap states.
127    pub fn state(&self) -> Result<BatState> {
128        let raw = self.raw_state();
129        if self.is_sector_bitmap {
130            match SectorBitmapState::from_raw(raw) {
131                Some(s) => Ok(BatState::SectorBitmap(s)),
132                None => Err(Error::InvalidSectorBitmapState(raw)),
133            }
134        } else {
135            match PayloadBlockState::from_raw(raw) {
136                Some(s) => Ok(BatState::Payload(s)),
137                None => Err(Error::InvalidBlockState(raw)),
138            }
139        }
140    }
141
142    /// Whether this entry represents a sector bitmap block (vs a payload block).
143    pub(crate) fn is_sector_bitmap(&self) -> bool {
144        self.is_sector_bitmap
145    }
146}
147
148/// BAT (Block Allocation Table) — zero-copy parser over a raw byte buffer.
149///
150/// The BAT entries are interleaved: every `chunk_ratio` payload block entries
151/// is followed by one sector bitmap block entry.
152#[derive(Clone)]
153pub struct Bat<'a> {
154    /// Raw BAT region bytes.
155    data: Cow<'a, [u8]>,
156    /// Chunk ratio = (2²³ × `LogicalSectorSize`) / `BlockSize`.
157    chunk_ratio: u64,
158}
159
160impl<'a> Bat<'a> {
161    /// Create a new BAT parser from raw bytes and chunk ratio.
162    ///
163    /// `chunk_ratio` determines how payload and sector bitmap entries are interleaved.
164    /// It is calculated as `(2^23 * LogicalSectorSize) / BlockSize`.
165    pub(crate) fn new(data: &'a [u8], chunk_ratio: u64) -> Self {
166        Self {
167            data: Cow::Borrowed(data),
168            chunk_ratio,
169        }
170    }
171
172    /// Total number of 64-bit entries in the BAT buffer.
173    #[must_use]
174    pub(crate) fn len(&self) -> usize {
175        self.data.len() / 8
176    }
177
178    /// Whether the BAT buffer contains no entries.
179    #[cfg(test)]
180    #[must_use]
181    pub(crate) fn is_empty(&self) -> bool {
182        self.data.is_empty()
183    }
184
185    /// Determine whether the entry at `index` is a sector bitmap block entry.
186    ///
187    /// Layout: every `chunk_ratio + 1` entries, the last one is a sector bitmap entry.
188    fn is_sector_bitmap_entry(&self, index: usize) -> bool {
189        if self.chunk_ratio == 0 {
190            return false;
191        }
192        let Ok(chunk_ratio) = usize::try_from(self.chunk_ratio) else {
193            return false;
194        };
195        let stride = chunk_ratio.saturating_add(1);
196        index % stride == chunk_ratio
197    }
198
199    /// Get a zero-copy entry view by index.
200    ///
201    /// # Errors
202    ///
203    /// Returns [`Error::BatEntryNotFound`] if the index is out of bounds.
204    ///
205    /// # Panics
206    ///
207    /// Panics if internal bounds validation is violated before converting the
208    /// 8-byte slice into an array.
209    pub fn entry(&self, index: u64) -> Result<BatEntry<'_>> {
210        let idx = usize::try_from(index).map_err(|_| Error::BatEntryNotFound { index })?;
211        let offset = idx * 8;
212        if offset.saturating_add(8) > self.data.len() {
213            return Err(Error::BatEntryNotFound { index });
214        }
215        let bytes: &[u8; 8] = self.data[offset..offset + 8]
216            .try_into()
217            .expect("length already validated");
218        Ok(BatEntry {
219            bytes,
220            is_sector_bitmap: self.is_sector_bitmap_entry(idx),
221        })
222    }
223
224    /// Unchecked entry access (used by the iterator).
225    fn entry_unchecked(&self, index: usize) -> BatEntry<'_> {
226        let offset = index * 8;
227        let bytes: &[u8; 8] = self.data[offset..offset + 8]
228            .try_into()
229            .expect("iterator index is within bounds");
230        BatEntry {
231            bytes,
232            is_sector_bitmap: self.is_sector_bitmap_entry(index),
233        }
234    }
235
236    /// Iterate all entries as zero-copy views.
237    ///
238    /// The returned iterator borrows this BAT and yields exactly as many items
239    /// as there are 64-bit entries in the buffer.
240    pub fn entries(&self) -> impl Iterator<Item = BatEntry<'_>> + '_ {
241        (0..self.len()).map(move |i| self.entry_unchecked(i))
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    /// Build a BAT buffer with the given 64-bit LE entries.
250    fn make_buf(entries: &[u64]) -> Vec<u8> {
251        let mut buf = Vec::with_capacity(entries.len() * 8);
252        for &e in entries {
253            buf.extend_from_slice(&e.to_le_bytes());
254        }
255        buf
256    }
257
258    #[test]
259    fn entry_count_matches_buffer() {
260        let buf = make_buf(&[0, 0, 0, 0, 0, 0]);
261        let bat = Bat::new(&buf, 4);
262        assert_eq!(bat.len(), 6);
263        assert!(!bat.is_empty());
264    }
265
266    #[test]
267    fn empty_bat() {
268        let bat = Bat::new(&[], 4);
269        assert!(bat.is_empty());
270    }
271
272    #[test]
273    fn state_extraction_all_payload_values() {
274        // chunk_ratio = 4 → entries 0-3 are payload, entry 4 is sector bitmap
275        let buf = make_buf(&[0, 1, 2, 3, 0, 6, 7, 4, 5]);
276        let bat = Bat::new(&buf, 4);
277
278        // Payload entries (indices 0-3)
279        assert_eq!(
280            bat.entry(0).unwrap().payload_state(),
281            Some(PayloadBlockState::NotPresent)
282        );
283        assert_eq!(
284            bat.entry(1).unwrap().payload_state(),
285            Some(PayloadBlockState::Undefined)
286        );
287        assert_eq!(
288            bat.entry(2).unwrap().payload_state(),
289            Some(PayloadBlockState::Zero)
290        );
291        assert_eq!(
292            bat.entry(3).unwrap().payload_state(),
293            Some(PayloadBlockState::Unmapped)
294        );
295
296        // Entry 4 is sector bitmap
297        assert!(bat.entry(4).unwrap().is_sector_bitmap());
298
299        // Entry 5 = next chunk, payload again
300        assert_eq!(
301            bat.entry(5).unwrap().payload_state(),
302            Some(PayloadBlockState::FullyPresent)
303        );
304        assert_eq!(
305            bat.entry(6).unwrap().payload_state(),
306            Some(PayloadBlockState::PartiallyPresent)
307        );
308
309        // Reserved values 4, 5 → None
310        assert_eq!(bat.entry(7).unwrap().payload_state(), None);
311        assert_eq!(bat.entry(8).unwrap().payload_state(), None);
312    }
313
314    #[test]
315    fn state_extraction_sector_bitmap() {
316        let buf = make_buf(&[0, 0, 0, 0, 0, 0, 0, 0, 6]);
317        let bat = Bat::new(&buf, 4);
318
319        // Entry 4 is sector bitmap
320        let sb = bat.entry(4).unwrap();
321        assert!(sb.is_sector_bitmap());
322        assert_eq!(
323            sb.sector_bitmap_state(),
324            Some(SectorBitmapState::NotPresent)
325        );
326        assert_eq!(
327            sb.state().unwrap(),
328            BatState::SectorBitmap(SectorBitmapState::NotPresent)
329        );
330
331        // Entry 8 (index 8) = chunk_stride=5, 8%5=3 → payload, not SB
332        // Entry 9 would be SB (9%5=4), but only 9 entries
333        // Let's add more: entry 9 is SB
334        let buf2 = make_buf(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 6]);
335        let bat2 = Bat::new(&buf2, 4);
336        let sb2 = bat2.entry(9).unwrap();
337        assert!(sb2.is_sector_bitmap());
338        assert_eq!(sb2.sector_bitmap_state(), Some(SectorBitmapState::Present));
339    }
340
341    #[test]
342    fn file_offset_mb_extraction() {
343        // FileOffsetMB in bits 20-63
344        // Value = state in bits 0-2, offset_mb in bits 20-63
345        let offset_mb: u64 = 0x1234; // arbitrary
346        let mut raw_bytes = [0u8; 8];
347        {
348            let bits = raw_bytes.view_bits_mut::<Lsb0>();
349            bits[0..3].store::<u8>(6u8); // FullyPresent
350            bits[20..64].store::<u64>(offset_mb);
351        }
352        let buf = make_buf(&[u64::from_le_bytes(raw_bytes)]);
353        let bat = Bat::new(&buf, 4);
354
355        let entry = bat.entry(0).unwrap();
356        assert_eq!(entry.raw_state(), 6);
357        assert_eq!(entry.file_offset_mb(), offset_mb);
358    }
359
360    #[test]
361    fn file_offset_mb_44bit_max() {
362        // Max 44-bit value
363        let max_offset: u64 = 0x0FFF_FFFF_FFFF;
364        let mut raw_bytes = [0u8; 8];
365        {
366            let bits = raw_bytes.view_bits_mut::<Lsb0>();
367            bits[0..3].store::<u8>(0u8); // NotPresent
368            bits[20..64].store::<u64>(max_offset);
369        }
370        let buf = make_buf(&[u64::from_le_bytes(raw_bytes)]);
371        let bat = Bat::new(&buf, 4);
372
373        assert_eq!(bat.entry(0).unwrap().file_offset_mb(), max_offset);
374    }
375
376    #[test]
377    fn entry_out_of_bounds() {
378        let buf = make_buf(&[0, 0]);
379        let bat = Bat::new(&buf, 4);
380
381        assert!(bat.entry(2).is_err());
382        assert!(bat.entry(100).is_err());
383    }
384
385    #[test]
386    fn entries_iterator_yields_correct_count() {
387        let buf = make_buf(&[0, 1, 2, 3, 4, 5, 6, 7]);
388        let bat = Bat::new(&buf, 4);
389
390        let entries: Vec<_> = bat.entries().collect();
391        assert_eq!(entries.len(), 8);
392
393        // Verify each entry's raw_state matches the original value
394        for (i, entry) in entries.iter().enumerate() {
395            assert_eq!(
396                entry.raw_state(),
397                u8::try_from(i).expect("test index fits u8")
398            );
399        }
400    }
401
402    #[test]
403    fn interleaving_pattern() {
404        // chunk_ratio = 3 → stride = 4
405        // Indices: 0=P, 1=P, 2=P, 3=SB, 4=P, 5=P, 6=P, 7=SB
406        let buf = make_buf(&[0; 8]);
407        let bat = Bat::new(&buf, 3);
408
409        assert!(!bat.entry(0).unwrap().is_sector_bitmap());
410        assert!(!bat.entry(1).unwrap().is_sector_bitmap());
411        assert!(!bat.entry(2).unwrap().is_sector_bitmap());
412        assert!(bat.entry(3).unwrap().is_sector_bitmap());
413        assert!(!bat.entry(4).unwrap().is_sector_bitmap());
414        assert!(!bat.entry(5).unwrap().is_sector_bitmap());
415        assert!(!bat.entry(6).unwrap().is_sector_bitmap());
416        assert!(bat.entry(7).unwrap().is_sector_bitmap());
417    }
418
419    #[test]
420    fn bat_entry_state_returns_correct_variant() {
421        let buf = make_buf(&[6, 0, 0, 0, 6]);
422        let bat = Bat::new(&buf, 4);
423
424        // Entry 0: payload, raw_state=6 → FullyPresent
425        assert_eq!(
426            bat.entry(0).unwrap().state().unwrap(),
427            BatState::Payload(PayloadBlockState::FullyPresent)
428        );
429
430        // Entry 4: sector bitmap, raw_state=6 → Present
431        assert_eq!(
432            bat.entry(4).unwrap().state().unwrap(),
433            BatState::SectorBitmap(SectorBitmapState::Present)
434        );
435    }
436
437    #[test]
438    fn invalid_state_4_returns_error_for_payload() {
439        // chunk_ratio = 4 → entry 0 is payload with raw_state=4 (reserved)
440        let buf = make_buf(&[4]);
441        let bat = Bat::new(&buf, 4);
442        let err = bat.entry(0).unwrap().state().unwrap_err();
443        assert!(matches!(err, Error::InvalidBlockState(4)));
444    }
445
446    #[test]
447    fn invalid_state_5_returns_error_for_payload() {
448        // chunk_ratio = 4 → entry 0 is payload with raw_state=5 (reserved)
449        let buf = make_buf(&[5]);
450        let bat = Bat::new(&buf, 4);
451        let err = bat.entry(0).unwrap().state().unwrap_err();
452        assert!(matches!(err, Error::InvalidBlockState(5)));
453    }
454}