Newer
Older
ubixfs-2 / ubixfs_test.pas
@flameshadow flameshadow on 10 Jun 2005 87 KB UbixFS
unit UbixFS;

interface

uses device, lists;

const INODE_NO_FLAG     = $00000000;
const INODE_IN_USE      = $00000001;

const ATTR_INODE        = $00000004;
const INODE_LOGGED      = $00000008;
const INODE_DELETED     = $00000010;
const PERMANENT_FLAGS   = $0000ffff;
const INODE_NO_CACHE    = $00010000;
const INODE_WAS_WRITTEN = $00020000;
const S_IFDIR           = $00040000;  // POSIX
const NO_TRANSACTION    = $00040000;

const DATASTREAM_DIRECT_BLOCKS = 16;    // number of direct blocks in the dataStream 

const UBIXFS_MAGIC_1    = $A0A0A0A;
const UBIXFS_MAGIC_2    = $B0B0B0B;
const UBIXFS_MAGIC_3    = $C0C0C0C;
const UBIXFS_INODE_MAGIC = $3BBE0AD9;

const UBIXFS_CLEAN = $434C454E;
const UBIXFS_DIRTY = $44495254;

const SECTOR_SHIFT = 9;
const SECTOR_SIZE = 1 shl SECTOR_SHIFT;

type
  int8          = shortInt;
  int16         = smallInt;
  int32         = longInt;

  uInt8         = byte;
  uInt16        = word;
  uInt32        = dWord;
{ Common Integer array definitions }

type
  int8ArrayPtr  = ^int8Array;
  int8Array     = array[ 0..0 ] of int8;

  int16ArrayPtr = ^int16Array;
  int16Array    = array[ 0..0 ] of int16;

  int32ArrayPtr = ^int32Array;
  int32Array    = array[ 0..0 ] of int32;

  { Common Unsigned Integer array definitions }

type
  uInt8ArrayPtr = ^uInt8Array;
  uInt8Array    = array[ 0..0 ] of uInt8;

  uInt16ArrayPtr= ^uInt16Array;
  uInt16Array   = array[ 0..0 ] of uInt16;

  uInt32ArrayPtr= ^uInt32Array;
  uInt32Array   = array[ 0..0 ] of uInt32;

type


(*  TBlockRun = packed record
    AG     : int32;
    start  : uInt16;
    len    : uInt16;
  end;

  inodeAddr = TblockRun;

  uPtr = packed record
   case integer of
     0: (i      : int32);
     1: (u      : uInt32);
     2: (s      : single);
     3: (d      : double);
     4: (p      : pointer);
     5: (offset : int64);
     6: (iAddr  : inodeAddr);
  end;
*)
  PdiskSuperBlock = ^diskSuperBlock;
  diskSuperBlock = packed record
    name        : array[0..31] of char;
    magic1      : int32;

    fsByteOrder : int32;

    // blockSize on disk 
    blockSize   : uInt32;

    // number of bits needed to shift a block number to get a byte address
    blockShift  : uInt32;

    numBlocks   : int64;
    usedBlocks  : int64;

    inodeSize   : uInt32;
    magic2      : uInt32;

    blocksPerAG : uInt32;
    AGShift     : uInt32;
    numAGs      : uInt32;

    flags       : uInt32;

    logBlocks   : TBlockRun;
    logStart    : int64;
    logEnd      : int64;

    magic3      : int32;
    BAT         : inodeAddr;
    rootDir     : inodeAddr;
    indicies    : inodeAddr;

    pad         : array[0..371] of byte;
  end; // diskSuperBlock

  PDataStream = ^TDataStream;
  TDataStream = packed record
    direct        : array[0..DATASTREAM_DIRECT_BLOCKS-1] of TBlockRun;
    maxDirect     : int64;
    indirect      : TBlockRun;
    maxIndirect   : int64;
    doubleIndirect: TBlockrun;
    maxDIndirect  : int64;
    size          : int64;
    blockCount    : uInt32;
  end; // TDataStream

  PubixfsInode = ^ubixfsInode;
  ubixfsInode = packed record
    magic         : int32;
    inodeNum      : inodeAddr;
    uid           : uInt32;
    gid           : uInt32;
    mode          : int32;
    flags         : int32;
    createTime    : int64;
    modifiedTime  : int64;
    parent        : inodeAddr;
    attributes    : inodeAddr;
    iType         : uInt32;
    inodeSize     : uInt32;
    data          : uPtr;         {this was the etc field in bfs}
    blocks        : TDataStream;
    smallData     : array[0..0] of byte;
  end; // ubixfsInode

  PVNode = ^TVNode;
  TVNode = object
    iAddr    : inodeAddr;
    position : int64;
    flags    : uInt32; 
  end; // TVNode

  PVnid = ^TVnid;
  TVnid = object
    refCount : uInt32;
    iAddr    : inodeAddr;
    mode     : uInt32;
  end; // TVnid

