////////////////////////////////////////////////////////////////////////////////////
// W32STAPE.C   --   Hercules Win32 SCSI Tape handling module
//
// (c) Copyright "Fish" (David B. Trout), 2005-2009. Released under
// the Q Public License (http://www.hercules-390.org/herclic.html)
// as modifications to Hercules.
////////////////////////////////////////////////////////////////////////////////////
//
//  This module contains only WIN32 support for SCSI tapes.
//  Primary SCSI Tape support is in module 'scsitape.c'...
//
//  PROGRAMMING NOTE: we rely on the known fact that
//  'NO_ERROR' == 0 and 'INVALID_HANDLE_VALUE' == -1.
//
////////////////////////////////////////////////////////////////////////////////////

// $Id: w32stape.c 5365 2009-06-02 03:10:33Z fish $
//
// $Log$
// Revision 1.19  2009/01/23 13:17:44  bernard
// copyright notice
//
// Revision 1.18  2008/03/31 06:36:49  fish
// (untab)
//
// Revision 1.17  2008/03/30 02:51:34  fish
// Fix SCSI tape EOV (end of volume) processing
//
// Revision 1.16  2007/11/30 14:54:33  jmaynard
// Changed conmicro.cx to hercules-390.org or conmicro.com, as needed.
//
// Revision 1.15  2007/07/24 22:36:33  fish
// Fix tape Synchronize CCW (x'43') to do actual commit
//
// Revision 1.14  2007/07/24 21:57:29  fish
// Fix Win32 SCSI tape "Locate" and "ReadBlockId" SNAFU
//
// Revision 1.13  2007/06/23 00:04:19  ivan
// Update copyright notices to include current year (2007)
//
// Revision 1.12  2006/12/08 09:43:33  jj
// Add CVS message log
//

#include "hstdinc.h"

#define _W32STAPE_C_
#define _HTAPE_DLL_

#include "hercules.h"
#include "w32stape.h"
#include "tapedev.h"        // (need IS_TAPE_BLKID_BOT)

#ifdef _MSVC_

////////////////////////////////////////////////////////////////////////////////////
// Global data...

#define  W32STAPE_MAX_FDNUMS  (32)      // (admitedly low, but easily increased)

typedef int  ifd_t;     // (internal fd)
typedef int  ufd_t;     // (user fd)

#define  W32STAPE_IFD2UFD( ifd )         ((ufd_t)( (ifd) |  0x7F000000 ))
#define  W32STAPE_UFD2IFD( ufd )         ((ifd_t)( (ufd) & ~0x7F000000 ))

static  BYTE    g_ifds    [ W32STAPE_MAX_FDNUMS ]  = {0};   // (0 == avail, 0xFF == used)
static  HANDLE  g_handles [ W32STAPE_MAX_FDNUMS ]  = {0};   // (WIN32 handles)
static  char*   g_fnames  [ W32STAPE_MAX_FDNUMS ]  = {0};   // (for posterity)
static  U32     g_fstats  [ W32STAPE_MAX_FDNUMS ]  = {0};   // (running status)
static  U32     g_BOTmsk  [ W32STAPE_MAX_FDNUMS ]  = {0};   // (BOT block-id mask)
static  U32     g_BOTbot  [ W32STAPE_MAX_FDNUMS ]  = {0};   // (BOT block-id value)

static  TAPE_GET_DRIVE_PARAMETERS g_drive_parms [ W32STAPE_MAX_FDNUMS ] = {0};  // (drive parameters)

static  LOCK    g_lock; // (master global access lock)

#define  lock()     obtain_w32stape_lock()
#define  unlock()   release_lock( &g_lock )

static void obtain_w32stape_lock()
{
    static int bDidInit  = 0;
    static int bInitBusy = 1;
    if (!bDidInit)
    {
        bDidInit = 1;
        initialize_lock ( &g_lock );
        memset( g_ifds,    0x00, sizeof ( g_ifds    ) );
        memset( g_handles, 0x00, sizeof ( g_handles ) );
        memset( g_fnames,  0x00, sizeof ( g_fnames  ) );
        memset( g_fstats,  0x00, sizeof ( g_fstats  ) );
        memset( g_BOTmsk,  0xFF, sizeof ( g_BOTmsk  ) );
        memset( g_BOTbot,  0x00, sizeof ( g_BOTbot  ) );
        bInitBusy = 0;
    }
    while (bInitBusy) Sleep(10);
    obtain_lock ( &g_lock );
}

////////////////////////////////////////////////////////////////////////////////////
// Allocate an internal fd number...

static
ifd_t  w32_alloc_ifd()
{
    ifd_t ifd = -1;
    errno = EMFILE;

    lock();
    {
        BYTE* pifd_slot = memchr( g_ifds, 0, W32STAPE_MAX_FDNUMS );

        if (pifd_slot)
        {
            int n = (int) (pifd_slot - g_ifds);

            if ( n >= 0  &&  n < W32STAPE_MAX_FDNUMS )
            {
                *pifd_slot = 1;
                errno = 0;
                ifd = n;
            }
        }
    }
    unlock();

    return ifd;
}

////////////////////////////////////////////////////////////////////////////////////
// Release an internal fd number...

static
int w32_free_ifd( ifd_t  ifd )
{
    int rc = 0;
    errno = 0;

    lock();
    {
        if ( ifd >= 0  &&  ifd < W32STAPE_MAX_FDNUMS )
            g_ifds [ ifd ] = 0;
        else
        {
            rc = -1;
            errno = EBADF;
        }
    }
    unlock();

    return rc;
}

