/*
 * ethflopd is serving files through the ethflop protocol. Runs on Linux.
 *
 * http://ethflop.sourceforge.net
 *
 * ethflopd is distributed under the terms of the MIT License, as listed
 * below.
 *
 * Copyright (C) 2019-2024 Mateusz Viste
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/* struct dirent, used by scandir() */
#if defined(__DOS__)
  #include <dos.h>
  #include <direct.h>
  #include <io.h>              /* open(), read(), write(), close() */
  #define htobe16(x) (((x & 0x00ff) << 8) | ((x & 0xff00) >> 8))
  #define le16toh(x) (x)
  #define htole16(x) (x)
#else
  #include <dirent.h>
  #include <endian.h>        /* htobe16(), be16toh(), etc */
#endif

#include <errno.h>
#include <fcntl.h>           /* flags for open() */
#include <fnmatch.h>
#include <limits.h>          /* PATH_MAX and such */
#include <stdio.h>
#include <string.h>          /* mempcy() */
#include <stdint.h>          /* uint16_t, uint32_t */
#include <stdlib.h>          /* realpath() */
#include <time.h>            /* clock_t */
#include <unistd.h>          /* close(), getopt(), optind */

/* O_BINARY is a historical DOS flag that could be passed to open(), it no
 * longer exists on modern systems so I am just defining it as a no-op so the
 * file-handling code does not require any special cases or ifdefs */
#if !defined(O_BINARY)
#define O_BINARY 0
#endif

#include "core.h"

/* set to 1 to enable DEBUG mode (lots of printf calls) */
#define DEBUG 0

/* set to percentage of simulated packet loss (test purposes only!) */
#define SIMLOSS_INP 0  /* % of INPUT packets likely to get lost */
#define SIMLOSS_OUT 0  /* % of OUTPUT packets likely to get lost */


#if DEBUG > 0
#define DBG printf
#else
#define DBG(...)
#endif


/* list of current clients */
static struct cliententry *glob_clist;


/* creates a file, writes a 16K header to it, followed by 16K of zeroed data.
 * returns 0 on success, errno otherwise
 * this function destroys hdr16k! */
static int img_create(const char *fname, unsigned char *hdr16k, unsigned short sectors) {
  int fd;
  int i;

  fd = open(fname, O_CREAT | O_WRONLY | O_BINARY | O_EXCL, S_IRUSR | S_IWUSR);
  if (fd == -1) return(errno);

  /* apply boot sector and FAT to floppy image followed by 16K of zeroed data
   * to make sure all root entries are zeroed */
  for (i = 0; i < 2; i++) {
    if (i == 1) memset(hdr16k, 0, 16384);
    if (write(fd, hdr16k, 16384) != 16384) goto ERRNO_CLOSE_QUIT;
  }

  /* if the image is a standard floppy size (up to 2.88M) then allocate the
   * entire space now, so the user gets standard images that he may use also
   * with other software. Non-standard sizes are kept thin-provisionned so
   * they are very fast to generate. Otherwise generating a 31M image was
   * sometimes generating timeouts on the DOS version of ethflopd because the
   * server was busy for a few seconds initializing the image. */
  if (sectors <= 5760) { /* 5760 sectors (of 512b) is 2.88M */
    /* NOTE 1: this filesize expansion is NOT zeroing data on some platforms
     * (typically: DOS). It is not a problem but it is the reason why the root
     * entries above had to be explicitely initialized to zero.
     * NOTE 2: lseek() must be used instead of fseek(), since the latter does
     * not allow setting the pointer beyond EOF */
    if (lseek(fd, ((off_t)sectors * 512) - 1, SEEK_SET) == -1l) goto ERRNO_CLOSE_QUIT;

    /* write one byte to make sure it all worked. In theory writing 0 bytes
     * should also work, but such write never returns an error. */
#if defined(__DOS__)
    { /* use _dos_write() because write() is VERY slow (zeroes data, probably) */
      unsigned byteswritten;
      if (_dos_write(fd, "", 1, &byteswritten) != 0) goto ERRNO_CLOSE_QUIT;
      if (byteswritten != 1) {
        errno = ENOSPC;
        goto ERRNO_CLOSE_QUIT;
      }
    }
#else
    if (write(fd, "", 1) != 1) goto ERRNO_CLOSE_QUIT;
#endif
  }

  close(fd);

  return(0);

  ERRNO_CLOSE_QUIT:
  {
    int err = errno;
    close(fd);
    unlink(fname);
    return(err);
  }
}


static unsigned char MYMAC[6];


