1 Introduction
2 Ground Rules

Building a File System
3 File Systems
4 File Content Data Structure
5 Allocation Cluster Manager
6 Exceptions and Emancipation
7 Base Classes, Testing, and More
8 File Meta Data
9 Native File Class
10 Our File System
11 Allocation Table
12 File System Support Code
13 Initializing the File System
14 Contiguous Files
15 Rebuilding the File System
16 Native File System Support Methods
17 Lookups, Wildcards, and Unicode, Oh My
18 Finishing the File System Class

The Init Program
19 Hardware Abstraction and UOS Architecture
20 Init Command Mode
21 Using Our File System
22 Hardware and Device Lists
23 Fun with Stores: Partitions
24 Fun with Stores: RAID
25 Fun with Stores: RAM Disks
26 Init wrap-up

The Executive
27 Overview of The Executive
28 Starting the Kernel
29 The Kernel
30 Making a Store Bootable
31 The MMC
32 The HMC
33 Loading the components
34 Using the File Processor
35 Symbols and the SSC
36 The File Processor and Device Management
37 The File Processor and File System Management
38 Finishing Executive Startup

Users and Security
39 Introduction to Users and Security
40 More Fun With Stores: File Heaps
41 File Heaps, part 2
42 SysUAF
43 TUser
44 SysUAF API

UCL
45 Shells and UCL

Glossary/Index


Download sources
Download binaries

Initializing the File System