////////////////////////////////////////////////////////////////////////////////////
// Retrieve the status of the tape drive...

static
DWORD  w32_get_tape_status ( HANDLE hFile )
{
    // ***************************************************************
    //  PROGRAMMING NOTE: it is THIS LOOP (retrieving the status
    //  of the tape drive) that takes UP TO *10* SECONDS TO COMPLETE
    //  if there is no tape mounted on the drive whereas it completes
    //  immediately when there IS a tape mounted!  I have no idea why
    //  Windows behave so unusually/inefficiently in this way! - Fish
    // ***************************************************************

    DWORD dwTapeStatus;

    // (NOTE: see also: KB 111837: "ERROR_BUS_RESET May Be Benign")

    do dwTapeStatus = GetTapeStatus( hFile );
    while (ERROR_BUS_RESET == dwTapeStatus);

    return dwTapeStatus;
}

////////////////////////////////////////////////////////////////////////////////////
// Open tape device...

DLL_EXPORT
ufd_t w32_open_tape ( const char* path, int oflag, ... )
{
    ifd_t       ifd;
    HANDLE      hFile;
    char        szTapeDeviceName[10];
    const char* pszTapeDevNum;
    DWORD       dwDesiredAccess, dwSizeofDriveParms, dwRetCode;

    // Reserve an fd number right away and bail if none available...
    if ( (ifd = w32_alloc_ifd()) < 0 )
        return -1;

    // If they specified a Windows device name,
    // use it as-is.

    if (1
        && strnfilenamecmp( path, "\\\\.\\", 4 ) == 0
        &&                  path            [4]  != 0
    )
    {
        strlcpy( szTapeDeviceName, path, sizeof(szTapeDeviceName) );
    }
    else // (not a Windows device name)
    {
        // The device name is a Cygwin/*nix device name.
        // Name must be either "/dev/nst0" or "/dev/st0"

        if (1
            &&  strnfilenamecmp( path, "/dev/", 5 ) == 0
            &&  (
                    strnfilenamecmp( (pszTapeDevNum=path+8)-3, "nst", 3 ) == 0
                    ||
                    strnfilenamecmp( (pszTapeDevNum=path+7)-2, "st",  2 ) == 0
                )
            &&  strlen(pszTapeDevNum) == 1
            &&  isdigit(*pszTapeDevNum)
        )
        {
            // Change it to a Windows device name (e.g. \\.\Tape0)

            strlcpy( szTapeDeviceName, WIN32_TAPE_DEVICE_NAME, sizeof(szTapeDeviceName) );
            szTapeDeviceName[8] = *pszTapeDevNum;
            szTapeDeviceName[9] = 0;

            // PROGRAMMING NOTE: the "rewind at close" option (implied by
            // virtue of the filename being "/dev/st0" and not "/dev/nst0")
            // was handled (detected/remembered) by the higher-level caller.
        }
        else
        {
            VERIFY( w32_free_ifd( ifd ) == 0 );
            errno = EINVAL;     // (bad device name)
            return -1;          // (open failure)
        }
    }

    // We only support O_BINARY with either O_RDWR or O_RDONLY

    if (1
        && (( O_BINARY | O_RDWR  ) != oflag)
        && (( O_BINARY | O_RDONLY) != oflag)
    )
    {
        VERIFY( w32_free_ifd( ifd ) == 0 );
        errno = EINVAL;     // (invalid open flags)
        return -1;          // (open failure)
    }

    // Set desired access

    dwDesiredAccess = GENERIC_READ;

    if ( oflag & O_RDWR )
        dwDesiredAccess |= GENERIC_WRITE;

    // Open the tape drive...

    hFile = CreateFile
    (
        szTapeDeviceName,   // filename
        dwDesiredAccess,    // desired access
        0,                  // share mode (0 == exclusive)
        NULL,               // security == default
        OPEN_EXISTING,      // "file" (device actually) must already exist
        0,                  // no special access flags needed
        NULL                // not using template
    );

    if ( INVALID_HANDLE_VALUE == hFile )
    {
        int save_errno = w32_trans_w32error( GetLastError() );
        VERIFY( w32_free_ifd( ifd ) == 0 );
        errno = save_errno;
        return -1;
    }

    // Save drive parameters for later...

    memset( &g_drive_parms[ifd], 0, sizeof(TAPE_GET_DRIVE_PARAMETERS) );
    dwSizeofDriveParms = sizeof(TAPE_GET_DRIVE_PARAMETERS);

    do
    {
        dwRetCode = GetTapeParameters
        (
            hFile,
            GET_TAPE_DRIVE_INFORMATION,
            &dwSizeofDriveParms,
            &g_drive_parms[ifd]
        );
    }
    while ((NO_ERROR != dwRetCode)              // (if not normal completion,
    &&                                          // check for retry conditions)
    (0
        || ERROR_MEDIA_CHANGED == dwRetCode     // (likely but unimportant; retry)
        || ERROR_BUS_RESET     == dwRetCode     // (unlikely but possible;  retry)
    ));

    // Did that work?

    if (NO_ERROR != dwRetCode)
    {
        int save_errno = w32_trans_w32error( GetLastError() );
        CloseHandle( hFile );
        VERIFY( w32_free_ifd( ifd ) == 0 );
        errno = save_errno;
        return -1;
    }

    ASSERT( NO_ERROR == dwRetCode );
    ASSERT( sizeof(TAPE_GET_DRIVE_PARAMETERS) == dwSizeofDriveParms );

    // Save control info & return their file descriptor...

    g_handles [ ifd ]  = hFile;                     // (WIN32 handle)
    g_fnames  [ ifd ]  = strdup( path );            // (for posterity)
    g_fstats  [ ifd ]  = GMT_ONLINE (0xFFFFFFFF);   // (initial status)
    g_BOTmsk  [ ifd ]  = 0xFFFFFFFF;                // (BOT block-id mask)
    g_BOTbot  [ ifd ]  = 0x00000000;                // (BOT block-id value)

    return W32STAPE_IFD2UFD( ifd );                 // (user fd result)
}

