mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
block: qcow: Read/write on disk structure via zerocopy on a struct
Use a pair of structs for the basic QCOW V2 header and V3 additional fields and serialize them to/from disk via the zerocopy crate. This removes the need to manage the position to read from/write to. Assisted-by: Claude:Opus-4.8 Signed-off-by: Rob Bradford <rbradford@meta.com>
This commit is contained in:
@@ -14,10 +14,11 @@ use std::str::FromStr;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use vmm_sys_util::file_traits::FileSync;
|
||||
use zerocopy::big_endian::{U32 as BeU32, U64 as BeU64};
|
||||
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use super::decoder::{Decoder, ZlibDecoder, ZstdDecoder};
|
||||
use super::parser::{Error, Result};
|
||||
use super::qcow_raw_file::BeUint;
|
||||
use super::util::{div_round_up_u32, div_round_up_u64};
|
||||
use crate::aligned_file::AlignedFile;
|
||||
use crate::error::{BlockError, BlockErrorKind, BlockResult};
|
||||
@@ -207,6 +208,84 @@ pub struct QcowHeader {
|
||||
pub backing_file: Option<BackingFileConfig>,
|
||||
}
|
||||
|
||||
/// On-disk layout of the bare qcow2 header shared by v2 and v3 (72 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
|
||||
struct RawHeaderV2 {
|
||||
magic: BeU32,
|
||||
version: BeU32,
|
||||
backing_file_offset: BeU64,
|
||||
backing_file_size: BeU32,
|
||||
cluster_bits: BeU32,
|
||||
size: BeU64,
|
||||
crypt_method: BeU32,
|
||||
l1_size: BeU32,
|
||||
l1_table_offset: BeU64,
|
||||
refcount_table_offset: BeU64,
|
||||
refcount_table_clusters: BeU32,
|
||||
nb_snapshots: BeU32,
|
||||
snapshots_offset: BeU64,
|
||||
}
|
||||
|
||||
impl RawHeaderV2 {
|
||||
fn from_header(header: &QcowHeader) -> Self {
|
||||
Self {
|
||||
magic: BeU32::new(header.magic),
|
||||
version: BeU32::new(header.version),
|
||||
backing_file_offset: BeU64::new(header.backing_file_offset),
|
||||
backing_file_size: BeU32::new(header.backing_file_size),
|
||||
cluster_bits: BeU32::new(header.cluster_bits),
|
||||
size: BeU64::new(header.size),
|
||||
crypt_method: BeU32::new(header.crypt_method),
|
||||
l1_size: BeU32::new(header.l1_size),
|
||||
l1_table_offset: BeU64::new(header.l1_table_offset),
|
||||
refcount_table_offset: BeU64::new(header.refcount_table_offset),
|
||||
refcount_table_clusters: BeU32::new(header.refcount_table_clusters),
|
||||
nb_snapshots: BeU32::new(header.nb_snapshots),
|
||||
snapshots_offset: BeU64::new(header.snapshots_offset),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On-disk layout of the fields v3 adds after the bare header (32 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
|
||||
struct RawHeaderV3Tail {
|
||||
incompatible_features: BeU64,
|
||||
compatible_features: BeU64,
|
||||
autoclear_features: BeU64,
|
||||
refcount_order: BeU32,
|
||||
header_size: BeU32,
|
||||
}
|
||||
|
||||
impl RawHeaderV3Tail {
|
||||
fn from_header(header: &QcowHeader) -> Self {
|
||||
Self {
|
||||
incompatible_features: BeU64::new(header.incompatible_features),
|
||||
compatible_features: BeU64::new(header.compatible_features),
|
||||
autoclear_features: BeU64::new(header.autoclear_features),
|
||||
refcount_order: BeU32::new(header.refcount_order),
|
||||
header_size: BeU32::new(header.header_size),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
|
||||
struct ExtensionHeader {
|
||||
extension_type: BeU32,
|
||||
length: BeU32,
|
||||
}
|
||||
|
||||
impl ExtensionHeader {
|
||||
fn end() -> Self {
|
||||
Self {
|
||||
extension_type: BeU32::new(HEADER_EXT_END),
|
||||
length: BeU32::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QcowHeader {
|
||||
/// Read header extensions, optionally collecting feature names for error reporting.
|
||||
pub(super) fn read_header_extensions(
|
||||
@@ -218,19 +297,19 @@ impl QcowHeader {
|
||||
let mut offset = header.header_size as u64;
|
||||
|
||||
loop {
|
||||
let mut field = [0u8; size_of::<u32>()];
|
||||
let mut field = [0u8; size_of::<ExtensionHeader>()];
|
||||
f.read_exact_at(&mut field, offset)
|
||||
.map_err(Error::ReadingHeader)?;
|
||||
offset += field.len() as u64;
|
||||
let ext_type = u32::from_be_slice(&field) as u32;
|
||||
|
||||
let extension =
|
||||
ExtensionHeader::read_from_bytes(&field).expect("buffer covers extension header");
|
||||
let ext_type = extension.extension_type.get();
|
||||
if ext_type == HEADER_EXT_END {
|
||||
break;
|
||||
}
|
||||
|
||||
f.read_exact_at(&mut field, offset)
|
||||
.map_err(Error::ReadingHeader)?;
|
||||
offset += field.len() as u64;
|
||||
let ext_length = u32::from_be_slice(&field) as u32;
|
||||
let ext_length = extension.length.get();
|
||||
|
||||
match ext_type {
|
||||
HEADER_EXT_BACKING_FORMAT => {
|
||||
@@ -277,85 +356,66 @@ impl QcowHeader {
|
||||
|
||||
/// Creates a QcowHeader from a reference to a file.
|
||||
pub fn new(f: &AlignedFile) -> Result<QcowHeader> {
|
||||
// Decode the next big endian u32 from the buffer and advance `pos`.
|
||||
fn next_u32(buf: &[u8], pos: &mut usize) -> u32 {
|
||||
let value = u32::from_be_slice(&buf[*pos..]) as u32;
|
||||
*pos += size_of::<u32>();
|
||||
value
|
||||
}
|
||||
|
||||
// Decode the next big endian u64 from the buffer and advance `pos`.
|
||||
fn next_u64(buf: &[u8], pos: &mut usize) -> u64 {
|
||||
let value = u64::from_be_slice(&buf[*pos..]);
|
||||
*pos += size_of::<u64>();
|
||||
value
|
||||
}
|
||||
|
||||
// The bare header fits in V3_BARE_HEADER_SIZE plus the optional
|
||||
// compression field. Read it once, then decode each field from the
|
||||
// buffer at its running offset.
|
||||
// compression field. Read it once, then decode each region as a typed
|
||||
// view whose layout matches the on-disk header.
|
||||
let mut buf = [0u8; V3_BARE_HEADER_SIZE as usize + size_of::<u64>()];
|
||||
f.read_exact_at(&mut buf, 0).map_err(Error::ReadingHeader)?;
|
||||
|
||||
let mut pos = 0;
|
||||
let magic = next_u32(&buf, &mut pos);
|
||||
// `buf` is always larger than the views, and the views are unaligned,
|
||||
// so the casts cannot fail.
|
||||
let (v2, tail) = RawHeaderV2::ref_from_prefix(&buf).expect("buffer covers the v2 header");
|
||||
|
||||
let magic = v2.magic.get();
|
||||
if magic != QCOW_MAGIC {
|
||||
return Err(Error::InvalidMagic);
|
||||
}
|
||||
|
||||
let version = next_u32(&buf, &mut pos);
|
||||
let version = v2.version.get();
|
||||
|
||||
let mut header = QcowHeader {
|
||||
magic,
|
||||
version,
|
||||
backing_file_offset: next_u64(&buf, &mut pos),
|
||||
backing_file_size: next_u32(&buf, &mut pos),
|
||||
cluster_bits: next_u32(&buf, &mut pos),
|
||||
size: next_u64(&buf, &mut pos),
|
||||
crypt_method: next_u32(&buf, &mut pos),
|
||||
l1_size: next_u32(&buf, &mut pos),
|
||||
l1_table_offset: next_u64(&buf, &mut pos),
|
||||
refcount_table_offset: next_u64(&buf, &mut pos),
|
||||
refcount_table_clusters: next_u32(&buf, &mut pos),
|
||||
nb_snapshots: next_u32(&buf, &mut pos),
|
||||
snapshots_offset: next_u64(&buf, &mut pos),
|
||||
incompatible_features: if version == 2 {
|
||||
0
|
||||
} else {
|
||||
next_u64(&buf, &mut pos)
|
||||
},
|
||||
compatible_features: if version == 2 {
|
||||
0
|
||||
} else {
|
||||
next_u64(&buf, &mut pos)
|
||||
},
|
||||
autoclear_features: if version == 2 {
|
||||
0
|
||||
} else {
|
||||
next_u64(&buf, &mut pos)
|
||||
},
|
||||
refcount_order: if version == 2 {
|
||||
DEFAULT_REFCOUNT_ORDER
|
||||
} else {
|
||||
next_u32(&buf, &mut pos)
|
||||
},
|
||||
header_size: if version == 2 {
|
||||
V2_BARE_HEADER_SIZE
|
||||
} else {
|
||||
next_u32(&buf, &mut pos)
|
||||
},
|
||||
backing_file_offset: v2.backing_file_offset.get(),
|
||||
backing_file_size: v2.backing_file_size.get(),
|
||||
cluster_bits: v2.cluster_bits.get(),
|
||||
size: v2.size.get(),
|
||||
crypt_method: v2.crypt_method.get(),
|
||||
l1_size: v2.l1_size.get(),
|
||||
l1_table_offset: v2.l1_table_offset.get(),
|
||||
refcount_table_offset: v2.refcount_table_offset.get(),
|
||||
refcount_table_clusters: v2.refcount_table_clusters.get(),
|
||||
nb_snapshots: v2.nb_snapshots.get(),
|
||||
snapshots_offset: v2.snapshots_offset.get(),
|
||||
incompatible_features: 0,
|
||||
compatible_features: 0,
|
||||
autoclear_features: 0,
|
||||
refcount_order: DEFAULT_REFCOUNT_ORDER,
|
||||
header_size: V2_BARE_HEADER_SIZE,
|
||||
compression_type: CompressionType::Zlib,
|
||||
backing_file: None,
|
||||
};
|
||||
if version == 3 && header.header_size > V3_BARE_HEADER_SIZE {
|
||||
let raw_compression_type = next_u64(&buf, &mut pos) >> (64 - 8);
|
||||
header.compression_type = if raw_compression_type == COMPRESSION_TYPE_ZLIB {
|
||||
Ok(CompressionType::Zlib)
|
||||
} else if raw_compression_type == COMPRESSION_TYPE_ZSTD {
|
||||
Ok(CompressionType::Zstd)
|
||||
} else {
|
||||
Err(Error::UnsupportedCompressionType)
|
||||
}?;
|
||||
|
||||
if version != 2 {
|
||||
let (v3, rest) =
|
||||
RawHeaderV3Tail::ref_from_prefix(tail).expect("buffer covers the v3 header");
|
||||
header.incompatible_features = v3.incompatible_features.get();
|
||||
header.compatible_features = v3.compatible_features.get();
|
||||
header.autoclear_features = v3.autoclear_features.get();
|
||||
header.refcount_order = v3.refcount_order.get();
|
||||
header.header_size = v3.header_size.get();
|
||||
|
||||
if version == 3 && header.header_size > V3_BARE_HEADER_SIZE {
|
||||
let (compression, _) =
|
||||
BeU64::ref_from_prefix(rest).expect("buffer covers the compression field");
|
||||
let raw_compression_type = compression.get() >> (64 - 8);
|
||||
header.compression_type = if raw_compression_type == COMPRESSION_TYPE_ZLIB {
|
||||
Ok(CompressionType::Zlib)
|
||||
} else if raw_compression_type == COMPRESSION_TYPE_ZSTD {
|
||||
Ok(CompressionType::Zstd)
|
||||
} else {
|
||||
Err(Error::UnsupportedCompressionType)
|
||||
}?;
|
||||
}
|
||||
}
|
||||
if header.backing_file_size > MAX_BACKING_FILE_SIZE {
|
||||
return Err(Error::BackingFileTooLong(header.backing_file_size as usize));
|
||||
@@ -508,36 +568,20 @@ impl QcowHeader {
|
||||
pub fn write_to(&self, f: &AlignedFile) -> Result<()> {
|
||||
// Build the header in memory, then write it in one positional write.
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&self.magic.to_be_bytes());
|
||||
buf.extend_from_slice(&self.version.to_be_bytes());
|
||||
buf.extend_from_slice(&self.backing_file_offset.to_be_bytes());
|
||||
buf.extend_from_slice(&self.backing_file_size.to_be_bytes());
|
||||
buf.extend_from_slice(&self.cluster_bits.to_be_bytes());
|
||||
buf.extend_from_slice(&self.size.to_be_bytes());
|
||||
buf.extend_from_slice(&self.crypt_method.to_be_bytes());
|
||||
buf.extend_from_slice(&self.l1_size.to_be_bytes());
|
||||
buf.extend_from_slice(&self.l1_table_offset.to_be_bytes());
|
||||
buf.extend_from_slice(&self.refcount_table_offset.to_be_bytes());
|
||||
buf.extend_from_slice(&self.refcount_table_clusters.to_be_bytes());
|
||||
buf.extend_from_slice(&self.nb_snapshots.to_be_bytes());
|
||||
buf.extend_from_slice(&self.snapshots_offset.to_be_bytes());
|
||||
let v2 = RawHeaderV2::from_header(self);
|
||||
buf.extend_from_slice(v2.as_bytes());
|
||||
|
||||
if self.version == 3 {
|
||||
buf.extend_from_slice(&self.incompatible_features.to_be_bytes());
|
||||
buf.extend_from_slice(&self.compatible_features.to_be_bytes());
|
||||
buf.extend_from_slice(&self.autoclear_features.to_be_bytes());
|
||||
buf.extend_from_slice(&self.refcount_order.to_be_bytes());
|
||||
buf.extend_from_slice(&self.header_size.to_be_bytes());
|
||||
let v3 = RawHeaderV3Tail::from_header(self);
|
||||
buf.extend_from_slice(v3.as_bytes());
|
||||
|
||||
if self.header_size > V3_BARE_HEADER_SIZE {
|
||||
// no compression
|
||||
buf.extend_from_slice(&0u64.to_be_bytes());
|
||||
buf.extend_from_slice(BeU64::new(0).as_bytes());
|
||||
}
|
||||
|
||||
// header extension type: end of header extension area
|
||||
buf.extend_from_slice(&0u32.to_be_bytes());
|
||||
// length of header extension data: 0
|
||||
buf.extend_from_slice(&0u32.to_be_bytes());
|
||||
let end_extension = ExtensionHeader::end();
|
||||
buf.extend_from_slice(end_extension.as_bytes());
|
||||
}
|
||||
|
||||
f.write_all_at(&buf, 0).map_err(Error::WritingHeader)?;
|
||||
|
||||
@@ -1035,6 +1035,40 @@ mod unit_tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_write_matches_qcow2_layout() {
|
||||
for expected in [valid_header_v2(), valid_header_v3()] {
|
||||
let header = QcowHeader::new(&basic_file(&expected)).expect("Failed to read header.");
|
||||
let disk_file: AlignedFile =
|
||||
AlignedFile::new(TempFile::new().unwrap().into_file(), false);
|
||||
|
||||
header
|
||||
.write_to(&disk_file)
|
||||
.expect("Failed to write header.");
|
||||
|
||||
let mut actual = vec![0; expected.len()];
|
||||
disk_file.read_exact_at(&mut actual, 0).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
let mut expected = valid_header_v3();
|
||||
expected[72..80].copy_from_slice(&IncompatFeatures::COMPRESSION.bits().to_be_bytes());
|
||||
expected[100..104].copy_from_slice(&(V3_BARE_HEADER_SIZE + 8).to_be_bytes());
|
||||
expected.extend_from_slice(&(1u64 << (64 - 8)).to_be_bytes());
|
||||
expected.extend_from_slice(&HEADER_EXT_END.to_be_bytes());
|
||||
expected.extend_from_slice(&0u32.to_be_bytes());
|
||||
|
||||
let header = QcowHeader::new(&basic_file(&expected)).expect("Failed to read zstd header.");
|
||||
let disk_file: AlignedFile = AlignedFile::new(TempFile::new().unwrap().into_file(), false);
|
||||
header
|
||||
.write_to(&disk_file)
|
||||
.expect("Failed to write header.");
|
||||
|
||||
let mut actual = vec![0; expected.len()];
|
||||
disk_file.read_exact_at(&mut actual, 0).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_v2_with_backing() {
|
||||
let header = QcowHeader::create_for_size_and_path(2, 0x10_0000, Some("/my/path/to/a/file"))
|
||||
|
||||
Reference in New Issue
Block a user