/*
 * Copyright (c) 2006-2010 Douglas Gilbert.
 * All rights reserved.
 * Use of this source code is governed by a BSD-style
 * license that can be found in the BSD_LICENSE file.
 */

/* sg_pt_win32 version 1.11 20100321 */

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>

#include "sg_pt.h"
#include "sg_lib.h"
#include "sg_pt_win32.h"

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

/* Use the Microsoft SCSI Pass Through (SPT) interface. It has two
 * variants: "SPT" where data is double buffered; and "SPTD" where data
 * pointers to the user space are passed to the OS. Only Windows
 * 2000 and later (i.e. not 95,98 or ME).
 * There is no ASPI interface which relies on a dll from adaptec.
 * This code uses cygwin facilities and is built in a cygwin
 * shell. It can be run in a normal DOS shell if the cygwin1.dll
 * file is put in an appropriate place.
 * This code can build in a MinGW environment.
 *
 * N.B. MSDN says that the "SPT" interface (i.e. double buffered)
 * should be used for small amounts of data (it says "< 16 KB").
 * The direct variant (i.e. IOCTL_SCSI_PASS_THROUGH_DIRECT) should
 * be used for larger amounts of data but the buffer needs to be
 * "cache aligned". Is that 16 byte alignment or greater?
 *
 * This code will default to indirect (i.e. double buffered) access
 * unless the WIN32_SPT_DIRECT preprocessor constant is defined in
 * config.h .
 */

#define DEF_TIMEOUT 60       /* 60 seconds */
#define MAX_OPEN_SIMULT 8
#define WIN32_FDOFFSET 32

struct sg_pt_handle {
    int in_use;
    HANDLE fh;
    char adapter[32];
    int bus;
    int target;
    int lun;
};

struct sg_pt_handle handle_arr[MAX_OPEN_SIMULT];

struct sg_pt_win32_scsi {
    unsigned char * dxferp;
    int dxfer_len;
    unsigned char * sensep;
    int sense_len;
    int scsi_status;
    int resid;
    int sense_resid;
    int in_err;
    int os_err;                 /* pseudo unix error */
    int transport_err;          /* windows error number */
#ifdef WIN32_SPT_DIRECT
    SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER swb;
#else
    /* Last entry in structure so data buffer can be extended */
    SCSI_PASS_THROUGH_WITH_BUFFERS swb;
#endif
};

/* embed pointer so can change on fly if (non-direct) data buffer
 * is not big enough */
struct sg_pt_base {
    struct sg_pt_win32_scsi * implp;
};


/* Returns >= 0 if successful. If error in Unix returns negated errno. */
int
scsi_pt_open_device(const char * device_name, int read_only, int verbose)
{
    int oflags = 0 /* O_NONBLOCK*/ ;

    oflags |= (read_only ? 0 : 0);      /* was ... ? O_RDONLY : O_RDWR) */
    return scsi_pt_open_flags(device_name, oflags, verbose);
}

/*
 * Similar to scsi_pt_open_device() but takes Unix style open flags OR-ed
 * together. The 'flags' argument is ignored in Windows.
 * Returns >= 0 if successful, otherwise returns negated errno.
 * Optionally accept leading "\\.\". If given something of the form
 * "SCSI<num>:<bus>,<target>,<lun>" where the values in angle brackets
 * are integers, then will attempt to open "\\.\SCSI<num>:" and save the
 * other three values for the DeviceIoControl call. The trailing ".<lun>"
 * is optionally and if not given 0 is assumed. Since "PhysicalDrive"
 * is a lot of keystrokes, "PD" is accepted and converted to the longer
 * form.
 */
