From 3f02ccaa8cc4ed547c8fdaa2dd592e835879789c Mon Sep 17 00:00:00 2001 From: Rob Bradford Date: Fri, 19 Jul 2019 12:16:04 +0100 Subject: [PATCH] qcow: Add support for QCOW v2 header The QCOW2 format is documented here: https://git.qemu.org/?p=qemu.git;a=blob;f=docs/interop/qcow2.txt;hb=HEAD The only difference between v2 and v3 is the addition of some extra fields into the header in v3 for which there are default values in v2. This introduces a new unit test for the behaviour but it has been manually verified by the converting the image from v3 to v2 with a command like: qemu-img convert -O qcow2 -o compat=0.10 clear-29620-cloud.img clear-29620-cloud.img.v2 Signed-off-by: Rob Bradford --- qcow/src/qcow.rs | 129 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 99 insertions(+), 30 deletions(-) diff --git a/qcow/src/qcow.rs b/qcow/src/qcow.rs index ff5db9a73..534f55eed 100755 --- a/qcow/src/qcow.rs +++ b/qcow/src/qcow.rs @@ -116,6 +116,7 @@ const MAX_CLUSTER_BITS: u32 = 30; // Only support 2 byte refcounts, 2^refcount_order bits. const DEFAULT_REFCOUNT_ORDER: u32 = 4; +const V2_BARE_HEADER_SIZE: u32 = 72; const V3_BARE_HEADER_SIZE: u32 = 104; // bits 0-8 and 56-63 are reserved. @@ -175,9 +176,11 @@ impl QcowHeader { f.read_u64::().map_err(Error::ReadingHeader) } + let version = read_u32_from_file(f)?; + Ok(QcowHeader { magic, - version: read_u32_from_file(f)?, + version, backing_file_offset: read_u64_from_file(f)?, backing_file_size: read_u32_from_file(f)?, cluster_bits: read_u32_from_file(f)?, @@ -189,16 +192,36 @@ impl QcowHeader { refcount_table_clusters: read_u32_from_file(f)?, nb_snapshots: read_u32_from_file(f)?, snapshots_offset: read_u64_from_file(f)?, - incompatible_features: read_u64_from_file(f)?, - compatible_features: read_u64_from_file(f)?, - autoclear_features: read_u64_from_file(f)?, - refcount_order: read_u32_from_file(f)?, - header_size: read_u32_from_file(f)?, + incompatible_features: if version == 2 { + 0 + } else { + read_u64_from_file(f)? + }, + compatible_features: if version == 2 { + 0 + } else { + read_u64_from_file(f)? + }, + autoclear_features: if version == 2 { + 0 + } else { + read_u64_from_file(f)? + }, + refcount_order: if version == 2 { + DEFAULT_REFCOUNT_ORDER + } else { + read_u32_from_file(f)? + }, + header_size: if version == 2 { + V2_BARE_HEADER_SIZE + } else { + read_u32_from_file(f)? + }, }) } /// Create a header for the given `size`. - pub fn create_for_size(size: u64) -> QcowHeader { + pub fn create_for_size(version: u32, size: u64) -> QcowHeader { let cluster_bits: u32 = DEFAULT_CLUSTER_BITS; let cluster_size: u32 = 0x01 << cluster_bits; // L2 blocks are always one cluster long. They contain cluster_size/sizeof(u64) addresses. @@ -209,7 +232,7 @@ impl QcowHeader { let header_clusters = div_round_up_u32(size_of::() as u32, cluster_size); QcowHeader { magic: QCOW_MAGIC, - version: 3, + version, backing_file_offset: 0, backing_file_size: 0, cluster_bits: DEFAULT_CLUSTER_BITS, @@ -240,7 +263,11 @@ impl QcowHeader { compatible_features: 0, autoclear_features: 0, refcount_order: DEFAULT_REFCOUNT_ORDER, - header_size: V3_BARE_HEADER_SIZE, + header_size: if version == 2 { + V2_BARE_HEADER_SIZE + } else { + V3_BARE_HEADER_SIZE + }, } } @@ -336,8 +363,8 @@ impl QcowFile { pub fn from(mut file: File) -> Result { let header = QcowHeader::new(&mut file)?; - // Only v3 files are supported. - if header.version != 3 { + // Only v2 and v3 files are supported. + if header.version != 2 && header.version != 3 { return Err(Error::UnsupportedVersion(header.version)); } @@ -461,8 +488,8 @@ impl QcowFile { } /// Creates a new QcowFile at the given path. - pub fn new(mut file: File, virtual_size: u64) -> Result { - let header = QcowHeader::create_for_size(virtual_size); + pub fn new(mut file: File, version: u32, virtual_size: u64) -> Result { + let header = QcowHeader::create_for_size(version, virtual_size); file.seek(SeekFrom::Start(0)).map_err(Error::SeekingFile)?; header.write_to(&mut file)?; @@ -1569,7 +1596,7 @@ where match dst_type { ImageType::Qcow2 => { - let mut dst_writer = QcowFile::new(dst_file, src_size)?; + let mut dst_writer = QcowFile::new(dst_file, 3, src_size)?; convert_reader_writer(reader, &mut dst_writer, src_size) } ImageType::Raw => { @@ -1625,7 +1652,7 @@ mod tests { use std::io::{Read, Seek, SeekFrom, Write}; use tempfile::tempfile; - fn valid_header() -> Vec { + fn valid_header_v3() -> Vec { vec![ 0x51u8, 0x46, 0x49, 0xfb, // magic 0x00, 0x00, 0x00, 0x03, // version @@ -1648,6 +1675,24 @@ mod tests { ] } + fn valid_header_v2() -> Vec { + vec![ + 0x51u8, 0x46, 0x49, 0xfb, // magic + 0x00, 0x00, 0x00, 0x02, // version + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // backing file offset + 0x00, 0x00, 0x00, 0x00, // backing file size + 0x00, 0x00, 0x00, 0x10, // cluster_bits + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, // size + 0x00, 0x00, 0x00, 0x00, // crypt method + 0x00, 0x00, 0x01, 0x00, // L1 size + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // L1 table offset + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // refcount table offset + 0x00, 0x00, 0x00, 0x03, // refcount table clusters + 0x00, 0x00, 0x00, 0x00, // nb snapshots + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // snapshots offset + ] + } + fn with_basic_file(header: &[u8], mut testfn: F) where F: FnMut(File), @@ -1665,26 +1710,46 @@ mod tests { F: FnMut(QcowFile), { let tmp = tempfile().unwrap(); - let qcow_file = QcowFile::new(tmp, file_size).unwrap(); + let qcow_file = QcowFile::new(tmp, 3, file_size).unwrap(); testfn(qcow_file); // File closed when the function exits. } #[test] - fn default_header() { - let header = QcowHeader::create_for_size(0x10_0000); + fn default_header_v2() { + let header = QcowHeader::create_for_size(2, 0x10_0000); let mut disk_file: File = tempfile().unwrap(); header .write_to(&mut disk_file) - .expect("Failed to write header to shm."); + .expect("Failed to write header to temporary file."); + disk_file.seek(SeekFrom::Start(0)).unwrap(); + QcowFile::from(disk_file).expect("Failed to create Qcow from default Header"); + } + + #[test] + fn default_header_v3() { + let header = QcowHeader::create_for_size(3, 0x10_0000); + let mut disk_file: File = tempfile().unwrap(); + header + .write_to(&mut disk_file) + .expect("Failed to write header to temporary file."); disk_file.seek(SeekFrom::Start(0)).unwrap(); QcowFile::from(disk_file).expect("Failed to create Qcow from default Header"); } #[test] fn header_read() { - with_basic_file(&valid_header(), |mut disk_file: File| { - QcowHeader::new(&mut disk_file).expect("Failed to create Header."); + with_basic_file(&valid_header_v2(), |mut disk_file: File| { + let header = QcowHeader::new(&mut disk_file).expect("Failed to create Header."); + assert_eq!(header.version, 2); + assert_eq!(header.refcount_order, DEFAULT_REFCOUNT_ORDER); + assert_eq!(header.header_size, V2_BARE_HEADER_SIZE); + }); + with_basic_file(&valid_header_v3(), |mut disk_file: File| { + let header = QcowHeader::new(&mut disk_file).expect("Failed to create Header."); + assert_eq!(header.version, 3); + assert_eq!(header.refcount_order, DEFAULT_REFCOUNT_ORDER); + assert_eq!(header.header_size, V3_BARE_HEADER_SIZE); }); } @@ -1698,7 +1763,7 @@ mod tests { #[test] fn invalid_refcount_order() { - let mut header = valid_header(); + let mut header = valid_header_v3(); header[99] = 2; with_basic_file(&header, |disk_file: File| { QcowFile::from(disk_file).expect_err("Invalid refcount order worked."); @@ -1707,7 +1772,7 @@ mod tests { #[test] fn write_read_start() { - with_basic_file(&valid_header(), |disk_file: File| { + with_basic_file(&valid_header_v3(), |disk_file: File| { let mut q = QcowFile::from(disk_file).unwrap(); q.write(b"test first bytes") .expect("Failed to write test string."); @@ -1720,7 +1785,7 @@ mod tests { #[test] fn offset_write_read() { - with_basic_file(&valid_header(), |disk_file: File| { + with_basic_file(&valid_header_v3(), |disk_file: File| { let mut q = QcowFile::from(disk_file).unwrap(); let b = [0x55u8; 0x1000]; q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek."); @@ -1734,7 +1799,7 @@ mod tests { #[test] fn write_zeroes_read() { - with_basic_file(&valid_header(), |disk_file: File| { + with_basic_file(&valid_header_v3(), |disk_file: File| { let mut q = QcowFile::from(disk_file).unwrap(); // Write some test data. let b = [0x55u8; 0x1000]; @@ -1760,7 +1825,7 @@ mod tests { // Choose a size that is larger than a cluster. // valid_header uses cluster_bits = 12, which corresponds to a cluster size of 4096. const CHUNK_SIZE: usize = 4096 * 2 + 512; - with_basic_file(&valid_header(), |disk_file: File| { + with_basic_file(&valid_header_v3(), |disk_file: File| { let mut q = QcowFile::from(disk_file).unwrap(); // Write some test data. let b = [0x55u8; CHUNK_SIZE]; @@ -1781,7 +1846,11 @@ mod tests { #[test] fn test_header() { - with_basic_file(&valid_header(), |disk_file: File| { + with_basic_file(&valid_header_v2(), |disk_file: File| { + let q = QcowFile::from(disk_file).unwrap(); + assert_eq!(q.virtual_size(), 0x20_0000_0000); + }); + with_basic_file(&valid_header_v3(), |disk_file: File| { let q = QcowFile::from(disk_file).unwrap(); assert_eq!(q.virtual_size(), 0x20_0000_0000); }); @@ -1789,7 +1858,7 @@ mod tests { #[test] fn read_small_buffer() { - with_basic_file(&valid_header(), |disk_file: File| { + with_basic_file(&valid_header_v3(), |disk_file: File| { let mut q = QcowFile::from(disk_file).unwrap(); let mut b = [5u8; 16]; q.seek(SeekFrom::Start(1000)).expect("Failed to seek."); @@ -1801,7 +1870,7 @@ mod tests { #[test] fn replay_ext4() { - with_basic_file(&valid_header(), |disk_file: File| { + with_basic_file(&valid_header_v3(), |disk_file: File| { let mut q = QcowFile::from(disk_file).unwrap(); const BUF_SIZE: usize = 0x1000; let mut b = [0u8; BUF_SIZE]; @@ -2337,7 +2406,7 @@ mod tests { #[test] fn rebuild_refcounts() { - with_basic_file(&valid_header(), |mut disk_file: File| { + with_basic_file(&valid_header_v3(), |mut disk_file: File| { let header = QcowHeader::new(&mut disk_file).expect("Failed to create Header."); let cluster_size = 65536; let mut raw_file =