/* #includes */ /*{{{C}}}*//*{{{*/
#include "config.h"

#include <assert.h>
#include <errno.h>
#include <ctype.h>

#include "cpmdir.h"
#include "cpmfs.h"

#ifdef USE_DMALLOC
#include <dmalloc.h>
#endif
/*}}}*/
/* types */ /*{{{*/
#define PHYSICAL_SECTOR_1       1 /* First physical sector */

/* Use the INT13 interface rather than INT25/INT26. This appears to
 * improve performance, but is less well tested. */
#define USE_INT13

/* Windows 95 disk I/O functions - based on Stan Mitchell's DISKDUMP.C */
#define VWIN32_DIOC_DOS_IOCTL   1   // DOS ioctl calls 4400h-4411h
#define VWIN32_DIOC_DOS_INT25   2   // absolute disk read, DOS int 25h
#define VWIN32_DIOC_DOS_INT26   3   // absolute disk write, DOS int 26h
#define VWIN32_DIOC_DOS_INT13   4   // BIOS INT13 functions

typedef struct _DIOC_REGISTERS {
    DWORD reg_EBX;
    DWORD reg_EDX;
    DWORD reg_ECX;
    DWORD reg_EAX;
    DWORD reg_EDI;
    DWORD reg_ESI;
    DWORD reg_Flags;
    }
    DIOC_REGISTERS, *PDIOC_REGISTERS;

#define   LEVEL0_LOCK   0
#define   LEVEL1_LOCK   1
#define   LEVEL2_LOCK   2
#define   LEVEL3_LOCK   3
#define   LEVEL1_LOCK_MAX_PERMISSION      0x0001

#define   DRIVE_IS_REMOTE                 0x1000
#define   DRIVE_IS_SUBST                  0x8000

/*********************************************************
 **** Note: all MS-DOS data structures must be packed ****
 ****       on a one-byte boundary.                   ****
 *********************************************************/
#pragma pack(1)

typedef struct _DISKIO {
    DWORD diStartSector;    // sector number to start at
    WORD  diSectors;        // number of sectors
    DWORD diBuffer;         // address of buffer
    }
    DISKIO, *PDISKIO;

typedef struct MID {
    WORD  midInfoLevel;       // information level, must be 0
    DWORD midSerialNum;       // serial number for the medium
    char  midVolLabel[11];    // volume label for the medium
    char  midFileSysType[8];  // type of file system as 8-byte ASCII
    }
    MID, *PMID;

typedef struct driveparams {    /* Disk geometry */
    BYTE special;
    BYTE devicetype;
    WORD deviceattrs;
    WORD cylinders;
    BYTE mediatype;
    /* BPB starts here */
    WORD bytespersector;
    BYTE sectorspercluster;
    WORD reservedsectors;
    BYTE numberofFATs;
    WORD rootdirsize;
    WORD totalsectors;
    BYTE mediaid;
    WORD sectorsperfat;
    WORD sectorspertrack;
    WORD heads;
    DWORD hiddensectors;
    DWORD bigtotalsectors;
    BYTE  reserved[6];
    /* BPB ends here */
    WORD sectorcount;
    WORD sectortable[80];
    } DRIVEPARAMS, *PDRIVEPARAMS;
/*}}}*/

