(c) Copyright 1989-1999 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