int
scsi_pt_open_flags(const char * device_name,
                   int flags __attribute__ ((unused)),
                   int verbose)
{
    int len, k, adapter_num, bus, target, lun, off, got_scsi_name;
    int index, num, got_pd_name, pd_num;
    struct sg_pt_handle * shp;
    char buff[8];

    if (NULL == sg_warnings_strm)
        sg_warnings_strm = stderr;
    /* lock */
    for (k = 0; k < MAX_OPEN_SIMULT; k++)
        if (0 == handle_arr[k].in_use)
            break;
    if (k == MAX_OPEN_SIMULT) {
        if (verbose)
            fprintf(sg_warnings_strm, "too many open handles "
                    "(%d)\n", MAX_OPEN_SIMULT);
        return -EMFILE;
    } else
        handle_arr[k].in_use = 1;
    /* unlock */
    index = k;
    shp = handle_arr + index;
    adapter_num = 0;
    bus = 0;    /* also known as 'PathId' in MS docs */
    target = 0;
    lun = 0;
    got_pd_name = 0;
    got_scsi_name = 0;
    len = strlen(device_name);
    if ((len > 4) && (0 == strncmp("\\\\.\\", device_name, 4)))
        off = 4;
    else
        off = 0;
    if (len > (off + 2)) {
        buff[0] = toupper(device_name[off + 0]);
        buff[1] = toupper(device_name[off + 1]);
        if (0 == strncmp("PD", buff, 2)) {
            num = sscanf(device_name + off + 2, "%d", &pd_num);
            if (1 == num)
                got_pd_name = 1;
        }
        if (0 == got_pd_name) {
            buff[2] = toupper(device_name[off + 2]);
            buff[3] = toupper(device_name[off + 3]);
            if (0 == strncmp("SCSI", buff, 4)) {
                num = sscanf(device_name + off + 4, "%d:%d,%d,%d",
                             &adapter_num, &bus, &target, &lun);
                if (num < 3) {
                    if (verbose)
                        fprintf(sg_warnings_strm, "expected format like: "
                                "'SCSI<port>:<bus>.<target>[.<lun>]'\n");
                    shp->in_use = 0;
                    return -EINVAL;
                }
                got_scsi_name = 1;
            }
        }
    }
    shp->bus = bus;
    shp->target = target;
    shp->lun = lun;
    memset(shp->adapter, 0, sizeof(shp->adapter));
    strncpy(shp->adapter, "\\\\.\\", 4);
    if (got_pd_name)
        snprintf(shp->adapter + 4, sizeof(shp->adapter) - 5,
                 "PhysicalDrive%d", pd_num);
    else if (got_scsi_name)
        snprintf(shp->adapter + 4, sizeof(shp->adapter) - 5, "SCSI%d:",
                 adapter_num);
    else
        snprintf(shp->adapter + 4, sizeof(shp->adapter) - 5, "%s",
                 device_name + off);
    shp->fh = CreateFile(shp->adapter, GENERIC_READ | GENERIC_WRITE,
                         FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
                         OPEN_EXISTING, 0, NULL);
    if (shp->fh == INVALID_HANDLE_VALUE) {
        if (verbose)
            fprintf(sg_warnings_strm, "Windows CreateFile error=%ld\n",
                    GetLastError());
        shp->in_use = 0;
        return -ENODEV;
    }
    return index + WIN32_FDOFFSET;
}


/* Returns 0 if successful. If error in Unix returns negated errno. */
int
scsi_pt_close_device(int device_fd)
{
    struct sg_pt_handle * shp;
    int index;

    index = device_fd - WIN32_FDOFFSET;

    if ((index < 0) || (index >= WIN32_FDOFFSET))
        return -ENODEV;
    shp = handle_arr + index;
    CloseHandle(shp->fh);
    shp->bus = 0;
    shp->target = 0;
    shp->lun = 0;
    memset(shp->adapter, 0, sizeof(shp->adapter));
    shp->in_use = 0;
    return 0;
}

struct sg_pt_base *
construct_scsi_pt_obj()
{
    struct sg_pt_win32_scsi * psp;
    struct sg_pt_base * vp = NULL;

    psp = (struct sg_pt_win32_scsi *)calloc(sizeof(struct sg_pt_win32_scsi),
                                            1);
    if (psp) {
        psp->swb.spt.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED;
        psp->swb.spt.SenseInfoLength = SCSI_MAX_SENSE_LEN;
        psp->swb.spt.SenseInfoOffset =
                offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucSenseBuf);
        psp->swb.spt.TimeOutValue = DEF_TIMEOUT;
        vp = malloc(sizeof(struct sg_pt_win32_scsi *)); // yes a pointer
        if (vp)
            vp->implp = psp;
        else
            free(psp);
    }
    return vp;
}

void
destruct_scsi_pt_obj(struct sg_pt_base * vp)
{
    if (vp) {
        struct sg_pt_win32_scsi * psp = vp->implp;

        if (psp) {
            free(psp);
        }
        free(vp);
    }
}