Note: I found two bugs in the TUOS_Native_File code. First, it should have been updating the EOF field of the header to match the highest address written to. It was a simple change in the _Write method and also the Set_Size method (when the file is being truncated, so that EOF isn't past the physical end of the file). Second, it was calculating the cluster incorrectly in Set_Size for boundary conditions.
A bug was also found in the TUOS_String_Table class, in the Set_Index_File method. It was using the stream size (the physical space allocated to the file) rather than the EOF marker (which indicates the actual used space for the file). Again, a simple fix. I had only tested the class against a memory store with no previous data.
I'll provide updated sources in the near future. Now back to our planned programming...

In this article, we start upon writing our file system class. First, the standard methods we see in all of our classes:

function TUOS_Native_File_System.Is_Class( Name : PChar ) : boolean ;

begin
    Result := lowercase( Name ) = 'tuos_native_file_system' ;
    if( not Result ) then
    begin
        Result := inherited Is_Class( Name ) ;
    end ;
end ;


function TUOS_Native_File_System.Get_Store ;

begin
    Result := _Store ;
end ;


procedure TUOS_Native_File_System.Set_Store( Value : TCOM_Managed_Store64 ) ;

begin
    Set_Last_Error( nil ) ;
    if( Value <> nil ) then
    begin
        Value.Attach ;
    end ;
    if( _Store <> nil ) then
    begin
        _Store.Detach ;
    end ;
    _Store := Value ;
end ;

There is also a getter and setter for the heap. Since the file system component code cannot call the Microsoft Windows API to allocate dynamic memory, we need to ask UOS for a heap manager. However, our file system will also be used during system initialization, prior to UOS actually being loaded and available. In such case, we need the initialization code to be able to provide the component with a heap manager. Because hooking into the heap management this way will affect everything compiled with our file system class (which is all the classes that it uses), we don't need to bother with setting the heap for any other objects. This one place handles it all.

function TUOS_Native_File_System.Get_Heap : TCOM_Heap64 ;

begin
    Result := _Heap ;
end ;


procedure TUOS_Native_File_System.Set_Heap( Value : TCOM_Heap64 ) ;

begin
    Set_Last_Error( nil ) ;
    if( Value <> nil ) then
    begin
        Value.Attach ;
        SetMemoryManager( NewMemMgr ) ;
    end ;
    if( _Heap <> nil ) then
    begin
        _Heap.Detach ;
    end ;
    _Heap := Value ;
end ;

Delphi provides a simple way to intercept heap calls via the SetMemoryManager. Here is our implementation of NewMemMgr:
var _Heap : TCOM_Heap64 = nil ;

function NewGetMem( Size : Integer ) : Pointer ;

begin
    Result := pointer( _Heap.Allocate( Size ) ) ;
end ;


function NewFreeMem( P : Pointer ) : Integer ;

begin
    Result := 0 ;
    _Heap.Deallocate( integer( P ), 0 ) ;
end ;


function NewReallocMem( P : Pointer ; Size : Integer ) : Pointer ;

begin
    Result := pointer( _Heap.Reallocate( integer( P ), 0, Size ) ) ;
end ;


const NewMemMgr : TMemoryManager = (
                                      GetMem : NewGetMem ;
                                      FreeMem : NewFreeMem ;
                                      ReallocMem : NewReallocMem ;
                                   ) ;

Before we can use a file system that is on a store, we need to have a file system structure on the store. So, the first thing we will write is our initialization method. Since Initialize is already a method name (for initializing the class instance), we will simply call our method Init.

procedure TUOS_Native_File_System.Init( FSLabel : PChar ;
    Patterns, Clustersize, Folder_Clustersize, Flags : longint ;
    var AT_Offset : TStore_Address64 ; Notifier : TScan_Notify ) ;

var Abort : boolean ;
    BootRec : FS_Boot_Record ;
    Buffer, Buffer1 : pByte_Array ;
    Buff_Size : longint ;
    C : cardinal ;
    Cluster : TStore_Address64 ;
    Current : int64 ;
    FAT : TUOS_Native_File ;
    FBadBlocks : TUOS_Native_File ;
    FHeader : TUOS_File_Header ;
    FIndex : TUOS_Native_File ;
    FRoot : TUOS_Native_File ;
    FStrings : TUOS_Native_File ;
    FStore : TUOS_Native_File ;
    Header : FS_Header ;
    I : integer ;
    Max : int64 ;
    P : TStore_Address64 ;
    Percent : cardinal ;
    Second_Attempt : boolean ;
    ST : string ;
    UEC : TUnified_Exception ;
    V : byte ;

Whatever code calls this method to initialize the file system passes a string label that serves as a "name" for the store, a number of patterns to run, the minimum cluster size of the store, the cluster to use for folders, some options (flags) for the store, and the offset of the allocation table on the store. Notifier is an optional callback that is used in surface scans. The code for this method is algorithmically simple, although it has to do several tasks. We will examine each of these tasks in turn. Flags is a bit mask with the following meanings:
ValueMnuemonicMeaning
1UOSFSF_DirtyIndicates that the store is mounted, if set.
2UOSFSF_PrivateIndicates that the store is private if set, or public if not.

These flags have no meaning to the Init method except that we will set the flags field in the store header to the passed value. We will discuss their use in the future.

begin // TUOS_Native_File_System.Init
    if( _Store = nil ) then
    begin
        exit ;
    end ;

    // Default the values...
    if( Clustersize < _Store.Min_Storage ) then
    begin
        Clustersize := _Store.Min_Storage ;
    end ;
    if( Folder_Clustersize < Clustersize ) then
    begin
        Folder_Clustersize := Clustersize ;
    end ;
    Clustersize := Clustersize and ( not ( _Store.Min_Storage - 1 ) ) ;
    Folder_Clustersize := Folder_Clustersize and ( not ( _Store.Min_Storage - 1 ) ) ;
    _Store.Set_Min_Storage( Clustersize ) ;
    C := _Store.Max_Storage div Clustersize ; // Total clusters
    if( AT_Offset = 0 ) then // Default to center of store
    begin
        AT_Offset := ( _Store.Max_Storage + ( C div 8 ) ) div 2 ;
    end ;

    // Allocate buffers...
    Buff_Size := Folder_Clustersize ;
    Buffer := Allocmem( Buff_Size ) ;
    Buffer1 := allocmem( Buff_Size ) ;

First, we must have a store to initialize, so we exit if no store has been set. Then we make sure that the requested clustersize is no smaller than the store's minimum allocation size. In effect, if the caller passes 0 for the clustersize, it will default to the store's inherent cluster size. Next, we make sure that the folder clustersize is no smaller than the default clustersize. Then we normalize the cluster sizes so that they are even multiples of the store's cluster size.
Once the clustersizes are verified/set we set the store's minimum store size to the new cluster size (which may be the same as the current one). Now we can calculate the total number of clusters on the store, and how large our allocation table must be. If the user requested the default AT position, we will set it so that it occupies the center of the store. In the case of disks, this should minimize the amount of disk head movement that results from updating the table while UOS is running. Of course, this assumes that the disk is mostly full so that there is an equal amount of data on both sides of the table. For a disk that is not near capacity, the table may have more data before it than after it, or vice versa. In general, placing it in the center of the store gives the best overall performance for most circumstances. Giving the user the opportunity to adjust the location of the table allows them to customize disk performance based on their needs and expectations. AT_Offset is passed by reference, so that if we assign it, the caller will know what the actual position is. "Wait!" you say - we've already said that the allocation table is always updated in memory and only written out to the store when it is dismounted. So, why would the position of the table have any bearing on disk performance? Well, this is generally correct. However, there is an exception in a certain case (dual-ported disks). In the normal situation, it doesn't matter where the table is positioned, so the center of the store is as good as any other place. In the exceptionally case, the position will have an impact on disk performance. So, placing it in the center works in both cases.
Finally we allocate a couple of buffers large enough to hold the folder cluster size. Since the folder cluster size will never be smaller than the default clustersize, the buffers will be large enough for whatever purpose we need later in the method.

At this point, we are ready to do the work of initializing. We will include the code in a try/finally block to make sure that the buffers are released when we exit:

    try
    .
    .
    .
    finally
        freemem( Buffer ) ;
        freemem( Buffer1 ) ;
    end ;

Patterns and bad blocks
Magnetic media, such as disks, are subject to wear and tear - especially removable disks, such as floppy disks. But even with minimal wear, it is possible for the media to have some flaws from the production process. Imagine the consequences of a single bit that cannot be changed in the middle of a financial file that could completely change the value of someone's bank balance. Obviously, the data we store on our disk must not be corrupted, so how do we handle the situation of bad (or marginal) spots on the media? Modern hard disks are tested before they leave the factory and contain sector replacement logic in the drive circuitry. Any bad sectors on the disk are ignored, and a reserved sector at the end of the disk is used to remap the bad sector to a good one. This happens automatically by the drive and we don't have to worry about it - from the outside, all the sectors look good. For this reason, we don't usually need to run a surface test to validate the reliability of the sectors. However, if there is a sector that is not marginal enough to be automatically remapped at production, but is marginal enough that after thousands of write operations (and perhaps some shocks to the drive), it starts to become less reliable, then we will want to remove it from the list of available clusters. Some drives already support this dynamic bad sector replacement, but it is not universal and UOS is intended to work reliably even with disks that are not reliable.
This is why Init supports running surface scans. It does this by writing values to each cluster, and reading the cluster back, comparing what was read with what was written. If there is even one bit different, we know that cluster contains at least one bad sector. To be very certain, we can try multiple patterns of bits on each cluster, just to verify that we don't have a "stuck" bit somewhere. The Patterns parameter indicates how many patterns to run. If 0 is passed, no surface scan is done. Anything beyond six patterns is probably overkill, but the user can run as many as patterns as he wants. Obviously, the more patterns that are run, the longer the process will take. The less reliable the media, the more patterns ought to be run so as to identify as many bad - or marginal - spots as possible before the media starts getting used for data storage. Since the surface scan can take a long time on large stores (especially if many patterns are run), we use the Notifier callback (if not null) to communicate our progress back to the caller. Here is the definition of the TScan_Notify data type:

type TScan_Notify = procedure( Sender : pointer ; Progress, Count : cardinal ;
    var Abort : boolean ) of object ;

This callback passes a pointer to the file system object, a percent complete, and a count of bad clusters. Further, a boolean variable is passed by reference so that the process can be aborted by the caller before the surface scan completes.

The next question that arises is, how do we remove these bad clusters from "circulation"? One thing we could do is simply mark the cluster as used in the allocation table. However, since we only write the allocation table out when the store is specifically dismounted, if the system crashes, we have to rebuild the table. But, this means that we lose the allocated bad clusters. So, we need to store the bad clusters in a more persistent area. There are many possible approaches to this, but we will use this one: we will have a file, appropriately called BadBlocks.sys, that will contain the bad clusters. An obvious approach is to simply have the file contents be a list of the bad clusters. A slightly less obvious, but more efficient, approach is to have the file be made up of the bad clusters themselves. That is, the cluster allocation chain itself will consist entirely of the bad clusters. Thus, the only overhead of the list of bad clusters is the header for the file and the allocation chain. This has the benefit of making sure the allocation table has the bad clusters marked as allocated if we have to rebuild the table since the rebuild function will simply process all the files on the store, running through the allocation chains and updating the table as it goes along. That is, the process doesn't need to know anything about bad blocks - it just deals with files. This is also more storage-efficient than storing the clusters as file data because the same amount of space would be used to store them in file data, but with the added overhead of the cluster chain for the file data.

We need to build up a list of bad clusters so that we can create BadBlocks.sys with the appropriate clusters. We have to complete the list before we create this file (or do any other file system work for that matter) since we have to avoid making use of any bad clusters as we allocate space for our on-store file system structures. Here is the list and the local routine used to maintain this list as we do our scan:

var BadBlocks : Pint64_Array ;
    BadBlock_Count : cardinal ;

    procedure Add_Bad_Block( P : TStore_Address64 ) ;

    begin
        reallocmem( Badblocks, ( Badblock_Count + 1 ) * 8 ) ;
        BadBlocks[ Badblock_Count ] := P ;
        inc( Badblock_Count ) ;
    end ;

We simply reallocate the buffer to be larger, then store the passed cluster address in the buffer.

Now, let's examine the scanning process.

        // Run patterns...
        BadBlocks := nil ;
        BadBlock_Count := 0 ;
        Max := C * Patterns div 100 ; // Number of operations that comprise 1% of the scan
        Current := Max ;
        if( Patterns > 0 ) then
        begin
            _Store.Format ;
        end ;
        while( Patterns > 0 ) do
        begin
            dec( Patterns ) ;
            case( Patterns ) of
                0 : V := 0 ;
                1 : V := 255 ;
                2 : V := 170 ; // 10101010
                3 : V := 85 ; // 01010101
                4 : V := 205 ; // 11001100
                5 : V := 51 ; // 00110011
                else V := Patterns ;
            end ;

First, if the caller requested any patterns, we make sure the store has its low-level formatting. For most hard disks, this is done before the drive is sold and the Format method will do nothing. For things such as floppy disks, this will write out low-level formatting information that the media needs before we even bother with reading/writing sectors.
Note that we have 6 specific bit patterns that we have defined. Beyond that, we simply use the current pattern iteration as the bit pattern.
            fillchar( Buffer^, Clustersize, V ) ;
            _Store.Set_Root( Buffer^ ) ;
            if( _Store.Last_Error <> nil ) then
            begin
                Set_Last_Error( Create_Exception( UOS_File_System_Error_Bad_Boot_Sector, 
                    _Store.Last_Error ) ) ;
                exit ;
            end ;
            _Store.Get_Root( Buffer1^ ) ;
            if( _Store.Last_Error <> nil ) then
            begin
                Set_Last_Error( Create_Exception( UOS_File_System_Error_Bad_Boot_Sector,
                    _Store.Last_Error ) ) ;
                exit ;
            end ;
            for I := 0 to Clustersize - 1 do
            begin
                if( Buffer^[ I ] <> Buffer1^[ I ] ) then
                begin
                    Set_Last_Error( Create_Exception( UOS_File_System_Error_Bad_Boot_Sector,
                        nil ) ) ;
                    exit ;
                end ;
            end ;

We fill the buffer with the pattern, then we specifically try the root cluster. If we find that the boot sector is bad, we immediately exit since that is the one cluster that absolutely must be good since it is where we start to find the beginning of the file system structure. We write the one buffer, read into the other buffer, then compare each byte between the buffers. If the store has an error, that indicates that the drive detected the bad area (usually this indicates that low-level formatting information on the media is bad as well).
            P := Clustersize ;
            while( P < _Store.Max_Storage ) do
            begin
                dec( Current ) ;
                if( Current <= 0 ) then
                begin
                    Current := Max ;
                    inc( Percent ) ;
                    if( assigned( Notifier ) ) then
                    begin
                        Abort := False ;
                        Notifier( self, Percent, BadBlock_Count, Abort ) ;
                        if( Abort ) then
                        begin
                            exit ;
                        end ;
                    end ;
                end ;
                _Store.Write_Data( Buffer^, P, Clustersize, UEC ) ;
                if( UEC <> nil ) then
                begin
                    Add_Bad_Block( P ) ;
                    continue ;
                end ;
                _Store.Read_Data( Buffer1^, P, Clustersize, UEC ) ;
                if( UEC <> nil ) then
                begin
                    Add_Bad_Block( P ) ;
                    continue ;
                end ;
                for I := 0 to Clustersize - 1 do
                begin
                    if( Buffer[ I ] <> Buffer1[ I ] ) then
                    begin
                        Add_Bad_Block( P ) ;
                        continue ;
                    end ;
                end ;
                P := P + Clustersize ;
            end ; // while( P < _Store.Max_Storage )
        end ; // while( Patterns > 0 )

The rest of each pattern loop involves stepping through the rest of the clusters and doing the same test as we did for the root cluster, except that an error doesn't abort the process. Instead, we add the cluster to our list of bad blocks. Also, at the start of the cluster loop, we check to see if we are another 1% of the way through the whole scan process. If so, we do the callback and abort if told to.

Once we have scanned the disk and built our list of bad blocks (if any), the next step is to make sure our initial allocation table represents these clusters as unavailable (allocated). Here is the code:

        for I := 0 to Badblock_Count - 1 do // Mark bad blocks as allocated
        begin
            Store.Allocate_At( Badblocks^[ I ], Clustersize ) ;
        end ;

Creating the base file system
Next, we need to mark the allocation table, itself, as allocated. We do this by telling the store to allocate the appropriate number of clusters at the determined offset. However, it is possible that some of the requested clusters are bad and thus already marked as allocated. So, if our request fails, we try to move the allocation table to a higher cluster offset. We repeat this process until we succeed or reach the final cluster. In this case, we then try the move the table to a lower cluster offset - we start at the first cluster and move up until we can find a place to put it. If we can't find a place to put the table, we exit with an error. Note that if we cannot find enough contiguous clusters for the allocation table, it probably indicates some media so riddled with errors that we probably don't want to use it anyway. Since AT_Offset is passed by reference, the caller will know exactly where the table ended up.
        while( not Store.Allocate_At( AT_Offset, C div 8 ) ) do
        begin
            AT_Offset := At_Offset + Clustersize ; // Try next cluster
            if( AT_Offset >= Store.Max_Storage ) then
            begin
                if( Second_Attempt ) then
                begin
                    Set_Last_Error( Create_Exception( UOS_File_System_Out_Of_Space, UEC ) ) ;
                    exit ;
                end ;
                AT_Offset := Clustersize ; // Start over at beginning of store
                Second_Attempt := True ;
            end ;
        end ; // while

Now we are ready to create the actual file system structure on the store. This process will be different than normal file system access because there is no existing file system for methods like Create_File to operate on. Thus, we must manually set up the initial structure. We won't know the location of the store header until we allocate it, so we have to allocate it before we set the pointer in the boot sector. Further, we don't want to give the store the appearance of being properly set up until we finish it, so we won't set the boot sector pointer until the end. These restrictions basically tell us that we have to set up the store backwards. That is, we have to write the file and directory contents before we write the headers for those files and directories. And we have to set up the root directory before we set up the store header.
Before we continue, let's look at what our file structure will look like (at a high level) when we are finished. As we discussed in a previous article, we need a root directory that is the place we start from when doing any file operations. Because there is no parent directory for the root directory, we will simply allocate a cluster to contain the file header for the directory. This is what the store header will point to. Other than that, the root directory works like any other directory in the file system. The user can add any number of folders to the root directory, but we reserve the following folders:
Store
System
UOS
Users
We will discuss all of them, in time. For now, we are only concerned with the Store folder. This folder contains the files that are specific to the store. For instance, BadBlocks.sys will exist in this folder. As we discussed back with the String Table class, we have three files associated with the strings used in the file system. These files will also reside in the Store folder. They are:
File nameContents
AT.sysstring allocation table
Index.sysstring indexes
Strings.sysstring table data

Although some operating systems have the store's allocation table represented as a file on the store, UOS doesn't do so. Yes, it could, but it would be extra work with no benefit. We don't allow anyone, except the managed store, to have direct access to the table data. The rebuild process will simply use the Allocate_At method to rebuild the table. So there is no need for the storage overhead of a file header. This means that the store's allocation table is the only structure on the store that isn't accessible via a file, but this one "inconsistency" is something I'm willing to live with!

If you are perceptive, you may have already realized that we have a "chicken or the egg" scenario. That is, all files have names and those names are pointers into the string table, which is a file. How does Strings.sys have a name within itself before it even exists? This is another reason why we can't use normal File System methods to create, or access, these files.
Here is the code to create the root directory:

        // Write root folder header...
        P := Store.Allocate( Clustersize ) ;
        FRoot := TUOS_Native_File.Create ;
        FRoot.Store := Store ;
        FRoot.Header.Clustersize := Folder_Clustersize ;
        FRoot.Header.Creation := Get_Timestamp ;
        FRoot.Header.Flags := FAF_PERMANENT or FAF_SYSTEM or FAF_DIRECTORY ;
        FRoot.Header.Last_Access := FRoot.Header.Creation ;
        FRoot.Header.Last_Modified := FRoot.Header.Creation ;

We can use the TUOS_Native_File class to access files since it knows nothing about the file system - it just deals with the allocation chain on the store, and a few portions of the file header. The root file has no name, so we don't need to concern ourselves with that. We set the clustersize and various dates. Then we set the root folder flags to indicate that this is a system file that cannot be deleted (FAF_PERMANENT) and is marked as a directory. We don't write the header out yet - we'll do that in a bit.

Next, we create the string table, string index, and string allocation table. We set up the TUOS_Native_File instances and write the hand-coded initial data for these files.

        // Create String table...
        FStrings := TUOS_Native_File.Create ;
        FStrings.Store := Store ;
        FStrings.Header.Creation := Get_Timestamp ;
        FStrings.Header.Flags := FAF_PERMANENT or FAF_SYSTEM ;
        FStrings.Header.Last_Access := FStrings.Header.Creation ;
        FStrings.Header.Last_Modified := FStrings.Header.Creation ;
        FStrings.Header.Name := 44 ;
        ST :=
            #0#0#0#0 + #0#0#0#0 + // Initial zero entry (0)
            #1#0#0#0 + #5'Sto' + 're'#0#0 + #0#0#0#0 + // "Store" (8)
            #1#0#0#0 + #13'Bad' + 'Bloc' + 'ks.s' + 'ys'#0#0 + // "BadBlocks.sys" (24)
            #1#0#0#0 + #11'Str' + 'ings' + '.sys' + // "Strings.sys" (44)
            #1#0#0#0 + #9'Ind' + 'ex.s' + 'ys'#0#0 + // "Index.sys" (60)
            #1#0#0#0 + #6'AT.' + 'sys'#0 + #0#0#0#0 ; // "AT.sys" (76)
        FStrings.Write( 0, 0, length( ST ), PChar( ST )[ 0 ] ) ; // 92 bytes total

        // String index...
        FIndex := TUOS_Native_File.Create ;
        FIndex.Store := Store ;
        FIndex.Header.Creation := Get_Timestamp ;
        FIndex.Header.Flags := FAF_PERMANENT or FAF_SYSTEM ;
        FIndex.Header.Last_Access := FIndex.Header.Creation ;
        FIndex.Header.Last_Modified := FIndex.Header.Creation ;
        FIndex.Header.Name := 60 ;
        ST := #76#0#0#0 + #24#0#0#0 + #60#0#0#0 + #8#0#0#0 + #44#0#0#0 ;
        FIndex.Write( 0, 0, length( ST ), PChar( ST )[ 0 ] ) ;

        // String table allocation table...
        FAT := TUOS_Native_File.Create ;
        FAT.Store := Store ;
        FAT.Header.Creation := Get_Timestamp ;
        FAT.Header.Flags := FAF_PERMANENT or FAF_SYSTEM ;
        FAT.Header.Last_Access := FAT.Header.Creation ;
        FAT.Header.Last_Modified := FAT.Header.Creation ;
        FAT.Header.Name := 76 ;
        C := $3FF ; // First 11 bits of AT = 88 bytes
        FAT.Write( 0, 0, 2, C ) ;

Now we create the BadBlock.sys file header, and assign the bad allocation clusters. Note that this required adding the Set_Allocation_Cluster method to the TUOS_Native_File class.

        // BadBlocks...
        FBadBlocks := TUOS_Native_File.Create ;
        FBadBlocks.Store := Store ;
        FBadBlocks.Header.Creation := Get_Timestamp ;
        FBadBlocks.Header.Flags := FAF_PERMANENT or FAF_SYSTEM ;
        FBadBlocks.Header.Last_Access := FBadBlocks.Header.Creation ;
        FBadBlocks.Header.Last_Modified := FBadBlocks.Header.Creation ;
        FBadBlocks.Header.Name := 24 ;
        FBadBlocks.Header.Size := Badblock_Count * Store.Min_Storage ;
        for I := 0 to BadBlock_Count - 1 do
        begin
            move( BadBlocks[ I ], Cluster, sizeof( Cluster ) ) ;
            FBadBlocks.Set_Allocation_Cluster( 0, I, Cluster ) ;
        end ;

We create the Store folder header, and then we write the headers for the other files to the store folder file. Finally we write the Store folder header to the root directory and then write the root directory header.

        // Store folder...
        FStore := TUOS_Native_File.Create ;
        FStore.Store := Store ;
        FStore.Header.Creation := Get_Timestamp ;
        FStore.Header.Flags := FAF_PERMANENT or FAF_SYSTEM or FAF_DIRECTORY ;
        FStore.Header.Last_Access := FStore.Header.Creation ;
        FStore.Header.Last_Modified := FStore.Header.Creation ;
        FStore.Header.Name := 8 ;

        // Write files to store folder...
        fillchar( Buffer^, Buff_Size, 0 ) ;
        move( FAT.Header, Buffer[ 0 ], sizeof( FAT.Header ) ) ;
        FStore.Write( 0, 0, Clustersize, Buffer^ ) ;
        move( Findex.Header, Buffer[ 0 ], sizeof( Findex.Header ) ) ;
        FStore.Write( 0, sizeof( FIndex.Header ), Clustersize, Buffer^ ) ;
        move( FStrings.Header, Buffer[ 0 ], sizeof( FStrings.Header ) ) ;
        FStore.Write( 0, sizeof( FIndex.Header ) * 2, Clustersize, Buffer^ ) ;
        move( FBadBlocks.Header, Buffer[ 0 ], sizeof( FBadBlocks.Header ) ) ;
        FStore.Write( 0, sizeof( FIndex.Header ) * 3, Clustersize, Buffer^ ) ;

        // Update store folder header to root folder...
        move( FStore.Header, Buffer[ 0 ], sizeof( FStore.Header ) ) ;
        FRoot.Write( 0, 0, Clustersize, Buffer^ ) ;

        // Write root directory file header...
        move( FRoot.Header, Buffer^, sizeof( FRoot.Header ) ) ;
        _Store.Write_Data( Buffer^, P, sizeof( FRoot.Header ), UEC ) ;
        if( UEC <> nil ) then
        begin
            Set_Last_Error( Create_Exception( UOS_File_System_Write_Error, UEC ) ) ;
            exit ;
        end ;

At this point, we have the basic file structure set up. Next, we write out the store header, itself, "plugging in" our root folder pointer.

        // Build store header...
        fillchar( Header, sizeof( Header ), 0 ) ;
        Header.ID := -1 ;
        Header.ID1 := 135 ;
        Header.Version := 10 ; // V1.0
        Header.AT_Offset := AT_Offset ;
        Header.AT_Size := C div 8 ;
        Header.Flags := Flags ;
        Header.Clustersize := Clustersize ;
        Header.Folder_Clustersize := Folder_Clustersize ;
        Header.Root := P ;
        Header.Volume_Label := FSLabel ;

        // Write store header...
        move( Header, Buffer^, sizeof( Header ) ) ;
        P := Store.Allocate( Clustersize ) ;
        _Store.Write_Data( Buffer^, P, Clustersize, UEC ) ;
        if( UEC <> nil ) then
        begin
            Set_Last_Error( Create_Exception( UOS_File_System_Write_Error, UEC ) ) ;
            exit ;
        end ;

Next we write out the allocation table, and finally we update the boot cluster.

        Store.Set_Allocation_Table_Offset( AT_Offset ) ;
        Store.Flush_Allocation_Table ;

        // Update Boot cluster...
        fillchar( Buffer^, Clustersize, 0 ) ;
        BootRec.Header := P ; // File system header
        move( BootRec, Buffer^, sizeof( BootRec ) ) ;
        _Store.Set_Root( Buffer^ ) ;
        UEC := _Store.Last_Error ;
        if( UEC <> nil ) then
        begin
            Set_Last_Error( Create_Exception( UOS_File_System_Error_Bad_Boot_Sector,
		UEC ) ) ;
            exit ;
        end ;

And that's it! The store is now initialized with a fresh UOS file system.

Here is the new Set_Allocation_Cluster method in TUOS_Native_File.

procedure TUOS_Native_File.Set_Allocation_Cluster( Stream, Index : longint ;
    Value : TStore_Address64 ) ;

var ACM : TNative_File_ACM ;

begin
    if( Stream < 0 ) then
    begin
        Set_Last_Error( Create_Exception( UOS_File_Error_Invalid_Stream_Index, nil ) ) ;
        exit ;
    end ;
    Set_Last_Error( nil ) ;
    ACM := Find_Data_Stream( Stream ) ;
    if( ACM = nil ) then
    begin
        if( _Store.Last_Error <> nil ) then
        begin
            Set_Last_Error( Create_Exception( UOS_File_Error_Write_Failure, 
                _Store.Last_Error ) ) ;
        end else
        begin
            Set_Last_Error( Create_Exception( UOS_File_Error_No_Such_Stream, nil ) ) ;
        end ;
        exit ;
    end ;
    if( Stream = 0 ) then
    begin
        if( Index < Max_Header_Cluster ) then
        begin
            Header.Clusters[ Index ] := Value ;
            Dirty := True ;
            exit ;
        end ;
        Index := Index - Max_Header_Cluster - 1 ;
    end ;
    ACM.Set_Allocation_Cluster( Index, Value ) ;
    if( ACM.Last_Error <> nil ) then
    begin
        Set_Last_Error( Create_Exception( UOS_File_Error_IO_Error, _Store.Last_Error ) ) ;
    end ;
end ;

In turn, this calls a new method in the allocation cluster manager, which is defined as follows:
procedure TCOM_Allocation_Cluster_Manager64.Set_Allocation_Cluster( I : longint ;
     Cluster : TStore_Address64 ) ;

var AC, Offset : cardinal ;

begin
    Set_Last_Error( nil ) ;
    if( I < 0 ) then
    begin
        exit ;
    end ;
    if( Size < I * _Clustersize ) then
    begin
        Size := I * _Clustersize ;
    end ;
    AC := I div _Max_Index ; // Allocation cluster index
    Offset := I mod _Max_Index ; // Pointer index within allocation cluster index
    Pull( AC ) ;
    if( _Last_Error <> nil ) then
    begin
        exit ;
    end ;
    Buffer^[ Offset ] := Cluster ;
    Write( _Current_AC_Pointer ) ;
end ;

In the next articles, we will write the methods that will allow us to navigate and modify our file system.