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::{self, 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). For an owned, `Send + Sync` handle that outlives the
61/// volume, enable the `vfs` feature and use `crate::vfs::VssSnapshotSource`.
62pub struct Snapshot<'v, R> {
63    reader: &'v mut R,
64    state: SnapshotState,
65}
66
67/// The reader-independent half of a reconstructed snapshot: everything the
68/// copy-on-write overlay algorithm needs, holding no borrow of the volume.
69///
70/// Split out from [`Snapshot`] so a caller that must own its reader — the
71/// `forensic_vfs::ImageSource` adapter in [`crate::vfs`], whose positioned reads
72/// take `&self` on a `Send + Sync + 'static` handle — can pair one immutable
73/// state with its own reader instead of a `'v` borrow of a `VssVolume`. Reads
74/// take the reader as a parameter, so the state stays shareable while only the
75/// cursor needs exclusive access.
76///
77/// Every read here is I/O-only by construction: a corrupt descriptor
78/// reconstructs as zeros rather than erroring, so the error type is
79/// [`std::io::Error`], not [`VssError`].
80pub(crate) struct SnapshotState {
81    volume_size: u64,
82    /// Diff-area block descriptors keyed by their original (volume) offset.
83    block_map: BTreeMap<u64, Vec<BlockDescriptor>>,
84    /// Concatenated store bitmap; bit `n` set ⇒ volume block `n` was unallocated.
85    bitmap: Vec<u8>,
86}
87
88impl<R: Read + Seek> VssVolume<R> {
89    /// Build a reconstructed [`Snapshot`] of store `index`.
90    ///
91    /// Walks the store's block-descriptor list and bitmap eagerly (both are
92    /// small relative to the volume), then borrows the reader so
93    /// [`Snapshot::read_block`] / [`Snapshot::read_at`] can materialize blocks.
94    ///
95    /// # Errors
96    /// - [`VssError::StoreIndexOutOfRange`] if `index` is past the last store.
97    /// - [`VssError::StoreInfoUnavailable`] if the store has no type-0x03 catalog
98    ///   pointer (so neither the store-header nor the bitmap offset is known).
99    /// - [`VssError::Io`] on an underlying read/seek failure.
100    pub fn snapshot(&mut self, index: usize) -> Result<Snapshot<'_, R>, VssError> {
101        let state = self.snapshot_state(index)?;
102        Ok(Snapshot {
103            reader: &mut self.reader,
104            state,
105        })
106    }
107
108    /// Build the reader-independent [`SnapshotState`] of store `index`, walking
109    /// the diff-area and bitmap chains. The reader is released on return, so the
110    /// caller is free to pair the state with a reader it owns.
111    pub(crate) fn snapshot_state(&mut self, index: usize) -> Result<SnapshotState, VssError> {
112        let (store_header_offset, store_bitmap_offset) = {
113            let count = self.stores.len();
114            let d = self
115                .stores
116                .get(index)
117                .ok_or(VssError::StoreIndexOutOfRange { index, count })?;
118            let header = d
119                .store_header_offset
120                .ok_or(VssError::StoreInfoUnavailable { index })?;
121            // The bitmap offset is captured from the same type-0x03 entry as the
122            // header, so it is present whenever the header is.
123            let bitmap = d
124                .store_bitmap_offset
125                .ok_or(VssError::StoreInfoUnavailable { index })?;
126            (header, bitmap)
127        };
128
129        // The block-descriptor list is the block immediately after the store
130        // header, chaining via `next_offset` like every store-block chain.
131        let diff_start = store_header_offset.saturating_add(BLOCK_SIZE as u64);
132        let block_map = build_block_map(&mut self.reader, diff_start, self.volume_size)?;
133        let bitmap = build_bitmap(&mut self.reader, store_bitmap_offset, self.volume_size)?;
134
135        Ok(SnapshotState {
136            volume_size: self.volume_size,
137            block_map,
138            bitmap,
139        })
140    }
141}
142
143impl<R: Read + Seek> Snapshot<'_, R> {
144    /// Reconstruct the single aligned 16384-byte block containing `offset`.
145    ///
146    /// `offset` need not be block-aligned; the block it falls in is reconstructed
147    /// in full. A block at or past the end of the volume reconstructs as zeros.
148    ///
149    /// # Errors
150    /// [`VssError::Io`] on an underlying read/seek failure.
151    pub fn read_block(&mut self, offset: u64) -> Result<[u8; BLOCK_SIZE], VssError> {
152        Ok(self.state.read_block(self.reader, offset)?)
153    }
154
155    /// Reconstruct an arbitrary-length, arbitrary-offset read, materializing each
156    /// spanned block and copying the requested slice out.
157    ///
158    /// Bytes at or past the end of the volume read as zeros.
159    ///
160    /// # Errors
161    /// [`VssError::Io`] on an underlying read/seek failure.
162    pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), VssError> {
163        Ok(self.state.read_at(self.reader, offset, buf)?)
164    }
165}
166
167impl SnapshotState {
168    /// Whether volume block `block_number` was unallocated in the snapshot (its
169    /// store-bitmap bit is set). Out-of-range block numbers read as allocated.
170    #[must_use]
171    fn is_unallocated(&self, block_number: u64) -> bool {
172        let byte = block_number / 8;
173        let bit = (block_number % 8) as u32;
174        usize::try_from(byte)
175            .ok()
176            .and_then(|i| self.bitmap.get(i))
177            .is_some_and(|b| b & (1u8 << bit) != 0)
178    }
179
180    /// Reconstruct the single aligned 16384-byte block containing `offset`,
181    /// pulling live and store blocks through `reader`.
182    ///
183    /// `offset` need not be block-aligned; the block it falls in is reconstructed
184    /// in full. A block at or past the end of the volume reconstructs as zeros.
185    ///
186    /// # Errors
187    /// The underlying read/seek failure.
188    pub(crate) fn read_block<R: Read + Seek>(
189        &self,
190        reader: &mut R,
191        offset: u64,
192    ) -> Result<[u8; BLOCK_SIZE], io::Error> {
193        let block_number = offset / BLOCK_SIZE as u64;
194        let base = block_number.saturating_mul(BLOCK_SIZE as u64);
195        let mut out = [0u8; BLOCK_SIZE];
196
197        match self.block_map.get(&base) {
198            Some(descriptors) if !descriptors.is_empty() => {
199                // Base data: the last plain descriptor's store block, or the live
200                // volume block when the block has no plain descriptor. A plain
201                // descriptor has none of forwarder/overlay/not-used set.
202                let last_plain = descriptors.iter().rfind(|d| {
203                    !d.flags.is_forwarder() && !d.flags.is_overlay() && !d.flags.is_not_used()
204                });
205                let base_source = last_plain.map_or(base, |d| d.store_offset);
206                read_block_at(reader, base_source, self.volume_size, &mut out)?;
207
208                // Overlay descriptors patch in their allocated 512-byte sub-blocks.
209                for d in descriptors
210                    .iter()
211                    .filter(|d| d.flags.is_overlay() && !d.flags.is_not_used())
212                {
213                    let mut overlay = [0u8; BLOCK_SIZE];
214                    if read_block_at(reader, d.store_offset, self.volume_size, &mut overlay)? {
215                        apply_overlay(&mut out, &overlay, d.allocation_bitmap);
216                    }
217                }
218                Ok(out)
219            }
220            _ => {
221                if self.is_unallocated(block_number) {
222                    Ok(out) // already zeroed
223                } else {
224                    read_block_at(reader, base, self.volume_size, &mut out)?;
225                    Ok(out)
226                }
227            }
228        }
229    }
230
231    /// Reconstruct an arbitrary-length, arbitrary-offset read, materializing each
232    /// spanned block and copying the requested slice out.
233    ///
234    /// Bytes at or past the end of the volume read as zeros.
235    ///
236    /// # Errors
237    /// The underlying read/seek failure.
238    pub(crate) fn read_at<R: Read + Seek>(
239        &self,
240        reader: &mut R,
241        offset: u64,
242        buf: &mut [u8],
243    ) -> Result<(), io::Error> {
244        let mut written = 0usize;
245        let mut cursor = offset;
246        while written < buf.len() {
247            let base = (cursor / BLOCK_SIZE as u64).saturating_mul(BLOCK_SIZE as u64);
248            let within = (cursor - base) as usize;
249            let block = self.read_block(reader, base)?;
250            let n = (BLOCK_SIZE - within).min(buf.len() - written);
251            buf[written..written + n].copy_from_slice(&block[within..within + n]);
252            written += n;
253            cursor = cursor.saturating_add(n as u64);
254        }
255        Ok(())
256    }
257}
258
259/// Overwrite the sub-blocks of `out` selected by `allocation_bitmap` (bit `i`,
260/// LSB-first) with `overlay`'s corresponding 512-byte sub-block.
261fn apply_overlay(out: &mut [u8; BLOCK_SIZE], overlay: &[u8; BLOCK_SIZE], allocation_bitmap: u32) {
262    for i in 0..SUB_BLOCK_COUNT {
263        if allocation_bitmap & (1u32 << i) != 0 {
264            let start = i * SUB_BLOCK_SIZE;
265            let end = start + SUB_BLOCK_SIZE;
266            out[start..end].copy_from_slice(&overlay[start..end]);
267        }
268    }
269}
270
271/// Read the 16384-byte block at volume `offset` into `out`.
272///
273/// Returns `Ok(true)` when the block was fully in range and read, `Ok(false)`
274/// when `offset` runs past the end of the volume (in which case `out` is left
275/// zeroed and the caller treats the source as contributing nothing).
276fn read_block_at<R: Read + Seek>(
277    reader: &mut R,
278    offset: u64,
279    volume_size: u64,
280    out: &mut [u8; BLOCK_SIZE],
281) -> Result<bool, io::Error> {
282    match offset.checked_add(BLOCK_SIZE as u64) {
283        Some(end) if end <= volume_size => {
284            reader.seek(SeekFrom::Start(offset))?;
285            reader.read_exact(out)?;
286            Ok(true)
287        }
288        _ => {
289            *out = [0u8; BLOCK_SIZE];
290            Ok(false)
291        }
292    }
293}
294
295/// Walk the 0x0003 block-descriptor (diff-area) chain from `first`, collecting
296/// every descriptor keyed by its original (volume) offset.
297///
298/// Bounded exactly like the catalog walk: a visited-set breaks cycles,
299/// [`MAX_STORE_BLOCKS`] caps the chain length, and every block offset is
300/// range-checked against the volume before it is read. A block whose record type
301/// is not 0x0003 stops the walk. Within a block, a fully-zero 32-byte record
302/// terminates that block's descriptor list.
303fn build_block_map<R: Read + Seek>(
304    reader: &mut R,
305    first: u64,
306    volume_size: u64,
307) -> Result<BTreeMap<u64, Vec<BlockDescriptor>>, VssError> {
308    let mut map: BTreeMap<u64, Vec<BlockDescriptor>> = BTreeMap::new();
309    let mut visited: HashSet<u64> = HashSet::new();
310    let mut next = first;
311    let mut blocks = 0usize;
312
313    while next != 0 && blocks < MAX_STORE_BLOCKS {
314        if !visited.insert(next) {
315            break; // cycle
316        }
317        match next.checked_add(BLOCK_SIZE as u64) {
318            Some(end) if end <= volume_size => {}
319            _ => break, // out of range / overflow
320        }
321
322        reader.seek(SeekFrom::Start(next))?;
323        let mut block = vec![0u8; BLOCK_SIZE];
324        reader.read_exact(&mut block)?;
325        let header = StoreBlockHeader::parse(&block);
326        if header.record_type != DIFF_AREA_RECORD_TYPE {
327            break;
328        }
329
330        let mut off = STORE_BLOCK_HEADER_LEN;
331        while off + BLOCK_DESCRIPTOR_LEN <= BLOCK_SIZE {
332            let record = &block[off..off + BLOCK_DESCRIPTOR_LEN];
333            if record.iter().all(|&x| x == 0) {
334                break; // zero record terminates this block's descriptors
335            }
336            let descriptor = BlockDescriptor::parse(record);
337            map.entry(descriptor.original_offset)
338                .or_default()
339                .push(descriptor);
340            off += BLOCK_DESCRIPTOR_LEN;
341        }
342
343        blocks += 1;
344        next = header.next_offset;
345    }
346
347    Ok(map)
348}
349
350/// Walk the 0x0006 store-bitmap chain from `first`, concatenating each block's
351/// payload (the bytes after its 128-byte header) in `relative_offset` order into
352/// one contiguous bitmap.
353///
354/// Bounded the same three ways as [`build_block_map`]; a block whose record type
355/// is not 0x0006 stops the walk.
356fn build_bitmap<R: Read + Seek>(
357    reader: &mut R,
358    first: u64,
359    volume_size: u64,
360) -> Result<Vec<u8>, VssError> {
361    let mut pieces: Vec<(u64, Vec<u8>)> = Vec::new();
362    let mut visited: HashSet<u64> = HashSet::new();
363    let mut next = first;
364    let mut blocks = 0usize;
365
366    while next != 0 && blocks < MAX_STORE_BLOCKS {
367        if !visited.insert(next) {
368            break; // cycle
369        }
370        match next.checked_add(BLOCK_SIZE as u64) {
371            Some(end) if end <= volume_size => {}
372            _ => break, // out of range / overflow
373        }
374
375        reader.seek(SeekFrom::Start(next))?;
376        let mut block = vec![0u8; BLOCK_SIZE];
377        reader.read_exact(&mut block)?;
378        let header = StoreBlockHeader::parse(&block);
379        if header.record_type != BITMAP_RECORD_TYPE {
380            break;
381        }
382
383        pieces.push((
384            header.relative_offset,
385            block[STORE_BLOCK_HEADER_LEN..BLOCK_SIZE].to_vec(),
386        ));
387
388        blocks += 1;
389        next = header.next_offset;
390    }
391
392    pieces.sort_by_key(|(rel, _)| *rel);
393    let mut bitmap = Vec::with_capacity(
394        pieces
395            .len()
396            .saturating_mul(BLOCK_SIZE - STORE_BLOCK_HEADER_LEN),
397    );
398    for (_, payload) in pieces {
399        bitmap.extend_from_slice(&payload);
400    }
401    Ok(bitmap)
402}