static const struct {
  unsigned char md;        /* media descriptor */
  unsigned char secttrack; /* sectors per track (max. 62) */
  unsigned char cylinders; /* cylinders (or tracks per side, max. 255) */
  unsigned char heads;     /* heads (sides) */
  unsigned char maxRootEntries; /* max number of FAT12 root entries */
  unsigned char clustersz;      /* cluster size, in sectors (FAT12 supports up to 4084 clusters) */
  unsigned char fatsz;          /* single FAT size, in sectors */
  unsigned short totsectors;    /* total sectors count */
  char *description;            /* human description, used for the help message */
  /*              MD  sect trk hd root clustsz fatsz totsectors */
} FDPARMS[] = {{0xFD,  9,  40, 2, 112,     2,    2,       720, "360K (1983)"},
               {0xF9,  9,  80, 2, 112,     2,    3,      1440, "720K (1986)"},
               {0xF9, 15,  80, 2, 224,     1,    7,      2400, "1.2M (1984, IBM AT)"},
               {0xF0, 18,  80, 2, 224,     1,    9,      2880, "1.44M, the classic 3.5\" floppy since 1987"},
               {0xF0, 21,  80, 2,  16,     4,    3,      3360, "1.68M (DMF), big clusters and tiny root!"},
               {0xF0, 21,  82, 2,  16,     4,    3,      3444, "1.72M (DMF), big clusters and tiny root!"},
               {0xF0, 36,  80, 2, 224,     2,    9,      5760, "2.88M, a 1991 invention that never caught on"},
               {0xF0, 60,  80, 2, 224,     4,    8,      9600, "4.8M, custom format"},
               {0xF0, 60, 135, 2, 224,     4,   12,     16200, "8.1M, custom format"},
               {0xF0, 60, 160, 2, 224,     8,    8,     19200, "9.6M, custom format"},
               {0xF0, 62, 250, 2, 224,     8,   12,     31000, "15.5M, custom format"},
               {0xF0, 62, 250, 4, 224,    16,   12,     62000, "31M, custom format"},
               {0x00,  0,   0, 0,   0,     0,    0,         0, ""}}; /* end   */
/* some calculations
 * totsectors = sect * trk * hd
 * numOfClusters = (totsectors - 1 - (2*fatsz)) / clustsz
 * fatsz = (numOfClusters * 1.5) / 512 */


/* generates a formatted MAC address printout and returns a static buffer */
static char *printmac(const unsigned char *b) {
  static char macbuf[18];
  sprintf(macbuf, "%02X:%02X:%02X:%02X:%02X:%02X", b[0], b[1], b[2], b[3], b[4], b[5]);
  return(macbuf);
}


/* returns the FDPARMS index matching a size of sz KiB, or -1 if not found */
static int getFDPARMSidbysize(unsigned short sz) {
  int i;
  for (i = 0;; i++) {
    if (FDPARMS[i].md == 0) return(-1); /* reached end of list */
    if (FDPARMS[i].totsectors == sz * 2) return(i);
  }
}


/* creates a new 16K header for floppy image in memory, by generating a
 * boot sector and FAT-12 structure. sz is the expected size, in KiB
 * returns zero on error, disk size (in sectors) otherwise */
static unsigned short floppygen(unsigned char *dst, unsigned short sz) {
  int type;

  /* validate dst */
  if (dst == NULL) return(0);

  /* translate sz to type id */
  type = getFDPARMSidbysize(sz);
  if (type < 0) return(0); /* invalid type */

  /* zero out the header */
  memset(dst, 0, 16384);

  /* BOOT SECTOR */
  dst[0] = 0xEB;   /* jmp */
  dst[1] = 0xFE;   /* jmp */
  dst[2] = 0x90;   /* jmp */
  memcpy(dst + 3, "MSDOS5.0", 8);  /* OEM sig */
  dst[0x0B] = 0;   /* bytes per sect LSB */
  dst[0x0C] = 2;   /* bytes per sect MSB */
  dst[0x0D] = FDPARMS[type].clustersz;   /* cluster size, in number of sectors */
  dst[0x0E] = 1;   /* reserved logical sectors (number of sectors before 1st FAT) */
  dst[0x0F] = 0;   /* reserved logical sectors */
  dst[0x10] = 2;   /* num of FATs */
  dst[0x11] = FDPARMS[type].maxRootEntries; /* max num of root dirs, must be multiple of 16 (LSB) */
  dst[0x12] = 0;   /* max num of root dirs, must be multiple of 16 (MSB) */
  dst[0x13] = (uint8_t)(FDPARMS[type].totsectors);        /* total sectors, LSB */
  dst[0x14] = (uint8_t)(FDPARMS[type].totsectors >> 8);   /* tot sectors, MSB */
  dst[0x15] = FDPARMS[type].md; /* media descriptor */
  dst[0x16] = FDPARMS[type].fatsz; /* sectors per FAT, LSB */
  dst[0x17] = 0; /* sectors per FAT, MSB */
  dst[0x18] = FDPARMS[type].secttrack;  /* sectors per track (LSB) */
  dst[0x19] = 0;                       /* sectors per track (MSB) */
  dst[0x1A] = FDPARMS[type].heads;  /* heads count (LSB) */
  dst[0x1B] = 0;                   /* heads count (MSB) */
  /* 1C 1D 1E 1F   - hidden sectors */
  /* 20 21 22 23   - total sectors (extended) */
  /* 24 25         - reserved bytes */
  dst[0x26] = 0x29;              /* 0x29 means that volid, vol label and fsname are present (below) */
  dst[0x27] = (uint8_t)(time(NULL));        /* volume id */
  dst[0x28] = (uint8_t)(time(NULL) >> 8);   /* volume id */
  dst[0x29] = (uint8_t)(time(NULL) >> 16);  /* volume id */
  dst[0x2A] = (uint8_t)(time(NULL) >> 24);  /* volume id */
  memcpy(dst + 0x2B, "NO NAME    ", 11);   /* volume label (2B 2C 2D 2E) */
  memcpy(dst + 0x36, "FAT12   ", 8); /* filesystem name */
  /* 448 bytes of boot code */
  dst[0x1FE] = 0x55;    /* boot sig */
  dst[0x1FF] = 0xAA;    /* boot sig */
  /* empty FAT tables: they simply start by a 3-bytes signature. a FAT sig is
   * two (12 bit) cluster entries: first byte of first entry is a copy of the
   * media descriptor, all other bits are set to 1. */
  dst[0x200] = FDPARMS[type].md; /* 1st FAT starts right after bootsector */
  dst[0x201] = 0xff;
  dst[0x202] = 0xff;
  /* 2nd FAT is at 512 + (sectorsperfat * 512), ie. it follows the 1st FAT table */
  memcpy(dst + 0x200 + (FDPARMS[type].fatsz * 512), dst + 0x200, 3);
  /* */
  return(FDPARMS[type].totsectors);
}


