cpmbdos.cc

//*************************************************************************
//  MODULE : CpmBdos - The Basic Disk Operating System object members     *
//  AUTHOR : Ron Chernich                                                 *
//  PURPOSE: This is the actual CP/M-like BDOS (Basic Disk Operating      *
//           System), providing file operations and low level block       *
//           access. Normally, char at a time buffering would be provided *
//           by the compiler - but RCOS builds this into the Exec/PCI.    *
//  HISTORY:                                                              *
//    13-AUG-94 First version                                             *
//*************************************************************************

#include "cpmfs.hh"
            
            
//////////////////////////////////////////////////////////////////////////
//         The BDOS .. Instantiate one of these per disk drive.
//------------------------------------------------------------------------
// Each file consists of 0..31 "extents" where an extent is a physical
// entry in the directory.  Each extent can contain either 8 or 16
// disk allocation blocks, depending on the total blocks on the media.
// This is because 16 bytes of each dir entry are reserved for allocation
// block numbers.  IBM 3740 floppies have 243 blocks total, hence a block
// ID fits in one byte.  Bigger disks need two bytes per block number.
//   (Aside: MS-DOS compromised, using 12 bits for a group number)
// A block is a power of two multiple of 1024 bytes - this is the smallest
// piece of disk that can be allocated.  When (say) 16 blocks have been
// allocated, a new "extent" is opened, duplicating the file name in the
// first free directory slot.  Blocks are allocated on a first free basis
// from the "Allocation Vector" for the disk.
//
// CP/M supported only block at a time transfers (it was up to compilers to
// implement byte at a time access).  Entry  contains the "current"
// 128 byte record of the last allocation block that is in use. If you write
// beyond the last 128 byte record of the last block, a new block is 
// allocated, opening extents as required.  Finding out where you are in
// the current rec of the last block required an EOF mark (usually 0x1a).
// When introduced, MS-DOS kept an actual byte count, making the EOF mark
// redundant.  But, to this day, some old ported proggies still write 0x1a
// EOF marks in text files!
//
// For an IBM 3740 8" diskette with 241 available 1K blocks, each extent
// can reference 16K and the maximum 32 extents for one file references
// more than the disk can hold.  Larger disks required larger block sizes.
//
//////////////////////////////////////////////////////////////////////////
// Class constructor sets params for the unit, allocates space for the
// checksum vector (if we have any checked Dir entries), creates the
// allocation vector (which will be initialised when the disk is "logged")
// If the sector skew factor is not zero, a translate table is built.
//
CpmBdos::CpmBdos (DPB& dpb, DPARAM& mac, UINT16 nChanId) :
  pDpb(&dpb), nFsc(mac.nFsc), nLsc(mac.nLsc), nSkf(mac.nSkf),
  nBls(mac.nBls), nDks(mac.nDks), nDir(mac.nDir), nCks(mac.nCks),
  nOfs(mac.nOfs), nChan(nChanId)
{
  nAlb = (nDks > 255) ? 8 : 16;
  if (0 == nCks)
    pCsv = NULL;
  else {
    pCsv = new BYTE[nCks];
    if (NULL == pCsv)
      return;
    memset(pCsv, 0xff, nCks);
  }
  if (0 == nSkf)
    pXlt = NULL;
  else {
    int nSecs = nLsc + (nFsc ? 0 : 1);
    if (NULL == (pXlt = new BYTE[nSecs]))
      return;
    int nSec = nFsc;
    for (int i = 0; i < nSecs; i++) {
      *(pXlt + i) = nSec;
      nSec += nSkf;
      if (nSec > nLsc) {
        nSec -= nLsc;
        if (nSec == nFsc)
          ++nSec;
      }
    }
  }
  pAlv = new BYTE[(nDks + 7) / 8];
}

////////////////
// shut down
//
CpmBdos::~CpmBdos (void)
{
  if (pAlv)
    DELETE_ARRAY pAlv;
  if (pXlt)
    DELETE_ARRAY pXlt;
  if (pCsv)
    DELETE_ARRAY pCsv;
}

