use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use crate::io::Read;
use crate::path::Path;
#[path = "boot.rs"]
pub mod boot;
#[path = "dir.rs"]
pub mod dir;
#[path = "fsinfo.rs"]
pub mod fsinfo;
#[path = "handle.rs"]
pub mod handle;
#[path = "mutate.rs"]
pub mod mutate;
#[path = "size_plan.rs"]
pub mod size_plan;
#[path = "table.rs"]
pub mod table;
use crate::fs::fat::FatKind;
use boot::BootSector;
use fsinfo::FsInfo;
use table::Fat;
use crate::Result;
use crate::block::BlockDevice;
use crate::fs::dir_batch::{DEFAULT_CAPACITY, DirBatch};
pub const MIN_FAT32_CLUSTERS: u32 = 65525;
pub const SECTOR: u32 = 512;
const ROOT_ENTRIES_PER_SECTOR: u16 = (SECTOR / dir::ENTRY_SIZE as u32) as u16;
const FLOPPY_ROOT_ENTRIES: u16 = 224;
const DEFAULT_ROOT_ENTRIES: u16 = 512;
const FLOPPY_MAX_SECTORS: u32 = 5760;
#[derive(Debug, Clone)]
pub struct FatFormatOpts {
pub kind: FatKind,
pub total_sectors: u32,
pub volume_id: u32,
pub volume_label: [u8; 11],
pub root_entries: Option<u16>,
}
impl Default for FatFormatOpts {
fn default() -> Self {
Self {
kind: FatKind::Fat32,
total_sectors: 0,
volume_id: 0,
volume_label: *b"NO NAME ",
root_entries: None,
}
}
}
impl FatFormatOpts {
pub fn apply_options(&mut self, map: &mut crate::format_opts::OptionMap) -> crate::Result<()> {
if let Some(s) = map.take_str("fat_type") {
self.kind = parse_fat_kind(&s)?;
}
if let Some(v) = map.take_u32("total_sectors")? {
self.total_sectors = v;
}
if let Some(v) = map.take_u32("volume_id")? {
self.volume_id = v;
}
if let Some(label) = map.take_label::<11>("volume_label", b' ')? {
self.volume_label = label;
}
if let Some(v) = map.take_u32("root_entries")? {
let v = u16::try_from(v).map_err(|_| {
crate::Error::InvalidArgument(format!("fat: root_entries={v} exceeds 65535"))
})?;
validate_root_entries(v)?;
self.root_entries = Some(v);
}
Ok(())
}
}
pub fn parse_fat_kind(s: &str) -> Result<FatKind> {
match s.trim().to_ascii_lowercase().as_str() {
"fat12" | "12" => Ok(FatKind::Fat12),
"fat16" | "16" => Ok(FatKind::Fat16),
"fat32" | "32" | "vfat" | "fat" => Ok(FatKind::Fat32),
other => Err(crate::Error::InvalidArgument(format!(
"fat: unknown FAT type {other:?} — expected fat12, fat16 or fat32"
))),
}
}
pub fn min_volume_bytes(fs_type: &str) -> u64 {
let kind = parse_fat_kind(fs_type).unwrap_or(FatKind::Fat32);
match kind {
FatKind::Fat12 => 1024 * 1024,
FatKind::Fat16 => u64::from(FatKind::Fat16.min_clusters()) * 1024,
FatKind::Fat32 => u64::from(MIN_FAT32_CLUSTERS) * 1024,
}
}
fn validate_root_entries(v: u16) -> Result<()> {
if v == 0 || !v.is_multiple_of(ROOT_ENTRIES_PER_SECTOR) {
return Err(crate::Error::InvalidArgument(format!(
"fat: root_entries must be a non-zero multiple of {ROOT_ENTRIES_PER_SECTOR} \
(got {v})"
)));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Geometry {
pub(crate) spc: u8,
pub(crate) fat_size: u32,
pub(crate) reserved: u16,
pub(crate) root_entries: u16,
pub(crate) clusters: u32,
}
pub(super) struct DirLayout {
chunks: Vec<(u64, usize)>,
chunk_bytes: usize,
clusters: Vec<u32>,
fixed_root: bool,
}
impl DirLayout {
pub(super) fn len(&self) -> usize {
self.chunks.iter().map(|&(_, n)| n).sum()
}
pub(super) fn offset_of(&self, pos: usize) -> u64 {
let (base, _) = self.chunks[pos / self.chunk_bytes];
base + (pos % self.chunk_bytes) as u64
}
pub(super) fn is_fixed_root(&self) -> bool {
self.fixed_root
}
pub(super) fn last_cluster(&self) -> Option<u32> {
self.clusters.last().copied()
}
fn extend_with(&mut self, clusters: &[u32], offsets: impl Fn(u32) -> u64) {
for &c in clusters {
self.chunks.push((offsets(c), self.chunk_bytes));
self.clusters.push(c);
}
}
pub(super) fn write_range(
&self,
dev: &mut dyn BlockDevice,
bytes: &[u8],
start: usize,
end: usize,
) -> Result<()> {
let first = start / self.chunk_bytes;
let last = (end - 1) / self.chunk_bytes;
for i in first..=last {
let (off, n) = self.chunks[i];
let at = i * self.chunk_bytes;
dev.write_at(off, &bytes[at..at + n])?;
}
Ok(())
}
pub(super) fn read_all(&self, dev: &mut dyn BlockDevice) -> Result<Vec<u8>> {
let mut buf = vec![0u8; self.len()];
let mut at = 0usize;
for &(off, n) in &self.chunks {
dev.read_at(off, &mut buf[at..at + n])?;
at += n;
}
Ok(buf)
}
}
#[derive(Debug)]
pub struct Fat32 {
boot: BootSector,
fat: Fat,
next_free: u32,
dir_batch: DirBatch<u32, mutate::PendingEntry>,
pending_names: BTreeMap<u32, BTreeSet<String>>,
pending_shorts: BTreeMap<u32, BTreeSet<[u8; 11]>>,
}
impl Fat32 {
pub(crate) 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,
}
}
pub(crate) fn default_root_entries(total_sectors: u32) -> u16 {
if total_sectors <= FLOPPY_MAX_SECTORS {
FLOPPY_ROOT_ENTRIES
} else {
DEFAULT_ROOT_ENTRIES
}
}
fn converge_fat_size(
kind: FatKind,
total_sectors: u32,
spc: u8,
reserved: u32,
num_fats: u32,
root_sectors: u32,
) -> Result<(u32, u32)> {
let mut fat_size = 1u32;
loop {
let meta = reserved + num_fats * fat_size + root_sectors;
if meta >= total_sectors {
return Err(crate::Error::InvalidArgument(format!(
"{}: volume too small to hold the FAT metadata",
kind.as_str()
)));
}
let clusters = (total_sectors - meta) / spc as u32;
let needed = kind
.fat_bytes(u64::from(clusters) + 2)
.div_ceil(u64::from(SECTOR)) as u32;
if needed <= fat_size {
return Ok((fat_size, clusters));
}
fat_size = needed;
}
}
pub(crate) fn geometry(
kind: FatKind,
total_sectors: u32,
root_entries: Option<u16>,
) -> Result<Geometry> {
let num_fats = 2u32;
let (reserved, root_entries) = if kind == FatKind::Fat32 {
(32u32, 0u16)
} else {
let re = root_entries.unwrap_or_else(|| Self::default_root_entries(total_sectors));
validate_root_entries(re)?;
(1u32, re)
};
let root_sectors = (u32::from(root_entries) * dir::ENTRY_SIZE as u32).div_ceil(SECTOR);
if kind == FatKind::Fat32 {
let spc = Self::pick_spc(total_sectors);
let (fat_size, clusters) = Self::converge_fat_size(
kind,
total_sectors,
spc,
reserved,
num_fats,
root_sectors,
)?;
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(Geometry {
spc,
fat_size,
reserved: reserved as u16,
root_entries,
clusters,
});
}
let mut smallest = None;
for &spc in &[1u8, 2, 4, 8, 16, 32, 64] {
let (fat_size, clusters) = Self::converge_fat_size(
kind,
total_sectors,
spc,
reserved,
num_fats,
root_sectors,
)?;
smallest.get_or_insert(clusters);
if clusters > kind.max_clusters() {
continue; }
if clusters < kind.min_clusters() {
break; }
return Ok(Geometry {
spc,
fat_size,
reserved: reserved as u16,
root_entries,
clusters,
});
}
let got = smallest.unwrap_or(0);
if got < kind.min_clusters() {
Err(crate::Error::InvalidArgument(format!(
"{}: {got} clusters is below the {} minimum of {} — use a larger volume \
(or a smaller FAT type)",
kind.as_str(),
kind.as_str(),
kind.min_clusters()
)))
} else {
Err(crate::Error::InvalidArgument(format!(
"{}: a volume of {total_sectors} sectors needs more than {} clusters even at \
the largest cluster size — use a wider FAT type",
kind.as_str(),
kind.max_clusters()
)))
}
}
pub fn format(dev: &mut dyn BlockDevice, opts: &FatFormatOpts) -> Result<Self> {
let kind = opts.kind;
let total = opts.total_sectors;
let need = total as u64 * SECTOR as u64;
if dev.total_size() < need {
return Err(crate::Error::InvalidArgument(format!(
"{}: device has {} bytes, need {need}",
kind.as_str(),
dev.total_size()
)));
}
let geom = Self::geometry(kind, total, opts.root_entries)?;
let mut boot = BootSector::defaults_for(kind);
boot.sectors_per_cluster = geom.spc;
boot.total_sectors = total;
boot.fat_size = geom.fat_size;
boot.reserved_sector_count = geom.reserved;
boot.root_entry_count = geom.root_entries;
boot.volume_id = opts.volume_id;
boot.volume_label = opts.volume_label;
let fat_bytes = geom.fat_size as usize * SECTOR as usize;
let mut fat = Fat::new(kind, fat_bytes, boot.media);
if kind == FatKind::Fat32 {
fat.set(boot.root_cluster, fat.eoc());
}
let mut fs = Self {
boot,
fat,
next_free: if kind == FatKind::Fat32 { 3 } else { 2 },
dir_batch: DirBatch::new(DEFAULT_CAPACITY),
pending_names: BTreeMap::new(),
pending_shorts: BTreeMap::new(),
};
let meta_bytes = u64::from(fs.boot.data_start_sector()) * u64::from(SECTOR);
dev.zero_range(0, meta_bytes)?;
if kind == FatKind::Fat32 {
let cluster_bytes = u64::from(geom.spc) * u64::from(SECTOR);
dev.zero_range(fs.cluster_offset(fs.boot.root_cluster), cluster_bytes)?;
}
let root_off = fs.root_dir_offset();
dev.write_at(root_off, &fs.volume_label_entry())?;
fs.flush(dev)?;
Ok(fs)
}
pub fn kind(&self) -> FatKind {
self.boot.kind
}
fn root_dir_offset(&self) -> u64 {
if self.boot.kind == FatKind::Fat32 {
self.cluster_offset(self.boot.root_cluster)
} else {
u64::from(self.boot.root_dir_start_sector()) * u64::from(SECTOR)
}
}
fn is_fixed_root(&self, dir_id: u32) -> bool {
self.boot.kind != FatKind::Fat32 && dir_id == 0
}
pub(super) fn dir_layout(&self, dir_id: u32) -> Result<DirLayout> {
let cb = self.cluster_bytes() as usize;
if self.is_fixed_root(dir_id) {
let total = usize::from(self.boot.root_entry_count) * dir::ENTRY_SIZE;
let base = self.root_dir_offset();
let mut chunks = Vec::with_capacity(total.div_ceil(cb));
let mut at = 0usize;
while at < total {
let n = cb.min(total - at);
chunks.push((base + at as u64, n));
at += n;
}
return Ok(DirLayout {
chunks,
chunk_bytes: cb,
clusters: Vec::new(),
fixed_root: true,
});
}
let clusters = self.fat.chain(dir_id, self.boot.cluster_count())?;
let chunks = clusters
.iter()
.map(|&c| (self.cluster_offset(c), cb))
.collect();
Ok(DirLayout {
chunks,
chunk_bytes: cb,
clusters,
fixed_root: false,
})
}
pub(super) fn grow_dir(&mut self, layout: &mut DirLayout, n: u32) -> Result<()> {
if layout.is_fixed_root() {
return Err(self.fixed_root_full_err());
}
let extra = self.alloc_free_clusters(n)?;
if let Some(last) = layout.last_cluster() {
self.fat.set(last, extra[0]);
}
let data_start = u64::from(self.boot.data_start_sector()) * u64::from(SECTOR);
let spc = u64::from(self.boot.sectors_per_cluster);
layout.extend_with(&extra, |c| {
data_start + (u64::from(c) - 2) * spc * u64::from(SECTOR)
});
Ok(())
}
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,
mtime: 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
}
#[cfg(feature = "std")]
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() || c >= self.boot.cluster_count() + 2 {
return Err(crate::Error::Unsupported(format!(
"{}: out of clusters",
self.boot.kind.as_str()
)));
}
chain.push(c);
self.next_free += 1;
}
for w in chain.windows(2) {
self.fat.set(w[0], w[1]);
}
let eoc = self.fat.eoc();
self.fat.set(*chain.last().unwrap(), eoc);
Ok(chain)
}
#[cfg(feature = "std")]
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(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
self.flush_dir_batches(dev)?;
if !self.fat.is_dirty() {
return Ok(());
}
let boot_bytes = self.boot.encode();
dev.write_at(0, &boot_bytes)?;
if self.boot.kind == FatKind::Fat32 {
let backup = self.boot.backup_boot_sector;
if backup != 0 {
dev.write_at(backup 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,
)?;
if backup != 0 {
dev.write_at((backup 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)?;
}
self.fat.mark_clean();
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
}
#[cfg(feature = "std")]
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,
..Default::default()
};
let mut fs = Self::format(dev, &opts)?;
fs.populate_from_host_dir(dev, src)?;
fs.flush(dev)?;
dev.sync()?;
Ok(())
}
#[cfg(feature = "std")]
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)
}
#[cfg(feature = "std")]
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; }
let mtime = mutate::host_mtime_secs(&meta);
if ft.is_file() {
let size = meta.len();
if size > u64::from(u32::MAX) {
return Err(crate::Error::InvalidArgument(format!(
"{}: {} is {size} bytes; FAT files cannot exceed 4 GiB",
self.boot.kind.as_str(),
path.display()
)));
}
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,
mtime,
&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,
mtime,
&mut short_seq,
);
}
}
self.write_dir_entries(dev, dir_cluster, &entries)?;
Ok(())
}
#[cfg(feature = "std")]
#[allow(clippy::too_many_arguments)]
fn push_entry(
&self,
entries: &mut Vec<u8>,
name: &str,
attr: u8,
first_cluster: u32,
file_size: u32,
mtime: 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,
mtime,
};
entries.extend_from_slice(&entry.encode());
}
#[cfg(feature = "std")]
fn write_dir_entries(
&mut self,
dev: &mut dyn BlockDevice,
dir_cluster: u32,
entries: &[u8],
) -> Result<()> {
let cb = self.cluster_bytes() as usize;
if self.is_fixed_root(dir_cluster) {
let capacity = usize::from(self.boot.root_entry_count) * dir::ENTRY_SIZE;
if entries.len() > capacity {
return Err(self.fixed_root_full_err());
}
let mut buf = entries.to_vec();
buf.resize(capacity, 0);
dev.write_at(self.root_dir_offset(), &buf)?;
return Ok(());
}
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(())
}
pub(super) fn fixed_root_full_err(&self) -> crate::Error {
crate::Error::Unsupported(format!(
"{}: the root directory is fixed at {} entries and is full — reformat with a \
larger `-O root_entries=`, nest the files in a subdirectory, or use fat32",
self.boot.kind.as_str(),
self.boot.root_entry_count
))
}
#[cfg(feature = "std")]
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(())
}
#[cfg(feature = "std")]
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)?;
let kind = boot.kind;
if boot.bytes_per_sector as u32 != SECTOR {
return Err(crate::Error::Unsupported(format!(
"{}: only 512-byte sectors are supported (got {})",
kind.as_str(),
boot.bytes_per_sector
)));
}
let total_sectors = boot.total_sectors as u64;
let volume_bytes = total_sectors.checked_mul(SECTOR as u64).ok_or_else(|| {
crate::Error::InvalidImage(format!("{}: total_sectors overflow", kind.as_str()))
})?;
if volume_bytes > dev.total_size() {
return Err(crate::Error::InvalidImage(format!(
"{}: volume of {volume_bytes} bytes exceeds device size {}",
kind.as_str(),
dev.total_size()
)));
}
let meta_sectors = u64::from(boot.data_start_sector());
if meta_sectors > total_sectors {
return Err(crate::Error::InvalidImage(format!(
"{}: reserved + FATs + root ({meta_sectors} sectors) overruns volume of \
{total_sectors} sectors",
kind.as_str()
)));
}
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(kind, &fat_bytes);
let next_free = fat.capacity() as u32;
Ok(Self {
boot,
fat,
next_free,
dir_batch: DirBatch::new(DEFAULT_CAPACITY),
pending_names: BTreeMap::new(),
pending_shorts: BTreeMap::new(),
})
}
pub fn boot_sector(&self) -> &BootSector {
&self.boot
}
pub fn fat(&self) -> &Fat {
&self.fat
}
pub(super) fn fat_mut(&mut self) -> &mut Fat {
&mut self.fat
}
pub(super) fn hint_next_free(&mut self, cluster: u32) {
if cluster >= 2 && cluster < self.boot.cluster_count() + 2 {
self.next_free = cluster;
}
}
pub fn chain_of(&self, start: u32) -> Result<Vec<u32>> {
self.fat.chain(start, self.boot.cluster_count())
}
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:?}"
))
})?;
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
};
}
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))
}
pub(super) fn first_cluster_for_kind(&self, raw: u32) -> u32 {
if self.boot.kind == FatKind::Fat32 {
raw
} else {
raw & 0xFFFF
}
}
fn read_dir_bytes(&self, dev: &mut dyn BlockDevice, dir_cluster: u32) -> Result<Vec<u8>> {
self.dir_layout(dir_cluster)?.read_all(dev)
}
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 slot in bytes.as_chunks::<{ dir::ENTRY_SIZE }>().0 {
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(mut entry) => {
if entry.attr & dir::ATTR_VOLUME_ID != 0
&& entry.attr & dir::ATTR_DIRECTORY == 0
{
lfn_run.clear();
continue;
}
entry.first_cluster = self.first_cluster_for_kind(entry.first_cluster);
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> Read for FatFileReader<'a> {
fn read(&mut self, buf: &mut [u8]) -> crate::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(crate::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)
}
}
#[cfg(feature = "std")]
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,
mtime: 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, meta.mtime)
}
fn create_file_streaming(
&mut self,
dev: &mut dyn BlockDevice,
path: &Path,
body: &mut dyn Read,
len: u64,
meta: crate::fs::FileMeta,
) -> Result<()> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 path".into()))?;
self.add_file_from_reader(dev, s, body, len, meta.mtime)
}
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, meta.mtime)
}
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.flush_dir_batches(dev)?;
self.list_path(dev, s)
}
fn getattr(&mut self, dev: &mut dyn BlockDevice, path: &Path) -> Result<crate::fs::FileAttrs> {
use crate::fs::{EntryKind, FileAttrs};
if path == Path::new("/") || path.as_os_str().is_empty() {
return Ok(FileAttrs::defaults_for(EntryKind::Dir, 0, 0));
}
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 path".into()))?;
self.flush_dir_batches(dev)?;
let (entry, _dir_cluster) = self.resolve_entry(dev, s)?;
let kind = if entry.attr & dir::ATTR_DIRECTORY != 0 {
EntryKind::Dir
} else {
EntryKind::Regular
};
let mut attrs = FileAttrs::defaults_for(kind, u64::from(entry.file_size), 0);
if entry.attr & dir::ATTR_READ_ONLY != 0 {
attrs.mode = match kind {
EntryKind::Dir => 0o555,
_ => 0o444,
};
}
attrs.mtime = entry.mtime;
attrs.atime = entry.mtime;
attrs.ctime = entry.mtime;
Ok(attrs)
}
fn set_attrs(
&mut self,
dev: &mut dyn BlockDevice,
path: &Path,
attrs: crate::fs::SetAttrs,
) -> Result<()> {
let Some(mode) = attrs.mode else {
return Ok(());
};
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 path".into()))?;
let read_only = (mode & 0o200) == 0;
self.set_entry_readonly(dev, s, read_only)
}
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()))?;
self.flush_dir_batches(dev)?;
let r = self.open_file_reader(dev, s)?;
Ok(Box::new(r))
}
fn open_file_ro<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &Path,
) -> Result<Box<dyn crate::fs::FileReadHandle + 'a>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 path".into()))?;
let (parent_cluster, leaf) = self.resolve_parent(dev, s)?;
let found = self
.find_entry(dev, parent_cluster, &leaf)?
.ok_or_else(|| crate::Error::InvalidArgument(format!("fat32: {s:?} not found")))?;
if found.entry.attr & dir::ATTR_DIRECTORY != 0 {
return Err(crate::Error::InvalidArgument(format!(
"fat32: {s:?} is a directory, not a file"
)));
}
let mutate::FoundEntry {
layout,
entry_pos,
entry,
..
} = found;
let inner = handle::FatFileHandle::open_existing(self, dev, &layout, entry_pos, entry)?;
Ok(Box::new(handle::ReadOnlyFatHandle::new(inner)))
}
fn open_file_rw<'a>(
&'a mut self,
dev: &'a mut dyn BlockDevice,
path: &Path,
flags: crate::fs::OpenFlags,
meta: Option<crate::fs::FileMeta>,
) -> Result<Box<dyn crate::fs::FileHandle + 'a>> {
let s = path
.to_str()
.ok_or_else(|| crate::Error::InvalidArgument("fat32: non-UTF-8 path".into()))?;
self.flush_dir_batches(dev)?;
let (parent_cluster, leaf) = self.resolve_parent(dev, s)?;
let existing = self.find_entry(dev, parent_cluster, &leaf)?;
let found = match existing {
Some(f) => {
if f.entry.attr & dir::ATTR_DIRECTORY != 0 {
return Err(crate::Error::InvalidArgument(format!(
"fat32: {s:?} is a directory, not a file"
)));
}
f
}
None => {
if !flags.create {
return Err(crate::Error::InvalidArgument(format!(
"fat32: {s:?} not found and `create` is false"
)));
}
if meta.is_none() {
return Err(crate::Error::InvalidArgument(
"fat32: open_file_rw with create=true requires meta".into(),
));
}
let mtime = meta.as_ref().map(|m| m.mtime).unwrap_or(0);
self.add_file_from_reader(dev, s, &mut crate::io::empty(), 0, mtime)?;
self.flush_dir_batches(dev)?;
self.find_entry(dev, parent_cluster, &leaf)?
.ok_or_else(|| {
crate::Error::InvalidImage(
"fat32: created file disappeared before open".into(),
)
})?
}
};
let mutate::FoundEntry {
layout,
entry_pos,
entry,
..
} = found;
let mut handle =
handle::FatFileHandle::open_existing(self, dev, &layout, entry_pos, entry)?;
if flags.truncate {
crate::fs::FileHandle::set_len(&mut handle, 0)?;
}
if flags.append {
use crate::io::Seek as _;
let len = crate::fs::FileHandle::len(&handle);
handle
.seek(crate::io::SeekFrom::Start(len))
.map_err(crate::Error::Io)?;
}
Ok(Box::new(handle))
}
fn flush(&mut self, dev: &mut dyn BlockDevice) -> Result<()> {
Self::flush(self, dev)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::MemoryBackend;
use crate::fs::{FileMeta, FileSource, Filesystem, OpenFlags};
use crate::io::{Seek as _, SeekFrom, Write as _};
fn fresh_volume() -> (MemoryBackend, Fat32) {
let mut dev = MemoryBackend::new(48 * 1024 * 1024);
let opts = FatFormatOpts {
total_sectors: 48 * 1024 * 1024 / 512,
volume_id: 0xCAFE_F00D,
volume_label: *b"OPENRWTEST ",
..Default::default()
};
let fs = Fat32::format(&mut dev, &opts).unwrap();
(dev, fs)
}
fn read_all(fs: &mut Fat32, dev: &mut dyn BlockDevice, path: &str) -> Vec<u8> {
let mut r = fs
.open_file_reader(dev, path)
.expect("open_file_reader for read_all");
let mut out = Vec::new();
r.read_to_end(&mut out).expect("read_to_end");
out
}
#[test]
fn geometry_small_volume() {
let g = Fat32::geometry(FatKind::Fat32, 131072, None).unwrap();
assert_eq!(g.spc, 1);
assert!(g.fat_size > 0);
assert!(g.clusters >= MIN_FAT32_CLUSTERS);
assert!(32 + 2 * g.fat_size + g.clusters * g.spc as u32 <= 131072);
assert!(g.fat_size * (SECTOR / 4) >= g.clusters + 2);
}
#[test]
fn geometry_rejects_tiny_volume() {
assert!(Fat32::geometry(FatKind::Fat32, 8192, None).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 ",
..Default::default()
};
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!(fs.fat.is_eoc(fs.fat.get(2)));
}
#[test]
fn open_rejects_oversized_fat_size() {
let (mut dev, _fs) = fresh_volume();
let mut bs = [0u8; 512];
dev.read_at(0, &mut bs).unwrap();
bs[36..40].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
dev.write_at(0, &bs).unwrap();
match Fat32::open(&mut dev) {
Err(crate::Error::InvalidImage(_)) => {}
other => panic!("expected InvalidImage, got {other:?}"),
}
}
#[test]
fn open_rejects_fat_too_small_for_cluster_count() {
let (mut dev, _fs) = fresh_volume();
let mut bs = [0u8; 512];
dev.read_at(0, &mut bs).unwrap();
bs[36..40].copy_from_slice(&1u32.to_le_bytes()); dev.write_at(0, &bs).unwrap();
match Fat32::open(&mut dev) {
Err(crate::Error::InvalidImage(msg)) => assert!(msg.contains("cannot map"), "{msg}"),
other => panic!("expected InvalidImage, got {other:?}"),
}
}
#[test]
fn names_longer_than_255_units_are_rejected() {
let (mut dev, mut fs) = fresh_volume();
let ok = "n".repeat(255);
let too_long = "n".repeat(256);
fs.add_file_from_reader(&mut dev, &format!("/{ok}"), &mut crate::io::empty(), 0, 0)
.unwrap();
for r in [
fs.add_file_from_reader(
&mut dev,
&format!("/{too_long}"),
&mut crate::io::empty(),
0,
0,
),
fs.add_dir(&mut dev, &format!("/{too_long}"), 0),
] {
match r {
Err(crate::Error::InvalidArgument(msg)) => assert!(msg.contains("limit"), "{msg}"),
other => panic!("expected InvalidArgument, got {other:?}"),
}
}
fs.flush(&mut dev).unwrap();
let mut fs2 = Fat32::open(&mut dev).unwrap();
let listed = fs2.list(&mut dev, Path::new("/")).unwrap();
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].name, ok);
}
#[test]
fn fat12_ignores_high_first_cluster_bits() {
use crate::io::Read as _;
let mut dev = MemoryBackend::new(1440 * 1024);
let opts = FatFormatOpts {
kind: FatKind::Fat12,
total_sectors: 2880,
..Default::default()
};
let mut fs = Fat32::format(&mut dev, &opts).unwrap();
assert_eq!(fs.kind(), FatKind::Fat12);
fs.add_file_from_reader(&mut dev, "/a.txt", &mut &b"abc"[..], 3, 0)
.unwrap();
fs.flush(&mut dev).unwrap();
let (parent, leaf) = fs.resolve_parent(&mut dev, "/a.txt").unwrap();
let found = fs.find_entry(&mut dev, parent, &leaf).unwrap().unwrap();
let off = found.layout.offset_of(found.entry_pos);
dev.write_at(off + 20, &0xABCDu16.to_le_bytes()).unwrap();
let mut fs2 = Fat32::open(&mut dev).unwrap();
let (entry, _) = fs2.resolve_entry(&mut dev, "/a.txt").unwrap();
assert!(entry.first_cluster < 0x1_0000, "{:#x}", entry.first_cluster);
assert_eq!(read_all(&mut fs2, &mut dev, "/a.txt"), b"abc");
let mut h = fs2.open_file_ro(&mut dev, Path::new("/a.txt")).unwrap();
let mut got = Vec::new();
h.read_to_end(&mut got).unwrap();
assert_eq!(got, b"abc");
}
#[test]
fn resolve_entry_rejects_a_file_as_a_path_prefix() {
let (mut dev, mut fs) = fresh_volume();
fs.create_file(
&mut dev,
Path::new("/a.txt"),
FileSource::Reader {
reader: Box::new(crate::io::Cursor::new(b"abc".to_vec())),
len: 3,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
match fs.resolve_entry(&mut dev, "/a.txt/b") {
Err(crate::Error::InvalidArgument(msg)) => {
assert!(msg.contains("not a directory"), "{msg}")
}
other => panic!("expected InvalidArgument, got {other:?}"),
}
assert!(fs.open_file_reader(&mut dev, "/a.txt/b").is_err());
}
struct CountingDev {
inner: MemoryBackend,
writes: usize,
}
impl crate::io::Read for CountingDev {
fn read(&mut self, buf: &mut [u8]) -> crate::io::Result<usize> {
self.inner.read(buf)
}
}
impl crate::io::Write for CountingDev {
fn write(&mut self, buf: &[u8]) -> crate::io::Result<usize> {
self.writes += 1;
self.inner.write(buf)
}
fn flush(&mut self) -> crate::io::Result<()> {
self.inner.flush()
}
}
impl crate::io::Seek for CountingDev {
fn seek(&mut self, pos: SeekFrom) -> crate::io::Result<u64> {
self.inner.seek(pos)
}
}
impl BlockDevice for CountingDev {
fn block_size(&self) -> u32 {
self.inner.block_size()
}
fn total_size(&self) -> u64 {
self.inner.total_size()
}
fn sync(&mut self) -> Result<()> {
self.inner.sync()
}
fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<()> {
self.writes += 1;
self.inner.write_at(offset, buf)
}
}
#[test]
fn flush_is_a_no_op_when_the_fat_is_clean() {
let (mem, _fs) = fresh_volume();
let mut dev = CountingDev {
inner: mem,
writes: 0,
};
let mut fs = Fat32::open(&mut dev).unwrap();
assert!(!fs.fat().is_dirty());
fs.flush(&mut dev).unwrap();
assert_eq!(dev.writes, 0, "clean flush wrote to the device");
fs.add_file_from_reader(&mut dev, "/a.txt", &mut &b"abc"[..], 3, 0)
.unwrap();
assert!(fs.fat().is_dirty());
fs.flush(&mut dev).unwrap();
assert!(!fs.fat().is_dirty());
let after_first = dev.writes;
assert!(after_first > 0);
fs.flush(&mut dev).unwrap();
assert_eq!(dev.writes, after_first, "second flush rewrote metadata");
{
let mut h = fs.open_file_ro(&mut dev, Path::new("/a.txt")).unwrap();
let mut buf = [0u8; 3];
crate::io::Read::read_exact(&mut h, &mut buf).unwrap();
assert_eq!(&buf, b"abc");
}
assert_eq!(dev.writes, after_first);
let mut fs2 = Fat32::open(&mut dev).unwrap();
assert_eq!(read_all(&mut fs2, &mut dev, "/a.txt"), b"abc");
}
fn short_name_of(fs: &mut Fat32, dev: &mut dyn BlockDevice, path: &str) -> [u8; 11] {
fs.flush_dir_batches(dev).unwrap();
let (parent, leaf) = fs.resolve_parent(dev, path).unwrap();
fs.find_entry(dev, parent, &leaf)
.unwrap()
.unwrap_or_else(|| panic!("{path} not found"))
.entry
.name_83
}
#[test]
fn empty_long_named_files_get_distinct_short_names() {
use crate::io::Read as _;
let (mut dev, mut fs) = fresh_volume();
let names = ["readme-one.txt", "readme-two.txt", "readme-three.txt"];
for n in names {
fs.add_file_from_reader(&mut dev, &format!("/{n}"), &mut crate::io::empty(), 0, 0)
.unwrap();
}
let staged: BTreeSet<[u8; 11]> = fs.pending_shorts[&fs.boot.root_cluster].clone();
assert_eq!(staged.len(), 3, "{staged:?}");
fs.flush(&mut dev).unwrap();
let mut fs2 = Fat32::open(&mut dev).unwrap();
let shorts: BTreeSet<[u8; 11]> = names
.iter()
.map(|n| short_name_of(&mut fs2, &mut dev, &format!("/{n}")))
.collect();
assert_eq!(shorts.len(), 3, "{shorts:?}");
assert!(!shorts.contains(b"FT000000 "), "{shorts:?}");
for (i, n) in names.iter().enumerate() {
let mut h = fs2
.open_file_rw(
&mut dev,
Path::new(&format!("/{n}")),
OpenFlags::default(),
None,
)
.unwrap();
h.write_all(format!("body {i}").as_bytes()).unwrap();
h.sync().unwrap();
}
for (i, n) in names.iter().enumerate() {
let mut got = Vec::new();
fs2.read_file(&mut dev, Path::new(&format!("/{n}")))
.unwrap()
.read_to_end(&mut got)
.unwrap();
assert_eq!(got, format!("body {i}").into_bytes(), "{n}");
}
}
#[test]
fn generated_short_name_probes_past_collisions() {
let (_dev, fs) = fresh_volume();
let name = "some long name.bin";
let seed = dir::short_name_seed(name);
let mut taken = BTreeSet::new();
assert_eq!(
fs.unique_short_name(name, 0, &taken).unwrap().0,
dir::generate_83(name, seed)
);
taken.insert(dir::generate_83(name, seed));
taken.insert(dir::generate_83(name, seed.wrapping_add(1)));
assert_eq!(
fs.unique_short_name(name, 0, &taken).unwrap().0,
dir::generate_83(name, seed.wrapping_add(2))
);
taken.insert(dir::generate_83(name, 77));
assert_eq!(
fs.unique_short_name(name, 77, &taken).unwrap().0,
dir::generate_83(name, 78)
);
assert_eq!(
fs.unique_short_name("HELLO.TXT", 0, &taken).unwrap(),
(*b"HELLO TXT", false)
);
}
#[test]
fn removed_name_can_be_recreated_in_the_same_session() {
use crate::io::Read as _;
let (mut dev, mut fs) = fresh_volume();
let body = |s: &str| FileSource::Reader {
reader: Box::new(crate::io::Cursor::new(s.as_bytes().to_vec())),
len: s.len() as u64,
};
fs.create_file(
&mut dev,
Path::new("/again.txt"),
body("first"),
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
fs.remove(&mut dev, "/again.txt").unwrap();
fs.create_file(
&mut dev,
Path::new("/again.txt"),
body("second"),
FileMeta::default(),
)
.expect("re-create after remove");
fs.remove(&mut dev, "/again.txt").unwrap();
fs.create_file(
&mut dev,
Path::new("/again.txt"),
body("third"),
FileMeta::default(),
)
.expect("re-create after unflushed remove");
fs.flush(&mut dev).unwrap();
let mut fs2 = Fat32::open(&mut dev).unwrap();
let listed = fs2.list(&mut dev, Path::new("/")).unwrap();
assert_eq!(listed.len(), 1, "{listed:?}");
let mut got = Vec::new();
fs2.read_file(&mut dev, Path::new("/again.txt"))
.unwrap()
.read_to_end(&mut got)
.unwrap();
assert_eq!(got, b"third");
}
#[test]
fn files_of_4gib_or_more_are_rejected() {
let (mut dev, mut fs) = fresh_volume();
let free_before = fs.count_free_clusters();
let err = fs
.add_file_from_reader(&mut dev, "/big.bin", &mut crate::io::empty(), 1u64 << 32, 0)
.unwrap_err();
assert!(matches!(err, crate::Error::InvalidArgument(_)), "{err:?}");
assert_eq!(fs.count_free_clusters(), free_before);
assert!(!fs.pending_names.contains_key(&fs.boot.root_cluster));
let mut h = fs
.open_file_rw(
&mut dev,
Path::new("/h.bin"),
OpenFlags {
create: true,
..Default::default()
},
Some(FileMeta::default()),
)
.unwrap();
h.seek(SeekFrom::Start(u64::from(u32::MAX) - 1)).unwrap();
let err = h.write(b"abcd").unwrap_err();
assert_eq!(err.kind(), crate::io::ErrorKind::InvalidInput, "{err}");
assert_eq!(h.len(), 0);
}
#[test]
fn flush_skips_backup_boot_region_when_absent() {
let (mut dev, _fs) = fresh_volume();
let mut bs = [0u8; 512];
dev.read_at(0, &mut bs).unwrap();
bs[50..52].copy_from_slice(&0u16.to_le_bytes()); dev.write_at(0, &bs).unwrap();
dev.zero_range(6 * 512, 2 * 512).unwrap();
let mut fs = Fat32::open(&mut dev).unwrap();
assert_eq!(fs.boot_sector().backup_boot_sector, 0);
fs.create_file(
&mut dev,
Path::new("/a.txt"),
FileSource::Reader {
reader: Box::new(crate::io::Cursor::new(b"abc".to_vec())),
len: 3,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
let mut scrubbed = [0u8; 2 * 512];
dev.read_at(6 * 512, &mut scrubbed).unwrap();
assert!(
scrubbed.iter().all(|&b| b == 0),
"backup region was written"
);
let mut head = [0u8; 512];
dev.read_at(0, &mut head).unwrap();
assert_eq!(&head[510..512], &[0x55, 0xAA]);
assert_eq!(&head[82..87], b"FAT32");
let mut fsinfo = [0u8; 4];
dev.read_at(512, &mut fsinfo).unwrap();
assert_eq!(fsinfo, *b"RRaA");
let mut fs2 = Fat32::open(&mut dev).unwrap();
assert_eq!(read_all(&mut fs2, &mut dev, "/a.txt"), b"abc");
}
#[test]
fn open_file_rw_partial_write_round_trip() {
let (mut dev, mut fs) = fresh_volume();
let initial = vec![0xAAu8; 200];
fs.create_file(
&mut dev,
Path::new("hello.bin"),
FileSource::Reader {
reader: Box::new(crate::io::Cursor::new(initial.clone())),
len: 200,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
let patch = [0x55u8; 16];
{
let mut h = fs
.open_file_rw(&mut dev, Path::new("hello.bin"), OpenFlags::default(), None)
.unwrap();
h.seek(SeekFrom::Start(100)).unwrap();
h.write_all(&patch).unwrap();
h.sync().unwrap();
}
let got = read_all(&mut fs, &mut dev, "hello.bin");
assert_eq!(got.len(), 200);
assert!(got[..100].iter().all(|&b| b == 0xAA));
assert_eq!(&got[100..116], &patch);
assert!(got[116..].iter().all(|&b| b == 0xAA));
}
#[test]
fn set_attrs_read_only_round_trip() {
use crate::fs::SetAttrs;
let (mut dev, mut fs) = fresh_volume();
let mtime = 1_615_779_298u32;
fs.create_file(
&mut dev,
Path::new("ro.txt"),
FileSource::Reader {
reader: Box::new(crate::io::Cursor::new(vec![0x42u8; 64])),
len: 64,
},
FileMeta {
mtime,
..FileMeta::default()
},
)
.unwrap();
fs.flush(&mut dev).unwrap();
let base = fs.getattr(&mut dev, Path::new("ro.txt")).unwrap();
assert_eq!(base.mode, 0o644);
let want_mtime = base.mtime;
assert_ne!(want_mtime, 0);
fs.set_attrs(
&mut dev,
Path::new("ro.txt"),
SetAttrs {
mode: Some(0o444),
..SetAttrs::default()
},
)
.unwrap();
fs.flush(&mut dev).unwrap();
let mut fs = Fat32::open(&mut dev).unwrap();
let a = fs.getattr(&mut dev, Path::new("ro.txt")).unwrap();
assert_eq!(a.mode, 0o444);
assert_eq!(a.mtime, want_mtime);
fs.set_attrs(
&mut dev,
Path::new("ro.txt"),
SetAttrs {
mode: Some(0o644),
..SetAttrs::default()
},
)
.unwrap();
fs.flush(&mut dev).unwrap();
let mut fs = Fat32::open(&mut dev).unwrap();
let a = fs.getattr(&mut dev, Path::new("ro.txt")).unwrap();
assert_eq!(a.mode, 0o644);
assert_eq!(a.mtime, want_mtime);
}
#[test]
fn open_file_rw_extends_file() {
let (mut dev, mut fs) = fresh_volume();
let initial = vec![0x11u8; 50];
fs.create_file(
&mut dev,
Path::new("grow.bin"),
FileSource::Reader {
reader: Box::new(crate::io::Cursor::new(initial)),
len: 50,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
let pattern: Vec<u8> = (0..1024u32).map(|i| (i & 0xFF) as u8).collect();
{
let mut h = fs
.open_file_rw(&mut dev, Path::new("grow.bin"), OpenFlags::default(), None)
.unwrap();
assert_eq!(h.len(), 50);
h.seek(SeekFrom::Start(2000)).unwrap();
h.write_all(&pattern).unwrap();
assert_eq!(h.len(), 2000 + 1024);
h.sync().unwrap();
}
let got = read_all(&mut fs, &mut dev, "grow.bin");
assert_eq!(got.len(), 3024);
assert!(got[..50].iter().all(|&b| b == 0x11));
assert!(got[50..2000].iter().all(|&b| b == 0));
assert_eq!(&got[2000..], &pattern[..]);
}
#[test]
fn open_file_rw_set_len_grow_and_shrink() {
let (mut dev, mut fs) = fresh_volume();
let initial = vec![0x77u8; 128];
fs.create_file(
&mut dev,
Path::new("resize.bin"),
FileSource::Reader {
reader: Box::new(crate::io::Cursor::new(initial)),
len: 128,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
{
let mut h = fs
.open_file_rw(
&mut dev,
Path::new("resize.bin"),
OpenFlags::default(),
None,
)
.unwrap();
h.set_len(4096).unwrap();
assert_eq!(h.len(), 4096);
h.sync().unwrap();
}
let after_grow = read_all(&mut fs, &mut dev, "resize.bin");
assert_eq!(after_grow.len(), 4096);
assert!(after_grow[..128].iter().all(|&b| b == 0x77));
assert!(after_grow[128..].iter().all(|&b| b == 0));
{
let mut h = fs
.open_file_rw(
&mut dev,
Path::new("resize.bin"),
OpenFlags::default(),
None,
)
.unwrap();
h.set_len(64).unwrap();
assert_eq!(h.len(), 64);
h.sync().unwrap();
}
let after_shrink = read_all(&mut fs, &mut dev, "resize.bin");
assert_eq!(after_shrink.len(), 64);
assert!(after_shrink.iter().all(|&b| b == 0x77));
}
#[test]
fn open_file_rw_append() {
let (mut dev, mut fs) = fresh_volume();
let initial = b"head".to_vec();
fs.create_file(
&mut dev,
Path::new("app.txt"),
FileSource::Reader {
reader: Box::new(crate::io::Cursor::new(initial.clone())),
len: initial.len() as u64,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
{
let mut h = fs
.open_file_rw(
&mut dev,
Path::new("app.txt"),
OpenFlags {
append: true,
..OpenFlags::default()
},
None,
)
.unwrap();
h.write_all(b"-tail").unwrap();
h.sync().unwrap();
}
let got = read_all(&mut fs, &mut dev, "app.txt");
assert_eq!(got, b"head-tail");
}
#[test]
fn open_file_rw_create_new() {
let (mut dev, mut fs) = fresh_volume();
{
let mut h = fs
.open_file_rw(
&mut dev,
Path::new("brand-new.dat"),
OpenFlags {
create: true,
..OpenFlags::default()
},
Some(FileMeta::default()),
)
.unwrap();
assert_eq!(h.len(), 0);
h.write_all(b"hello from rw create").unwrap();
h.sync().unwrap();
}
let got = read_all(&mut fs, &mut dev, "brand-new.dat");
assert_eq!(got, b"hello from rw create");
match fs.open_file_rw(&mut dev, Path::new("never.bin"), OpenFlags::default(), None) {
Ok(_) => panic!("expected error for non-existent path with create=false"),
Err(crate::Error::InvalidArgument(_)) => {}
Err(e) => panic!("unexpected error: {e:?}"),
}
}
#[test]
fn batched_many_files_one_dir_round_trip() {
let (mut dev, mut fs) = fresh_volume();
fs.create_dir(&mut dev, Path::new("/d"), FileMeta::default())
.unwrap();
let n = 50usize;
for i in 0..n {
let body = format!("file-body-{i:03}");
fs.create_file(
&mut dev,
&crate::path::PathBuf::from(format!("/d/f{i:03}.txt")),
FileSource::Reader {
reader: Box::new(crate::io::Cursor::new(body.clone().into_bytes())),
len: body.len() as u64,
},
FileMeta::default(),
)
.unwrap();
}
fs.flush(&mut dev).unwrap();
let mut fs2 = Fat32::open(&mut dev).unwrap();
let listed: BTreeSet<String> =
crate::fs::Filesystem::list(&mut fs2, &mut dev, Path::new("/d"))
.unwrap()
.into_iter()
.map(|e| e.name)
.collect();
assert_eq!(listed.len(), n, "expected {n} files, got {listed:?}");
for i in 0..n {
let name = format!("f{i:03}.txt");
assert!(listed.contains(&name), "missing {name}");
let got = read_all(&mut fs2, &mut dev, &format!("/d/{name}"));
assert_eq!(got, format!("file-body-{i:03}").into_bytes());
}
}
#[test]
fn open_file_ro_random_seek_fat() {
use crate::io::Read as _;
let (mut dev, mut fs) = fresh_volume();
let data: Vec<u8> = (0..16_384u32).map(|i| (i & 0xFF) as u8).collect();
fs.create_file(
&mut dev,
Path::new("ro.bin"),
FileSource::Reader {
reader: Box::new(crate::io::Cursor::new(data.clone())),
len: data.len() as u64,
},
FileMeta::default(),
)
.unwrap();
fs.flush(&mut dev).unwrap();
let mut h = fs
.open_file_ro(&mut dev, Path::new("ro.bin"))
.expect("open_file_ro");
assert_eq!(h.len(), data.len() as u64);
assert!(!h.is_empty());
h.seek(SeekFrom::Start(9000)).unwrap();
let mut buf = [0u8; 64];
h.read_exact(&mut buf).unwrap();
assert_eq!(&buf[..], &data[9000..9064]);
h.seek(SeekFrom::Start(123)).unwrap();
let mut buf2 = [0u8; 32];
h.read_exact(&mut buf2).unwrap();
assert_eq!(&buf2[..], &data[123..155]);
}
}