static struct cliententry *findorcreateclient(const uint8_t *mac, char *log) {
  struct cliententry *e;

  for (e = glob_clist; e != NULL; e = e->next) {
    if (memcmp(mac, e->mac, 6) == 0) return(e);
  }

  /* nothing found */
  e = calloc(1, sizeof(struct cliententry));
  if (e == NULL) {
    strcpy(log, "failed to init a new client entry (out of memory)");
    return(NULL); /* out of memory */
  }
  memcpy(e->mac, mac, 6);
  e->fd = -1;
  e->next = glob_clist;
  glob_clist = e;
  return(e);
}


/* turns a character c into its up-case variant */
static char upchar(char c) {
  if ((c >= 'a') && (c <= 'z')) c -= ('a' - 'A');
  return(c);
}


/* turns a character c into its lo-case variant */
static char lochar(char c) {
  if ((c >= 'A') && (c <= 'Z')) c += ('a' - 'A');
  return(c);
}


/* turns a string into all-upper-case characters, up to n chars max */
static void upstring(char *s, unsigned short n) {
  while ((n-- != 0) && (*s != 0)) {
    *s = upchar(*s);
    s++;
  }
}


/* turns a string into all-lower-case characters, up to n chars max */
static void lostring(char *s, unsigned short n) {
  while ((n-- != 0) && (*s != 0)) {
    *s = lochar(*s);
    s++;
  }
}


/* converts a CHS tuple into an LBA sector id */
static unsigned short chs2lba(unsigned short c, unsigned short h, unsigned short s, unsigned short totheads, unsigned short sectspertrack) {
  unsigned short sectid;
  sectid = (c * totheads + h) * sectspertrack + (s - 1);
  return(sectid);
}


/* generates a human message about server's status */
static void fill_status_msg(void *dst, const struct cliententry *ce) {
  char *ptr = (char *)dst;

  ptr += sprintf(ptr, "ethflop server: %s\r\n", printmac(MYMAC));

  if (ce->driveid > 1) { /* ethflop 0.6 and 0.6.1 did not provide the driveid */
    ptr += sprintf(ptr, "virt. floppy: ");
  } else {
    ptr += sprintf(ptr, "virt. floppy in drive %c: ", 'A' + ce->driveid);
  }

  if (ce->curflopid[0] != 0) {
    ptr += sprintf(ptr, "%s (%uK", ce->curflopid, ce->sectcount >> 1);
    if (ce->ro != 0) ptr += sprintf(ptr, ", write-protected");
    ptr += sprintf(ptr, ")\r\n");
  } else {
    ptr += sprintf(ptr, "<NONE>\r\n");
  }

  *ptr = '$';
}