////////////////////////////////////////////////////////////////////////////////////
// Define physical BOT block-id mask / value...

// PROGRAMMING NOTE: For the time being, we require 'tapedev.c' to provide to us
// the information we need in order to detect physical BOT (load-point). This is
// only until such time as I can add SCSI PassThru support to Hercules so that we
// can talk SCSI directly to the device ourselves (to determine such things as
// what type of device (manufacturer/model) we're dealing with, etc).

DLL_EXPORT
int w32_define_BOT ( ufd_t ufd, U32 msk, U32 bot )
{
    ifd_t  ifd  = W32STAPE_UFD2IFD( ufd );

    lock();

    if (0
        ||         ifd    <   0
        ||         ifd    >=  W32STAPE_MAX_FDNUMS
        || g_ifds[ ifd ]  ==  0
    )
    {
        unlock();
        errno = EBADF;
        return -1;
    }

    g_BOTmsk [ ifd ]  = msk;   // (BOT block-id mask)
    g_BOTbot [ ifd ]  = bot;   // (BOT block-id value)

    unlock();

    return 0;
}

////////////////////////////////////////////////////////////////////////////////////
// Post-process a tape i/o return code...
//
// Examine 'errno' (which should have been manually set to the return
// code from the current i/o) and update the internal status appropriately,
// depending on what type of error it was (tapemark, etc)...
//
//     -------------------------------------------------------------
//     ***  THIS FUNCTION SHOULD BE CALLED AFTER EVERY TAPE I/O  ***
//     -------------------------------------------------------------
//
// An errno of 'EINTR' means the error was spurious (media changed, etc)
// and that the caller should try the same i/o again (retry their i/o).
//
// EXAMPLE:
//
//     do
//     {
//         errno = SetTapePosition( ... );
//         errno = w32_internal_rc ( pStat );
//     }
//     while ( EINTR == errno );
//     return errno ? -1 : 0;
//
////////////////////////////////////////////////////////////////////////////////////

//     ***  THIS FUNCTION SHOULD BE CALLED AFTER EVERY TAPE I/O  ***