/////////////////////
// The public interface used to simulate interrupt service vectoring.
// The disk controller channel device calls her when a disk operation
// has finished, supplying the packet of info (a XfReq object) that
// encapsulates the transfer. The member who launched the packet knew
// where to go next and placed an appropriate constant in the packet,
// so at the mere expense of a few (!) nested stack frames..
//
void CpmBdos::NxtFunc (XfReq *pX)
{
  switch (pX->ivec) {
    case LOG1   : LogDisk1(pX); break;
    case CLOSE1 : Close1(pX);   break;
    case CLOSE2 : Close2(pX);   break;
    case OPEN1  : Open1(pX);    break;
    case CREAT1 : Creat1(pX);   break;
    case CREAT2 : Creat2(pX);   break;
    case CREAT3 : Creat3(pX);   break;
    case READ0  : Read(pX, 0);  break;
    case READ1  : Read1(pX);    break;
    case WRITE1 : Write1(pX);   break;
    case REMOV1 : Remov1(pX);   break;
    case REMOV2 : Remov2(pX);   break;
    case WRITE2 : break;
 }
}

/////////////////////////////////////////////////////////////////////////
//                      These Members are PRIVATE
//----------------------------------------------------------------------
// The BDOS re-enterent mechanism runs by re-posting messages to itself.
// Since the Kernel::Run message dispatcher destroys messages after
// they have been dispatched, we must copy the message to be re-posted
// otherwise we end up multiply free-ing mamory (which is very bad).
//
void CpmBdos::ReGenerate (XfReq* pOld, UINT16 nTag)
{
  XfReq *pNew = new XfReq;
  *pNew = *pOld;
  message msg(nId, 0, nTag, (void*)pNew);
  pTx->PostMsg(nChan, &msg);
}

//////////////
// Set/reset the passed bit of the disk allocation vector
//
void CpmBdos::AllocBlk (UINT16 nBit, BYTE nState)
{
  UINT16 nByte = nBit / 8;
  BYTE   nMask = 0x80 >> (nBit % 8);

  if (nState)
    *(pAlv + nByte) |= nMask;
  else
    *(pAlv + nByte) &= ~nMask;
}

//////////
// Scan the Disk Alocation vector for the first free block
// number (ie first zero bit, working left to right)
// RETURNS: block number, or zero if disk full!
//
UINT16 CpmBdos::GetFreeBlk (void)
{
  UINT16 nByte = 0;
  BYTE nMask = 0x80;
  
  while (nByte < nBls) {
    for (int i = 0; i < 8; i++)
      if (0x00 == (*(pAlv + nByte) & nMask >> i))
        return (UINT16)((nByte * 8) + i);
    ++nByte;
  }
  return 0x00;
}

///////////
// Set sector for transfer -
// using sector skew translate table if not null
//
UINT16 CpmBdos::SecXlt (UINT16 nLogSec)
{
  return (pXlt) ? *(pXlt + nLogSec - nFsc) : (nLogSec);
}

/////////////
// Calculate track and sector numbers of passed record within block..
//
void CpmBdos::BlkDecode (UINT16 nBlk, UINT16 nRec,
                         UINT16 *pnTrk, UINT16 *pnSec)
{
  if (nBlk < nDks) {
    UINT16 nSecs = (nBlk * nBls / pDpb->nSecSize) + nRec;
    *pnTrk = nOfs + (UINT16)(nSecs / pDpb->nSecs);
    *pnSec = SecXlt(nFsc + (UINT16)(nSecs % pDpb->nSecs));
  }
}

/////////////
// A read/write has failed. Decrement the retry counter and if not zero,
// post to the channel controller to try it again. If the error persists,
// see your doctor (ghod, i hope this never gets called to the end).
//
void CpmBdos::DoRetry (XfReq *pX)
{
  if (--(pX->nRetry)) {
    message msg(nId, 16, 16, (void*)pX);
    pTx->PostMsg(nChan, &msg);
  }
  else {
    if (pX->pNxt) {
      XfReq *pInner = pX->pNxt;
      pInner->bRes = FALSE;
      delete pX;
      NxtFunc(pInner);
      return;
    }
    if (LOG1 == pX->ivec)
      delete pX->pFcb;
    else {
      message msg(nId, FS_Ok);
      pTx->PostMsg(pX->uProc, &msg);
    }
  }
}

