Redesigning the filesystem crate
📅 2025-05-18 · ✍️ Bas v.d. Wiel · 🏷 design
One of the hardest parts of this project is learning Rust. It’s also one of my
main personal goals with this one. Rust is hardly forgiving, but using it made
me a much better developer over the past few months. Unfortunately that also meant
not having much progress to show for all my effort. Right now, I ran into my own
code for the filesystem
crate. That’s the part of DOSContainer that handles
everything to do with FAT. I’m essentially reimplementing the FAT filesystem in a
very unorthodox way. Here’s why that is so complicated.
FAT in itself is not a difficult system, fortunately. The complexity for me arises from learning a completely new programming language while at the same time aiming tom implement literally ALL of FAT’s history. From every tiny nook and cranny that started with IBM PC-DOS 1.00 in 1981 all the way up to FAT32 as it was delivered in Windows 95 OSR2. That’s decades of history and no source code, let alone version control. I’m on my own trying to reverse-engineer file systems from actual operating system behavior as observed through actual use.
The design of my code
The fact that I’m looking to reimplement all of FAT also means there’s no other codebase I can really lean on. No sensible programmer in 2025 is going to implement FAT in a way that lines up precisely with everything that was ever published by anyone in the DOS operating system market over the past four decades. So I’m now leaning into Rust, figuring out how to design proper software while at the same time figuring out why file system programming is hard.
My current implementation centers on a few Rust structs:
- AllocationTable
- Pool
- DirEntry
Tying these three together is the Fat12
struct (currently) as there will be many
others. What that boils down to right now:
pub struct Fat12 {
pool: Pool,
}
Simple, right? A Pool
is a collection of DirEntry
instances that represent
files, directories and volume labels for now. Each of these has a unique ID which the
Uuid
crate provides me with:
pub struct DirEntry {
uid: Uuid,
parent: Option<Uuid>,
attributes: Attributes,
name: Option<EntryName>,
creation_time: NaiveDateTime,
start_cluster: ClusterIndex,
file_size: usize,
can_be_parent: bool,
}
The Pool
does some gatekeeping in that it assures no entries get added that don’t follow
the rules. Must be a valid DirEntry to begin with, and the parent must exist and be able to accept
child-entries.
The EntryName
struct abstracts away everything to do with validating FAT-compatible file
names and it currently does so quite badly, unfortunately.
These DirEntry
structures are one part of the equation. They permit laying out actual files and
directories inside the data area of the disk. The File Allocation Table is another structure that needs
careful maintenance:
pub struct AllocationTable {
clusters: BTreeMap<ClusterIndex, ClusterValue>,
cluster_count: usize,
fat_type: FatType,
}
This is the base structure that keeps track of which on-disk clusters are allocated and which are not. It’s a very simple linked list with a small number of specials:
pub enum ClusterValue {
Next(ClusterIndex),
EndOfChain,
Free,
Reserved,
Bad,
}
These special values make sense in code, and will be translated down to specific numerical values for the on-disk final representation.. where they differ from vendor to vendor and version to version. That’s why I’m aiming to design the file system’s initial representation as abstractly as I can: use generic terms, create types for things that are more complicated than a simple number etc.
Anyway, just a quick peek into what I’m doing. Eventually this will result in a working PC-DOS 1.00 floppy, which will also mark the DOSContainer 1.00 release. I bit of quite a bit more than I thought I had by starting this project a year and a half ago, but so far I’m learning a lot and having fun!