static int process_data(struct FRAME *frame, struct cliententry *ce, char *log) {
  unsigned short sid; /* sector id */

  /* switch src and dst addresses so the reply header is ready */
  memcpy(frame->dmac, frame->smac, 6);  /* copy source mac into dst field */
  memcpy(frame->smac, MYMAC, 6); /* copy my mac into source field */

  ce->driveid = frame->driveid; /* refresh the client's driveid */

  /* decode query type */
  switch(frame->ax >> 8) {
    case 0x00: /* DISK RESET - special case: write current FLOPID (8 chars) in
      frame's DATA and a human text message in DATA+100h. This query is used in
      only one case: when ethflop is being loaded and discovers the server */
      memcpy(frame->data, ce->curflopid, 8);
      fill_status_msg(frame->data + 0x100, ce);
      sprintf(log, "client connected: %s", printmac(ce->mac));
      return(0);
    case 0x02: /* READ SECTOR #sectnum FROM CHS CH:DH:CL*/
    case 0x03: /* WRITE SECTOR #sectnum FROM CHS CH:DH:CL */
      sid = chs2lba(frame->cx >> 8, frame->dx >> 8, frame->cx & 0xff, ce->chs_totheads, ce->chs_sectspertrack) + frame->sectnum;
      if (ce->fd == -1) {
        sprintf(log, "read or write attempt at empty drive");
        frame->ax = 0x4000; /* error 'seek failed' + 0 sectors read/written */
        return(0);
      }
      if (sid >= ce->sectcount) {
        sprintf(log, "read or write attempt past last sector (%u > %u)", sid, ce->sectcount - 1);
        frame->ax &= 0x00ff;
        frame->ax |= 0x4000;  /* ah=40h = seek failed */
        return(0);
      }
      /* position the file pointer at the requested place. this may be beyond
       * the file size if the image is thin-provisionned, hence it is important
       * to use lseek() and not fseek() as the latter does not allow to set the
       * file pointer beyond EOF. */
      if (lseek(ce->fd, (off_t)sid * 512, SEEK_SET) == -1) {
        sprintf(log, "lseek() failed: %s", strerror(errno));
        frame->ax &= 0x00ff;
        frame->ax |= 0x4000;  /* ah=40h = seek failed */
        return(0);
      }
      if ((frame->ax >> 8) == 0x03) { /* WRITE OP */
        if (ce->ro != 0) {
          sprintf(log, "attempt to write to a write-protected disk");
          frame->ax &= 0x00ff;
          frame->ax |= 0x0300;  /* ah=3 = disk is write protected */
          return(0);
        }
        if (write(ce->fd, frame->data, 512) != 512) {
          sprintf(log, "fwrite() failure: %s (%s, sect %u)", strerror(errno), ce->curflopid, sid);
          frame->ax &= 0x00ff;
          frame->ax |= 0x0400;  /* ah=4 = sector not found / read error */
          return(0);
        }
        if (sid > ce->lastimgsector) ce->lastimgsector = sid; /* update last actual sector in img if needed ("moving the thin provsionning marker") */
      } else { /* READ OP */
        /* if reading past actually provisionned sectors then return an
         * all-zeroes block */
        if (sid > ce->lastimgsector) {
          memset(frame->data, 0, 512);
        } else {
          if (read(ce->fd, frame->data, 512) != 512) {
            sprintf(log, "read() failure: %s (%s, sect %u)", strerror(errno), ce->curflopid, sid);
            frame->ax &= 0x00ff;
            frame->ax |= 0x0400;  /* ah=4 = sector not found / read error */
            return(0);
          }
        }
      }
      frame->ax = frame->sectnum + 1; /* al = sectors read, ah = 0 (success) */
      return(0);
    case 0x04: /* VERIFY - in fact this was never meant to actually verify data. always succeeds */
      if (ce->fd == -1) {
        sprintf(log, "verify attempt at empty drive");
        frame->ax = 0x4000; /* error 'seek failed' + 0 sectors verified */
        return(0);
      }
      sid = chs2lba(frame->cx >> 8, frame->dx >> 8, frame->cx & 0xff, ce->chs_totheads, ce->chs_sectspertrack);
      /* check how many sectors are valid */
      if (sid + (frame->ax & 0xff) > ce->sectcount) {
        unsigned short validsectors;
        if (sid > ce->sectcount) {
          validsectors = 0;
        } else {
          validsectors = (ce->sectcount - sid) + 1;
        }
        frame->ax = 0x4000 | validsectors; /* error 'seek failed' + validsectors verified */
        return(0);
      }
      frame->ax &= 0x00ff;  /* al = sectors verified, ah = 0 (success) */
      return(0);
    case 0x15: /* GET DISK TYPE */
      /* this routine is buggy - it returns AH=0 on success, while RBIL
       * says that success is indicated by CF=0 but AH should report a 0x02
       * code in AH... I can't do this sadly because ethflop assumes ah=0 for
       * success, and would set CF for any other value */
      if (ce->fd == -1) {
        frame->ax &= 0x00ff;
        frame->ax |= 0x4000; /* ah=31h - no media in drive */
        return(0);
      }
      frame->ax &= 0x00ff;
      frame->cx = 0;  /* number of sectors, high word */
      frame->dx = ce->sectcount; /* number of sectors, low word */
      return(0);
    case 0x16: /* DETECT DISK CHANGE */
      frame->ax &= 0x00ff;
      if (ce->diskchangeflag != 0) {
        frame->ax |= 0x0600;   /* AH=6 - change line active */
        ce->diskchangeflag = 0; /* reset the flag so it's reported only once */
      }
      return(0);
    case 0x20: /* GET CURRENT MEDIA FORMAT */
      if (ce->fd == -1) {
        frame->ax &= 0x00ff;
        frame->ax |= 0x3100; /* set AH to error "media not present" */
        return(0);
      }
      if (ce->sectcount == 1440) {
        frame->ax = 0x0003; /* AH=0 (success), AL=media type is 720K */
        return(0);
      }
      if (ce->sectcount == 2880) {
        frame->ax = 0x0004; /* AH=0 (success), AL=media type is 1.44M */
        return(0);
      }
      if (ce->sectcount == 5760) {
        frame->ax = 0x0006; /* AH=0 (success), AL=media type is 2.88M */
        return(0);
      }
      if (ce->sectcount == 720) {
        frame->ax = 0x000C; /* AH=0 (success), AL=media type is 360K */
        return(0);
      }
      if (ce->sectcount == 2400) {
        frame->ax = 0x000D; /* AH=0 (success), AL=media type is 1.2M */
        return(0);
      }
      frame->ax &= 0x00ff;
      frame->ax |= 0x3200;  /* ah=30h = drive does not support media type */
      sprintf(log, "unable to recognize media type (%u sectors)", ce->sectcount);
      return(0);
  }

  /* set ah to error 01 */
  frame->ax &= 0x00ff;
  frame->ax |= 0x0100;
  return(0);
}


/* parse data looking for a cmd, arg and arg2. returns number of arguments, or -1 on error */
static int parseargs(char *cmd, int cmdsz, char *arg, char *arg2, int argsz, const char *data) {
  int i, l, arglen = 0, arglen2 = 0, cmdlen = 0;
  int gotcmd = 0, gotarg = 0;
  l = data[0];
  /* printf("l=%d\n", l); */
  for (i = 1; i <= l; i++) {
/*    printf("i=%d ; data[i]='%c'\n", i, data[i]); */
    if (data[i] == ' ') {
      if (cmdlen > 0) gotcmd = 1;
      if (arglen > 0) gotarg = 1;
      if (arglen2 > 0) break;
      continue; /* skip spaces */
    }
    if (gotcmd == 0) {
      cmd[cmdlen++] = data[i];
    } else if (gotarg == 0) {
      arg[arglen++] = data[i];
    } else {
      arg2[arglen2++] = data[i];
    }
    if (cmdlen > cmdsz) return(-1);
    if (arglen > argsz) return(-1);
    if (arglen2 > argsz) return(-1);
  }
  cmd[cmdlen] = 0;
  arg[arglen] = 0;
  arg2[arglen2] = 0;
  DBG(" cmd='%s' arg='%s' arg2='%s'\n", cmd, arg, arg2);
  if (arglen2 > 0) return(3);
  if (arglen > 0) return(2);
  if (cmdlen > 0) return(1);
  return(0);
}


