mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
Switch parse_qcow and BackingFile::new from qcow::Result to BlockResult. Every early return site now produces an explicit BlockError with the appropriate kind. Remaining internal calls to functions still on qcow::Result rely on the From scaffolding and will be converted in subsequent commits. Two helpers are added to BlockError. with_kind replaces the classification on an existing error, used in QcowDiskSync::new to avoid double wrapping when the caller needs a different kind. into_source consumes the error and returns the boxed source, used at the recursive BackingFile open to extract the qcow::Error for BackingFileOpen without letting qcow::Error hold a BlockError. The qcow_sync boundary is simplified to a single closure that operates on the BlockError already returned by parse_qcow. Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
1748 lines
63 KiB
Rust
1748 lines
63 KiB
Rust
// Copyright © 2021 Intel Corporation
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
|
|
|
|
use std::cmp::min;
|
|
use std::collections::VecDeque;
|
|
use std::fs::File;
|
|
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
|
|
use std::sync::Arc;
|
|
use std::{fmt, io, ptr, slice};
|
|
|
|
use vmm_sys_util::eventfd::EventFd;
|
|
use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt};
|
|
|
|
use crate::async_io::{AsyncIo, AsyncIoError, AsyncIoResult, BorrowedDiskFd, DiskFileError};
|
|
use crate::disk_file;
|
|
use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp};
|
|
use crate::qcow::metadata::{
|
|
BackingRead, ClusterReadMapping, ClusterWriteMapping, DeallocAction, QcowMetadata,
|
|
};
|
|
use crate::qcow::qcow_raw_file::QcowRawFile;
|
|
use crate::qcow::{
|
|
BackingFile, BackingKind, Error as QcowError, MAX_NESTING_DEPTH, RawFile, parse_qcow,
|
|
};
|
|
|
|
/// Raw backing file using pread64 on a duplicated fd.
|
|
struct RawBacking {
|
|
fd: OwnedFd,
|
|
virtual_size: u64,
|
|
}
|
|
|
|
// SAFETY: The only I/O operation is pread64 which is position independent
|
|
// and safe for concurrent use from multiple threads.
|
|
unsafe impl Sync for RawBacking {}
|
|
|
|
impl BackingRead for RawBacking {
|
|
fn read_at(&self, address: u64, buf: &mut [u8]) -> io::Result<()> {
|
|
if address >= self.virtual_size {
|
|
buf.fill(0);
|
|
return Ok(());
|
|
}
|
|
let available = (self.virtual_size - address) as usize;
|
|
if available >= buf.len() {
|
|
pread_exact(self.fd.as_raw_fd(), buf, address)
|
|
} else {
|
|
pread_exact(self.fd.as_raw_fd(), &mut buf[..available], address)?;
|
|
buf[available..].fill(0);
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// QCOW2 backing file with RwLock metadata and pread64 data reads.
|
|
///
|
|
/// Read only because backing files never receive writes. Nested backing
|
|
/// files are handled recursively.
|
|
struct Qcow2MetadataBacking {
|
|
metadata: Arc<QcowMetadata>,
|
|
data_fd: OwnedFd,
|
|
backing_file: Option<Arc<dyn BackingRead>>,
|
|
}
|
|
|
|
// SAFETY: All reads go through QcowMetadata which uses RwLock
|
|
// and pread64 which is position independent and thread safe.
|
|
unsafe impl Sync for Qcow2MetadataBacking {}
|
|
|
|
impl BackingRead for Qcow2MetadataBacking {
|
|
fn read_at(&self, address: u64, buf: &mut [u8]) -> io::Result<()> {
|
|
let virtual_size = self.metadata.virtual_size();
|
|
if address >= virtual_size {
|
|
buf.fill(0);
|
|
return Ok(());
|
|
}
|
|
let available = (virtual_size - address) as usize;
|
|
if available < buf.len() {
|
|
self.read_clusters(address, &mut buf[..available])?;
|
|
buf[available..].fill(0);
|
|
return Ok(());
|
|
}
|
|
self.read_clusters(address, buf)
|
|
}
|
|
}
|
|
|
|
impl Qcow2MetadataBacking {
|
|
/// Resolve cluster mappings via metadata then read allocated clusters
|
|
/// with pread64.
|
|
fn read_clusters(&self, address: u64, buf: &mut [u8]) -> io::Result<()> {
|
|
let total_len = buf.len();
|
|
let has_backing = self.backing_file.is_some();
|
|
|
|
let mappings = self
|
|
.metadata
|
|
.map_clusters_for_read(address, total_len, has_backing)?;
|
|
|
|
let mut buf_offset = 0usize;
|
|
for mapping in mappings {
|
|
match mapping {
|
|
ClusterReadMapping::Zero { length } => {
|
|
buf[buf_offset..buf_offset + length as usize].fill(0);
|
|
buf_offset += length as usize;
|
|
}
|
|
ClusterReadMapping::Allocated {
|
|
offset: host_offset,
|
|
length,
|
|
} => {
|
|
pread_exact(
|
|
self.data_fd.as_raw_fd(),
|
|
&mut buf[buf_offset..buf_offset + length as usize],
|
|
host_offset,
|
|
)?;
|
|
buf_offset += length as usize;
|
|
}
|
|
ClusterReadMapping::Compressed { data } => {
|
|
let len = data.len();
|
|
buf[buf_offset..buf_offset + len].copy_from_slice(&data);
|
|
buf_offset += len;
|
|
}
|
|
ClusterReadMapping::Backing {
|
|
offset: backing_offset,
|
|
length,
|
|
} => {
|
|
self.backing_file.as_ref().unwrap().read_at(
|
|
backing_offset,
|
|
&mut buf[buf_offset..buf_offset + length as usize],
|
|
)?;
|
|
buf_offset += length as usize;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Drop for Qcow2MetadataBacking {
|
|
fn drop(&mut self) {
|
|
self.metadata.shutdown();
|
|
}
|
|
}
|
|
|
|
/// Construct a thread safe backing file reader.
|
|
fn shared_backing_from(bf: BackingFile) -> BlockResult<Arc<dyn BackingRead>> {
|
|
let (kind, virtual_size) = bf.into_kind();
|
|
|
|
let dup_fd = |fd: BorrowedFd<'_>| -> BlockResult<OwnedFd> {
|
|
fd.try_clone_to_owned().map_err(|e| {
|
|
BlockError::new(
|
|
BlockErrorKind::Io,
|
|
QcowError::BackingFileIo(String::new(), e),
|
|
)
|
|
.with_op(ErrorOp::DupBackingFd)
|
|
})
|
|
};
|
|
|
|
match kind {
|
|
BackingKind::Raw(raw_file) => {
|
|
let fd = dup_fd(raw_file.as_fd())?;
|
|
Ok(Arc::new(RawBacking { fd, virtual_size }))
|
|
}
|
|
BackingKind::Qcow { inner, backing } => {
|
|
let data_fd = dup_fd(inner.raw_file.as_fd())?;
|
|
Ok(Arc::new(Qcow2MetadataBacking {
|
|
metadata: Arc::new(QcowMetadata::new(*inner)),
|
|
data_fd,
|
|
backing_file: backing.map(|bf| shared_backing_from(*bf)).transpose()?,
|
|
}))
|
|
}
|
|
#[cfg(test)]
|
|
BackingKind::QcowFile(_) => {
|
|
unreachable!("QcowFile variant is only used by set_backing_file() in tests")
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct QcowDiskSync {
|
|
metadata: Arc<QcowMetadata>,
|
|
/// Shared across queues, resolved once at construction.
|
|
backing_file: Option<Arc<dyn BackingRead>>,
|
|
sparse: bool,
|
|
data_raw_file: QcowRawFile,
|
|
}
|
|
|
|
impl fmt::Debug for QcowDiskSync {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("QcowDiskSync")
|
|
.field("sparse", &self.sparse)
|
|
.field("has_backing", &self.backing_file.is_some())
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl QcowDiskSync {
|
|
pub fn new(
|
|
file: File,
|
|
direct_io: bool,
|
|
backing_files: bool,
|
|
sparse: bool,
|
|
) -> BlockResult<Self> {
|
|
let max_nesting_depth = if backing_files { MAX_NESTING_DEPTH } else { 0 };
|
|
let (inner, backing_file, sparse) =
|
|
parse_qcow(RawFile::new(file, direct_io), max_nesting_depth, sparse).map_err(|e| {
|
|
let e = if !backing_files && matches!(e.kind(), BlockErrorKind::Overflow) {
|
|
e.with_kind(BlockErrorKind::UnsupportedFeature)
|
|
} else {
|
|
e
|
|
};
|
|
e.with_op(ErrorOp::Open)
|
|
})?;
|
|
let data_raw_file = inner.raw_file.clone();
|
|
Ok(QcowDiskSync {
|
|
metadata: Arc::new(QcowMetadata::new(inner)),
|
|
backing_file: backing_file.map(shared_backing_from).transpose()?,
|
|
sparse,
|
|
data_raw_file,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Drop for QcowDiskSync {
|
|
fn drop(&mut self) {
|
|
self.metadata.shutdown();
|
|
}
|
|
}
|
|
|
|
impl disk_file::DiskSize for QcowDiskSync {
|
|
fn logical_size(&self) -> BlockResult<u64> {
|
|
Ok(self.metadata.virtual_size())
|
|
}
|
|
}
|
|
|
|
impl disk_file::PhysicalSize for QcowDiskSync {
|
|
fn physical_size(&self) -> BlockResult<u64> {
|
|
Ok(self.data_raw_file.physical_size()?)
|
|
}
|
|
}
|
|
|
|
impl disk_file::DiskFd for QcowDiskSync {
|
|
fn fd(&self) -> BorrowedDiskFd<'_> {
|
|
BorrowedDiskFd::new(self.data_raw_file.as_fd().as_raw_fd())
|
|
}
|
|
}
|
|
|
|
impl disk_file::Geometry for QcowDiskSync {}
|
|
|
|
impl disk_file::SparseCapable for QcowDiskSync {
|
|
fn supports_sparse_operations(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn supports_zero_flag(&self) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl disk_file::Resizable for QcowDiskSync {
|
|
fn resize(&mut self, size: u64) -> BlockResult<()> {
|
|
if self.backing_file.is_some() {
|
|
return Err(BlockError::new(
|
|
BlockErrorKind::UnsupportedFeature,
|
|
DiskFileError::ResizeError(io::Error::other(
|
|
"resize not supported with backing file",
|
|
)),
|
|
)
|
|
.with_op(ErrorOp::Resize));
|
|
}
|
|
self.metadata.resize(size).map_err(|e| {
|
|
BlockError::new(BlockErrorKind::Io, DiskFileError::ResizeError(e))
|
|
.with_op(ErrorOp::Resize)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl disk_file::DiskFile for QcowDiskSync {}
|
|
|
|
impl disk_file::AsyncDiskFile for QcowDiskSync {
|
|
fn try_clone(&self) -> BlockResult<Box<dyn disk_file::AsyncDiskFile>> {
|
|
Ok(Box::new(QcowDiskSync {
|
|
metadata: Arc::clone(&self.metadata),
|
|
backing_file: self.backing_file.as_ref().map(Arc::clone),
|
|
sparse: self.sparse,
|
|
data_raw_file: self.data_raw_file.clone(),
|
|
}))
|
|
}
|
|
|
|
// ring_depth is unused - this sync backend performs blocking I/O
|
|
// instead of submitting to an async ring.
|
|
fn new_async_io(&self, _ring_depth: u32) -> BlockResult<Box<dyn AsyncIo>> {
|
|
Ok(Box::new(QcowSync::new(
|
|
Arc::clone(&self.metadata),
|
|
self.data_raw_file.clone(),
|
|
self.backing_file.as_ref().map(Arc::clone),
|
|
self.sparse,
|
|
)))
|
|
}
|
|
}
|
|
|
|
pub struct QcowSync {
|
|
metadata: Arc<QcowMetadata>,
|
|
data_file: QcowRawFile,
|
|
/// See the backing_file field on QcowDiskSync.
|
|
backing_file: Option<Arc<dyn BackingRead>>,
|
|
sparse: bool,
|
|
eventfd: EventFd,
|
|
completion_list: VecDeque<(u64, i32)>,
|
|
}
|
|
|
|
impl QcowSync {
|
|
fn new(
|
|
metadata: Arc<QcowMetadata>,
|
|
data_file: QcowRawFile,
|
|
backing_file: Option<Arc<dyn BackingRead>>,
|
|
sparse: bool,
|
|
) -> Self {
|
|
QcowSync {
|
|
metadata,
|
|
data_file,
|
|
backing_file,
|
|
sparse,
|
|
eventfd: EventFd::new(libc::EFD_NONBLOCK)
|
|
.expect("Failed creating EventFd for QcowSync"),
|
|
completion_list: VecDeque::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- Position independent I/O helpers --
|
|
//
|
|
// Duplicated file descriptors share the kernel file description and thus the
|
|
// file position. Using seek then read from multiple queues races on that
|
|
// shared position. pread64 and pwrite64 are atomic and never touch the position.
|
|
|
|
/// Read exactly the requested bytes at offset, looping on short reads.
|
|
fn pread_exact(fd: RawFd, buf: &mut [u8], offset: u64) -> io::Result<()> {
|
|
let mut total = 0usize;
|
|
while total < buf.len() {
|
|
// SAFETY: buf and fd are valid for the lifetime of the call.
|
|
let ret = unsafe {
|
|
libc::pread64(
|
|
fd,
|
|
buf[total..].as_mut_ptr() as *mut libc::c_void,
|
|
buf.len() - total,
|
|
(offset + total as u64) as libc::off_t,
|
|
)
|
|
};
|
|
if ret < 0 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
if ret == 0 {
|
|
return Err(io::Error::from(io::ErrorKind::UnexpectedEof));
|
|
}
|
|
total += ret as usize;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Write all bytes to fd at offset, looping on short writes.
|
|
fn pwrite_all(fd: RawFd, buf: &[u8], offset: u64) -> io::Result<()> {
|
|
let mut total = 0usize;
|
|
while total < buf.len() {
|
|
// SAFETY: buf and fd are valid for the lifetime of the call.
|
|
let ret = unsafe {
|
|
libc::pwrite64(
|
|
fd,
|
|
buf[total..].as_ptr() as *const libc::c_void,
|
|
buf.len() - total,
|
|
(offset + total as u64) as libc::off_t,
|
|
)
|
|
};
|
|
if ret < 0 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
if ret == 0 {
|
|
return Err(io::Error::other("pwrite64 wrote 0 bytes"));
|
|
}
|
|
total += ret as usize;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// -- iovec helper functions --
|
|
//
|
|
// Operate on the iovec array as a flat byte stream.
|
|
|
|
/// Copy data into iovecs starting at the given byte offset.
|
|
///
|
|
/// # Safety
|
|
/// Caller must ensure iovecs point to valid, writable memory of sufficient size.
|
|
unsafe fn scatter_to_iovecs(iovecs: &[libc::iovec], start: usize, data: &[u8]) {
|
|
let mut remaining = data;
|
|
let mut pos = 0usize;
|
|
for iov in iovecs {
|
|
let iov_end = pos + iov.iov_len;
|
|
if iov_end <= start || remaining.is_empty() {
|
|
pos = iov_end;
|
|
continue;
|
|
}
|
|
let iov_start = start.saturating_sub(pos);
|
|
let available = iov.iov_len - iov_start;
|
|
let count = min(available, remaining.len());
|
|
// SAFETY: iov_base is valid for iov_len bytes per caller contract.
|
|
unsafe {
|
|
let dst = (iov.iov_base as *mut u8).add(iov_start);
|
|
ptr::copy_nonoverlapping(remaining.as_ptr(), dst, count);
|
|
}
|
|
remaining = &remaining[count..];
|
|
if remaining.is_empty() {
|
|
break;
|
|
}
|
|
pos = iov_end;
|
|
}
|
|
}
|
|
|
|
/// Zero fill iovecs starting at the given byte offset for the given length.
|
|
///
|
|
/// # Safety
|
|
/// Caller must ensure iovecs point to valid, writable memory of sufficient size.
|
|
unsafe fn zero_fill_iovecs(iovecs: &[libc::iovec], start: usize, len: usize) {
|
|
let mut remaining = len;
|
|
let mut pos = 0usize;
|
|
for iov in iovecs {
|
|
let iov_end = pos + iov.iov_len;
|
|
if iov_end <= start || remaining == 0 {
|
|
pos = iov_end;
|
|
continue;
|
|
}
|
|
let iov_start = start.saturating_sub(pos);
|
|
let available = iov.iov_len - iov_start;
|
|
let count = min(available, remaining);
|
|
// SAFETY: iov_base is valid for iov_len bytes per caller contract.
|
|
unsafe {
|
|
let dst = (iov.iov_base as *mut u8).add(iov_start);
|
|
ptr::write_bytes(dst, 0, count);
|
|
}
|
|
remaining -= count;
|
|
if remaining == 0 {
|
|
break;
|
|
}
|
|
pos = iov_end;
|
|
}
|
|
}
|
|
|
|
/// Gather bytes from iovecs starting at the given byte offset into a Vec.
|
|
///
|
|
/// # Safety
|
|
/// Caller must ensure iovecs point to valid, readable memory of sufficient size.
|
|
unsafe fn gather_from_iovecs(iovecs: &[libc::iovec], start: usize, len: usize) -> Vec<u8> {
|
|
let mut result = Vec::with_capacity(len);
|
|
let mut remaining = len;
|
|
let mut pos = 0usize;
|
|
for iov in iovecs {
|
|
let iov_end = pos + iov.iov_len;
|
|
if iov_end <= start || remaining == 0 {
|
|
pos = iov_end;
|
|
continue;
|
|
}
|
|
let iov_start = start.saturating_sub(pos);
|
|
let available = iov.iov_len - iov_start;
|
|
let count = min(available, remaining);
|
|
// SAFETY: iov_base is valid for iov_len bytes per caller contract.
|
|
unsafe {
|
|
let src = (iov.iov_base as *const u8).add(iov_start);
|
|
result.extend_from_slice(slice::from_raw_parts(src, count));
|
|
}
|
|
remaining -= count;
|
|
if remaining == 0 {
|
|
break;
|
|
}
|
|
pos = iov_end;
|
|
}
|
|
result
|
|
}
|
|
|
|
impl AsyncIo for QcowSync {
|
|
fn notifier(&self) -> &EventFd {
|
|
&self.eventfd
|
|
}
|
|
|
|
fn read_vectored(
|
|
&mut self,
|
|
offset: libc::off_t,
|
|
iovecs: &[libc::iovec],
|
|
user_data: u64,
|
|
) -> AsyncIoResult<()> {
|
|
let address = offset as u64;
|
|
let total_len: usize = iovecs.iter().map(|v| v.iov_len).sum();
|
|
|
|
let has_backing = self.backing_file.is_some();
|
|
let mappings = self
|
|
.metadata
|
|
.map_clusters_for_read(address, total_len, has_backing)
|
|
.map_err(AsyncIoError::ReadVectored)?;
|
|
|
|
let mut buf_offset = 0usize;
|
|
for mapping in mappings {
|
|
match mapping {
|
|
ClusterReadMapping::Zero { length } => {
|
|
// SAFETY: iovecs point to valid guest memory buffers
|
|
unsafe { zero_fill_iovecs(iovecs, buf_offset, length as usize) };
|
|
buf_offset += length as usize;
|
|
}
|
|
ClusterReadMapping::Allocated {
|
|
offset: host_offset,
|
|
length,
|
|
} => {
|
|
let mut buf = vec![0u8; length as usize];
|
|
pread_exact(self.data_file.as_raw_fd(), &mut buf, host_offset)
|
|
.map_err(AsyncIoError::ReadVectored)?;
|
|
// SAFETY: iovecs point to valid guest memory buffers
|
|
unsafe { scatter_to_iovecs(iovecs, buf_offset, &buf) };
|
|
buf_offset += length as usize;
|
|
}
|
|
ClusterReadMapping::Compressed { data } => {
|
|
let len = data.len();
|
|
// SAFETY: iovecs point to valid guest memory buffers
|
|
unsafe { scatter_to_iovecs(iovecs, buf_offset, &data) };
|
|
buf_offset += len;
|
|
}
|
|
ClusterReadMapping::Backing {
|
|
offset: backing_offset,
|
|
length,
|
|
} => {
|
|
let mut buf = vec![0u8; length as usize];
|
|
self.backing_file
|
|
.as_ref()
|
|
.unwrap()
|
|
.read_at(backing_offset, &mut buf)
|
|
.map_err(AsyncIoError::ReadVectored)?;
|
|
// SAFETY: iovecs point to valid guest memory buffers
|
|
unsafe { scatter_to_iovecs(iovecs, buf_offset, &buf) };
|
|
buf_offset += length as usize;
|
|
}
|
|
}
|
|
}
|
|
|
|
self.completion_list
|
|
.push_back((user_data, total_len as i32));
|
|
self.eventfd.write(1).unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn write_vectored(
|
|
&mut self,
|
|
offset: libc::off_t,
|
|
iovecs: &[libc::iovec],
|
|
user_data: u64,
|
|
) -> AsyncIoResult<()> {
|
|
let address = offset as u64;
|
|
let total_len: usize = iovecs.iter().map(|v| v.iov_len).sum();
|
|
let mut buf_offset = 0usize;
|
|
|
|
while buf_offset < total_len {
|
|
let curr_addr = address + buf_offset as u64;
|
|
let cluster_size = self.metadata.cluster_size();
|
|
let intra_offset = self.metadata.cluster_offset(curr_addr);
|
|
let remaining_in_cluster = (cluster_size - intra_offset) as usize;
|
|
let count = min(total_len - buf_offset, remaining_in_cluster);
|
|
|
|
// Read backing data for COW if this is a partial cluster
|
|
// write to an unallocated cluster with a backing file.
|
|
let backing_data = if let Some(backing) = self
|
|
.backing_file
|
|
.as_ref()
|
|
.filter(|_| intra_offset != 0 || count < cluster_size as usize)
|
|
{
|
|
let cluster_begin = curr_addr - intra_offset;
|
|
let mut data = vec![0u8; cluster_size as usize];
|
|
backing
|
|
.read_at(cluster_begin, &mut data)
|
|
.map_err(AsyncIoError::WriteVectored)?;
|
|
Some(data)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let mapping = self
|
|
.metadata
|
|
.map_cluster_for_write(curr_addr, backing_data)
|
|
.map_err(AsyncIoError::WriteVectored)?;
|
|
|
|
match mapping {
|
|
ClusterWriteMapping::Allocated {
|
|
offset: host_offset,
|
|
} => {
|
|
// SAFETY: iovecs point to valid guest memory buffers
|
|
let buf = unsafe { gather_from_iovecs(iovecs, buf_offset, count) };
|
|
pwrite_all(self.data_file.as_raw_fd(), &buf, host_offset)
|
|
.map_err(AsyncIoError::WriteVectored)?;
|
|
}
|
|
}
|
|
buf_offset += count;
|
|
}
|
|
|
|
self.completion_list
|
|
.push_back((user_data, total_len as i32));
|
|
self.eventfd.write(1).unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn fsync(&mut self, user_data: Option<u64>) -> AsyncIoResult<()> {
|
|
self.metadata.flush().map_err(AsyncIoError::Fsync)?;
|
|
if let Some(user_data) = user_data {
|
|
self.completion_list.push_back((user_data, 0));
|
|
self.eventfd.write(1).unwrap();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn next_completed_request(&mut self) -> Option<(u64, i32)> {
|
|
self.completion_list.pop_front()
|
|
}
|
|
|
|
fn punch_hole(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
|
let virtual_size = self.metadata.virtual_size();
|
|
let cluster_size = self.metadata.cluster_size();
|
|
|
|
let result = self
|
|
.metadata
|
|
.deallocate_bytes(
|
|
offset,
|
|
length as usize,
|
|
self.sparse,
|
|
virtual_size,
|
|
cluster_size,
|
|
self.backing_file.as_deref(),
|
|
)
|
|
.map_err(AsyncIoError::PunchHole);
|
|
|
|
match result {
|
|
Ok(actions) => {
|
|
for action in actions {
|
|
match action {
|
|
DeallocAction::PunchHole {
|
|
host_offset,
|
|
length,
|
|
} => {
|
|
let _ = self.data_file.file_mut().punch_hole(host_offset, length);
|
|
}
|
|
DeallocAction::WriteZeroes {
|
|
host_offset,
|
|
length,
|
|
} => {
|
|
let _ = self
|
|
.data_file
|
|
.file_mut()
|
|
.write_zeroes_at(host_offset, length);
|
|
}
|
|
}
|
|
}
|
|
self.completion_list.push_back((user_data, 0));
|
|
self.eventfd.write(1).unwrap();
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
let errno = if let AsyncIoError::PunchHole(ref io_err) = e {
|
|
-io_err.raw_os_error().unwrap_or(libc::EIO)
|
|
} else {
|
|
-libc::EIO
|
|
};
|
|
self.completion_list.push_back((user_data, errno));
|
|
self.eventfd.write(1).unwrap();
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn write_zeroes(&mut self, offset: u64, length: u64, user_data: u64) -> AsyncIoResult<()> {
|
|
// For QCOW2 write_zeroes uses cluster deallocation, same as punch_hole.
|
|
// Unallocated clusters inherently read as zero in the QCOW2 format.
|
|
self.punch_hole(offset, length, user_data)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod unit_tests {
|
|
use std::io::{Seek, SeekFrom, Write};
|
|
use std::thread;
|
|
|
|
use vmm_sys_util::tempfile::TempFile;
|
|
|
|
use super::*;
|
|
use crate::disk_file::{AsyncDiskFile, DiskSize, Resizable};
|
|
use crate::qcow::{BackingFileConfig, ImageType, QcowFile, RawFile};
|
|
|
|
fn create_disk_with_data(
|
|
file_size: u64,
|
|
data: &[u8],
|
|
offset: u64,
|
|
sparse: bool,
|
|
) -> (TempFile, QcowDiskSync) {
|
|
let temp_file = TempFile::new().unwrap();
|
|
{
|
|
let raw_file = RawFile::new(temp_file.as_file().try_clone().unwrap(), false);
|
|
let mut qcow_file = QcowFile::new(raw_file, 3, file_size, sparse).unwrap();
|
|
qcow_file.seek(SeekFrom::Start(offset)).unwrap();
|
|
qcow_file.write_all(data).unwrap();
|
|
qcow_file.flush().unwrap();
|
|
}
|
|
let disk = QcowDiskSync::new(
|
|
temp_file.as_file().try_clone().unwrap(),
|
|
false,
|
|
false,
|
|
sparse,
|
|
)
|
|
.unwrap();
|
|
(temp_file, disk)
|
|
}
|
|
|
|
fn async_read(disk: &QcowDiskSync, offset: u64, len: usize) -> Vec<u8> {
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
let mut buf = vec![0xFFu8; len];
|
|
let iovec = libc::iovec {
|
|
iov_base: buf.as_mut_ptr() as *mut libc::c_void,
|
|
iov_len: buf.len(),
|
|
};
|
|
async_io
|
|
.read_vectored(offset as libc::off_t, &[iovec], 1)
|
|
.unwrap();
|
|
let (user_data, result) = async_io.next_completed_request().unwrap();
|
|
assert_eq!(user_data, 1);
|
|
assert_eq!(result as usize, len, "read should return requested length");
|
|
buf
|
|
}
|
|
|
|
fn async_write(disk: &QcowDiskSync, offset: u64, data: &[u8]) {
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
let iovec = libc::iovec {
|
|
iov_base: data.as_ptr() as *mut libc::c_void,
|
|
iov_len: data.len(),
|
|
};
|
|
async_io
|
|
.write_vectored(offset as libc::off_t, &[iovec], 1)
|
|
.unwrap();
|
|
let (user_data, result) = async_io.next_completed_request().unwrap();
|
|
assert_eq!(user_data, 1);
|
|
assert_eq!(result as usize, data.len());
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow_async_punch_hole_completion() {
|
|
let data = vec![0xDD; 128 * 1024];
|
|
let offset = 0u64;
|
|
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true);
|
|
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
async_io.punch_hole(offset, data.len() as u64, 100).unwrap();
|
|
let (user_data, result) = async_io.next_completed_request().unwrap();
|
|
assert_eq!(user_data, 100);
|
|
assert_eq!(result, 0, "punch_hole should succeed");
|
|
drop(async_io);
|
|
|
|
let read_buf = async_read(&disk, offset, data.len());
|
|
assert!(
|
|
read_buf.iter().all(|&b| b == 0),
|
|
"Punched hole should read as zeros"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow_async_write_zeroes_completion() {
|
|
let data = vec![0xEE; 256 * 1024];
|
|
let offset = 64 * 1024u64;
|
|
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true);
|
|
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
async_io
|
|
.write_zeroes(offset, data.len() as u64, 200)
|
|
.unwrap();
|
|
let (user_data, result) = async_io.next_completed_request().unwrap();
|
|
assert_eq!(user_data, 200);
|
|
assert_eq!(result, 0, "write_zeroes should succeed");
|
|
drop(async_io);
|
|
|
|
let read_buf = async_read(&disk, offset, data.len());
|
|
assert!(
|
|
read_buf.iter().all(|&b| b == 0),
|
|
"Zeroed region should read as zeros"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow_async_multiple_operations() {
|
|
let data = vec![0xFF; 64 * 1024];
|
|
let (_temp, _) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true);
|
|
|
|
// Write data at multiple offsets via QcowFile first, then punch
|
|
{
|
|
let temp_file = _temp.as_file().try_clone().unwrap();
|
|
let raw_file = RawFile::new(temp_file, false);
|
|
let mut qcow_file = QcowFile::from(raw_file).unwrap();
|
|
for i in 0..4u64 {
|
|
let off = i * 128 * 1024;
|
|
qcow_file.seek(SeekFrom::Start(off)).unwrap();
|
|
qcow_file.write_all(&data).unwrap();
|
|
}
|
|
qcow_file.flush().unwrap();
|
|
}
|
|
|
|
let disk =
|
|
QcowDiskSync::new(_temp.as_file().try_clone().unwrap(), false, false, true).unwrap();
|
|
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
|
|
async_io.punch_hole(0, 64 * 1024, 1).unwrap();
|
|
async_io.punch_hole(128 * 1024, 64 * 1024, 2).unwrap();
|
|
async_io.punch_hole(256 * 1024, 64 * 1024, 3).unwrap();
|
|
|
|
let (ud, res) = async_io.next_completed_request().unwrap();
|
|
assert_eq!(ud, 1);
|
|
assert_eq!(res, 0);
|
|
let (ud, res) = async_io.next_completed_request().unwrap();
|
|
assert_eq!(ud, 2);
|
|
assert_eq!(res, 0);
|
|
let (ud, res) = async_io.next_completed_request().unwrap();
|
|
assert_eq!(ud, 3);
|
|
assert_eq!(res, 0);
|
|
assert!(async_io.next_completed_request().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow_punch_hole_then_read() {
|
|
// Verify that after punch_hole, a second async_io sees zeros.
|
|
let data = vec![0xAB; 128 * 1024];
|
|
let offset = 0u64;
|
|
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true);
|
|
|
|
let mut async_io1 = disk.new_async_io(1).unwrap();
|
|
async_io1
|
|
.punch_hole(offset, data.len() as u64, 100)
|
|
.unwrap();
|
|
let (user_data, result) = async_io1.next_completed_request().unwrap();
|
|
assert_eq!(user_data, 100);
|
|
assert_eq!(result, 0);
|
|
drop(async_io1);
|
|
|
|
// Read via second async_io, should see zeros
|
|
let read_buf = async_read(&disk, offset, data.len());
|
|
assert!(
|
|
read_buf.iter().all(|&b| b == 0),
|
|
"After punch_hole, read should return zeros"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow_disk_sync_punch_hole_with_new_async_io() {
|
|
// Simulates the real usage pattern of write data, punch hole, then read back.
|
|
let data = vec![0xCD; 64 * 1024]; // one cluster
|
|
let offset = 1024 * 1024u64; // 1MB offset
|
|
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &data, offset, true);
|
|
|
|
// Punch hole to simulate DISCARD
|
|
let mut async_io1 = disk.new_async_io(1).unwrap();
|
|
async_io1.punch_hole(offset, data.len() as u64, 1).unwrap();
|
|
let (user_data, result) = async_io1.next_completed_request().unwrap();
|
|
assert_eq!(user_data, 1);
|
|
assert_eq!(result, 0, "punch_hole should succeed");
|
|
drop(async_io1);
|
|
|
|
// Read from the same location to verify
|
|
let read_buf = async_read(&disk, offset, data.len());
|
|
assert!(
|
|
read_buf.iter().all(|&b| b == 0),
|
|
"After punch_hole via new_async_io, read should return zeros"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow_async_read_write_roundtrip() {
|
|
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true);
|
|
|
|
let data = vec![0x42u8; 64 * 1024];
|
|
let offset = 0u64;
|
|
|
|
async_write(&disk, offset, &data);
|
|
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
async_io.fsync(Some(10)).unwrap();
|
|
let (ud, res) = async_io.next_completed_request().unwrap();
|
|
assert_eq!(ud, 10);
|
|
assert_eq!(res, 0);
|
|
drop(async_io);
|
|
|
|
let read_buf = async_read(&disk, offset, data.len());
|
|
assert_eq!(read_buf, data, "Read-back should match written data");
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow_async_read_unallocated() {
|
|
// Reading from an unallocated region should return zeros.
|
|
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true);
|
|
let read_buf = async_read(&disk, 0, 64 * 1024);
|
|
assert!(
|
|
read_buf.iter().all(|&b| b == 0),
|
|
"Unallocated region should read as zeros"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow_async_cross_cluster_read_write() {
|
|
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true);
|
|
|
|
// Default cluster size is 64KB. Write 96KB starting at 32KB to cross the boundary.
|
|
let data: Vec<u8> = (0..96 * 1024).map(|i| (i % 251) as u8).collect();
|
|
let offset = 32 * 1024u64;
|
|
|
|
async_write(&disk, offset, &data);
|
|
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
async_io.fsync(Some(99)).unwrap();
|
|
drop(async_io);
|
|
|
|
let read_buf = async_read(&disk, offset, data.len());
|
|
assert_eq!(
|
|
read_buf, data,
|
|
"Cross cluster read should match written data"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_backing_file_read() {
|
|
let backing_temp = TempFile::new().unwrap();
|
|
let cluster_size = 1u64 << 16;
|
|
let file_size = cluster_size * 4;
|
|
let pattern: Vec<u8> = (0..file_size as usize).map(|i| (i % 251) as u8).collect();
|
|
backing_temp.as_file().write_all(&pattern).unwrap();
|
|
backing_temp.as_file().sync_all().unwrap();
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Raw),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
// Read first cluster - should come from backing file
|
|
let buf = async_read(&disk, 0, cluster_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[..cluster_size as usize],
|
|
"First cluster should match backing file data"
|
|
);
|
|
|
|
let buf = async_read(&disk, cluster_size, cluster_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[cluster_size as usize..2 * cluster_size as usize],
|
|
"Second cluster should match backing file data"
|
|
);
|
|
|
|
// Read a partial range spanning cluster boundary
|
|
let mid = cluster_size - 512;
|
|
let len = 1024usize;
|
|
let buf = async_read(&disk, mid, len);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[mid as usize..mid as usize + len],
|
|
"Cross cluster read from backing should match"
|
|
);
|
|
|
|
let buf = async_read(&disk, 0, file_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[..],
|
|
"Full file read from backing should match"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_backing_file_read_qcow2_backing() {
|
|
let backing_temp = TempFile::new().unwrap();
|
|
let cluster_size = 1u64 << 16;
|
|
let file_size = cluster_size * 4;
|
|
let pattern: Vec<u8> = (0..file_size as usize).map(|i| (i % 251) as u8).collect();
|
|
{
|
|
let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false);
|
|
let mut qcow = QcowFile::new(raw, 3, file_size, true).unwrap();
|
|
qcow.seek(SeekFrom::Start(0)).unwrap();
|
|
qcow.write_all(&pattern).unwrap();
|
|
qcow.flush().unwrap();
|
|
}
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Qcow2),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
// Read first cluster - should come from QCOW2 backing
|
|
let buf = async_read(&disk, 0, cluster_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[..cluster_size as usize],
|
|
"First cluster from QCOW2 backing should match"
|
|
);
|
|
|
|
let buf = async_read(&disk, 0, file_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[..],
|
|
"Full file from QCOW2 backing should match"
|
|
);
|
|
|
|
// Write to first cluster, then verify second cluster still reads from backing
|
|
let new_data = vec![0xAB; cluster_size as usize];
|
|
async_write(&disk, 0, &new_data);
|
|
{
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
async_io.fsync(Some(99)).unwrap();
|
|
}
|
|
|
|
let buf = async_read(&disk, 0, cluster_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&new_data[..],
|
|
"Written cluster should be new data"
|
|
);
|
|
|
|
let buf = async_read(&disk, cluster_size, cluster_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[cluster_size as usize..2 * cluster_size as usize],
|
|
"Unwritten cluster should still come from backing"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi_queue_concurrent_reads() {
|
|
// Verify that multiple queues (threads) can read simultaneously.
|
|
// This exercises the RwLock + pread64 design: concurrent L2 cache hits
|
|
// proceed in parallel and data reads are position independent.
|
|
let cluster_size = 1u64 << 16;
|
|
let file_size = cluster_size * 16;
|
|
let pattern: Vec<u8> = (0..file_size as usize).map(|i| (i % 251) as u8).collect();
|
|
let (_temp, disk) = create_disk_with_data(file_size, &pattern, 0, true);
|
|
let disk = Arc::new(disk);
|
|
|
|
let threads: Vec<_> = (0..8)
|
|
.map(|t| {
|
|
let disk = Arc::clone(&disk);
|
|
let pattern = pattern.clone();
|
|
thread::spawn(move || {
|
|
for i in 0..16u64 {
|
|
// Each thread reads clusters in a different order
|
|
let cluster_idx = (i + t * 2) % 16;
|
|
let offset = cluster_idx * cluster_size;
|
|
let buf = async_read(&disk, offset, cluster_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[offset as usize..(offset + cluster_size) as usize],
|
|
"Thread {t} cluster {cluster_idx} mismatch"
|
|
);
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
for t in threads {
|
|
t.join().unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi_queue_concurrent_reads_qcow2_backing() {
|
|
// Same as above but reads go through a Qcow2MetadataBacking,
|
|
// exercising concurrent metadata resolution + pread64 in the backing.
|
|
let backing_temp = TempFile::new().unwrap();
|
|
let cluster_size = 1u64 << 16;
|
|
let file_size = cluster_size * 16;
|
|
let pattern: Vec<u8> = (0..file_size as usize).map(|i| (i % 251) as u8).collect();
|
|
{
|
|
let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false);
|
|
let mut qcow = QcowFile::new(raw, 3, file_size, true).unwrap();
|
|
qcow.seek(SeekFrom::Start(0)).unwrap();
|
|
qcow.write_all(&pattern).unwrap();
|
|
qcow.flush().unwrap();
|
|
}
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Qcow2),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = Arc::new(QcowDiskSync::new(file, false, true, true).unwrap());
|
|
|
|
let threads: Vec<_> = (0..8)
|
|
.map(|t| {
|
|
let disk = Arc::clone(&disk);
|
|
let pattern = pattern.clone();
|
|
thread::spawn(move || {
|
|
for i in 0..16u64 {
|
|
let cluster_idx = (i + t * 2) % 16;
|
|
let offset = cluster_idx * cluster_size;
|
|
let buf = async_read(&disk, offset, cluster_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[offset as usize..(offset + cluster_size) as usize],
|
|
"Thread {t} cluster {cluster_idx} mismatch (qcow2 backing)"
|
|
);
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
for t in threads {
|
|
t.join().unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_three_layer_backing_chain() {
|
|
// raw base -> qcow2 mid -> qcow2 overlay
|
|
// Tests recursive shared_backing_from() with nested backing.
|
|
let cluster_size = 1u64 << 16;
|
|
let file_size = cluster_size * 4;
|
|
let base_pattern: Vec<u8> = (0..file_size as usize).map(|i| (i % 251) as u8).collect();
|
|
|
|
// Layer 0: raw base
|
|
let base_temp = TempFile::new().unwrap();
|
|
base_temp.as_file().write_all(&base_pattern).unwrap();
|
|
base_temp.as_file().sync_all().unwrap();
|
|
let base_path = base_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
// Layer 1: qcow2 mid pointing at raw base, write to cluster 0 only
|
|
let mid_temp = TempFile::new().unwrap();
|
|
let mid_pattern = vec![0xBBu8; cluster_size as usize];
|
|
{
|
|
let raw = RawFile::new(mid_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: base_path,
|
|
format: Some(ImageType::Raw),
|
|
};
|
|
let mut mid =
|
|
QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap();
|
|
mid.seek(SeekFrom::Start(0)).unwrap();
|
|
mid.write_all(&mid_pattern).unwrap();
|
|
mid.flush().unwrap();
|
|
}
|
|
let mid_path = mid_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
// Layer 2: qcow2 overlay pointing at qcow2 mid, write to cluster 1 only
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
let overlay_pattern = vec![0xCCu8; cluster_size as usize];
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: mid_path,
|
|
format: Some(ImageType::Qcow2),
|
|
};
|
|
let mut overlay =
|
|
QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap();
|
|
overlay.seek(SeekFrom::Start(cluster_size)).unwrap();
|
|
overlay.write_all(&overlay_pattern).unwrap();
|
|
overlay.flush().unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
// Cluster 0: mid wrote 0xBB
|
|
let buf = async_read(&disk, 0, cluster_size as usize);
|
|
assert!(
|
|
buf.iter().all(|&b| b == 0xBB),
|
|
"Cluster 0 should come from mid layer"
|
|
);
|
|
|
|
// Cluster 1: overlay wrote 0xCC
|
|
let buf = async_read(&disk, cluster_size, cluster_size as usize);
|
|
assert!(
|
|
buf.iter().all(|&b| b == 0xCC),
|
|
"Cluster 1 should come from overlay"
|
|
);
|
|
|
|
// Cluster 2: falls through mid (unwritten) to raw base
|
|
let buf = async_read(&disk, cluster_size * 2, cluster_size as usize);
|
|
let expected_start = (cluster_size * 2) as usize;
|
|
assert_eq!(
|
|
&buf[..],
|
|
&base_pattern[expected_start..expected_start + cluster_size as usize],
|
|
"Cluster 2 should come from raw base"
|
|
);
|
|
|
|
// Cluster 3: also falls through to raw base
|
|
let buf = async_read(&disk, cluster_size * 3, cluster_size as usize);
|
|
let expected_start = (cluster_size * 3) as usize;
|
|
assert_eq!(
|
|
&buf[..],
|
|
&base_pattern[expected_start..expected_start + cluster_size as usize],
|
|
"Cluster 3 should come from raw base"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_backing_cow_preserves_all_unwritten_clusters() {
|
|
// Write to specific clusters in the overlay, verify all others still
|
|
// read from the qcow2 backing correctly.
|
|
let cluster_size = 1u64 << 16;
|
|
let num_clusters = 8u64;
|
|
let file_size = cluster_size * num_clusters;
|
|
let pattern: Vec<u8> = (0..file_size as usize).map(|i| (i % 251) as u8).collect();
|
|
|
|
let backing_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false);
|
|
let mut qcow = QcowFile::new(raw, 3, file_size, true).unwrap();
|
|
qcow.seek(SeekFrom::Start(0)).unwrap();
|
|
qcow.write_all(&pattern).unwrap();
|
|
qcow.flush().unwrap();
|
|
}
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Qcow2),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
let written = vec![0xFFu8; cluster_size as usize];
|
|
for &idx in &[0u64, 3, 7] {
|
|
async_write(&disk, idx * cluster_size, &written);
|
|
}
|
|
{
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
async_io.fsync(Some(99)).unwrap();
|
|
}
|
|
|
|
for &idx in &[0u64, 3, 7] {
|
|
let buf = async_read(&disk, idx * cluster_size, cluster_size as usize);
|
|
assert!(
|
|
buf.iter().all(|&b| b == 0xFF),
|
|
"Cluster {idx} should be written data"
|
|
);
|
|
}
|
|
|
|
// Verify unwritten clusters read from backing
|
|
for idx in 0..num_clusters {
|
|
if idx == 0 || idx == 3 || idx == 7 {
|
|
continue;
|
|
}
|
|
let offset = idx * cluster_size;
|
|
let buf = async_read(&disk, offset, cluster_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[offset as usize..(offset + cluster_size) as usize],
|
|
"Cluster {idx} should come from backing"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow2_backing_read_beyond_virtual_size() {
|
|
// Read starting past the backing file virtual_size should return zeros.
|
|
let cluster_size = 1u64 << 16;
|
|
let backing_size = cluster_size * 2;
|
|
let overlay_size = cluster_size * 4; // overlay is larger than backing
|
|
|
|
let backing_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false);
|
|
let mut qcow = QcowFile::new(raw, 3, backing_size, true).unwrap();
|
|
qcow.seek(SeekFrom::Start(0)).unwrap();
|
|
qcow.write_all(&vec![0xAA; backing_size as usize]).unwrap();
|
|
qcow.flush().unwrap();
|
|
}
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Qcow2),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, overlay_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
// Read cluster 2 (past backing virtual_size) - should be zeros
|
|
let buf = async_read(&disk, backing_size, cluster_size as usize);
|
|
assert!(
|
|
buf.iter().all(|&b| b == 0),
|
|
"Read beyond backing virtual_size should return zeros"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow2_backing_read_spanning_virtual_size() {
|
|
// Read that starts within backing bounds but extends past virtual_size.
|
|
// First part should have backing data, remainder should be zeros.
|
|
let cluster_size = 1u64 << 16;
|
|
let backing_size = cluster_size * 2;
|
|
let overlay_size = cluster_size * 4;
|
|
|
|
let backing_temp = TempFile::new().unwrap();
|
|
let backing_data = vec![0xBBu8; backing_size as usize];
|
|
{
|
|
let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false);
|
|
let mut qcow = QcowFile::new(raw, 3, backing_size, true).unwrap();
|
|
qcow.seek(SeekFrom::Start(0)).unwrap();
|
|
qcow.write_all(&backing_data).unwrap();
|
|
qcow.flush().unwrap();
|
|
}
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Qcow2),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, overlay_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
// Read 2 clusters starting at cluster 1 (spans backing boundary)
|
|
let read_len = cluster_size as usize * 2;
|
|
let buf = async_read(&disk, cluster_size, read_len);
|
|
|
|
// First cluster should be backing data
|
|
assert!(
|
|
buf[..cluster_size as usize].iter().all(|&b| b == 0xBB),
|
|
"First half should come from backing"
|
|
);
|
|
|
|
// Second cluster is past backing virtual_size - zeros
|
|
assert!(
|
|
buf[cluster_size as usize..].iter().all(|&b| b == 0),
|
|
"Second half should be zeros (past backing virtual_size)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_raw_backing_read_beyond_virtual_size() {
|
|
// Read past raw backing file virtual_size should return zeros.
|
|
let cluster_size = 1u64 << 16;
|
|
let backing_size = cluster_size * 2;
|
|
let overlay_size = cluster_size * 4;
|
|
|
|
let backing_temp = TempFile::new().unwrap();
|
|
let backing_data = vec![0xDD; backing_size as usize];
|
|
backing_temp.as_file().write_all(&backing_data).unwrap();
|
|
backing_temp.as_file().sync_all().unwrap();
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Raw),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, overlay_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
// Read cluster 2 (past backing size) - should be zeros
|
|
let buf = async_read(&disk, backing_size, cluster_size as usize);
|
|
assert!(
|
|
buf.iter().all(|&b| b == 0),
|
|
"Read beyond raw backing virtual_size should return zeros"
|
|
);
|
|
|
|
// Read spanning boundary: cluster 1 has data, cluster 2 zeros
|
|
let read_len = cluster_size as usize * 2;
|
|
let buf = async_read(&disk, cluster_size, read_len);
|
|
assert!(
|
|
buf[..cluster_size as usize].iter().all(|&b| b == 0xDD),
|
|
"First half should come from raw backing"
|
|
);
|
|
assert!(
|
|
buf[cluster_size as usize..].iter().all(|&b| b == 0),
|
|
"Second half should be zeros (past raw backing size)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_qcow2_backing_cross_cluster_read() {
|
|
// Read spanning a cluster boundary through qcow2 backing.
|
|
// Exercises the read_clusters loop in Qcow2MetadataBacking.
|
|
let cluster_size = 1u64 << 16;
|
|
let file_size = cluster_size * 4;
|
|
let pattern: Vec<u8> = (0..file_size as usize).map(|i| (i % 251) as u8).collect();
|
|
|
|
let backing_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(backing_temp.as_file().try_clone().unwrap(), false);
|
|
let mut qcow = QcowFile::new(raw, 3, file_size, true).unwrap();
|
|
qcow.seek(SeekFrom::Start(0)).unwrap();
|
|
qcow.write_all(&pattern).unwrap();
|
|
qcow.flush().unwrap();
|
|
}
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Qcow2),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
// Read spanning clusters 1-2 boundary: 512 bytes before + 512 after
|
|
let mid = cluster_size - 512;
|
|
let len = 1024usize;
|
|
let buf = async_read(&disk, mid, len);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[mid as usize..mid as usize + len],
|
|
"Cross cluster read through qcow2 backing should match"
|
|
);
|
|
|
|
// Read spanning clusters 0-1-2 (3 clusters worth)
|
|
let start = cluster_size / 2;
|
|
let len = cluster_size as usize * 2;
|
|
let buf = async_read(&disk, start, len);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[start as usize..start as usize + len],
|
|
"Multi cluster read through qcow2 backing should match"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_punch_hole_with_backing_fallthrough() {
|
|
// Write to overlay, then punch hole. After punch, the cluster should
|
|
// fall through to backing data (not zeros).
|
|
let cluster_size = 1u64 << 16;
|
|
let file_size = cluster_size * 4;
|
|
let pattern: Vec<u8> = (0..file_size as usize).map(|i| (i % 251) as u8).collect();
|
|
|
|
let backing_temp = TempFile::new().unwrap();
|
|
backing_temp.as_file().write_all(&pattern).unwrap();
|
|
backing_temp.as_file().sync_all().unwrap();
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Raw),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
let written = vec![0xFFu8; cluster_size as usize];
|
|
async_write(&disk, 0, &written);
|
|
{
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
async_io.fsync(Some(99)).unwrap();
|
|
}
|
|
|
|
let buf = async_read(&disk, 0, cluster_size as usize);
|
|
assert!(buf.iter().all(|&b| b == 0xFF), "Should read written data");
|
|
|
|
// Punch hole on cluster 0 - should deallocate and fall through to backing
|
|
{
|
|
let mut async_io = disk.new_async_io(1).unwrap();
|
|
async_io.punch_hole(0, cluster_size, 42).unwrap();
|
|
let (ud, res) = async_io.next_completed_request().unwrap();
|
|
assert_eq!(ud, 42);
|
|
assert_eq!(res, 0);
|
|
}
|
|
|
|
// Now read should return backing data, not zeros
|
|
let buf = async_read(&disk, 0, cluster_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[..cluster_size as usize],
|
|
"After punch_hole with backing, should read backing data"
|
|
);
|
|
|
|
// Cluster 1 should still be backing data throughout
|
|
let buf = async_read(&disk, cluster_size, cluster_size as usize);
|
|
assert_eq!(
|
|
&buf[..],
|
|
&pattern[cluster_size as usize..2 * cluster_size as usize],
|
|
"Untouched cluster should read from backing"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rewrite_allocated_cluster() {
|
|
// Write to a cluster, then overwrite it. The second write should hit
|
|
// the already allocated path in map_write (no new cluster allocation).
|
|
let (_temp, disk) = create_disk_with_data(100 * 1024 * 1024, &[], 0, true);
|
|
let cluster_size = 1u64 << 16;
|
|
|
|
let data1 = vec![0xAAu8; cluster_size as usize];
|
|
async_write(&disk, 0, &data1);
|
|
{
|
|
let mut aio = disk.new_async_io(1).unwrap();
|
|
aio.fsync(Some(1)).unwrap();
|
|
}
|
|
let buf = async_read(&disk, 0, cluster_size as usize);
|
|
assert!(buf.iter().all(|&b| b == 0xAA), "First write should stick");
|
|
|
|
let data2 = vec![0xBBu8; cluster_size as usize];
|
|
async_write(&disk, 0, &data2);
|
|
{
|
|
let mut aio = disk.new_async_io(1).unwrap();
|
|
aio.fsync(Some(2)).unwrap();
|
|
}
|
|
let buf = async_read(&disk, 0, cluster_size as usize);
|
|
assert!(
|
|
buf.iter().all(|&b| b == 0xBB),
|
|
"Overwrite should replace data"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_partial_cluster_write_with_backing_cow() {
|
|
// Partial cluster write to an overlay with a backing file triggers COW.
|
|
// The unwritten part of the cluster must be copied from backing.
|
|
let cluster_size = 1u64 << 16;
|
|
let file_size = cluster_size * 4;
|
|
let pattern: Vec<u8> = (0..file_size as usize).map(|i| (i % 251) as u8).collect();
|
|
|
|
let backing_temp = TempFile::new().unwrap();
|
|
backing_temp.as_file().write_all(&pattern).unwrap();
|
|
backing_temp.as_file().sync_all().unwrap();
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Raw),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
// Write 4KB at offset 4KB within cluster 0 (partial cluster)
|
|
let write_offset = 4096u64;
|
|
let write_len = 4096usize;
|
|
let write_data = vec![0xEEu8; write_len];
|
|
async_write(&disk, write_offset, &write_data);
|
|
{
|
|
let mut aio = disk.new_async_io(1).unwrap();
|
|
aio.fsync(Some(1)).unwrap();
|
|
}
|
|
|
|
let buf = async_read(&disk, 0, cluster_size as usize);
|
|
|
|
// Before the write: should be COW'd from backing
|
|
assert_eq!(
|
|
&buf[..write_offset as usize],
|
|
&pattern[..write_offset as usize],
|
|
"Pre write region should be COW from backing"
|
|
);
|
|
|
|
assert_eq!(
|
|
&buf[write_offset as usize..write_offset as usize + write_len],
|
|
&write_data[..],
|
|
"Written region should be new data"
|
|
);
|
|
|
|
// After the write: should be COW'd from backing
|
|
let after_offset = write_offset as usize + write_len;
|
|
assert_eq!(
|
|
&buf[after_offset..cluster_size as usize],
|
|
&pattern[after_offset..cluster_size as usize],
|
|
"Post write region should be COW from backing"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_partial_cluster_deallocate() {
|
|
// Punch hole on a partial cluster range. The deallocate_bytes path
|
|
// should produce WriteZeroes actions for partial clusters.
|
|
let cluster_size = 1u64 << 16;
|
|
let file_size = cluster_size * 4;
|
|
|
|
let data: Vec<u8> = (0..2 * cluster_size as usize)
|
|
.map(|i| (i % 251) as u8)
|
|
.collect();
|
|
let (_temp, disk) = create_disk_with_data(file_size, &data, 0, true);
|
|
|
|
// Punch a partial range: last 4KB of cluster 0 + first 4KB of cluster 1
|
|
let punch_offset = cluster_size - 4096;
|
|
let punch_len = 8192u64;
|
|
{
|
|
let mut aio = disk.new_async_io(1).unwrap();
|
|
aio.punch_hole(punch_offset, punch_len, 10).unwrap();
|
|
let (ud, res) = aio.next_completed_request().unwrap();
|
|
assert_eq!(ud, 10);
|
|
assert_eq!(res, 0);
|
|
}
|
|
|
|
let buf = async_read(&disk, 0, 2 * cluster_size as usize);
|
|
|
|
// Before punch: unchanged
|
|
assert_eq!(
|
|
&buf[..punch_offset as usize],
|
|
&data[..punch_offset as usize],
|
|
"Data before punch should be unchanged"
|
|
);
|
|
|
|
// Punched region: zeros
|
|
assert!(
|
|
buf[punch_offset as usize..(punch_offset + punch_len) as usize]
|
|
.iter()
|
|
.all(|&b| b == 0),
|
|
"Punched region should be zeros"
|
|
);
|
|
|
|
// After punch: unchanged
|
|
let after = (punch_offset + punch_len) as usize;
|
|
assert_eq!(
|
|
&buf[after..2 * cluster_size as usize],
|
|
&data[after..2 * cluster_size as usize],
|
|
"Data after punch should be unchanged"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resize_grow() {
|
|
let cluster_size = 1u64 << 16;
|
|
let initial_size = cluster_size * 4;
|
|
let data = vec![0xAA; cluster_size as usize];
|
|
let (_temp, mut disk) = create_disk_with_data(initial_size, &data, 0, true);
|
|
|
|
assert_eq!(disk.logical_size().unwrap(), initial_size);
|
|
|
|
let new_size = cluster_size * 8;
|
|
disk.resize(new_size).unwrap();
|
|
assert_eq!(disk.logical_size().unwrap(), new_size);
|
|
|
|
// Original data intact
|
|
let buf = async_read(&disk, 0, cluster_size as usize);
|
|
assert!(
|
|
buf.iter().all(|&b| b == 0xAA),
|
|
"Original data should survive resize"
|
|
);
|
|
|
|
// New region reads as zeros
|
|
let buf = async_read(&disk, initial_size, cluster_size as usize);
|
|
assert!(
|
|
buf.iter().all(|&b| b == 0),
|
|
"Newly grown region should read as zeros"
|
|
);
|
|
|
|
// Can write to newly grown region
|
|
let new_data = vec![0xBB; cluster_size as usize];
|
|
async_write(&disk, initial_size, &new_data);
|
|
{
|
|
let mut aio = disk.new_async_io(1).unwrap();
|
|
aio.fsync(Some(1)).unwrap();
|
|
}
|
|
let buf = async_read(&disk, initial_size, cluster_size as usize);
|
|
assert!(
|
|
buf.iter().all(|&b| b == 0xBB),
|
|
"Write to grown region should work"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resize_with_backing_file_rejected() {
|
|
let backing_temp = TempFile::new().unwrap();
|
|
let cluster_size = 1u64 << 16;
|
|
let file_size = cluster_size * 4;
|
|
backing_temp
|
|
.as_file()
|
|
.write_all(&vec![0u8; file_size as usize])
|
|
.unwrap();
|
|
backing_temp.as_file().sync_all().unwrap();
|
|
let backing_path = backing_temp.as_path().to_str().unwrap().to_string();
|
|
|
|
let overlay_temp = TempFile::new().unwrap();
|
|
{
|
|
let raw = RawFile::new(overlay_temp.as_file().try_clone().unwrap(), false);
|
|
let backing_config = BackingFileConfig {
|
|
path: backing_path,
|
|
format: Some(ImageType::Raw),
|
|
};
|
|
let _overlay =
|
|
QcowFile::new_from_backing(raw, 3, file_size, &backing_config, true).unwrap();
|
|
}
|
|
|
|
let file = overlay_temp.as_file().try_clone().unwrap();
|
|
let mut disk = QcowDiskSync::new(file, false, true, true).unwrap();
|
|
|
|
assert_eq!(disk.logical_size().unwrap(), file_size);
|
|
let result = disk.resize(file_size * 2);
|
|
assert!(result.is_err(), "resize with backing file should fail");
|
|
assert_eq!(
|
|
disk.logical_size().unwrap(),
|
|
file_size,
|
|
"size should be unchanged after failed resize"
|
|
);
|
|
}
|
|
}
|