block: qcow: Fix v3 header writing and add extension tests

The write_to() function is used by test code to create qcow2 files for
testing. For v3 headers with extended header_size (>104), it needs to:

1. Write the mandatory compression_type field at bytes 104-111
2. Write the header extension end marker at the header_size offset
3. Seek to backing_file_offset before writing the backing file path

Additionally, create_for_size_and_path() must set backing_file_offset
to account for the 8 byte extension end marker in v3 files, so the
backing file path doesn't overwrite the extension area.

Add unit tests for read_header_extensions() covering backing format
parsing (raw/qcow2), unknown extensions, and error cases (invalid
formats, invalid UTF-8). These tests depend on the header writing fixes
to create properly formatted v3 test files.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski 2026-01-12 15:10:31 +01:00 committed by Rob Bradford
parent 9eb2b9b0e5
commit 3fed706d6a

View file

@ -451,10 +451,13 @@ impl QcowHeader {
Ok(QcowHeader {
magic: QCOW_MAGIC,
version,
backing_file_offset: (if backing_file.is_none() {
0
} else {
backing_file_offset: backing_file.map_or(0, |_| {
header_size
+ if version == 3 {
QCOW_EMPTY_HEADER_EXTENSION_SIZE
} else {
0
}
}) as u64,
backing_file_size: backing_file.map_or(0, |x| x.len()) as u32,
cluster_bits: DEFAULT_CLUSTER_BITS,
@ -528,11 +531,20 @@ impl QcowHeader {
write_u64_to_file(file, self.autoclear_features)?;
write_u32_to_file(file, self.refcount_order)?;
write_u32_to_file(file, self.header_size)?;
if self.header_size > V3_BARE_HEADER_SIZE {
write_u64_to_file(file, 0)?; // no compression
}
write_u32_to_file(file, 0)?; // header extension type: end of header extension area
write_u32_to_file(file, 0)?; // length of header extension data: 0
}
if let Some(backing_file_path) = self.backing_file.as_ref().map(|bf| &bf.path) {
if self.backing_file_offset > 0 {
file.seek(SeekFrom::Start(self.backing_file_offset))
.map_err(Error::WritingHeader)?;
}
write!(file, "{backing_file_path}").map_err(Error::WritingHeader)?;
}
@ -2324,6 +2336,120 @@ mod unit_tests {
);
}
/// Helper to create a test file with header extensions
fn create_header_with_extension(ext_type: u32, ext_data: &[u8]) -> (RawFile, QcowHeader) {
let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None)
.expect("Failed to create header.");
let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false);
header.write_to(&mut disk_file).unwrap();
// Write extension
disk_file
.seek(SeekFrom::Start(header.header_size as u64))
.unwrap();
disk_file.write_u32::<BigEndian>(ext_type).unwrap();
disk_file
.write_u32::<BigEndian>(ext_data.len() as u32)
.unwrap();
disk_file.write_all(ext_data).unwrap();
// Add padding to 8-byte boundary
let padding = (8 - (ext_data.len() % 8)) % 8;
if padding > 0 {
disk_file.write_all(&vec![0u8; padding]).unwrap();
}
disk_file.write_u32::<BigEndian>(HEADER_EXT_END).unwrap();
disk_file.rewind().unwrap();
(disk_file, header)
}
#[test]
fn read_header_extensions_unknown_extension() {
let (mut disk_file, mut header) = create_header_with_extension(
0x12345678, // unknown type
"test".as_bytes(),
);
// Extension parsing needs a backing file to set format on
header.backing_file = Some(BackingFileConfig {
path: "/test/backing".to_string(),
format: None,
});
QcowHeader::read_header_extensions(&mut disk_file, &mut header).unwrap();
assert_eq!(header.backing_file.as_ref().and_then(|bf| bf.format), None);
}
#[test]
fn read_header_extensions_raw_format() {
let (mut disk_file, mut header) =
create_header_with_extension(HEADER_EXT_BACKING_FORMAT, "raw".as_bytes());
header.backing_file = Some(BackingFileConfig {
path: "/test/backing".to_string(),
format: None,
});
QcowHeader::read_header_extensions(&mut disk_file, &mut header).unwrap();
assert_eq!(
header.backing_file.as_ref().and_then(|bf| bf.format),
Some(ImageType::Raw)
);
}
#[test]
fn read_header_extensions_qcow2_format() {
let (mut disk_file, mut header) =
create_header_with_extension(HEADER_EXT_BACKING_FORMAT, "qcow2".as_bytes());
header.backing_file = Some(BackingFileConfig {
path: "/test/backing".to_string(),
format: None,
});
QcowHeader::read_header_extensions(&mut disk_file, &mut header).unwrap();
assert_eq!(
header.backing_file.as_ref().and_then(|bf| bf.format),
Some(ImageType::Qcow2)
);
}
#[test]
fn read_header_extensions_invalid_format() {
let (mut disk_file, mut header) =
create_header_with_extension(HEADER_EXT_BACKING_FORMAT, "vmdk".as_bytes());
header.backing_file = Some(BackingFileConfig {
path: "/test/backing".to_string(),
format: None,
});
let result = QcowHeader::read_header_extensions(&mut disk_file, &mut header);
assert!(matches!(
result.unwrap_err(),
Error::UnsupportedBackingFileFormat(_)
));
}
#[test]
fn read_header_extensions_invalid_utf8() {
let (mut disk_file, mut header) = create_header_with_extension(
HEADER_EXT_BACKING_FORMAT,
&[0xFF, 0xFE, 0xFD], // invalid UTF-8
);
let result = QcowHeader::read_header_extensions(&mut disk_file, &mut header);
// Should fail with InvalidBackingFileName error
assert!(matches!(
result.unwrap_err(),
Error::InvalidBackingFileName(_)
));
}
#[test]
fn no_backing_file() {
// `backing_file` is `None`