static
int w32_internal_rc ( U32* pStat )
{
    ASSERT( pStat );    // (sanity check)

    // PROGRAMMING NOTE: the 'door open' (no tape in drive) and the
    // 'write protected' statuses are "sticky" in that they never change
    // until a new/different tape is mounted. All the other statuses
    // however, change dynamically as one does i/o to the tape...

    if (0
        || ERROR_BUS_RESET            == errno // (See KB 111837: "ERROR_BUS_RESET May Be Benign")
        || ERROR_MEDIA_CHANGED        == errno
        || ERROR_DEVICE_NOT_CONNECTED == errno // (shouldn't occur but we'll check anyway)
        || ERROR_DEV_NOT_EXIST        == errno // (shouldn't occur but we'll check anyway)
        || ERROR_FILE_NOT_FOUND       == errno // (shouldn't occur but we'll check anyway)
    )
    {
        *pStat  &=  ~GMT_DR_OPEN (0xFFFFFFFF);
        *pStat  &=  ~GMT_WR_PROT (0xFFFFFFFF);
    }

    // (see PROGRAMMING NOTE above)

    *pStat  &=  ~GMT_BOT (0xFFFFFFFF);
    *pStat  &=  ~GMT_SM  (0xFFFFFFFF);
    *pStat  &=  ~GMT_EOF (0xFFFFFFFF);
    *pStat  &=  ~GMT_EOT (0xFFFFFFFF);
    *pStat  &=  ~GMT_EOD (0xFFFFFFFF);

    if (0
        || ERROR_BUS_RESET            == errno // (spurious error; retry)
        || ERROR_MEDIA_CHANGED        == errno // (spurious error; retry)
//      || ERROR_DEVICE_NOT_CONNECTED == errno // (PERM ERROR! NO RETRY!)
//      || ERROR_DEV_NOT_EXIST        == errno // (PERM ERROR! NO RETRY!)
//      || ERROR_FILE_NOT_FOUND       == errno // (PERM ERROR! NO RETRY!)
    )
    {
        return EINTR;   // (Interrupted system call; Retry)
    }

    // (see PROGRAMMING NOTE further above)

    switch (errno)
    {
        default:                          break;  // (leave errno set to whatever it already is)
        case NO_ERROR:    errno = 0;      break;  // (normal expected i/o result)

        case ERROR_BEGINNING_OF_MEDIA: *pStat |= GMT_BOT     (0xFFFFFFFF); errno = EIO;       break;
        case ERROR_END_OF_MEDIA:       *pStat |= GMT_EOT     (0xFFFFFFFF); errno = ENOSPC;    break;

        //  "ERROR_END_OF_MEDIA"
        //
        //      Msg:   "The physical end of the tape has been reached."
        //
        //      The EOT warning reflector has been reached or passed (i.e. you're
        //      now/still in the "EOT Warning Zone" area). Writing additional data
        //      and/or tapemarks may still be possible depending on the size of the
        //      EOT Warning Zone (as set by a SetTapeParameters call with a non-zero
        //      EOTWarningZoneSize value (if supported; see further below)) and
        //      how much data you've already written to the EOT Warning Zone area
        //      (i.e. once you're in the warning area, this "error" occurs after
        //      EACH and EVERY I/O [in the warning zone area] until the ABSOLUTE
        //      physical end-of-tape (ERROR_EOM_OVERFLOW) is reached; see below).
        //
        //
        //                       ***********************
        //                       **  IMPORTANT NOTE!  **
        //                       ***********************
        //
        //                    This is NOT actually an "error"!!!
        //
        //
        //      When this "error" occurs, your "ReadFile" and/or "WriteFile" call
        //      returns 'FALSE' even though ALL of your requested data was actually
        //      written successfully!! This can be verified by checking to ensure
        //      the returned "number of bytes written" actually matches the amount
        //      you asked to be written. If they're the same (and they ALWAYS will
        //      be for this specific "error" code), then it means this "error" is
        //      NOT actually an error at all, but rather just a WARNING instead!!
        //      (Had it been an actual i/o error, the error code would have been
        //      some other DIFFERENT error code value instead!!)
        //
        //
        //                       ***********************
        //                       **  ALSO IMPORTANT!  **
        //                       ***********************
        //      See also:
        //
        //    http://fixunix.com/storage/205622-bug-dlttape-sys-no-eot-warning.html
        //
        //      for ADDITIONAL IMPORTANT INFORMATION regarding always having to
        //      specifically request that this "error" code be returned to you:
        //
        //      Even when a drive reports it does not support the setting of the
        //      the 'EOTWarningZoneSize' value (i.e. the FeaturesLow field of the
        //      GetTapeParameters call returns '0' for TAPE_DRIVE_SET_EOT_WZ_SIZE
        //      field), it may still be possible for "ERROR_END_OF_MEDIA" warnings
        //      to be generated anyway by simply calling SetTapeParameters with a
        //      non-zero 'EOTWarningZoneSize' value anyway.
        //
        //      The reason for this is because some drives may not allow CHANGING
        //      the value (thus the reason for it reporting that setting the value
        //      is not supported), but may nevertheless still support the ENABLING
        //      of their own hard-coded internal value. That is to say, while the
        //      size of the warning zone may not be modifiable (as it may be hard-
        //      coded and thus unchangeable), the drive may still have the ability
        //      to REPORT reaching the EOT Warning zone IF SPECIFICALLY REQUESTED
        //      TO DO SO! (which is presumably what requesting a non-zero Warning
        //      Zone size would end up doing: i.e. even though such calls APPEAR
        //      to fail, they actually DO succeed in accomplishing SOMETHING, just
        //      not what you originally/specifically requested).
        //
        //      Thus calling SetTapeParameters with a non-zero 'EOTWarningZoneSize'
        //      value might very well succeed anyway even though GetTapeParameters
        //      reports that doing so is not supported, and by so doing, may cause
        //      the drive to begin reporting of "ERROR_END_OF_MEDIA" (whereas not
        //      attempting to do so would end up leaving the drive in its default
        //      non-reporting mode. That is to say, you should ALWAYS try setting
        //      a non-zero 'EOTWarningZoneSize' value, ignoring any "unsupported"
        //      error code that may be returned from such a call.)

        case ERROR_EOM_OVERFLOW:       *pStat |= GMT_EOT     (0xFFFFFFFF); errno = EIO;       break;

        //  "ERROR_EOM_OVERFLOW"
        //
        //      Msg:   "Physical end of tape encountered."
        //
        //      This error code means that the actual physical end-of-media has been
        //      reached, and no more data can be written to the tape. This includes
        //      tapemarks as well.
        //
        //                       ***********************
        //                       **  IMPORTANT NOTE!  **
        //                       ***********************
        //
        //                 This is a HARD (UNRECOVERABLE) error!!
        //
        //      To be programmatically informed of when you are coming close to the
        //      physical end-of-the-tape (such that you could be assured room still
        //      remained to write logical end-of-volume labels for example), simply
        //      call SetTapeParameters with a non-zero 'EOTWarningZoneSize' value
        //      and treat any "ERROR_END_OF_MEDIA" "errors" received when writing
        //      as warnings instead. (See prior discussion of "ERROR_END_OF_MEDIA"
        //      return code further above)

        case ERROR_NO_DATA_DETECTED:   *pStat |= GMT_EOD     (0xFFFFFFFF); errno = EIO;       break;
        case ERROR_FILEMARK_DETECTED:  *pStat |= GMT_EOF     (0xFFFFFFFF); errno = EIO;       break;
        case ERROR_SETMARK_DETECTED:   *pStat |= GMT_SM      (0xFFFFFFFF); errno = EIO;       break;
        case ERROR_NOT_READY:          *pStat |= GMT_DR_OPEN (0xFFFFFFFF); errno = ENOMEDIUM; break;
        case ERROR_NO_MEDIA_IN_DRIVE:  *pStat |= GMT_DR_OPEN (0xFFFFFFFF); errno = ENOMEDIUM; break;
        case ERROR_WRITE_PROTECT:      *pStat |= GMT_WR_PROT (0xFFFFFFFF); errno = EROFS;     break;
    }
    return errno;
}

////////////////////////////////////////////////////////////////////////////////////
// (forward references for private helper functions)

