diff --git a/block/src/qcow/mod.rs b/block/src/qcow/mod.rs index 5e1ec8d54..cc3fb1d14 100644 --- a/block/src/qcow/mod.rs +++ b/block/src/qcow/mod.rs @@ -11,11 +11,12 @@ mod refcount; mod vec_cache; use std::cmp::{max, min}; +use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; use std::fs::OpenOptions; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::mem::size_of; use std::os::fd::{AsRawFd, RawFd}; -use std::str; +use std::str::{self, FromStr}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use libc::{EINVAL, EIO, ENOSPC}; @@ -111,6 +112,8 @@ pub enum Error { TooManyL1Entries(u64), #[error("Ref count table too large: {0}")] TooManyRefcounts(u64), + #[error("Unsupported backing file format: {0}")] + UnsupportedBackingFileFormat(String), #[error("Unsupported compression type")] UnsupportedCompressionType, #[error("Unsupported refcount order")] @@ -125,18 +128,46 @@ pub enum Error { pub type Result = std::result::Result; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ImageType { Raw, Qcow2, } +impl Display for ImageType { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + ImageType::Raw => write!(f, "raw"), + ImageType::Qcow2 => write!(f, "qcow2"), + } + } +} + +impl FromStr for ImageType { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "raw" => Ok(ImageType::Raw), + "qcow2" => Ok(ImageType::Qcow2), + _ => Err(Error::UnsupportedBackingFileFormat(s.to_string())), + } + } +} + #[derive(Clone, Debug)] pub enum CompressionType { Zlib, Zstd, } +#[derive(Debug, Clone)] +pub struct BackingFileConfig { + pub path: String, + // If this is None, we will autodetect it. + pub format: Option, +} + // Maximum data size supported. const MAX_QCOW_FILE_SIZE: u64 = 0x01 << 44; // 16 TB. @@ -243,7 +274,7 @@ pub struct QcowHeader { pub compression_type: CompressionType, // Post-header entries - pub backing_file_path: Option, + pub backing_file: Option, } impl QcowHeader { @@ -307,7 +338,7 @@ impl QcowHeader { read_u32_from_file(f)? }, compression_type: CompressionType::Zlib, - backing_file_path: None, + backing_file: None, }; if version == 3 && header.header_size > V3_BARE_HEADER_SIZE { let raw_compression_type = read_u64_from_file(f)? >> (64 - 8); @@ -328,10 +359,9 @@ impl QcowHeader { let mut backing_file_name_bytes = vec![0u8; header.backing_file_size as usize]; f.read_exact(&mut backing_file_name_bytes) .map_err(Error::ReadingHeader)?; - header.backing_file_path = Some( - String::from_utf8(backing_file_name_bytes) - .map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?, - ); + let path = String::from_utf8(backing_file_name_bytes) + .map_err(|err| Error::InvalidBackingFileName(err.utf8_error()))?; + header.backing_file = Some(BackingFileConfig { path, format: None }); } Ok(header) } @@ -407,7 +437,10 @@ impl QcowHeader { refcount_order: DEFAULT_REFCOUNT_ORDER, header_size, compression_type: CompressionType::Zlib, - backing_file_path: backing_file.map(String::from), + backing_file: backing_file.map(|path| BackingFileConfig { + path: String::from(path), + format: None, + }), }) } @@ -449,7 +482,7 @@ impl QcowHeader { write_u32_to_file(file, 0)?; // length of header extension data: 0 } - if let Some(backing_file_path) = self.backing_file_path.as_ref() { + if let Some(backing_file_path) = self.backing_file.as_ref().map(|bf| &bf.path) { write!(file, "{backing_file_path}").map_err(Error::WritingHeader)?; } @@ -479,6 +512,92 @@ fn max_refcount_clusters(refcount_order: u32, cluster_size: u32, num_clusters: u for_data + for_refcounts } +trait BackingFileOps: Send + Seek + Read { + fn read_at(&mut self, address: u64, buf: &mut [u8]) -> std::io::Result<()> { + self.seek(SeekFrom::Start(address))?; + self.read_exact(buf) + } + fn clone_box(&self) -> Box; +} + +impl BackingFileOps for QcowFile { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl BackingFileOps for RawFile { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +/// Backing file wrapper +struct BackingFile { + inner: Box, +} + +impl BackingFile { + fn new( + backing_file_config: Option<&BackingFileConfig>, + direct_io: bool, + max_nesting_depth: u32, + ) -> Result> { + let Some(config) = backing_file_config else { + return Ok(None); + }; + + // Check nesting depth - applies to any backing file + if max_nesting_depth == 0 { + return Err(Error::MaxNestingDepthExceeded); + } + + let backing_raw_file = OpenOptions::new() + .read(true) + .open(&config.path) + .map_err(Error::BackingFileIo)?; + + let mut raw_file = RawFile::new(backing_raw_file, direct_io); + + // Determine backing file format from header extension or auto-detect + let backing_format = match config.format { + Some(format) => format, + None => detect_image_type(&mut raw_file)?, + }; + + let inner: Box = match backing_format { + ImageType::Raw => Box::new(raw_file), + ImageType::Qcow2 => { + let backing_qcow = + QcowFile::from_with_nesting_depth(raw_file, max_nesting_depth - 1) + .map_err(|e| Error::BackingFileOpen(Box::new(e)))?; + Box::new(backing_qcow) + } + }; + + Ok(Some(Self { inner })) + } + + #[inline] + fn read_at(&mut self, address: u64, buf: &mut [u8]) -> std::io::Result<()> { + self.inner.read_at(address, buf) + } +} + +impl Clone for BackingFile { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone_box(), + } + } +} + +impl Debug for BackingFile { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.debug_struct("BackingFile").finish() + } +} + /// Represents a qcow2 file. This is a sparse file format maintained by the qemu project. /// Full documentation of the format can be found in the qemu repository. /// @@ -509,7 +628,7 @@ pub struct QcowFile { // List of unreferenced clusters available to be used. unref clusters become available once the // removal of references to them have been synced to disk. avail_clusters: Vec, - backing_file: Option>, + backing_file: Option, } impl QcowFile { @@ -548,24 +667,8 @@ impl QcowFile { let direct_io = file.is_direct(); - let backing_file = if let Some(backing_file_path) = header.backing_file_path.as_ref() { - if max_nesting_depth == 0 { - return Err(Error::MaxNestingDepthExceeded); - } - let path = backing_file_path.clone(); - let backing_raw_file = OpenOptions::new() - .read(true) - .open(path) - .map_err(Error::BackingFileIo)?; - let backing_file = Self::from_with_nesting_depth( - RawFile::new(backing_raw_file, direct_io), - max_nesting_depth - 1, - ) - .map_err(|e| Error::BackingFileOpen(Box::new(e)))?; - Some(Box::new(backing_file)) - } else { - None - }; + let backing_file = + BackingFile::new(header.backing_file.as_ref(), direct_io, max_nesting_depth)?; // Only support two byte refcounts. let refcount_bits: u64 = 0x01u64 @@ -692,28 +795,23 @@ impl QcowFile { QcowFile::new_from_header(file, &header) } - /// Creates a new QcowFile at the given path. + /// Creates a new QcowFile at the given path with a backing file. pub fn new_from_backing( file: RawFile, version: u32, - backing_file_name: &str, - backing_file_max_nesting_depth: u32, + backing_file_size: u64, + backing_config: &BackingFileConfig, ) -> Result { - let direct_io = file.is_direct(); - let backing_raw_file = OpenOptions::new() - .read(true) - .open(backing_file_name) - .map_err(Error::BackingFileIo)?; - let backing_file = Self::from_with_nesting_depth( - RawFile::new(backing_raw_file, direct_io), - backing_file_max_nesting_depth, - ) - .map_err(|e| Error::BackingFileOpen(Box::new(e)))?; - let size = backing_file.virtual_size(); - let header = QcowHeader::create_for_size_and_path(version, size, Some(backing_file_name))?; - let mut result = QcowFile::new_from_header(file, &header)?; - result.backing_file = Some(Box::new(backing_file)); - Ok(result) + let mut header = QcowHeader::create_for_size_and_path( + version, + backing_file_size, + Some(&backing_config.path), + )?; + if let Some(backing_file) = &mut header.backing_file { + backing_file.format = backing_config.format; + } + QcowFile::new_from_header(file, &header) + // backing_file is loaded by new_from_header -> Self::from() based on the header } fn new_from_header(mut file: RawFile, header: &QcowHeader) -> Result { @@ -741,7 +839,9 @@ impl QcowFile { } pub fn set_backing_file(&mut self, backing: Option>) { - self.backing_file = backing; + self.backing_file = backing.map(|b| BackingFile { + inner: Box::new(*b), + }); } /// Returns the `QcowHeader` for this file. @@ -1246,8 +1346,7 @@ impl QcowFile { let cluster_size = self.raw_file.cluster_size(); let cluster_begin = address - (address % cluster_size); let mut cluster_data = vec![0u8; cluster_size as usize]; - backing.seek(SeekFrom::Start(cluster_begin))?; - backing.read_exact(&mut cluster_data)?; + backing.read_at(cluster_begin, &mut cluster_data)?; Some(cluster_data) } else { None @@ -1664,8 +1763,7 @@ impl Read for QcowFile { if (self.file_read(curr_addr, count, &mut buf[nread..(nread + count)])?).is_some() { // Data is successfully read from the cluster } else if let Some(backing) = self.backing_file.as_mut() { - backing.seek(SeekFrom::Start(curr_addr))?; - backing.read_exact(&mut buf[nread..(nread + count)])?; + backing.read_at(curr_addr, &mut buf[nread..(nread + count)])?; } else { // Previously unwritten region, return zeros for b in &mut buf[nread..(nread + count)] { @@ -2147,10 +2245,13 @@ mod unit_tests { disk_file.rewind().unwrap(); let read_header = QcowHeader::new(&mut disk_file).expect("Failed to create header."); assert_eq!( - header.backing_file_path, + header.backing_file.as_ref().map(|bf| bf.path.clone()), Some(String::from("/my/path/to/a/file")) ); - assert_eq!(read_header.backing_file_path, header.backing_file_path); + assert_eq!( + read_header.backing_file.as_ref().map(|bf| &bf.path), + header.backing_file.as_ref().map(|bf| &bf.path) + ); } #[test] @@ -2164,10 +2265,13 @@ mod unit_tests { disk_file.rewind().unwrap(); let read_header = QcowHeader::new(&mut disk_file).expect("Failed to create header."); assert_eq!( - header.backing_file_path, + header.backing_file.as_ref().map(|bf| bf.path.clone()), Some(String::from("/my/path/to/a/file")) ); - assert_eq!(read_header.backing_file_path, header.backing_file_path); + assert_eq!( + read_header.backing_file.as_ref().map(|bf| &bf.path), + header.backing_file.as_ref().map(|bf| &bf.path) + ); } #[test]