static int validatediskname(const char *n) {
  int i;
  if ((n == NULL) || (*n == 0)) return(-1);
  for (i = 0; n[i] != 0; i++) {
    if (i == 8) return(-1);
    if ((n[i] >= 'a') && (n[i] <= 'z')) continue;
    if ((n[i] >= 'A') && (n[i] <= 'Z')) continue;
    if ((n[i] >= '0') && (n[i] <= '9')) continue;
    if (n[i] == '_') continue;
    if (n[i] == '-') continue;
    if (n[i] == '&') continue;
    return(-1);
  }
  return(0);
}


static char *disk2fname(const char *dname) {
  static char fname[16];
  snprintf(fname, sizeof(fname), "%s.img", dname);
  lostring(fname, sizeof(fname));
  return(fname);
}


static const struct cliententry *findcliententrybywritelock(const char *dname) {
  const struct cliententry *ce;
  for (ce = glob_clist; ce != NULL; ce = ce->next) {
    if ((ce->ro == 0) && (strcmp(ce->curflopid, dname) == 0)) return(ce);
  }
  return(NULL);
}


static const struct cliententry *findcliententrybylock(const char *dname) {
  const struct cliententry *ce;
  for (ce = glob_clist; ce != NULL; ce = ce->next) {
    if (strcmp(ce->curflopid, dname) == 0) return(ce);
  }
  return(NULL);
}


/* used by imgfilelist to sort the filenames of floppy images */
static int sortfilelist(const void *f1, const void *f2) {
  unsigned char i;
  for (i = 0; i < 8; i++) {
    if (*(unsigned char *)f1 > *(unsigned char *)f2) return(1);
    if (*(unsigned char *)f1 < *(unsigned char *)f2) return(-1);
    f1 = (char *)f1 + 1;
    f2 = (char *)f2 + 1;
  }
  return(0);
}


unsigned short ethflop_imgfilelist(char *s, unsigned short sz) {
  DIR *dir;
  struct dirent *d;
  unsigned short slen;
  unsigned short count = 0;
  char *s_start = s; /* save it for later */

  dir = opendir(".");
  if (dir == NULL) return(0);

  for (;;) {
    d = readdir(dir);
    if (d == NULL) break;
#ifdef __WATCOMC__
    if (d->d_attr & _A_SUBDIR) continue;
#else
    if (d->d_type == DT_DIR) continue;
#endif
    slen = strlen(d->d_name);
    if (slen < 5) continue; /* A.IMG */

    if (strcasecmp(d->d_name + slen - 4, ".img") != 0) continue;
    if (slen > 12) continue; /* an image must be 8+3 compliant */

    slen -= 4;
    d->d_name[slen] = 0; /* truncate extension */

    /* not enough space left: overwrite last string with an error message
     * msg must be 10 chars and has to start with an ascii char > 'Z' so it
     * stays at the end of the list after sorting */
    if (sz < 12) {
      sprintf(s - 10, "[too long]");
      break;
    }

    /* upcase all files */
    upstring(d->d_name, 8);

    /* Each filename is NUL-padded to 10 bytes so I do not have to worry about
     * columns: DOS will wrap it nicely at either 40 or 80 columns. Also,
     * buffer becomes a list of 10-bytes elements, makes sorting easy. */
    memset(s, 0, 10);
    strcpy(s, d->d_name);
    s += 10;
    sz -= 10;
    count++;
  }

  /* sort the list */
  qsort(s_start, count, 10, sortfilelist);

  /* DOS terminator */
  *s = '$';

  closedir(dir);
  return(count);
}


/* returns 0 if file f does not exist, 1 otherwise */
static int fileexists(const char *f) {
  int fd;
  fd = open(f, O_RDONLY);
  if (fd == -1) return(0);
  close(fd);
  return(1);
}


int ethflop_eject(struct cliententry *ce, char *msg) {
  if (ce->fd == -1) {
    sprintf(msg, "ERROR: no virtual floppy loaded$");
    return(-1);
  }
  close(ce->fd);
  ce->fd = -1;
  ce->sectcount = 0;
  ce->ro = 0;
  sprintf(msg, "Disk %s ejected$", ce->curflopid);
  memset(ce->curflopid, 0, sizeof(ce->curflopid));
  return(0);
}


