block: Implement synchronous I/O engine for Flat VMDK

Implements synchronous I/O engine for flat VMDK backend.
It uses an extent aware worker to map each request
to one or more backing extents. The implementation supports extents
opened with O_DIRECT using AlignedFile.

Async backends of io_uring and AIO are unimplemented because
requests spanning extents cannot be submitted with one fd + offset.

Signed-off-by: Sumedh Alok Sharma <sumsharma@microsoft.com>
This commit is contained in:
Sumedh Alok Sharma
2026-07-13 09:47:13 +00:00
committed by Wei Liu
parent 0769215d42
commit e6fd5fefc4
3 changed files with 673 additions and 13 deletions

View File

@@ -0,0 +1,293 @@
// Copyright © 2026, Microsoft Corporation
//
// SPDX-License-Identifier: Apache-2.0
use std::os::unix::fs::FileExt;
use std::sync::Arc;
use std::{cmp, io};
use vmm_sys_util::eventfd::EventFd;
use crate::AlignedFile;
use crate::async_io::{
AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult, CompletionCommon,
};
use crate::formats::vmdk::flat::{ExtentAccess, VmdkExtent};
/// Synchronous, extent-aware I/O worker for flat VMDK images.
///
/// Maps each guest I/O request to one or more backing extents.
///
/// Async backends (io_uring/AIO) are not supported.
pub(crate) struct FlatVmdkSync {
extents: Arc<Vec<VmdkExtent>>,
size: u64,
completions: CompletionCommon,
}
impl FlatVmdkSync {
pub fn new(extents: Arc<Vec<VmdkExtent>>, size: u64) -> Self {
FlatVmdkSync {
extents,
size,
completions: CompletionCommon::new(),
}
}
// Returns the extent containing virtual `offset`, or `None` if out of range.
fn extent_at(&self, offset: u64) -> Option<&VmdkExtent> {
self.extents
.iter()
.find(|e| offset >= e.virtual_start && offset < e.virtual_start + e.length)
}
fn check_access(&self, start: u64, total: u64, is_read: bool) -> io::Result<()> {
let end = start + total;
let mut cur = start;
while cur < end {
let extent = self.extent_at(cur).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "offset outside any VMDK extent")
})?;
match extent.access {
ExtentAccess::NoAccess => {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("VMDK extent at offset {cur} is NOACCESS; request rejected"),
));
}
ExtentAccess::ReadOnly if !is_read => {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("write to read-only VMDK extent at offset {cur} rejected"),
));
}
_ => {}
}
cur = extent.virtual_start + extent.length;
}
Ok(())
}
// Reads or writes a single contiguous segment of one extent through the
// extent's `AlignedFile`.
fn segment_io(
file: &AlignedFile,
file_offset: u64,
op: &mut AsyncIoOperation,
buf_start: usize,
seg_len: usize,
is_read: bool,
) -> io::Result<usize> {
// O_DIRECT unaligned
if file.alignment() != 0 {
return if is_read {
file.read_unaligned(file_offset, seg_len, |data| {
op.write_bytes_at(buf_start, data)
})
} else {
file.write_unaligned(file_offset, seg_len, |data| {
op.read_bytes_at(buf_start, data)
})
};
}
// Aligned & Buffered
let mut buf = vec![0u8; seg_len];
let mut done = 0usize;
if is_read {
while done < seg_len {
match file.read_at(&mut buf[done..], file_offset + done as u64) {
Ok(0) => break, // EOF: nothing more to read
Ok(n) => done += n,
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
op.write_bytes_at(buf_start, &buf[..done])?;
Ok(done)
} else {
op.read_bytes_at(buf_start, &mut buf)?;
while done < seg_len {
match file.write_at(&buf[done..], file_offset + done as u64) {
Ok(0) => break, // no progress: avoid spinning forever
Ok(n) => done += n,
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(done)
}
}
// Single-extent path: the whole request lives in `extent`.
//
// The guest iovecs are handed to `AlignedFile::{read,write}_vectored_at`
fn single_extent_io(
&self,
extent: &VmdkExtent,
op: &mut AsyncIoOperation,
) -> io::Result<usize> {
let file = extent.file.as_ref().ok_or_else(|| {
io::Error::new(
io::ErrorKind::PermissionDenied,
"VMDK extent is not accessible",
)
})?;
let file_offset = extent.file_base_offset + (op.offset() as u64 - extent.virtual_start);
let iovecs = op.iovecs();
// SAFETY: the iovec buffers are owned by `op` and remain valid for the
// duration of this call.
unsafe {
if op.is_read() {
file.read_vectored_at(iovecs, file_offset)
} else {
file.write_vectored_at(iovecs, file_offset)
}
}
}
// Slow path: the request straddles >= 2 extents.
//
// A single guest request here maps onto several different backing files,
// Every segment goes through `segment_io` regardless of
// alignment.
fn spanning_io(&self, op: &mut AsyncIoOperation) -> io::Result<usize> {
let start = op.offset() as u64;
let total = op.total_len() as u64;
let is_read = op.is_read();
let mut done: u64 = 0;
while done < total {
let cur = start + done;
let extent = self.extent_at(cur).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "offset outside any VMDK extent")
})?;
let extent_end = extent.virtual_start + extent.length;
// Bytes handled in this extent before reaching its boundary.
let seg_len = cmp::min(total - done, extent_end - cur) as usize;
let file = extent.file.as_ref().ok_or_else(|| {
io::Error::new(
io::ErrorKind::PermissionDenied,
"VMDK extent is not accessible",
)
})?;
let file_offset = extent.file_base_offset + (cur - extent.virtual_start);
let n = Self::segment_io(file, file_offset, op, done as usize, seg_len, is_read)?;
done += n as u64;
if n < seg_len {
break; // short read/write
}
}
Ok(done as usize)
}
}
impl AsyncIo for FlatVmdkSync {
fn notifier(&self) -> &EventFd {
self.completions.notifier()
}
fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> {
let start = op.offset() as u64;
let total = op.total_len() as u64;
let is_read = op.is_read();
// Bounds check against the virtual disk size (overflow-safe: `start`
// is checked before subtracting it from `size`).
if start > self.size || total > self.size - start {
let error = io::Error::new(
io::ErrorKind::InvalidData,
format!(
"VMDK request [{start}, {}) exceeds virtual size {}",
start + total,
self.size
),
);
return Err(if is_read {
AsyncIoError::ReadVectored(error)
} else {
AsyncIoError::WriteVectored(error)
});
}
// Reject the request up front if any extent it touches forbids it:
// NOACCESS extents reject all I/O.
if total != 0
&& let Err(error) = self.check_access(start, total, is_read)
{
return Err(if is_read {
AsyncIoError::ReadVectored(error)
} else {
AsyncIoError::WriteVectored(error)
});
}
let result = if total == 0 {
Ok(0)
} else if let Some(extent) = self.extent_at(start) {
if start + total <= extent.virtual_start + extent.length {
// Entire request fits in one extent
self.single_extent_io(extent, &mut op)
} else {
// Request crosses an extent boundary
self.spanning_io(&mut op)
}
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"offset outside any VMDK extent",
))
};
let bytes = result.map_err(|e| {
if is_read {
AsyncIoError::ReadVectored(e)
} else {
AsyncIoError::WriteVectored(e)
}
})?;
self.completions
.complete(AsyncIoCompletion::from_operation(op, bytes as i32));
Ok(())
}
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
// Flush every extent: a single guest flush must durably persist data
// that may have been written across multiple extent files.
for extent in self.extents.iter() {
// Skip NoAccess extents, which have no open file.
if let Some(file) = extent.file.as_ref() {
file.sync_all().map_err(AsyncIoError::Fsync)?;
}
}
if let Some(user_data) = user_data {
self.completions
.complete(AsyncIoCompletion::new(user_data, 0, None));
}
Ok(())
}
fn next_completed_request(&mut self) -> Option<AsyncIoCompletion> {
self.completions.next_completed()
}
fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
// Flat VMDK is not sparse-capable (see `SparseCapable` impl), so this
// should never be negotiated by the guest.
Err(AsyncIoError::PunchHole(io::Error::other(
"punch_hole not supported for flat VMDK",
)))
}
fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> {
Err(AsyncIoError::WriteZeroes(io::Error::other(
"write_zeroes not supported for flat VMDK",
)))
}
}

