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 three 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//!
18//! All three expose the same cursor-free, thread-safe positioned-read API
19//! (`read_at` + `len`), so the open/parse pass reads small structures from their
20//! known offsets and the data path reads just the resolved block — never the
21//! whole file.
22
23use std::fs::File;
24use std::io;
25use std::sync::Arc;
26
27/// Fill `buf` from `file` starting at `offset`, returning the bytes read (short
28/// only at end of file).
29///
30/// Uses the OS positioned-read primitive — `pread(2)` on Unix, `seek_read`
31/// (a `ReadFile` carrying its own `OVERLAPPED` offset) on Windows — so it takes
32/// `&File` and never touches a shared cursor. That makes it safe to call
33/// concurrently from many threads on one handle: each call carries its own
34/// offset, so there is no read/seek race. Keeps `forbid(unsafe)` (no mmap).
35fn pread(file: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
36    #[cfg(unix)]
37    use std::os::unix::fs::FileExt;
38    #[cfg(windows)]
39    use std::os::windows::fs::FileExt;
40
41    let mut total = 0usize;
42    while total < buf.len() {
43        #[cfg(unix)]
44        let res = file.read_at(&mut buf[total..], offset + total as u64);
45        #[cfg(windows)]
46        let res = file.seek_read(&mut buf[total..], offset + total as u64);
47        #[cfg(not(any(unix, windows)))]
48        let res: io::Result<usize> = Err(io::Error::new(
49            io::ErrorKind::Unsupported,
50            "positioned reads unsupported on this platform",
51        ));
52        match res {
53            Ok(0) => break,
54            Ok(n) => total += n,
55            Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
56            Err(e) => return Err(e),
57        }
58    }
59    Ok(total)
60}
61
62/// A positioned, cursor-free reader over one VHDX container's bytes.
63///
64/// `read_at(buf, offset)` fills `buf` from logical `offset` within the container
65/// and returns the byte count (short only at end of container) — never touching
66/// a shared cursor, so it is safe to call concurrently through `&self`.
67pub enum Backing {
68    /// A loose container file: positioned reads go straight to the OS handle.
69    File(File),
70    /// A contiguous sub-range `[base, base+len)` of a larger shared file.
71    Sub {
72        /// The backing file (shared; positioned reads carry their own offset).
73        file: Arc<File>,
74        /// Absolute file offset where this container's bytes begin.
75        base: u64,
76        /// Length of this container in bytes.
77        len: u64,
78    },
79    /// An in-RAM container (e.g. the legacy `from_bytes` path or an inflated
80    /// zip entry).
81    Mem(Arc<[u8]>),
82}
83
84impl Backing {
85    /// Construct a [`Backing::Sub`] over `[base, base+len)` of `file`.
86    #[must_use]
87    pub fn sub(file: Arc<File>, base: u64, len: u64) -> Self {
88        Backing::Sub { file, base, len }
89    }
90
91    /// Construct an in-RAM [`Backing::Mem`] from owned bytes.
92    #[must_use]
93    pub fn from_bytes(bytes: impl Into<Arc<[u8]>>) -> Self {
94        Backing::Mem(bytes.into())
95    }
96
97    /// Total length of this container in bytes.
98    #[must_use]
99    pub fn len(&self) -> u64 {
100        match self {
101            Backing::File(f) => f.metadata().map_or(0, |m| m.len()),
102            Backing::Sub { len, .. } => *len,
103            Backing::Mem(b) => b.len() as u64,
104        }
105    }
106
107    /// Whether the container is empty.
108    #[must_use]
109    pub fn is_empty(&self) -> bool {
110        self.len() == 0
111    }
112
113    /// Fill `buf` from logical `offset` within this container, returning the
114    /// bytes read (short only at the container's end). Cursor-free and
115    /// thread-safe.
116    ///
117    /// # Errors
118    /// Propagates the underlying I/O error for [`Backing::File`] /
119    /// [`Backing::Sub`]; [`Backing::Mem`] never fails.
120    pub fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
121        match self {
122            Backing::File(f) => pread(f, buf, offset),
123            Backing::Sub { file, base, len } => {
124                // Clamp the request to this container's window so a Sub never
125                // reads past its end into a neighbouring entry. A read starting
126                // beyond the window yields 0 (clean EOF), mirroring File/Mem.
127                let avail = len.saturating_sub(offset);
128                if avail == 0 {
129                    return Ok(0);
130                }
131                let want = (buf.len() as u64).min(avail) as usize;
132                pread(file, &mut buf[..want], base + offset)
133            }
134            Backing::Mem(bytes) => {
135                let off = offset.min(bytes.len() as u64) as usize;
136                let src = &bytes[off..];
137                let n = src.len().min(buf.len());
138                buf[..n].copy_from_slice(&src[..n]);
139                Ok(n)
140            }
141        }
142    }
143
144    /// Read exactly `len` bytes starting at `offset` into a fresh `Vec`.
145    ///
146    /// Used by the open/parse pass to pull a small, known-size structure
147    /// (a header slot, the region table, the metadata region, the BAT) into a
148    /// bounded buffer. The returned `Vec` is short only when the container ends
149    /// before `offset + len` (so the caller still range-checks the parse).
150    ///
151    /// # Errors
152    /// Propagates the underlying positioned-read error.
153    pub fn read_exact_at(&self, offset: u64, len: usize) -> io::Result<Vec<u8>> {
154        let mut buf = vec![0u8; len];
155        let n = self.read_at(&mut buf, offset)?;
156        buf.truncate(n);
157        Ok(buf)
158    }
159}
160
161impl std::fmt::Debug for Backing {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        match self {
164            Backing::File(_) => f.debug_struct("File").field("len", &self.len()).finish(),
165            Backing::Sub { base, len, .. } => f
166                .debug_struct("Sub")
167                .field("base", base)
168                .field("len", len)
169                .finish(),
170            Backing::Mem(b) => f.debug_struct("Mem").field("len", &b.len()).finish(),
171        }
172    }
173}