use super::writer::HfsFormatOpts;
use crate::fs::FsSizePlan;
const NODE: u64 = 512;
const DESC: u64 = 14;
const DIR_BODY: u64 = 70;
const FILE_BODY: u64 = 102;
const THREAD_BODY: u64 = 46;
const CAT_KEY_LEN_MAX: u64 = 37;
const BITS_PER_VBM_SECTOR: u64 = 4096;
fn round_up_even(n: u64) -> u64 {
n + (n & 1)
}
fn record_cost(name_len: u64, body: u64) -> u64 {
let key = round_up_even(7 + name_len);
round_up_even(key + body) + 2
}
fn auto_block_size(total_sectors: u64) -> u64 {
let mut bs = 512u64;
while total_sectors / (bs / 512) > 60_000 {
bs += 512;
}
bs
}
pub struct HfsClassicSizePlan {
catalog_bytes: u64,
file_lens: Vec<u64>,
}
impl HfsClassicSizePlan {
#[must_use]
pub fn new(_opts: &HfsFormatOpts) -> Self {
let mut p = Self {
catalog_bytes: 0,
file_lens: Vec::new(),
};
p.add_dir_records(0);
p
}
fn key_name_len(name: &str) -> u64 {
(name.chars().count() as u64).min(31)
}
fn leaf_name_of(path: &str) -> u64 {
Self::key_name_len(path.rsplit('/').next().unwrap_or(""))
}
fn add_dir_records(&mut self, name_len: u64) {
self.catalog_bytes += record_cost(name_len, DIR_BODY);
self.catalog_bytes += record_cost(0, THREAD_BODY);
}
fn add_file_record(&mut self, name_len: u64) {
self.catalog_bytes += record_cost(name_len, FILE_BODY);
}
fn catalog_nodes(&self) -> u64 {
const MAX_RECORD: u64 = 144;
let usable = NODE - DESC - 2 - MAX_RECORD;
let leaves = self.catalog_bytes.div_ceil(usable).max(1);
let _ = CAT_KEY_LEN_MAX;
let mut index = 0u64;
let mut level = leaves;
while level > 1 {
level = level.div_ceil(10);
index += level;
}
1 + leaves + index
}
fn data_blocks(&self, bs: u64) -> u64 {
self.file_lens.iter().map(|&l| l.div_ceil(bs)).sum()
}
fn content_blocks(&self, bs: u64) -> u64 {
let cat_blocks = (self.catalog_nodes() * NODE).div_ceil(bs);
let ext_blocks = NODE.div_ceil(bs); cat_blocks + ext_blocks + self.data_blocks(bs)
}
fn sectors_at(&self, bs: u64) -> u64 {
let spab = bs / 512;
let blocks = self.content_blocks(bs);
let vbm = blocks.div_ceil(BITS_PER_VBM_SECTOR);
5 + vbm + blocks * spab
}
}
impl FsSizePlan for HfsClassicSizePlan {
fn add_dir(&mut self, path: &str) {
self.add_dir_records(Self::leaf_name_of(path));
}
fn add_file(&mut self, path: &str, len: u64) {
self.add_file_record(Self::leaf_name_of(path));
self.file_lens.push(len);
}
fn add_symlink(&mut self, path: &str, target: &str) {
self.add_file_record(Self::leaf_name_of(path));
self.file_lens.push((target.len() as u64).max(1));
}
fn add_device(&mut self, path: &str) {
self.add_file_record(Self::leaf_name_of(path));
}
fn total_size(&self) -> u64 {
let mut bs = 512u64;
for _ in 0..8 {
let sectors = self.sectors_at(bs);
let next = auto_block_size(sectors);
if next == bs {
break;
}
bs = next;
}
self.sectors_at(bs) * 512
}
}
#[cfg(test)]
mod tests {
use super::*;
fn opts() -> HfsFormatOpts {
HfsFormatOpts {
volume_name: "Test".into(),
block_size: None,
}
}
#[test]
fn empty_volume_is_small() {
let p = HfsClassicSizePlan::new(&opts());
let kib = p.total_size() / 1024;
assert!(kib < 64, "empty HFS should be a few KiB, got {kib} KiB");
}
#[test]
fn files_add_data_blocks() {
let mut p = HfsClassicSizePlan::new(&opts());
p.add_file("/big", 100_000);
assert_eq!(p.data_blocks(512), 100_000u64.div_ceil(512));
}
#[test]
fn empty_files_use_no_data() {
let mut p = HfsClassicSizePlan::new(&opts());
for i in 0..10 {
p.add_file(&format!("/f{i}"), 0);
}
assert_eq!(p.data_blocks(512), 0);
}
#[test]
fn many_entries_grow_catalog() {
let mut p = HfsClassicSizePlan::new(&opts());
let base = p.catalog_nodes();
for i in 0..2000 {
p.add_file(&format!("/file_{i:05}.txt"), 0);
}
assert!(p.catalog_nodes() > base);
}
#[test]
fn monotonic_in_size() {
let small = {
let mut p = HfsClassicSizePlan::new(&opts());
p.add_file("/a", 1000);
p.total_size()
};
let big = {
let mut p = HfsClassicSizePlan::new(&opts());
p.add_file("/a", 10_000_000);
p.total_size()
};
assert!(big > small);
}
}