pub mod boot;
pub mod dir;
pub mod fsinfo;
pub mod mutate;
pub mod table;
use std::io::Read;
use std::path::Path;
use boot::BootSector;
use fsinfo::FsInfo;
use table::Fat;
use crate::Result;
use crate::block::BlockDevice;
pub const MIN_FAT32_CLUSTERS: u32 = 65525;
pub const SECTOR: u32 = 512;
#[derive(Debug, Clone)]
pub struct FatFormatOpts {
pub total_sectors: u32,
pub volume_id: u32,
pub volume_label: [u8; 11],
}
impl Default for FatFormatOpts {
fn default() -> Self {
Self {
total_sectors: 0,
volume_id: 0,
volume_label: *b"NO NAME ",
}
}
}
#[derive(Debug)]
pub struct Fat32 {
boot: BootSector,
fat: Fat,
next_free: u32,
}
impl Fat32 {
fn pick_spc(total_sectors: u32) -> u8 {
match total_sectors {
0..=532_480 => 1, 532_481..=16_777_216 => 8, 16_777_217..=33_554_432 => 16,
33_554_433..=67_108_864 => 32,
_ => 64,
}
}
fn geometry(total_sectors: u32) -> Result<(u8, u32, u32)> {
let spc = Self::pick_spc(total_sectors);
let reserved = 32u32;
let num_fats = 2u32;
let entries_per_fat_sector = SECTOR / 4;
let mut fat_size = 1u32;
loop {
let meta = reserved + num_fats * fat_size;
if meta >= total_sectors {
return Err(crate::Error::InvalidArgument(
"fat32: volume too small to hold the FAT metadata".into(),
));
}
let clusters = (total_sectors - meta) / spc as u32;
let needed = (clusters + 2).div_ceil(entries_per_fat_sector);
if needed <= fat_size {
if clusters < MIN_FAT32_CLUSTERS {
return Err(crate::Error::InvalidArgument(format!(
"fat32: {clusters} clusters is below the FAT32 minimum of \
{MIN_FAT32_CLUSTERS} — use a volume of at least ~33 MiB"
)));
}
return Ok((spc, fat_size, clusters));
}
fat_size = needed;
}
}
pub fn format(dev: &mut dyn BlockDevice, opts: &FatFormatOpts) -> Result<Self> {
let total = opts.total_sectors;
let need = total as u64 * SECTOR as u64;
if dev.total_size() < need {
return Err(crate::Error::InvalidArgument(format!(
"fat32: device has {} bytes, need {need}",
dev.total_size()
)));
}
let (spc, fat_size, clusters) = Self::geometry(total)?;
let mut boot = BootSector::fat32_default();
boot.sectors_per_cluster = spc;
boot.total_sectors = total;
boot.fat_size = fat_size;
boot.volume_id = opts.volume_id;
boot.volume_label = opts.volume_label;
let fat_entries = (fat_size * (SECTOR / 4)) as usize;
let mut fat = Fat::new(fat_entries, boot.media);
fat.set(boot.root_cluster, table::EOC);
let fs = Self {
boot,
fat,
next_free: 3,
};
dev.zero_range(0, need)?;
dev.write_at(
fs.cluster_offset(fs.boot.root_cluster),
&fs.volume_label_entry(),
)?;
let _ = clusters;
fs.flush(dev)?;
Ok(fs)
}
fn volume_label_entry(&self) -> [u8; dir::ENTRY_SIZE] {
dir::DirEntry {
name_83: self.boot.volume_label,
attr: dir::ATTR_VOLUME_ID,
first_cluster: 0,
file_size: 0,
}
.encode()
}
fn cluster_offset(&self, cluster: u32) -> u64 {
let sector =
self.boot.data_start_sector() + (cluster - 2) * self.boot.sectors_per_cluster as u32;
sector as u64 * SECTOR as u64
}
fn cluster_bytes(&self) -> u64 {
self.boot.sectors_per_cluster as u64 * SECTOR as u64
}
fn alloc_chain(&mut self, n: u32) -> Result<Vec<u32>> {
if n == 0 {
return Ok(Vec::new());
}
let mut chain = Vec::with_capacity(n as usize);
for _ in 0..n {
let c = self.next_free;
if c as usize >= self.fat.capacity() {
return Err(crate::Error::Unsupported("fat32: out of clusters".into()));
}
chain.push(c);
self.next_free += 1;
}
for w in chain.windows(2) {
self.fat.set(w[0], w[1]);
}
self.fat.set(*chain.last().unwrap(), table::EOC);
Ok(chain)
}
fn write_chain(&self, dev: &mut dyn BlockDevice, chain: &[u32], data: &[u8]) -> Result<()> {
let cb = self.cluster_bytes() as usize;
for (i, &c) in chain.iter().enumerate() {
let start = i * cb;
if start >= data.len() {
break;
}
let end = (start + cb).min(data.len());
dev.write_at(self.cluster_offset(c), &data[start..end])?;
}
Ok(())
}
pub fn flush(&self, dev: &mut dyn BlockDevice) -> Result<()> {
let boot_bytes = self.boot.encode();
dev.write_at(0, &boot_bytes)?;
dev.write_at(
self.boot.backup_boot_sector as u64 * SECTOR as u64,
&boot_bytes,
)?;
let clusters = self.boot.cluster_count();
let free_count = self.count_free_clusters();
let next_hint = if self.next_free >= 2 && self.next_free < clusters + 2 {
self.next_free
} else {
2
};
let fsinfo = FsInfo {
free_count,
next_free: next_hint,
};
let fsinfo_bytes = fsinfo.encode();
dev.write_at(
self.boot.fs_info_sector as u64 * SECTOR as u64,
&fsinfo_bytes,
)?;
dev.write_at(
(self.boot.backup_boot_sector as u64 + 1) * SECTOR as u64,
&fsinfo_bytes,
)?;
let fat_bytes = self.fat.encode();
for i in 0..self.boot.num_fats as u32 {
let off = (self.boot.reserved_sector_count as u64
+ i as u64 * self.boot.fat_size as u64)
* SECTOR as u64;
dev.write_at(off, &fat_bytes)?;
}
Ok(())
}
fn count_free_clusters(&self) -> u32 {
let clusters = self.boot.cluster_count();
let mut n = 0u32;
for c in 2..(2 + clusters) {
if self.fat.get(c) == table::FREE {
n += 1;
}
}
n
}
pub fn build_from_host_dir(
dev: &mut dyn BlockDevice,
total_sectors: u32,
src: &Path,
volume_id: u32,
volume_label: [u8; 11],
) -> Result<()> {
let opts = FatFormatOpts {
total_sectors,
volume_id,
volume_label,
};
let mut fs = Self::format(dev, &opts)?;
fs.populate_from_host_dir(dev, src)?;
fs.flush(dev)?;
dev.sync()?;
Ok(())
}
pub fn populate_from_host_dir(&mut self, dev: &mut dyn BlockDevice, src: &Path) -> Result<()> {
let root_cluster = self.boot.root_cluster;
self.write_dir_tree(dev, src, root_cluster, true, root_cluster)
}
fn write_dir_tree(
&mut self,
dev: &mut dyn BlockDevice,
src: &Path,
dir_cluster: u32,
is_root: bool,
parent_cluster: u32,
) -> Result<()> {
let mut entries: Vec<u8> = Vec::new();
if is_root {
entries.extend_from_slice(&self.volume_label_entry());
} else {
entries.extend_from_slice(&dot_entry(b". ", dir_cluster));
let pc = if parent_cluster == self.boot.root_cluster {
0
} else {
parent_cluster
};
entries.extend_from_slice(&dot_entry(b".. ", pc));
}
let mut short_seq: u32 = 0;
let mut children: Vec<(std::path::PathBuf, std::fs::Metadata)> = Vec::new();
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let meta = entry.metadata()?;
children.push((entry.path(), meta));
}
children.sort_by(|a, b| a.0.file_name().cmp(&b.0.file_name()));
for (path, meta) in children {
let name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 file name".into()))?
.to_string();
let ft = meta.file_type();
if ft.is_symlink() {
continue; }
if ft.is_file() {
let size = meta.len();
let cb = self.cluster_bytes();
let n_clusters = size.div_ceil(cb).max(1) as u32;
let chain = self.alloc_chain(n_clusters)?;
self.stream_file(dev, &path, &chain, size)?;
let first = if size == 0 { 0 } else { chain[0] };
self.push_entry(
&mut entries,
&name,
dir::ATTR_ARCHIVE,
first,
size as u32,
&mut short_seq,
);
if size == 0 {
self.free_unused_chain(&chain);
}
} else if ft.is_dir() {
let chain = self.alloc_chain(1)?;
let child_cluster = chain[0];
self.write_dir_tree(dev, &path, child_cluster, false, dir_cluster)?;
self.push_entry(
&mut entries,
&name,
dir::ATTR_DIRECTORY,
child_cluster,
0,
&mut short_seq,
);
}
}
self.write_dir_entries(dev, dir_cluster, &entries)?;
Ok(())
}
fn push_entry(
&self,
entries: &mut Vec<u8>,
name: &str,
attr: u8,
first_cluster: u32,
file_size: u32,
short_seq: &mut u32,
) {
let upper = name.to_ascii_uppercase();
let (name_83, need_lfn) = if dir::is_valid_83(&upper) {
(dir::pack_83(&upper), upper != name)
} else {
let s = dir::generate_83(name, *short_seq);
*short_seq += 1;
(s, true)
};
if need_lfn {
let csum = dir::lfn_checksum(&name_83);
for frag in dir::encode_lfn_run(name, csum) {
entries.extend_from_slice(&frag);
}
}
let entry = dir::DirEntry {
name_83,
attr,
first_cluster,
file_size,
};
entries.extend_from_slice(&entry.encode());
}
fn write_dir_entries(
&mut self,
dev: &mut dyn BlockDevice,
dir_cluster: u32,
entries: &[u8],
) -> Result<()> {
let cb = self.cluster_bytes() as usize;
let need_clusters = entries.len().div_ceil(cb).max(1) as u32;
let mut chain = vec![dir_cluster];
if need_clusters > 1 {
let extra = self.alloc_chain(need_clusters - 1)?;
self.fat.set(dir_cluster, extra[0]);
chain.extend_from_slice(&extra);
}
let mut buf = entries.to_vec();
buf.resize(need_clusters as usize * cb, 0);
self.write_chain(dev, &chain, &buf)?;
Ok(())
}
fn stream_file(
&self,
dev: &mut dyn BlockDevice,
host: &Path,
chain: &[u32],
size: u64,
) -> Result<()> {
if size == 0 {
return Ok(());
}
let cb = self.cluster_bytes() as usize;
let mut file = std::fs::File::open(host)?;
let mut buf = vec![0u8; cb];
let mut remaining = size;
for &c in chain {
let want = remaining.min(cb as u64) as usize;
buf[..want].fill(0);
file.read_exact(&mut buf[..want])?;
dev.write_at(self.cluster_offset(c), &buf[..want])?;
remaining -= want as u64;
if remaining == 0 {
break;
}
}
Ok(())
}
fn free_unused_chain(&mut self, chain: &[u32]) {
for &c in chain {
self.fat.set(c, table::FREE);
}
if let Some(&first) = chain.first()
&& first + chain.len() as u32 == self.next_free
{
self.next_free = first;
}
}
pub fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
let mut bs = [0u8; 512];
dev.read_at(0, &mut bs)?;
let boot = BootSector::decode(&bs)?;
if boot.bytes_per_sector as u32 != SECTOR {
return Err(crate::Error::Unsupported(format!(
"fat32: only 512-byte sectors are supported (got {})",
boot.bytes_per_sector
)));
}
let fat_bytes_len = boot.fat_size as u64 * SECTOR as u64;
let mut fat_bytes = vec![0u8; fat_bytes_len as usize];
let fat_off = boot.reserved_sector_count as u64 * SECTOR as u64;
dev.read_at(fat_off, &mut fat_bytes)?;
let fat = Fat::decode(&fat_bytes);
let next_free = fat.capacity() as u32;
Ok(Self {
boot,
fat,
next_free,
})
}
pub fn boot_sector(&self) -> &BootSector {
&self.boot
}
pub fn fat(&self) -> &Fat {
&self.fat
}
pub fn chain_of(&self, start: u32) -> Result<Vec<u32>> {
self.fat.chain(start)
}
pub fn list_path(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<Vec<crate::fs::DirEntry>> {
let cluster = self.resolve_dir(dev, path)?;
self.list_cluster(dev, cluster)
}
pub fn open_file_reader<'a>(
&self,
dev: &'a mut dyn BlockDevice,
path: &str,
) -> Result<FatFileReader<'a>> {
let (entry, dir_cluster) = self.resolve_entry(dev, path)?;
if entry.attr & dir::ATTR_DIRECTORY != 0 {
return Err(crate::Error::InvalidArgument(format!(
"fat32: {path:?} is a directory, not a file"
)));
}
let _ = dir_cluster; let chain = if entry.first_cluster < 2 {
Vec::new() } else {
self.chain_of(entry.first_cluster)?
};
let cluster_bytes = self.cluster_bytes();
let data_start = self.boot.data_start_sector() as u64 * SECTOR as u64;
let spc = self.boot.sectors_per_cluster;
Ok(FatFileReader {
dev,
chain,
cluster_bytes,
data_start,
spc,
remaining: entry.file_size as u64,
cluster_idx: 0,
cluster_off: 0,
})
}
pub fn resolve_dir(&self, dev: &mut dyn BlockDevice, path: &str) -> Result<u32> {
let parts = split_path(path);
let mut cluster = self.boot.root_cluster;
for part in parts {
let entries = self.list_cluster_raw(dev, cluster)?;
let next = entries
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(part))
.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"fat32: no such entry {part:?} under {path:?}"
))
})?;
if next.1.attr & dir::ATTR_DIRECTORY == 0 {
return Err(crate::Error::InvalidArgument(format!(
"fat32: {part:?} is not a directory"
)));
}
cluster = if next.1.first_cluster == 0 {
self.boot.root_cluster
} else {
next.1.first_cluster
};
}
Ok(cluster)
}
pub fn resolve_entry(
&self,
dev: &mut dyn BlockDevice,
path: &str,
) -> Result<(dir::DirEntry, u32)> {
let parts = split_path(path);
if parts.is_empty() {
return Err(crate::Error::InvalidArgument(
"fat32: cannot resolve root \"/\" as a file entry".into(),
));
}
let mut cluster = self.boot.root_cluster;
let (last, prefix) = parts.split_last().unwrap();
for part in prefix {
let entries = self.list_cluster_raw(dev, cluster)?;
let next = entries
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(part))
.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"fat32: no such entry {part:?} under {path:?}"
))
})?;
cluster = if next.1.first_cluster == 0 {
self.boot.root_cluster
} else {
next.1.first_cluster
};
}
let entries = self.list_cluster_raw(dev, cluster)?;
let found = entries
.into_iter()
.find(|(name, _)| name.eq_ignore_ascii_case(last))
.ok_or_else(|| {
crate::Error::InvalidArgument(format!(
"fat32: no such entry {last:?} under {path:?}"
))
})?;
Ok((found.1, cluster))
}
fn read_dir_bytes(&self, dev: &mut dyn BlockDevice, dir_cluster: u32) -> Result<Vec<u8>> {
let chain = self.chain_of(dir_cluster)?;
let cb = self.cluster_bytes() as usize;
let mut buf = vec![0u8; chain.len() * cb];
for (i, &c) in chain.iter().enumerate() {
dev.read_at(self.cluster_offset(c), &mut buf[i * cb..(i + 1) * cb])?;
}
Ok(buf)
}
fn list_cluster_raw(
&self,
dev: &mut dyn BlockDevice,
dir_cluster: u32,
) -> Result<Vec<(String, dir::DirEntry)>> {
let bytes = self.read_dir_bytes(dev, dir_cluster)?;
let mut out = Vec::new();
let mut lfn_run: Vec<dir::LfnFragment> = Vec::new();
for chunk in bytes.chunks_exact(dir::ENTRY_SIZE) {
let slot: &[u8; dir::ENTRY_SIZE] = chunk.try_into().unwrap();
match dir::classify_slot(slot) {
dir::RawSlot::End => break,
dir::RawSlot::Deleted => {
lfn_run.clear();
}
dir::RawSlot::Lfn(frag) => {
lfn_run.push(frag);
}
dir::RawSlot::ShortEntry(entry) => {
if entry.attr & dir::ATTR_VOLUME_ID != 0
&& entry.attr & dir::ATTR_DIRECTORY == 0
{
lfn_run.clear();
continue;
}
let short_name = entry.short_name_string();
if short_name == "." || short_name == ".." {
lfn_run.clear();
continue;
}
let name = dir::assemble_lfn(&lfn_run, &entry.name_83)
.unwrap_or_else(|| short_name.clone());
lfn_run.clear();
out.push((name, entry));
}
}
}
Ok(out)
}
fn list_cluster(
&self,
dev: &mut dyn BlockDevice,
dir_cluster: u32,
) -> Result<Vec<crate::fs::DirEntry>> {
use crate::fs::{DirEntry as FsDirEntry, EntryKind};
let entries = self.list_cluster_raw(dev, dir_cluster)?;
Ok(entries
.into_iter()
.map(|(name, e)| {
let is_dir = e.attr & dir::ATTR_DIRECTORY != 0;
FsDirEntry {
name,
inode: e.first_cluster,
kind: if is_dir {
EntryKind::Dir
} else {
EntryKind::Regular
},
size: if is_dir { 0 } else { u64::from(e.file_size) },
}
})
.collect())
}
}
fn split_path(path: &str) -> Vec<&str> {
path.split(['/', '\\'])
.filter(|p| !p.is_empty() && *p != ".")
.collect()
}
pub struct FatFileReader<'a> {
dev: &'a mut dyn BlockDevice,
chain: Vec<u32>,
cluster_bytes: u64,
data_start: u64,
spc: u8,
remaining: u64,
cluster_idx: usize,
cluster_off: u64,
}
impl<'a> std::io::Read for FatFileReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.remaining == 0 || self.cluster_idx >= self.chain.len() {
return Ok(0);
}
let avail_in_cluster = self.cluster_bytes - self.cluster_off;
let want = (buf.len() as u64).min(avail_in_cluster).min(self.remaining) as usize;
let cluster = self.chain[self.cluster_idx];
let cluster_start =
self.data_start + (cluster as u64 - 2) * self.spc as u64 * SECTOR as u64;
let off = cluster_start + self.cluster_off;
self.dev
.read_at(off, &mut buf[..want])
.map_err(std::io::Error::other)?;
self.cluster_off += want as u64;
self.remaining -= want as u64;
if self.cluster_off == self.cluster_bytes {
self.cluster_idx += 1;
self.cluster_off = 0;
}
Ok(want)
}
}
fn dot_entry(name_83: &[u8; 11], cluster: u32) -> [u8; dir::ENTRY_SIZE] {
dir::DirEntry {
name_83: *name_83,
attr: dir::ATTR_DIRECTORY,
first_cluster: cluster,
file_size: 0,
}
.encode()
}
impl crate::fs::FilesystemFactory for Fat32 {
type FormatOpts = FatFormatOpts;
fn format(dev: &mut dyn BlockDevice, opts: &Self::FormatOpts) -> Result<Self> {
Self::format(dev, opts)
}
fn open(dev: &mut dyn BlockDevice) -> Result<Self> {
Self::open(dev)
}
}
impl crate::fs::Filesystem for Fat32 {
fn create_file(
&mut self,
dev: &mut dyn BlockDevice,
path: &Path,
src: crate::fs::FileSource,
_meta: crate::fs::FileMeta,
) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 path".into()))?;
let (mut reader, len) = src.open()?;
self.add_file_from_reader(dev, s, &mut reader, len)
}
fn create_dir(
&mut self,
dev: &mut dyn BlockDevice,
path: &Path,
_meta: crate::fs::FileMeta,
) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 path".into()))?;
self.add_dir(dev, s)
}
fn create_symlink(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &Path,
_target: &Path,
_meta: crate::fs::FileMeta,
) -> Result<()> {
Err(crate::Error::Unsupported(
"fat32: filesystem does not support symbolic links".into(),
))
}
fn create_device(
&mut self,
_dev: &mut dyn BlockDevice,
_path: &Path,
_kind: crate::fs::DeviceKind,
_major: u32,
_minor: u32,
_meta: crate::fs::FileMeta,
) -> Result<()> {
Err(crate::Error::Unsupported(
"fat32: filesystem does not support device / FIFO / socket nodes".into(),
))
}
fn remove(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 path".into()))?;
self.remove(dev, s)
}
fn list(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<Vec<crate::fs::DirEntry>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 path".into()))?;
self.list_path(dev, s)
}
fn read_file<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &Path,
) -> Result<Box<dyn Read + 'a>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 path".into()))?;
let r = self.open_file_reader(dev, s)?;
Ok(Box::new(r))
}
fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
Self::flush(self, dev)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
#[test]
fn geometry_small_volume() {
let (spc, fat_size, clusters) = Fat32::geometry(131072).unwrap();
assert_eq!(spc, 1);
assert!(fat_size > 0);
assert!(clusters >= MIN_FAT32_CLUSTERS);
assert!(32 + 2 * fat_size + clusters * spc as u32 <= 131072);
assert!(fat_size * (SECTOR / 4) >= clusters + 2);
}
#[test]
fn geometry_rejects_tiny_volume() {
assert!(Fat32::geometry(8192).is_err());
}
#[test]
fn format_empty_volume() {
let mut dev = MemoryBackend::new(48 * 1024 * 1024);
let opts = FatFormatOpts {
total_sectors: 48 * 1024 * 1024 / 512,
volume_id: 0xCAFE_F00D,
volume_label: *b"TESTVOL ",
};
let fs = Fat32::format(&mut dev, &opts).unwrap();
let mut bs = [0u8; 512];
dev.read_at(0, &mut bs).unwrap();
let decoded = BootSector::decode(&bs).unwrap();
assert_eq!(decoded.total_sectors, opts.total_sectors);
assert_eq!(decoded.root_cluster, 2);
assert_eq!(decoded.volume_id, 0xCAFE_F00D);
let mut backup = [0u8; 512];
dev.read_at(6 * 512, &mut backup).unwrap();
assert_eq!(bs, backup);
assert!(Fat::is_eoc(fs.fat.get(2)));
}
}