use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use crate::error::{Qcow2Error, Result};
const MAX_L1_ENTRIES: usize = 1 << 20;
const MAX_CLUSTERS_SCANNED: u64 = 16 << 20;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Qcow2RefcountReport {
pub refcount_order: u32,
pub refcount_table_offset: u64,
pub refcount_table_clusters: u32,
pub allocated_clusters: u64,
pub orphan_clusters: u64,
}
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)
}
fn read_u64_table(file: &mut File, offset: u64, n: usize) -> Vec<u64> {
if n == 0 || offset == 0 {
return Vec::new();
}
if file.seek(SeekFrom::Start(offset)).is_err() {
return Vec::new();
}
let mut buf = vec![0u8; n * 8];
let mut filled = 0;
while filled < buf.len() {
match file.read(&mut buf[filled..]) {
Ok(0) | Err(_) => break,
Ok(k) => filled += k,
}
}
buf[..filled]
.chunks_exact(8)
.map(|c| {
let mut a = [0u8; 8];
a.copy_from_slice(c);
u64::from_be_bytes(a)
})
.collect()
}
fn refcount_of(
file: &mut File,
refcount_table: &[u64],
cluster_idx: u64,
refcount_bits: u32,
cluster_size: u64,
) -> Option<u64> {
let refcounts_per_block = (cluster_size * 8) / u64::from(refcount_bits);
if refcounts_per_block == 0 {
return None;
}
let table_idx = (cluster_idx / refcounts_per_block) as usize;
let block_idx = cluster_idx % refcounts_per_block;
let entry = *refcount_table.get(table_idx)?;
let block_offset = entry & !(cluster_size - 1);
if block_offset == 0 {
return Some(0);
}
let bit_off = block_idx * u64::from(refcount_bits);
let byte_off = block_offset + (bit_off / 8);
match refcount_bits {
8 => {
let mut b = [0u8; 1];
read_at(file, byte_off, &mut b)?;
Some(u64::from(b[0]))
}
16 => {
let mut b = [0u8; 2];
read_at(file, byte_off, &mut b)?;
Some(u64::from(u16::from_be_bytes(b)))
}
32 => {
let mut b = [0u8; 4];
read_at(file, byte_off, &mut b)?;
Some(u64::from(u32::from_be_bytes(b)))
}
64 => {
let mut b = [0u8; 8];
read_at(file, byte_off, &mut b)?;
Some(u64::from_be_bytes(b))
}
bits @ (1 | 2 | 4) => {
let mut b = [0u8; 1];
read_at(file, byte_off, &mut b)?;
let within = (bit_off % 8) as u32;
let mask = (1u64 << bits) - 1;
let shift = 8 - within - bits;
Some((u64::from(b[0]) >> shift) & mask)
}
_ => None,
}
}
fn read_at(file: &mut File, offset: u64, buf: &mut [u8]) -> Option<()> {
file.seek(SeekFrom::Start(offset)).ok()?;
file.read_exact(buf).ok()?;
Some(())
}
pub fn refcount_report(path: &Path) -> Result<Qcow2RefcountReport> {
let mut file = File::open(path)?;
let mut hdr = [0u8; 104];
let n = file.read(&mut hdr)?;
let hdr = &hdr[..n];
if hdr.len() < crate::header::MIN_HEADER_SIZE {
return Err(Qcow2Error::FileTooSmall);
}
if be_u32(hdr, 0) != crate::header::MAGIC {
return Err(Qcow2Error::BadMagic);
}
let version = be_u32(hdr, 4);
if !(2..=3).contains(&version) {
return Err(Qcow2Error::UnsupportedVersion(version));
}
let cluster_bits = be_u32(hdr, 20);
if !(9..=20).contains(&cluster_bits) {
return Err(Qcow2Error::ClusterBitsOutOfRange(cluster_bits));
}
let cluster_size = 1u64 << cluster_bits;
let l1_size = be_u32(hdr, 36) as usize;
let l1_table_offset = be_u64(hdr, 40);
let refcount_table_offset = be_u64(hdr, 48);
let refcount_table_clusters = be_u32(hdr, 56);
let refcount_order = if version == 3 && hdr.len() >= 100 {
be_u32(hdr, 96)
} else {
4
};
if refcount_order > 6 {
return Ok(Qcow2RefcountReport {
refcount_order,
refcount_table_offset,
refcount_table_clusters,
allocated_clusters: 0,
orphan_clusters: 0,
});
}
let refcount_bits = 1u32 << refcount_order;
let rt_entries = (u64::from(refcount_table_clusters) * cluster_size / 8) as usize;
let rt_entries = rt_entries.min(MAX_L1_ENTRIES);
let refcount_table = read_u64_table(&mut file, refcount_table_offset, rt_entries);
let l1_entries = l1_size.min(MAX_L1_ENTRIES);
let l1_table = read_u64_table(&mut file, l1_table_offset, l1_entries);
let l2_entries_per_table = cluster_size / 8;
let mut allocated_clusters: u64 = 0;
let mut orphan_clusters: u64 = 0;
'outer: for &l1_entry in &l1_table {
let l2_offset = l1_entry & 0x00ff_ffff_ffff_fe00;
if l2_offset == 0 {
continue;
}
let l2 = read_u64_table(&mut file, l2_offset, l2_entries_per_table as usize);
for &l2_entry in &l2 {
if l2_entry & (1 << 62) != 0 {
continue;
}
if l2_entry & 1 != 0 {
continue;
}
let host_offset = l2_entry & 0x00ff_ffff_ffff_fe00;
if host_offset == 0 {
continue; }
allocated_clusters += 1;
if allocated_clusters > MAX_CLUSTERS_SCANNED {
break 'outer;
}
let cluster_idx = host_offset / cluster_size;
if let Some(0) = refcount_of(
&mut file,
&refcount_table,
cluster_idx,
refcount_bits,
cluster_size,
) {
orphan_clusters += 1;
}
}
}
Ok(Qcow2RefcountReport {
refcount_order,
refcount_table_offset,
refcount_table_clusters,
allocated_clusters,
orphan_clusters,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::io::Write;
const CB: u32 = 9; const CS: u64 = 1 << CB;
fn write_tmp(data: &[u8]) -> tempfile::NamedTempFile {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(data).unwrap();
f
}
fn build(refcount_value: u16) -> Vec<u8> {
let l1_off = CS;
let rt_off = CS * 2;
let block_off = CS * 3;
let l2_off = CS * 4;
let data_off = CS * 5;
let mut img = vec![0u8; (CS * 6) as usize];
img[0..4].copy_from_slice(&crate::header::MAGIC.to_be_bytes());
img[4..8].copy_from_slice(&3u32.to_be_bytes());
img[20..24].copy_from_slice(&CB.to_be_bytes());
img[24..32].copy_from_slice(&CS.to_be_bytes()); img[36..40].copy_from_slice(&1u32.to_be_bytes()); img[40..48].copy_from_slice(&l1_off.to_be_bytes());
img[48..56].copy_from_slice(&rt_off.to_be_bytes());
img[56..60].copy_from_slice(&1u32.to_be_bytes()); img[96..100].copy_from_slice(&4u32.to_be_bytes()); img[100..104].copy_from_slice(&104u32.to_be_bytes());
let l1e = l2_off | (1 << 63);
img[l1_off as usize..l1_off as usize + 8].copy_from_slice(&l1e.to_be_bytes());
img[rt_off as usize..rt_off as usize + 8].copy_from_slice(&block_off.to_be_bytes());
let data_cluster_idx = (data_off / CS) as usize;
let block_entry = block_off as usize + data_cluster_idx * 2; img[block_entry..block_entry + 2].copy_from_slice(&refcount_value.to_be_bytes());
let l2e = data_off | (1 << 63);
img[l2_off as usize..l2_off as usize + 8].copy_from_slice(&l2e.to_be_bytes());
img
}
#[test]
fn referenced_cluster_is_not_an_orphan() {
let f = write_tmp(&build(1));
let r = refcount_report(f.path()).unwrap();
assert_eq!(r.refcount_order, 4);
assert_eq!(r.allocated_clusters, 1);
assert_eq!(r.orphan_clusters, 0);
}
#[test]
fn refcount_zero_cluster_is_flagged_as_orphan() {
let f = write_tmp(&build(0));
let r = refcount_report(f.path()).unwrap();
assert_eq!(r.allocated_clusters, 1);
assert_eq!(r.orphan_clusters, 1, "refcount-0 allocated cluster is orphaned");
}
#[test]
fn missing_refcount_block_means_orphan() {
let mut img = build(7);
let rt_off = (CS * 2) as usize;
img[rt_off..rt_off + 8].copy_from_slice(&0u64.to_be_bytes());
let f = write_tmp(&img);
let r = refcount_report(f.path()).unwrap();
assert_eq!(r.orphan_clusters, 1);
}
#[test]
fn bad_magic_is_rejected() {
let mut img = build(1);
img[0] = 0;
let f = write_tmp(&img);
assert!(matches!(refcount_report(f.path()), Err(Qcow2Error::BadMagic)));
}
#[test]
fn too_small_is_rejected() {
let f = write_tmp(&[0u8; 8]);
assert!(matches!(
refcount_report(f.path()),
Err(Qcow2Error::FileTooSmall)
));
}
#[test]
fn bad_version_is_rejected() {
let mut img = build(1);
img[4..8].copy_from_slice(&7u32.to_be_bytes());
let f = write_tmp(&img);
assert!(matches!(
refcount_report(f.path()),
Err(Qcow2Error::UnsupportedVersion(7))
));
}
#[test]
fn bad_cluster_bits_is_rejected() {
let mut img = build(1);
img[20..24].copy_from_slice(&30u32.to_be_bytes());
let f = write_tmp(&img);
assert!(matches!(
refcount_report(f.path()),
Err(Qcow2Error::ClusterBitsOutOfRange(30))
));
}
#[test]
fn oversized_refcount_order_is_reported_without_scanning() {
let mut img = build(1);
img[96..100].copy_from_slice(&9u32.to_be_bytes()); let f = write_tmp(&img);
let r = refcount_report(f.path()).unwrap();
assert_eq!(r.refcount_order, 9);
assert_eq!(r.allocated_clusters, 0);
assert_eq!(r.orphan_clusters, 0);
}
#[test]
fn compressed_and_zero_clusters_are_skipped() {
let mut img = build(1);
let l2_off = (CS * 4) as usize;
let comp = (1u64 << 62) | 0x1234;
img[l2_off..l2_off + 8].copy_from_slice(&comp.to_be_bytes());
let f = write_tmp(&img);
let r = refcount_report(f.path()).unwrap();
assert_eq!(r.allocated_clusters, 0, "compressed cluster is not counted");
}
#[test]
fn zero_flag_cluster_is_skipped() {
let mut img = build(1);
let l2_off = (CS * 4) as usize;
img[l2_off..l2_off + 8].copy_from_slice(&1u64.to_be_bytes()); let f = write_tmp(&img);
let r = refcount_report(f.path()).unwrap();
assert_eq!(r.allocated_clusters, 0);
}
#[test]
fn v2_defaults_to_order_four() {
let mut img = build(1);
img[4..8].copy_from_slice(&2u32.to_be_bytes()); img[96..100].copy_from_slice(&0u32.to_be_bytes()); let f = write_tmp(&img);
let r = refcount_report(f.path()).unwrap();
assert_eq!(r.refcount_order, 4);
}
#[test]
fn open_nonexistent_is_io_error() {
assert!(refcount_report(Path::new("/tmp/no_such_qcow2_rc.qcow2")).is_err());
}
}