Skip to main content

vhdx/
backing.rs

1//! Pluggable, positioned backing store for one VHDX container.
2//!
3//! The reader historically held the **entire image** in a `Vec<u8>` — a 2 TB
4//! VHDX meant a 2 TB heap. [`Backing`] generalises that to four positioned-read
5//! backings WITHOUT a boxed trait, so the hot read path stays vtable-free (a
6//! `match` the compiler can inline, not a dynamic dispatch):
7//!
8//! - [`Backing::File`] — a loose container file (the bounded path), read with
9//!   the OS positioned-read primitive: only the BAT-selected blocks ever touch
10//!   RAM, so peak memory no longer scales with image size.
11//! - [`Backing::Sub`] — a contiguous sub-range of a larger file (a STORED, i.e.
12//!   uncompressed, zip entry sits at a fixed offset inside the archive):
13//!   `read_at(buf, off)` preads at `base + off`, clamped to `len`.
14//! - [`Backing::Mem`] — an in-RAM buffer (the legacy `from_bytes` path, or a
15//!   DEFLATED zip entry inflated to memory once): `read_at` copies from the
16//!   slice.
17//! - [`Backing::Reader`] — an arbitrary boxed [`ReadSeekSend`] reader (the
18//!   forensic-vfs engine path): `read_at` locks a mutex, seeks to the requested
19//!   offset, and reads — bridging the `Read + Seek` world to the positioned-read
20//!   API. Keeps `forbid(unsafe)` (no mmap).
21//!
22//! All four expose the same cursor-free positioned-read API (`read_at` + `len`),
23//! so the open/parse pass reads small structures from their known offsets and the
24//! data path reads just the resolved block — never the whole file.
25
26use std::fs::File;
27use std::io::{self, Read, Seek, SeekFrom};
28use std::sync::{Arc, Mutex};
29
30/// A boxed `Read + Seek + Send` reader that can be used as a [`Backing::Reader`].
31///
32/// Any type implementing `Read + Seek + Send` satisfies this trait via the blanket
33/// impl below. This is the trait [`VhdxReader::open_reader`] accepts so the
34/// forensic-vfs engine can hand a `SourceCursor` (which is `Read + Seek + Send`)
35/// straight to the VHDX parser without forensic-vfs appearing in the production
36/// dependency tree.
37pub trait ReadSeekSend: Read + Seek + Send {}
38
39impl<T: Read + Seek + Send> ReadSeekSend for T {}
40
41/// Fill `buf` from `file` starting at `offset`, returning the bytes read (short
42/// only at end of file).
43///
44/// Uses the OS positioned-read primitive — `pread(2)` on Unix, `seek_read`
45/// (a `ReadFile` carrying its own `OVERLAPPED` offset) on Windows — so it takes
46/// `&File` and never touches a shared cursor. That makes it safe to call
47/// concurrently from many threads on one handle: each call carries its own
48/// offset, so there is no read/seek race. Keeps `forbid(unsafe)` (no mmap).
49fn pread(file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
50    #[cfg(unix)]
51    use std::os::unix::fs::FileExt;
52    #[cfg(windows)]
53    use std::os::windows::fs::FileExt;
54
55    let mut total = 0usize;
56    while total < buf.len() {
57        #[cfg(unix)]
58        let res = file.read_at(&mut buf[total..], offset + total as u64);
59        #[cfg(windows)]
60        let res = file.seek_read(&mut buf[total..], offset + total as u64);
61        #[cfg(not(any(unix, windows)))]
62        let res: io::Result<usize> = Err(io::Error::new(
63            io::ErrorKind::Unsupported,
64            "positioned reads unsupported on this platform",
65        ));
66        match res {
67            Ok(0) => break,
68            Ok(n) => total += n,
69            Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
70            Err(e) => return Err(e),
71        }
72    }
73    Ok(total)
74}
75
76/// A positioned, cursor-free reader over one VHDX container's bytes.
77///
78/// `read_at(buf, offset)` fills `buf` from logical `offset` within the container
79/// and returns the byte count (short only at end of container) — never touching
80/// a shared cursor, so it is safe to call concurrently through `&self`.
81pub enum Backing {
82    /// A loose container file: positioned reads go straight to the OS handle.
83    File(File),
84    /// A contiguous sub-range `[base, base+len)` of a larger shared file.
85    Sub {
86        /// The backing file (shared; positioned reads carry their own offset).
87        file: Arc<File>,
88        /// Absolute file offset where this container's bytes begin.
89        base: u64,
90        /// Length of this container in bytes.
91        len: u64,
92    },
93    /// An in-RAM container (e.g. the legacy `from_bytes` path or an inflated
94    /// zip entry).
95    Mem(Arc<[u8]>),
96    /// An arbitrary seekable reader (e.g. a forensic-vfs `SourceCursor`).
97    ///
98    /// The inner reader is `Read + Seek + Send` but not cursor-free: `read_at`
99    /// locks the mutex, seeks to the requested offset, then reads. Because the
100    /// lock is held only for the duration of one `read_at` call, this is safe
101    /// under `&self` — the mutex serialises concurrent accesses.
102    Reader {
103        /// The seekable reader, guarded by a mutex to satisfy `&self` [`Backing::read_at`].
104        inner: Mutex<Box<dyn ReadSeekSend>>,
105        /// Total byte length of the container, measured at construction.
106        len: u64,
107    },
108}
109
110impl Backing {
111    /// Construct a [`Backing::Sub`] over `[base, base+len)` of `file`.
112    #[must_use]
113    pub fn sub(file: Arc<File>, base: u64, len: u64) -> Self {
114        Backing::Sub { file, base, len }
115    }
116
117    /// Construct an in-RAM [`Backing::Mem`] from owned bytes.
118    #[must_use]
119    pub fn from_bytes(bytes: impl Into<Arc<[u8]>>) -> Self {
120        Backing::Mem(bytes.into())
121    }
122
123    /// Total length of this container in bytes.
124    #[must_use]
125    pub fn len(&self) -> u64 {
126        match self {
127            Backing::File(f) => f.metadata().map_or(0, |m| m.len()),
128            Backing::Mem(b) => b.len() as u64,
129            Backing::Sub { len, .. } | Backing::Reader { len, .. } => *len,
130        }
131    }
132
133    /// Whether the container is empty.
134    #[must_use]
135    pub fn is_empty(&self) -> bool {
136        self.len() == 0
137    }
138
139    /// Fill `buf` from logical `offset` within this container, returning the
140    /// bytes read (short only at the container's end). Cursor-free and
141    /// thread-safe.
142    ///
143    /// # Errors
144    /// Propagates the underlying I/O error for [`Backing::File`] /
145    /// [`Backing::Sub`]; [`Backing::Mem`] never fails.
146    pub fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
147        match self {
148            Backing::File(f) => pread(f, buf, offset),
149            Backing::Sub { file, base, len } => {
150                // Clamp the request to this container's window so a Sub never
151                // reads past its end into a neighbouring entry. A read starting
152                // beyond the window yields 0 (clean EOF), mirroring File/Mem.
153                let avail = len.saturating_sub(offset);
154                if avail == 0 {
155                    return Ok(0);
156                }
157                let want = (buf.len() as u64).min(avail) as usize;
158                pread(file, &mut buf[..want], base + offset)
159            }
160            Backing::Mem(bytes) => {
161                let off = offset.min(bytes.len() as u64) as usize;
162                let src = &bytes[off..];
163                let n = src.len().min(buf.len());
164                buf[..n].copy_from_slice(&src[..n]);
165                Ok(n)
166            }
167            Backing::Reader { inner, len } => {
168                // Clamp to container bounds first — a read starting at or past
169                // EOF returns 0 cleanly, mirroring the other variants.
170                let avail = len.saturating_sub(offset);
171                if avail == 0 {
172                    return Ok(0);
173                }
174                let want = (buf.len() as u64).min(avail) as usize;
175                // Acquire the lock. A poisoned mutex means the reader is in an
176                // inconsistent state; surface that as an I/O error rather than
177                // panicking, so the caller can decide how to handle it.
178                let mut guard = inner
179                    .lock()
180                    .map_err(|_| io::Error::other("backing reader mutex poisoned"))?;
181                guard.seek(SeekFrom::Start(offset))?;
182                // Read in a loop to fill the buffer (mirroring pread semantics:
183                // short reads are only expected at EOF).
184                let dst = &mut buf[..want];
185                let mut total = 0;
186                while total < dst.len() {
187                    match guard.read(&mut dst[total..])? {
188                        0 => break, // EOF reached before filling the buffer.
189                        n => total += n,
190                    }
191                }
192                Ok(total)
193            }
194        }
195    }
196
197    /// Read exactly `len` bytes starting at `offset` into a fresh `Vec`.
198    ///
199    /// Used by the open/parse pass to pull a small, known-size structure
200    /// (a header slot, the region table, the metadata region, the BAT) into a
201    /// bounded buffer. The returned `Vec` is short only when the container ends
202    /// before `offset + len` (so the caller still range-checks the parse).
203    ///
204    /// # Errors
205    /// Propagates the underlying positioned-read error.
206    pub fn read_exact_at(&self, offset: u64, len: usize) -> io::Result<Vec<u8>> {
207        let mut buf = vec![0u8; len];
208        let n = self.read_at(&mut buf, offset)?;
209        buf.truncate(n);
210        Ok(buf)
211    }
212}
213
214impl std::fmt::Debug for Backing {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        match self {
217            Backing::File(_) => f.debug_struct("File").field("len", &self.len()).finish(),
218            Backing::Sub { base, len, .. } => f
219                .debug_struct("Sub")
220                .field("base", base)
221                .field("len", len)
222                .finish(),
223            Backing::Mem(b) => f.debug_struct("Mem").field("len", &b.len()).finish(),
224            Backing::Reader { len, .. } => f.debug_struct("Reader").field("len", len).finish(),
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use std::io::Cursor;
232
233    use super::{Backing, ReadSeekSend};
234
235    /// Blanket impl: [`std::io::Cursor`]`<Vec<u8>>` satisfies [`ReadSeekSend`].
236    fn cursor_backing(data: Vec<u8>) -> Backing {
237        let len = data.len() as u64;
238        let inner = std::sync::Mutex::new(Box::new(Cursor::new(data)) as Box<dyn ReadSeekSend>);
239        Backing::Reader { inner, len }
240    }
241
242    #[test]
243    fn reader_backing_len_matches_construction() {
244        let b = cursor_backing(vec![1u8, 2, 3, 4, 5]);
245        assert_eq!(b.len(), 5);
246        assert!(!b.is_empty());
247    }
248
249    #[test]
250    fn reader_backing_read_at_fills_buf() {
251        let b = cursor_backing(vec![10, 20, 30, 40, 50]);
252        let mut buf = [0u8; 3];
253        let n = b.read_at(&mut buf, 1).unwrap();
254        assert_eq!(n, 3);
255        assert_eq!(buf, [20, 30, 40]);
256    }
257
258    #[test]
259    fn reader_backing_read_at_clamps_at_eof() {
260        let b = cursor_backing(vec![10, 20, 30]);
261        let mut buf = [0u8; 10];
262        let n = b.read_at(&mut buf, 1).unwrap();
263        assert_eq!(n, 2, "read past end should be clamped");
264        assert_eq!(&buf[..2], &[20, 30]);
265    }
266
267    #[test]
268    fn reader_backing_read_at_past_eof_returns_zero() {
269        let b = cursor_backing(vec![1, 2, 3]);
270        let mut buf = [0xFFu8; 4];
271        let n = b.read_at(&mut buf, 100).unwrap();
272        assert_eq!(n, 0);
273    }
274
275    #[test]
276    fn reader_backing_debug_does_not_panic() {
277        let b = cursor_backing(vec![0u8; 8]);
278        let s = format!("{b:?}");
279        assert!(s.contains("Reader"), "Debug output should name the variant");
280    }
281
282    #[test]
283    fn reader_backing_read_exact_at_works() {
284        let b = cursor_backing(vec![0, 1, 2, 3, 4, 5, 6, 7]);
285        let v = b.read_exact_at(2, 4).unwrap();
286        assert_eq!(v, vec![2, 3, 4, 5]);
287    }
288
289    #[test]
290    fn empty_reader_backing_is_empty() {
291        let b = cursor_backing(vec![]);
292        assert!(b.is_empty());
293        assert_eq!(b.len(), 0);
294    }
295}