(c)  Copyright 1989 Commodore-Amiga, Inc.   All rights reserved.
The information contained herein is subject to change without notice, and 
is provided "as is" without warranty of any kind, either expressed or implied.  
The entire risk as to the use of this information is assumed by the user.




                  Low Level Differences Between FFS 
                     and the ROM Filing System

                         by Steve Beats


Most developers know by now that a Fast Filing System (FFS) for hard disks 
has been added to V1.3 of the Amiga system software.  The following are 
descriptions and 'C' structure definitions representing the current layout 
of data on both the new FFS and the old ROM filing system disks.  

These structures are provided for those of you who wish to write disk salvage
utilities and have a valid requirement for this information.  It is also quite
useful to have this information handy if you've managed to trash a partition
and only have DiskEd available to attempt fixing it.  Please note that these
structures assume a fixed, 512 byte disk block.  This is a valid assumption 
for filing systems up to V1.3.  However, this will NOT be the case under
V1.4 Kickstart which will support full variable-sized disk blocks.

DiskDoctor-type utilities should run as many DOS checks as possible before
assuming a disk has the layout described here.  Check the DOS type field on
the boot block and run checksums on the root block after ensuring that the
static fields are set to the expected values.  Hash table size will always
be set to 72 for instance, this can be used as one of the consistency checks
before proceeding to directly read from or write to the disk.

NEVER use this knowledge in an application program.  It is guaranteed to
break under V1.4 Kickstart.  We are moving away from the single file system
environment, especially since it's so easy to create and mount alternate
filing systems on the Amiga.  Applications should stick strictly to the DOS
library routines or the packet interface to a filing system.  Any other method
of accessing files from the disk will not work.





The Boot Block
--------------

The boot block is always on the first sector of a partition.  This is
calculated as LowCyl * Surfaces * BlocksPerTrack.  The only field of any 
use in the boot block is the first ULONG.  This should contain the string 
"DOS" terminated by a 0 for the old filing system or a 1 for the fast filing 
system.  Generally this should be the only field that needs checking to 
determine file system type.  

However, if the first cylinder of a partition has gone bad, this field may 
be invalid.  In this case, it is possible to use just the root block and the 
data blocks to determine file system type.  Under V1.3 Workbench, DOSType
can be determined from the environment vector for the given partition
so this is a good alternative to snooping other parts of the disk.

On floppy drives, the boot block will also contain boot code which is
responsible for initializing DOS when booting from a floppy.  Make sure this 
data remains valid, or advise the user to run Install before using the 
"doctored" disk again.  On hard disks, there is no code executed from the 
boot block, it is the responsibility of an autobooting hard disk driver to do 
the DOS intitialization.  In this case, it's OK to write back the boot block 
with just the DOSx identifier initialized.


Root Block
----------

This is the most important block as far as the filing system is concerned.
It is called the root block because it is actually the "root" of the filing 
system tree.  All other files and directories are accessed by keying them 
off the root block.  Even if a file is stored many levels deep in the 
directory structure, it is ultimately connected to the root through its
parent directories.

If the root block of a disk goes bad, then the whole partition will have to 
be scanned for "orphaned" files and directories.  The links to these 
recovered files will have to be rebuilt in a new root block which can then 
be written back to disk.  

The root is positioned such that the worst case seek to get to the root will 
be no more than half the number of cylinders in the partition.  In other 
words, the root is in the middle of the partition.  The disk block that the 
root is stored on is calculated as follows:

	LowerKey = LowerCyl * Surfaces * BlocksPerTrack + Reserved
	UpperKey = ((UpperCyl+1) * Surfaces * BlocksPerTrack) - 1
	RootKey  = (UpperKey - LowerKey) / 2

Note that the UpperKey used here may not be the physical UpperKey used by 
an FFS partition if PreAlloc is set to something other than 0.  Since the use 
of PreAlloc was a new feature of FFS it was not included in the calculation 
of the root block so as to maintain compatibility with the Format and DiskEd 
commands.  If an FFS partition is being used, UpperKey should be modified as 
UpperKey -= PreAlloc AFTER performing the root block calculation.  The values 
of UpperKey and LowerKey will be useful for checking extension and hash 
chains for validity.

   Note: all block numbers stored within the filing system are relative to
   the beginning of the partition not including the Reserved blocks.  This
   means that the physical disk block = LowerKey + Block# - Reserved.  Since
   a block number of 0 is used to terminate lists, the zero block can never 
   be made available to the filing system.  Hence the need for at least 1
   "Reserved" block on all partitions.

