use std::io::Read;
use crate::Result;
pub const MAGIC: [u8; 4] = *b"QFI\xfb";
pub const VERSION_V2: u32 = 2;
pub const VERSION_V3: u32 = 3;
pub const V2_HEADER_LEN: usize = 72;
pub const V3_HEADER_LEN: usize = 104;
pub mod incompat {
pub const DIRTY: u64 = 1 << 0;
pub const CORRUPT: u64 = 1 << 1;
pub const EXTERNAL_DATA_FILE: u64 = 1 << 2;
pub const COMPRESSION_TYPE: u64 = 1 << 3;
pub const EXTENDED_L2: u64 = 1 << 4;
}
#[derive(Debug, Clone)]
pub struct Header {
pub version: u32,
pub backing_file_offset: u64,
pub backing_file_size: u32,
pub cluster_bits: u32,
pub size: u64,
pub crypt_method: u32,
pub l1_size: u32,
pub l1_table_offset: u64,
pub refcount_table_offset: u64,
pub refcount_table_clusters: u32,
pub nb_snapshots: u32,
pub snapshots_offset: u64,
pub incompatible_features: u64,
pub compatible_features: u64,
pub autoclear_features: u64,
pub refcount_order: u32,
pub header_length: u32,
}
impl Header {
pub fn cluster_size(&self) -> u64 {
1u64 << self.cluster_bits
}
pub fn l2_entries_per_cluster(&self) -> u64 {
self.cluster_size() / 8
}
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < V2_HEADER_LEN {
return Err(crate::Error::InvalidImage(format!(
"qcow2: header buffer is {} bytes, need ≥ {V2_HEADER_LEN}",
buf.len()
)));
}
if buf[0..4] != MAGIC {
return Err(crate::Error::InvalidImage(
"qcow2: bad magic (not a qcow2 image)".into(),
));
}
let version = u32_be(buf, 4);
if version != VERSION_V2 && version != VERSION_V3 {
return Err(crate::Error::Unsupported(format!(
"qcow2: only versions 2 and 3 are supported (got {version})"
)));
}
let cluster_bits = u32_be(buf, 20);
if !(9..=21).contains(&cluster_bits) {
return Err(crate::Error::InvalidImage(format!(
"qcow2: cluster_bits {cluster_bits} out of range [9, 21]"
)));
}
let mut h = Self {
version,
backing_file_offset: u64_be(buf, 8),
backing_file_size: u32_be(buf, 16),
cluster_bits,
size: u64_be(buf, 24),
crypt_method: u32_be(buf, 32),
l1_size: u32_be(buf, 36),
l1_table_offset: u64_be(buf, 40),
refcount_table_offset: u64_be(buf, 48),
refcount_table_clusters: u32_be(buf, 56),
nb_snapshots: u32_be(buf, 60),
snapshots_offset: u64_be(buf, 64),
incompatible_features: 0,
compatible_features: 0,
autoclear_features: 0,
refcount_order: 4, header_length: V2_HEADER_LEN as u32,
};
if version == VERSION_V3 {
if buf.len() < V3_HEADER_LEN {
return Err(crate::Error::InvalidImage(format!(
"qcow2: v3 header truncated ({} bytes, need ≥ {V3_HEADER_LEN})",
buf.len()
)));
}
h.incompatible_features = u64_be(buf, 72);
h.compatible_features = u64_be(buf, 80);
h.autoclear_features = u64_be(buf, 88);
h.refcount_order = u32_be(buf, 96);
h.header_length = u32_be(buf, 100);
if h.header_length < V3_HEADER_LEN as u32 {
return Err(crate::Error::InvalidImage(format!(
"qcow2: v3 header_length {} < {V3_HEADER_LEN}",
h.header_length
)));
}
}
h.validate()?;
Ok(h)
}
fn validate(&self) -> Result<()> {
if self.backing_file_offset != 0 {
return Err(crate::Error::Unsupported(
"qcow2: backing files are not supported".into(),
));
}
if self.crypt_method != 0 {
return Err(crate::Error::Unsupported(
"qcow2: encrypted images are not supported".into(),
));
}
if self.refcount_order != 4 {
return Err(crate::Error::Unsupported(format!(
"qcow2: only refcount_order=4 (16-bit) is supported (got {})",
self.refcount_order
)));
}
let bad = self.incompatible_features
& (incompat::DIRTY
| incompat::CORRUPT
| incompat::EXTERNAL_DATA_FILE
| incompat::COMPRESSION_TYPE
| incompat::EXTENDED_L2);
if bad != 0 {
return Err(crate::Error::Unsupported(format!(
"qcow2: incompatible features {bad:#x} not supported (dirty/corrupt/external-data/compression/extended-L2)"
)));
}
let unknown = self.incompatible_features & !bad;
if unknown != 0 {
return Err(crate::Error::Unsupported(format!(
"qcow2: unknown incompatible_features {unknown:#x}"
)));
}
Ok(())
}
pub fn encode_v3(&self) -> [u8; V3_HEADER_LEN] {
let mut b = [0u8; V3_HEADER_LEN];
b[0..4].copy_from_slice(&MAGIC);
b[4..8].copy_from_slice(&VERSION_V3.to_be_bytes());
b[8..16].copy_from_slice(&self.backing_file_offset.to_be_bytes());
b[16..20].copy_from_slice(&self.backing_file_size.to_be_bytes());
b[20..24].copy_from_slice(&self.cluster_bits.to_be_bytes());
b[24..32].copy_from_slice(&self.size.to_be_bytes());
b[32..36].copy_from_slice(&self.crypt_method.to_be_bytes());
b[36..40].copy_from_slice(&self.l1_size.to_be_bytes());
b[40..48].copy_from_slice(&self.l1_table_offset.to_be_bytes());
b[48..56].copy_from_slice(&self.refcount_table_offset.to_be_bytes());
b[56..60].copy_from_slice(&self.refcount_table_clusters.to_be_bytes());
b[60..64].copy_from_slice(&self.nb_snapshots.to_be_bytes());
b[64..72].copy_from_slice(&self.snapshots_offset.to_be_bytes());
b[72..80].copy_from_slice(&self.incompatible_features.to_be_bytes());
b[80..88].copy_from_slice(&self.compatible_features.to_be_bytes());
b[88..96].copy_from_slice(&self.autoclear_features.to_be_bytes());
b[96..100].copy_from_slice(&self.refcount_order.to_be_bytes());
b[100..104].copy_from_slice(&(V3_HEADER_LEN as u32).to_be_bytes());
b
}
}
pub fn read_header_bytes(r: &mut (impl Read + ?Sized)) -> std::io::Result<[u8; V3_HEADER_LEN]> {
let mut buf = [0u8; V3_HEADER_LEN];
r.read_exact(&mut buf)?;
Ok(buf)
}
fn u32_be(buf: &[u8], off: usize) -> u32 {
u32::from_be_bytes(buf[off..off + 4].try_into().unwrap())
}
fn u64_be(buf: &[u8], off: usize) -> u64 {
u64::from_be_bytes(buf[off..off + 8].try_into().unwrap())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_v3_header() -> Header {
Header {
version: VERSION_V3,
backing_file_offset: 0,
backing_file_size: 0,
cluster_bits: 16,
size: 64 * 1024 * 1024,
crypt_method: 0,
l1_size: 1,
l1_table_offset: 3 * 65536,
refcount_table_offset: 65536,
refcount_table_clusters: 1,
nb_snapshots: 0,
snapshots_offset: 0,
incompatible_features: 0,
compatible_features: 0,
autoclear_features: 0,
refcount_order: 4,
header_length: V3_HEADER_LEN as u32,
}
}
#[test]
fn encode_decode_roundtrip_v3() {
let h = sample_v3_header();
let bytes = h.encode_v3();
let decoded = Header::decode(&bytes).unwrap();
assert_eq!(decoded.cluster_bits, 16);
assert_eq!(decoded.cluster_size(), 65536);
assert_eq!(decoded.size, 64 * 1024 * 1024);
assert_eq!(decoded.l1_size, 1);
assert_eq!(decoded.refcount_order, 4);
assert_eq!(decoded.header_length, V3_HEADER_LEN as u32);
}
#[test]
fn rejects_bad_magic() {
let mut bytes = sample_v3_header().encode_v3();
bytes[0] = b'X';
assert!(matches!(
Header::decode(&bytes),
Err(crate::Error::InvalidImage(_))
));
}
#[test]
fn rejects_compression_feature() {
let mut h = sample_v3_header();
h.incompatible_features = incompat::COMPRESSION_TYPE;
let bytes = h.encode_v3();
assert!(matches!(
Header::decode(&bytes),
Err(crate::Error::Unsupported(_))
));
}
#[test]
fn rejects_unknown_incompat_bit() {
let mut h = sample_v3_header();
h.incompatible_features = 1u64 << 30; let bytes = h.encode_v3();
assert!(matches!(
Header::decode(&bytes),
Err(crate::Error::Unsupported(_))
));
}
#[test]
fn v2_decodes_with_defaults() {
let mut b = [0u8; V2_HEADER_LEN];
b[0..4].copy_from_slice(&MAGIC);
b[4..8].copy_from_slice(&2u32.to_be_bytes());
b[20..24].copy_from_slice(&16u32.to_be_bytes()); b[24..32].copy_from_slice(&(1024u64 * 1024 * 1024).to_be_bytes());
b[36..40].copy_from_slice(&2u32.to_be_bytes()); b[40..48].copy_from_slice(&(3 * 65536u64).to_be_bytes());
b[48..56].copy_from_slice(&65536u64.to_be_bytes());
b[56..60].copy_from_slice(&1u32.to_be_bytes());
let h = Header::decode(&b).unwrap();
assert_eq!(h.version, 2);
assert_eq!(h.refcount_order, 4); assert_eq!(h.incompatible_features, 0);
}
}