int ethflop_insert(struct cliententry *ce, const char *arg, char *msg, unsigned char roflag) {
  int i;
  const struct cliententry *sce;
  unsigned char buff[22]; /* enough to read the tot sectors in boot sector */

  if (arg[0] == 0) {
    sprintf(msg, "ERROR: you must specify a diskname$");
    return(-1);
  }
  if (validatediskname(arg) != 0) {
    sprintf(msg, "ERROR: specified disk name is invalid (%s)$", arg);
    return(-1);
  }
  if (ce->fd != -1) {
    sprintf(msg, "ERROR: you must first eject your current virtual floppy (%s)$", ce->curflopid);
    return(-1);
  }

  /* if mounting read-only then check that nobody has write access */
  if (roflag != 0) {
    sce = findcliententrybywritelock(arg);
    if (sce != NULL) {
      sprintf(msg, "ERROR: disk '%s' is write-locked by %s'$", arg, printmac(sce->mac));
      return(-1);
    }
  } else { /* if mounting with write access: nobody else is allowed */
    sce = findcliententrybylock(arg);
    if (sce != NULL) {
      sprintf(msg, "ERROR: disk '%s' is being used by %s'$", arg, printmac(sce->mac));
      return(-1);
    }
  }

  /* try loading the disk image */
  if (roflag != 0) {
    ce->ro = 1;
    ce->fd = open(disk2fname(arg), O_RDONLY | O_BINARY);
  } else {
    ce->ro = 0;
    ce->fd = open(disk2fname(arg), O_RDWR | O_BINARY);
  }
  if (ce->fd == -1) {
    sprintf(msg, "ERROR: disk %s not found$", arg);
    return(-1);
  }

  /* good - how many sectors do we have? */
  if (read(ce->fd, buff, sizeof(buff)) != sizeof(buff)) {
    sprintf(msg, "ERROR: disk %s malformed$", arg);
    close(ce->fd);
    ce->fd = -1;
    return(-1);
  }
  ce->sectcount = (unsigned short)((buff[0x14] << 8) | buff[0x13]);

  ce->lastimgsector = (unsigned short)(lseek(ce->fd, 0, SEEK_END) / 512);
  if ((ce->lastimgsector == 0) || ((ce->lastimgsector > ce->sectcount))) {
    sprintf(msg, "ERROR: disk %s malformed (lastimgsector=%u sectcount=%u)$", arg, ce->lastimgsector, ce->sectcount);
    close(ce->fd);
    ce->fd = -1;
    return(-1);
  }
  ce->lastimgsector -= 1;

  /* find out the type of floppy */
  i = getFDPARMSidbysize(ce->sectcount / 2);
  if (i < 0) {
    close(ce->fd);
    ce->fd = -1;
    sprintf(msg, "ERROR: unknown disk format$");
    return(-1);
  }
  ce->chs_sectspertrack = FDPARMS[i].secttrack;
  ce->chs_totheads = FDPARMS[i].heads;
  strncpy(ce->curflopid, arg, sizeof(ce->curflopid));
  if (ce->driveid < 2) { /* newer ethflop provide drive id */
    sprintf(msg, "Disk %s loaded (%d KiB%s) in drive %c:$", ce->curflopid, ce->sectcount / 2, (ce->ro == 0)?"":", write-protected", 'A' + ce->driveid);
  } else {
    sprintf(msg, "Disk %s loaded (%d KiB%s)$", ce->curflopid, ce->sectcount / 2, (ce->ro == 0)?"":", write-protected");
  }
  ce->diskchangeflag = 1;
  return(0);
}