Here is the 'C' structure definition of a 512 byte root block as used by
both FFS and OLDFS partitions or floppies.  It is very similar to a 
UserDirectoryBlock but some of the fields are different or used in a 
different manner by the filing system.




   struct RootBlock {
	LONG	Type;		/* This is used to mark the type of this
				   block and is set to TYPE_SHORT (2) */
	ULONG	OwnKey;		/* Not used by root, must be set to 0 */
	ULONG	SeqNum;		/* Not used by root, must be set to 0 */
	ULONG	HTSize;		/* Size of the hash table in longwords,
				   must be set to 72 */
	ULONG	Nothing1;	/* reserved for future revs, must be 0 */
	LONG	Checksum;	/* balance to 0 checksum.  When all longs
				   in the block are added (ignoring carry)
				   the sum should be 0 */
	ULONG	HashTable[72];	/* hash table containing block numbers of
				   files and directories in the root */
	LONG	BitmapFlag;	/* flag to say whether the bitmap is valid
				   or not.  -1=valid. 0=invalid.  If a
				   partition is mounted (or uninhibited)
				   with BitmapFlag = 0 then the validator
				   will kick in to rebuild the bitmap */

/* the BitmapKeys and BitmapExtend fields shown here are valid for FFS only.
   The ROM filing system has these two entries reversed and the slots in the
   BitmapKeys array are used from BitmapKeys[24] to BitmapKeys[0] */

	ULONG	BitmapKeys[25];	/* An array of block numbers for the bitmap
				   on this partition.  A block number of 0
				   indicates the end of the list. */
	ULONG	BitmapExtend;	/* If all of the BitmapKeys have been used
				   then this will contain the block number
				   of a disk block containing more Bitmap-
				   Keys.  0 means no extender block present.
				   OLDFS has a bug where it assumes that
				   the root has protection bits and attempts
				   to set them.  Since they coincide with this
				   field, this is why there is a 53Meg limit
				   on partition size under OLDFS */
	ULONG	DirAltered[3];	/* A DOS DateStamp indicating the date when
				   the root block was last modified or a
				   file in the root block was modified */
	char	Name[40];	/* Name of this volume as a BCPL string
				   with number of bytes as the first
				   character.  Only 30 chars used */
	ULONG	DiskAltered[3];	/* A DOS DateStamp indicating the date when
				   any file or section of the partition was
				   modified.  (FFS has a bug which prevents
				   it from updating this correctly, the
				   DirAltered date gets changed instead) */
	ULONG	DiskMade[3];	/* A DOS DateStamp indicating the date when
				   this partition was first formatted and
				   therefore created */
	ULONG	Nothing2;	/* reserved for future revs, must be 0 */
	ULONG	Nothing3;	/* reserved for future revs, must be 0 */
	ULONG	Nothing4;	/* reserved for future revs, must be 0 */
	LONG	SecondaryType;	/* Qualifier to Type.  Will be set to
				   ST_ROOT (1) */
};


Bitmap Extension
----------------

Bitmap extensions, which are pointed to by the BitmapExtend entry in the root 
block, are very simple in structure.  They consist of a list of 127 pointers 
to bitmap blocks with the last entry being a pointer to another bitmap
extender block.  There are no checksums on this data at all.  An entry of
0 terminates the list.


Bitmap blocks
-------------

Bitmap blocks are pointed to by the root blocks BitmapKeys array and also
by any subsequent bitmap extender blocks if the disk is large enough.

Each bitmap block contains a longword sum-to-zero followed by 127 longs of 
bitmap information (i.e., the 128 longwords in the bitmap block will sum up 
to zero on a valid disk).  This means that each bitmap block can hold enough
information for 127*32 disk blocks or just less than 2 Megabytes of disk
space.  