static char *strwin32error(void) /*{{{*/
{
    static char buffer[1024];

    FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                  NULL,
                  GetLastError(),
                  MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
                  (LPTSTR)buffer,
                  1023, NULL);
    return buffer;
}
/*}}}*/
static BOOL LockVolume( HANDLE hDisk ) /*{{{*/
{
    DWORD ReturnedByteCount;

    return DeviceIoControl( hDisk, FSCTL_LOCK_VOLUME, NULL, 0, NULL,
                0, &ReturnedByteCount, NULL );
}
/*}}}*/
static BOOL UnlockVolume( HANDLE hDisk )  /*{{{*/
{
    DWORD ReturnedByteCount;

    return DeviceIoControl( hDisk, FSCTL_UNLOCK_VOLUME, NULL, 0, NULL,
                0, &ReturnedByteCount, NULL );
}
/*}}}*/
static BOOL DismountVolume( HANDLE hDisk ) /*{{{*/
{
    DWORD ReturnedByteCount;

    return DeviceIoControl( hDisk, FSCTL_DISMOUNT_VOLUME, NULL, 0, NULL,
                0, &ReturnedByteCount, NULL );
}
/*}}}*/
static int GetDriveParams( HANDLE hVWin32Device, int volume, DRIVEPARAMS* pParam ) /*{{{*/
  {
  DIOC_REGISTERS reg;
  BOOL bResult;
  DWORD cb;

  reg.reg_EAX = 0x440d; // IOCTL for block device
  reg.reg_EBX = volume; // one-based drive number
  reg.reg_ECX = 0x0860; // Get Device params
  reg.reg_EDX = (DWORD)pParam;
  reg.reg_Flags = 1; // preset the carry flag

  bResult = DeviceIoControl( hVWin32Device, VWIN32_DIOC_DOS_IOCTL,
              &reg, sizeof( reg ), &reg, sizeof( reg ), &cb, 0 ); 

  if ( !bResult || (reg.reg_Flags & 1) ) 
      return (reg.reg_EAX & 0xffff);

  return 0;
  }
/*}}}*/
static int SetDriveParams( HANDLE hVWin32Device, int volume, DRIVEPARAMS* pParam ) /*{{{*/
  {
  DIOC_REGISTERS reg;
  BOOL bResult;
  DWORD cb;

  reg.reg_EAX = 0x440d; // IOCTL for block device
  reg.reg_EBX = volume; // one-based drive number
  reg.reg_ECX = 0x0840; // Set Device params
  reg.reg_EDX = (DWORD)pParam;
  reg.reg_Flags = 1; // preset the carry flag

  bResult = DeviceIoControl( hVWin32Device, VWIN32_DIOC_DOS_IOCTL,
              &reg, sizeof( reg ), &reg, sizeof( reg ), &cb, 0 ); 

  if ( !bResult || (reg.reg_Flags & 1) ) 
      return (reg.reg_EAX & 0xffff);

  return 0;
  }
/*}}}*/
static int GetMediaID( HANDLE hVWin32Device, int volume, MID* pMid ) /*{{{*/
  {
  DIOC_REGISTERS reg;
  BOOL bResult;
  DWORD cb;

  reg.reg_EAX = 0x440d; // IOCTL for block device
  reg.reg_EBX = volume; // one-based drive number
  reg.reg_ECX = 0x0866; // Get Media ID
  reg.reg_EDX = (DWORD)pMid;
  reg.reg_Flags = 1; // preset the carry flag

  bResult = DeviceIoControl( hVWin32Device, VWIN32_DIOC_DOS_IOCTL,
              &reg, sizeof( reg ), &reg, sizeof( reg ), &cb, 0 );

  if ( !bResult || (reg.reg_Flags & 1) ) 
      return (reg.reg_EAX & 0xffff);

  return 0;
  }