int  w32_internal_mtop  ( HANDLE hFile, U32* pStat, struct mtop*  mtop,  ifd_t ifd );
int  w32_internal_mtget ( HANDLE hFile, U32* pStat, struct mtget* mtget, ifd_t ifd );
int  w32_internal_mtpos ( HANDLE hFile, U32* pStat, DWORD* pdwLogPos,
                                                    DWORD* pdwAbsPos,    ifd_t ifd );

////////////////////////////////////////////////////////////////////////////////////
// Close tape device...

DLL_EXPORT
int w32_close_tape ( ufd_t  ufd )
{
    ifd_t  ifd  = W32STAPE_UFD2IFD( ufd );
    int    rc   = -1;
    errno       = EBADF;

    lock();

    if (1
        &&         ifd    >=  0
        &&         ifd    <   W32STAPE_MAX_FDNUMS
        && g_ifds[ ifd ]  !=  0
    )
    {
        // Deallocate resources

        HANDLE  hFile  = g_handles[ ifd ];
        char*   pName  = g_fnames [ ifd ];

        g_handles[ ifd ] = NULL;
        g_fnames [ ifd ] = NULL;
        g_fstats [ ifd ] = GMT_DR_OPEN (0xFFFFFFFF);
        g_BOTmsk [ ifd ] = 0xFFFFFFFF;
        g_BOTbot [ ifd ] = 0x00000000;

        VERIFY( w32_free_ifd( ifd ) == 0 );

        // Close the file...

        free( pName );

        errno  =  CloseHandle( hFile ) ? 0 : w32_trans_w32error( GetLastError() );
        rc     =  errno ? -1 : 0;
    }

    unlock();

    return rc;
}

////////////////////////////////////////////////////////////////////////////////////
// Read tape...

DLL_EXPORT
ssize_t  w32_read_tape ( ufd_t ufd, void* buf, size_t nbyte )
{
    BOOL    bSuccess;
    DWORD   dwBytesRead;
    DWORD   dwLastError;

    ifd_t   ifd    = W32STAPE_UFD2IFD( ufd );
    U32*    pStat  = NULL;
    HANDLE  hFile;

    if (!buf)
    {
        errno = EINVAL;
        return -1;
    }

    lock();

    if (0
        ||         ifd    <   0
        ||         ifd    >=  W32STAPE_MAX_FDNUMS
        || g_ifds[ ifd ]  ==  0
    )
    {
        unlock();
        errno = EBADF;
        return -1;
    }

    unlock();

    hFile = g_handles[ ifd ];
    pStat = &g_fstats[ ifd ];

    // Do the i/o, save results, update device status
    // (based on the results), then check results...

    do
    {
        dwBytesRead = 0;
        bSuccess    = ReadFile( hFile, buf, (DWORD)nbyte, &dwBytesRead, NULL );
        errno       = (dwLastError = GetLastError());
        errno       = w32_internal_rc ( pStat );
    }
    while ( !bSuccess && EINTR == errno );

    // Success?  (see: "ERROR_END_OF_MEDIA" in function 'w32_internal_rc')

    if (bSuccess || ERROR_END_OF_MEDIA == dwLastError)
    {
        ASSERT( bSuccess || ENOSPC == errno );
        return ( (ssize_t) dwBytesRead );
    }

    ASSERT( !bSuccess && ERROR_END_OF_MEDIA != dwLastError && ENOSPC != errno );

    // The i/o "failed".  Check to see if it was just a tapemark...

    if ( EIO == errno && GMT_EOF( *pStat ) )
    {
        ASSERT( ERROR_FILEMARK_DETECTED == dwLastError );
        return 0;   // (tapemark)
    }

    // EIO != errno || !GMT_EOF( *pStat )  -->  bona fide i/o error...

    ASSERT( ERROR_FILEMARK_DETECTED != dwLastError );
    return -1;
}

////////////////////////////////////////////////////////////////////////////////////
// Write tape...

DLL_EXPORT
ssize_t  w32_write_tape ( ufd_t ufd, const void* buf, size_t nbyte )
{
    BOOL     bSuccess;
    DWORD    dwBytesWritten;
    DWORD    dwLastError;

    ifd_t    ifd    = W32STAPE_UFD2IFD( ufd );
    U32*     pStat  = NULL;
    HANDLE   hFile;

    if (!buf)
    {
        errno = EINVAL;
        return -1;
    }

    lock();

    if (0
        ||         ifd    <   0
        ||         ifd    >=  W32STAPE_MAX_FDNUMS
        || g_ifds[ ifd ]  ==  0
    )
    {
        unlock();
        errno = EBADF;
        return -1;
    }

    unlock();

    hFile = g_handles[ ifd ];
    pStat = &g_fstats[ ifd ];

    // Do the i/o, save results, update device status
    // (based on the results), then check results...

    do
    {
        dwBytesWritten = 0;
        bSuccess       = WriteFile( hFile, buf, (DWORD)nbyte, &dwBytesWritten, NULL );
        errno          = (dwLastError = GetLastError());
        errno          = w32_internal_rc ( pStat );
    }
    while ( !bSuccess && EINTR == errno );

    // Success?  (see: "ERROR_END_OF_MEDIA" in function 'w32_internal_rc')

    if (bSuccess || ERROR_END_OF_MEDIA == dwLastError)
    {
        ASSERT( bSuccess || ENOSPC == errno );
        ASSERT( ((size_t)dwBytesWritten) == nbyte );  // (MUST be true!!)
        return ( (ssize_t) dwBytesWritten );
    }

    // I/O error...

    ASSERT( !bSuccess && ERROR_END_OF_MEDIA != dwLastError && ENOSPC != errno );
    return -1;
}

