mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
block: Add VMDK descriptor file parser for Flat createType
Adds parser for the Virtual Machine DisK descriptor file. The parser supports 2 flat/pre-allocated create types, monolithicFlat & twoGbMaxExtentFlat. The parser marks any other type as unsupported. The implementation references VMDK descriptor specification from https://web.archive.org/web/20240527050638if_/https://www.vmware.com/app/vmdk/?src=vmdk Signed-off-by: Sumedh Alok Sharma <sumsharma@microsoft.com>
This commit is contained in:
committed by
Wei Liu
parent
dcea656a72
commit
7b24354020
@@ -11,3 +11,4 @@ pub mod qcow;
|
|||||||
pub mod raw;
|
pub mod raw;
|
||||||
pub mod vhd;
|
pub mod vhd;
|
||||||
pub mod vhdx;
|
pub mod vhdx;
|
||||||
|
pub mod vmdk;
|
||||||
|
|||||||
513
block/src/formats/vmdk/descriptor.rs
Normal file
513
block/src/formats/vmdk/descriptor.rs
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
// Copyright © 2026, Microsoft Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//! Parser for the flat VMDK text descriptor.
|
||||||
|
//!
|
||||||
|
//! A flat VMDK stores its layout in a small text descriptor: a header, a list
|
||||||
|
//! of `FLAT` extent lines, and a disk database (DDB). Only the `monolithicFlat`
|
||||||
|
//! and `twoGbMaxExtentFlat` create types are recognized.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::os::unix::fs::FileExt;
|
||||||
|
use std::str::Lines;
|
||||||
|
|
||||||
|
use crate::AlignedFile;
|
||||||
|
|
||||||
|
const VMDK_DESCRIPTOR_HEADER: &str = "# Disk DescriptorFile";
|
||||||
|
const VMDK_DESCRIPTOR_EXTENTS: &str = "# Extent description";
|
||||||
|
const VMDK_DESCRIPTOR_DDB: &str = "# The Disk Data Base";
|
||||||
|
const VMDK_DESCRIPTOR_DDB_2: &str = "#DDB";
|
||||||
|
|
||||||
|
/// Flat VMDK create types.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub enum VMDKDiskType {
|
||||||
|
#[default]
|
||||||
|
CreateTypeUnsupported,
|
||||||
|
MonolithicFlat,
|
||||||
|
TwoGbMaxExtentFlat,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flat VMDK extent line fields.
|
||||||
|
/// Format of each extent line:
|
||||||
|
/// `<access> <sectors> <type> "<file>" [offset]`.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct VmdkExtentHeader {
|
||||||
|
pub access: String,
|
||||||
|
pub size_in_sectors: u64,
|
||||||
|
pub extent_type: String,
|
||||||
|
pub filename: String,
|
||||||
|
pub offset_in_sectors: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Descriptor header fields.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct VmdkDescriptorHeader {
|
||||||
|
pub version: u32,
|
||||||
|
pub cid: u32,
|
||||||
|
pub parent_cid: u32,
|
||||||
|
pub create_type: VMDKDiskType,
|
||||||
|
pub parent_filename_hint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ordered list of extents.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct VmdkDescriptorExtents {
|
||||||
|
pub extents: Vec<VmdkExtentHeader>,
|
||||||
|
}
|
||||||
|
/// Disk database.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct VmdkDescriptorDdb {
|
||||||
|
pub entries: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the whole descriptor into memory through an `AlignedFile` and return it
|
||||||
|
// as a `String`.
|
||||||
|
fn read_descriptor(file: &File) -> io::Result<String> {
|
||||||
|
let aligned = AlignedFile::new(file.try_clone()?, true);
|
||||||
|
let len = file.metadata()?.len() as usize;
|
||||||
|
let mut buf = vec![0u8; len];
|
||||||
|
let mut filled = 0;
|
||||||
|
while filled < buf.len() {
|
||||||
|
match aligned.read_at(&mut buf[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(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.truncate(filled);
|
||||||
|
// A descriptor is ASCII text, so invalid UTF-8 means "not a descriptor".
|
||||||
|
String::from_utf8(buf).map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"VMDK descriptor is not valid UTF-8",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_header<'a>(
|
||||||
|
lines: &mut Lines<'a>,
|
||||||
|
) -> io::Result<(VmdkDescriptorHeader, &'a str)> {
|
||||||
|
let header_line = lines.next().unwrap_or_default();
|
||||||
|
|
||||||
|
// Reject actual disk data (or an embedded descriptor, which is
|
||||||
|
// unsupported): a flat descriptor must start with the header line.
|
||||||
|
if header_line.trim_end() != VMDK_DESCRIPTOR_HEADER {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("Not a VMDK descriptor file: missing header: {header_line}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut header = VmdkDescriptorHeader::default();
|
||||||
|
let mut last_comment_line = "";
|
||||||
|
|
||||||
|
for line in lines.by_ref() {
|
||||||
|
if line.starts_with('#') {
|
||||||
|
// End of the header section.
|
||||||
|
last_comment_line = line;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let parts: Vec<&str> = line.split('=').map(|s| s.trim()).collect();
|
||||||
|
if parts.len() == 2 {
|
||||||
|
match parts[0] {
|
||||||
|
"version" => header.version = parts[1].parse().unwrap_or(0),
|
||||||
|
"CID" => header.cid = u32::from_str_radix(parts[1], 16).unwrap_or(0),
|
||||||
|
"parentCID" => header.parent_cid = u32::from_str_radix(parts[1], 16).unwrap_or(0),
|
||||||
|
"createType" => {
|
||||||
|
// Tools such as qemu-img quote the value, e.g.
|
||||||
|
// createType="monolithicFlat", strip the quotes.
|
||||||
|
header.create_type = match parts[1].trim_matches('"') {
|
||||||
|
"monolithicFlat" => VMDKDiskType::MonolithicFlat,
|
||||||
|
"twoGbMaxExtentFlat" => VMDKDiskType::TwoGbMaxExtentFlat,
|
||||||
|
_ => VMDKDiskType::CreateTypeUnsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"parentFileNameHint" => header.parent_filename_hint = parts[1].to_string(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((header, last_comment_line))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_extents_and_ddb(
|
||||||
|
lines: &mut Lines<'_>,
|
||||||
|
last_comment_line: &str,
|
||||||
|
) -> io::Result<(VmdkDescriptorExtents, VmdkDescriptorDdb)> {
|
||||||
|
let mut extents = VmdkDescriptorExtents::default();
|
||||||
|
let mut ddb = VmdkDescriptorDdb::default();
|
||||||
|
|
||||||
|
if last_comment_line != VMDK_DESCRIPTOR_EXTENTS {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"Expected the extents section comment line",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut in_extents_section = true;
|
||||||
|
for line in lines.by_ref() {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if line.starts_with('#') {
|
||||||
|
if line == VMDK_DESCRIPTOR_DDB || line == VMDK_DESCRIPTOR_DDB_2 {
|
||||||
|
in_extents_section = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"Expected the DDB section comment line",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if in_extents_section {
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.len() == 4 || parts.len() == 5 {
|
||||||
|
let size_in_sectors = parts[1].parse::<u64>().map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!(
|
||||||
|
"VMDK extent size '{}' is not a valid sector count",
|
||||||
|
parts[1]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let offset_in_sectors = match parts.get(4) {
|
||||||
|
Some(offset) => offset.parse::<u64>().map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("VMDK extent offset '{offset}' is not a valid sector count"),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
let extent = VmdkExtentHeader {
|
||||||
|
access: parts[0].to_string(),
|
||||||
|
size_in_sectors,
|
||||||
|
extent_type: parts[2].to_string(),
|
||||||
|
filename: parts[3].trim_matches('"').to_string(),
|
||||||
|
offset_in_sectors,
|
||||||
|
};
|
||||||
|
extents.extents.push(extent);
|
||||||
|
} else {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"Malformed VMDK extent line",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let parts: Vec<&str> = line.split('=').map(|s| s.trim()).collect();
|
||||||
|
if parts.len() == 2 {
|
||||||
|
ddb.entries
|
||||||
|
.insert(parts[0].to_string(), parts[1].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((extents, ddb))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true when `prefix` begins with the `# Disk DescriptorFile` header.
|
||||||
|
pub fn has_descriptor_header(prefix: &[u8]) -> bool {
|
||||||
|
prefix.starts_with(VMDK_DESCRIPTOR_HEADER.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true for a supported flat VMDK: create type `monolithicFlat` or
|
||||||
|
/// `twoGbMaxExtentFlat` with only `FLAT` extents.
|
||||||
|
pub fn is_flat_vmdk(f: &mut File) -> io::Result<bool> {
|
||||||
|
let content = match read_descriptor(f) {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::InvalidData => return Ok(false),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
let mut lines = content.lines();
|
||||||
|
|
||||||
|
let (header, last_line) = match parse_header(&mut lines) {
|
||||||
|
Ok(parsed) => parsed,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::InvalidData => return Ok(false),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
match header.create_type {
|
||||||
|
VMDKDiskType::MonolithicFlat | VMDKDiskType::TwoGbMaxExtentFlat => {}
|
||||||
|
_ => return Ok(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
let extents = match parse_extents_and_ddb(&mut lines, last_line) {
|
||||||
|
Ok((extents, _ddb)) => extents,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::InvalidData => return Ok(false),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
for extent in &extents.extents {
|
||||||
|
if extent.extent_type != "FLAT" {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn parse_hdr(input: &str) -> io::Result<(VmdkDescriptorHeader, String)> {
|
||||||
|
let mut lines = input.lines();
|
||||||
|
let (header, last) = parse_header(&mut lines)?;
|
||||||
|
Ok((header, last.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_body(
|
||||||
|
last_comment: &str,
|
||||||
|
body: &str,
|
||||||
|
) -> io::Result<(VmdkDescriptorExtents, VmdkDescriptorDdb)> {
|
||||||
|
let mut lines = body.lines();
|
||||||
|
parse_extents_and_ddb(&mut lines, last_comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two-stage parse, as `VmdkDescriptor::new` chains it.
|
||||||
|
fn parse_full(
|
||||||
|
input: &str,
|
||||||
|
) -> io::Result<(
|
||||||
|
VmdkDescriptorHeader,
|
||||||
|
VmdkDescriptorExtents,
|
||||||
|
VmdkDescriptorDdb,
|
||||||
|
)> {
|
||||||
|
let mut lines = input.lines();
|
||||||
|
let (header, last) = parse_header(&mut lines)?;
|
||||||
|
let (extents, ddb) = parse_extents_and_ddb(&mut lines, last)?;
|
||||||
|
Ok((header, extents, ddb))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_flat_extent_with_ddb() {
|
||||||
|
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||||
|
# The Disk Data Base\n\
|
||||||
|
ddb.adapterType = \"ide\"\n\
|
||||||
|
ddb.geometry.sectors = \"63\"\n";
|
||||||
|
|
||||||
|
let (extents, ddb) = parse_body("# Extent description", body).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(extents.extents.len(), 1);
|
||||||
|
let e = &extents.extents[0];
|
||||||
|
assert_eq!(e.access, "RW");
|
||||||
|
assert_eq!(e.size_in_sectors, 2_097_152);
|
||||||
|
assert_eq!(e.extent_type, "FLAT");
|
||||||
|
assert_eq!(e.filename, "disk-flat.vmdk");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ddb.entries.get("ddb.adapterType").map(String::as_str),
|
||||||
|
Some("\"ide\"")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ddb.entries.get("ddb.geometry.sectors").map(String::as_str),
|
||||||
|
Some("\"63\"")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_extents_two_gb_max() {
|
||||||
|
let body: &str = "RW 4192256 FLAT \"disk-s001.vmdk\"\n\
|
||||||
|
RW 4192256 FLAT \"disk-s002.vmdk\"\n\
|
||||||
|
RW 2097152 FLAT \"disk-s003.vmdk\"\n\
|
||||||
|
# The Disk Data Base\n\
|
||||||
|
ddb.adapterType = \"lsilogic\"\n";
|
||||||
|
|
||||||
|
let (extents, _ddb) = parse_body("# Extent description", body).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(extents.extents.len(), 3);
|
||||||
|
assert_eq!(extents.extents[0].filename, "disk-s001.vmdk");
|
||||||
|
assert_eq!(extents.extents[2].filename, "disk-s003.vmdk");
|
||||||
|
assert!(extents.extents.iter().all(|e| e.extent_type == "FLAT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extent_line_with_optional_offset_field() {
|
||||||
|
// 5-field form: <access> <sectors> <type> <file> <offset>
|
||||||
|
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\" 0\n";
|
||||||
|
|
||||||
|
let (extents, _ddb) = parse_body("# Extent description", body).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(extents.extents.len(), 1);
|
||||||
|
assert_eq!(extents.extents[0].filename, "disk-flat.vmdk");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extent_access_modes_are_preserved() {
|
||||||
|
let body: &str = "RDONLY 2097152 FLAT \"ro.vmdk\"\n\
|
||||||
|
NOACCESS 1048576 FLAT \"noaccess.vmdk\"\n";
|
||||||
|
|
||||||
|
let (extents, _ddb) = parse_body("# Extent description", body).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(extents.extents.len(), 2);
|
||||||
|
assert_eq!(extents.extents[0].access, "RDONLY");
|
||||||
|
assert_eq!(extents.extents[1].access, "NOACCESS");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_wrong_leading_comment() {
|
||||||
|
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n";
|
||||||
|
// Must be told we are at "# Extent description", anything else errors.
|
||||||
|
parse_body("# The Disk Data Base", body).unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_malformed_extent_line() {
|
||||||
|
// Only three fields -> malformed.
|
||||||
|
let body: &str = "RW 2097152 FLAT\n";
|
||||||
|
parse_body("# Extent description", body).unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_non_numeric_extent_size() {
|
||||||
|
// A non-numeric sector count must be rejected, not coerced to 0.
|
||||||
|
let body: &str = "RW notanumber FLAT \"disk-flat.vmdk\"\n";
|
||||||
|
parse_body("# Extent description", body).unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_non_numeric_extent_offset() {
|
||||||
|
// A non-numeric trailing offset must be rejected too.
|
||||||
|
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\" xyz\n";
|
||||||
|
parse_body("# Extent description", body).unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unexpected_comment_in_body() {
|
||||||
|
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||||
|
# Some other comment\n";
|
||||||
|
parse_body("# Extent description", body).unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skips_blank_line_inside_extent_section() {
|
||||||
|
// qemu-img emits a blank line between the last extent and the
|
||||||
|
// "# The Disk Data Base" marker, it must be skipped, not rejected.
|
||||||
|
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||||
|
\n\
|
||||||
|
# The Disk Data Base\n";
|
||||||
|
let (extents, _ddb) = parse_body("# Extent description", body).unwrap();
|
||||||
|
assert_eq!(extents.extents.len(), 1);
|
||||||
|
assert_eq!(extents.extents[0].filename, "disk-flat.vmdk");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_missing_descriptor_header() {
|
||||||
|
let input: &str = "NOT_A_DESCRIPTOR\nversion=1\n";
|
||||||
|
parse_hdr(input).unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_header_fields() {
|
||||||
|
let input: &str = "# Disk DescriptorFile\n\
|
||||||
|
version=1\n\
|
||||||
|
CID=fffffffe\n\
|
||||||
|
parentCID=ffffffff\n\
|
||||||
|
createType=monolithicFlat\n\
|
||||||
|
# Extent description\n";
|
||||||
|
|
||||||
|
let (header, last) = parse_hdr(input).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(header.version, 1);
|
||||||
|
assert_eq!(header.cid, 0xffff_fffe);
|
||||||
|
assert_eq!(header.parent_cid, 0xffff_ffff);
|
||||||
|
assert!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||||
|
assert_eq!(last, "# Extent description");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_quoted_create_type() {
|
||||||
|
let input: &str = "# Disk DescriptorFile\n\
|
||||||
|
version=1\n\
|
||||||
|
createType=\"twoGbMaxExtentFlat\"\n\
|
||||||
|
# Extent description\n";
|
||||||
|
|
||||||
|
let (header, _last) = parse_hdr(input).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
header.create_type,
|
||||||
|
VMDKDiskType::TwoGbMaxExtentFlat
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_monolithic_flat_descriptor() {
|
||||||
|
let input: &str = "# Disk DescriptorFile\n\
|
||||||
|
version=1\n\
|
||||||
|
createType=monolithicFlat\n\
|
||||||
|
# Extent description\n\
|
||||||
|
RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||||
|
# The Disk Data Base\n\
|
||||||
|
ddb.adapterType = \"ide\"\n";
|
||||||
|
|
||||||
|
let (header, extents, ddb) = parse_full(input).unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||||
|
assert_eq!(extents.extents.len(), 1);
|
||||||
|
assert_eq!(extents.extents[0].access, "RW");
|
||||||
|
assert_eq!(
|
||||||
|
ddb.entries.get("ddb.adapterType").map(String::as_str),
|
||||||
|
Some("\"ide\"")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_two_gb_max_extent_flat_descriptor() {
|
||||||
|
let input: &str = "# Disk DescriptorFile\n\
|
||||||
|
version=1\n\
|
||||||
|
createType=twoGbMaxExtentFlat\n\
|
||||||
|
# Extent description\n\
|
||||||
|
RW 4192256 FLAT \"disk-s001.vmdk\"\n\
|
||||||
|
RW 4192256 FLAT \"disk-s002.vmdk\"\n\
|
||||||
|
# The Disk Data Base\n";
|
||||||
|
|
||||||
|
let (header, extents, _ddb) = parse_full(input).unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
header.create_type,
|
||||||
|
VMDKDiskType::TwoGbMaxExtentFlat
|
||||||
|
));
|
||||||
|
assert_eq!(extents.extents.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_qemu_style_descriptor() {
|
||||||
|
// Mirrors a real qemu-img monolithicFlat descriptor: quoted createType,
|
||||||
|
// blank lines between sections, 5-field extent line with a trailing
|
||||||
|
// offset, and the "#DDB" marker form.
|
||||||
|
let input: &str = "# Disk DescriptorFile\n\
|
||||||
|
version=1\n\
|
||||||
|
CID=eb2295a4\n\
|
||||||
|
parentCID=ffffffff\n\
|
||||||
|
createType=\"monolithicFlat\"\n\
|
||||||
|
\n\
|
||||||
|
# Extent description\n\
|
||||||
|
RW 6291456 FLAT \"t-flat.vmdk\" 0\n\
|
||||||
|
\n\
|
||||||
|
# The Disk Data Base\n\
|
||||||
|
#DDB\n\
|
||||||
|
\n\
|
||||||
|
ddb.virtualHWVersion = \"4\"\n\
|
||||||
|
ddb.adapterType = \"ide\"\n";
|
||||||
|
|
||||||
|
let (header, extents, ddb) = parse_full(input).unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||||
|
assert_eq!(extents.extents.len(), 1);
|
||||||
|
assert_eq!(extents.extents[0].access, "RW");
|
||||||
|
assert_eq!(extents.extents[0].size_in_sectors, 6_291_456);
|
||||||
|
assert_eq!(extents.extents[0].extent_type, "FLAT");
|
||||||
|
assert_eq!(extents.extents[0].filename, "t-flat.vmdk");
|
||||||
|
assert_eq!(
|
||||||
|
ddb.entries.get("ddb.adapterType").map(String::as_str),
|
||||||
|
Some("\"ide\"")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
block/src/formats/vmdk/mod.rs
Normal file
12
block/src/formats/vmdk/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//! Flat VMDK block backend.
|
||||||
|
//!
|
||||||
|
//! Supports the `monolithicFlat` and `twoGbMaxExtentFlat` create types with
|
||||||
|
//! synchronous, extent-aware I/O.
|
||||||
|
|
||||||
|
mod descriptor;
|
||||||
|
|
||||||
|
pub use descriptor::{has_descriptor_header, is_flat_vmdk};
|
||||||
Reference in New Issue
Block a user