/*--------------------------------------*
 | File: DT.c - Rev. 1.18 920229        |
 +--------------------------------------+
 | DT: disk test, a la Norton Utilities |
 +--------------------------------------+------------------*
 | Author:  Maurizio Loreti, aka MLO or I3NOO.             |
 | Address: University of Padova - Department of Physics   |
 |          Via F. Marzolo, 8 - 35131 PADOVA - Italy       |
 | Phone:   (39)(49) 844-313         FAX: (39)(49) 844-245 |
 | E-Mail:  LORETI at IPDINFN (BITNET); or VAXFPD::LORETI  |
 |         (DECnet) - VAXFPD is node 38.257 i.e. 39169; or |
 |          LORETI@VAXFPD.PD.INFN.IT (INTERNET).           |
 | Home: Via G. Donizetti 6 - 35010 CADONEGHE (PD) - Italy |
 *---------------------------------------------------------*/

/**
 | #include's
**/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <exec/types.h>
#include <exec/memory.h>
#include <libraries/dos.h>
#include <devices/trackdisk.h>
#include <intuition/intuitionbase.h>
#include <proto/dos.h>
#include <proto/exec.h>
#include <proto/intuition.h>
#include "mlo.h"
#include "proto.h"

/**
 | #define's.
 |
 | FILENAME_MAX was supposed to be in <stdio.h> for ANSI
 | C compilers, but is not there in SAS-C 5.10.
 | NUMHEADS is the number of heads in the drive: it could be obtained
 | with a TD_GETGEOMETRY call, but this is v2.0 specific; for AmigaDOS
 | v1.3 there is no command to obtain this parameter but (RKM devices,
 | page 313, under TD_GETNUMTRACKS) "... the standard 3.5" Amiga drive
 | has two heads". So this program will not work for non-standard drives.
 | TD_CYL is the number of bytes per cylinder.
**/

#define FILENAME_MAX  108

#define NUMHEADS      2
#define TD_CYL        (TD_SECTOR * NUMSECS)

#define ON            1
#define OFF           0

#define BRK_DETECTED  0x1
#define WARN_PRINTED  0x10
#define INTERNAL_ERR  (BRK_DETECTED | WARN_PRINTED)

/**
 | A structure definition to store directory entries, when recursively
 | checking all files.
**/

typedef struct sdirEntry {
  struct sdirEntry *next;
  char name[1];
} dirEntry;

/**
 | Global variables
**/

struct IntuitionBase *IntuitionBase = NULL; /* Pointer to Int. library */

/**
 | 'Local' global variables
**/

static struct MsgPort *diskPort = NULL;   /* Various Intuition pointers */
static struct IOExtTD *diskReq = NULL;    /*   for disk input */
static BYTE *diskBuffer = NULL;           /* Disk input buffer */
static struct FileInfoBlock *pFIB = NULL; /* Buffer for directory scan */
static ULONG diskChangeCount;             /* Disk change count */
static int nErFil = 0;                    /* Total number of errors */
static int nDirs = 0;                     /* Nr. of checked directories */
static int nFiles = 0;                    /* Nr. of checked files */
static Boolean fromWorkBench;             /* Scheduled from WB or CLI ? */
static unsigned abortDT = 0;              /* True when CTRL-C hit */
static int maxDrive = NUMUNITS - 1;       /* Highest possible drive */
static int numCyls;                       /* Number of cylinders on drive */

/**
 | Local procedures
**/

static unsigned checkBreak(void);
static   void   checkDir(char *path, const Boolean root);
static   void   checkFile(char *name);
static   void   motor(const ULONG action);
static   void   pcl(int before, int after, char *fmt, ...);
static   void   readCyl(const int cyl, const int hd);
static   void   seekFullRange(const SHORT howmany);
static   void   syntax(void);