void
clear_scsi_pt_obj(struct sg_pt_base * vp)
{
    struct sg_pt_win32_scsi * psp = vp->implp;

    if (psp) {
        memset(psp, 0, sizeof(struct sg_pt_win32_scsi));
        psp->swb.spt.DataIn = SCSI_IOCTL_DATA_UNSPECIFIED;
        psp->swb.spt.SenseInfoLength = SCSI_MAX_SENSE_LEN;
        psp->swb.spt.SenseInfoOffset =
                offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucSenseBuf);
        psp->swb.spt.TimeOutValue = DEF_TIMEOUT;
    }
}

void
set_scsi_pt_cdb(struct sg_pt_base * vp, const unsigned char * cdb,
                int cdb_len)
{
    struct sg_pt_win32_scsi * psp = vp->implp;

    if (psp->swb.spt.CdbLength > 0)
        ++psp->in_err;
    if (cdb_len > (int)sizeof(psp->swb.spt.Cdb)) {
        ++psp->in_err;
        return;
    }
    memcpy(psp->swb.spt.Cdb, cdb, cdb_len);
    psp->swb.spt.CdbLength = cdb_len;
}

void
set_scsi_pt_sense(struct sg_pt_base * vp, unsigned char * sense,
                  int sense_len)
{
    struct sg_pt_win32_scsi * psp = vp->implp;

    if (psp->sensep)
        ++psp->in_err;
    memset(sense, 0, sense_len);
    psp->sensep = sense;
    psp->sense_len = sense_len;
}

/* from device */
void
set_scsi_pt_data_in(struct sg_pt_base * vp, unsigned char * dxferp,
                    int dxfer_len)
{
    struct sg_pt_win32_scsi * psp = vp->implp;

    if (psp->dxferp)
        ++psp->in_err;
    if (dxfer_len > 0) {
        psp->dxferp = dxferp;
        psp->dxfer_len = dxfer_len;
        psp->swb.spt.DataIn = SCSI_IOCTL_DATA_IN;
    }
}

/* to device */
void
set_scsi_pt_data_out(struct sg_pt_base * vp, const unsigned char * dxferp,
                     int dxfer_len)
{
    struct sg_pt_win32_scsi * psp = vp->implp;

    if (psp->dxferp)
        ++psp->in_err;
    if (dxfer_len > 0) {
        psp->dxferp = (unsigned char *)dxferp;
        psp->dxfer_len = dxfer_len;
        psp->swb.spt.DataIn = SCSI_IOCTL_DATA_OUT;
    }
}

void
set_scsi_pt_packet_id(struct sg_pt_base * vp __attribute__ ((unused)),
                      int pack_id __attribute__ ((unused)))
{
}

void
set_scsi_pt_tag(struct sg_pt_base * vp, uint64_t tag __attribute__ ((unused)))
{
    struct sg_pt_win32_scsi * psp = vp->implp;

    ++psp->in_err;
}

void
set_scsi_pt_task_management(struct sg_pt_base * vp,
                            int tmf_code __attribute__ ((unused)))
{
    struct sg_pt_win32_scsi * psp = vp->implp;

    ++psp->in_err;
}

void
set_scsi_pt_task_attr(struct sg_pt_base * vp,
                      int attrib __attribute__ ((unused)),
                      int priority __attribute__ ((unused)))
{
    struct sg_pt_win32_scsi * psp = vp->implp;

    ++psp->in_err;
}

void
set_scsi_pt_flags(struct sg_pt_base * objp, int flags)
{
    /* do nothing, suppress warnings */
    objp = objp;
    flags = flags;
}

/* Executes SCSI command (or at least forwards it to lower layers).
 * Clears os_err field prior to active call (whose result may set it
 * again). */
