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