/*}}}*/
static int VolumeCheck(HANDLE hVWin32Device, int volume, WORD* flags ) /*{{{*/
{
  DIOC_REGISTERS reg;
  BOOL bResult;
  DWORD cb;

  reg.reg_EAX = 0x4409; // Is Drive Remote
  reg.reg_EBX = volume; // one-based drive number
  reg.reg_Flags = 1; // preset the carry flag

  bResult = DeviceIoControl( hVWin32Device, VWIN32_DIOC_DOS_IOCTL,
              &reg, sizeof( reg ), &reg, sizeof( reg ), &cb, 0 ); 

  if ( !bResult || (reg.reg_Flags & 1) ) 
      return (reg.reg_EAX & 0xffff);

  *flags = (WORD)(reg.reg_EDX & 0xffff);
  return 0;
}
/*}}}*/
static int LockLogicalVolume(HANDLE hVWin32Device, int volume, int lock_level, int permissions) /*{{{*/
{
  DIOC_REGISTERS reg;
  BOOL bResult;
  DWORD cb;

  reg.reg_EAX = 0x440d; // generic IOCTL
  reg.reg_ECX = 0x084a; // lock logical volume 
  reg.reg_EBX = volume | (lock_level << 8);
  reg.reg_EDX = permissions;
  reg.reg_Flags = 1; // preset the carry flag

  bResult = DeviceIoControl( hVWin32Device, VWIN32_DIOC_DOS_IOCTL,
              &reg, sizeof( reg ), &reg, sizeof( reg ), &cb, 0 ); 

  if ( !bResult || (reg.reg_Flags & 1) ) 
      return (reg.reg_EAX & 0xffff);

  return 0;
}
/*}}}*/
static int UnlockLogicalVolume( HANDLE hVWin32Device, int volume ) /*{{{*/
{
  DIOC_REGISTERS reg;
  BOOL bResult;
  DWORD cb;

  reg.reg_EAX = 0x440d;
  reg.reg_ECX = 0x086a; // lock logical volume 
  reg.reg_EBX = volume;
  reg.reg_Flags = 1; // preset the carry flag

  bResult = DeviceIoControl( hVWin32Device, VWIN32_DIOC_DOS_IOCTL,
              &reg, sizeof( reg ), &reg, sizeof( reg ), &cb, 0 ); 

  if ( !bResult || (reg.reg_Flags & 1) ) return -1;
  return 0;
}
/*}}}*/
static int w32mode(int mode) /*{{{*/
{
    switch(mode)
    {
        case O_RDONLY: return GENERIC_READ;
        case O_WRONLY: return GENERIC_WRITE;
    }
    return GENERIC_READ | GENERIC_WRITE;
}
/*}}}*/