The bits in the bitmap do not correspond exactly to the way that the blocks 
are arranged on the disk.  This is an artifact caused by the method in which 
the bitmap is accessed.  When determining or setting the state of a bitmap 
entry, the filing system fetches the data a longword at a time and then uses 
the block number modulo 32 to determine which bit in that longword is to be 
examined.  

This means that bit 0 of longword 0 corresponds to block LowerKey while bit 0 
of longword 1 corresponds to block LowerKey+32.  If you were to examine the 
bitmap left to right one bit at a time, the bits would appear to be backwards
on 32 bit boundaries.  However, if you access the bits logically using the 
68000 bit manipulation instructions, left to right ordering is preserved.

The bitmap only contains information about the blocks LowerKey through 
UpperKey.  That is, Reserved and PreAlloc'ed blocks are not included in the 
bitmap.  The bitmap blocks themselves are included and marked as used.

A bit set to 1 in the bitmap indicates that the corresponding block in the
partition is free while a 0 bit indicates that the block is used.  While
this may seem backwards, there is a valid reason for this.  When the filing 
system is searching for a free block it is much easier to test a longword at
a time for the non-zero condition.  This indicates that there is a free bit 
somewhere in that block of 32 bits and a bit search can be started.  

If 0 were to indicate a free block, every single bit would have to be tested 
individually to see if there were any free ones.  Testing all 32 bits at 
once would not yield any useful information apart from the fact that all of 
the blocks were free, a particularly rare occurence in a typical partition.




User Directory Blocks
---------------------

The user directory blocks are distinct from the root block in that they are 
creatable and deletable by the user or application program.  The root block, 
while being a special case of a directory, cannot be created or deleted 
using normal filing system calls.  Another major distinction is that the root 
will NEVER have a parent directory while user directory blocks will ALWAYS 
have one.

   struct UserDirectoryBlock {
	LONG	Type;		/* This is used to mark the type of this
				   block and is set to TYPE_SHORT (2) */
	ULONG	OwnKey;		/* Set to directory's own block number */
	ULONG	Spare1;		/* Not used, must be set to 0 */
	ULONG	Spare2;		/* Not used, must be set to 0 */
	ULONG	Spare3;		/* Not used, must be set to 0 */
	LONG	Checksum;	/* balance to 0 checksum.  When all longs
				   in the block are added (ignoring carry)
				   the sum should be 0 */
	ULONG	HashTable[72];	/* hash table containing block numbers of
				   files and directories in this directory */
	LONG	Spare4;		/* Not used, must be set to 0 */
	LONG	Spare5;		/* Not used, must be set to 0 */
	ULONG	Protection;	/* Protection bits for this directory */
 	LONG	Spare6;		/* Not used, must be set to 0 */
	char	Comment[92];	/* Directory comment as a BCPL string, only
				   80 characters can be used including the
				   length byte at the beginning */
	ULONG	Created[3];	/* DOS DateStamp struct indicating when
				   this directory was created or modified */
	char	DirName[36];	/* name of this directory as a BCPL string.
				   only 30 characters are used */
	LONG	Spare7[7];	/* Not used, must be set to 0 */
	ULONG	HashChain;	/* block number of the next file on the
				   hashchain if there's a hashing collision.
				   0 means no next file or directory. */
	ULONG	Parent;		/* Block number of the parent directory of
				   this directory.  */
	ULONG	Spare8;		/* Not used. must be set to 0 */
	LONG	SecondaryType;	/* Qualifier to Type.  Will be set to
				   ST_USERDIR (2) */

   };


File Header Blocks
------------------

All files have an associated file header block that describes the file name 
and creation date along with pointers to the required ancilliary data such 
as extension blocks and protection information.  When a file is Locked the 
returned FileLock has a field called fl_Key.  This field contains the block 
number where this header can be found in the partition. (This field contains 
an address in the case of the RAM disk).  

