# FAT filesystem

The unit at [fs/fat.fs] implements the Filesystem API [fs/core] for the FAT
filesystem. It supports FAT12 and FAT16 but is still quite rough around the
edges.

* No FAT32
* No long filenames. It does gracefully ignore their presence though.
* No cluster reallocation. You are constantly leaking them through usage.

## Usage

You mount a FAT through "newfatfs ( blk -- fs )". For a given Blk [io/blk]
"blk" where the first sector has a proper FAT header, this call will yield a
struct that extends Filesystem and implements in entirely.

You might also be interested in [doc/fs/fatt], the "FAT tools" unit.

## FSID

The FSID of a FAT mount has this structure:

b31:16 cluster
b15:0  direntry index

These two number represent the cluster of the parent directory of the target
file or directory, and the index indicates where it appears in the parent
directory's direntry listing.

For example, a file "foo/bar" will have a FSID of $002a000b if "foo" lives on
cluster $2a and that it contains 12 children, "bar" being the last one.

The cluster part of a FSID really is the first cluster of the parent directory.
This means that it's not necessarily the cluster where the target entry lives.

When the cluster part of the FSID is zero, it means that this element lives in
the root directory. In this case, there's an exception: indexes begin at 1
instead of 0. This is done to disambiguate between the root directory (plain 0)
and the first element of it.

This scheme gives us an anchor to the parent of any file or directory, allowing
us to implement the "parent" method without needing expensive reverse lookups.

## direntry

In the code, a "direntry" is the address of a FAT entry structure in memory.
These references are short-lived because they're an address in the FAT sector
buffer which is very very often being overwritten.

File and directory IDs in FAT are the offset, on disk, of their corresponding
direntry.

## API

newfatfs ( blk -- fs )
  Initialize a new FATFS structure and yield it.

fatstorage! ( blk fs -- )
  Change the underlying storage for FAT fs "fs". You generally don't need to do
  that, but there's an exception with PCs, which boot using [drv/pc/int13h] and
  later need to upgrade to a "real" storage driver. Rather than creating a new
  FS structure entirely, it's better to just swap the underlying storage.

## Private API

### Structures

extends FS struct FATFS
  fields fatpart lastfreecluster
  fields fatsecsz secpercluster reservedseccnt FATcnt rootentcnt fatseccnt FATsz
endstruct

fatpart         A [io/part] targeting the FAT sectors
lastfreecluster Cache of last findfreecluster result

fields below are read-only unpacking of FAT header fields

fatsecsz       +11 BPB_BytsPerSec
secpercluster  +13 BPB_SecPerClus
reservedseccnt +14 BPB_RsvdSecCnt
FATcnt         +16 BPB_NumFATs
rootentcnt     +17 BPB_RootEntCnt
fatseccnt      +19 BPB_TotSec16
FATsz          +22 BPB_FATsz16

extends Stream struct FATFile
  fields cl0 clidx curcl adirty id fat
endstruct

cl0    First cluster of the file
clidx  Index in the chain where "curcl" is
curcl  Cluster containing current position
adirty Attributes dirty. When 1, must update entry on flush
id     FSID of file
fat    Pointer to parent FATFS struct

### Words

FAT@ ( cluster fs -- entry )
  Yield the entry present in in the FAT table at the specified cluster index.
  Automatically chooses between the 12-bit lookup or the 16-bit one.

  Note that clusters 0 and 1 are invalid.

FAT! ( entry cluster fs -- )
  Set entry in FAT table at specified cluster index to specified value.

nFAT@ ( cluster n fs -- entry f )
  Iterate cluster n times through FAT@ or as long as EOC is not reached.

  IF EOC is not reached, f=1 and entry is the result. Otherwise, f=0 and entry
  is the last cluster of the chain.

clusterpos ( cluster fs -- off )
  Yields the offset in bytes, in the FAT storage blk, where the specified
  cluster begins.

  Errors out if cluster is out of range.

### The direntry globals

There's a couple of words that are about iterating, reading and writing direntry
record and they're all related. They're also all static and global, which means
that only one direntry can be worked on at once.

First, there's "dirent", a static buffer DIRENTRYSZ in size. This is where
direntries are read and this is what we write back to storage.

Then, there's the iteration globals: efs, eid, eoff, ecl. Their goal is to allow
iterating direntries starting from a particular point. It's not an easy problem
to solve because direntries of the same iteration can span across multiple
clusters.

eid! ( id fs -- f )
  We could say it's the main word of this little subsystem. It figures out where
  the specified ID lives of disk and sets all iteration globals, eoff being the
  offset of that direntry on storage, ecl being its cluster.

  If the specified ID has a 0 ckuster component, it means it's a root FS
  element. ecl is then set to 0 and eoff is set to the root dir section of the
  FAT.

  If the index component ends up is smaller than what is allowed in its cluster
  chain, yield f=1, eid, eoff and ecl being valid.

  Otherwise, yield f=0. eoff will then be invalid, ecl will be the last cluster
  of the chain, eid will be the invalid id that we were trying to set.

  Aborts with error if "id" has an out of bounds cluster.

readentry ( -- )
  Read contents of eoff into "dirent", a static 32-bytes buffer.

writeentry ( -- )
  Write contents of "dirent" into eoff.

iterentry ( -- f )
  Increase eid by 1 and increase eoff appropriately. If we cross a cluster
  boundary, correctly change ecl. Yield f=1 unless EOC was encountered when
  iterating ecl.

## See also

* FAT: General Overview of On-Disk Format