int
do_scsi_pt(struct sg_pt_base * vp, int device_fd, int time_secs, int verbose)
{
    int index = device_fd - WIN32_FDOFFSET;
    struct sg_pt_win32_scsi * psp = vp->implp;
    struct sg_pt_handle * shp;
    BOOL status;
    ULONG returned;

    if (NULL == sg_warnings_strm)
        sg_warnings_strm = stderr;
    psp->os_err = 0;
    if (psp->in_err) {
        if (verbose)
            fprintf(sg_warnings_strm, "Replicated or unused set_scsi_pt...\n");
        return SCSI_PT_DO_BAD_PARAMS;
    }
    if (0 == psp->swb.spt.CdbLength) {
        if (verbose)
            fprintf(sg_warnings_strm, "No command (cdb) given\n");
        return SCSI_PT_DO_BAD_PARAMS;
    }

    index = device_fd - WIN32_FDOFFSET;
    if ((index < 0) || (index >= WIN32_FDOFFSET)) {
        if (verbose)
            fprintf(sg_warnings_strm, "Bad file descriptor\n");
        psp->os_err = ENODEV;
        return -psp->os_err;
    }
    shp = handle_arr + index;
    if (0 == shp->in_use) {
        if (verbose)
            fprintf(sg_warnings_strm, "File descriptor closed??\n");
        psp->os_err = ENODEV;
        return -psp->os_err;
    }
#ifdef WIN32_SPT_DIRECT
    psp->swb.spt.Length = sizeof (SCSI_PASS_THROUGH_DIRECT);
#else
    if (psp->dxfer_len > (int)sizeof(psp->swb.ucDataBuf)) {
        int extra = psp->dxfer_len - (int)sizeof(psp->swb.ucDataBuf);
        struct sg_pt_win32_scsi * epsp;

        if (verbose > 4)
            fprintf(sg_warnings_strm, "dxfer_len (%d) too large for initial "
                    "data buffer (%d bytes), try enlarging\n", psp->dxfer_len,
                    sizeof(psp->swb.ucDataBuf));
        epsp = (struct sg_pt_win32_scsi *)
               calloc(sizeof(struct sg_pt_win32_scsi) + extra, 1);
        if (NULL == epsp) {
            fprintf(sg_warnings_strm, "do_scsi_pt: failed to enlarge data "
                    "buffer to %d bytes\n", psp->dxfer_len);
            psp->os_err = ENOMEM;
            return -psp->os_err;
        }
        memcpy(epsp, psp, sizeof(struct sg_pt_win32_scsi));
        free(psp);
        vp->implp = epsp;
        psp = epsp;
    }
    psp->swb.spt.Length = sizeof (SCSI_PASS_THROUGH);
    psp->swb.spt.DataBufferOffset =
                offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS, ucDataBuf);
#endif
    psp->swb.spt.PathId = shp->bus;
    psp->swb.spt.TargetId = shp->target;
    psp->swb.spt.Lun = shp->lun;
    psp->swb.spt.TimeOutValue = time_secs;
    psp->swb.spt.DataTransferLength = psp->dxfer_len;
    if (verbose > 4) {
        fprintf(stderr, " spt:: adapter: %s  Length=%d ScsiStatus=%d "
                "PathId=%d TargetId=%d Lun=%d\n", shp->adapter,
                (int)psp->swb.spt.Length,
                (int)psp->swb.spt.ScsiStatus, (int)psp->swb.spt.PathId,
                (int)psp->swb.spt.TargetId, (int)psp->swb.spt.Lun);
        fprintf(stderr, "    CdbLength=%d SenseInfoLength=%d DataIn=%d "
                "DataTransferLength=%lu\n",
                (int)psp->swb.spt.CdbLength, (int)psp->swb.spt.SenseInfoLength,
                (int)psp->swb.spt.DataIn, psp->swb.spt.DataTransferLength);
#ifdef WIN32_SPT_DIRECT
        fprintf(stderr, "    TimeOutValue=%lu SenseInfoOffset=%lu\n",
                psp->swb.spt.TimeOutValue, psp->swb.spt.SenseInfoOffset);
#else
        fprintf(stderr, "    TimeOutValue=%lu DataBufferOffset=%lu "
                "SenseInfoOffset=%lu\n", psp->swb.spt.TimeOutValue,
                psp->swb.spt.DataBufferOffset, psp->swb.spt.SenseInfoOffset);
#endif
    }
#ifdef WIN32_SPT_DIRECT
    psp->swb.spt.DataBuffer = psp->dxferp;
    status = DeviceIoControl(shp->fh, IOCTL_SCSI_PASS_THROUGH_DIRECT,
                            &psp->swb,
                            sizeof(psp->swb),
                            &psp->swb,
                            sizeof(psp->swb),
                            &returned,
                            NULL);
#else
    if ((psp->dxfer_len > 0) && (SCSI_IOCTL_DATA_OUT == psp->swb.spt.DataIn))
        memcpy(psp->swb.ucDataBuf, psp->dxferp, psp->dxfer_len);
    status = DeviceIoControl(shp->fh, IOCTL_SCSI_PASS_THROUGH,
                            &psp->swb,
                            sizeof(psp->swb),
                            &psp->swb,
                            sizeof(psp->swb),
                            &returned,
                            NULL);
#endif
    if (! status) {
        psp->transport_err = GetLastError();
        if (verbose)
            fprintf(sg_warnings_strm, "Windows DeviceIoControl error=%d\n",
                    psp->transport_err);
        psp->os_err = EIO;
        return 0;       /* let app find transport error */
    }