/////////////////
// Several (lots) of members need to prepare for a full directory scan,
// so this little 'fella sets up the passed FCB for the common stuff..
//
void CpmBdos::DirScanPrep (FCB *pFcb, UINT16 *pnTrk, UINT16 *pnSec)
{
  pFcb->de.nRes1 = 0;
  pFcb->de.nRes2 = 0;
  pFcb->de.nExtent = 0;
  pFcb->nCurRec = nDir / 4;
  BlkDecode(0, 0, pnTrk, pnSec);
}

///////////////////
// Begin the process of Logging the Disk by creating an XfReq for the
// first record of the directory and queueing it for execution. Function
//  will receive the service "interrupt" and issue more
// requests until logging is complete.  Here I make an improvement on
// CP/M - A blank disk is all 0xE5's - valid Directory entries have
// a first byte of 0..15 and the erase process just rewrote this with
// 0xE5 (note that the name remained in tact - unlike MS-DOS).  We want
// to scan all valid entries to build the allocation vector.  Since
// entries are allocated by using the first available, sequentially,
// if we use 0xF7 to erase a file, the first time we strike an 0xE5
// flag, we know that no more valid files exist after this point -
// resulting in a considerably shortened log process.
// 
void CpmBdos::LogDisk (void)
{
  XfReq *pX;
  
  if (pCsv)
    memset(pCsv, 0xff, nCks);
  memset(pAlv, 0, (nDks + 7) / 8);
  UINT16 nDirBls = ((nDir * 32) + nBls - 1) / nBls;
  UINT16 nBlkVal = 0;
  while (nDirBls--)
    AllocBlk(nBlkVal++);
  pX = new XfReq;
  if (pX) {
    pX->pFnCb = this;
    pX->ivec  = LOG1;
    pX->cmnd  = DD_READ;
    pX->pDma  = (char*)DirBuf;
    pX->pFcb  = new FCB;
    if (NULL == pX->pFcb)
      delete pX;
    else {
      memset((char*)pX->pFcb, 0, sizeof(FCB));
      pX->pFcb->nCurRec = nDir / 4;
      DirScanPrep(pX->pFcb, &pX->nTrk, &pX->nSec);
      message msg(nId, 0, 0, (void*)pX);
      pTx->PostMsg(nChan, &msg);
    }
  }
}

///////////
// This is the directory scan log-in loop. We arrive here each time
// another DIR block has been read to scan for current files, setting
// the Allocation vectors from the individual entry maps and
// calculating the checksums for all (checked) dir entries until
// no more files CAN be found, or all DIR blocks have been scanned.
//
void CpmBdos::LogDisk1 (XfReq *pX)
{
  if (FALSE == pX->bRes)
    DoRetry(pX);
  else {
    int idx;
    for (idx = 0; idx < 4; idx++) {
      DIRENT *pDir = (DIRENT*)(pX->pDma + (idx * 32));
      if (0xE5 == pDir->nUsrNmbr)
        break;
      if (0xF7 != pDir->nUsrNmbr)
        for (int jdx = 0; jdx < 16; jdx++) {
          if (0 == pDir->nAllocBlk[jdx])
            break;
          AllocBlk((UINT16)pDir->nAllocBlk[jdx]);
        }
    }
    if ((idx < 4) || (--(pX->pFcb->nCurRec) <= 0)) {
      delete pX->pFcb;
      pX->pFcb = NULL;
    }
    else {
      ++(pX->pFcb->de.nRes2);
      if (pX->pFcb->de.nRes2 >= (nBls / 128)) {
        ++(pX->pFcb->de.nRes1);
        pX->pFcb->de.nRes2 = 0;
      }
      BlkDecode(pX->pFcb->de.nRes1, pX->pFcb->de.nRes2,
              &(pX->nTrk), &(pX->nSec));
      ReGenerate(pX, 1);
    }
  }
}

