#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
mod dynamic;
mod error;
mod read;
#[cfg(feature = "test-helpers")]
pub mod footer;
#[cfg(not(feature = "test-helpers"))]
mod footer;
#[cfg(feature = "vfs")]
pub mod vfs;
pub use error::VhdError;
pub use footer::{DiskType, VhdFooter};
pub trait ReadSeekSend: Read + Seek + Send + Sync {}
impl<T: Read + Seek + Send + Sync> ReadSeekSend for T {}
pub struct VhdReader {
inner: VhdInner,
pos: u64,
virtual_disk_size: u64,
original_size: u64,
}
enum VhdInner {
Fixed {
file: Box<dyn ReadSeekSend>,
},
Dynamic {
file: Box<dyn ReadSeekSend>,
bat: dynamic::BlockAllocationTable,
block_size: u32,
},
}
impl VhdReader {
pub fn open(path: &Path) -> Result<Self, VhdError> {
Self::open_reader(Box::new(std::fs::File::open(path)?))
}
pub fn open_reader(mut backing: Box<dyn ReadSeekSend>) -> Result<Self, VhdError> {
let mut data = Vec::new();
backing.read_to_end(&mut data)?;
let footer = footer::VhdFooter::parse(&data)?;
let (inner, virtual_disk_size) = match footer.disk_type {
footer::DiskType::Fixed => (VhdInner::Fixed { file: backing }, footer.current_size),
footer::DiskType::Dynamic => {
let dyn_hdr = dynamic::DynamicHeader::parse(&data, footer.data_offset)?;
let bat = dynamic::BlockAllocationTable::parse(&data, &dyn_hdr)?;
(
VhdInner::Dynamic {
file: backing,
bat,
block_size: dyn_hdr.block_size,
},
footer.current_size,
)
}
};
Ok(VhdReader {
inner,
pos: 0,
virtual_disk_size,
original_size: footer.original_size,
})
}
pub fn virtual_disk_size(&self) -> u64 {
self.virtual_disk_size
}
pub fn original_size(&self) -> u64 {
self.original_size
}
pub fn disk_type(&self) -> DiskType {
match &self.inner {
VhdInner::Fixed { .. } => DiskType::Fixed,
VhdInner::Dynamic { .. } => DiskType::Dynamic,
}
}
}
impl Read for VhdReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.pos >= self.virtual_disk_size || buf.is_empty() {
return Ok(0);
}
let remaining = (self.virtual_disk_size - self.pos) as usize;
let to_read = buf.len().min(remaining);
match &mut self.inner {
VhdInner::Fixed { file } => {
file.seek(SeekFrom::Start(self.pos))?;
let n = file.read(&mut buf[..to_read])?;
self.pos += n as u64;
Ok(n)
}
VhdInner::Dynamic {
file,
bat,
block_size,
} => {
let block_size_u64 = u64::from(*block_size);
let block_end = ((self.pos / block_size_u64) + 1) * block_size_u64;
let chunk = to_read.min((block_end - self.pos) as usize);
if let Some(file_off) = bat
.file_offset_for_byte(self.pos)
.map_err(|e| std::io::Error::other(e.to_string()))?
{
file.seek(SeekFrom::Start(file_off))?;
let n = file.read(&mut buf[..chunk])?;
self.pos += n as u64;
Ok(n)
} else {
buf[..chunk].fill(0);
self.pos += chunk as u64;
Ok(chunk)
}
}
}
}
}
impl Seek for VhdReader {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let new_pos = match pos {
SeekFrom::Start(n) => n as i64,
SeekFrom::Current(n) => self.pos as i64 + n,
SeekFrom::End(n) => self.virtual_disk_size as i64 + n,
};
if new_pos < 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"seek before start",
));
}
self.pos = new_pos as u64;
Ok(self.pos)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixed_vhd_bytes(sector_data: &[u8]) -> Vec<u8> {
let mut buf = sector_data.to_vec();
buf.extend_from_slice(&footer::test_fixed_footer(sector_data.len() as u64));
buf
}
fn write_tmp(data: &[u8]) -> tempfile::NamedTempFile {
use std::io::Write;
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(data).unwrap();
f
}
#[test]
fn seek_current_and_end_and_before_start() {
let image = fixed_vhd_bytes(&[0u8; 1024]);
let tmp = write_tmp(&image);
let mut r = VhdReader::open(tmp.path()).unwrap();
r.seek(SeekFrom::Start(200)).unwrap();
assert_eq!(r.seek(SeekFrom::Current(100)).unwrap(), 300); assert_eq!(
r.seek(SeekFrom::End(-10)).unwrap(),
r.virtual_disk_size() - 10
);
assert!(r.seek(SeekFrom::Current(-100_000)).is_err());
}
#[test]
fn open_reader_over_cursor_matches_open_path() {
use std::io::Cursor;
let sector: Vec<u8> = (0u8..=255).cycle().take(1024).collect();
let image = fixed_vhd_bytes(§or);
let tmp = write_tmp(&image);
let mut via_path = VhdReader::open(tmp.path()).expect("open path");
let mut want = Vec::new();
via_path.read_to_end(&mut want).expect("read path");
let mut via_reader =
VhdReader::open_reader(Box::new(Cursor::new(image.clone()))).expect("open_reader");
let mut got = Vec::new();
via_reader.read_to_end(&mut got).expect("read reader");
assert_eq!(
got, want,
"open_reader must read byte-identically to open(path)"
);
assert_eq!(via_reader.virtual_disk_size(), via_path.virtual_disk_size());
}
#[test]
fn open_nonexistent_returns_err() {
assert!(VhdReader::open(Path::new("/tmp/no_such.vhd")).is_err());
}
#[test]
fn open_empty_file_returns_err() {
let f = write_tmp(&[]);
assert!(VhdReader::open(f.path()).is_err());
}
#[test]
fn open_non_vhd_file_returns_err() {
let f = write_tmp(b"this is not a vhd file at all, no footer here");
assert!(VhdReader::open(f.path()).is_err());
}
#[test]
fn fixed_vhd_size_matches_footer() {
let sector = vec![0u8; 512];
let vhd = fixed_vhd_bytes(§or);
let f = write_tmp(&vhd);
let reader = VhdReader::open(f.path()).expect("open fixed vhd");
assert_eq!(reader.virtual_disk_size(), 512);
}
#[test]
fn fixed_vhd_disk_type_is_fixed() {
let sector = vec![0u8; 512];
let vhd = fixed_vhd_bytes(§or);
let f = write_tmp(&vhd);
let reader = VhdReader::open(f.path()).expect("open fixed vhd");
assert_eq!(reader.disk_type(), DiskType::Fixed);
}
#[test]
fn fixed_vhd_read_returns_sector_data() {
let mut sector = vec![0u8; 512];
sector[42] = 0xDE;
sector[43] = 0xAD;
let vhd = fixed_vhd_bytes(§or);
let f = write_tmp(&vhd);
let mut reader = VhdReader::open(f.path()).expect("open");
let mut buf = vec![0u8; 512];
reader.read_exact(&mut buf).expect("read");
assert_eq!(buf[42], 0xDE);
assert_eq!(buf[43], 0xAD);
}
#[test]
fn seek_and_read_at_offset() {
let mut sector = vec![0u8; 512];
sector[100] = 0xBE;
sector[101] = 0xEF;
let vhd = fixed_vhd_bytes(§or);
let f = write_tmp(&vhd);
let mut reader = VhdReader::open(f.path()).expect("open");
reader.seek(SeekFrom::Start(100)).unwrap();
let mut buf = [0u8; 2];
reader.read_exact(&mut buf).unwrap();
assert_eq!(buf, [0xBE, 0xEF]);
}
#[test]
fn differencing_disk_returns_err() {
let mut footer_bytes = footer::test_fixed_footer(512);
footer_bytes[60] = 0;
footer_bytes[61] = 0;
footer_bytes[62] = 0;
footer_bytes[63] = 4;
let mut vhd = vec![0u8; 512];
vhd.extend_from_slice(&footer_bytes);
let f = write_tmp(&vhd);
assert!(VhdReader::open(f.path()).is_err());
}
#[test]
fn vhd_reader_is_send() {
fn assert_send<T: Send>() {}
assert_send::<VhdReader>();
}
#[test]
fn dynamic_vhd_block_size_zero_rejected() {
use std::io::Write;
const BLOCK_SIZE: u64 = 0; let mut file = vec![0u8; 4096];
let footer = {
let mut f = vec![0u8; 512];
f[0..8].copy_from_slice(b"conectix");
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(&(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;
for (i, &b) in f.iter().enumerate() {
if !(64..68).contains(&i) {
s = s.wrapping_add(u32::from(b));
}
}
f[64..68].copy_from_slice(&(!s).to_be_bytes());
f
};
file[0..512].copy_from_slice(&footer);
file[3584..4096].copy_from_slice(&footer);
file[512..520].copy_from_slice(b"cxsparse");
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());
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(&file).unwrap();
assert!(
VhdReader::open(tmp.path()).is_err(),
"block_size=0 must be rejected at open() to prevent div-by-zero"
);
}
#[test]
fn bitmap_sectors_computed_for_4mib_block_size() {
use std::io::Write;
const BLOCK_SIZE: u64 = 4 * 1024 * 1024;
let mut file = vec![0u8; 4096];
let footer = {
let mut f = vec![0u8; 512];
f[0..8].copy_from_slice(b"conectix");
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;
for (i, &b) in f.iter().enumerate() {
if !(64..68).contains(&i) {
s = s.wrapping_add(u32::from(b));
}
}
f[64..68].copy_from_slice(&(!s).to_be_bytes());
f
};
file[0..512].copy_from_slice(&footer); file[3584..4096].copy_from_slice(&footer);
file[512..520].copy_from_slice(b"cxsparse");
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());
file[2048..2560].fill(0xFF); file[2560..3072].fill(0xFF);
file[3072..3584].fill(0xAB);
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(&file).unwrap();
let mut reader = VhdReader::open(tmp.path()).expect("open synthetic 4MiB-block vhd");
let mut buf = [0u8; 512];
reader.seek(SeekFrom::Start(0)).unwrap();
reader
.read_exact(&mut buf)
.expect("read block 0 data sector 0");
assert_eq!(
buf, [0xABu8; 512],
"with 4 MiB block_size, bitmap is 2 sectors (1024 bytes); \
BITMAP_SECTORS must not be hardcoded to 1"
);
}
}