void main(
  int argc,
  char **argv
){
  int drive, cyl, head;
  SHORT error;
  char driveName[5];

/**
 | To be called from CLI, with DT DFx[:] ; if called from the
 | Workbench, a prompt for the floppy unit is sent to the console
 | window---created from the Lattice initialisation routine umain(),
 | gently hacked for this program.
 |
 |  Pass 1: a seek over full range;
 |  Pass 2: read all cylinders;
 |  Pass 3: read all files record by record.
 |
 | But first, check the input arguments...
**/

  if (fromWorkBench = !argc) {
    do {
      fprintf(stdout, "\nDrive to test (DF0-DF%d) ? ", maxDrive);
      (void) fgets(driveName, sizeof(driveName), stdin);
    } while (strnicmp(driveName, "df", 2)     ||
             (drive = atoi(driveName+2)) < 0  ||
             drive > maxDrive);
  } else {
    if (argc != 2                     ||
        strnicmp(*++argv, "df", 2)    ||
        (drive = atoi(*argv+2)) < 0   ||
        drive > maxDrive)                   syntax();
  }

/**
 | Open properly the device, check for a disk in drive,
 | obtain disk parameters from various status commands
 | and obtain memory for our internal buffers.
**/

  if (!(diskPort = CreatePort(NULL, 0L))) {
    fprintf(stderr, "Can't create I/O port!\n");
    cleanup(SYS_ABORT_CODE);
  }

  if (!(diskReq = (struct IOExtTD *)
      CreateExtIO(diskPort, sizeof(struct IOExtTD))) ) {
    fprintf(stderr, "Can't obtain I/O request block!\n");
    cleanup(SYS_ABORT_CODE);
  }

  sprintf(driveName, "DF%d:", drive);

  if (error =
      OpenDevice(TD_NAME, drive, (struct IORequest *) diskReq, 0L)) {
        fprintf(stderr,
                "Error 0x%X returned by OpenDevice for drive %s ...\n",
                error, driveName);
    cleanup(SYS_ABORT_CODE);
  }

  if ((diskBuffer = (BYTE *) AllocMem(TD_CYL, MEMF_CHIP)) == NULL   ||
      (pFIB = (struct FileInfoBlock *)
            AllocMem(sizeof(struct FileInfoBlock), MEMF_CLEAR)) == NULL) {
    fprintf(stderr, "Can't allocate internal buffers ...\n");
    cleanup(SYS_ABORT_CODE);
  }

  diskReq->iotd_Req.io_Command = TD_GETNUMTRACKS;
  (void) DoIO((struct IORequest *) diskReq);
  numCyls = diskReq->iotd_Req.io_Actual / NUMHEADS;

  diskReq->iotd_Req.io_Command = TD_CHANGESTATE;
  (void) DoIO((struct IORequest *) diskReq);
  if (diskReq->iotd_Req.io_Actual) {
    fprintf(stdout, "No disk present in drive %d ...\n", drive);
    cleanup(SYS_ABORT_CODE);
  }

  diskReq->iotd_Req.io_Command = TD_CHANGENUM;
  (void) DoIO((struct IORequest *) diskReq);
  fprintf(stdout, "Change number for drive %s is %d;\n",
          driveName, (diskChangeCount = diskReq->iotd_Req.io_Actual));

/**
 | Pass 1
**/

  motor(ON);
  seekFullRange(1);

/**
 | Pass 2
**/

  fprintf(stdout, "Checking all disk tracks:\n");
  for (cyl=0; cyl<numCyls; cyl++) {
    for (head=0; head<NUMHEADS; head++) {
      pcl(0, 0, "  reading cylinder %d, head %d ...", cyl, head);
      if (checkBreak()) {
        motor(OFF);
        cleanup(SYS_NORMAL_CODE);
      }
      readCyl(cyl, head);
      if (error = diskReq->iotd_Req.io_Error) {
        pcl(0, 1, "* Error 0x%X detected for cylinder %d, head %d",
            error, cyl, head);
        nErFil++;
      }
    }
  }
  motor(OFF);

  if (nErFil) {
    pcl(0, 1, "* %d hard errors detected reading drive %s.",
        nErFil, driveName);
    cleanup(SYS_ABORT_CODE);
  } else {
    pcl(0, 1, "  no errors detected reading drive %s.", driveName);
  }

/**
 | Pass 3
**/

  pcl(0, 1, "Checking all files in drive %s", driveName);
  checkDir(driveName, True);
  pcl(0, 2, "%d director%s and %d file%s checked: %d error%s detected.",
      nDirs,  (nDirs  == 1 ? "y" : "ies"),
      nFiles, (nFiles == 1 ? ""  : "s"),
      nErFil, (nErFil == 1 ? ""  : "s"));

  cleanup(SYS_NORMAL_CODE);
}

static unsigned checkBreak(void)
{
  if (!abortDT  && 
    (SetSignal(0L, SIGBREAKF_CTRL_C) & SIGBREAKF_CTRL_C))
      abortDT |= BRK_DETECTED;
  if (abortDT  &&  !(abortDT & WARN_PRINTED)) {
    pcl(1, 1, "*** DT: BREAK ***");
    abortDT |= WARN_PRINTED;
  }
  return abortDT;
}