static int process_ctrl(struct FRAME *frame, struct cliententry *ce, char *log) {
  int argc;
  char cmd[8], arg[16], arg2[16];
  const struct cliententry *sce;

  /* switch src and dst addresses so the reply header is ready */
  memcpy(frame->dmac, frame->smac, 6);  /* copy source mac into dst field */
  memcpy(frame->smac, MYMAC, 6); /* copy my mac into source field */

  /* parse command list */
  argc = parseargs(cmd, sizeof(cmd) - 1, arg, arg2, sizeof(arg) - 1, (char *)frame->data);
  lostring(cmd, sizeof(cmd));
  upstring(arg, sizeof(arg));
  upstring(arg2, sizeof(arg2));

  if (argc < 1) {
    sprintf(log, "illegal query from %s", printmac(ce->mac));
    return(-1);
  }

  /* clear out pkt data */
  memset(frame->data, '*', 512);

  /* SHOW STATUS */
  if (strcmp(cmd, "s") == 0) {
    fill_status_msg(frame->data, ce);
    goto DONE;
  }

  /* INSERT (READ+WRITE) */
  if (strcmp(cmd, "i") == 0) {
    ethflop_insert(ce, arg, (char *)(frame->data), 0);
    goto DONE;
  }

 /* INSERT (WRITE-PROTECTED) */
  if (strcmp(cmd, "ip") == 0) {
    ethflop_insert(ce, arg, (char *)(frame->data), 1);
    goto DONE;
  }

  /* EJECT */
  if (strcmp(cmd, "e") == 0) {
    ethflop_eject(ce, (char *)(frame->data));
    goto DONE;
  }

  /* DELETE (REMOVE) */
  if (strcmp(cmd, "d") == 0) {
    if (arg[0] == 0) {
      sprintf((char *)(frame->data), "ERROR: you must specify a diskname$");
      goto DONE;
    }
    if (validatediskname(arg) != 0) {
      sprintf((char *)(frame->data), "ERROR: specified disk name is invalid$");
      goto DONE;
    }
    sce = findcliententrybylock(arg);
    if (sce != NULL) {
      sprintf((char *)(frame->data), "ERROR: disk %s is currently being used by %s$", arg, printmac(sce->mac));
      goto DONE;
    }

    /* try removing it */
    if (unlink(disk2fname(arg)) != 0) {
      sprintf((char *)(frame->data), "ERROR: failed to delete disk %s (%s)$", arg, strerror(errno));
      goto DONE;
    }
    sprintf((char *)(frame->data), "Disk %s has been deleted$", arg);
    goto DONE;
  }

  /* LISTING */
  if (strcmp(cmd, "l") == 0) {
    char *ptr = (char *)(frame->data);
    if (ethflop_imgfilelist(ptr, sizeof(frame->data)) == 0) {
      sprintf(ptr, "no virtual floppy disks available$");
    }
    goto DONE;
  }

  /* NEW IMAGE */
  if (cmd[0] == 'n') {
    static unsigned char imghdr16k[16384];
    unsigned short fsize_sectors;
    clock_t t1;
    int i;

    /* used to measure the time it takes to create the disk */
    t1 = clock();

    /* zero out image headerr to avoid leaving there any kind of garbage data */
    memset(imghdr16k, 0, sizeof(imghdr16k));

    /* compute the header */
    fsize_sectors = floppygen(imghdr16k, atoi(cmd + 1));
    if (fsize_sectors == 0) {
      char *ptr = (char *)(frame->data);
      int sz;
      sz = sprintf(ptr, "Valid floppy sizes are:\r\n");
      for (i = 0; FDPARMS[i].md != 0; i++) {
        if (sz + strlen(FDPARMS[i].description) + 12 > 512) break;
        sz += sprintf(ptr + sz, "%5d: %s\r\n", FDPARMS[i].totsectors / 2, FDPARMS[i].description);
      }
      ptr[sz] = '$'; /* append the DOS string terminator */
      goto DONE;
    }

    if (validatediskname(arg) != 0) {
      sprintf((char *)(frame->data), "ERROR: specified disk name is invalid$");
      goto DONE;
    }

    if (fileexists(disk2fname(arg)) != 0) {
      sprintf((char *)(frame->data), "ERROR: a disk with this name already exists$");
      goto DONE;
    }

    /* try creating the image, set i to errno on error */
    i = img_create(disk2fname(arg), imghdr16k, fsize_sectors);
    if (i != 0) {
      sprintf((char *)(frame->data), "ERROR: disk %s failed to be initialized (%s)$", arg, strerror(i));
      sprintf(log, "ERR: creating disk %s (%u sects) failed (%s)$", arg, fsize_sectors, strerror(i));
    } else {
      sprintf((char *)(frame->data), "Disk %s created (%u KiB)$", arg, fsize_sectors / 2);
      /* measure time (and convert to ms) */
      t1 = (clock() - t1) * 1000 / CLOCKS_PER_SEC;
      sprintf(log, "Disk %s created (%u KiB), generated in %lu ms", arg, fsize_sectors / 2, (unsigned long)t1);
    }

    goto DONE;
  }

  /* RENAME */
  if (cmd[0] == 'r') {
    char buff[20];
    /* validate src and dst disk names */
    if ((validatediskname(arg) != 0) || (validatediskname(arg2) != 0)) {
      sprintf((char *)(frame->data), "ERROR: invalid disk name$");
      goto DONE;
    }
    /* make sure src exists */
    if (fileexists(disk2fname(arg)) == 0) {
      sprintf((char *)(frame->data), "ERROR: %s disk does not exist$", arg);
      goto DONE;
    }
    /* make sure dst does not exists yet */
    if (fileexists(disk2fname(arg2)) != 0) {
      sprintf((char *)(frame->data), "ERROR: %s disk already exists$", arg2);
      goto DONE;
    }
    /* make sure src is not in use */
    sce = findcliententrybylock(arg);
    if (sce != NULL) {
      sprintf((char *)(frame->data), "ERROR: the %s disk is currently used by %s$", arg, printmac(sce->mac));
      goto DONE;
    }
    /* do it - but convert arg to fname first since disk2fname() cannot be
     * called twice as it uses a single static buffer */
    strcpy(buff, disk2fname(arg));
    if (rename(buff, disk2fname(arg2)) != 0) {
      sprintf((char *)(frame->data), "ERROR: %s$", strerror(errno));
      goto DONE;
    }
    sprintf((char *)(frame->data), "Disk %s renamed to %s$", arg, arg2);
    goto DONE;
  }

  sprintf((char *)(frame->data), "invalid command$");

  DONE:

  /* set answer to current flopid */
  memcpy(&(frame->ax), ce->curflopid, 8);

  return(0);
}


/* used for debug output of frames on screen */
#if DEBUG > 0
static void dumpframe(const void *ptr, int len) {
  int i, b;
  int lines;
  const int LINEWIDTH=16;
  const unsigned char *frame = ptr;
  const struct FRAME *fields = ptr;
  char flopid[16];

  /* FIELDS */
  printf(" * ax=0x%04X bx=0x%04X cx=0x%04X dx=0x%04X\n", le16toh(fields->ax), le16toh(fields->bx), le16toh(fields->cx), le16toh(fields->dx));
  memset(flopid, 0, sizeof(flopid));
  memcpy(flopid, fields->flopid, 8);
  printf(" * flopid=%s reqid=%u sectnum=%u csum=0x%04X\n", flopid, fields->reqid, fields->sectnum, le16toh(fields->csum));

  /* HEX DUMP NOW */
  lines = (len + LINEWIDTH - 1) / LINEWIDTH; /* compute the number of lines */
  /* display line by line */
  for (i = 0; i < lines; i++) {
    /* read the line and output hex data */
    for (b = 0; b < LINEWIDTH; b++) {
      int offset = (i * LINEWIDTH) + b;
      if (b == LINEWIDTH / 2) printf(" ");
      if (offset < len) {
        printf(" %02X", frame[offset]);
      } else {
        printf("   ");
      }
    }
    printf(" | "); /* delimiter between hex and ascii */
    /* now output ascii data */
    for (b = 0; b < LINEWIDTH; b++) {
      int offset = (i * LINEWIDTH) + b;
      if (b == LINEWIDTH / 2) printf(" ");
      if (offset >= len) {
        printf(" ");
        continue;
      }
      if ((frame[offset] >= ' ') && (frame[offset] <= '~')) {
        printf("%c", frame[offset]);
      } else {
        printf(".");
      }
    }
    /* newline and loop */
    printf("\n");
  }
}
#endif


