use std::collections::HashMap;
use super::format::FormatOpts;
use crate::fs::{FsSizePlan, split_parent_name};
const MFT_RECORD: u64 = 1024;
const SYSTEM_RECORDS: u64 = 16;
const INITIAL_MFT_CLUSTERS: u64 = 16;
const BUILD_HEADROOM_CLUSTERS: u64 = INITIAL_MFT_CLUSTERS;
const LOGFILE_BYTES: u64 = 64 * 1024;
const UPCASE_BYTES: u64 = 128 * 1024;
#[derive(Default, Clone, Copy)]
struct Dir {
index_bytes: u64,
}
pub struct NtfsSizePlan {
cluster_size: u64,
mft_records: u64,
data_clusters: u64,
dirs: HashMap<String, Dir>,
}
impl NtfsSizePlan {
const MAX_INDEX_ROOT_BYTES: u64 = 512;
#[must_use]
pub fn new(opts: &FormatOpts) -> Self {
let cluster_size = u64::from(opts.bytes_per_sector) * u64::from(opts.sectors_per_cluster);
let mut dirs = HashMap::new();
dirs.insert("/".to_string(), Dir::default());
Self {
cluster_size: cluster_size.max(512),
mft_records: SYSTEM_RECORDS,
data_clusters: 0,
dirs,
}
}
fn round8(n: u64) -> u64 {
(n + 7) & !7
}
fn name_units(name: &str) -> u64 {
name.encode_utf16().count() as u64
}
fn file_name_value_len(name_units: u64) -> u64 {
66 + 2 * name_units
}
fn index_entry_size(name_units: u64) -> u64 {
Self::round8(16 + Self::file_name_value_len(name_units))
}
fn resident_budget(name_units: u64) -> u64 {
let fn_attr = 24 + Self::round8(Self::file_name_value_len(name_units));
MFT_RECORD.saturating_sub(64 + 96 + fn_attr + 24 + 16)
}
fn charge_parent(&mut self, path: &str) {
let (parent, name) = split_parent_name(path);
let entry = Self::index_entry_size(Self::name_units(name));
self.dirs.entry(parent.to_string()).or_default().index_bytes += entry;
}
fn dir_index_clusters(&self, d: &Dir) -> u64 {
if d.index_bytes <= Self::MAX_INDEX_ROOT_BYTES {
return 0;
}
let leaf_usable = self.cluster_size.saturating_sub(64 + 16) * 7 / 8;
let leaves = d.index_bytes.div_ceil(leaf_usable.max(1)).max(1);
let mut internal = 0u64;
let mut level = leaves;
while level > 1 {
level = level.div_ceil(28);
internal += level;
}
leaves + internal
}
fn mft_clusters(&self) -> u64 {
let recs_per_cluster = self.cluster_size / MFT_RECORD;
let mut clusters = INITIAL_MFT_CLUSTERS;
while clusters * recs_per_cluster < self.mft_records {
clusters *= 2;
}
clusters
}
fn mft_bitmap_clusters(&self) -> u64 {
let recs = self.mft_clusters() * (self.cluster_size / MFT_RECORD);
(recs.div_ceil(8).div_ceil(self.cluster_size)).max(1) * 2
}
fn clusters_for(&self, len: u64) -> u64 {
len.div_ceil(self.cluster_size)
}
fn bitmap_clusters(&self, total_clusters: u64) -> u64 {
total_clusters.div_ceil(8).div_ceil(self.cluster_size)
}
}
impl FsSizePlan for NtfsSizePlan {
fn add_dir(&mut self, path: &str) {
self.charge_parent(path);
self.mft_records += 1;
self.dirs.entry(path.to_string()).or_default();
}
fn add_file(&mut self, path: &str, len: u64) {
self.charge_parent(path);
self.mft_records += 1;
let name = path.rsplit('/').next().unwrap_or("");
if len > Self::resident_budget(Self::name_units(name)) {
self.data_clusters += self.clusters_for(len);
}
}
fn add_symlink(&mut self, path: &str, _target: &str) {
self.charge_parent(path);
self.mft_records += 1;
}
fn add_device(&mut self, path: &str) {
self.charge_parent(path);
self.mft_records += 1;
}
fn total_size(&self) -> u64 {
let dir_index: u64 = self.dirs.values().map(|d| self.dir_index_clusters(d)).sum();
let fixed = 1
+ 1
+ LOGFILE_BYTES.div_ceil(self.cluster_size)
+ 1
+ UPCASE_BYTES.div_ceil(self.cluster_size)
+ 1;
let core = fixed
+ self.mft_clusters()
+ self.mft_bitmap_clusters()
+ self.data_clusters
+ dir_index;
let mut total = core + self.bitmap_clusters(core + 1);
for _ in 0..8 {
let next = core + self.bitmap_clusters(total);
if next <= total {
break;
}
total = next;
}
(total + BUILD_HEADROOM_CLUSTERS + 1) * self.cluster_size
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_volume_is_small() {
let p = NtfsSizePlan::new(&FormatOpts::default());
let mib = p.total_size() / (1024 * 1024);
assert!(mib < 4, "empty NTFS plan should be < 4 MiB, got {mib}");
}
#[test]
fn small_files_stay_resident() {
let mut p = NtfsSizePlan::new(&FormatOpts::default());
for i in 0..50 {
p.add_file(&format!("/f{i}"), 100);
}
assert_eq!(p.data_clusters, 0, "tiny files should be resident");
}
#[test]
fn large_file_adds_clusters() {
let mut p = NtfsSizePlan::new(&FormatOpts::default());
p.add_file("/big", 1_000_000);
assert_eq!(p.data_clusters, 1_000_000u64.div_ceil(4096));
}
#[test]
fn many_records_double_the_mft() {
let mut p = NtfsSizePlan::new(&FormatOpts::default());
let base = p.mft_clusters();
for i in 0..5000 {
p.add_file(&format!("/file_{i:05}.txt"), 0);
}
assert!(p.mft_clusters() > base);
assert_eq!(p.mft_clusters(), 2048);
}
#[test]
fn big_directory_promotes() {
let mut p = NtfsSizePlan::new(&FormatOpts::default());
for i in 0..400 {
p.add_file(&format!("/file_{i:04}"), 0);
}
let root = p.dirs.get("/").unwrap();
assert!(
p.dir_index_clusters(root) > 0,
"400 children should promote"
);
}
#[test]
fn monotonic_in_size() {
let small = {
let mut p = NtfsSizePlan::new(&FormatOpts::default());
p.add_file("/a", 5000);
p.total_size()
};
let big = {
let mut p = NtfsSizePlan::new(&FormatOpts::default());
p.add_file("/a", 50_000_000);
p.total_size()
};
assert!(big > small);
}
}