//////////////////
// This is the loop point for "Open" operations.  If the read was Ok, we
// scan the Dir record, terminating "not found" if we encounter an 0xE5
// flag.  Valid names are matched against the required name (Wild cards
// ARE permitted) and the actual disk dir entry copied over the user's
// FCB on first match (the extent is also matched).
//
void CpmBdos::Open1 (XfReq *pX)
{
  if (FALSE == pX->bRes)
    DoRetry(pX);
  else {
    for (int idx = 0; idx < 4; idx++) {
      DIRENT *pDir = (DIRENT*)(pX->pDma + (idx * 32));
      if (0xE5 == pDir->nUsrNmbr) {
        Open2(pX, FALSE);
        return;
      }
      if ((0xF7 != pDir->nUsrNmbr) && (pDir->nExtent == pX->pFcb->de.nExtent)) {
        for (int jdx = 0; jdx < FILENAME_LEN; jdx++)
          if ((pX->pFcb->de.fname[jdx] != pDir->fname[jdx]) &&
              (pX->pFcb->de.fname[jdx] != '?'))
            break;
        if (jdx == FILENAME_LEN) {
          pX->pFcb->de.nRes1 = idx * 32;
          pX->pFcb->de.nRes2 = 0;
          pX->pFcb->nCurRec = 0;
          Open2(pX, TRUE);
          return;
        }
      }
    }
    if (--pX->pFcb->nCurRec <= 0)
      Open2(pX, FALSE);
    else {
      ++(pX->pFcb->de.nRes2);
      if (pX->pFcb->de.nRes2 >= (nBls / 128)) {
        ++(pX->pFcb->de.nRes1);
        pX->pFcb->de.nRes2 = 0;
      }
      BlkDecode(pX->pFcb->de.nRes1, pX->pFcb->de.nRes2,
              &(pX->nTrk), &(pX->nSec));
      ReGenerate(pX, 2);
      //message msg(nId, 2, 2, (void*)pX);
      //pTx->PostMsg(nChan, &msg);
    }
  }
}

//////////////
// This is the end of the "Open" process and we've either found the file
// or we've not - so it's time to clean up and reply to the request.
//
// We must check for chained XfReq blocks, because we may have been
// called to open the next extent of a file during a Read or Write, or
// scan for the file before doing a Creat.  In such cases, control gets
// passed via the usual  member of the attached request.  If
// there is no attached request, we message back to the original request
// process ID via Kernel post.
//
void CpmBdos::Open2 (XfReq *pX, BOOL bFound)
{
  if (NULL == pX->pNxt) {
    char *cp1 = (char*)&pX->pFcb->de;
    char *cp2 = (char*)pX->pDma+pX->pFcb->de.nRes1;
    memcpy(cp1, cp2, sizeof(DIRENT));
    pX->pFcb->nCurRec = 0;
    message msg(nId, (bFound ? FS_Ok : FS_NotFound));
    pTx->PostMsg(pX->uProc, &msg);
  }
  else {
    XfReq *pInner = pX->pNxt;
    pInner->bRes = bFound;
    switch (pInner->ivec) {
      case CLOSE1:
      case REMOV1:
        pInner->nTrk = pX->nTrk;
        pInner->nSec = pX->nSec;
        pInner->nSid = pX->nSid;
        pInner->pDma = pX->pDma;
        pInner->pFcb->de.nRes1 = pX->pFcb->de.nRes1;
        break;
    default:
      memcpy((char*)&pInner->pFcb->de, (char*)&pX->pFcb->de, sizeof(DIRENT));
      pInner->pFcb->nCurRec = 0;
    }
    delete pX->pFcb;
    NxtFunc(pInner);
  }
}