View File

@@ -5,8 +5,6 @@
//! Flat VMDK extent layout: opens the data extents referenced by the //! Flat VMDK extent layout: opens the data extents referenced by the
//! descriptor and maps the virtual disk onto them. //! descriptor and maps the virtual disk onto them.
#![allow(dead_code)]
use std::ffi::{CString, OsStr}; use std::ffi::{CString, OsStr};
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io; use std::io;
@@ -19,7 +17,7 @@ use std::sync::Arc;
use log::warn; use log::warn;
use crate::formats::vmdk::descriptor::VmdkDescriptor; use crate::formats::vmdk::descriptor::VmdkDescriptor;
use crate::{AlignedFile, DiskTopology}; use crate::{AlignedFile, DiskTopology, query_device_size};
const VMDK_SECTOR_SIZE: u64 = 512; const VMDK_SECTOR_SIZE: u64 = 512;
@@ -346,18 +344,16 @@ impl FlatVmdk {
Arc::clone(&self.extents) Arc::clone(&self.extents)
} }
/// Host allocation size: the sum of every opened extent file's size. /// Host allocation size: the sum of every opened extent's actually
/// `NoAccess` extents contribute 0 to the total. /// allocated storage (`st_blocks * 512` for regular files, device size for
/// block devices), so sparse extents are reported correctly. `NoAccess`
/// extents (no open file) contribute 0, as does any extent whose size
/// cannot be queried.
pub fn physical_block_size(&self) -> u64 { pub fn physical_block_size(&self) -> u64 {
self.extents self.extents
.iter() .iter()
.map(|extent| { .filter_map(|extent| extent.file.as_ref())
extent .map(|f| query_device_size(f.file()).map_or(0, |(_, physical)| physical))
.file
.as_ref()
.and_then(|f| f.metadata().ok())
.map_or(0, |m| m.len())
})
.sum() .sum()
} }

