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}