///////////////
// Arriving here, we have scanned the directory for the file and the
// result sits in bRes: FALSE means the file does not exists, so we 
// need to scan for the first free directory entry of the disk.
//
void CpmBdos::Creat1 (XfReq *pX)
{
  if (pX->bRes) {
    message msg(nId, FS_Exists);
    pTx->PostMsg(pX->uProc, &msg);
  }
  else {
    DirScanPrep(pX->pFcb, &pX->nTrk, &pX->nSec);
    pX->ivec = CREAT2;
    pX->cmnd = DD_READ;
    pX->pDma = (char*)DirBuf;
    message msg(nId, 3, 3, (void*)pX);
    pTx->PostMsg(nChan, &msg);
  }
}

///////////////
// At this point, the outer packet has the result of a directory
// scan for the file we want to kill off.  If it was found, we can
// simply set its flag char to "erased" and issue the rewrite.
// If not found, it's only an error if we were looking for the
// first extent - not found on subsequent extents is an Ok situation.
//
void CpmBdos::Remov1 (XfReq *pX)
{
  if (FALSE == pX->bRes) {
    message msg(nId, pX->pFcb->de.nExtent ? FS_Ok : FS_NotFound);
    pTx->PostMsg(pX->uProc, &msg);
  }
  else {
    DIRENT *pDir = (DIRENT*)(pX->pDma + pX->pFcb->de.nRes1);
    pDir->nUsrNmbr = 0xF7;
    pX->ivec = REMOV2;
    pX->cmnd = DD_WRITE;
    message msg(nId, 4, 4, (void*)pX);
    pTx->PostMsg(nChan, &msg);
  }
}

/////////////
// If the rewrite worked, we must unallocate (in our in-memory bit map)
// all blocks associated with this file.  If the extent was full, there
// may be another which needs to be crossed off..
//
void CpmBdos::Remov2 (XfReq *pX)
{
  if (FALSE == pX->bRes)
    DoRetry(pX);
  else {
    BOOL bDone;
    if (nAlb > 8) {
      BYTE *pB = pX->pFcb->de.nAllocBlk;
      for (int idx = 0; *pB && (idx < nAlb); pB++, idx++)
        DeAllocBlk((UINT16)*pB);
      bDone = ((idx < nAlb) && (pX->pFcb->de.nRcnt < 7)) ? TRUE : FALSE;
    }
    else {
      UINT16 *pN = (UINT16*)pX->pFcb->de.nAllocBlk;
      for (int idx = 0; *pN && (idx < nAlb); pN++, idx++)
        DeAllocBlk(*pN);
      bDone = ((idx < nAlb) && (pX->pFcb->de.nRcnt < 7)) ? TRUE : FALSE;
    }
    if (bDone) {
      message msg(nId, FS_Ok);
      pTx->PostMsg(pX->uProc, &msg);
    }
    else {
      ++pX->pFcb->de.nExtent;
      Remov(pX);
    }
  }
}


///////////////
// we arrive here when the nested dir scan has located the dir block
// (which was cunningly transferred to the inner XfReq sec/trk vars)
// so from here we issue the re-write..
//
void CpmBdos::Close1 (XfReq *pX)
{
  pX->ivec = CLOSE2;
  pX->cmnd = DD_WRITE;
  char *cp = (char*)pX->pDma;
  memcpy(cp+pX->pFcb->de.nRes1, (char*)&pX->pFcb->de, sizeof(DIRENT));
  message msg(nId, 5, 5, (void*)pX);
  pTx->PostMsg(nChan, &msg);
}

///////////////
// ..and we arrive here after the write attempt has taken place.
//
void CpmBdos::Close2 (XfReq *pX)
{
  if (FALSE == pX->bRes)
    DoRetry(pX);
  else {
    message msg(nId, FS_Ok);
    pTx->PostMsg(pX->uProc, &msg);
  }
}
  
