use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom, Write};
use crate::Result;
use super::header::Header;
pub const REFCOUNT_ENTRY_BYTES: u64 = 2;
pub struct Refcount {
pub cluster_size: u64,
pub table_offset: u64,
pub table_clusters: u32,
pub table: Vec<u64>,
pub block_cache: HashMap<u64, RefcountBlock>,
pub block_cache_cap: usize,
pub next_free_hint: u64,
pub table_dirty: bool,
}
pub struct RefcountBlock {
pub entries: Vec<u16>,
pub dirty: bool,
}
impl Refcount {
pub fn entries_per_block(&self) -> u64 {
self.cluster_size / REFCOUNT_ENTRY_BYTES
}
pub fn table_capacity(&self) -> u64 {
self.table_clusters as u64 * self.cluster_size / 8
}
pub fn load<F: Read + Seek>(file: &mut F, header: &Header) -> Result<Self> {
let cluster_size = header.cluster_size();
let table_entries = (header.refcount_table_clusters as u64 * cluster_size / 8) as usize;
let mut raw = vec![0u8; table_entries * 8];
file.seek(SeekFrom::Start(header.refcount_table_offset))?;
file.read_exact(&mut raw)?;
let table: Vec<u64> = raw
.chunks_exact(8)
.map(|c| u64::from_be_bytes(c.try_into().unwrap()))
.collect();
Ok(Self {
cluster_size,
table_offset: header.refcount_table_offset,
table_clusters: header.refcount_table_clusters,
table,
block_cache: HashMap::new(),
block_cache_cap: 32,
next_free_hint: 0,
table_dirty: false,
})
}
pub fn new_fresh(
cluster_size: u64,
table_offset: u64,
first_refcount_block_offset: u64,
initial_clusters: &[u64],
) -> Self {
let table_capacity = cluster_size / 8;
let mut table = vec![0u64; table_capacity as usize];
table[0] = first_refcount_block_offset;
let entries_per_block = cluster_size / REFCOUNT_ENTRY_BYTES;
let mut entries = vec![0u16; entries_per_block as usize];
for &c in initial_clusters {
entries[c as usize] = 1;
}
let mut block_cache = HashMap::new();
block_cache.insert(
first_refcount_block_offset,
RefcountBlock {
entries,
dirty: true,
},
);
Self {
cluster_size,
table_offset,
table_clusters: 1,
table,
block_cache,
block_cache_cap: 32,
next_free_hint: initial_clusters.iter().copied().max().unwrap_or(0) + 1,
table_dirty: true,
}
}
fn load_block<F: Read + Seek>(
&mut self,
file: &mut F,
block_off: u64,
) -> Result<&mut RefcountBlock> {
if !self.block_cache.contains_key(&block_off) {
file.seek(SeekFrom::Start(block_off))?;
let mut raw = vec![0u8; self.cluster_size as usize];
file.read_exact(&mut raw)?;
let entries: Vec<u16> = raw
.chunks_exact(2)
.map(|c| u16::from_be_bytes(c.try_into().unwrap()))
.collect();
if self.block_cache.len() >= self.block_cache_cap
&& let Some(k) = self
.block_cache
.iter()
.find(|(_, v)| !v.dirty)
.map(|(k, _)| *k)
{
self.block_cache.remove(&k);
}
self.block_cache.insert(
block_off,
RefcountBlock {
entries,
dirty: false,
},
);
}
Ok(self.block_cache.get_mut(&block_off).unwrap())
}
pub fn alloc_cluster<F: Read + Write + Seek>(
&mut self,
file: &mut F,
file_len: &mut u64,
) -> Result<u64> {
let hint_block = self.next_free_hint / self.entries_per_block();
let table_len = self.table.len();
for sweep in 0..2 {
let (lo, hi) = if sweep == 0 {
(hint_block as usize, table_len)
} else {
(0, hint_block as usize)
};
for block_idx in lo..hi {
let block_off = self.table[block_idx];
if block_off == 0 {
continue;
}
let entries_per_block = self.entries_per_block();
let _block = self.load_block(file, block_off)?;
let start_in_block = if sweep == 0 && block_idx == hint_block as usize {
(self.next_free_hint % entries_per_block) as usize
} else {
0
};
let block = self.block_cache.get_mut(&block_off).unwrap();
for (i, slot) in block.entries.iter_mut().enumerate().skip(start_in_block) {
if *slot == 0 {
*slot = 1;
block.dirty = true;
let cluster = block_idx as u64 * entries_per_block + i as u64;
self.next_free_hint = cluster + 1;
return Ok(cluster);
}
}
}
}
let new_cluster = *file_len / self.cluster_size;
let entries_per_block = self.entries_per_block();
let block_idx = (new_cluster / entries_per_block) as usize;
if block_idx >= table_len {
return Err(crate::Error::Unsupported(
"qcow2: refcount table is full (image would exceed 16 PiB at default cluster size)"
.into(),
));
}
let block_off = self.table[block_idx];
if block_off == 0 {
let rcb_cluster = new_cluster;
let data_cluster = new_cluster + 1;
let next_block_idx = (data_cluster / entries_per_block) as usize;
if next_block_idx != block_idx {
return Err(crate::Error::Unsupported(
"qcow2: allocation across refcount-block boundary not implemented".into(),
));
}
let rcb_off = rcb_cluster * self.cluster_size;
let data_off = data_cluster * self.cluster_size;
let mut entries = vec![0u16; entries_per_block as usize];
entries[(rcb_cluster % entries_per_block) as usize] = 1;
entries[(data_cluster % entries_per_block) as usize] = 1;
self.block_cache.insert(
rcb_off,
RefcountBlock {
entries,
dirty: true,
},
);
self.table[block_idx] = rcb_off;
self.table_dirty = true;
*file_len = data_off + self.cluster_size;
self.next_free_hint = data_cluster + 1;
return Ok(data_cluster);
}
let _block = self.load_block(file, block_off)?;
let block = self.block_cache.get_mut(&block_off).unwrap();
let idx = (new_cluster % entries_per_block) as usize;
block.entries[idx] = 1;
block.dirty = true;
*file_len = (new_cluster + 1) * self.cluster_size;
self.next_free_hint = new_cluster + 1;
Ok(new_cluster)
}
pub fn flush<F: Write + Seek>(&mut self, file: &mut F) -> Result<()> {
for (&off, block) in self.block_cache.iter_mut() {
if !block.dirty {
continue;
}
let mut raw = vec![0u8; self.cluster_size as usize];
for (i, &e) in block.entries.iter().enumerate() {
raw[i * 2..i * 2 + 2].copy_from_slice(&e.to_be_bytes());
}
file.seek(SeekFrom::Start(off))?;
file.write_all(&raw)?;
block.dirty = false;
}
if self.table_dirty {
let mut raw = vec![0u8; self.table.len() * 8];
for (i, &e) in self.table.iter().enumerate() {
raw[i * 8..i * 8 + 8].copy_from_slice(&e.to_be_bytes());
}
file.seek(SeekFrom::Start(self.table_offset))?;
file.write_all(&raw)?;
self.table_dirty = false;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn fresh_state_marks_initial_clusters() {
let r = Refcount::new_fresh(512, 512, 1024, &[0, 1, 2, 3]);
assert_eq!(r.table[0], 1024);
let block = r.block_cache.get(&1024).unwrap();
assert_eq!(block.entries[0], 1);
assert_eq!(block.entries[1], 1);
assert_eq!(block.entries[2], 1);
assert_eq!(block.entries[3], 1);
assert_eq!(block.entries[4], 0);
assert_eq!(r.next_free_hint, 4);
}
#[test]
fn alloc_picks_first_free_in_existing_block() {
let mut r = Refcount::new_fresh(512, 512, 1024, &[0, 1, 2, 3]);
let mut file_len = 4 * 512;
let mut buf: Vec<u8> = Vec::new();
let mut cur = Cursor::new(&mut buf);
let c = r.alloc_cluster(&mut cur, &mut file_len).unwrap();
assert_eq!(c, 4);
let block = r.block_cache.get(&1024).unwrap();
assert_eq!(block.entries[4], 1);
}
}