/* Device_open           -- Open an image file                      */ /*{{{*/
const char *Device_open(struct Device *sb, const char *filename, int mode, const char *deviceOpts)
{
    /* Windows 95/NT: floppy drives using handles */ 
    if (strlen(filename) == 2 && filename[1] == ':')    /* Drive name */
    {
        char vname[20];
        DWORD dwVers;

        sb->fd = -1;
        dwVers = GetVersion();

        if (dwVers & 0x80000000L) /* Win32s (3.1) or Win32c (Win95) */
        {
            int lock, driveno, res, permissions;
            unsigned short drive_flags;
            MID media;

            vname[0] = toupper(filename[0]);
            driveno = vname[0] - 'A' + 1;   // 1=A: 2=B:
            sb->drvtype = CPMDRV_WIN95;
            sb->hdisk   = CreateFile( "\\\\.\\vwin32",
                                       0,
                                       0,
                                       NULL,
                                       0,
                                       FILE_FLAG_DELETE_ON_CLOSE,
                                       NULL );
            if (!sb->hdisk)
            {
                return "Failed to open VWIN32 driver.";
            }
            if (VolumeCheck(sb->hdisk, driveno, &drive_flags))
            {
                CloseHandle(sb->hdisk);
                return "Invalid drive";
            } 
            res = GetMediaID( sb->hdisk, driveno, &media );
            if ( res )
            {
                const char *lboo = NULL;

                if ( res == ERROR_INVALID_FUNCTION && 
                            (drive_flags & DRIVE_IS_REMOTE )) 
                     lboo = "Network drive";
                else if (res == ERROR_ACCESS_DENIED) lboo = "Access denied";
                /* nb: It's perfectly legitimate for GetMediaID() to fail; most CP/M */
                /*     CP/M disks won't have a media ID. */ 
           
                if (lboo != NULL)
                {
                   CloseHandle(sb->hdisk);
                   return lboo;
                }
            }
            if (!res && 
                (!memcmp( media.midFileSysType, "CDROM", 5 ) ||
                 !memcmp( media.midFileSysType, "CD001", 5 ) ||
                 !memcmp( media.midFileSysType, "CDAUDIO", 5 )))
            {
                CloseHandle(sb->hdisk);
                return "CD-ROM drive";
            }
            if (w32mode(mode) & GENERIC_WRITE)
            {
                lock = LEVEL0_LOCK; /* Exclusive access */
                permissions = 0;
            }
            else
            {
                lock = LEVEL1_LOCK; /* Allow other processes access */
                permissions = LEVEL1_LOCK_MAX_PERMISSION;
            }
            if (LockLogicalVolume( sb->hdisk, driveno, lock, permissions))
            {
                CloseHandle(sb->hdisk);
                return "Could not acquire a lock on the drive.";
            }
 
            sb->fd = driveno;   /* 1=A: 2=B: etc - we will need this later */
            
        }
        else
        {
            sprintf(vname, "\\\\.\\%s", filename);
            sb->drvtype = CPMDRV_WINNT;
            sb->hdisk   = CreateFile(vname,         /* Name */
                                     w32mode(mode), /* Access mode */
                                     FILE_SHARE_READ|FILE_SHARE_WRITE, /*Sharing*/
                                     NULL,          /* Security attributes */ 
                                     OPEN_EXISTING, /* See MSDN */
                                     0,             /* Flags & attributes */
                                     NULL);         /* Template file */

            if (sb->hdisk != INVALID_HANDLE_VALUE)
            {
                sb->fd = 1;   /* Arbitrary value >0 */
                if (LockVolume(sb->hdisk) == FALSE)    /* Lock drive */
                {
                    char *lboo = strwin32error();
                    CloseHandle(sb->hdisk);
                    sb->fd = -1;
                    return lboo;
                }
            }
            else return strwin32error();
        }
        sb->opened = 1;
        return NULL;
    }

    /* Not a floppy. Treat it as a normal file */

    mode |= O_BINARY;
    sb->fd = open(filename, mode);
    if (sb->fd == -1) return strerror(errno);
    sb->drvtype = CPMDRV_FILE;
    sb->opened  = 1;
    return NULL;
}
/*}}}*/
/* Device_setGeometry    -- Set disk geometry                       */ /*{{{*/
void Device_setGeometry(struct Device *this, int secLength, int sectrk, int tracks)
{
  int n;

  this->secLength=secLength;
  this->sectrk=sectrk;
  this->tracks=tracks;
  if (this->drvtype == CPMDRV_WIN95)
  {
      DRIVEPARAMS drvp;
      memset(&drvp, 0, sizeof(drvp));
      if (GetDriveParams( this->hdisk, this->fd, &drvp )) return;

      drvp.bytespersector  = secLength;
      drvp.sectorspertrack = sectrk;
      drvp.totalsectors    = sectrk * tracks;

/* Guess the cylinder/head configuration from the track count. This will
 * get single-sided 80-track discs wrong, but it's that or double-sided
 * 40-track (or add cylinder/head counts to diskdefs) 
 */
      if (tracks < 44)
      {
        drvp.cylinders       = tracks;
        drvp.heads           = 1;
      }
      else
      {
        drvp.cylinders       = tracks / 2;
        drvp.heads           = 2;
      }

/* Set up "reasonable" values for the other members */

      drvp.sectorspercluster = 1024 / secLength;
      drvp.reservedsectors   = 1;
      drvp.numberofFATs      = 2;
      drvp.sectorcount       = sectrk;
      drvp.rootdirsize       = 64;
      drvp.mediaid           = 0xF0;
      drvp.hiddensectors     = 0;
      drvp.sectorsperfat     = 3;
      for (n = 0; n < sectrk; n++)
      {
          drvp.sectortable[n*2]   = n + PHYSICAL_SECTOR_1;    /* Physical sector numbers */ 
          drvp.sectortable[n*2+1] = secLength;
      }
      drvp.special = 6;
/* We have not set:

    drvp.mediatype   
    drvp.devicetype  
    drvp.deviceattrs  

    which should have been read correctly by GetDriveParams().
  */
      SetDriveParams( this->hdisk, this->fd, &drvp );
  }
}
/*}}}*/
/* Device_close          -- Close an image file                     */ /*{{{*/
const char *Device_close(struct Device *sb)
{
    sb->opened = 0;
    switch(sb->drvtype)
    {
        case CPMDRV_WIN95:
            UnlockLogicalVolume(sb->hdisk, sb->fd );
            if (!CloseHandle( sb->hdisk )) return strwin32error();
            return NULL;

        case CPMDRV_WINNT:
            DismountVolume(sb->hdisk);
            UnlockVolume(sb->hdisk);
            if (!CloseHandle(sb->hdisk)) return strwin32error();
            return NULL; 
    }
    if (close(sb->fd)) return strerror(errno);
    return NULL; 
}
/*}}}*/
/* Device_readSector     -- read a physical sector                  */ /*{{{*/
const char *Device_readSector(const struct Device *drive, int track, int sector, char *buf)
{
  int res;
  off_t offset;

  assert(sector>=0);
  assert(sector<drive->sectrk);
  assert(track>=0);
  assert(track<drive->tracks);

  offset = ((sector+track*drive->sectrk)*drive->secLength);

  if (drive->drvtype == CPMDRV_WINNT)
  {
        LPVOID iobuffer;
        DWORD  bytesread;
    
        if (SetFilePointer(drive->hdisk, offset, NULL, FILE_BEGIN) == INVALID_FILE_SIZE)
        {
            return strwin32error();
        }
        iobuffer = VirtualAlloc(NULL, drive->secLength, MEM_COMMIT, PAGE_READWRITE);
        if (!iobuffer) 
        {
            return strwin32error();
        }
        res = ReadFile(drive->hdisk, iobuffer, drive->secLength, &bytesread, NULL);
        if (!res)
        {
            char *lboo = strwin32error();
            VirtualFree(iobuffer, drive->secLength, MEM_RELEASE);
            return lboo;
        } 

        memcpy(buf, iobuffer, drive->secLength);
        VirtualFree(iobuffer, drive->secLength, MEM_RELEASE);

        if (bytesread < (unsigned)drive->secLength)
        {
            memset(buf + bytesread, 0, drive->secLength - bytesread);
        }
        return NULL;
  }

  if (drive->drvtype == CPMDRV_WIN95)
  {
        DIOC_REGISTERS reg;
        BOOL bResult;
        DWORD cb;

#ifdef USE_INT13
        int cyl, head;

        if (drive->tracks < 44) { cyl = track;    head = 0; }
        else                    { cyl = track/2;  head = track & 1; }

        reg.reg_EAX      = 0x0201;  // Read 1 sector
        reg.reg_EBX      = (DWORD)buf;
        reg.reg_ECX      = (cyl << 8)  | (sector + PHYSICAL_SECTOR_1);
        reg.reg_EDX      = (head << 8) | (drive->fd - 1);
        reg.reg_Flags    = 1;       // preset the carry flag
        bResult          = DeviceIoControl( drive->hdisk, VWIN32_DIOC_DOS_INT13,
              &reg, sizeof( reg ), &reg, sizeof( reg ), &cb, 0 );
#else
        DISKIO di;

        reg.reg_EAX      = drive->fd - 1;  // zero-based volume number
        reg.reg_EBX      = (DWORD)&di;
        reg.reg_ECX      = 0xffff;  // use DISKIO structure
        reg.reg_Flags    = 1;       // preset the carry flag
        di.diStartSector = sector+track*drive->sectrk;
        di.diSectors     = 1;
        di.diBuffer      = (DWORD)buf;
        bResult          = DeviceIoControl( drive->hdisk, VWIN32_DIOC_DOS_INT25,
             &reg, sizeof( reg ), &reg, sizeof( reg ), &cb, 0 );

#endif
        if ( !bResult || (reg.reg_Flags & 1) )
        {
            if (GetLastError()) return strwin32error();
            return "Unknown read error.";
        }
        return 0;
  }

  if (lseek(drive->fd,offset,SEEK_SET)==-1)
  {
    return strerror(errno);
  }
  if ((res=read(drive->fd, buf, drive->secLength)) != drive->secLength)
  {
    if (res==-1)
    {
      return strerror(errno);
    }
    else memset(buf+res,0,drive->secLength-res); /* hit end of disk image */
  }
  return NULL;
}
/*}}}*/
/* Device_writeSector    -- write physical sector                   */ /*{{{*/
const char *Device_writeSector(const struct Device *drive, int track, int sector, const char *buf)
{
  off_t offset;
  int res;

  assert(sector>=0);
  assert(sector<drive->sectrk);
  assert(track>=0);
  assert(track<drive->tracks);

  offset = ((sector+track*drive->sectrk)*drive->secLength);

  if (drive->drvtype == CPMDRV_WINNT)
  {
        LPVOID iobuffer;
        DWORD  byteswritten;

        if (SetFilePointer(drive->hdisk, offset, NULL, FILE_BEGIN) == INVALID_FILE_SIZE)
        {
            return strwin32error();
        }
        iobuffer = VirtualAlloc(NULL, drive->secLength, MEM_COMMIT, PAGE_READWRITE);
        if (!iobuffer)
        {
            return strwin32error();
        }
        memcpy(iobuffer, buf, drive->secLength);
        res = WriteFile(drive->hdisk, iobuffer, drive->secLength, &byteswritten, NULL);
        if (!res || (byteswritten < (unsigned)drive->secLength))
        {
            char *lboo = strwin32error();
            VirtualFree(iobuffer, drive->secLength, MEM_RELEASE);
            return lboo;
        }

        VirtualFree(iobuffer, drive->secLength, MEM_RELEASE);
        return NULL;
  }

  if (drive->drvtype == CPMDRV_WIN95)
  {
        DIOC_REGISTERS reg;
        BOOL bResult;
        DWORD cb;

#ifdef USE_INT13
        int cyl, head;

        if (drive->tracks < 44) { cyl = track;    head = 0; }
        else                    { cyl = track/2;  head = track & 1; }

        reg.reg_EAX      = 0x0301;  // Write 1 sector
        reg.reg_EBX      = (DWORD)buf;
        reg.reg_ECX      = (cyl << 8)  | (sector + PHYSICAL_SECTOR_1);
        reg.reg_EDX      = (head << 8) | (drive->fd - 1);
        reg.reg_Flags    = 1;       // preset the carry flag
        bResult          = DeviceIoControl( drive->hdisk, VWIN32_DIOC_DOS_INT13,
              &reg, sizeof( reg ), &reg, sizeof( reg ), &cb, 0 );
#else
        DISKIO di;

        reg.reg_EAX      = drive->fd - 1;  // zero-based volume number
        reg.reg_EBX      = (DWORD)&di;
        reg.reg_ECX      = 0xffff;  // use DISKIO structure
        reg.reg_Flags    = 1;       // preset the carry flag
        di.diStartSector = sector+track*drive->sectrk;
        di.diSectors     = 1;
        di.diBuffer      = (DWORD)buf;
        bResult          = DeviceIoControl( drive->hdisk, VWIN32_DIOC_DOS_INT26,
              &reg, sizeof( reg ), &reg, sizeof( reg ), &cb, 0 ); 
#endif

        if ( !bResult || (reg.reg_Flags & 1) )
        {
            if (GetLastError()) return strwin32error();
            return "Unknown write error.";
        }
        return NULL;
  }

  if (lseek(drive->fd,offset, SEEK_SET)==-1)
  {
    return strerror(errno);
  }
  if (write(drive->fd, buf, drive->secLength) == drive->secLength) return NULL;
  return strerror(errno);
}
/*}}}*/