/////////////
// This is the loop point for the scan for a free directory entry as
// flagged by an 0xE5 (never used) or 0xF7 (erased) flag.  If one is
// found, it is allocated as a zero length file.   of the FCB
// is set to the logical entry number for convenience and the block
// containing the entry rewritten.
//
// CAUTION: There should be a lock placed on the block someplace to
// prevent the classic "lost update" scenario being enacted yet again.
// this is left as an exercise for the user (ho, ho, ho).
//
void CpmBdos::Creat2 (XfReq *pX)
{
  if (FALSE == pX->bRes)
    DoRetry(pX);
  else {
    int idx;
    for (idx = 0; idx < 4; idx++) {
      DIRENT *pDir = (DIRENT*)(pX->pDma + (idx * 32));
      if ((0xF7 == pDir->nUsrNmbr) || (0xE5 == pDir->nUsrNmbr)) {
        memset((char*)pDir, 0, sizeof(DIRENT));
        memcpy(pDir->fname, pX->pFcb->de.fname, FILENAME_LEN);
        pX->ivec = CREAT3;
        pX->cmnd = DD_WRITE;
        ReGenerate(pX, 6);
        //message msg(nId, 6, 6, (void*)pX);
        //pTx->PostMsg(nChan, &msg);
        return;
      }
    }
    if (--(pX->pFcb->de.nRcnt) <= 0) {
      message msg(nId, FS_DiskFull);
      pTx->PostMsg(pX->uProc, &msg);
    }
    else {
      ++(pX->pFcb->de.nRes2);
      if (pX->pFcb->de.nRes2 >= (nBls / 128)) {
        ++(pX->pFcb->de.nRes1);
        pX->pFcb->de.nRes2 = 0;
      }
      BlkDecode(pX->pFcb->de.nRes1, pX->pFcb->de.nRes2,
              &(pX->nTrk), &(pX->nSec));
      ReGenerate(pX, 7);
      //message msg(nId, 7, 7, (void*)pX);
      //pTx->PostMsg(nChan, &msg);
    }
  }
}

////////////////
// This is a common usage point for all operations that entail a rewrite
// to the Directory. We check the return code, vectoring to the retry
// code if needs be, otherwise check for attached transfers and act..
//
void CpmBdos::Creat3 (XfReq *pX)
{
  if (FALSE == pX->bRes)
    DoRetry(pX);
  else {
    if (NULL == pX->pNxt) {
      pX->pFcb->nCurRec = 0;
      message msg(nId, FS_Ok);
      pTx->PostMsg(pX->uProc, &msg);
    }
    else {
      XfReq *pInner = pX->pNxt;
      memcpy((char*)&pInner->pFcb->de, (char*)&pX->pFcb->de, sizeof(DIRENT));
      pInner->pFcb->nCurRec = 0;
      delete pX;
      NxtFunc(pInner);
    }
  }
}

//////////////////
// arrive here after an attempted record read - retry on error, or
// perform house keeping of FCB and tidy up.
//
void CpmBdos::Read1 (XfReq *pX)
{
  if (FALSE == pX->bRes)
    DoRetry(pX);
  else {
    ++pX->pFcb->nCurRec;
    message msg(nId, FS_Ok);
    pTx->PostMsg(pX->uProc, &msg);
  }    
}

//////////////////
// Arrive here after an attempted record write - retry on error, or
// perform house keeping of FCB and reply to the caller. Scenarios:
//
// 1.  A previously written record has been re-written:
//     No extra action.
// 2.  The record was after the last used and:
//     - the current block is the cardinal last for the extent, or
//     - is the last allocation block of the extent:
//       Increment  (range is 0..nRpb-1; 0..7 for IBM 3740).
//
// This will set  to nRpb-1 when the last record of the last
// block of the extent is used - indicating to  that a new
// extent must be opened before further writes can take place.
//
// When a new allocation block is added,  is zeroed.
//
void CpmBdos::Write1 (XfReq *pX)
{
  if (FALSE == pX->bRes)
    DoRetry(pX);
  else {
    ++pX->pFcb->nCurRec;
    BYTE nRpb = nBls / 128;
    BYTE nAbs = pX->pFcb->nCurRec / nRpb;
    BYTE nRec = pX->pFcb->nCurRec % nRpb;
    if ((pX->pFcb->de.nRcnt < nRec) && 
       ((nAbs == nAlb-1) || (0 == pX->pFcb->de.nAllocBlk[nAbs+1])))
       pX->pFcb->de.nRcnt = nRec;
    message msg(nId, FS_Ok);
    pTx->PostMsg(pX->uProc, &msg);
  }
}


