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}