use std::collections::HashMap;
use super::constants::{ADDRS_PER_BLOCK, ADDRS_PER_INODE, NR_DENTRY_IN_BLOCK};
use super::dir::INLINE_DENTRY_NR;
use crate::fs::{FsSizePlan, split_parent_name};
const BLOCK: u64 = 4096;
const BLOCKS_PER_SEG: u64 = 512;
const META_BLOCKS: u64 = 2 + (2 + 2 + 2 + 1) * BLOCKS_PER_SEG;
const MAX_INLINE_DATA: u64 = 3672;
const MIN_MAIN_SEGS: u64 = 6;
#[derive(Default, Clone, Copy)]
struct Dir {
entries: u64,
slots: u64,
}
#[derive(Default)]
pub struct F2fsSizePlan {
node_blocks: u64,
data_blocks: u64,
dirs: HashMap<String, Dir>,
}
impl F2fsSizePlan {
#[must_use]
pub fn new() -> Self {
let mut dirs = HashMap::new();
dirs.insert("/".to_string(), Dir::default());
Self {
node_blocks: 1, 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.slots += (name.len() as u64).max(1).div_ceil(8);
}
fn indirect_nodes(d: u64) -> u64 {
let inline = ADDRS_PER_INODE as u64;
if d <= inline {
return 0;
}
let per = ADDRS_PER_BLOCK as u64;
let direct = (d - inline).div_ceil(per); direct + direct.div_ceil(per) + 1
}
fn dir_data_blocks(d: &Dir) -> u64 {
if d.entries <= INLINE_DENTRY_NR as u64 {
return 0;
}
d.slots.div_ceil(NR_DENTRY_IN_BLOCK as u64).max(1)
}
}
impl FsSizePlan for F2fsSizePlan {
fn add_dir(&mut self, path: &str) {
self.charge_parent(path);
self.node_blocks += 1;
self.dirs.entry(path.to_string()).or_default();
}
fn add_file(&mut self, path: &str, len: u64) {
self.charge_parent(path);
self.node_blocks += 1;
if len > MAX_INLINE_DATA {
let d = len.div_ceil(BLOCK);
self.data_blocks += d;
self.node_blocks += Self::indirect_nodes(d);
}
}
fn add_symlink(&mut self, path: &str, target: &str) {
self.charge_parent(path);
self.node_blocks += 1;
if target.len() as u64 > MAX_INLINE_DATA {
self.data_blocks += 1;
}
}
fn add_device(&mut self, path: &str) {
self.charge_parent(path);
self.node_blocks += 1; }
fn total_size(&self) -> u64 {
let dir_data: u64 = self.dirs.values().map(Self::dir_data_blocks).sum();
let node_segs = self.node_blocks.div_ceil(BLOCKS_PER_SEG);
let data_segs = (self.data_blocks + dir_data).div_ceil(BLOCKS_PER_SEG);
let main_segs = (node_segs + data_segs + 5).max(MIN_MAIN_SEGS);
(META_BLOCKS + main_segs * BLOCKS_PER_SEG) * BLOCK
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_is_meta_plus_floor() {
let p = F2fsSizePlan::new();
let mib = p.total_size() / (1024 * 1024);
assert!((26..=30).contains(&mib), "got {mib} MiB");
}
#[test]
fn small_files_and_dirs_inline() {
let mut p = F2fsSizePlan::new();
for i in 0..50 {
p.add_file(&format!("/f{i}"), 100); }
assert_eq!(p.data_blocks, 0);
assert_eq!(F2fsSizePlan::dir_data_blocks(p.dirs.get("/").unwrap()), 0);
}
#[test]
fn large_file_adds_data_and_indirect_nodes() {
let mut p = F2fsSizePlan::new();
p.add_file("/big", 8 * 1024 * 1024);
assert_eq!(p.data_blocks, (8 * 1024 * 1024u64).div_ceil(BLOCK));
assert!(p.node_blocks > 2); }
#[test]
fn big_directory_needs_dentry_blocks() {
let mut p = F2fsSizePlan::new();
for i in 0..300 {
p.add_file(&format!("/file_{i:04}"), 10);
}
assert!(F2fsSizePlan::dir_data_blocks(p.dirs.get("/").unwrap()) > 0);
}
}