Skip to main content

vmdk/
recovery.rs

1//! Opt-in RGD recovery — resolving reads through the redundant grain directory
2//! when the primary grain directory is damaged. The read path (`grain_location`,
3//! `iter_allocated_grains`) calls into these resolvers; they are no-ops unless
4//! `enable_rgd_fallback()` was called.
5
6use std::io::{self, Read, Seek, SeekFrom};
7
8use crate::header::{GD_AT_END, SECTOR_SIZE};
9use crate::VmdkReader;
10
11impl<R: Read + Seek> VmdkReader<R> {
12    /// Enable opt-in RGD fallback: a read whose primary grain-table pointer is out of
13    /// bounds is resolved through the redundant grain directory instead, recovering
14    /// data from a damaged primary GD that `qemu-img` would simply fail on.
15    pub fn enable_rgd_fallback(&mut self) {
16        self.rgd_fallback = true;
17    }
18
19    /// Number of grains resolved via the redundant grain directory so far (pointer- or
20    /// entry-level recovery). Zero on a healthy image; non-zero quantifies how much of a
21    /// damaged image was reconstructed from the RGD.
22    #[must_use]
23    pub fn rgd_recovery_count(&self) -> u64 {
24        self.rgd_recovery_count
25    }
26
27    /// Resolve the grain-table sector for `gd_idx`, preferring the primary pointer and
28    /// falling back to the redundant grain directory when the primary is unusable.
29    /// Returns the primary pointer unchanged when no better candidate exists, so the
30    /// non-fallback error/sparse behaviour is preserved.
31    pub(crate) fn resilient_gt_sector(
32        &mut self,
33        gd_idx: usize,
34        primary: u32,
35        num_gtes_per_gt: u64,
36    ) -> io::Result<u32> {
37        let file_len = self.inner.seek(SeekFrom::End(0))?;
38        let gt_byte_len = num_gtes_per_gt * 4;
39        let usable = |sec: u32| {
40            sec != 0
41                && u64::from(sec)
42                    .saturating_mul(SECTOR_SIZE)
43                    .saturating_add(gt_byte_len)
44                    <= file_len
45        };
46        if usable(primary) {
47            return Ok(primary);
48        }
49        let rgd = self.rgd_dir_entry(gd_idx, file_len)?;
50        if usable(rgd) {
51            crate::diag::pointer_recovered(gd_idx, primary, rgd);
52            return Ok(rgd);
53        }
54        Ok(primary)
55    }
56
57    /// Read entry `gd_idx` from the redundant grain directory, or 0 if the RGD is
58    /// absent or the entry itself would fall outside the file.
59    pub(crate) fn rgd_dir_entry(&mut self, gd_idx: usize, file_len: u64) -> io::Result<u32> {
60        if self.rgd_offset == 0 || self.rgd_offset == GD_AT_END {
61            return Ok(0);
62        }
63        if gd_idx >= self.gd_entry_count {
64            return Ok(0);
65        }
66        let entry_byte = self
67            .rgd_offset
68            .saturating_mul(SECTOR_SIZE)
69            .saturating_add(gd_idx as u64 * 4);
70        if entry_byte.saturating_add(4) > file_len {
71            return Ok(0);
72        }
73        let mut b = [0u8; 4];
74        self.read_exact_at(entry_byte, &mut b)?;
75        Ok(u32::from_le_bytes(b))
76    }
77
78    /// Read the full redundant grain table referenced by RGD entry `gd_idx`, or `None`
79    /// if the RGD entry is absent or the grain table would fall outside the file.
80    pub(crate) fn read_redundant_gt(
81        &mut self,
82        gd_idx: usize,
83        num_gtes_per_gt: u64,
84    ) -> io::Result<Option<Vec<u8>>> {
85        let file_len = self.inner.seek(SeekFrom::End(0))?;
86        let sector = self.rgd_dir_entry(gd_idx, file_len)?;
87        if sector == 0 {
88            return Ok(None);
89        }
90        let gt_byte = u64::from(sector) * SECTOR_SIZE;
91        let gt_byte_len = num_gtes_per_gt * 4;
92        if gt_byte.saturating_add(gt_byte_len) > file_len {
93            return Ok(None);
94        }
95        let mut b = vec![0u8; gt_byte_len as usize];
96        self.read_exact_at(gt_byte, &mut b)?;
97        Ok(Some(b))
98    }
99
100    /// Read grain-table entry `gte_idx` from the redundant grain table referenced by
101    /// RGD entry `gd_idx`, or 0 if the RGD entry or the target entry is out of bounds.
102    /// Used for content-level recovery when a primary GT entry has been lost.
103    pub(crate) fn rgd_gte(
104        &mut self,
105        gd_idx: usize,
106        gte_idx: u64,
107        num_gtes_per_gt: u64,
108    ) -> io::Result<u32> {
109        let file_len = self.inner.seek(SeekFrom::End(0))?;
110        let rgd_gt_sector = self.rgd_dir_entry(gd_idx, file_len)?;
111        if rgd_gt_sector == 0 {
112            return Ok(0);
113        }
114        let gt_byte = u64::from(rgd_gt_sector) * SECTOR_SIZE;
115        let gt_byte_len = num_gtes_per_gt * 4;
116        if gt_byte.saturating_add(gt_byte_len) > file_len {
117            return Ok(0);
118        }
119        let entry_byte = gt_byte + gte_idx * 4;
120        if entry_byte.saturating_add(4) > file_len {
121            return Ok(0);
122        }
123        let mut b = [0u8; 4];
124        self.read_exact_at(entry_byte, &mut b)?;
125        Ok(u32::from_le_bytes(b))
126    }
127}