/////////////////////////////////////////////////////////////////////////
//                      These Members are PROTECTED
//----------------------------------------------------------------------
// Scan the directory for the first file matching the name in the passed
// FCB and extent matching the FCB extent number.  We start by requesting
// the first record of block zero, setting  as the block counter
//  as record of block counter and  as the total records
// for the search. These will be replaced by the Dir entry detail, if found.
// 
void CpmBdos::Open (XfReq *pX)
{
  DirScanPrep(pX->pFcb, &pX->nTrk, &pX->nSec);
  pX->ivec  = OPEN1;
  pX->cmnd  = DD_READ;
  pX->pDma = (char*)DirBuf;
  message msg(nId, 8, 8, (void*)pX);
  pTx->PostMsg(nChan, &msg);
}

/////////////
// This activity must fail if the file exists - so we begin by nesting
// this transfer request inside one that will try to open a file of the
// same name.
//
void CpmBdos::Creat (XfReq *pX)
{
  XfReq *pNew = new XfReq(nId);

  if (pNew && (pNew->pFcb = new FCB)) {
    memcpy((char*)pNew->pFcb, (char*)pX->pFcb, sizeof(FCB));
    pX->ivec = CREAT1;
    pNew->pNxt = pX;
    pNew->ivec = OPEN1;
    pNew->cmnd = DD_READ;
    pNew->pDma = (char*)DirBuf;
    pNew->pFnCb = this;
    DirScanPrep(pNew->pFcb, &pNew->nTrk, &pNew->nSec);
    message msg(nId, 9, 9, (void*)pNew);
    pTx->PostMsg(nChan, &msg);
  }
}

////////////////
// Read a record sequentially (at this time random access is provisioned
// for, but not implemented). There are 3 scenarios:
//
// 1.  The record is within the scope of the current extent - we simply
//     calculate its disk location and ask for a transfer.
// 2.  The record is in the next extent (and there is one).  Open the next
//     extent by upping the FCB extent number and callin Open1, then
//     return here to continue as for (1) above.
// 3.  We are at EOF - either in this extent (easy to test), or we need
//     another extent and there isn't one.
//
void CpmBdos::Read (XfReq *pX, UINT16 nType)
{
  pX->ivec  = READ1;
  pX->cmnd  = DD_READ;
  BYTE nRpb = nBls / 128;
  BYTE nAbs = pX->pFcb->nCurRec / nRpb;
  BYTE nRec = pX->pFcb->nCurRec % nRpb;
  if ((nRec > pX->pFcb->de.nRcnt) &&
      (0 == pX->pFcb->de.nAllocBlk[nAbs+1])) {
    message msg(pX->uProc, FS_EOF);
    pTx->PostMsg(nChan, &msg);
    return;
  }
  if (pX->pFcb->de.nAllocBlk[nAbs]) {
    BlkDecode(pX->pFcb->de.nAllocBlk[nAbs], nRec, &pX->nTrk, &pX->nSec);
    message msg(nId, 10, 10, (void*)pX);
    pTx->PostMsg(nChan, &msg);
  }
  else {
    XfReq *pNew = new XfReq;
    memcpy((char*)pNew->pFcb, (char*)pX->pFcb, sizeof(FCB));
    pX->ivec = READ0;
    pNew->pNxt = pX;
    pNew->ivec = OPEN1;
    pNew->cmnd = DD_READ;
    pNew->pFcb->de.nExtent++;
    pNew->pDma = (char*)DirBuf;
    DirScanPrep(pNew->pFcb, &pNew->nTrk, &pNew->nSec);
    message msg(nId, 11, 11, (void*)pNew);
    pTx->PostMsg(nChan, &msg);
  }
}