View File

@@ -1,4 +1,4 @@
// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. // Copyright © 2026, Microsoft Corporation
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
@@ -8,6 +8,377 @@
//! synchronous, extent-aware I/O. //! synchronous, extent-aware I/O.
mod descriptor; mod descriptor;
mod engine_sync;
mod flat; mod flat;
use std::fs::File;
use std::io;
use std::os::unix::io::AsRawFd;
use std::path::Path;
pub use descriptor::{has_descriptor_header, is_flat_vmdk}; pub use descriptor::{has_descriptor_header, is_flat_vmdk};
use self::engine_sync::FlatVmdkSync;
use self::flat::FlatVmdk;
use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError};
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
use crate::{DiskTopology, disk_file};
#[derive(Debug)]
pub struct VmdkDisk {
inner: FlatVmdk,
}
impl VmdkDisk {
/// Builds a Flat VMDK disk backend.
pub fn new(file: File, path: &Path, direct: bool) -> Result<Self, BlockError> {
let inner = FlatVmdk::new(file, path, direct)?;
Ok(VmdkDisk { inner })
}
}
impl disk_file::DiskSize for VmdkDisk {
fn logical_size(&self) -> BlockResult<u64> {
Ok(self.inner.virtual_block_size())
}
}
impl disk_file::PhysicalSize for VmdkDisk {
fn physical_size(&self) -> BlockResult<u64> {
Ok(self.inner.physical_block_size())
}
}
// Expose the descriptor file's fd for advisory image locking.
impl disk_file::DiskFd for VmdkDisk {
fn fd(&self) -> BorrowedDiskFd<'_> {
BorrowedDiskFd::new(self.inner.as_raw_fd())
}
}
impl disk_file::Geometry for VmdkDisk {
fn topology(&self) -> DiskTopology {
self.inner.topology()
}
}
impl disk_file::SparseCapable for VmdkDisk {}
// Flat VMDK keeps no in-memory format metadata, so no-op.
impl disk_file::MetadataSync for VmdkDisk {}
impl disk_file::Resizable for VmdkDisk {
fn resize(&mut self, _size: u64) -> BlockResult<()> {
Err(BlockError::new(
BlockErrorKind::UnsupportedFeature,
DiskFileError::ResizeError(io::Error::other("resize not supported for flat VMDK")),
)
.with_op(ErrorOp::Resize))
}
}
impl disk_file::DiskFile for VmdkDisk {}
impl disk_file::AsyncDiskFile for VmdkDisk {
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
Ok(Box::new(VmdkDisk {
inner: self.inner.clone(),
}))
}
fn create_async_io(&self, ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
// VMDK provides a synchronous, extent-aware worker, so the io_uring ring
// depth is unused here.
let _ = ring_depth;
Ok(Box::new(FlatVmdkSync::new(
self.inner.extents(),
self.inner.virtual_block_size(),
)))
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use vmm_sys_util::tempdir::TempDir;
use super::*;
use crate::disk_file::{AsyncDiskFile, DiskFd, DiskSize, PhysicalSize, Resizable};
const SECTOR: u64 = 512;
// Builds a flat VMDK in `dir`: a descriptor plus one backing data file per
// extent. When `allocate` is true the extent files are filled with real
// blocks (fixed / pre-allocated layout used in practice), otherwise they
// are created sparse via `set_len` (declared length but no allocated
// blocks). `extents` entries are (filename, access, sectors). Returns the
// descriptor path.
fn build_flat_vmdk(
dir: &Path,
create_type: &str,
extents: &[(&str, &str, u64)],
allocate: bool,
) -> PathBuf {
let mut desc = String::from("# Disk DescriptorFile\n");
desc.push_str("version=1\n");
desc.push_str("CID=fffffffe\n");
desc.push_str("parentCID=ffffffff\n");
desc.push_str(&format!("createType={create_type}\n"));
desc.push_str("# Extent description\n");
for (filename, access, sectors) in extents {
let mut data = File::create(dir.join(filename)).unwrap();
if allocate {
data.write_all(&vec![0u8; (sectors * SECTOR) as usize])
.unwrap();
} else {
data.set_len(sectors * SECTOR).unwrap();
}
data.sync_all().unwrap();
desc.push_str(&format!("{access} {sectors} FLAT \"{filename}\"\n"));
}
desc.push_str("# The Disk Data Base\n");
desc.push_str("ddb.adapterType = \"ide\"\n");
let desc_path = dir.join("disk.vmdk");
let mut df = File::create(&desc_path).unwrap();
df.write_all(desc.as_bytes()).unwrap();
df.sync_all().unwrap();
desc_path
}
// Sparse extents.
fn write_flat_vmdk(dir: &Path, create_type: &str, extents: &[(&str, &str, u64)]) -> PathBuf {
build_flat_vmdk(dir, create_type, extents, false)
}
// Fully pre-allocated extents.
fn write_flat_vmdk_allocated(
dir: &Path,
create_type: &str,
extents: &[(&str, &str, u64)],
) -> PathBuf {
build_flat_vmdk(dir, create_type, extents, true)
}
fn open_descriptor(path: &Path) -> File {
File::open(path).unwrap()
}
// Writes a descriptor referencing `extent_lines` verbatim (no backing data
// files are created). Used to exercise `FlatVmdk::new`'s per-extent
// validation, whose zero-size/overflow checks all run before an extent file
// would be opened.
fn write_descriptor(dir: &Path, create_type: &str, extent_lines: &[&str]) -> PathBuf {
let mut desc = String::from("# Disk DescriptorFile\n");
desc.push_str("version=1\n");
desc.push_str("CID=fffffffe\n");
desc.push_str("parentCID=ffffffff\n");
desc.push_str(&format!("createType={create_type}\n"));
desc.push_str("# Extent description\n");
for line in extent_lines {
desc.push_str(line);
desc.push('\n');
}
desc.push_str("# The Disk Data Base\n");
desc.push_str("ddb.adapterType = \"ide\"\n");
let desc_path = dir.join("disk.vmdk");
let mut df = File::create(&desc_path).unwrap();
df.write_all(desc.as_bytes()).unwrap();
df.sync_all().unwrap();
desc_path
}
#[test]
fn logical_and_physical_size_single_extent() {
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
let path = write_flat_vmdk(
dir.as_path(),
"monolithicFlat",
&[("disk-flat.vmdk", "RW", 2048)],
);
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
assert_eq!(disk.logical_size().unwrap(), 2048 * SECTOR);
// The extent is created sparse (`set_len`), so no blocks are allocated
// and the `st_blocks`-based physical size is 0.
assert_eq!(disk.physical_size().unwrap(), 0);
}
#[test]
fn logical_size_sums_multiple_extents() {
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
let path = write_flat_vmdk(
dir.as_path(),
"twoGbMaxExtentFlat",
&[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 1024)],
);
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
assert_eq!(disk.logical_size().unwrap(), (2048 + 1024) * SECTOR);
// Sparse extents: no blocks are allocated, so physical size is 0.
assert_eq!(disk.physical_size().unwrap(), 0);
}
#[test]
fn physical_size_matches_fully_allocated_extents() {
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
let path = write_flat_vmdk_allocated(
dir.as_path(),
"twoGbMaxExtentFlat",
&[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 1024)],
);
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
// Fully pre-allocated extents: host allocation (st_blocks) equals the
// declared logical size.
assert_eq!(disk.logical_size().unwrap(), (2048 + 1024) * SECTOR);
assert_eq!(disk.physical_size().unwrap(), (2048 + 1024) * SECTOR);
}
#[test]
fn fd_exposes_descriptor_file() {
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
let path = write_flat_vmdk(
dir.as_path(),
"monolithicFlat",
&[("disk-flat.vmdk", "RW", 64)],
);
let file = open_descriptor(&path);
let expected = file.as_raw_fd();
let disk = VmdkDisk::new(file, &path, false).unwrap();
assert_eq!(disk.fd().as_raw_fd(), expected);
}
#[test]
fn resize_is_unsupported() {
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
let path = write_flat_vmdk(
dir.as_path(),
"monolithicFlat",
&[("disk-flat.vmdk", "RW", 64)],
);
let mut disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
let err = disk.resize(4096).unwrap_err();
assert_eq!(err.kind(), BlockErrorKind::UnsupportedFeature);
}
#[test]
fn try_clone_preserves_size() {
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
let path = write_flat_vmdk(
dir.as_path(),
"monolithicFlat",
&[("disk-flat.vmdk", "RW", 2048)],
);
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
let cloned = disk.try_clone().unwrap();
assert_eq!(cloned.logical_size().unwrap(), disk.logical_size().unwrap());
}
#[test]
fn create_async_io_builds_worker() {
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
let path = write_flat_vmdk(
dir.as_path(),
"monolithicFlat",
&[("disk-flat.vmdk", "RW", 2048)],
);
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
// Ring depth is ignored by the synchronous VMDK worker.
disk.create_async_io(0).unwrap();
}
#[test]
fn create_async_io_supports_multi_extent() {
let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap();
let path = write_flat_vmdk(
dir.as_path(),
"twoGbMaxExtentFlat",
&[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 2048)],
);
let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap();
disk.create_async_io(32).unwrap();
}
#[test]
fn new_rejects_zero_sector_extent() {
let dir = TempDir::new_with_prefix("/tmp/vmdk-zero-test").unwrap();
let path = write_descriptor(
dir.as_path(),
"monolithicFlat",
&["RW 0 FLAT \"disk-flat.vmdk\""],
);
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn new_rejects_extent_size_overflow() {
// size_in_sectors * 512 must fit in a u64.
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-size-test").unwrap();
let line = format!("RW {} FLAT \"disk-flat.vmdk\"", u64::MAX);
let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]);
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn new_rejects_extent_file_offset_overflow() {
// The offset * 512 must fit in a u64.
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-offset-test").unwrap();
let line = format!("RW 1 FLAT \"disk-flat.vmdk\" {}", u64::MAX);
let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]);
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn new_rejects_extent_file_range_overflow() {
// Each of offset*512 and size*512 fits in a u64, but their sum (the last
// byte the extent addresses in its backing file) overflows.
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-range-test").unwrap();
// offset_in_sectors = floor(u64::MAX / 512) => offset bytes = u64::MAX -
// 511, size 1 sector (512 bytes) pushes the end one byte past u64::MAX.
let offset = u64::MAX / 512;
let line = format!("RW 1 FLAT \"disk-flat.vmdk\" {offset}");
let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]);
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn new_rejects_total_virtual_size_overflow() {
// Two extents whose individual lengths fit in a u64 but whose running
// sum (the total virtual disk size) overflows. size = 2^55 - 1 =>
// length = 2^64 - 512, two of them overflow the total.
let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-total-test").unwrap();
let size = (1u64 << 55) - 1;
let l1 = format!("NOACCESS {size} FLAT \"s001.vmdk\"");
let l2 = format!("NOACCESS {size} FLAT \"s002.vmdk\"");
let path = write_descriptor(
dir.as_path(),
"twoGbMaxExtentFlat",
&[l1.as_str(), l2.as_str()],
);
let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
}