use crate::error::{Qcow2Error, Result};
pub const MAGIC: u32 = 0x5146_49fb;
pub const MIN_HEADER_SIZE: usize = 72;
const INCOMPAT_EXTERNAL_DATA: u64 = 1 << 2;
const INCOMPAT_COMPRESSION_TYPE: u64 = 1 << 3;
const INCOMPAT_EXTENDED_L2: u64 = 1 << 4;
const INCOMPAT_UNSUPPORTED: u64 = INCOMPAT_EXTERNAL_DATA | INCOMPAT_COMPRESSION_TYPE | INCOMPAT_EXTENDED_L2;
fn be_u32(data: &[u8], off: usize) -> u32 {
let mut b = [0u8; 4];
if let Some(s) = data.get(off..off + 4) {
b.copy_from_slice(s);
}
u32::from_be_bytes(b)
}
fn be_u64(data: &[u8], off: usize) -> u64 {
let mut b = [0u8; 8];
if let Some(s) = data.get(off..off + 8) {
b.copy_from_slice(s);
}
u64::from_be_bytes(b)
}
pub struct Qcow2Header {
pub cluster_bits: u32, pub disk_size: u64, pub l1_size: u32, pub l1_table_offset: u64,
}
impl Qcow2Header {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < MIN_HEADER_SIZE {
return Err(Qcow2Error::FileTooSmall);
}
let magic = be_u32(data, 0);
if magic != MAGIC {
return Err(Qcow2Error::BadMagic);
}
let version = be_u32(data, 4);
if !(2..=3).contains(&version) {
return Err(Qcow2Error::UnsupportedVersion(version));
}
let backing_file_offset = be_u64(data, 8);
if backing_file_offset != 0 {
return Err(Qcow2Error::BackingFileNotSupported);
}
let cluster_bits = be_u32(data, 20);
if !(9..=20).contains(&cluster_bits) {
return Err(Qcow2Error::ClusterBitsOutOfRange(cluster_bits));
}
let disk_size = be_u64(data, 24);
let encryption_method = be_u32(data, 32);
if encryption_method != 0 {
return Err(Qcow2Error::EncryptedNotSupported);
}
let l1_size = be_u32(data, 36);
let l1_table_offset = be_u64(data, 40);
if version == 3 && data.len() >= 80 {
let incompat = be_u64(data, 72);
let unsupported = incompat & INCOMPAT_UNSUPPORTED;
if unsupported != 0 {
return Err(Qcow2Error::UnsupportedIncompatibleFeatures(unsupported));
}
}
Ok(Qcow2Header { cluster_bits, disk_size, l1_size, l1_table_offset })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Qcow2Info {
pub version: u32,
pub cluster_bits: u32,
pub virtual_disk_size: u64,
pub l1_size: u32,
pub has_backing_file: bool,
pub encryption_method: u32,
pub snapshot_count: u32,
pub incompatible_features: u64,
pub backing_file: Option<String>,
pub backing_format: Option<String>,
}
impl Qcow2Info {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < MIN_HEADER_SIZE {
return Err(Qcow2Error::FileTooSmall);
}
let magic = be_u32(data, 0);
if magic != MAGIC {
return Err(Qcow2Error::BadMagic);
}
let version = be_u32(data, 4);
if version == 1 {
return Ok(Qcow2Info {
version,
cluster_bits: 0,
virtual_disk_size: be_u64(data, 24),
l1_size: 0,
has_backing_file: be_u64(data, 8) != 0,
encryption_method: be_u32(data, 36),
snapshot_count: 0,
incompatible_features: 0,
backing_file: backing_file_name(data),
backing_format: None,
});
}
if !(2..=3).contains(&version) {
return Err(Qcow2Error::UnsupportedVersion(version));
}
let has_backing_file = be_u64(data, 8) != 0;
let cluster_bits = be_u32(data, 20);
let virtual_disk_size = be_u64(data, 24);
let encryption_method = be_u32(data, 32);
let l1_size = be_u32(data, 36);
let snapshot_count = be_u32(data, 60);
let incompatible_features = if version == 3 && data.len() >= 80 {
be_u64(data, 72)
} else {
0
};
let backing_file = backing_file_name(data);
let backing_format = backing_format_name(data, version);
Ok(Qcow2Info {
version,
cluster_bits,
virtual_disk_size,
l1_size,
has_backing_file,
encryption_method,
snapshot_count,
incompatible_features,
backing_file,
backing_format,
})
}
}
const EXT_BACKING_FORMAT: u32 = 0xE279_2ACA;
const EXT_END: u32 = 0x0000_0000;
const MAX_BACKING_LEN: usize = 4096;
fn backing_file_name(data: &[u8]) -> Option<String> {
let offset = be_u64(data, 8);
let size = be_u32(data, 16) as usize;
if offset == 0 || size == 0 {
return None;
}
let size = size.min(MAX_BACKING_LEN);
let start = usize::try_from(offset).ok()?;
let end = start.checked_add(size)?;
let bytes = data.get(start..end)?;
Some(String::from_utf8_lossy(bytes).into_owned())
}
fn backing_format_name(data: &[u8], version: u32) -> Option<String> {
let mut pos = if version == 3 {
(be_u32(data, 100) as usize).max(MIN_HEADER_SIZE)
} else {
MIN_HEADER_SIZE
};
for _ in 0..64 {
let ext_type = be_u32(data, pos);
let ext_len = be_u32(data, pos + 4) as usize;
if ext_type == EXT_END {
return None;
}
let data_start = pos.checked_add(8)?;
let data_end = data_start.checked_add(ext_len)?;
if ext_type == EXT_BACKING_FORMAT {
let len = ext_len.min(MAX_BACKING_LEN);
let slice_end = data_start.checked_add(len)?;
let bytes = data.get(data_start..slice_end)?;
return Some(String::from_utf8_lossy(bytes).into_owned());
}
let padded = ext_len.checked_add(7)? & !7usize;
pos = data_start.checked_add(padded)?;
if data_end > data.len() || pos >= data.len() {
return None;
}
}
None
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn build(
version: u32,
backing_off: u64,
backing_name: &[u8],
header_len: u32,
extensions: &[u8],
) -> Vec<u8> {
let cap = (header_len as usize)
.max(backing_off as usize + backing_name.len())
.max(112)
+ extensions.len()
+ 64;
let mut d = vec![0u8; cap];
d[0..4].copy_from_slice(&MAGIC.to_be_bytes());
d[4..8].copy_from_slice(&version.to_be_bytes());
d[8..16].copy_from_slice(&backing_off.to_be_bytes());
d[16..20].copy_from_slice(&(backing_name.len() as u32).to_be_bytes());
d[20..24].copy_from_slice(&16u32.to_be_bytes()); if version == 3 {
d[100..104].copy_from_slice(&header_len.to_be_bytes());
}
let ext_start = if version == 3 { header_len as usize } else { MIN_HEADER_SIZE };
d[ext_start..ext_start + extensions.len()].copy_from_slice(extensions);
if backing_off != 0 {
let s = backing_off as usize;
d[s..s + backing_name.len()].copy_from_slice(backing_name);
}
d
}
fn ext(ty: u32, payload: &[u8]) -> Vec<u8> {
let mut e = Vec::new();
e.extend_from_slice(&ty.to_be_bytes());
e.extend_from_slice(&(payload.len() as u32).to_be_bytes());
e.extend_from_slice(payload);
let pad = (8 - (payload.len() % 8)) % 8;
e.extend(std::iter::repeat_n(0u8, pad));
e
}
#[test]
fn no_backing_file_yields_none() {
let mut d = vec![0u8; 112];
d[0..4].copy_from_slice(&MAGIC.to_be_bytes());
d[4..8].copy_from_slice(&3u32.to_be_bytes());
let info = Qcow2Info::parse(&d).unwrap();
assert!(info.backing_file.is_none());
assert!(info.backing_format.is_none());
}
#[test]
fn backing_file_name_is_extracted() {
let name = b"base.qcow2";
let mut endmark = Vec::new();
endmark.extend_from_slice(&ext(EXT_END, &[]));
let d = build(3, 200, name, 112, &endmark);
let info = Qcow2Info::parse(&d).unwrap();
assert_eq!(info.backing_file.as_deref(), Some("base.qcow2"));
assert!(info.backing_format.is_none(), "no format extension present");
}
#[test]
fn backing_format_extension_is_extracted() {
let mut exts = Vec::new();
exts.extend_from_slice(&ext(EXT_BACKING_FORMAT, b"qcow2"));
exts.extend_from_slice(&ext(EXT_END, &[]));
let d = build(3, 200, b"base.qcow2", 112, &exts);
let info = Qcow2Info::parse(&d).unwrap();
assert_eq!(info.backing_format.as_deref(), Some("qcow2"));
assert_eq!(info.backing_file.as_deref(), Some("base.qcow2"));
}
#[test]
fn format_extension_after_an_unknown_extension() {
let mut exts = Vec::new();
exts.extend_from_slice(&ext(0x1234_5678, b"ignored payload"));
exts.extend_from_slice(&ext(EXT_BACKING_FORMAT, b"raw"));
exts.extend_from_slice(&ext(EXT_END, &[]));
let d = build(3, 0, b"", 112, &exts);
let info = Qcow2Info::parse(&d).unwrap();
assert_eq!(info.backing_format.as_deref(), Some("raw"));
}
#[test]
fn v2_extensions_begin_at_offset_72() {
let mut exts = Vec::new();
exts.extend_from_slice(&ext(EXT_BACKING_FORMAT, b"raw"));
exts.extend_from_slice(&ext(EXT_END, &[]));
let d = build(2, 0, b"", 72, &exts);
let info = Qcow2Info::parse(&d).unwrap();
assert_eq!(info.backing_format.as_deref(), Some("raw"));
}
#[test]
fn backing_offset_past_window_yields_none() {
let mut d = vec![0u8; 112];
d[0..4].copy_from_slice(&MAGIC.to_be_bytes());
d[4..8].copy_from_slice(&3u32.to_be_bytes());
d[8..16].copy_from_slice(&100_000u64.to_be_bytes()); d[16..20].copy_from_slice(&10u32.to_be_bytes());
let info = Qcow2Info::parse(&d).unwrap();
assert!(info.backing_file.is_none());
}
#[test]
fn oversized_backing_size_is_capped_not_panicking() {
let name = b"x".repeat(50);
let mut d = build(3, 200, &name, 112, &ext(EXT_END, &[]));
d[16..20].copy_from_slice(&u32::MAX.to_be_bytes());
let info = Qcow2Info::parse(&d).unwrap();
assert!(info.backing_file.is_some() || info.backing_file.is_none());
}
#[test]
fn qcow1_version_is_reported_leniently() {
let mut d = vec![0u8; 72];
d[0..4].copy_from_slice(&MAGIC.to_be_bytes());
d[4..8].copy_from_slice(&1u32.to_be_bytes()); d[8..16].copy_from_slice(&0u64.to_be_bytes()); d[24..32].copy_from_slice(&(4u64 << 20).to_be_bytes()); let info = Qcow2Info::parse(&d).unwrap();
assert_eq!(info.version, 1);
assert_eq!(info.virtual_disk_size, 4 << 20);
assert!(!info.has_backing_file);
assert!(info.backing_format.is_none());
}
#[test]
fn unsupported_version_still_errors() {
let mut d = vec![0u8; 72];
d[0..4].copy_from_slice(&MAGIC.to_be_bytes());
d[4..8].copy_from_slice(&7u32.to_be_bytes());
assert!(matches!(
Qcow2Info::parse(&d),
Err(Qcow2Error::UnsupportedVersion(7))
));
}
#[test]
fn no_end_marker_stops_at_window_end() {
let exts = ext(0x1111_1111, b"abcdefgh");
let d = build(3, 0, b"", 112, &exts);
let info = Qcow2Info::parse(&d).unwrap();
assert!(info.backing_format.is_none());
}
}