/* compute the eflop csum of frame, result is always little-endian */
static unsigned short cksum(const struct FRAME *frame) {
  unsigned short res = 0;
  const uint16_t *ptr = (const void *)&(frame->protover);
  int l = 10 + 256; /* how many words to process */
  while (l--) {
    res = (unsigned short)(res >> 15) | (unsigned short)(res << 1); /* rol 1 */
    res ^= le16toh(*ptr);
    ptr++;
  }
  return(htole16(res));
}


void ethflop_init(const unsigned char *mymac) {
  memcpy(MYMAC, mymac, 6);
  glob_clist = NULL;
}


const struct FRAME *ethflop_process(const void *frameptr, unsigned short framelen, char *log) {
  struct cliententry *ce;
  const struct FRAME *frame = frameptr;
  struct FRAME *ceframe = NULL;
  unsigned short cksum_mine;

  /* validate frame length */
  if (framelen != sizeof(struct FRAME)) return(NULL);

  /* validate this is for me (or broadcast) */
  if ((memcmp(MYMAC, frame->dmac, 6) != 0) && (memcmp("\xff\xff\xff\xff\xff\xff", frame->dmac, 6) != 0)) return(NULL); /* skip anything that is not for me */

  /* is this valid ethertype ?*/
  if ((frame->etype != htobe16(0xEFDD)) && (frame->etype != htobe16(0xEFDC))) {
    sprintf(log, "Error: Received invalid ethertype frame");
    return(NULL);
  }

  /* validate CKSUM */
  cksum_mine = cksum(frame);
  if (cksum_mine != frame->csum) {
    sprintf(log, "CHECKSUM MISMATCH! Computed: 0x%02Xh Received: 0x%02Xh", cksum_mine, frame->csum);
    return(NULL);
  }

  #if DEBUG > 0
  DBG("Received frame from %s\n", printmac(frame->smac));
  dumpframe(frame, sizeof(struct FRAME));
  #endif
  #if SIMLOSS_INP > 0
  /* simulated frame LOSS (input) */
  if ((rand() % 100) < SIMLOSS_INP) {
    sprintf(log, "INPUT LOSS! (reqid %u)", frame->reqid);
    return(NULL);
  }
  #endif

  /* find client entry */
  ce = findorcreateclient(frame->smac, log);
  if (ce == NULL) {
    sprintf(log, "ERROR: OUT OF MEMORY!");
    return(NULL);
  }

  /* is it a retransmission that I processed already? */
  if ((ce->last_frame_recvd_reqid == frame->reqid) && (ce->last_frame_recvd_csum == frame->csum)) {
    sprintf(log, "retransmission, query 0x%02X answered from cache", frame->reqid);
    return(&(ce->last_frame_sent));
  }

  /* reset csum and reqid to avoid false cache hits in case of an error */
  ce->last_frame_recvd_csum = 0;
  ce->last_frame_recvd_reqid = 0;

  /* copy frame to client context */
  ceframe = &(ce->last_frame_sent);
  memcpy(ceframe, frame, sizeof(struct FRAME));

  /* convert ax/bx/cx/dx to host order */
  ceframe->ax = le16toh(ceframe->ax);
  ceframe->bx = le16toh(ceframe->bx);
  ceframe->cx = le16toh(ceframe->cx);
  ceframe->dx = le16toh(ceframe->dx);

  /* process frame */
  if (frame->etype == htobe16(0xEFDD)) {
    if (process_data(ceframe, ce, log) != 0) return(NULL);
  } else if (frame->etype == htobe16(0xEFDC)) {
    if (process_ctrl(ceframe, ce, log) != 0) return(NULL);
  } else {
    sprintf(log, "Error: unsupported ethertype from %s", printmac(frame->smac));
    return(NULL);
  }

  #if SIMLOSS_OUT > 0
  /* simulated frame LOSS (output) */
  if ((rand() % 100) < SIMLOSS_OUT) {
    sprintf(log, "OUTPUT LOSS! (reqid %u)", frame->reqid);
    return(NULL);
  }
  #endif
  DBG("---------------------------------\n");

  /* convert new register values to little-endian */
  ceframe->ax = htole16(ceframe->ax);
  ceframe->bx = htole16(ceframe->bx);
  ceframe->cx = htole16(ceframe->cx);
  ceframe->dx = htole16(ceframe->dx);

  /* fill in checksum into the answer */
  ceframe->csum = cksum(ceframe);

  /* remember what answer has been answered so I can cache it */
  ce->last_frame_recvd_csum = frame->csum;
  ce->last_frame_recvd_reqid = frame->reqid;

  #if DEBUG > 0
    DBG("Sending back an answer of %lu bytes\n", sizeof(struct FRAME));
    dumpframe(ceframe, sizeof(struct FRAME));
  #endif

  return(ceframe);
}


struct cliententry *ethflop_getclients(void) {
  return(glob_clist);
}