#ifndef WIN32_SPT_DIRECT
    if ((psp->dxfer_len > 0) && (SCSI_IOCTL_DATA_IN == psp->swb.spt.DataIn))
        memcpy(psp->dxferp, psp->swb.ucDataBuf, psp->dxfer_len);
#endif

    psp->scsi_status = psp->swb.spt.ScsiStatus;
    if ((SAM_STAT_CHECK_CONDITION == psp->scsi_status) ||
        (SAM_STAT_COMMAND_TERMINATED == psp->scsi_status))
        memcpy(psp->sensep, psp->swb.ucSenseBuf, psp->sense_len);
    else
        psp->sense_len = 0;
    psp->sense_resid = 0;
    if ((psp->dxfer_len > 0) && (psp->swb.spt.DataTransferLength > 0))
        psp->resid = psp->dxfer_len - psp->swb.spt.DataTransferLength;
    else
        psp->resid = 0;

    return 0;
}

int
get_scsi_pt_result_category(const struct sg_pt_base * vp)
{
    const struct sg_pt_win32_scsi * psp = vp->implp;

    if (psp->transport_err)     /* give transport error highest priority */
        return SCSI_PT_RESULT_TRANSPORT_ERR;
    else if (psp->os_err)
        return SCSI_PT_RESULT_OS_ERR;
    else if ((SAM_STAT_CHECK_CONDITION == psp->scsi_status) ||
             (SAM_STAT_COMMAND_TERMINATED == psp->scsi_status))
        return SCSI_PT_RESULT_SENSE;
    else if (psp->scsi_status)
        return SCSI_PT_RESULT_STATUS;
    else
        return SCSI_PT_RESULT_GOOD;
}

int
get_scsi_pt_resid(const struct sg_pt_base * vp)
{
    const struct sg_pt_win32_scsi * psp = vp->implp;

    return psp->resid;
}

int
get_scsi_pt_status_response(const struct sg_pt_base * vp)
{
    const struct sg_pt_win32_scsi * psp = vp->implp;

    return psp->scsi_status;
}

int
get_scsi_pt_sense_len(const struct sg_pt_base * vp)
{
    const struct sg_pt_win32_scsi * psp = vp->implp;
    int len;

    len = psp->sense_len - psp->sense_resid;
    return (len > 0) ? len : 0;
}

int
get_scsi_pt_duration_ms(const struct sg_pt_base * vp __attribute__ ((unused)))
{
    // const struct sg_pt_freebsd_scsi * psp = vp->implp;

    return -1;
}

int
get_scsi_pt_transport_err(const struct sg_pt_base * vp)
{
    const struct sg_pt_win32_scsi * psp = vp->implp;

    return psp->transport_err;
}

int
get_scsi_pt_os_err(const struct sg_pt_base * vp)
{
    const struct sg_pt_win32_scsi * psp = vp->implp;

    return psp->os_err;
}


char *
get_scsi_pt_transport_err_str(const struct sg_pt_base * vp, int max_b_len,
                              char * b)
{
    struct sg_pt_win32_scsi * psp = (struct sg_pt_win32_scsi *)vp->implp;
    LPVOID lpMsgBuf;
    int k, num, ch;

    if (max_b_len < 2) {
        if (1 == max_b_len)
            b[0] = '\0';
        return b;
    }
    memset(b, 0, max_b_len);
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER |
        FORMAT_MESSAGE_FROM_SYSTEM,
        NULL,
        psp->transport_err,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR) &lpMsgBuf,
        0, NULL );
    num = lstrlen((LPCTSTR)lpMsgBuf);
    if (num < 1)
        return b;
    num = (num < max_b_len) ? num : (max_b_len - 1);
    for (k = 0; k < num; ++k) {
        ch = *((LPCTSTR)lpMsgBuf + k);
        if ((ch >= 0x0) && (ch < 0x7f))
            b[k] = ch & 0x7f;
        else
            b[k] = '?';
    }
    return b;
}

char *
get_scsi_pt_os_err_str(const struct sg_pt_base * vp, int max_b_len, char * b)
{
    const struct sg_pt_win32_scsi * psp = vp->implp;
    const char * cp;

    cp = safe_strerror(psp->os_err);
    strncpy(b, cp, max_b_len);
    if ((int)strlen(cp) >= max_b_len)
        b[max_b_len - 1] = '\0';
    return b;
}