////////////////////////////////////////////////////////////////////////////////////
// ioctl...    (perform some type of control function, e.g. fsf, rewind, etc)

DLL_EXPORT
int w32_ioctl_tape ( ufd_t ufd, int request, ... )
{
    va_list  vl;
    void*    ptr    = NULL;
    int      rc     = 0;
    ifd_t    ifd    = W32STAPE_UFD2IFD( ufd );
    U32*     pStat  = NULL;
    HANDLE   hFile;

    lock();

    if (0
        ||         ifd    <   0
        ||         ifd    >=  W32STAPE_MAX_FDNUMS
        || g_ifds[ ifd ]  ==  0
    )
    {
        unlock();
        errno = EBADF;
        return -1;
    }

    unlock();

    hFile = g_handles[ ifd ];
    pStat = &g_fstats[ ifd ];

    va_start ( vl, request );
    ptr = va_arg( vl, void* );

    if ( !ptr )
    {
        errno = EINVAL;
        return -1;
    }

    switch (request)
    {
        case MTIOCTOP:  // (perform tape operation)
        {
            struct mtop* mtop = ptr;
            rc = w32_internal_mtop ( hFile, pStat, mtop, ifd );
        }
        break;

        case MTIOCGET:  // (retrieve tape status)
        {
            struct mtget* mtget = ptr;
            memset( mtget, 0, sizeof(*mtget) );
            rc = w32_internal_mtget ( hFile, pStat, mtget, ifd );
        }
        break;

        case MTIOCPOS:  // (retrieve tape position)
        {
            struct mtpos* mtpos = ptr;
            memset( mtpos, 0, sizeof(*mtpos) );
            rc = w32_internal_mtpos( hFile, pStat, &mtpos->mt_blkno, NULL, ifd );
        }
        break;

        default:    // (invalid/unsupported ioctl code)
        {
            errno = EINVAL;
            rc = -1;
        }
        break;
    }

    return rc;
}

////////////////////////////////////////////////////////////////////////////////////
// Private internal helper function...   return 0 == success, -1 == failure

