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}