use std::collections::HashMap;
use super::dir::{ENTRY_SIZE, LFN_CHARS_PER_ENTRY, is_valid_83};
use super::{Fat32, MIN_FAT32_CLUSTERS, SECTOR};
use crate::fs::{FsSizePlan, split_parent_name};
const RESERVED_SECTORS: u32 = 32;
const NUM_FATS: u32 = 2;
const ENTRIES_PER_FAT_SECTOR: u32 = SECTOR / 4;
fn slots_for_name(name: &str) -> u64 {
let lfn = if is_valid_83(name) {
0
} else {
let units = name.encode_utf16().count();
units.div_ceil(LFN_CHARS_PER_ENTRY) as u64
};
1 + lfn
}
#[derive(Debug, Default)]
pub struct FatSizePlan {
file_lens: Vec<u64>,
dir_slots: HashMap<String, u64>,
}
impl FatSizePlan {
#[must_use]
pub fn new() -> Self {
let mut dir_slots = HashMap::new();
dir_slots.insert("/".to_string(), 1u64);
Self {
file_lens: Vec::new(),
dir_slots,
}
}
fn charge_parent(&mut self, path: &str) {
let (parent, name) = split_parent_name(path);
*self.dir_slots.entry(parent.to_string()).or_insert(0) += slots_for_name(name);
}
fn clusters_at(&self, spc: u64) -> u64 {
let cluster_bytes = spc * u64::from(SECTOR);
let mut clusters = 0u64;
for &len in &self.file_lens {
clusters += len.div_ceil(cluster_bytes); }
for &slots in self.dir_slots.values() {
let bytes = (slots + 1) * ENTRY_SIZE as u64;
clusters += bytes.div_ceil(cluster_bytes).max(1);
}
clusters
}
}
impl FsSizePlan for FatSizePlan {
fn add_dir(&mut self, path: &str) {
self.charge_parent(path);
self.dir_slots.entry(path.to_string()).or_insert(2);
}
fn add_file(&mut self, path: &str, len: u64) {
self.charge_parent(path);
self.file_lens.push(len);
}
fn add_symlink(&mut self, path: &str, target: &str) {
self.charge_parent(path);
self.file_lens.push(target.len() as u64);
}
fn add_device(&mut self, path: &str) {
self.charge_parent(path);
}
fn total_size(&self) -> u64 {
let spc0 = 1u64;
let need0 = self.clusters_at(spc0).max(u64::from(MIN_FAT32_CLUSTERS));
let fat0 = (need0 + 2).div_ceil(u64::from(ENTRIES_PER_FAT_SECTOR));
let mut total = u64::from(RESERVED_SECTORS) + u64::from(NUM_FATS) * fat0 + need0 * spc0;
for _ in 0..64 {
let ts = u32::try_from(total).unwrap_or(u32::MAX);
match Fat32::geometry(ts) {
Ok((g_spc, _fat, avail)) => {
let g_spc = u64::from(g_spc);
let need = self.clusters_at(g_spc).max(u64::from(MIN_FAT32_CLUSTERS));
if u64::from(avail) >= need {
return total * u64::from(SECTOR);
}
total += (need - u64::from(avail)) * g_spc + 1;
}
Err(_) => {
let floor = u64::from(RESERVED_SECTORS)
+ u64::from(NUM_FATS) * 520
+ u64::from(MIN_FAT32_CLUSTERS);
total = total.max(floor) + 1;
}
}
}
total * u64::from(SECTOR)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_tree_is_fat32_minimum() {
let plan = FatSizePlan::new();
let size = plan.total_size();
assert!(size >= u64::from(MIN_FAT32_CLUSTERS) * 512);
assert!(
size < 40 * 1024 * 1024,
"empty FAT32 should be ~33 MiB, got {size}"
);
}
#[test]
fn slots_for_name_short_vs_long() {
assert_eq!(slots_for_name("README"), 1); assert_eq!(slots_for_name("KERNEL.IMG"), 1);
assert_eq!(slots_for_name("a-twenty-char-name!!"), 3);
}
#[test]
fn large_content_exceeds_floor_and_grows_monotonically() {
let mut small = FatSizePlan::new();
for i in 0..10 {
small.add_file(&format!("/f{i}.bin"), 1024);
}
let mut big = FatSizePlan::new();
for i in 0..10 {
big.add_file(&format!("/f{i}.bin"), 4 * 1024 * 1024 * 1024);
}
assert!(big.total_size() > small.total_size());
assert!(big.total_size() > 40u64 * 1024 * 1024 * 1024);
}
#[test]
fn estimated_size_actually_fits_and_reads_back() {
use crate::block::MemoryBackend;
use crate::fs::fat::{Fat32, FatFormatOpts};
use crate::repack::{Source, populate_fat32_from_source};
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("sub")).unwrap();
for i in 0..30 {
std::fs::write(
dir.path().join(format!("sub/file_{i}.txt")),
format!("contents of file {i}\n"),
)
.unwrap();
}
std::fs::write(dir.path().join("big.bin"), vec![7u8; 200_000]).unwrap();
let src = Source::HostDir(dir.path().to_path_buf());
let bytes = crate::analyze::size_for_source(&src, "fat32")
.unwrap()
.unwrap();
let mut dev = MemoryBackend::new(bytes);
let opts = FatFormatOpts {
total_sectors: (bytes / 512) as u32,
volume_id: 0,
volume_label: *b"NO NAME ",
};
let mut fat = Fat32::format(&mut dev, &opts).unwrap();
populate_fat32_from_source(&mut dev, &mut fat, &src).expect("populate must fit");
fat.flush(&mut dev).unwrap();
let fat = Fat32::open(&mut dev).unwrap();
let mut out = Vec::new();
let mut r = fat.open_file_reader(&mut dev, "/sub/file_3.txt").unwrap();
std::io::Read::read_to_end(&mut r, &mut out).unwrap();
assert_eq!(out, b"contents of file 3\n");
}
#[test]
fn directories_charge_their_parent() {
let mut plan = FatSizePlan::new();
plan.add_dir("/sub");
plan.add_file("/sub/a.txt", 10);
assert!(plan.dir_slots.contains_key("/sub"));
assert!(plan.dir_slots["/"] > 1); }
}