static
int w32_internal_mtop ( HANDLE hFile, U32* pStat, struct mtop* mtop, ifd_t ifd )
{
    int rc = 0;

    ASSERT( pStat && mtop );    // (sanity check)

    // General technique: do the i/o, save results, update the
    // device status (based on the results), then check results...

    switch ( mtop->mt_op )
    {
        case MTLOAD:    // (load media)
        {
            if ( 1 != mtop->mt_count )
            {
                errno = EINVAL;
                rc = -1;
            }
            else
            {
                do
                {
                    errno = PrepareTape( hFile, TAPE_LOAD, FALSE );
                    errno = w32_internal_rc ( pStat );
                }
                while ( EINTR == errno );
            }
        }
        break;

        case MTUNLOAD:  // (unload media)
        case MTOFFL:    // (make media offline (same as unload))
        {
            if ( 1 != mtop->mt_count )
            {
                errno = EINVAL;
                rc = -1;
            }
            else
            {
                do
                {
                    errno = PrepareTape( hFile, TAPE_UNLOAD, FALSE );
                    errno = w32_internal_rc ( pStat );
                }
                while ( EINTR == errno );
            }
        }
        break;

        case MTSEEK:    // (position media)
        {
            do
            {
                errno = SetTapePosition( hFile, TAPE_LOGICAL_BLOCK, 0, mtop->mt_count, 0, FALSE );
                errno = w32_internal_rc ( pStat );
            }
            while ( EINTR == errno );
        }
        break;

        case MTREW:     // (rewind)
        {
            if ( 1 != mtop->mt_count )
            {
                errno = EINVAL;
                rc = -1;
            }
            else
            {
                do
                {
                    errno = SetTapePosition( hFile, TAPE_REWIND, 0, 0, 0, FALSE );
                    errno = w32_internal_rc ( pStat );
                }
                while ( EINTR == errno );
            }
        }
        break;

        case MTFSF:     // (FORWARD  space FILE)
        case MTBSF:     // (BACKWARD space FILE)
        {
            if ( !mtop->mt_count )
            {
                errno = EINVAL;
                rc = -1;
            }
            else
            {
                LARGE_INTEGER liCount;

                liCount.QuadPart = mtop->mt_count;

                if ( MTBSF == mtop->mt_op )
                    liCount.QuadPart = -liCount.QuadPart; // (negative == backwards)

                do
                {
                    errno = SetTapePosition( hFile, TAPE_SPACE_FILEMARKS, 0, liCount.LowPart, liCount.HighPart, FALSE );
                    errno = w32_internal_rc ( pStat );
                }
                while ( EINTR == errno );
            }
        }
        break;

        case MTFSR:     // (FORWARD  space BLOCK)
        case MTBSR:     // (BACKWARD space BLOCK)
        {
            if ( !mtop->mt_count )
            {
                errno = EINVAL;
                rc = -1;
            }
            else
            {
                LARGE_INTEGER liCount;

                liCount.QuadPart = mtop->mt_count;

                if ( MTBSR == mtop->mt_op )
                    liCount.QuadPart = -liCount.QuadPart; // (negative == backwards)

                do
                {
                    errno = SetTapePosition( hFile, TAPE_SPACE_RELATIVE_BLOCKS, 0, liCount.LowPart, liCount.HighPart, FALSE );
                    errno = w32_internal_rc ( pStat );
                }
                while ( EINTR == errno );
            }
        }
        break;

        case MTSETBLK:  // (set blocksize)
        {
            TAPE_SET_MEDIA_PARAMETERS  media_parms;

            media_parms.BlockSize = mtop->mt_count;

            do
            {
                errno = SetTapeParameters( hFile, SET_TAPE_MEDIA_INFORMATION, &media_parms );
                errno = w32_internal_rc ( pStat );
            }
            while ( EINTR == errno );
        }
        break;

        case MTEOTWARN:   // (set EOT Warning Zone size in bytes)
        {
            TAPE_SET_DRIVE_PARAMETERS   set_drive_parms;

            set_drive_parms.ECC                = g_drive_parms[ifd].ECC;
            set_drive_parms.Compression        = g_drive_parms[ifd].Compression;
            set_drive_parms.DataPadding        = g_drive_parms[ifd].DataPadding;
            set_drive_parms.ReportSetmarks     = g_drive_parms[ifd].ReportSetmarks;
            set_drive_parms.EOTWarningZoneSize = mtop->mt_count;

            do
            {
                errno = SetTapeParameters( hFile, SET_TAPE_DRIVE_INFORMATION, &set_drive_parms );
                errno = w32_internal_rc ( pStat );
            }
            while ( EINTR == errno );
        }
        break;

        case MTWEOF:    // (write TAPEMARK)
        {
            if ( mtop->mt_count < 0 )
            {
                errno = EINVAL;
                rc = -1;
            }
            else
            {
                // PROGRAMMING NOTE: We prefer "long" filemarks over any other type
                // because, according to the SDK documentaion:
                //
                //    "A short filemark contains a short erase gap that cannot be
                //     overwritten unless the write operation is performed from the
                //     beginning of the partition or from an earlier long filemark."
                //
                //    "A long filemark contains a long erase gap that allows an
                //     application to position the tape at the beginning of the filemark
                //     and to overwrite the filemark and the erase gap."
                //
                // Thus if TAPE_LONG_FILEMARKS is not supported we try ONLY the generic
                // TAPE_FILEMARKS variety and return an error if that fails; we do NOT
                // ever attempt the TAPE_SHORT_FILEMARKS or TAPE_SETMARKS variety.

                DWORD  dwTapemarkType = TAPE_LONG_FILEMARKS;

                if ( !( g_drive_parms[ifd].FeaturesHigh & TAPE_DRIVE_WRITE_LONG_FMKS ) )
                    dwTapemarkType = TAPE_FILEMARKS;

                do
                {
                    errno = WriteTapemark( hFile, dwTapemarkType, mtop->mt_count, FALSE );
                    errno = w32_internal_rc ( pStat );
                }
                while ( EINTR == errno );
            }
        }
        break;

        case MTERASE: // (write erase gap or erase entire tape (data security erase))
        {
            if (1
                && 0 != mtop->mt_count  // (0 == write erase gap at current position)
                && 1 != mtop->mt_count  // (1 == erases the remainder of entire tape)
            )
            {
                errno = EINVAL;
                rc = -1;
            }
            else
            {
                DWORD  dwEraseType  =
                    mtop->mt_count ? TAPE_ERASE_LONG : TAPE_ERASE_SHORT;

                do
                {
                    errno = EraseTape( hFile, dwEraseType, FALSE );
                    errno = w32_internal_rc ( pStat );
                }
                while ( EINTR == errno );
            }
        }
        break;

        case MTNOP:         // (no operation)
        {
            errno = 0;
            rc = 0;
        }
        break;

        default:        // (invalid/unsupported tape operation)
        {
            errno = EINVAL;
            rc = -1;
        }
        break;
    }

    return (rc = (0 == errno || ENOSPC == errno) ? 0 : /* errno != 0 && errno != ENOSPC */ -1);
}

////////////////////////////////////////////////////////////////////////////////////
// Private internal helper function...   return 0 == success, -1 == failure