(*  PFileDescriptor = ^TFileDescriptor;
  TFileDescriptor = object
    iAddr    : inodeAddr;
    refCount : uInt32;    
    flags    : uInt32;
    handle   : uInt32;
  end; // fileDescriptor *)

  PUbixFS = ^TUbixFS;
  TUbixFS = object
   private
    dev           : PDevice;
    superBlock    : ^diskSuperBlock;
    blockCache    : PbTree;
    blockTime     : PbTree;
    vnids         : PbTree;
    vNodeQueue    : PQueue;
    fileDescriptors : ^array[0..0] of TVNode;

    maxCacheEntry : uInt32;
    maxFDEntry    : uInt32;
    
    procedure       assert(x:boolean);
    function        allocBRun(blockRun:TBlockRun):TBlockRun;
    function        allocInode(parentIAddr:inodeAddr):PUbixfsInode;
    function        extendFile(inode:PubixfsInode; newSize:int64):integer;
    function        findBRun(dataStream:PDataStream; blockNum:uInt32):TBlockRun;
    function        findBRunDirect(dataStream:PDataStream; blockNum:uInt32):TBlockRun;    
    function        findBRunIndirect(dataStream:PDataStream; blockNum:uInt32):TBlockRun;
    function        findBRunDIndirect(dataStream:PDataStream; blockNum:uInt32):TBlockRun;
    function        freeUsedBlocks(blockRun:TBlockRun):integer;
    function        getFileSize(inode:PUbixfsInode):int64; 
    function        getFileSpace(inode:PUbixfsInode):int64; 
    function        iAddrToStr(iAddr:inodeAddr):string;
    function        isValidIAddr(iAddr:inodeAddr):boolean;
    function        loadBlock(blockAddr:TBlockRun):pointer;

    function        markUsedBlocks(blockRun:TBlockRun):integer;
    function        nextAG(AG:uInt32):uInt32;
    function        pathToIAddr(path:PChar):inodeAddr;

    function        readDataStream(iAddr:inodeAddr; position:int64; buf:pointer; length:uInt32):integer; 
    function        saveBlock(blockAddr:TBlockRun; buf:pointer):boolean; 
    procedure       setBlockRun(var blockRun:TBlockRun; AG:uInt32; start, len:uInt16);
    procedure       setIAddr(var inode:inodeAddr; AG:uInt32; start, len:uInt16);
    function        strToIAddr(s:string):inodeAddr;
    function        writeDataStream(iAddr:inodeAddr; position:int64; buf:pointer; length:uInt32):integer;
   public
    constructor     init(device:Pdevice);
    procedure       printSuperBlock;
    procedure       assign(var F:TFileRec; const _name:string);
    procedure       reset(var F:TFileRec; blkSize:uInt32);
    procedure       rewrite(var F:TFileRec; blkSize:uInt32);
    procedure       sync;
    function        vfs_fCreate(filename:PChar):integer;
    function        vfs_fExist(path:PChar):integer;
    function        vfs_close(handle:integer):integer;
    function        vfs_format(device:PDevice; fsBlockSize:uInt32; quick:boolean):integer;
    function        vfs_init:integer;
    function        vfs_mkdir(path:PChar):integer;
    function        vfs_read(handle:integer; buffer:pointer; size:uInt32):integer; 
    function        vfs_seek(handle:integer; newPosition:int64):integer;   
    function        vfs_write(handle:integer; buffer:pointer; size:uInt32):integer;  
    procedure       examine;
    destructor      done; virtual;
  end; // TUbixFS
 
implementation

uses strings, dos, math, lists;

// IMPORTANT NOTE
//   You cannot under any circumstance change NUM_EXTENTION_BLOCKS. This
// constant defines the multiple of blocks that a file will be extended
// by, *and* it determines how many blocks are used in each blockRun in
// the Double Indirect dataStream.
//   NUM_EXTENSION_BLOCKS determines how much slack space is allocated for
// a file when it is extended. For example, a file with 0 bytes in it
// will take up 0 blocks for the data and 1 block for the inode (ignoring
// space required to hold the directory entry).  When the first byte is 
// written, extendFile() will allocate a multiple of NUM_EXTENSION_BLOCKS 
// to accomodate the amount of data being written out. So, a 1 byte file will 
// actually take up NUM_EXTENSION_BLOCKS (plus one for the inode) until the 
// file is closed. Then the file will be trimmed and the slack space reclaimed.
//   This value is also used in the double-indirect region of the dataStream
// to determine how many blocks each blockRun uses.

//         vv DO NOT CHANGE vv        //
const NUM_EXTENSION_BLOCKS = 4;       // <--- don't change this
//         ^^ DO NOT CHANGE ^^        //

const NUM_CACHE_ENTRIES = 8192; // default max entries in block cache
const NUM_FD_ENTRIES = 1024;    // default max entries in the File Descriptor Table

type 
  PCacheNode = ^TCacheNode;
  TCacheNode = object
   public
    address  : int64;
    lastUsed : int64;
    data     : pointer;
    dataSize : uInt32;
    dirty    : boolean;
    locked   : boolean;
    constructor init(_address, _lastUsed:int64; _dataSize:uInt32);
    destructor done;
  end; // TCacheNode

type 
  PUbixBTreeVFS = ^TUbixBTreeVFS;
  TUbixBTreeVFS = object(TBTreeVFS)
   private
    fs          : PUbixFS;
    iAddr       : inodeAddr;
    position    : int64;  // limits files to 4GB
   public
    constructor init(_fs:PUbixFS);
    function    fClose:boolean; virtual;
    function    fCreate(const filename:string):boolean; virtual;
    function    fExist(const filename:string):boolean; virtual;
    function    fOpen(const filename:string):boolean; virtual;
    function    fRead(var buf; size:uInt32):boolean; virtual;   
    function    fSeek(offset:uInt32):boolean; virtual;
    function    fWrite(var buf; size:uInt32):boolean; virtual;
    destructor  done; virtual; 
  end; // TUbixBTreeVFS

function null_file_proc conv arg_based (f: LongInt; buf: pointer; len: ULong; var act: ULong): LongInt;
begin
  runerror(999);
end;

constructor TCacheNode.init;
begin
  address  := _address;
  lastUsed := _lastUsed;
  dataSize := _dataSize;
  data := NIL;
  if (dataSize <> 0) then GetMem(data, dataSize);
  dirty    := FALSE;
  locked   := FALSE;
end; // TCacheNode.init

destructor TCacheNode.done;
begin
  if (data <> NIL) and (dataSize <> 0) then FreeMem(data, dataSize);
  address  := 0;
  lastUsed := 0;
  data     := NIL;
  dataSize := 0;
  dirty    := FALSE;
  locked   := FALSE;
end; // TCacheNode.done

constructor TUbixBTreeVFS.init;
begin
//  writeln('TUbixBTreeVFS.init()');
  fs         := _fs;
  fillchar(iAddr, sizeof(iAddr), 0);  
  position   := 0;
end; // TUbixBTreeVFS.init;

function TUbixBTreeVFS.fClose;
begin
  if (fs = NIL) then exit;
  position := 0;
end; // TUbixBTreeVFS.fClose

function TUbixBTreeVFS.fCreate;
begin
  // I *think* that this code is only called for the
  // B^Tree. Therefore, the file technically exists, but just
  // has nothing in it when we call fCreate.
  result := fOpen(filename);  
end; // TUbixBTreeVFS.fCreate

function TUbixBTreeVFS.fExist;
var 
  tmpIAddr:inodeAddr;
  tmpInode:PUbixfsInode;
begin
  result := FALSE;
  assert(fs <> NIL);
  if (fs = NIL) then exit;
  assert(fs^.superBlock <> NIL);
  if (fs^.superBlock = NIL) then exit;
  tmpIAddr := fs^.strToIAddr(filename);
  if not fs^.isValidIAddr(tmpIAddr) then exit;

  // If we've created an inode, but haven't written any data to it, then
  // the B^Tree hasn't written out a superblock, so technically the file doesn't
  // exist, and this is a really long run-on sentence.
  tmpInode := fs^.loadBlock(tmpIAddr);

  // I'm not sure why, but the compiler cannot properly evaluate (tmpInode^.blocks.size <> 0),
  // so we check both the high and low portions of the size to make sure it's not 0.
  //  result := (tmpInode <> NIL) and ((longLongRec(tmpInode^.blocks.size).lo <> 0) or (longLongRec(tmpInode^.blocks.size).hi <> 0));

  // However, checking for 0 and then taking the complement of that works .. so we go that way.
  result := ((tmpInode <> NIL) and (not (tmpInode^.blocks.size = 0)));
end; // TUbixBTreeVFS.fExist

function TUbixBTreeVFS.fOpen;
begin
  result := FALSE;
  if (fs = NIL) then exit;
  if (fs^.superBlock = NIL) then exit;
  iAddr := fs^.strToIAddr(filename);
  position := 0;
  result := fs^.isValidIAddr(iAddr);
end;  // TUbixBTreeVFS.fOpen

function TUbixBTreeVFS.fRead;
begin
  result := FALSE;
  if (fs = NIL) then exit;
//  with inode^.inodeNum do
//    write('fRead(', AG, ':', start, ':', len, ', ');
//  write(position, ', ');
//  write('@buf, ');

  result := (fs^.readDataStream(iAddr, position, @buf, size) = 0);
  position := position + int64(size);
end; // TUbixBTreeVFS.fRead

function TUbixBTreeVFS.fSeek;
begin
  // uhm.. if the desired offset is beyond the end of the file, 
  // should we not extend out the file? Or does that occur in writeDataStream ...
  position := offset;
  result := TRUE;
end; // TUbixBTreeVFS.fSeek

function TUbixBTreeVFS.fWrite;
begin
  result := FALSE;
  if (fs = NIL) then exit;
//  with inode^.inodeNum do
//    write('fWrite(', AG, ':', start, ':', len, ', ');
//  write(position, ', ');
//  write('@buf, ');
  result := (fs^.writeDataStream(iAddr, position, @buf, size) = 0);
  position := position + int64(size);
  //inc(position, size);
end; // TUbixBTreeVFS.fWrite;

destructor TUbixBTreeVFS.done;
begin
  // XXX: It's possible that we need to save the inode here since 
  // we possibly changed a directory
  fillchar(iAddr, sizeof(iAddr), 0);
  fs         := NIL;  // don't destruct it since we didn't allocate it
end; // TUbixBTreeVFS.done

constructor TUbixFS.init;
begin
  dev := device;
  superBlock := NIL;
  blockCache := NIL;
  blockTime  := NIL;
  vnids      := NIL;
  vnodeQueue := NIL;
  fileDescriptors := NIL;
  maxCacheEntry := NUM_CACHE_ENTRIES;
  maxFDEntry := NUM_FD_ENTRIES;
end; // TUbixFS.init

procedure TUbixFS.assert(x:boolean);
begin
  if not x then runerror(1000);
end; // TUbixFS.assert

{$system}
procedure TUbixFS.assign;
begin
  with f do begin
    magic   := @f;
    state   := [%file_assigned];
    name    := _name;
    rd_proc := null_file_proc;
    wr_proc := null_file_proc;
    check_magic();
  end; // with
end; // TUbixFS.assign

{$system}
procedure TUbixFS.reset;
begin
//  if IO_ApiRet <> 0 then IO_Error(IO_Apiret);
end; // TUbixFS.reset

procedure TUbixFS.rewrite;
begin

end; // TUbixFS.rewrite

function TUbixFS.allocBRun;
var
//  inode:PUbixfsInode;
  FBL:int8ArrayPtr;    // Free Block List array pointer
  FBLSize:uInt32;      // Free Block List array pointer size
  bitOffset:uInt32;
  byteOffset:uInt32;
  curBlock, baseBlock:uInt32;
  freeCount:uInt32;
begin
  // Notes: 
  // 2005-04-27 mji
  //   allocBRun() will allocate a blockRun that is near to the blockRun 
  // passed into the function.  The AG and Start field of blockRun will be
  // the starting point of where to look for free blocks.  The len field
  // holds how many contiguous blocks we need to allocate.
  //   We look through the BAT (Block Allocation Table) for a contiguous run that 
  // fits the requested length.  The BAT is a bitfield (used blocks are an 
  // on (1) bit, free blocks are an off (0) bit) of used/unused logical
  // blocks, where the logical block size was set when the device was formatted.
  // This shouldn't be confused with the sector size on the device, which
  // is always 512 bytes.  Block sizes are usually 1k, but can be any power
  // of 2 higher than that.  64k is probably the largest anyone would ever
  // actually use.  Since we only need to load one AG section out of the BAT
  // at a time, we only allocate one block worth of memory.
  //   The disk is broken up into Allocation Groups, where each allocation group
  // bitfield fits into a single block.  This means that for a device with 1k
  // blocks an AG will be 8192 blocks long.  Small devices (such as a floppy)
  // will only have 1 AG.
  //   To find a free run, we load a starting section of the BAT out of the 
  // BAT file.  The BAT is technically another file on the volume, but unlike 
  // normal files the BAT inode is referenced inside the superblock, and cannot
  // be touched by a normal user (if the volume is grown later, the BAT can be 
  // grown as well).
  //   Once the AG block list is loaded, we scan through looking for a contiguous
  // set of free blocks that fits our length requirement. In the event this
  // AG doesn't meet that requirement, we move to the next AG.  When we hit the
  // end of the volume, we go back to the beginning of the volume and continue
  // scanning until either we find a free run or we end up back where we began.
  // In the latter case, the disk would have to either be very fragmented or
  // full. If we succeed, we mark the blocks as used and return the blockRun.

  // XXX To do:
  // Might want to clear the blocks we allocated to avoid garbage from being present

  setBlockRun(result, 0, 0, 0);
  if (superBlock = NIL) then exit;
  if (blockRun.AG > superBlock^.numAGs) then exit;
  if (superBlock^.usedBlocks + int64(blockRun.len) > superBlock^.numBlocks) then exit; // should probably give a louder warning

  // we read in one block at a time
  FBLSize := superBlock^.blockSize;
  GetMem(FBL, FBLSize);
  assert(FBL <> NIL);
  fillchar(FBL^, FBLSize, 0);  // better safe than sorry

  assert(readDataStream(superBlock^.BAT, blockRun.AG shl (superblock^.AGShift shr 3), FBL, FBLSize) = 0); 
  byteOffset := blockRun.start shr 3;
  bitOffset := 7-(blockRun.start and 7);
  baseBlock := (blockRun.AG shl superBlock^.AGShift) + blockRun.start;
  curBlock := baseBlock;
  freeCount := 0;
  // an optimization would be to check to see if the byte is -1, thus skipping 8 at a time
  // This will need to be implemented (but can be later)
  repeat

    inc(curBlock);

    if (FBL^[byteOffset] and (1 shl bitOffset) = 0) then 
      inc(freeCount) 
    else 
      freeCount := 0;

    if (freeCount = blockRun.len) then break;      // did we meet our length requirement?

    if (bitOffset = 0) then 
      begin
        bitOffset := 7;
        inc(byteOffset);
      end
    else dec(bitOffset);

    if (byteOffset = superBlock^.blocksPerAG) then
      begin
        // We scanned through one Allocation Group of blocks. Move to the next one
        inc(blockRun.AG);

        // it's possible that we're wrapping back around to the beginning.
        if (blockRun.AG = superBlock^.numAGs) then 
          begin
            // We hit the end of the volume and wrapped back around
            blockRun.AG := 0;  // Back to the beginning AG
            curBlock    := 0;  // curBlock is from the base of the volume
            freeCount   := 0;  // reset this to 0
          end;         
        // read in the FBL for the AG we're currently on   
        assert(readDataStream(superBlock^.BAT, blockRun.AG shl (superblock^.AGShift shr 3), FBL, FBLSize) = 0); 
        byteOffset := 0;  // first byte in this block
        bitOffset  := 7;  // first bit of the first byte
      end; // if byteOffset = superBlock^.blocksPerAG

  until curBlock = baseBlock;  // repeat until we end up back where we started
  
  if (freeCount = blockRun.len) then   // we found a run
    begin
      blockRun.AG := (curBlock-blockRun.len) shr superBlock^.AGShift;
      blockRun.start := (curBlock-blockRun.len) and (superBlock^.blocksPerAG-1);
      markUsedBlocks(blockRun);
      result := blockRun;
    end
  else writeln('Failure in allocBRun()');
  FreeMem(FBL, FBLSize); 
  
end; // TUbixFS.allocBRun

function TUbixFS.allocInode;
var
  inode:PUbixfsInode;
  blockRun:TBlockRun;
  
begin
  // XXX ToDo:
  // need to fill in the rest of the fields in the inode record

  // Notes:
  // 2005-04-27 mji
  //   Inodes take up exactly one logical block on the device.  They
  // are usually in the same AG as their parent directory.  Allocating
  // one involves getting some memory for it and finding a free block
  // to hold it on the device.

  result := NIL;
  if (superBlock = NIL) then exit;
  assert(parentIAddr.AG < superBlock^.numAGs);
  if (parentIAddr.AG >= superBlock^.numAGs) then exit;  // parent addr is outside out block count?
  // ask for a single block somewhere around the beginning of the requested AG
  setBlockRun(blockRun, parentIAddr.AG, 0, 1); 
  blockRun := allocBRun(blockRun);
  assert(blockRun.len <> 0);
  if (blockRun.len = 0) then exit;     // this would be bad
  // Normally we'd allocate memory for the inode. Instead we let loadBlock() do that
  // for us.
  inode := loadBlock(blockRun);        

  assert(inode <> NIL);
  if (inode = NIL) then exit;
  fillchar(inode^, superBlock^.inodeSize, 0);

  with inode^ do
    begin
      magic := UBIXFS_INODE_MAGIC;

      // inodes point to themselves
      inodeNum := blockRun;

      uid := 0;  // this doesn't mean much here
      gid := 0;  
      // mode := ??
      flags := INODE_NO_FLAG;
      // createTime := 
      // modifiedTime :=

      parent := parentIAddr;
      setIAddr(attributes, 0, 0, 0);
      inodeSize := superBlock^.inodeSize;
      // data := 0;
      // there's no data yet in the file, so don't 
      // set blocks to anything (cleared above)
      blocks.blockCount := 0;

    end; // with
  result := inode;
end; // TUbixFS.allocInode

function TUbixFS.extendFile;

  function appendToDIndirect(newBlockRun:TBlockRun):boolean;
    var 
      extents:^array[0..0] of TBlockRun;
      indirectExtents:^array[0..0] of TBlockRun;
      extentsIndex, indirectExtentsIndex:uInt32;
      extentsUpperBound:uInt32;
      tmpBlockRun:TBlockRun;
    begin
      // Many apologies for the long variable names. This section is relatively
      // complicated, and it wouldn't help to have cryptic variables.
      result := FALSE; // failure by default
      if (newBlockRun.len mod NUM_EXTENSION_BLOCKS <> 0) then
        begin
          writeln('Double indirect block run is not a multiple of NUM_EXTENSION_BLOCKS');
          exit;
        end;
      extentsUpperBound := (superBlock^.blockSize div sizeof(TBlockRun)) - 1;

      if not isValidIAddr(inode^.blocks.doubleIndirect) then
        begin
          // allocate a double indirect block
          inode^.blocks.doubleIndirect := allocBRun(inode^.inodeNum); // allocate a block near the parent inode

          if not isValidIAddr(inode^.blocks.doubleIndirect) then
            begin
              writeln('Failed to allocate double indirect block');
              exit
            end;
          // We have a freshly allocated double indirect block. Now we need an indirect block
          extents^[0] := allocBRun(inode^.blocks.doubleIndirect); // allocate an indirect block near the dindirect block
          if not isValidIAddr(extents^[0]) then
            begin
              writeln('Failed to allocate indirect block beneath a double indirect block');
              exit
            end;
          // save out the doubleIndirect block
          saveBlock(inode^.blocks.doubleIndirect, extents);
          // Because we have a freshly allocated double indirect block, we have no entries
          // in it yet.  This means that our DIndirect index is 0, and our indirect index is also 0.
          extentsIndex := 0; 
          indirectExtentsIndex := 0;
        end
      else
        begin
          // double indirect block exists. Load it
          extents := loadBlock(inode^.blocks.doubleIndirect);
          assert(extents <> NIL);
//loadBlock(inode^.blocks.doubleIndirect, extents);
          // Now scan through (backwards) to find the last entry.
          for extentsIndex := extentsUpperBound downto 0 do
            if (extents^[extentsIndex].len <> 0) then break;
          // extentsIndex points to the entry which holds the indirect block with the last entry.
          // load that one
          indirectExtents := loadBlock(extents^[extentsIndex]);
          assert(indirectExtents <> NIL);
//loadBlock(extents^[extentsIndex], indirectExtents);
          // first check the last entry
          if (indirectExtents^[extentsUpperBound].len <> 0) then
            begin
              // this indirect block is full. Allocate a new one
              if (extentsIndex = extentsUpperBound) then
                begin
                  // I might as well halt at this point because the user is totally hosed.
                  writeln('Egad! The double indirect block section is full. Can not write any more to this file!');
                  exit;
                end; // if doomed
              extents^[extentsIndex+1] := allocBRun(extents^[extentsIndex]); 
              inc(extentsIndex);
              indirectExtentsIndex := 0;
              saveBlock(inode^.blocks.doubleIndirect, extents); // save, since we changed it
            end
          else
            begin
              // now scan forward
              for indirectExtentsIndex := 0 to extentsUpperBound do
                if (indirectExtents^[indirectExtentsIndex].len = 0) then break;
            end;
          // extentsIndex points to which indirect block we're on
          // indirectExtentsIndex points to the first free entry in that indirect block  
        end;

      while (newBlockRun.len <> 0) do
        begin
          tmpBlockRun := newBlockRun;
          tmpBlockRun.len := NUM_EXTENSION_BLOCKS;
          indirectExtents^[indirectExtentsIndex] := tmpBlockRun;
          with newBlockRun do
            begin
              dec(len, NUM_EXTENSION_BLOCKS);
              if ((uInt32(start) + NUM_EXTENSION_BLOCKS) > high(start)) then inc(AG);
              inc(start, NUM_EXTENSION_BLOCKS); // Warning! Relies on integer wrap-around (start is 16-bit)
            end; // with

          if (indirectExtentsIndex = extentsUpperBound) then
            begin
              // this indirect block is full. 
              // Save the current one
              saveBlock(extents^[extentsIndex], indirectExtents);
              // Allocate a new one
              if (extentsIndex = extentsUpperBound) then
                begin
                  // I might as well halt at this point because the user is totally hosed.
                  writeln('Egad! The double indirect block section is full. Can not write any more to this file!');
                  exit;
                end; // if doomed
              extents^[extentsIndex+1] := allocBRun(extents^[extentsIndex]); // allocate an indirect block near the previous one
              inc(extentsIndex);  // upadte which index in the double indirect block we're using
              indirectExtentsIndex := 0;
              indirectExtents := loadBlock(extents^[extentsIndex]);
              fillchar(indirectExtents^, superBlock^.blockSize, 0);
              saveBlock(inode^.blocks.doubleIndirect, extents); // save, since we changed it
            end
          else
            inc(indirectExtentsIndex);
          // update the size the maxDoubleIndirect blocks reference
          inode^.blocks.maxDIndirect := inode^.blocks.maxDIndirect + (NUM_EXTENSION_BLOCKS shl superBlock^.blockShift);
        end; // while
      saveBlock(extents^[extentsIndex], indirectExtents);
    end; // local function appendToDIndirect()

  function appendToIndirect(newBlockRun:TBlockRun):boolean;
    var 
      extents:^array[0..0] of TBlockRun;
      extentsUpperBound, index:uInt32;
    begin
      result := FALSE; // failure by default
      extentsUpperBound := superBlock^.blockSize div sizeof(TBlockRun)-1;
      
      if not isValidIAddr(inode^.blocks.indirect) then
        begin
          // allocate an indirect block
          inode^.blocks.indirect := allocBRun(inode^.inodeNum);  // alloate a block near the parent inode
          if not isValidIAddr(inode^.blocks.indirect) then
            begin
              writeln('Failed to allocate indirect block');
              exit
            end;
          extents := loadBlock(inode^.blocks.indirect);
          fillchar(extents^, superBlock^.blockSize, 0);
          // There might be garbage in the block we just allocated, so write out 
          // the empty extents pointer to disk.
          // On second thought, this is saved below no matter what, so don't
          // save it here.
          // saveBlock(inode^.blocks.indirect, extents);
        end
      else
        begin
          // the indirect block exists, so read it in
          extents := loadBlock(inode^.blocks.indirect);
          assert(extents <> NIL);
//loadBlock(inode^.blocks.indirect, extents);
        end; 
      // extents[] has the indirect block in it
      // We first check to make sure that we're not going to be moving
      // to the next section of the dataStream
      if (extents^[extentsUpperBound].len <> 0) then
        result := appendToDIndirect(newBlockRun)   // promote the newBlockRun to the DIndirect section
      else
        begin
          // scan through for the first free slot
          for index := 0 to extentsUpperBound do
            if extents^[index].len = 0 then break;
          extents^[index] := newBlockRun;              // index points to the first free slot. Fill it in with the newBlockRun
          saveBlock(inode^.blocks.indirect, extents);  // save out the indirect block. Could check for failure here
          // update the size the maxDoubleIndirect blocks reference
          inode^.blocks.maxIndirect := inode^.blocks.maxIndirect + (newBlockRun.len shl superBlock^.blockShift);
          result := TRUE;                                  // success
        end;
    end; // local function appendToIndirect()

  function appendToDirect(newBlockRun:TBlockRun):boolean;
    var index:uInt32;
    begin
      result := FALSE;
      with inode^, blocks do
        begin
          if (direct[high(direct)].len <> 0) then 
            result := appendToIndirect(newBlockRun)  
          else
            begin
              for index := 0 to high(direct) do
                if (direct[index].len = 0) then break;
              direct[index] := newBlockRun;
              maxDirect := maxDirect + (newBlockRun.len shl superBlock^.blockShift);
              result := TRUE;
            end; // else
        end; // with
    end; // local function appendToDirect

  function appendToDataStream(newBlockRun:TBlockRun):boolean;
    begin
      if (inode^.blocks.maxDIndirect <> 0) then result := appendToDIndirect(newBlockRun)
      else if (inode^.blocks.maxIndirect <> 0) then result := appendToIndirect(newBlockRun)
      else result := appendToDirect(newBlockRun);
    end; // local function appendToDataStream

  function continueIndirect(newBlockRun:TBlockRun):boolean;
    var 
      extents:^array[0..0] of TBlockRun;
      extentsUpperBound, index:uInt32;
    begin
      result := FALSE;
      extentsUpperBound := superBlock^.blockSize div sizeof(TBlockRun)-1;
      extents := loadBlock(inode^.blocks.indirect);
      assert(extents <> NIL);

      for index := extentsUpperBound downto 0 do
        if (extents^[index].len <> 0) then break;
     
      assert(extents^[index].len <> 0); // just to be sure
      // extents^[index] has the last entry
      if (uInt32(extents^[index].len) + newBlockRun.len < 65535) then
        begin
          inc(extents^[index].len, newBlockRun.len);
          inode^.blocks.maxIndirect := inode^.blocks.maxIndirect + (newBlockRun.len shl superBlock^.blockShift);
          saveBlock(inode^.blocks.indirect, extents);
          result := TRUE;
        end
      else // oops, we overflowed the length
        begin
          // need to check to see if we were the last indirect entry. If we were
          // then we need to allocate a double indirect block and store it in there
          if (index = extentsUpperBound) then
            result := appendToDIndirect(newBlockRun)
          else
            begin
              extents^[index+1] := newBlockRun;
              inode^.blocks.maxIndirect := inode^.blocks.maxIndirect + (newBlockRun.len shl superBlock^.blockShift);
              saveBlock(inode^.blocks.indirect, extents);
              result := TRUE;
            end; // else
        end; // else
     
    end; // local function continueIndirect

 function continueDirect(newBlockRun:TBlockRun):boolean;
  var
    index:uInt32;
  begin
    result := FALSE;
    with inode^, blocks do
      begin
        for index := high(direct) downto low(direct) do
          if (direct[index].len <> 0) then break;

        assert(direct[index].len <> 0);  // just to be sure
        // make sure we don't overflow the length
        if (uInt32(direct[index].len) + newBlockRun.len < 65535) then
          begin
            inc(direct[index].len, newBlockRun.len);  // update the last entry
            maxDirect := maxDirect + (newBlockRun.len shl superBlock^.blockShift);
            result := TRUE;
          end
        else  // oops, we overflowed the length
          begin
            // need to check to see if we were the last direct entry. If we were
            // then we need to allocate an indirect block and store it in there
            if (index = high(direct)) then 
              result := appendToIndirect(newBlockRun)
            else
              begin
                direct[index+1] := newBlockRun; // store it in the next available entry
                maxDirect := maxDirect + (newBlockRun.len shl superBlock^.blockShift);
                result := TRUE;
              end;
          end; // else
      end; // with
   end; // local function continueDirect

  function continueDataStream(newBlockRun:TBlockRun):boolean;
    begin
      // The extended blockRun continues a previous run.

      // When extending the file we request an allocation of more blocks 
      // near to the last blockRun of the file. If the new blocks 
      // are contiguous to the last blockRun of the file
      // then we can simply extend that blockRun.  This not only 
      // prevents fragmentation when possible, it also reduces the
      // number of actual blockRuns needed to store the file.
      if (inode^.blocks.maxDIndirect <> 0) then result := appendToDIndirect(newBlockRun)
      else if (inode^.blocks.maxIndirect <> 0) then result := continueIndirect(newBlockRun)
      else result := continueDirect(newBlockRun);
    end; // local function continueDataStream

var
  blockRun, exBlockRun:TBlockRun;
  blocksToAllocate:uInt32;

begin // extendFile()
  // XXX ToDo:
  // Need to verify support for indirect and double-indirect blocks

  // Notes:
  // 2005-04-27 mji
  //   This function is called by writeDataStream() when we're about to write
  // past the end of the file.  When extending out a file, it is almost never
  // extended by the amount written. Instead it is at least 1 block, and usually
  // more (in our case: 4, as mentioned below). Currently we pass in the number 
  // of blocks to extend the file by.  This is problematic for a few reasons. 
  //   The first is that the code to figure out how much to extend the file by 
  // would have to be in writeDataStream().  That's not necessarily a problem, 
  // except that the amount to be extended by depends on many factors, some 
  // including whether we're writing out more than the default small block 
  // extension size (currently is 4) or if we're in double indirect blocks, 
  // which extends out by NUM_DINDIRECT_BLOCKS (also currently 4).
  //   The second problem is that we save the inode here -- and also in 
  // writeDataStream() -- because the new size of the file is updated in wDS() 
  // instead of here.
  //   The third problem is when you seek past the end of the file and then
  // write.  Space should be allocated for all the space between the old
  // EOF and the new write position.  This is common behaviour for most 
  // filesystems, and should probably be emulated here as well.
  //   The proper solution is to pass in the new size and let this function 
  // work out exactly how much space on disk it is really going to take.

  result := -1;  // failure by default
  // basic sanity checks
  if (inode = NIL) or (superBlock = NIL) then exit;

  // if the file size is 0 then this is the first extension.
  if (inode^.blocks.size = 0) then  // first extension
    begin
      // A file's data is stored in the next allocation group from it's parent
      // inode. This will help distribute the data across the disk.
 
      // Find out how many blocks the new size will take up.  Do this by rounding
      // up the size to the nearest block. Then we round that up to the nearest
      // NUM_DIRECT_BLOCKS multiple.
      blocksToAllocate := uInt32(((newSize+(superBlock^.blockSize-1)) shr superBlock^.blockShift));
      blocksToAllocate := (blocksToAllocate + (NUM_EXTENSION_BLOCKS-1)) and not (NUM_EXTENSION_BLOCKS-1);
      setBlockRun(exBlockRun, nextAG(inode^.inodeNum.AG), 0, blocksToAllocate);
      exBlockRun := allocBRun(exBlockRun);    // try to allocate a blockrun
      assert(exBlockRun.len <> 0);
      if (exBlockRun.len = 0) then exit;      // erp. Couldn't extend the file
      inode^.blocks.direct[0] := exBlockRun;  // Set the first direct run
      // The space allocated for the file can and may be different than the
      // actual size.  Set the maxDirect to the size (in bytes) of how much space
      // we allocated
      inode^.blocks.maxDirect := blocksToAllocate shl superBlock^.blockShift;  
    end // size = 0
  else
    begin
    // the size of the file wasn't 0 (handled above). We need to extend the 
    // file out.  Find where the last blocks of the file are

    // Note:
    // 2005-04-28 mji
    //   We're going to have a potential problem here.  When we check to see how many blocks we
    // want to extend by, we don't know for sure if we're actually going to end up in the
    // dataStream section we were assuming.  For example, we may have no more room in the
    // indirect block section of dataStream and thus will have to store the entry in the double 
    // indirect area.  If the constants for each section are different, then we may be requesting the
    // wrong amount.  This is *only* a problem if any of the constants are different.  Therefore
    // the solution is to pick some arbitrary extension amount (probably 4 or 8 blocks) and stick 
    // with that.  If we pick the amount that will end up in the double indirect area (currently 4
    // but I'm considering 8) then we can easily break apart our blockRun if we're writing it
    // to the double indirect section.  
    // IMPORTANT NOTE
    //   Once this value is chosen it will be set in stone since we will break everything by 
    // changing it. Math done for retrieving the blockRun from the double indirect region will 
    // rely on this value, and it can never be changed.  BeFS uses 4 blocks for the double
    // indirect section, and that seems fine with me. The largest file using contiguous blockRuns
    // will be 38GB.  I am, however, pondering 8 blocks instead.  Remember, extensions are done 
    // as a multiple of this value.
    // 2005-04-28 mji
    //   I decided to use a single constant, NUM_EXTENSION_BLOCKS, and it is set to 4.
    blocksToAllocate := uInt32((newSize+(superBlock^.blockSize-1)) shr superBlock^.blockShift);    
    // blocksToAllocate is now rounded up to the nearest block of how much space we need.
    blocksToAllocate := (blocksToAllocate + (NUM_EXTENSION_BLOCKS-1)) and not (NUM_EXTENSION_BLOCKS-1);
    // blocksToAllocate is now rounded up to the nearest multiple of NUM_EXTENSION_BLOCKS
    // now subtract off how many blocks we already have
    dec(blocksToAllocate, inode^.blocks.blockCount);

    // XXX
    // I don't think this is right. We need to find the last block returned by
    // getFileSpace(), not the size.  Something to ponder/look into/test
    //blockRun := findBRun(@inode^.blocks, inode^.blocks.size shr superBlock^.blockShift);
    assert(inode^.blocks.blockCount <> 0);     // if blockCount is 0, we're in big trouble
    blockRun := findBRun(@inode^.blocks, inode^.blocks.blockCount-1);
    assert(blockRun.len <> 0);     
    if (blockRun.len = 0) then exit;           // This would be odd
    exBlockRun     := blockRun;                // copy the last run to exBlockRun
    exBlockRun.len := blocksToAllocate;               // set the requested number of blocks
writeln('numBlocks[5]: ', blocksToAllocate);
with exBlockRun do
writeln('exBlockRun before allocBRun() = ', ag, ':', start, ':', len);

    exBlockRun     := allocBRun(exBlockRun);   // allocate a block run near the previous one
with exBlockRun do
writeln('exBlockRun after allocBRun() = ', ag, ':', start, ':', len);
    assert(exBlockRun.len <> 0);
    if (exBlockRun.len = 0) then exit;         // should probably yell louder about this

    // blockRun points to the last blockRun structure associated with this inode
    if (exBlockRun.start = blockRun.start+1) and (exBlockRun.AG = blockRun.AG) then
      continueDataStream(exBlockRun)
    else
      appendToDataStream(exBlockRun);
  end;  // else size <> 0
  inode^.blocks.size := newSize;      // Now we update the size here and not in writeDataStream()
  inc(inode^.blocks.blockCount, blocksToAllocate);  // update the blockCount in the inode dataStream
  // no matter where we stored the extended blockRun, we have updated the inode

  assert(saveBlock(inode^.inodeNum, inode));  // 0 result means success
  result := 0;  // no error
end; // TUbixFS.extendFile

function TUbixFS.findBRun;
var
  position:int64;
begin
  setBlockRun(result, 0, 0, 0);
  if (dataStream = NIL) then exit;
  if (blockNum >= dataStream^.blockCount) then
    begin
      writeln('Requested block outside the block count in the dataStream');
      exit
    end;
  position := int64(blockNum) shl superBlock^.blockShift;
  if (position < dataStream^.maxDirect) then
    result := findBRunDirect(dataStream, blockNum)
  else if (position < dataStream^.maxIndirect) then
    result := findBRunIndirect(dataStream, blockNum)
  else if (position < dataStream^.maxDIndirect) then
    result := findBRunDIndirect(dataStream, blockNum)
  else writeln('Block position is not mapped by dataStream');
end; // TUbixFS.findBRun

function TUbixFS.findBRunDirect;
var
  sum, i, offset:uInt32;
begin
  // set the result to a nil blockrun
  setBlockRun(result, 0, 0, 0);

  sum := 0;
  with dataStream^ do
    for i := low(direct) to high(direct) do
      with direct[i] do
        begin
          if (blockNum < sum + len) then                // found it
            begin
              offset := blockNum - sum;
              setBlockRun(result, AG, start + offset, len - offset);
              exit
            end;
          inc(sum, len);
        end;  // for/with
end; // TUbixFS.findBRunDirect

function TUbixFS.findBRunIndirect;
var 
  extents:^array[0..0] of TBlockRun;
  sum, i, offset:uInt32;
begin
  // NOTE: Basic error checking on whether superblock <> NIL isn't done here.
  setBlockRun(result, 0, 0, 0);

  // load the indirect block
  extents := loadBlock(dataStream^.indirect);
  assert(extents <> NIL);

  // We either need to adjust sum forward or blockNum back.
  // sum := (dataStream^.maxDirect+superBlock^.blockSize-1) shr superBlock^.blockShift;

  // We need to decrease blockNum by the amount of blocks the direct portion took up.
  // This can be calculated by dividing the maxDirect byte size by the block size.

  blockNum := blockNum - dword(dataStream^.maxDirect shr superBlock^.blockShift);
  sum := 0;

  for i := 0 to (superBlock^.blockSize div sizeof(TBlockRun))-1 do
    with extents^[i] do
      begin
        if (blockNum < sum + len) then                // found it
          begin
            offset := blockNum - sum;
            setBlockRun(result, AG, start + offset, len - offset);
            break
          end;
        inc(sum, len);
      end; // for/with

end; // TUbixFS.findBRunIndirect

function TUbixFS.findBRunDIndirect;
var 
  extents:^array[0..0] of TBlockRun;
  sum, i, offset:uInt32;
begin
  // NOTE: Basic error checking on whether superblock <> NIL isn't done here.
  setBlockRun(result, 0, 0, 0);

  extents := loadBlock(dataStream^.doubleIndirect);
  assert(extents <> NIL);
  // Now that we've loaded the double indirect block, we need to 
  // find the indirect block that contains the block run we're looking for.
  // This is done with some clever shifting
  
  // first adjust the blockNum, taking into account how many blocks are used
  // by the direct and indirect sections

  blockNum := blockNum - dword(dataStream^.maxIndirect shr superBlock^.blockShift);

  // Each indirect block holds NUM_EXTENSION_BLOCKS*(superBlock^.blockSize div sizeof(TBlockRun)) blocks
  // e.g.
  //   NUM_EXTENSION_BLOCKS = 4 
  //   blockSize = 1024
  //   sizeof(TBlockRun) = 8
  //   blocks referenced per indirect block = 4 * (1024 / 8) === 512
  //   indirect block index = (block we're looking for) / (blocks referenced per indirect block)
  //   blockrun index in indirect block = (block we're looking for) % (blocks referenced per indirect block)

  i := blockNum div (NUM_EXTENSION_BLOCKS * (superBlock^.blockSize div sizeof(TBlockRun))); 
  // i should now hold which entry in the double indirect block we need to load
  assert(i < (superBlock^.blockSize div sizeof(TBlockRun)));

  // load the proper indirect block beneath the double indirect block
  extents := loadBlock(extents^[i]);
  assert(extents <> NIL);

  // now seek into this indirect block by taking the modulus of the number
  // of blocks referenced by an indirect block
  i := blockNum mod (NUM_EXTENSION_BLOCKS * (superBlock^.blockSize div sizeof(TBlockRun)));
  with extents^[i] do
    begin
      // extents^[i] is a block extent inside an indirect block which
      // was referenced by the double indirect block.  Now we need to see 
      // which block inside this extent we wanted. We do that by taking the 
      // modulus of the requested block number by the number of blocks 
      // referenced in each blockrun.  BeFS used 4 per; and as of writing 
      // this comment that is what I'm using.
      offset := blockNum and (NUM_EXTENSION_BLOCKS-1);
      setBlockRun(result, AG, start + offset, len - offset);
    end; // with
  
end; // TUbixFS.findBRunDIndirect

function TUbixFS.freeUsedBlocks;
var 
  FBL:int8ArrayPtr;
  FBLSize:uInt32;
  startByte, endByte:uInt32;
  bitCount, bitOffset:uInt32;
  startBlock:uInt32;
  byteOffset:uInt32;
begin
  // Notes:
  // 2005-04-27 mji
  //   freeUsedBlocks() will clear bits in the BAT that are associated
  // with the blockRun passed in.  To do this we load only the portion
  // of the BAT we're going to modify. (As a side note, readDataStream()
  // will load a minimum of one block anyway, so this may not really be
  // much of an optimization).
 
  result := -1;
  // basic sanity checks
  if ((superBlock = NIL) or (blockRun.len = 0)) then exit;

  // Compute how much of the BAT we have to read in.
  with blockRun, superBlock^ do
    begin
      startBlock := (AG shl AGShift) + start;
      startByte := startBlock shr 3;       // divide by 8 since it's a bitfield
      endByte := (startBlock + len) shr 3; // divide by 8 since it's a bitfield
      FBLSize := endByte - startByte + 1;

      GetMem(FBL, FBLSize);
      assert(readDataStream(BAT, startByte, FBL, FBLSize) = 0); 
      bitOffset := 7-(startBlock and 7);
      byteOffset := 0;

      for bitCount := 1 to len do
        begin   
          FBL^[byteOffset] := FBL^[byteOffset] and not (1 shl bitOffset);
          if (bitOffset = 0) then 
            begin
              bitOffset := 7;
              inc(byteOffset);
            end
          else dec(bitOffset);
        end; // for bitCount
      // save back out the modified block
      assert(writeDataStream(BAT, startByte, FBL, FBLSize) = 0);
 
      FreeMem(FBL, FBLSize);
      usedBlocks := usedBlocks - len;
    end; // with

  result := 0;
end; // TUbixFS.freeUsedBlocks

function TUbixFS.getFileSize;
begin
  // XXX ToDo:
  // Fix to support sizes > 4GB
  result := 0;
  if (inode = NIL) then exit;
  result := inode^.blocks.size;
end;  // TUbixFS.getFileSize

function TUbixFS.getFileSpace;
begin
  // getFileSpace() will return the size of the file's allocated space
  result := 0;
  if (inode = NIL) or (superBlock = NIL) then exit;
  result := inode^.blocks.blockCount shl superBlock^.blockShift;
end; // TUbixFS.getFileSpace

function TUbixFS.iAddrToStr;
var s:string;
begin
  // Note: this won't work in C/C++ because strings are really null
  // terminated character arrays. So, the B^Tree won't hold a string
  // with nulls in it properly
  length(s) := sizeof(iAddr);
  move(iAddr, s[1], sizeof(iAddr));
  result := s;
end; // TUbixFS.iAddrToStr;

function getRDTSCTime:int64;
var x:int64;
begin
  asm
        rdtsc
        mov     dword ptr x, eax
        mov     dword ptr x+4, edx 
  end;
  result := x;
end;

function TUbixFS.loadBlock;
var 
  blockAddress:int64;
  cacheNode:PCacheNode;
  u:uPtr;
  searchRec:BTreeSearchRec;
begin
  result := NIL;  // failure by default;
  // 2005-04-29 mji
  // I'm not really sure how much error checking to put in here. Chances are that
  // if we're calling this function everything is fine.  For now, with such limited
  // use I'll keep it pretty light. I think that this will eventually be used for 
  // dealing with the block cache, so more checking might be required later.
  if (dev = NIL) or (superBlock = NIL) or (blockCache = NIL) then exit; 

  with blockAddr, superBlock^ do
    blockAddress := (AG shl AGShift) + start;

  fillchar(u, sizeof(u), 0);  // just to be safe
  cacheNode := NIL;
  // XXX ToDo
  // Fix this so that if there is no cache, we only have one pointer
  // Check for a full cache

  if (blockCache^.Find(@blockAddress, u)) then
    begin
      // block was in the cache. 
      assert(u.p <> NIL);
      cacheNode := u.p;
      assert(cacheNode^.data <> NIL);
      // If we're going to honour the locked flag in the cacheNode, then
      // we should put a mutex here
      
      // Now we need to update the blockTime. We do this by first deleting
      // the cacheNode out of the blockTime cache.
      blockTime^.Delete(@cacheNode^.lastUsed, u);  // Delete old one
      cacheNode^.lastUsed := getRDTSCTime();       // Get the new timestamp
      blockTime^.Insert(@cacheNode^.lastUsed, u);  // Insert with new time
      result := cacheNode^.data;                   // return the data pointer
    end
  else
    begin
      // Requested block isn't in cache

      // XXX ToDo:
      // Check to see if we've hit the maximum cache entry limit. If so, we 
      // need to drop off the least recently used (look for the first key 
      // in blockTime) and possibly save it to disk if it has been modified.

      if (blockCache^.getKeyCount() = maxCacheEntry) then
        begin
          blockTime^.getFirstKey(searchRec);                  // least recently used block
          cacheNode := searchRec.value.p;                     // get the pointer from the value
          assert(cacheNode <> NIL);
          // I suppose we could write a syncBlock() function, since this
          // code is also duplicated in sync().
          if (cacheNode^.dirty) then
            begin
              with cacheNode^ do
                dev^.write(data, 
                           address * (dataSize shr SECTOR_SHIFT),
                           dataSize shr SECTOR_SHIFT
                          ); // dev^.write    
              cacheNode^.dirty := FALSE;
            end; // if dirty
          u.p := cacheNode;
          blockCache^.Delete(@cacheNode^.address, u);
          blockTime^.Delete(@cacheNode^.lastUsed, u);
          // We've deleted the cacheNode out of both the blockCache and
          // the blockTime trees. We're going to re-use the cacheNode.
          cacheNode^.address := blockAddress;
          cacheNode^.lastUsed := getRDTSCTime();
          // No need to update data, dataSize, or dirty. We re-use the first two,
          // and dirty will be set to FALSE above if necessary.  Might need to 
          // reset locked if we're using that.
        end
      else
        new(cacheNode, init(blockAddress, getRDTSCTime(), superBlock^.blockSize));

      assert(cacheNode <> NIL);
      assert(cacheNode^.data <> NIL);
      u.p := cacheNode;
      if not (blockCache^.Insert(@blockAddress, u)) then
        writeln('Error inserting into blockCache');
      if not (blockTime^.Insert(@cacheNode^.lastUsed, u)) then
        writeln('Error inserting into blockTime');
      // now we need to fill the data from the device
      with superBlock^ do
        dev^.read(cacheNode^.data,
                  blockAddress * (blockSize shr SECTOR_SHIFT),
                  blockSize shr SECTOR_SHIFT
                 ); // dev^.read
      result := cacheNode^.data;
    end; 
end; // TUbixFS.loadBlock

function TUbixFS.markUsedBlocks;
var 
  FBL:uInt8ArrayPtr;
  FBLSize:uInt32;
  startByte, endByte:uInt32;
  bitCount, bitOffset:uInt32;
  startBlock:uInt32;
  byteOffset:uInt32;
begin

  result := -1;
  // basic sanity checks
  if ((superBlock = NIL) or (blockRun.len = 0)) then exit;

with blockRun do writeln('---- marking used blocks: ', AG, ':', start, ':', len);

  // Compute how much of the BAT we have to read in.
  with blockRun, superBlock^ do
    begin
      startBlock := (AG shl AGShift) + start;
      startByte := startBlock shr 3;
      endByte := (startBlock + len) shr 3;
      FBLSize := endByte - startByte + 1;
      GetMem(FBL, FBLSize);
      fillchar(FBL^, FBLSize, 0);  // better safe than sorry
      assert(readDataStream(BAT, startByte, FBL, FBLSize) = 0); 

      bitOffset := 7-(startBlock and 7);
      byteOffset := 0;
      for bitCount := 1 to len do
        begin   
          FBL^[byteOffset] := FBL^[byteOffset] or (1 shl bitOffset);
          if (bitOffset = 0) then 
            begin
              bitOffset := 7;
              inc(byteOffset);
            end
          else dec(bitOffset);
        end; // for bitCount
      assert(writeDataStream(BAT, startByte, FBL, FBLSize) = 0);
 
      FreeMem(FBL, FBLSize);

      usedBlocks := usedBlocks + len;
    end; // with
  result := 0;
end; // TUbixFS.markUsedBlocks

function TUbixFS.nextAG;
begin
  // there's no error we can throw from here if superblock is NIL,
  // so don't bother to check.  Just hope whomever calls this
  // function has done some basic sanity checks
  result := (AG+1) mod superBlock^.numAGs
end; // TUbixFS.nextAG

function TUbixFS.pathToIAddr;

  function recursePath(path:PChar; len:integer; iAddr:inodeAddr):inodeAddr;
    var 
      dirTree:PBTree;
      u:uPtr;
      nameSize:integer;
      pfs:PUbixBTreeVFS;
    begin
      // if the string length is 0 then we have found the inode in the previous
      // recurse, so return what we found.
      if (len = 0) then 
        begin
          result := iAddr;
          exit;     
        end
      else
        setIAddr(result, 0, 0, 0);
      // hm. If we're looking for something like ``/.'' then the ``/'' is stripped
      // off in the main call leaving only ``.'', which means that this malformed 
      // path check would trigger.  Same with /foo/bar if bar is a dir.
      //if uInt8ArrayPtr(path)^[0] = ord('/') then exit;  // malformed path
      if not (isValidIAddr(iAddr)) then exit;           // sanity check
      nameSize := 0;
      while (len > 0) and (uInt8ArrayPtr(path)^[nameSize] <> ord('/')) do
        begin
          inc(nameSize);
          dec(len);    
        end;
      uInt8ArrayPtr(path)^[nameSize+1] := 0;
      if (len <> 0) then dec(len);
      new(dirTree, open(IAddrToStr(iAddr), new(PUbixBTreeVFS, init(@self))));
      if (dirTree = NIL) then exit;  // hm.. what happens to the PUbixBTreeVFS is this is NIL?
      // note that the dirTree is a tree of BT_PCHAR, and searches are case sensitive.
      // if the tree doesn't exist, Find() should return false.
      if dirTree^.Find(path, u) then result := recursePath(path + nameSize + 1, len, u.iAddr);
      dispose(dirTree, done);
    end; // recursePath

var len:integer;
begin
  setIAddr(result, 0, 0, 0);

  if (path = NIL) or (superBlock = NIL) then exit;  

  len := strlen(path);
  if (len = 0) then exit;                            // empty string
  if uInt8ArrayPtr(path)^[0] <> ord('/') then exit;  // malformed path

  result := recursePath(path+1, len-1, superBlock^.rootDir);

end; // TUbixFS.pathToIAddr

procedure TUbixFS.printSuperBlock;
begin
  if (superBlock = NIL) then exit;
  with superBlock^ do
    begin
      writeln('superBlock^.name........... ', name);
      writeln('superBlock^.magic1......... ', hex(magic1));
      writeln('superBlock^.fsByteOrder.... ', fsByteOrder);
      writeln('superBlock^.blockSize...... ', blockSize);
      writeln('superBlock^.blockShift..... ', blockShift);
      writeln('superBlock^.numBlocks...... ', numBlocks);
      writeln('superBlock^.usedBlocks..... ', usedBlocks);
      writeln('superBlock^.magic2......... ', hex(magic2));
      writeln('superBlock^.blocksPerAG.... ', blocksPerAG);
      writeln('superBlock^.AGShift........ ', AGShift);
      writeln('superBlock^.numAGs......... ', numAGs);
      writeln('superBlock^.flags.......... ', hex(flags));
      writeln('superBlock^.magic3......... ', hex(magic3));
      with superBlock^.BAT do
        writeln('superBlock^.BAT............ ', AG, ':', start, ':', len);
      with superBlock^.rootDir do
        writeln('superBlock^.rootDir........ ', AG, ':', start, ':', len);
    end;
end; // TUbixFS.printSuperBlock

function TUbixFS.saveBlock;
var 
  blockAddress:int64;
  cacheNode:PCacheNode;
  u:uPtr;
  searchRec:BTreeSearchRec;
begin

  result := FALSE;  // failure by default;
  if (blockCache = NIL) or (blockTime = NIL) or (dev = NIL) or (superBlock = NIL) then exit;
  with blockAddr, superBlock^ do
    blockAddress := (AG shl AGShift) + start;
  cacheNode := NIL;
  fillchar(u, sizeof(u), 0); // best be safe
  // first check to see if the blockAddress is in the cache
  if (blockCache^.Find(@blockAddress, u)) then
    begin
      // block was in cache
      cacheNode := u.p;
      assert(cacheNode <> NIL);
      assert(cacheNode^.data <> NIL);
      if (cacheNode^.data <> buf) then
        begin
          // The buffer with the data we want to save was not the same as the one
          // in the cache. This means that it was not obtained with loadBlock().
          // We need to copy that pointer to our block.
          move(buf^, cacheNode^.data^, cacheNode^.dataSize);
        end;
      // Now we need to update the blockTime. We do this by first deleting
      // the cacheNode out of the blockTime cache.
      blockTime^.Delete(@cacheNode^.lastUsed, u);  // Delete old one
      cacheNode^.lastUsed := getRDTSCTime();       // Get the new timestamp
      blockTime^.Insert(@cacheNode^.lastUsed, u);  // Insert with new time
    end
  else
    begin
      // block was not in cache
      if (blockCache^.getKeyCount() = maxCacheEntry) then
        begin
          blockTime^.getFirstKey(searchRec);
          cacheNode := searchRec.value.p;
          assert(cacheNode <> NIL);
          // I suppose we could write a syncBlock() function, since this
          // code is also duplicated in sync().
          if (cacheNode^.dirty) then
            begin
              with cacheNode^ do
                dev^.write(data, 
                           address * (dataSize shr SECTOR_SHIFT),
                           dataSize shr SECTOR_SHIFT
                          ); // dev^.write    
              cacheNode^.dirty := FALSE;
            end; // if dirty
          u.p := cacheNode;
          blockCache^.Delete(@cacheNode^.address, u);
          blockTime^.Delete(@cacheNode^.lastUsed, u);
          // We've deleted the cacheNode out of both the blockCache and
          // the blockTime trees. We're going to re-use the cacheNode.
          cacheNode^.address := blockAddress;
          cacheNode^.lastUsed := getRDTSCTime();
          // No need to update data, dataSize, or dirty. We re-use the first two,
          // and dirty will be set to FALSE above if necessary.  Might need to 
          // reset locked if we're using that.
        end
      else
        new(cacheNode, init(blockAddress, getRDTSCTime(), superBlock^.blockSize));

      assert(cacheNode <> NIL);
      assert(cacheNode^.data <> NIL);
      move(buf^, cacheNode^.data^, cacheNode^.dataSize);
      u.p := cacheNode;
      if not (blockCache^.Insert(@blockAddress, u)) then
        writeln('Error inserting into blockCache');
      if not (blockTime^.Insert(@cacheNode^.lastUsed, u)) then
        writeln('Error inserting into blockTime');
    end;
  cacheNode^.dirty := TRUE;  // indicate the block has been modified
  result := TRUE;
end; // TUbixFS.saveBlock

function TUbixFS.isValidIAddr;
begin
  result := (iAddr.len <> 0) and (iAddr.AG < superBlock^.numAGs); 
end; // TUbixFS.isValidIAddr

function TUbixFS.readDataStream;
var 
  tmpBlock:uInt8ArrayPtr;
  blockRun:TBlockRun;
  size, remainder:uInt32;
  blockStart:uInt32;
  blocksToRead:uInt32;
  bytesToRead:uInt32;
  inode:PUbixFSInode;
begin
 
  result := -1;
  if ((buf = NIL) or (superBlock = NIL)) then exit;
  inode := loadBlock(iAddr);
  tmpBlock := NIL;
  size := length;
  remainder := uInt32(position mod superBlock^.blockSize);

  if (remainder <> 0) then
    begin
      // the position we've been asked to read from doesn't align to
      // a block size. We need to read in the previous block and pad
      // the data
      blockStart := uInt32(position shr superBlock^.blockShift);
      blockRun := findBRun(@inode^.blocks, blockStart);
      assert(blockRun.len > 0);
      tmpBlock := loadBlock(blockRun);
      assert(tmpBlock <> NIL);
      bytesToRead := min(size, (superBlock^.blockSize - remainder));
      move(tmpBlock^[remainder], buf^, bytesToRead);
      inc(buf, bytesToRead);
      dec(size, bytesToRead);
      position := position + bytesToRead;
    end; // if remainder != 0

  // If the blockSize is always a power of 2, then you can eliminate the MOD and use
  // an AND operation instead, e.g. remainder := size and (superBlock^.blockSize-1)
  remainder := size mod superBlock^.blockSize; 
  dec(size, remainder);

  // Size is currently in bytes. The remainder variable above holds the number
  // of remaining bytes (if any) outside full blocks we have to write.

  // convert the size into blocks
  size := size shr superBlock^.blockShift;

  while (size > 0) do
    begin
      blockRun := findBRun(@inode^.blocks, dword(position shr superBlock^.blockShift));
      assert(blockRun.len > 0);
      for blocksToRead := 0 to min(blockRun.len, size)-1 do
        begin
          tmpBlock := loadBlock(blockRun);
          assert(tmpBlock <> NIL);
          move(tmpBlock^, buf^, superBlock^.blockSize);
          dec(size);
          inc(buf, superBlock^.blockSize);
          inc(blockRun.start);  // This might wrap around?
          dec(blockRun.len);    // might be unnecessary
        end; // for
    end; // while 

  if (remainder <> 0) then
    begin
      // There is less than a block of data left to read.
      blockStart := dword((position+length-remainder) shr superBlock^.blockShift);
      blockRun := findBRun(@inode^.blocks, blockStart);
      assert(blockRun.len > 0);
      tmpBlock := loadBlock(blockRun);
      assert(tmpBlock <> NIL);
      move(tmpBlock^, buf^, remainder);      
    end;

  result := 0;
end; // TUbixFS.readDataStream

procedure TUbixFS.setBlockRun;
begin
  blockRun.AG    := AG;
  blockRun.start := start;
  blockRun.len   := len;
end; // TUbixFS.setBlockRun

procedure TUbixFS.setIAddr;
begin
  inode.AG    := AG;
  inode.start := start;
  inode.len   := len;
end; // TUbixFS.setIAddr

function TUbixFS.strToIAddr;
begin
  // Note: this won't work in C/C++ because strings are really null
  // terminated character arrays. So, the B^Tree won't hold a string
  // with nulls in it properly
  // begin fiendishly clever hack
  move(s[1], result, sizeof(inodeAddr));
  // end fiendishly clever hack
end; // TUbixFS.strToIAddr

procedure TUbixFS.sync;
var 
  cacheNode:PCacheNode;
  searchRec:BTreeSearchRec;
begin
  // XXX ToDo:
  // Need to mark the superblock as clean.

  if (blockCache = NIL) or (dev = NIL) then exit;
  // We sync out the blocks in ascending order from the blockCache.
  // To do this we examine each entry to see if the block is dirty.
  // If it is, we write it out to the device and set dirty to FALSE.
  if (blockCache^.getFirstKey(searchRec)) then
    repeat
      cacheNode := searchRec.value.p;
      assert(cacheNode <> NIL);
      if (cacheNode^.dirty) then
        begin
          with cacheNode^ do
            dev^.write(data, 
                       address * (dataSize shr SECTOR_SHIFT),
                       dataSize shr SECTOR_SHIFT
                       ); // dev^.write    
          cacheNode^.dirty := FALSE;
        end;
    until not blockCache^.FindNext(searchRec);
  // Also write out the superblock.
  dev^.write(superBlock, 1, 1);
end; // TUbixFS.sync

function TUbixFS.vfs_close;
var 
  u:uPtr;
  vnid:PVnid;
begin
  result := -1;
  if (fileDescriptors = NIL) or (handle >= maxFDEntry) then exit;
  if not (vnids^.Find(@fileDescriptors^[handle].iAddr, u)) then writeln('Could not find iAddr in vnids');
  vnid := u.p;
  if (vnid = NIL) then 
    begin
      writeln('vnid is NIL');
      exit;
    end; 

  with vnid^ do
    begin
      dec(refCount);
      if (refCount = 0) then 
        begin   
          // refCount has fallen to 0. If the file is deleted we should take care of it here
          vnids^.Delete(@fileDescriptors^[handle].iAddr, u);
        end;
    end;
  if not vNodeQueue^.enqueue(handle) then writeln('Error enqueuing handle back into vNodeQueue');
  result := 0;
end; // TUbixFS.vfs_close

function TUbixFS.vfs_fCreate;
var 
  inode:PUbixFSInode;
  dirTree:PBTree;
  dirIAddr, fileIAddr:inodeAddr;
  filedes:int64;
  u:uPtr;
  searchRec:BTreeSearchRec;
  len : integer;
  path:PChar;
  filenamePtr:PChar; 
  curPos:integer;
  vnid:PVnid;
  handle:integer;
begin
  result := -1;  
  if (filename = NIL) or (superBlock = NIL) then exit;
  // we need to split the filename into it's component path and filename pieces.
  // From there we need to see if the file exists.  If it does then I guess we 
  // should truncate the file and load the inode.  Otherwise allocate a new inode
  len := strlen(filename);
  if (len = 0) then exit;
  if uInt8ArrayPtr(filename)^[0] <> ord('/') then exit;  // malformed path
  GetMem(path, len+1);
  strCopy(path, filename);
  curPos := len;

  setIAddr(dirIAddr, 0, 0, 0);  // zero out the inode address for the directory
  setIAddr(fileIAddr, 0, 0, 0); // zero out the inode address for the file

  while (curPos > 0) and (uInt8ArrayPtr(path)^[curPos] <> ord('/')) do dec(curPos);
  fileNamePtr := path + curPos + 1;
  if (curPos = 0) then
    begin
      // file is in the root dir
      writeln('path: /');
      writeln('filename: ', fileNamePtr);
      dirIAddr := pathToIAddr('/');
    end
  else
    begin
      uInt8ArrayPtr(path)^[curPos] := 0; // null out the separator
      writeln('Path: ', path);
      writeln('Filename: ', fileNamePtr);
      dirIAddr := pathToIAddr(path);
    end; 

  with dirIAddr do
    writeln('iaddr to path: ', AG, ':', start, ':', len);
  assert(isValidIAddr(dirIAddr));
  if not isValidIAddr(dirIAddr) then exit;
  // should probably check to make sure that we're opening a directory and not a file
  new(dirTree, open(IAddrToStr(dirIAddr), new(PUbixBTreeVFS, init(@self))));
  if (dirTree = NIL) then exit;
  // We need to ascertain if the file is existing or new.
  // If the file exists, we should truncate the file
  // If the file is new, we need to create a new inode and write the entry to the directory
  // Then we need to allocate a file descriptor and store it somewhere.  
  if dirTree^.Find(fileNamePtr, u) then 
    begin
      writeln(fileNamePtr, ' already exists');
    end
  else
    begin
      writeln('creating new file: ', fileNamePtr);
      writeln('allocating new inode');
      inode := allocInode(dirIAddr);   // alloc a new inode (pass in the parent iaddr)
      assert(inode <> NIL);            // make sure it isn't nil
      if (inode = NIL) then exit;
      saveBlock(inode^.inodeNum, inode);                // save the new inode
      u.iAddr := inode^.inodeNum;      // copy the inode addr to a uPtr      
      // now add the file to the directory
      writeln('Inserting ', fileNamePtr, ' into directory returned ', dirTree^.Insert(fileNamePtr, u));
    end;  
  dispose(dirTree, done);
  FreeMem(path, len+1);

  // This file doesn't have an associated vnid. Create a new one.
  new(vnid);
  vnid^.refCount := 1;
  vnid^.iAddr := inode^.inodeNum;
  vnid^.mode := 0; // XXX ?
  u.p := vnid;
  if not (vnids^.insert(@inode^.inodeNum, u)) then writeln('Could not insert into vnids');

  if not vNodeQueue^.dequeue(handle) then 
    begin 
      writeln('Out of file descriptors!');
      exit;
    end;

  with fileDescriptors^[handle] do
    begin
      iAddr := inode^.inodeNum;
      position := 0;
      flags := 0;  // XXX ?
    end;
  result := handle;
end; // TUbixFS.vfs_fCreate

function TUbixFS.vfs_fExist;
begin
  // XXX ToDo:
  // find out why fExist('/./.') fails
  // fExist('/.') no longer fails
  
  result := -1;
  if ((superBlock = NIL) or (path = NIL)) then exit;
  if (not isValidIAddr(superBlock^.rootDir)) then exit;
  if isValidIAddr(pathToIAddr(path)) then result := 0;
end; // TUbixFS.vfs_fExist

function TUbixFS.vfs_format;
var
  sector:array[0..511] of byte;
  fs:PUbixFS;
  logicalBlocks, fsBATSize, fsBATBlocks:uInt32;
  sb:PdiskSuperBlock;
  inode:PubixfsInode;
  i, fsBlockShift, lBlockShift, nodeSize, keyLength:integer;
  blockRun:TBlockRun;
  remainderBlocks:uInt32;
  blockBit:shortint;
begin
  result := -1;
  if (device = NIL) then exit;
  if (fsBlockSize < 1024) then
    begin
      writeln('Blocksize must be >= 1024');
      exit;
    end;
  // zero out the sector
  fillchar(sector, sizeof(sector), 0);

  writeln('device^.sectors: ', device^.sectors);

  if not quick then
    begin
      system.write('clearing device... ');

      for i := 0 to device^.sectors-1 do
        device^.write(@sector, i, 1);

      writeln('done');
    end; // if !quick


  fsBlockShift := 10;  {minimum is 1024}
  while (1 shl fsBlockShift < fsBlockSize) do inc(fsBlockShift);

  // allocate a new superBlock and clear it

  new(sb);
  if (sb = NIL) then exit;
  fillchar(sb^, sizeof(diskSuperBlock), 0);

  // device^.sectors is the number of 512 byte sectors

  lBlockShift := fsBlockSize shr 9;
  writeln('lBlockShift: ', lBlockShift);
  logicalBlocks := device^.sectors div lBlockShift;        // logical blocks
  writeln('logical blocks: ', logicalBlocks);

  with sb^ do
    begin
      strcopy(name, 'UbixFS');
      magic1      := UBIXFS_MAGIC_1;
      fsByteOrder := 0;
      blockSize   := fsBlockSize;
      blockShift  := fsBlockShift;
      numBlocks   := logicalBlocks;
      usedBlocks  := 0;            
      inodeSize   := fsBlockSize;
      magic2      := UBIXFS_MAGIC_2;
      blocksPerAG := min(65536, blockSize shl 3);
      AGShift     := 10;  // realistically it should be a min of 13
      while (1 shl AGShift <> blocksPerAG) do inc(AGShift);
      numAGs      := (dword(numBlocks)+(blocksPerAG-1)) div blocksPerAG;

      flags       := UBIXFS_CLEAN;
      setBlockRun(logBlocks, 0, 0, 0);
      logStart    := 0;
      logEnd      := 0;
      magic3      := UBIXFS_MAGIC_3;
      setIAddr(indicies, 0, 0, 0);
      setIAddr(rootDir, 0, 0, 0);
    end; // with
  // create the free block list inode 

  getmem(inode, fsBlockSize);  // make the inode the same size as a block
  if (inode = NIL) then exit;

  fillchar(inode^, sb^.inodeSize, 0);

  with inode^ do
    begin
      magic := UBIXFS_INODE_MAGIC;

      // inodes point to themselves
      setIAddr(inodeNum, 0, sb^.logBlocks.len+1, 1); // the +1 for the start might be wrong
      setIAddr(parent, 0, 0, 0);

      // If the partition is larger than 550GB we will need to allocate more than
      // one blockRun for the BAT.
      fsBATSize := logicalBlocks shr 3;  // compute how many bytes the BAT takes up
      fsBATBlocks := (fsBATSize + fsBlockSize-1) div fsBlockSize;
      writeln('fsBATSize: ', fsBATSize);
      writeln('fsBATBlocks: ', fsBATBlocks);
      setBlockRun(blocks.direct[0], 0, inodeNum.start+1, fsBATBlocks);

      blocks.size := (fsBATBlocks * fsBlockSize); // round the file size up to the nearest block size
      blocks.maxDirect := blocks.size-1;
      blocks.blockCount := fsBATBlocks;

      uid := 0;
      gid := 0;
      mode := S_IFDIR;

      flags := INODE_NO_FLAG;

      setIAddr(attributes, 0, 0, 0);
      inodeSize := sb^.inodeSize;

  end; {with}
  sb^.BAT := inode^.inodeNum;
  writeln('writing out superblock to sector 1');
  device^.write(sb, 1, 1);
  // note that the superblock does not contain proper inode addresses for the
  // BAT and rootDir. Those are sync'd to disk in the destructor after they've been
  // created below. 
    
  system.write('Writing out BAT inode... ');
  // write out the Block Allocation Table inode
  with inode^.inodeNum, sb^ do
    device^.write(inode,
                  ((AG shl AGShift) + start) * (inodeSize shr SECTOR_SHIFT),
                  inodeSize shr SECTOR_SHIFT
                 ); // device^.write 
  writeln('done');
  // Okay, the superblock is the only piece of information needed
  // to mount an UbixFS partition
  new(fs, init(device));
  assert(fs <> NIL);
  assert(fs^.vfs_init() = 0);

  // The freshly formatted parition is now mounted. 
  // From here we need to create:
  //   BAT (Block Allocation Table) file contents
  //   journal
  //   root dir 

  // inode still points to the BAT. Clear out the file in case people decided to
  // use quick formatting
  for i := 0 to (dword(inode^.blocks.size) div sizeof(sector))-1 do
    fs^.writeDataStream(sb^.BAT, i*sizeof(sector), @sector, sizeof(sector));
  setBlockRun(blockRun, 0, 0, 1);
  fs^.markusedBlocks(blockRun);         // mark superBlock block as used
  // mark the journal as used here
  fs^.markUsedBlocks(sb^.BAT);  // mark the BAT inode as used
  // now mark the BAT file blocks as used. Note that if the BAT takes up
  // more than one blockRun (unlikely, but not impossible) then this code
  // will have to check for that.
  writeln('BAT inode blocks.direct[0].len: ', inode^.blocks.direct[0].len);
  fs^.markUsedBlocks(inode^.blocks.direct[0]);  

  // If the number of blocksPerAG doesn't divide evenly into the
  // number of logical blocks, mark out all the "remainder" blocks
  // We need to mark the blocks as used in the BAT manually because
  // if we do it through markUsedBlocks() it will change the usedBlocks
  // count.

  remainderBlocks := (fsBATBlocks shl sb^.blockShift) - fsBATSize;
  writeln('RemainderBlocks: ', remainderBlocks);
  if (remainderBlocks <> 0) then
    begin
      // check if the remainder blocks are evenly divisible by 8
      if (remainderBlocks and 7 <> 0) then
        begin
          blockBit := uInt8(1 shl (remainderBlocks and 7)) -1;
          fs^.writeDataStream(sb^.BAT, fsBatSize, @blockBit, 1);
          blockBit := -1;
          for i := fsBatSize+1 to (fsBATBlocks shl sb^.blockShift)-1 do
            fs^.writeDataStream(sb^.BAT, i, @blockBit, 1);
        end
      else
        begin
          blockBit := -1;
          for i := fsBatSize to (fsBATBlocks shl sb^.blockShift)-1 do
            fs^.writeDataStream(sb^.BAT, i, @blockBit, 1);
        end;
    end;

  fs^.vfs_mkdir('/');
  if (fs^.vfs_fExist('/.') = -1) then 
    writeln('Warning! /. doesn''t exist') 
  else 
    writeln('fExist(''/.'') returned properly');
  if (fs^.vfs_fExist('/nofile.dat') <> -1) then 
    writeln('Warning! /nofile.dat exists?') 
  else 
    writeln('fExist(''/nofile.dat'') returned properly');

  if (fs^.vfs_fExist('/nodir/nofile.dat') <> -1) then 
    writeln('Warning! /nodir/nofile.dat exists?') 
  else 
    writeln('fExist(''/nodir/nofile.dat'') returned properly');

//  writeln('fs^.vfs_fExist(''/.'') returned: ', fs^.vfs_fExist('/.'));
//  writeln('fs^.vfs_fExist(''/nofile.dat'') returned: ', fs^.vfs_fExist('/nofile.dat')); 
  fs^.printSuperBlock;
  
  dispose(fs, done);
  writeln('Format complete!');
  dispose(sb);
  freemem(inode, fsBlockSize);
   
end; // TUbixfS.vfs_format

function TUbixFS.vfs_init;
var index:uInt32;
begin
  // This function should probably not be called more than once without
  // calling the appropiate shutdown code
  writeln('TUbixFS.vfs_init()');
  result := -1;
  if (dev = NIL) then exit;
  new(superBlock);
  assert(superBlock <> NIL);
  if (superBlock = NIL) then exit;
  fillchar(superBlock^, sizeof(superBlock^), 0); // better safe than sorry
  // read in the superBlock. It's always the 1st block on the partition (counting from 0)
  system.write('reading in superBlock... ');
  dev^.read(superBlock, 1, 1);
  writeln('done');

  assert(superBlock^.magic1 = UBIXFS_MAGIC_1);
  assert(superBlock^.magic2 = UBIXFS_MAGIC_2);
  assert(superBlock^.magic3 = UBIXFS_MAGIC_3);
  assert(strcomp(superBlock^.name, 'UbixFS') = 0);
  assert((1 shl superBlock^.blockShift) = superBlock^.blockSize);
  assert((1 shl superBlock^.AGShift) = superBlock^.blocksPerAG);
  assert(superBlock^.flags = UBIXFS_CLEAN);
  
  // create the file decriptor tree


  // The blockCache tree has a [key::value] pair of [block address::pointer to cacheNode].
  // The blockTime tree has a [key::value] pair of [last used time::pointer to cacheNode].

  // create the block cache tree
  new(blockCache, init('', 256, BT_INT64, NIL));
  // create the block last used time tree
  new(blockTime, init('', 256, BT_INT64, NIL));

  // Create the file descriptors and related stuff

  // vnids is a tree which has a [key::value] pair of [iAddr::pointer to vNode].
  new(vnids, init('', 256, BT_INT64, NIL));

  // vnodeQueue is a queue which has a list of available FD handles
  new(vNodeQueue, init(maxFDEntry, sizeof(uInt32)));
  // fill the vNodeQueue with a list of all available FD handles
  for index := 0 to maxFDEntry-1 do
    vNodeQueue^.enqueue(index);

  // Allocate and clear the file descriptor array
  GetMem(fileDescriptors, maxFDEntry*sizeof(TVNode));
  fillchar(fileDescriptors^, maxFDEntry*sizeof(TVNode), 0);
  
  // The last used time is obtained through the RDTSC instruction to give us a unique
  // and increasing timestamp.  When the cache is full we drop off the least recently 
  // used block.  Because the tree is sorted, this will be the first entry. 

  result := 0;  // success
end; // TUbixFS.vfs_init

function TUbixFS.vfs_mkdir;
const 
  dot:array[0..1] of char = '.'#0;
  dotdot:array[0..2] of char = '..'#0;
var 
  inode:PUbixFSInode;
  iAddr:inodeAddr;
  bRun:TBlockRun;
  len:integer;
  dirTree:PbTree;
  u:uPtr;
begin
  // XXX ToDo:
  // Ability to add sub directories

  result := -1;
  assert(path <> NIL);
  assert(superBlock <> NIL);
  if (path = NIL) or (superBlock = NIL) then exit;

  len := strlen(path);
  assert(len <> 0);
  if (len = 0) then exit;

  if (len = 1) and (uInt8ArrayPtr(path)^[0] = ord('/')) then
    begin
      writeln('Creating root directory');
      // wanting to create the root. Check to see if the root dir inode is empty
      assert(not isValidIAddr(superBlock^.rootDir));
      if isValidIAddr(superBlock^.rootDir) then exit;  // should set a proper error code here
      setIAddr(iAddr, 0, 0, 0);   // technically the "parent" iaddr
      inode := allocInode(iAddr); // allocate an inode (parent points to la-la land)
      assert(inode <> NIL);       // make sure it isn't nil
      if (inode = NIL) then exit;
      inode^.mode := S_IFDIR;
      superBlock^.rootDir := inode^.inodeNum;  // assign the root dir
      saveBlock(superBlock^.rootDir, inode);           // save the inode
      u.iAddr := inode^.inodeNum; // copy the inode addr to a uPtr

      // open the root dir
      with superBlock^.rootDir do
        writeln('Root dir inode: ', AG, ':', start, ':', len);
      writeln('Opening root dir tree');
      new(dirTree, init(iAddrToStr(superBlock^.rootDir), 1024, BT_PCHAR, new(PUbixBTreeVFS, init(@self))));
      writeln('Inserting ''.''');
      // Get ready to insert the '.' :: inode^.inodeNum key :: value pair
      writeln('dirTree^.Insert(@dot, u) returned ', dirTree^.Insert(@dot, u));     // '.' points to ourself
      dispose(dirTree, done);       // close and save the directory
//      saveInode(inode);           // save the inode      
      //freemem(inode, inode^.inodeSize);  // free the memory associate with the inode (saved above)
    end
  else
    begin
      // subdir support goes here
    end;
    
  result := 0;
end; // TUbixFS.vfs_mkdir

function TUbixFS.vfs_read;
var 
  inode:PUbixFSInode;
  u:uPtr;
begin
  result := -1;
  if (buffer = NIL) or (fileDescriptors = NIL) or (handle >= maxFDEntry) then exit;
  with fileDescriptors^[handle] do
    begin
      assert(readDataStream(iAddr, position, buffer, size) = 0);
      position := position + int64(size);
      result := size;
    end; // with
end; // TUbixFS.vfs_read

function TUbixFS.vfs_seek;
begin
  result := -1;
  if (fileDescriptors = NIL) or (handle >= maxFDEntry) then exit;
  fileDescriptors^[handle].position := newPosition;
  result := 0;
end; // TUbixFS.vfs_seek

function TUbixFS.vfs_write;
begin
  result := -1;
  if (buffer = NIL) or (fileDescriptors = NIL) or (handle >= maxFDEntry) then exit;
  with fileDescriptors^[handle] do
    begin
      assert(writeDataStream(iAddr, position, buffer, size) = 0);
      position := position + int64(size);
      result := size;
    end; // with
end; // TUbixFS.vfs_write


function TUbixFS.writeDataStream;
var 
  tmpBlock:uInt8ArrayPtr;
  blockRun:TBlockRun;
  size, remainder:uInt32;
  blockStart:uInt32;
  blocksToWrite:uInt32;
  bytesToWrite:uInt32;
  inodeChanged:boolean;
  inode:PUbixFSInode;
begin
  // XXX ToDo:
  // Read/Write through cache
  // fix block extension size
  // change whether the inode is saved or not
  result := -1;
  if ((buf = NIL) or (superBlock = NIL)) then exit; 
  inode := loadBlock(iAddr);
  tmpBlock := NIL;

  inodeChanged := FALSE;
  if (position + length > getFileSpace(inode)) then 
    begin
      extendFile(inode, position+length);  
//      inode^.blocks.size := position+length;  // update the blocks
      writeln('inode^.blocks.size (after extendFile) = ', inode^.blocks.size); 
    end
  else
    if (position + length > inode^.blocks.size) then
      begin
      	// The above case checks to see if we're writing out past
      	// the end of the allocated space on the device. If we do, we extend 
      	// out the file. However, if we're just writing past the end of 
      	// the file, we have to update the size.
        
      	inode^.blocks.size := position+length;
        // XXX To Do:
        // Might want to clear out the blocks we're skipping past, otherwise
        // there might be garbage in there
        assert(saveBlock(iAddr, inode));  // save the inode
      end;

  size := length;
  remainder := uInt32(position mod superBlock^.blockSize);

  if (remainder <> 0) then
    begin
      // the position we've been asked to write to doesn't align to
      // a block size. We need to read in the previous block and pad
      // the data
      blockStart := uInt32(position shr superBlock^.blockShift);
      blockRun := findBRun(@inode^.blocks, blockStart);
      assert(blockRun.len > 0);
      tmpBlock := loadBlock(blockRun);
      assert(tmpBlock <> NIL);
    
      if (tmpBlock = NIL) then exit;
      // read in the block
      readDataStream(iAddr, blockStart, tmpBlock, superBlock^.blockSize);
      bytesToWrite := min(size, (superBlock^.blockSize - remainder));
      move(buf^, tmpBlock^[remainder], bytesToWrite);
      saveBlock(blockRun, tmpBlock);
      inc(buf, bytesToWrite);
      dec(size, bytesToWrite);
      position := position + bytesToWrite;
    end; // if remainder != 0

  remainder := size mod superBlock^.blockSize;
  dec(size, remainder);

  // Size is currently in bytes. The remainder variable above holds the number
  // of remaining bytes (if any) outside full blocks we have to write.

  // convert the size into blocks
  size := size shr superBlock^.blockShift;

  while (size > 0) do
    begin
      blockRun := findBRun(@inode^.blocks, uInt32(position shr superBlock^.blockShift));
      assert(blockRun.len > 0);
      blocksToWrite := min(blockRun.len, size);
      for blocksToWrite := 0 to min(blockRun.len, size)-1 do
        begin
          saveBlock(blockRun, buf);
          inc(blockRun.start);  // This might wrap around?
          dec(blockRun.len);    // might be unnecessary
          dec(size);
          inc(buf, superBlock^.blockSize);
        end;
    end; // while

  if (remainder <> 0) then
    begin
      // There is less than a block of data left to write. Read in the old block
      blockStart := uInt32((position+length-remainder) shr superBlock^.blockShift);
      blockRun := findBRun(@inode^.blocks, blockStart);
      assert(blockRun.len > 0);
      // obtain a pointer to the block
      tmpBlock := loadBlock(blockRun);
      assert(tmpBlock <> NIL);
      if (tmpBlock = NIL) then exit;
      move(buf^, tmpBlock^[0], remainder);
      saveBlock(blockRun, tmpBlock);
    end;
  result := 0;
end; // TUbixFS.writeDataStream

destructor TUbixFS.done;
var 
  searchRec:BTreeSearchRec;
  inode:PUbixfsInode;
  cacheNode:PCacheNode;
  dirtyCount:integer;
  u:uPtr;
begin
  sync();
  // XXX ToDo:
  // close all "open" files manually.
  dispose(vnids, done);
  dispose(vNodeQueue, done);
  FreeMem(fileDescriptors, maxFDEntry*sizeof(TVNode));
  vnids := NIL;
  vNodeQueue := NIL;
  fileDescriptors := NIL;

  if (blockTime <> NIL) then dispose(blockTime, done);
  if (blockCache <> NIL) then 
    begin
      dirtyCount := 0;
      fillchar(u, sizeof(u), 0);
      while (blockCache^.getFirstKey(searchRec)) do
        begin
          cacheNode := searchRec.value.p;
          assert(cacheNode <> NIL);
          if (cacheNode^.dirty) then inc(dirtyCount);
          u.p := cacheNode;
          blockCache^.Delete(@cacheNode^.address, u);
          dispose(cacheNode, done);          
        end;
      if (dirtyCount <> 0) then writeln('Dirty blocks in cache: ', dirtyCount);
      dispose(blockCache, done);
    end;
  if (superBlock <> NIL) then dispose(superBlock);
end; // TUbixFS.done

procedure TUbixFS.examine;
var 
  dirTree:PBTree;
  searchRec:BTreeSearchRec;
  u:uPtr;
begin
  writeln('------------- examining FS -----------------');
  write('Opening root dir...');
  new(dirTree, open(IAddrToStr(superblock^.rootDir), new(PUbixBTreeVFS, init(@self))));
  //dirTree^.PrintList('rootDir.txt');
  writeln('done');
  writeln('Root directory has ', dirTree^.getKeyCount(), ' entrie(s)');
  dirTree^.getFirstKey(searchRec);
  repeat
    with searchRec.value.iAddr do
      write('value: ', AG, ':', start, ':', len);
    writeln('    key: ', pchar(searchRec.key));
  until not dirTree^.findNext(searchRec);
  dispose(dirTree, done);
  if (blockCache <> NIL) then writeln('Block cache has ', blockCache^.getKeyCount(), ' entries');
  writeln('------------------ done --------------------');
end;

end.