block: Detect and open flat VMDK images

Adds support to detect and open flat vmdk images.
In particular, it updates the `detect_image_type` method to
account for unaligned & small sized descriptor file (which describes
the VMDK disk) reads using AlignedFile::read_at instead of
read_file_at to loop over for small reads.

Signed-off-by: Sumedh Alok Sharma <sumsharma@microsoft.com>
This commit is contained in:
Sumedh Alok Sharma
2026-07-13 10:27:47 +00:00
committed by Wei Liu
parent e6fd5fefc4
commit 82b65c3352
4 changed files with 61 additions and 4 deletions

View File

@@ -24,6 +24,7 @@ use crate::formats::qcow::QcowDisk;
use crate::formats::raw::{RawBackend, RawDisk};
use crate::formats::vhd::VhdDisk;
use crate::formats::vhdx::VhdxDisk;
use crate::formats::vmdk::VmdkDisk;
use crate::{
ImageType, block_aio_is_supported, detect_image_type, open_disk_image, preallocate_disk,
};
@@ -98,6 +99,7 @@ pub fn open_disk(options: &DiskOpenOptions<'_>) -> BlockResult<OpenedDisk> {
ImageType::Raw => open_raw(file, options)?,
ImageType::Qcow2 => open_qcow2(file, options)?,
ImageType::Vhdx => open_vhdx(file, options)?,
ImageType::FlatVmdk => open_flat_vmdk(file, options)?,
ImageType::Unknown => {
return Err(
BlockError::from_kind(BlockErrorKind::UnsupportedFeature).with_path(options.path)
@@ -215,6 +217,16 @@ fn open_qcow2(
))
}
fn open_flat_vmdk(
file: fs::File,
options: &DiskOpenOptions<'_>,
) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
info!("Opening VMDK disk file with synchronous backend");
Ok(Box::new(
VmdkDisk::new(file, options.path, options.direct).map_err(|e| e.with_path(options.path))?,
))
}
#[cfg(test)]
mod unit_tests {
use std::path::Path;

View File

@@ -119,7 +119,11 @@ impl VhdFooter {
/// Determine image type through file parsing.
pub fn is_fixed_vhd(f: &mut File) -> io::Result<bool> {
let footer = VhdFooter::new(f)?;
let footer = match VhdFooter::new(f) {
Ok(footer) => footer,
Err(e) if e.kind() == io::ErrorKind::InvalidInput => return Ok(false),
Err(e) => return Err(e),
};
// "conectix" => 0x636f6e6563746978
Ok(footer.cookie() == 0x636f6e6563746978
@@ -229,4 +233,11 @@ mod unit_tests {
assert!(!(is_fixed_vhd(&mut file).unwrap()));
});
}
#[test]
fn test_is_fixed_vhd_short_file_is_not_vhd() {
let mut file: File = TempFile::new().unwrap().into_file();
file.write_all(b"# Disk DescriptorFile\n").unwrap();
assert!(!is_fixed_vhd(&mut file).unwrap());
}
}

View File

@@ -484,6 +484,7 @@ pub fn preallocate_disk<P: AsRef<Path>>(file: &File, path: P) {
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ImageType {
FlatVmdk,
FixedVhd,
Qcow2,
Raw,
@@ -495,6 +496,7 @@ pub enum ImageType {
impl fmt::Display for ImageType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImageType::FlatVmdk => write!(f, "vmdk"),
ImageType::FixedVhd => write!(f, "vhd"),
ImageType::Qcow2 => write!(f, "qcow2"),
ImageType::Raw => write!(f, "raw"),
@@ -513,6 +515,7 @@ impl FromStr for ImageType {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"vmdk" => Ok(ImageType::FlatVmdk),
"vhd" => Ok(ImageType::FixedVhd),
"qcow2" => Ok(ImageType::Qcow2),
"raw" => Ok(ImageType::Raw),
@@ -538,10 +541,23 @@ pub fn open_disk_image(path: &Path, options: &OpenOptions) -> BlockResult<File>
/// Determine image type through file parsing.
pub fn detect_image_type(f: &mut File) -> BlockResult<ImageType> {
let aligned = AlignedFile::new(f.try_clone()?, true);
// A VMDK descriptor file is small few hundred bytes of text. Read
// best-effort so a short file yields a zero-padded partial block instead
// of an UnexpectedEof, and let the per-format probes below decide.
let mut block = vec![0u8; aligned.alignment()];
aligned
.read_exact_at(&mut block, 0)
.map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))?;
let mut filled = 0;
while filled < block.len() {
match aligned.read_at(&mut block[filled..], filled as u64) {
Ok(0) => break,
Ok(n) => filled += n,
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => {
return Err(
BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType)
);
}
}
}
// Check 4 first bytes to get the header value and determine the image type
let image_type = if u32::from_be_bytes(block[0..4].try_into().unwrap()) == QCOW_MAGIC {
@@ -552,6 +568,11 @@ pub fn detect_image_type(f: &mut File) -> BlockResult<ImageType> {
ImageType::FixedVhd
} else if u64::from_le_bytes(block[0..8].try_into().unwrap()) == VHDX_SIGN {
ImageType::Vhdx
} else if formats::vmdk::has_descriptor_header(&block)
&& formats::vmdk::is_flat_vmdk(f)
.map_err(|e| BlockError::new(BlockErrorKind::Io, e).with_op(ErrorOp::DetectImageType))?
{
ImageType::FlatVmdk
} else {
ImageType::Raw
};
@@ -682,6 +703,18 @@ mod unit_tests {
use super::*;
#[test]
fn detect_short_file_is_not_eof_error() {
let tmp = TempFile::new().unwrap();
let mut f = tmp.into_file();
f.write_all(b"not-a-disk-magic-just-some-short-text\n")
.unwrap();
f.sync_all().unwrap();
let image_type = detect_image_type(&mut f).unwrap();
assert_eq!(image_type, ImageType::Raw);
}
#[test]
fn test_probe_regular_file_returns_valid_alignment() {
let temp_file = TempFile::new().unwrap();

View File

@@ -1597,6 +1597,7 @@ mod common_parallel {
ImageType::Qcow2 => ("qcow2", &[]),
ImageType::FixedVhd => ("vpc", &["-o", "subformat=fixed"]),
ImageType::Vhdx => ("vhdx", &[]),
ImageType::FlatVmdk => panic!("unsupported image_type {image_type}"),
ImageType::Unknown => panic!("unsupported image_type {image_type}"),
};
let image_type_str = image_type.to_string();