////////////////
// Write a (sequential) record (random access is provisioned for, but not
// implemented). This is VERY similar to read, but different enough to
// warrant its own entry point. There are 3 scenarios:
//
// 1.  The record is within the scope of the current extent:
//     Simply calculate its disk location and ask for a transfer.
//     (we can't do any FCB housekeeping until we know the write worked).
//
// 2.  The record is beyond the current allocation block and the next
//     allocation block is zero - get a free block (if possible) and
//     use its first record.
//
// 3.  The record is beyond the last block of this extent. Here there are
//     two sub-scenarios:
//       3a.  There is a next extent - open it and proceed as for (1).
//       3b.  There is no next extent - create one and proceed as for (3).
//     Note that (3b) may fail if there are no directory entries available,
//     even though there may still be disk blocks available.
//
void CpmBdos::Write (XfReq *pX, UINT16 nType)
{
  pX->ivec  = WRITE1;
  pX->cmnd  = DD_WRITE;
  BYTE nRpb = nBls / 128;
  BYTE nAbs = pX->pFcb->nCurRec / nRpb;
  BYTE nRec = pX->pFcb->nCurRec % nRpb;
  //
  // Scenario 1
  //
  if (pX->pFcb->de.nAllocBlk[nAbs]) {
    BlkDecode(pX->pFcb->de.nAllocBlk[nAbs], nRec, &pX->nTrk, &pX->nSec);
    message msg(nId, 12, 12, (void*)pX);
    pTx->PostMsg(nChan, &msg);
    return;
  }
  //
  // Scenario 2
  //
  if (0 == pX->pFcb->de.nAllocBlk[nAbs]) {
    pX->pFcb->de.nAllocBlk[nAbs] = GetFreeBlk();
    if (0 == pX->pFcb->de.nAllocBlk[nAbs]) {
      message msg(pX->uProc, FS_DiskFull);
      pTx->PostMsg(nChan, &msg);
      return;
    }
    else {
      AllocBlk(pX->pFcb->de.nAllocBlk[nAbs]);
      pX->pFcb->de.nRcnt = 0;
      BlkDecode(pX->pFcb->de.nAllocBlk[nAbs], nRec, &pX->nTrk, &pX->nSec);
      message msg(nId, 13, 13, (void*)pX);
      pTx->PostMsg(nChan, &msg);
      return;
    }
  }
  //
  // Scenario 3 (note temporary cop-out)
  // 
  if (TRUE) {
    message msg(pX->uProc, FS_DiskFull);
    pTx->PostMsg(nChan, &msg);
    return;
  }
}

//////////////
// A file Close operation needs to locate the Dir block containing the
// current extent and update the record (if it's chaged)..
//
void CpmBdos::Close (XfReq *pX)
{
  XfReq *pNew = new XfReq;

  if (pNew && (pNew->pFcb = new FCB)) {
    memcpy((char*)pNew->pFcb, (char*)pX->pFcb, sizeof(FCB));
    pX->ivec = CLOSE1;
    pNew->pNxt  = pX;
    pNew->pFnCb = this;
    pNew->ivec  = OPEN1;
    pNew->cmnd  = DD_READ;
    pNew->pDma  = (char*)DirBuf;
    DirScanPrep(pNew->pFcb, &pNew->nTrk, &pNew->nSec);
    message msg(nId, 14, 14, (void*)pNew);
    pTx->PostMsg(nChan, &msg);
  }
}

///////////////
// delete a current file -  first we must be able to open it..
//
void CpmBdos::Remov (XfReq *pX)
{
  XfReq *pNew = new XfReq;

  if (pNew && (pNew->pFcb = new FCB)) {
    memcpy((char*)pNew->pFcb, (char*)pX->pFcb, sizeof(FCB));
    pX->ivec = REMOV1;
    pNew->pNxt  = pX;
    pNew->pFnCb = this;
    pNew->ivec  = OPEN1;
    pNew->cmnd  = DD_READ;
    pNew->pDma  = (char*)DirBuf;
    DirScanPrep(pNew->pFcb, &pNew->nTrk, &pNew->nSec);
    message msg(nId, 15, 15, (void*)pNew);
    pTx->PostMsg(nChan, &msg);
  }
}



/////////////////////////////////// eof ////////////////////////////////////