1#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
14
15use std::io::{Read, Seek, SeekFrom};
16use std::path::Path;
17
18mod dynamic;
19mod error;
20mod read;
21
22#[cfg(feature = "test-helpers")]
23pub mod footer;
24#[cfg(not(feature = "test-helpers"))]
25mod footer;
26
27#[cfg(feature = "vfs")]
28pub mod vfs;
29
30pub use error::VhdError;
31pub use footer::{DiskType, VhdFooter};
32
33pub trait ReadSeekSend: Read + Seek + Send + Sync {}
39impl<T: Read + Seek + Send + Sync> ReadSeekSend for T {}
40
41pub struct VhdReader {
45 inner: VhdInner,
46 pos: u64,
47 virtual_disk_size: u64,
48 original_size: u64,
49}
50
51enum VhdInner {
52 Fixed {
53 file: Box<dyn ReadSeekSend>,
54 },
55 Dynamic {
56 file: Box<dyn ReadSeekSend>,
57 bat: dynamic::BlockAllocationTable,
58 block_size: u32,
59 },
60}
61
62impl VhdReader {
63 pub fn open(path: &Path) -> Result<Self, VhdError> {
68 Self::open_reader(Box::new(std::fs::File::open(path)?))
69 }
70
71 pub fn open_reader(mut backing: Box<dyn ReadSeekSend>) -> Result<Self, VhdError> {
76 let mut data = Vec::new();
80 backing.read_to_end(&mut data)?;
81 let footer = footer::VhdFooter::parse(&data)?;
82
83 let (inner, virtual_disk_size) = match footer.disk_type {
84 footer::DiskType::Fixed => (VhdInner::Fixed { file: backing }, footer.current_size),
85 footer::DiskType::Dynamic => {
86 let dyn_hdr = dynamic::DynamicHeader::parse(&data, footer.data_offset)?;
87 let bat = dynamic::BlockAllocationTable::parse(&data, &dyn_hdr)?;
88 (
89 VhdInner::Dynamic {
90 file: backing,
91 bat,
92 block_size: dyn_hdr.block_size,
93 },
94 footer.current_size,
95 )
96 }
97 };
98
99 Ok(VhdReader {
100 inner,
101 pos: 0,
102 virtual_disk_size,
103 original_size: footer.original_size,
104 })
105 }
106
107 pub fn virtual_disk_size(&self) -> u64 {
110 self.virtual_disk_size
111 }
112
113 pub fn original_size(&self) -> u64 {
117 self.original_size
118 }
119
120 pub fn disk_type(&self) -> DiskType {
122 match &self.inner {
123 VhdInner::Fixed { .. } => DiskType::Fixed,
124 VhdInner::Dynamic { .. } => DiskType::Dynamic,
125 }
126 }
127}
128
129impl Read for VhdReader {
130 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
131 if self.pos >= self.virtual_disk_size || buf.is_empty() {
132 return Ok(0);
133 }
134 let remaining = (self.virtual_disk_size - self.pos) as usize;
135 let to_read = buf.len().min(remaining);
136
137 match &mut self.inner {
138 VhdInner::Fixed { file } => {
139 file.seek(SeekFrom::Start(self.pos))?;
140 let n = file.read(&mut buf[..to_read])?;
141 self.pos += n as u64;
142 Ok(n)
143 }
144 VhdInner::Dynamic {
145 file,
146 bat,
147 block_size,
148 } => {
149 let block_size_u64 = u64::from(*block_size);
150 let block_end = ((self.pos / block_size_u64) + 1) * block_size_u64;
151 let chunk = to_read.min((block_end - self.pos) as usize);
152
153 if let Some(file_off) = bat
154 .file_offset_for_byte(self.pos)
155 .map_err(|e| std::io::Error::other(e.to_string()))?
156 {
157 file.seek(SeekFrom::Start(file_off))?;
158 let n = file.read(&mut buf[..chunk])?;
159 self.pos += n as u64;
160 Ok(n)
161 } else {
162 buf[..chunk].fill(0);
164 self.pos += chunk as u64;
165 Ok(chunk)
166 }
167 }
168 }
169 }
170}
171
172impl Seek for VhdReader {
173 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
174 let new_pos = match pos {
175 SeekFrom::Start(n) => n as i64,
176 SeekFrom::Current(n) => self.pos as i64 + n,
177 SeekFrom::End(n) => self.virtual_disk_size as i64 + n,
178 };
179 if new_pos < 0 {
180 return Err(std::io::Error::new(
181 std::io::ErrorKind::InvalidInput,
182 "seek before start",
183 ));
184 }
185 self.pos = new_pos as u64;
186 Ok(self.pos)
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 fn fixed_vhd_bytes(sector_data: &[u8]) -> Vec<u8> {
196 let mut buf = sector_data.to_vec();
197 buf.extend_from_slice(&footer::test_fixed_footer(sector_data.len() as u64));
198 buf
199 }
200
201 fn write_tmp(data: &[u8]) -> tempfile::NamedTempFile {
202 use std::io::Write;
203 let mut f = tempfile::NamedTempFile::new().unwrap();
204 f.write_all(data).unwrap();
205 f
206 }
207
208 #[test]
209 fn seek_current_and_end_and_before_start() {
210 let image = fixed_vhd_bytes(&[0u8; 1024]);
211 let tmp = write_tmp(&image);
212 let mut r = VhdReader::open(tmp.path()).unwrap();
213 r.seek(SeekFrom::Start(200)).unwrap();
214 assert_eq!(r.seek(SeekFrom::Current(100)).unwrap(), 300); assert_eq!(
216 r.seek(SeekFrom::End(-10)).unwrap(),
217 r.virtual_disk_size() - 10
218 );
219 assert!(r.seek(SeekFrom::Current(-100_000)).is_err());
221 }
222
223 #[test]
224 fn open_reader_over_cursor_matches_open_path() {
225 use std::io::Cursor;
226 let sector: Vec<u8> = (0u8..=255).cycle().take(1024).collect();
227 let image = fixed_vhd_bytes(§or);
228
229 let tmp = write_tmp(&image);
231 let mut via_path = VhdReader::open(tmp.path()).expect("open path");
232 let mut want = Vec::new();
233 via_path.read_to_end(&mut want).expect("read path");
234
235 let mut via_reader =
238 VhdReader::open_reader(Box::new(Cursor::new(image.clone()))).expect("open_reader");
239 let mut got = Vec::new();
240 via_reader.read_to_end(&mut got).expect("read reader");
241
242 assert_eq!(
243 got, want,
244 "open_reader must read byte-identically to open(path)"
245 );
246 assert_eq!(via_reader.virtual_disk_size(), via_path.virtual_disk_size());
247 }
248
249 #[test]
250 fn open_nonexistent_returns_err() {
251 assert!(VhdReader::open(Path::new("/tmp/no_such.vhd")).is_err());
252 }
253
254 #[test]
255 fn open_empty_file_returns_err() {
256 let f = write_tmp(&[]);
257 assert!(VhdReader::open(f.path()).is_err());
258 }
259
260 #[test]
261 fn open_non_vhd_file_returns_err() {
262 let f = write_tmp(b"this is not a vhd file at all, no footer here");
263 assert!(VhdReader::open(f.path()).is_err());
264 }
265
266 #[test]
267 fn fixed_vhd_size_matches_footer() {
268 let sector = vec![0u8; 512];
269 let vhd = fixed_vhd_bytes(§or);
270 let f = write_tmp(&vhd);
271 let reader = VhdReader::open(f.path()).expect("open fixed vhd");
272 assert_eq!(reader.virtual_disk_size(), 512);
273 }
274
275 #[test]
276 fn fixed_vhd_disk_type_is_fixed() {
277 let sector = vec![0u8; 512];
278 let vhd = fixed_vhd_bytes(§or);
279 let f = write_tmp(&vhd);
280 let reader = VhdReader::open(f.path()).expect("open fixed vhd");
281 assert_eq!(reader.disk_type(), DiskType::Fixed);
282 }
283
284 #[test]
285 fn fixed_vhd_read_returns_sector_data() {
286 let mut sector = vec![0u8; 512];
287 sector[42] = 0xDE;
288 sector[43] = 0xAD;
289 let vhd = fixed_vhd_bytes(§or);
290 let f = write_tmp(&vhd);
291 let mut reader = VhdReader::open(f.path()).expect("open");
292 let mut buf = vec![0u8; 512];
293 reader.read_exact(&mut buf).expect("read");
294 assert_eq!(buf[42], 0xDE);
295 assert_eq!(buf[43], 0xAD);
296 }
297
298 #[test]
299 fn seek_and_read_at_offset() {
300 let mut sector = vec![0u8; 512];
301 sector[100] = 0xBE;
302 sector[101] = 0xEF;
303 let vhd = fixed_vhd_bytes(§or);
304 let f = write_tmp(&vhd);
305 let mut reader = VhdReader::open(f.path()).expect("open");
306 reader.seek(SeekFrom::Start(100)).unwrap();
307 let mut buf = [0u8; 2];
308 reader.read_exact(&mut buf).unwrap();
309 assert_eq!(buf, [0xBE, 0xEF]);
310 }
311
312 #[test]
313 fn differencing_disk_returns_err() {
314 let mut footer_bytes = footer::test_fixed_footer(512);
316 footer_bytes[60] = 0;
318 footer_bytes[61] = 0;
319 footer_bytes[62] = 0;
320 footer_bytes[63] = 4;
321 let mut vhd = vec![0u8; 512];
322 vhd.extend_from_slice(&footer_bytes);
323 let f = write_tmp(&vhd);
324 assert!(VhdReader::open(f.path()).is_err());
325 }
326
327 #[test]
328 fn vhd_reader_is_send() {
329 fn assert_send<T: Send>() {}
330 assert_send::<VhdReader>();
331 }
332
333 #[test]
340 fn dynamic_vhd_block_size_zero_rejected() {
341 use std::io::Write;
342
343 const BLOCK_SIZE: u64 = 0; let mut file = vec![0u8; 4096];
345
346 let footer = {
347 let mut f = vec![0u8; 512];
348 f[0..8].copy_from_slice(b"conectix");
349 f[8..12].copy_from_slice(&0x0000_0002u32.to_be_bytes());
350 f[12..16].copy_from_slice(&0x0001_0000u32.to_be_bytes());
351 f[16..24].copy_from_slice(&512u64.to_be_bytes());
352 f[40..48].copy_from_slice(&(512u64).to_be_bytes()); f[48..56].copy_from_slice(&(512u64).to_be_bytes()); f[60..64].copy_from_slice(&3u32.to_be_bytes()); let mut s: u32 = 0;
356 for (i, &b) in f.iter().enumerate() {
357 if !(64..68).contains(&i) {
358 s = s.wrapping_add(u32::from(b));
359 }
360 }
361 f[64..68].copy_from_slice(&(!s).to_be_bytes());
362 f
363 };
364
365 file[0..512].copy_from_slice(&footer);
366 file[3584..4096].copy_from_slice(&footer);
367 file[512..520].copy_from_slice(b"cxsparse");
368 file[512 + 16..512 + 24].copy_from_slice(&1536u64.to_be_bytes());
369 file[512 + 28..512 + 32].copy_from_slice(&1u32.to_be_bytes());
370 file[512 + 32..512 + 36].copy_from_slice(&(BLOCK_SIZE as u32).to_be_bytes()); let mut tmp = tempfile::NamedTempFile::new().unwrap();
373 tmp.write_all(&file).unwrap();
374 assert!(
375 VhdReader::open(tmp.path()).is_err(),
376 "block_size=0 must be rejected at open() to prevent div-by-zero"
377 );
378 }
379
380 #[test]
388 fn bitmap_sectors_computed_for_4mib_block_size() {
389 use std::io::Write;
390
391 const BLOCK_SIZE: u64 = 4 * 1024 * 1024;
403 let mut file = vec![0u8; 4096];
404
405 let footer = {
407 let mut f = vec![0u8; 512];
408 f[0..8].copy_from_slice(b"conectix");
409 f[8..12].copy_from_slice(&0x0000_0002u32.to_be_bytes()); f[12..16].copy_from_slice(&0x0001_0000u32.to_be_bytes()); f[16..24].copy_from_slice(&512u64.to_be_bytes()); f[40..48].copy_from_slice(&BLOCK_SIZE.to_be_bytes()); f[48..56].copy_from_slice(&BLOCK_SIZE.to_be_bytes()); f[60..64].copy_from_slice(&3u32.to_be_bytes()); let mut s: u32 = 0;
417 for (i, &b) in f.iter().enumerate() {
418 if !(64..68).contains(&i) {
419 s = s.wrapping_add(u32::from(b));
420 }
421 }
422 f[64..68].copy_from_slice(&(!s).to_be_bytes());
423 f
424 };
425
426 file[0..512].copy_from_slice(&footer); file[3584..4096].copy_from_slice(&footer); file[512..520].copy_from_slice(b"cxsparse");
431 file[512 + 16..512 + 24].copy_from_slice(&1536u64.to_be_bytes()); file[512 + 28..512 + 32].copy_from_slice(&1u32.to_be_bytes()); file[512 + 32..512 + 36].copy_from_slice(&(BLOCK_SIZE as u32).to_be_bytes()); file[1536..1540].copy_from_slice(&4u32.to_be_bytes());
437
438 file[2048..2560].fill(0xFF); file[2560..3072].fill(0xFF); file[3072..3584].fill(0xAB);
446
447 let mut tmp = tempfile::NamedTempFile::new().unwrap();
448 tmp.write_all(&file).unwrap();
449
450 let mut reader = VhdReader::open(tmp.path()).expect("open synthetic 4MiB-block vhd");
451 let mut buf = [0u8; 512];
452 reader.seek(SeekFrom::Start(0)).unwrap();
453 reader
454 .read_exact(&mut buf)
455 .expect("read block 0 data sector 0");
456 assert_eq!(
457 buf, [0xABu8; 512],
458 "with 4 MiB block_size, bitmap is 2 sectors (1024 bytes); \
459 BITMAP_SECTORS must not be hardcoded to 1"
460 );
461 }
462
463 }