use std::collections::HashMap;
use super::format::{
AG0_METADATA_BLOCKS, DEFAULT_AGBLOCKS_PER_AG, MULTI_AG_THRESHOLD_BYTES, XFS_BLOCKSIZE,
choose_agcount,
};
use crate::fs::{FsSizePlan, split_parent_name};
const BS: u64 = XFS_BLOCKSIZE as u64;
const LITERAL: u64 = 336;
const INODES_PER_CHUNK: u64 = 64;
const BLOCKS_PER_CHUNK: u64 = 8;
const LOG_BLOCKS: u64 = 512;
const AG_HEADROOM: u64 = 16;
#[derive(Default, Clone, Copy)]
struct Dir {
entries: u64,
rec_bytes: u64,
}
#[derive(Default)]
pub struct XfsSizePlan {
inodes: u64,
data_blocks: u64,
dirs: HashMap<String, Dir>,
}
impl XfsSizePlan {
#[must_use]
pub fn new() -> Self {
let mut dirs = HashMap::new();
dirs.insert("/".to_string(), Dir::default()); Self {
inodes: 0,
data_blocks: 0,
dirs,
}
}
fn charge_parent(&mut self, path: &str) {
let (parent, name) = split_parent_name(path);
let d = self.dirs.entry(parent.to_string()).or_default();
d.entries += 1;
d.rec_bytes += name.len() as u64 + 16;
}
fn dir_blocks(d: &Dir) -> u64 {
if 10 + d.rec_bytes <= LITERAL {
return 0;
}
let data = (d.rec_bytes + 64).div_ceil(BS);
let leaf = (d.entries * 8 + 256).div_ceil(BS) + 1;
data + leaf + 2 }
}
impl FsSizePlan for XfsSizePlan {
fn add_dir(&mut self, path: &str) {
self.charge_parent(path);
self.inodes += 1;
self.dirs.entry(path.to_string()).or_default();
}
fn add_file(&mut self, path: &str, len: u64) {
self.charge_parent(path);
self.inodes += 1;
if len > LITERAL {
self.data_blocks += len.div_ceil(BS);
}
}
fn add_symlink(&mut self, path: &str, target: &str) {
self.charge_parent(path);
self.inodes += 1;
if target.len() as u64 > LITERAL {
self.data_blocks += (target.len() as u64).div_ceil(BS);
}
}
fn add_device(&mut self, path: &str) {
self.charge_parent(path);
self.inodes += 1; }
fn total_size(&self) -> u64 {
let dir_blocks: u64 = self.dirs.values().map(Self::dir_blocks).sum();
let inode_chunks = (self.inodes + 1).div_ceil(INODES_PER_CHUNK);
let inode_blocks = inode_chunks * BLOCKS_PER_CHUNK;
let content = LOG_BLOCKS + inode_blocks + dir_blocks + self.data_blocks + 16;
let mut total_blocks = content + AG0_METADATA_BLOCKS as u64 + AG_HEADROOM;
for _ in 0..8 {
let agcount = u64::from(choose_agcount(total_blocks * BS));
let ag_overhead = agcount * (AG0_METADATA_BLOCKS as u64 + AG_HEADROOM);
let next = content + ag_overhead;
if next <= total_blocks {
break;
}
total_blocks = next;
}
let _ = DEFAULT_AGBLOCKS_PER_AG;
let _ = MULTI_AG_THRESHOLD_BYTES;
total_blocks * BS
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_is_log_plus_root_overhead() {
let p = XfsSizePlan::new();
let blocks = p.total_size() / BS;
assert!(blocks >= LOG_BLOCKS + BLOCKS_PER_CHUNK);
assert!(blocks < LOG_BLOCKS + 64); }
#[test]
fn small_files_are_inline_no_data_blocks() {
let mut p = XfsSizePlan::new();
for i in 0..10 {
p.add_file(&format!("/f{i}"), 100); }
assert_eq!(p.data_blocks, 0);
}
#[test]
fn large_file_adds_extent_blocks() {
let mut p = XfsSizePlan::new();
p.add_file("/big", 1_000_000);
assert_eq!(p.data_blocks, 1_000_000u64.div_ceil(BS));
}
#[test]
fn many_children_force_block_dir() {
let mut p = XfsSizePlan::new();
for i in 0..50 {
p.add_file(&format!("/longish_name_{i:03}"), 10);
}
let root = XfsSizePlan::dir_blocks(p.dirs.get("/").unwrap());
assert!(root > 0);
}
}