This is one of the few things that can be assumed about file locks, and will
be supported in future revisions of the DOS filing system.  It's advisable
for alternate filing systems to support this field too, though not strictly 
nescessary or enforced by the operating system.

   struct FileHeaderBlock {
	LONG	Type;		/* This is used to mark the type of this
				   block and is set to TYPE_SHORT (2) */
	ULONG	OwnKey;		/* Set to file headers own block number */
	ULONG	HighSeq;	/* total blocks used in file (not updated
				   by FFS, only the old filing system) */
	ULONG	DataSize;	/* number of data blocks used for this file */
	ULONG	FirstBlock;	/* block number of first block in the file */
	LONG	Checksum;	/* balance to 0 checksum.  When all longs
				   in the block are added (ignoring carry)
				   the sum should be 0 */
	ULONG	DataBlocks[72];	/* table of block numbers for the first 72
				   blocks of data in this file.  Slots are
				   used from DataBlocks[71] back to
				   DataBlocks[0], then extension is used */
	ULONG	Spare1;		/* Not used, must be set to 0 */
	ULONG	Spare2;		/* not used, must be set to 0 */
	ULONG	Protect;	/* protection bits mask */
	ULONG	Date[3];	/* DOS DateStamp indicating creation date */
	char	FileName[36];	/* Filename (only 30 characters used though) */
	ULONG	Spare3[7];	/* not used, must be set to 0 */
	ULONG	HashChain;	/* next entry with the same hash value */
	ULONG	Parent;		/* block number of parent directory */
	ULONG	Extension;	/* when all data block slots are used, this
				   points to an extension block that has the
				   same format as the file header block (but
				   FileName and Date are not maintained and
				   secondary type is ST_LIST (16)). A
				   Block number of 0 terminates the list */
	LONG	Second;		/* qualifier to type = ST_FILE (-3)
   };



Data Blocks
-----------

FFS puts nothing but data into data blocks so that it can make full use of 
a DMA controller.  There is no need to strip any extra information before the 
data can be DMA'ed directly to the caller's buffer.  The old filing system is 
a little more paranoid and puts a lot of checksum information at the front
of each data block.  The format of a data block under the old system is as 
follows.

   struct FileDataBlock {
	LONG	Type;		/* This is used to mark the type of this
				   block and is set to TYPE_DATA (8). There
				   is no secondary type on a data block */
	ULONG	OwnKey;		/* Set to data blocks own block number */
	ULONG	SeqNumber;	/* Sequence number of this block in the file */
	ULONG	DataSize;	/* number of bytes of info used in this block */
	ULONG	NextBlock;	/* block number of next block in the file */
	LONG	Checksum;	/* balance to 0 checksum.  When all longs
				   in the block are added (ignoring carry)
				   the sum should be 0 */
	char	Data[488];	/* the actual data */
   }


Hashing Algorithm
-----------------

The hashing algorithm, which is used to determine where a file header goes in
the hash table of a directory, is the same for both FFS and the ROM filing
system.  The easiest way to describe it is with a small 'C' function.  Given
a CPTR to a BCPL string, this routine will return the array index into the
hash table.  This value is used to insert the file header with that name into
the appropriate hash table entry in the owning directory.

Hash( name )
unsigned char *name;
{
int val,i;

	val = (int)*name++;
	for(i=0; i<val; i++) val = ((val*13) + (int)toupper( *name++ ))&0x7ff;
	return(val % 72);
}

One major difference between FFS and the old filing system is that hash 
chains MUST be sorted for FFS.  When a hashing collision occurs, files
are linked together using the HashChain entry in the FileHeaderBlock or
UserDirectoryBlock.  The old filing system sticks the new header at the
head of the list, while FFS merges the new header into the list in
ascending block order.  

If FFS finds an unsorted hashchain, files will begin to dissappear when 
ExNexting a directory.  This "missing" file can still be accessed by name, 
so renaming it something else and back to its original name will resort the 
hashchain.  This should only be a problem if your FFS partition was created 
using early gamma versions of FFS and you didn't reformat when installing 
the release version.

Most differences between the old filing system and the new Fast Filing System
have been covered here.  Remember it is not appropriate to use this
information in applications other than disk salvage utilities.  You should
expect more changes to the file system under V1.4.  In particular, variable
sized blocks will be supported in the next release.  Stick to system calls
and the packet interface to insure future compatability.