static void checkDir(
  char *path,
  const Boolean root
){
  BPTR dlock;
  char fileName[FILENAME_MAX];
  char *pc;
  dirEntry *rdE = NULL;
  dirEntry *pdE;

/**
 | This procedure checks (recursively) a directory.
 | "path" contains the full directory name, and "root" is non-zero the
 | first time that checkDir() is called - i.e. for the root directory.
 | checkDir() scans the wanted directory, checking immediately the 'true'
 | files; the subdirectories (if any) are checked recursively at the
 | end, one by one.
 | If an error is detected from checkDir(), the directory/file test is
 | stopped and the global flag "abortDT" is set; this makes possible, in
 | the further steps, to recursively free() all the memory that has been
 | allocated in order to store subdirectory names.
 |
 | First: obtain a lock on the wanted directory, and Examine() the lock;
 | since only one directory is being scanned at a time, we can use a
 | single FileInfoBlock buffer.
**/

  if ((dlock = Lock(path, ACCESS_READ)) == NULL) {
    pcl(0, 1, "* Can't access directory %s !", path);
    abortDT = INTERNAL_ERR;
    nErFil++;
  } else {
    if (!Examine(dlock, pFIB)) {
      pcl(0, 1, "* Error return from Examine(), directory %s", path);
      abortDT = INTERNAL_ERR;
      nErFil++;
    } else {

/**
 | Prepare in "fileName" the full directory name - to which local
 | filenames will be appended.
**/

      if (root) {
        pc = strcpy(fileName, pFIB->fib_FileName);
        pc += strlen(fileName);
        *pc++ = ':';
        *pc = NIHIL;
        pcl(0, 1, "  checking files in root directory %s ...", fileName);
      } else {
        pc = strcpy(fileName, path);
        pc += strlen(fileName);
        pcl(0, 1, "  checking files in directory %s ...", fileName);
        *pc++ = '/';
      }

      nDirs++;

/**
 | Now, loop over all directory entries. As already said, all the
 | 'real' files are immediately checked; the subdirectory names are
 | stored in a linked list to be examined at the end. This list is
 | implemented as a LIFO tree (the simplest type).
**/

      while (ExNext(dlock, pFIB)) {
        (void) strcpy(pc, pFIB->fib_FileName);
        if (pFIB->fib_DirEntryType < 0) {
          checkFile(fileName);
        } else {

/**
 | If a memory allocation error is detected when asking space for
 | our linked list, we exit the "while" loop; setting the "abortDT"
 | flag before exiting, ensures that all the memory we had from
 | these malloc()'s will later be free()-ed recursively.
**/

          if ((pdE = malloc(sizeof(dirEntry) + strlen(fileName))) == NULL) {
            pcl(1, 1, "* Can't allocate heap memory!");
            abortDT = INTERNAL_ERR;
            break;
          }

          (void) strcpy(pdE->name, fileName);
          pdE->next = rdE;
          rdE = pdE;
        }

        if (checkBreak()) break;
      }

/**
 | We should check if ExNext() has failed, or if the last
 | entry has been found.
**/

      if (!abortDT   &&   IoErr() != ERROR_NO_MORE_ENTRIES) {
        pcl(1, 1, "* Error reading directory %s !", path);
        nErFil++;
      }
    }
    UnLock(dlock);
  }

/**
 | Now, loop over all detected subdirectories (if any);
 | freeing in the same time the memory used to store their names.
**/

  while (rdE != NULL) {
    if (!abortDT) checkDir(rdE->name, False);

    pdE = rdE->next;
    free(rdE);
    rdE = pdE;
  }
}

static void checkFile(
  char *name
){
  BPTR pFH;
  long ier;

/**
 | Check a file, opening and reading it record by record.
**/

  nFiles++;

  pcl(0, 0, "    file %s ...", name);
  if ((pFH = Open(name, MODE_OLDFILE)) == NULL) {
    pcl(0, 1, "* Error %d opening file \"%s\".", IoErr(), name);
    nErFil++;
  } else {
    while (!abortDT  &&  (ier = Read(pFH, diskBuffer, TD_CYL)) > 0) {
      (void) checkBreak();
    }
    Close(pFH);

    if (ier < 0) {
      pcl(0, 1, "* Error %d reading file \"%s\".", IoErr(), name);
      nErFil++;
    }
  }
}

