SchOS/docs/architecture/fat.md
Nexoniarz edb560de66 fat: FAT12, FAT16 and FAT32 with long names, read and write
The filesystem every other system can read: a USB stick, an SD card, the
EFI system partition. It is here first for the hardware report, which has
to leave a machine on something that can be read on another one.

  mount -t vfat /dev/usb0p1 /mnt        (msdos and fat work too; -o ro)

Written from Microsoft's specification, and matched to Linux vfat where
the specification leaves a choice: 8.3 names with the NT lowercase bits,
long-name entries otherwise, Linux's alias rule (REPORTS, then BIGNUM~1),
case-insensitive lookup, UTC times, the boot-sector dirty bit, every FAT
copy and FSInfo kept up to date. It uses its own sector cache over the
new blkdev_xfer, since a partition need not be a whole number of 4 KB
blocks, and every operation that changes something reaches the disk
before it returns. Inode numbers come from the entry position, or the
first cluster for a directory, plus a small table for entries that moved.

Around it:
- /dev lists every disk and partition the block layer found (major 8,
  minor = block device), and raw reads and writes on them go by sector.
- The ESP and MBR FAT partitions are registered, typed so the root probe
  passes over them. MBR entries with a bad status byte are skipped, so a
  superfloppy's boot code is not read as a partition table.
- statfs, sync, fsync, umount (EBUSY while in use), shutdown, reboot and
  the power button all know about FAT.
- create undoes and reports a failed ialloc or dirlink (ENOSPC, EINVAL,
  EPERM) instead of panicking; fs_ops gains refuse_types and no_hardlinks.
- filewrite returns a short write instead of panicking on it.
- utimensat is implemented. As a stub returning 0 it made toybox touch
  believe the file existed, so `touch newfile` created nothing on any
  filesystem.

Tested in QEMU against mkfs.vfat images (FAT12 floppy, FAT16 on MBR,
FAT32 superfloppy and GPT ESP) by writing, renaming, deleting, filling to
ENOSPC and truncating there, then checking with fsck.fat -n and mtools on
the host: all clean, contents match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 09:33:35 +02:00

105 lines
5.1 KiB
Markdown

# FAT
FAT12, FAT16 and FAT32 with VFAT long names, read and write.
`kernel/fs/fat/fat.c`, behind the same `fs_ops` vtable as SchFS.
It exists for one reason: it is the filesystem every other system can read. A
USB stick written here opens on Linux, Windows and macOS, and so does the EFI
system partition. The first user is the hardware report, which has to leave a
machine that may have no network on something you can carry to another one.
```
mount -t vfat /dev/usb0p1 /mnt # msdos and fat are accepted as well
mount -t vfat -o ro /dev/sata0p1 /boot/efi
umount /mnt
```
The disks and partitions the block layer found are in `/dev` under the names
it gave them (`usb0`, `usb0p1`, `sata0p2`, `nvme0p1`), generated like the rest
of devtmpfs: a stick appears when it is plugged in and goes when it is pulled.
---
## Written from the specification, matched to Linux
The on-disk rules come from Microsoft's FAT specification (1.03). Where the
specification leaves a choice, the choice is Linux's vfat, so that a volume
written here and one written by Linux look alike:
- **The FAT type is decided by the cluster count and nothing else**: under 4085
is FAT12, under 65525 FAT16, otherwise FAT32. The "FAT32" label in the boot
sector is a comment.
- **Short names.** A name that fits 8.3 in one case (`readme.txt`, `README.TXT`)
is stored as a short entry alone, with the lowercase bits in byte 12 that
Windows NT and Linux use. A name that does not (`ReadMe.txt`, `Reports`,
`big numbers.log`) gets long-name entries plus an alias: its uppercase self
when that loses nothing (`REPORTS`), otherwise `BIGNUM~1.LOG`, counting up
until it is unique.
- **Lookups ignore case** (ASCII and Latin-1), and a name with a long entry
still answers to its alias, so `PROGRA~1` works.
- **Times are stored as UTC.** FAT has no time zone, and that is what Linux
assumes when it is not told one.
- **The dirty bit.** Set in the boot sector while a volume is mounted
read-write, cleared on unmount, sync-on-shutdown and reboot. A volume found
with it set is reported (`fsck.fat` reads the same bit).
- **Every FAT copy is written**, unless the volume has mirroring turned off, in
which case only the active one is. FSInfo's free count and next-free hint are
read and kept up to date.
- **Refused, with Linux's errno**: device nodes, symlinks and sockets (`EPERM`,
through `fs_ops.refuse_types`), hard links (`EPERM`, `fs_ops.no_hardlinks`),
and names containing `" * / : < > ? \ |` (`EINVAL`).
Checked against `mkfs.vfat`-made FAT12, FAT16 (MBR) and FAT32 (superfloppy and
GPT ESP) volumes: written here, then `fsck.fat -n` and mtools on the host.
## Inodes, on a filesystem that has none
A FAT file *is* its directory entry: name, first cluster, size and times are all
in those 32 bytes. The inode number says where to find it:
| object | number |
|--------|--------|
| the root | `1` |
| a directory | `2 << 40` \| its first cluster |
| a file | `1 << 40` \| byte position of its short entry / 32 |
| a file created here, or one whose number is taken | `3 << 40` \| a counter |
A directory is numbered by its cluster because `..` must be able to find it,
and `..` holds the parent's cluster and nothing about where the parent's entry
is. A file is numbered by its entry, as Linux does it.
The VFS renames by linking the new name to the same number and then unlinking
the old one, so a file's entry can move while its number stays. A small
per-volume table records every number whose entry is not where the number says.
It is consulted first in both directions, and anything nobody holds can be
dropped from it, since the next lookup by name finds the entry afresh.
## I/O
Through a cache of its own (64 lines of 4 KB per volume), not the buffer cache.
FAT addresses sectors, a partition need not be a whole number of 4 KB blocks,
and the buffer cache's last block of one would read and write into the next
partition. `blkdev_xfer` takes sectors and refuses a transfer past the end
rather than panicking.
**Every operation that changes something writes its dirty lines back before it
returns.** A stick pulled out between two commands holds both of them. `sync`
and `fsync` also flush the drive's own cache. Unmount, shutdown, reboot and the
power button sync and mark the volume clean.
A new cluster is zeroed in the cache before use. FAT has no holes, and a
directory ends at the first entry whose first byte is zero.
## What it does not do
- **Names longer than 29 bytes.** FAT allows 255 UTF-16 units, but the VFS's
`DIRSIZ` caps a path component, so a longer name is cut short on the way in.
Lifting that is a VFS change.
- **Permissions and owners.** Every file shows as `0755 root:root`, like Linux
vfat with its defaults. The read-only attribute is kept but not enforced.
- **exFAT.** Large SD cards (SDXC, over 32 GB) ship with it. It is a different
filesystem, not a FAT variant.
- **Code pages.** Short names are read as CP437 and written as ASCII; anything
else goes in a long name.
- **Directories don't move**, which is the VFS's rule for every filesystem.
- **No `fsck`.** A volume found dirty is reported, not repaired.