static
int w32_internal_mtget ( HANDLE hFile, U32* pStat, struct mtget* mtget, ifd_t ifd )
{
    TAPE_GET_MEDIA_PARAMETERS   media_parms;
    DWORD                       dwRetCode, dwSize, dwLogicalPosition;

    ASSERT( pStat && mtget );

    mtget->mt_resid   =   0;            // (unknown/unsupported)
    mtget->mt_erreg   =   0;            // (unknown/unsupported)
    mtget->mt_fileno  =  -1;            // (unknown/unsupported)
    mtget->mt_blkno   =  -1;            // (unknown as of yet; set further below)
    mtget->mt_type    =  MT_ISSCSI2;    // "Generic ANSI SCSI-2 tape unit"
    mtget->mt_gstat   =  -1;            // (purposely invalid; set correctly below)

    // Reset the mounted status; it will get set further below...

    *pStat &= ~GMT_DR_OPEN (0xFFFFFFFF);

    // Attempt to retrieve the status of the tape-drive...

    dwRetCode = w32_get_tape_status( hFile );

    // Windows returns 'ERROR_NOT_READY' if no tape is mounted
    // instead of the usual expected 'ERROR_NO_MEDIA_IN_DRIVE'

    if ( ERROR_NOT_READY == dwRetCode )
        dwRetCode = ERROR_NO_MEDIA_IN_DRIVE;

    // If there is not tape mounted OR a new tape was mounted,
    // then the following status bits are now unknown/obsolete

    if (0
        || ERROR_NO_MEDIA_IN_DRIVE == dwRetCode
        || ERROR_MEDIA_CHANGED     == dwRetCode
    )
    {
        // (these statuse are now obsolete)
        *pStat  &=  ~GMT_WR_PROT (0xFFFFFFFF);
        *pStat  &=  ~GMT_BOT     (0xFFFFFFFF);
        *pStat  &=  ~GMT_EOT     (0xFFFFFFFF);
        *pStat  &=  ~GMT_EOD     (0xFFFFFFFF);
        *pStat  &=  ~GMT_EOF     (0xFFFFFFFF);
        *pStat  &=  ~GMT_SM      (0xFFFFFFFF);
    }

    // There's no sense trying to get media parameters
    // unless there's some media loaded on the drive!

    if ( ERROR_NO_MEDIA_IN_DRIVE == dwRetCode )
    {
        *pStat |= GMT_DR_OPEN (0xFFFFFFFF);     // (no tape mounted in drive)
        mtget->mt_gstat = *pStat;               // (return current status)
        return 0;                               // (nothing more we can do)
    }

    // A tape appears to be mounted on the drive...
    // Retrieve the media parameters information...

    dwSize = sizeof(media_parms);
    memset( &media_parms, 0, dwSize );
    dwRetCode = GetTapeParameters( hFile, GET_TAPE_MEDIA_INFORMATION, &dwSize, &media_parms );
    ASSERT( sizeof(media_parms) == dwSize );

    if ( NO_ERROR == dwRetCode )
    {
        mtget->mt_dsreg = media_parms.BlockSize;

        if (media_parms.WriteProtected)
            *pStat |=  GMT_WR_PROT (0xFFFFFFFF);
        else
            *pStat &= ~GMT_WR_PROT (0xFFFFFFFF);
    }
    else
        mtget->mt_dsreg = 0;    // (unknown; variable blocks presumed)

    // Lastly, attempt to determine if we are at BOT (i.e. load-point)...

    if ( 0 != ( errno = w32_internal_mtpos( hFile, pStat, &dwLogicalPosition, NULL, ifd ) ) )
    {
        mtget->mt_gstat = *pStat;
        return -1;
    }

    mtget->mt_blkno = dwLogicalPosition;

    if ( ( dwLogicalPosition & g_BOTmsk[ ifd ] ) == g_BOTbot[ ifd ] )
        *pStat |=  GMT_BOT (0xFFFFFFFF);
    else
        *pStat &= ~GMT_BOT (0xFFFFFFFF);

    mtget->mt_gstat = *pStat;
    return 0;
}

////////////////////////////////////////////////////////////////////////////////////
// Private internal helper function...   return 0 == success, -1 == failure

static
int  w32_internal_mtpos ( HANDLE hFile, U32* pStat, DWORD* pdwLogPos,
                                                    DWORD* pdwAbsPos, ifd_t ifd )
{
    DWORD dwDummyPartition, dwDummyPositionHigh;

    ASSERT( pStat && pdwLogPos );    // (sanity check)

    // PROGRAMMING NOTE: the SDK docs state that for the 'lpdwOffsetHigh'
    // parameter (i.e. dwDummyPositionHigh, the 5th paramater):
    //
    //    "This parameter can be NULL if the
    //     high-order bits are not required."
    //
    // But it LIES! Simple expirical observation reveals that ALL parameters
    // are in fact required. If any are NULL then 'GetTapePosition' crashes
    // and burns (which is unusual since usually when you pass invalid args
    // to an API it usually just returns an error code, but in this case it
    // doesn't. It actually crashes)

    do
    {
        U32 dummy_stat = 0;
        errno = GetTapePosition
        (
            hFile,
            TAPE_LOGICAL_POSITION,
            &dwDummyPartition,
            pdwLogPos,
            &dwDummyPositionHigh
        );
        errno = w32_internal_rc ( &dummy_stat );
    }
    while ( EINTR == errno );

    if (errno)
        return -1;

    if (pdwAbsPos)  // (may be NULL if they're not interested in it)
    {
        do
        {
            U32 dummy_stat = 0;
            errno = GetTapePosition
            (
                hFile,
                TAPE_ABSOLUTE_POSITION,
                &dwDummyPartition,
                pdwAbsPos,
                &dwDummyPositionHigh
            );
            errno = w32_internal_rc ( &dummy_stat );
        }
        while ( EINTR == errno );

        if (errno)
            return -1;
    }

    // PROGRAMMING NOTE: the Windows 'GetTapePosition' API returns either
    // a LOGICAL position value or an ABSOLUTE position value. Based on
    // trial and error it was determined the LOGICAL position corresponds
    // to the SCSI "READ POSITION" command's "first block location" value,
    // and the ABSOLUTE tape position appears to correspond to the SCSI
    // "last block location".
    
    // Since what we want is what IBM calls the "Channel block ID" (which
    // itself appears to correspond to what the SCSI documentation refers
    // to as the "First block location"), then what we want here is what
    // Windows refers to as the LOGICAL position, not the ABSOLUTE (i.e.
    // device-relative) position I originally thought we needed/wanted.

    if ( ( *pdwLogPos & g_BOTmsk[ ifd ] ) == g_BOTbot[ ifd ] )
        *pStat |=  GMT_BOT (0xFFFFFFFF);
    else
        *pStat &= ~GMT_BOT (0xFFFFFFFF);

    return 0;
}

////////////////////////////////////////////////////////////////////////////////////

#endif /* _MSVC_ */