void cleanup(
  const int code
){

/**
 | Releases all global resources, then exit to the operating system.
**/

  if (diskBuffer != NULL) FreeMem(diskBuffer, TD_CYL);
  if (pFIB != NULL)       FreeMem(pFIB, sizeof(struct FileInfoBlock));

  if (diskReq != NULL) {
    CloseDevice((struct IORequest *) diskReq);
    DeleteExtIO((struct IORequest *) diskReq);
  }
  if (diskPort != NULL)   DeletePort(diskPort);

  if (fromWorkBench) {
    int i;

    fprintf(stdout, "Strike <CR> to continue ... ");
    while ( (i = getchar()) != '\n'   &&   i != EOF)  { }
  }

  if (IntuitionBase != NULL) CloseLibrary((struct Library *) IntuitionBase);

  exit(code);
}

int CXBRK(void)
{
/**
 | If a CTRL-C is detected from the operating system,
 | we silently defer all handling until checkBreak() is called.
**/

  abortDT |= BRK_DETECTED;
  return 0;
}

static void motor(
  const ULONG action
){
  diskReq->iotd_Req.io_Length = action;
  diskReq->iotd_Req.io_Command = TD_MOTOR;
  (void) DoIO((struct IORequest *) diskReq);
}

static void pcl(
  int before,
  int after,
  char *fmt,
  ...
){
  va_list vl;
  static length = 0;
  int nc;

/**
 | What the hell is the delete-to-end-of-line sequence on the Amiga?
 | The AmigaDOS manual refers to the ANSI sequence <ESC>[1K - that do
 | not work in my NewCon windows; so I wrote this simple interface. When
 | overprinting, we check if the length of the new line is greater than
 | the length of the old one - if not, we output some trailing blanks.
 | "before" and "after" are the number of newlines to be printed before
 | and after this line; if "after" is 0 no newline but a carriage return
 | is output.
**/

  if (before) {
    while (before--) puts("");
    length = 0;
  }

  va_start(vl, fmt);
  nc = vfprintf(stdout, fmt, vl);
  va_end(vl);

  length -= nc;
  if (length > 0) fprintf(stdout, "%*s", length, " ");

  if (after) {
    while (after--) puts("");
    length = 0;
  } else {
    fprintf(stdout, "%c", '\r');
    length = nc;
  }
}

static void readCyl(
  const int cyl,
  const int hd
){
  diskReq->iotd_Req.io_Length = TD_CYL;
  diskReq->iotd_Req.io_Data = (APTR) diskBuffer;
  diskReq->iotd_Req.io_Command = ETD_READ;
  diskReq->iotd_Count = diskChangeCount;
  diskReq->iotd_Req.io_Offset =
           TD_SECTOR * (NUMSECS * (hd + NUMHEADS * cyl));
  (void) DoIO((struct IORequest *) diskReq);
}

static void seekFullRange(
  const SHORT howmany
){
  int i;
  SHORT error;

  for (i=1; i<=howmany; i++) {
    diskReq->iotd_Req.io_Offset =
          ((numCyls - 1) * NUMSECS * NUMHEADS - 1) * TD_SECTOR;
    diskReq->iotd_Req.io_Command = TD_SEEK;
    (void) DoIO((struct IORequest *) diskReq);
    if (error = diskReq -> iotd_Req.io_Error) {
      fprintf(stdout, "* Seek cycle %d, error 0x%X ...\n", i, error);
      cleanup(SYS_ABORT_CODE);
    }

    diskReq->iotd_Req.io_Offset = 0;
    diskReq->iotd_Req.io_Command = TD_SEEK;
    (void) DoIO((struct IORequest *) diskReq);
    if (error = diskReq->iotd_Req.io_Error) {
      fprintf(stdout, "* Seek cycle %d, error 0x%X ...\n", i, error);
      cleanup(SYS_ABORT_CODE);
    }
  }
  fprintf(stdout, "  no errors detected seeking over full disk range.\n");
}

static void syntax(void)
{
  fprintf(stdout,
        "\n\tUsage:\t\tDT DFn, where 'n' (0-%d) is the drive number.\n",
        maxDrive);
  fprintf(stdout,
        "\tPurpose:\tDisk test.\n\n");
  cleanup(SYS_NORMAL_CODE);
}
