Skip to main content

vsc/
reconstruct.rs

1//! Phase-2 copy-on-write snapshot reconstruction.
2//!
3//! Given a store's block-descriptor list (the 0x0003 diff-area chain) and store
4//! bitmap (the 0x0006 chain), this module materializes the snapshot's view of
5//! any 16384-byte volume block by overlaying the copy-on-write data saved in the
6//! store on top of the live volume.
7//!
8//! The algorithm is the one validated byte-for-byte against libvshadow
9//! (`pyvshadow`) over 1,415 blocks of the Magnet PC-MUS-001.E01 image — see
10//! `docs/RECONSTRUCTION.md`. For one block at volume offset `off` (`bn = off /
11//! 16384`, `base = bn * 16384`):
12//!
13//! - **Descriptor set non-empty:** the base is the last *plain* descriptor's
14//!   store block (a descriptor with none of forwarder/overlay/not-used set), or
15//!   the live volume block when there is no plain. Then each *overlay* descriptor
16//!   (overlay set, not-used clear) replaces the 512-byte sub-blocks selected by
17//!   its allocation bitmap.
18//! - **Descriptor set empty, bitmap bit set:** the block was unallocated in the
19//!   snapshot — 16384 zero bytes.
20//! - **Descriptor set empty, bitmap bit clear:** live passthrough.
21//!
22//! Every offset read from the image is range-checked against the volume before
23//! seeking: a corrupt descriptor never panics or reads out of bounds — a
24//! plain/live block that is out of range reconstructs as zeros, and an
25//! out-of-range overlay contributes nothing (its sub-blocks are skipped).
26
27use std::collections::{BTreeMap, HashSet};
28use std::io::{Read, Seek, SeekFrom};
29
30use crate::block::BlockDescriptor;
31use crate::catalog::BLOCK_SIZE;
32use crate::error::VssError;
33use crate::store::{StoreBlockHeader, STORE_BLOCK_HEADER_LEN};
34use crate::VssVolume;
35
36/// Record type of a block-descriptor-list (diff-area) block.
37const DIFF_AREA_RECORD_TYPE: u32 = 0x0003;
38
39/// Record type of a store-bitmap block.
40const BITMAP_RECORD_TYPE: u32 = 0x0006;
41
42/// Length of one block descriptor, in bytes.
43const BLOCK_DESCRIPTOR_LEN: usize = 32;
44
45/// Number of 512-byte sub-blocks an overlay allocation bitmap addresses.
46const SUB_BLOCK_COUNT: usize = 32;
47
48/// Size of one overlay sub-block (`16384 / 32`), in bytes.
49const SUB_BLOCK_SIZE: usize = BLOCK_SIZE / SUB_BLOCK_COUNT;
50
51/// Upper bound on store blocks walked in one chain, mirroring the catalog walk's
52/// cap so a corrupt or looping diff-area/bitmap chain is bounded.
53const MAX_STORE_BLOCKS: usize = 1 << 20;
54
55/// A reconstructed, read-only view of one shadow copy — the snapshot's view of
56/// the volume, materialized block by block on demand.
57///
58/// Built by [`VssVolume::snapshot`]; borrows the volume's reader mutably so
59/// reconstruction can pull live and store blocks lazily (the volumes are far too
60/// large to hold in memory).
61pub struct Snapshot<'v, R> {
62    reader: &'v mut R,
63    volume_size: u64,
64    /// Diff-area block descriptors keyed by their original (volume) offset.
65    block_map: BTreeMap<u64, Vec<BlockDescriptor>>,
66    /// Concatenated store bitmap; bit `n` set ⇒ volume block `n` was unallocated.
67    bitmap: Vec<u8>,
68}
69
70impl<R: Read + Seek> VssVolume<R> {
71    /// Build a reconstructed [`Snapshot`] of store `index`.
72    ///
73    /// Walks the store's block-descriptor list and bitmap eagerly (both are
74    /// small relative to the volume), then borrows the reader so
75    /// [`Snapshot::read_block`] / [`Snapshot::read_at`] can materialize blocks.
76    ///
77    /// # Errors
78    /// - [`VssError::StoreIndexOutOfRange`] if `index` is past the last store.
79    /// - [`VssError::StoreInfoUnavailable`] if the store has no type-0x03 catalog
80    ///   pointer (so neither the store-header nor the bitmap offset is known).
81    /// - [`VssError::Io`] on an underlying read/seek failure.
82    pub fn snapshot(&mut self, index: usize) -> Result<Snapshot<'_, R>, VssError> {
83        let (store_header_offset, store_bitmap_offset) = {
84            let count = self.stores.len();
85            let d = self
86                .stores
87                .get(index)
88                .ok_or(VssError::StoreIndexOutOfRange { index, count })?;
89            let header = d
90                .store_header_offset
91                .ok_or(VssError::StoreInfoUnavailable { index })?;
92            // The bitmap offset is captured from the same type-0x03 entry as the
93            // header, so it is present whenever the header is.
94            let bitmap = d
95                .store_bitmap_offset
96                .ok_or(VssError::StoreInfoUnavailable { index })?;
97            (header, bitmap)
98        };
99
100        // The block-descriptor list is the block immediately after the store
101        // header, chaining via `next_offset` like every store-block chain.
102        let diff_start = store_header_offset.saturating_add(BLOCK_SIZE as u64);
103        let block_map = build_block_map(&mut self.reader, diff_start, self.volume_size)?;
104        let bitmap = build_bitmap(&mut self.reader, store_bitmap_offset, self.volume_size)?;
105
106        Ok(Snapshot {
107            reader: &mut self.reader,
108            volume_size: self.volume_size,
109            block_map,
110            bitmap,
111        })
112    }
113}
114
115impl<R: Read + Seek> Snapshot<'_, R> {
116    /// Whether volume block `block_number` was unallocated in the snapshot (its
117    /// store-bitmap bit is set). Out-of-range block numbers read as allocated.
118    #[must_use]
119    fn is_unallocated(&self, block_number: u64) -> bool {
120        let byte = block_number / 8;
121        let bit = (block_number % 8) as u32;
122        usize::try_from(byte)
123            .ok()
124            .and_then(|i| self.bitmap.get(i))
125            .is_some_and(|b| b & (1u8 << bit) != 0)
126    }
127
128    /// Reconstruct the single aligned 16384-byte block containing `offset`.
129    ///
130    /// `offset` need not be block-aligned; the block it falls in is reconstructed
131    /// in full. A block at or past the end of the volume reconstructs as zeros.
132    ///
133    /// # Errors
134    /// [`VssError::Io`] on an underlying read/seek failure.
135    pub fn read_block(&mut self, offset: u64) -> Result<[u8; BLOCK_SIZE], VssError> {
136        let block_number = offset / BLOCK_SIZE as u64;
137        let base = block_number.saturating_mul(BLOCK_SIZE as u64);
138        let mut out = [0u8; BLOCK_SIZE];
139
140        match self.block_map.get(&base) {
141            Some(descriptors) if !descriptors.is_empty() => {
142                // Base data: the last plain descriptor's store block, or the live
143                // volume block when the block has no plain descriptor. A plain
144                // descriptor has none of forwarder/overlay/not-used set.
145                let last_plain = descriptors.iter().rfind(|d| {
146                    !d.flags.is_forwarder() && !d.flags.is_overlay() && !d.flags.is_not_used()
147                });
148                let base_source = last_plain.map_or(base, |d| d.store_offset);
149                read_block_at(self.reader, base_source, self.volume_size, &mut out)?;
150
151                // Overlay descriptors patch in their allocated 512-byte sub-blocks.
152                for d in descriptors
153                    .iter()
154                    .filter(|d| d.flags.is_overlay() && !d.flags.is_not_used())
155                {
156                    let mut overlay = [0u8; BLOCK_SIZE];
157                    if read_block_at(self.reader, d.store_offset, self.volume_size, &mut overlay)? {
158                        apply_overlay(&mut out, &overlay, d.allocation_bitmap);
159                    }
160                }
161                Ok(out)
162            }
163            _ => {
164                if self.is_unallocated(block_number) {
165                    Ok(out) // already zeroed
166                } else {
167                    read_block_at(self.reader, base, self.volume_size, &mut out)?;
168                    Ok(out)
169                }
170            }
171        }
172    }
173
174    /// Reconstruct an arbitrary-length, arbitrary-offset read, materializing each
175    /// spanned block and copying the requested slice out.
176    ///
177    /// Bytes at or past the end of the volume read as zeros.
178    ///
179    /// # Errors
180    /// [`VssError::Io`] on an underlying read/seek failure.
181    pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), VssError> {
182        let mut written = 0usize;
183        let mut cursor = offset;
184        while written < buf.len() {
185            let base = (cursor / BLOCK_SIZE as u64).saturating_mul(BLOCK_SIZE as u64);
186            let within = (cursor - base) as usize;
187            let block = self.read_block(base)?;
188            let n = (BLOCK_SIZE - within).min(buf.len() - written);
189            buf[written..written + n].copy_from_slice(&block[within..within + n]);
190            written += n;
191            cursor = cursor.saturating_add(n as u64);
192        }
193        Ok(())
194    }
195}
196
197/// Overwrite the sub-blocks of `out` selected by `allocation_bitmap` (bit `i`,
198/// LSB-first) with `overlay`'s corresponding 512-byte sub-block.
199fn apply_overlay(out: &mut [u8; BLOCK_SIZE], overlay: &[u8; BLOCK_SIZE], allocation_bitmap: u32) {
200    for i in 0..SUB_BLOCK_COUNT {
201        if allocation_bitmap & (1u32 << i) != 0 {
202            let start = i * SUB_BLOCK_SIZE;
203            let end = start + SUB_BLOCK_SIZE;
204            out[start..end].copy_from_slice(&overlay[start..end]);
205        }
206    }
207}
208
209/// Read the 16384-byte block at volume `offset` into `out`.
210///
211/// Returns `Ok(true)` when the block was fully in range and read, `Ok(false)`
212/// when `offset` runs past the end of the volume (in which case `out` is left
213/// zeroed and the caller treats the source as contributing nothing).
214fn read_block_at<R: Read + Seek>(
215    reader: &mut R,
216    offset: u64,
217    volume_size: u64,
218    out: &mut [u8; BLOCK_SIZE],
219) -> Result<bool, VssError> {
220    match offset.checked_add(BLOCK_SIZE as u64) {
221        Some(end) if end <= volume_size => {
222            reader.seek(SeekFrom::Start(offset))?;
223            reader.read_exact(out)?;
224            Ok(true)
225        }
226        _ => {
227            *out = [0u8; BLOCK_SIZE];
228            Ok(false)
229        }
230    }
231}
232
233/// Walk the 0x0003 block-descriptor (diff-area) chain from `first`, collecting
234/// every descriptor keyed by its original (volume) offset.
235///
236/// Bounded exactly like the catalog walk: a visited-set breaks cycles,
237/// [`MAX_STORE_BLOCKS`] caps the chain length, and every block offset is
238/// range-checked against the volume before it is read. A block whose record type
239/// is not 0x0003 stops the walk. Within a block, a fully-zero 32-byte record
240/// terminates that block's descriptor list.
241fn build_block_map<R: Read + Seek>(
242    reader: &mut R,
243    first: u64,
244    volume_size: u64,
245) -> Result<BTreeMap<u64, Vec<BlockDescriptor>>, VssError> {
246    let mut map: BTreeMap<u64, Vec<BlockDescriptor>> = BTreeMap::new();
247    let mut visited: HashSet<u64> = HashSet::new();
248    let mut next = first;
249    let mut blocks = 0usize;
250
251    while next != 0 && blocks < MAX_STORE_BLOCKS {
252        if !visited.insert(next) {
253            break; // cycle
254        }
255        match next.checked_add(BLOCK_SIZE as u64) {
256            Some(end) if end <= volume_size => {}
257            _ => break, // out of range / overflow
258        }
259
260        reader.seek(SeekFrom::Start(next))?;
261        let mut block = vec![0u8; BLOCK_SIZE];
262        reader.read_exact(&mut block)?;
263        let header = StoreBlockHeader::parse(&block);
264        if header.record_type != DIFF_AREA_RECORD_TYPE {
265            break;
266        }
267
268        let mut off = STORE_BLOCK_HEADER_LEN;
269        while off + BLOCK_DESCRIPTOR_LEN <= BLOCK_SIZE {
270            let record = &block[off..off + BLOCK_DESCRIPTOR_LEN];
271            if record.iter().all(|&x| x == 0) {
272                break; // zero record terminates this block's descriptors
273            }
274            let descriptor = BlockDescriptor::parse(record);
275            map.entry(descriptor.original_offset)
276                .or_default()
277                .push(descriptor);
278            off += BLOCK_DESCRIPTOR_LEN;
279        }
280
281        blocks += 1;
282        next = header.next_offset;
283    }
284
285    Ok(map)
286}
287
288/// Walk the 0x0006 store-bitmap chain from `first`, concatenating each block's
289/// payload (the bytes after its 128-byte header) in `relative_offset` order into
290/// one contiguous bitmap.
291///
292/// Bounded the same three ways as [`build_block_map`]; a block whose record type
293/// is not 0x0006 stops the walk.
294fn build_bitmap<R: Read + Seek>(
295    reader: &mut R,
296    first: u64,
297    volume_size: u64,
298) -> Result<Vec<u8>, VssError> {
299    let mut pieces: Vec<(u64, Vec<u8>)> = Vec::new();
300    let mut visited: HashSet<u64> = HashSet::new();
301    let mut next = first;
302    let mut blocks = 0usize;
303
304    while next != 0 && blocks < MAX_STORE_BLOCKS {
305        if !visited.insert(next) {
306            break; // cycle
307        }
308        match next.checked_add(BLOCK_SIZE as u64) {
309            Some(end) if end <= volume_size => {}
310            _ => break, // out of range / overflow
311        }
312
313        reader.seek(SeekFrom::Start(next))?;
314        let mut block = vec![0u8; BLOCK_SIZE];
315        reader.read_exact(&mut block)?;
316        let header = StoreBlockHeader::parse(&block);
317        if header.record_type != BITMAP_RECORD_TYPE {
318            break;
319        }
320
321        pieces.push((
322            header.relative_offset,
323            block[STORE_BLOCK_HEADER_LEN..BLOCK_SIZE].to_vec(),
324        ));
325
326        blocks += 1;
327        next = header.next_offset;
328    }
329
330    pieces.sort_by_key(|(rel, _)| *rel);
331    let mut bitmap = Vec::with_capacity(
332        pieces
333            .len()
334            .saturating_mul(BLOCK_SIZE - STORE_BLOCK_HEADER_LEN),
335    );
336    for (_, payload) in pieces {
337        bitmap.extend_from_slice(&payload);
338    }
339    Ok(bitmap)
340}