/*
 * A utility program originally written for the Linux OS SCSI subsystem.
 *
 * Copyright (C) 2000-2021 Ingo van Lil <inguin@gmx.de>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * This program can be used to send raw SCSI commands (with an optional
 * data phase) through a Generic SCSI interface.
 */

#define _XOPEN_SOURCE 600       /* clear up posix_memalign() warning */

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdbool.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <getopt.h>
#include <inttypes.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "sg_lib.h"
#include "sg_pt.h"
#include "sg_pt_nvme.h"
#include "sg_pr2serr.h"
#include "sg_unaligned.h"

#define SG_RAW_VERSION "0.4.37 (2021-06-01)"

#define DEFAULT_TIMEOUT 20
#define MIN_SCSI_CDBSZ 6
#define MAX_SCSI_CDBSZ 260
#define MAX_SCSI_DXLEN (1024 * 1024)

#define NVME_ADDR_DATA_IN  0xfffffffffffffffe
#define NVME_ADDR_DATA_OUT 0xfffffffffffffffd
#define NVME_DATA_LEN_DATA_IN  0xfffffffe
#define NVME_DATA_LEN_DATA_OUT 0xfffffffd

static struct option long_options[] = {
    { "binary",  no_argument,       NULL, 'b' },
    { "cmdfile", required_argument, NULL, 'c' },
    { "cmdset",  required_argument, NULL, 'C' },
    { "enumerate", no_argument,     NULL, 'e' },
    { "help",    no_argument,       NULL, 'h' },
    { "infile",  required_argument, NULL, 'i' },
    { "skip",    required_argument, NULL, 'k' },
    { "nosense", no_argument,       NULL, 'n' },
    { "nvm",     no_argument,       NULL, 'N' },
    { "outfile", required_argument, NULL, 'o' },
    { "raw",     no_argument,       NULL, 'w' },
    { "request", required_argument, NULL, 'r' },
    { "readonly", no_argument,      NULL, 'R' },
    { "scan",    required_argument, NULL, 'Q' },
    { "send",    required_argument, NULL, 's' },
    { "timeout", required_argument, NULL, 't' },
    { "verbose", no_argument,       NULL, 'v' },
    { "version", no_argument,       NULL, 'V' },
    { 0, 0, 0, 0 }
};

struct opts_t {
    bool cmdfile_given;
    bool do_datain;
    bool datain_binary;
    bool do_dataout;
    bool do_enumerate;
    bool no_sense;
    bool do_nvm;     /* the NVMe command set: NVM containing its READ+WRITE */
    bool do_help;
    bool verbose_given;
    bool version_given;
    int cdb_length;
    int cmdset;
    int datain_len;
    int dataout_len;
    int timeout;
    int raw;
    int readonly;
    int scan_first;
    int scan_last;
    int verbose;
    off_t dataout_offset;
    uint8_t cdb[MAX_SCSI_CDBSZ];        /* might be NVMe command (64 byte) */
    const char *cmd_file;
    const char *datain_file;
    const char *dataout_file;
    char *device_name;
};


static void
pr_version()
{
    pr2serr("sg_raw " SG_RAW_VERSION "\n"
            "Copyright (C) 2007-2021 Ingo van Lil <inguin@gmx.de>\n"
            "This is free software.  You may redistribute copies of it "
            "under the terms of\n"
            "the GNU General Public License "
            "<https://www.gnu.org/licenses/gpl.html>.\n"
            "There is NO WARRANTY, to the extent permitted by law.\n");
}

static void
usage()
{
    pr2serr("Usage: sg_raw [OPTION]* DEVICE [CDB0 CDB1 ...]\n"
            "\n"
            "Options:\n"
            "  --binary|-b            Dump data in binary form, even when "
            "writing to\n"
            "                         stdout\n"
            "  --cmdfile=CF|-c CF     CF is file containing command in hex "
            "bytes\n"
            "  --cmdset=CS|-C CS      CS is 0 (def) heuristic chooses "
            "command set;\n"
            "                         1: force SCSI; 2: force NVMe\n"
            "  --enumerate|-e         Decodes cdb name then exits; requires "
            "DEVICE but\n"
            "                         ignores it\n"
            "  --help|-h              Show this message and exit\n"
            "  --infile=IFILE|-i IFILE    Read binary data to send (i.e. "
            "data-out)\n"
            "                             from IFILE (default: stdin)\n"
            "  --nosense|-n           Don't display sense information\n"
            "  --nvm|-N               command is for NVM command set (e.g. "
            "Read);\n"
            "                         default, if NVMe fd, Admin command "
            "set\n"
            "  --outfile=OFILE|-o OFILE    Write binary data from device "
            "(i.e. data-in)\n"
            "                              to OFILE (def: hexdump to "
            "stdout)\n"
            "  --raw|-w               interpret CF (command file) as "
            "binary (def:\n"
            "                         interpret as ASCII hex)\n"
            "  --readonly|-R          Open DEVICE read-only (default: "
            "read-write)\n"
            "  --request=RLEN|-r RLEN    Request up to RLEN bytes of data "
            "(data-in)\n"
            "  --scan=FO,LO|-Q FO,LO    scan command set from FO (first "
            "opcode)\n"
            "                           to LO (last opcode) inclusive. Uses "
            "given\n"
            "                           command bytes, varying the opcode\n"
            "  --send=SLEN|-s SLEN    Send SLEN bytes of data (data-out)\n"
            "  --skip=KLEN|-k KLEN    Skip the first KLEN bytes when "
            "reading\n"
            "                         data to send (default: 0)\n"
            "  --timeout=SECS|-t SECS    Timeout in seconds (default: 20)\n"
            "  --verbose|-v           Increase verbosity\n"
            "  --version|-V           Show version information and exit\n"
            "\n"
            "Between 6 and 260 command bytes (two hex digits each) can be "
            "specified\nand will be sent to DEVICE. Lengths RLEN, SLEN and "
            "KLEN are decimal by\ndefault. Bidirectional commands "
            "accepted.\n\nSimple example: Perform INQUIRY on /dev/sg0:\n"
            "  sg_raw -r 1k /dev/sg0 12 00 00 00 60 00\n");
}

static int
parse_cmd_line(struct opts_t * op, int argc, char *argv[])
{
    while (1) {
        int c, n;
        const char * cp;

        c = getopt_long(argc, argv, "bc:C:ehi:k:nNo:Q:r:Rs:t:vVw",
                        long_options, NULL);
        if (c == -1)
            break;

        switch (c) {
        case 'b':
            op->datain_binary = true;
            break;
        case 'c':
            op->cmd_file = optarg;
            op->cmdfile_given = true;
            break;
        case 'C':
            n = sg_get_num(optarg);
            if ((n < 0) || (n > 2)) {
                pr2serr("Invalid argument to --cmdset= expect 0, 1 or 2\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            op->cmdset = n;
            break;
        case 'e':
            op->do_enumerate = true;
            break;
        case 'h':
        case '?':
            op->do_help = true;
            return 0;
        case 'i':
            if (op->dataout_file) {
                pr2serr("Too many '--infile=' options\n");
                return SG_LIB_CONTRADICT;
            }
            op->dataout_file = optarg;
            break;
        case 'k':
            n = sg_get_num(optarg);
            if (n < 0) {
                pr2serr("Invalid argument to '--skip'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            op->dataout_offset = n;
            break;
        case 'n':
            op->no_sense = true;
            break;
        case 'N':
            op->do_nvm = true;
            break;
        case 'o':
            if (op->datain_file) {
                pr2serr("Too many '--outfile=' options\n");
                return SG_LIB_CONTRADICT;
            }
            op->datain_file = optarg;
            break;
        case 'Q':       /* --scan=FO,LO */
            cp = strchr(optarg, ',');
            if (NULL == cp) {
                pr2serr("--scan= expects two numbers, comma separated\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            n = sg_get_num(optarg);
            if ((n < 0) || (n > 255)) {
                pr2serr("Invalid first number to --scan= expect 0 to 255\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            op->scan_first = n;
            n = sg_get_num(cp + 1);
            if ((n < 0) || (n > 255)) {
                pr2serr("Invalid second number to --scan= expect 0 to 255\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            op->scan_last = n;
            if (op->scan_first >= n)
                pr2serr("Warning: scan range degenerate, ignore\n");
            break;
        case 'r':
            op->do_datain = true;
            n = sg_get_num(optarg);
            if (n < 0 || n > MAX_SCSI_DXLEN) {
                pr2serr("Invalid argument to '--request'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            op->datain_len = n;
            break;
        case 'R':
            ++op->readonly;
            break;
        case 's':
            op->do_dataout = true;
            n = sg_get_num(optarg);
            if (n < 0 || n > MAX_SCSI_DXLEN) {
                pr2serr("Invalid argument to '--send'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            op->dataout_len = n;
            break;
        case 't':
            n = sg_get_num(optarg);
            if (n < 0) {
                pr2serr("Invalid argument to '--timeout'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            op->timeout = n;
            break;
        case 'v':
            op->verbose_given = true;
            ++op->verbose;
            break;
        case 'V':
            op->version_given = true;
            break;
        case 'w':       /* -r and -R already in use, this is --raw */
            ++op->raw;
            break;
        default:
            return SG_LIB_SYNTAX_ERROR;
        }
    }

    if (op->version_given
#ifdef DEBUG
        && ! op->verbose_given
#endif
       )
        return 0;

    if (optind >= argc) {
        pr2serr("No device specified\n");
        return SG_LIB_SYNTAX_ERROR;
    }
    op->device_name = argv[optind];
    ++optind;

    while (optind < argc) {
        char *opt = argv[optind++];
        char *endptr;
        int cmd = strtol(opt, &endptr, 16);
        if (*opt == '\0' || *endptr != '\0' || cmd < 0x00 || cmd > 0xff) {
            pr2serr("Invalid command byte '%s'\n", opt);
            return SG_LIB_SYNTAX_ERROR;
        }

        if (op->cdb_length > MAX_SCSI_CDBSZ) {
            pr2serr("CDB too long (max. %d bytes)\n", MAX_SCSI_CDBSZ);
            return SG_LIB_SYNTAX_ERROR;
        }
        op->cdb[op->cdb_length] = cmd;
        ++op->cdb_length;
    }

    if (op->cmdfile_given) {
        int err;

        err = sg_f2hex_arr(op->cmd_file, (op->raw > 0) /* as_binary */,
                           false /* no_space */, op->cdb, &op->cdb_length,
                           MAX_SCSI_CDBSZ);
        if (err) {
            pr2serr("Unable to parse: %s as %s\n", op->cmd_file,
                    (op->raw > 0) ? "binary" : "hex");
            return SG_LIB_SYNTAX_ERROR;
        }
        if (op->verbose > 2) {
            pr2serr("Read %d from %s . They are in hex:\n", op->cdb_length,
                    op->cmd_file);
            hex2stderr(op->cdb, op->cdb_length, -1);
        }
    }
    if (op->cdb_length < MIN_SCSI_CDBSZ) {
        pr2serr("CDB too short (min. %d bytes)\n", MIN_SCSI_CDBSZ);
        return SG_LIB_SYNTAX_ERROR;
    }
    if (op->do_enumerate || (op->verbose > 1)) {
        bool is_scsi_cdb = sg_is_scsi_cdb(op->cdb, op->cdb_length);
        int sa;
        char b[80];

        if ((1 == op->cmdset) && !is_scsi_cdb) {
            is_scsi_cdb = true;
            if (op->verbose > 3)
                printf(">>> overriding cmdset guess to SCSI\n");
        }
        if ((2 == op->cmdset) && is_scsi_cdb) {
            is_scsi_cdb = false;
            if (op->verbose > 3)
                printf(">>> overriding cmdset guess to NVMe\n");
        }
        if (is_scsi_cdb) {
            if (op->cdb_length > 16) {
                sa = sg_get_unaligned_be16(op->cdb + 8);
                if ((0x7f != op->cdb[0]) && (0x7e != op->cdb[0]))
                    printf(">>> Unlikely to be SCSI CDB since all over 16 "
                           "bytes long should\n>>> start with 0x7f or "
                           "0x7e\n");
            } else
                sa = op->cdb[1] & 0x1f;
            sg_get_opcode_sa_name(op->cdb[0], sa, 0, sizeof(b), b);
            printf("Attempt to decode cdb name: %s\n", b);
        } else
            printf(">>> Seems to be NVMe %s command\n",
                   sg_get_nvme_opcode_name(op->cdb[0], ! op->do_nvm,
                                           sizeof(b), b));
    }
    return 0;
}

static int
skip(int fd, off_t offset)
{
    int err;
    off_t remain;
    char buffer[512];

    if (lseek(fd, offset, SEEK_SET) >= 0)
        return 0;

    // lseek failed; fall back to reading and discarding data
    remain = offset;
    while (remain > 0) {
        ssize_t amount, done;
        amount = (remain < (off_t)sizeof(buffer)) ? remain
                                         : (off_t)sizeof(buffer);
        done = read(fd, buffer, amount);
        if (done < 0) {
            err = errno;
            perror("Error reading input data to skip");
            return sg_convert_errno(err);
        } else if (done == 0) {
            pr2serr("EOF on input file/stream\n");
            return SG_LIB_FILE_ERROR;
        } else
            remain -= done;
    }
    return 0;
}

static uint8_t *
fetch_dataout(struct opts_t * op, uint8_t ** free_buf, int * errp)
{
    bool ok = false;
    int fd, len, tot_len, boff, err;
    uint8_t *buf = NULL;

    *free_buf = NULL;
    if (errp)
        *errp = 0;
    if (op->dataout_file) {
        fd = open(op->dataout_file, O_RDONLY);
        if (fd < 0) {
            err = errno;
            if (errp)
                *errp = sg_convert_errno(err);
            perror(op->dataout_file);
            goto bail;
        }
    } else
        fd = STDIN_FILENO;
    if (sg_set_binary_mode(fd) < 0) {
        err = errno;
        if (errp)
            *errp = err;
        perror("sg_set_binary_mode");
        goto bail;
    }

    if (op->dataout_offset > 0) {
        err = skip(fd, op->dataout_offset);
        if (err != 0) {
            if (errp)
                *errp = err;
            goto bail;
        }
    }

    tot_len = op->dataout_len;
    buf = sg_memalign(tot_len, 0 /* page_size */, free_buf, false);
    if (buf == NULL) {
        pr2serr("sg_memalign: failed to get %d bytes of memory\n", tot_len);
        if (errp)
            *errp = sg_convert_errno(ENOMEM);
        goto bail;
    }

    for (boff = 0; boff < tot_len; boff += len) {
        len = read(fd, buf + boff , tot_len - boff);
        if (len < 0) {
            err = errno;
            if (errp)
                *errp = sg_convert_errno(err);
            perror("Failed to read input data");
            goto bail;
        } else if (0 == len) {
            if (errp)
                *errp = SG_LIB_FILE_ERROR;
            pr2serr("EOF on input file/stream at buffer offset %d\n", boff);
            goto bail;
        }
    }
    ok = true;

bail:
    if (fd >= 0 && fd != STDIN_FILENO)
        close(fd);
    if (! ok) {
        if (*free_buf) {
            free(*free_buf);
            *free_buf = NULL;
        }
        return NULL;
    }
    return buf;
}

static int
write_dataout(const char *filename, uint8_t *buf, int len)
{
    int ret = SG_LIB_FILE_ERROR;
    int fd;

    if ((filename == NULL) ||
        ((1 == strlen(filename)) && ('-' == filename[0])))
        fd = STDOUT_FILENO;
    else {
        fd = creat(filename, 0666);
        if (fd < 0) {
            ret = sg_convert_errno(errno);
            perror(filename);
            goto bail;
        }
    }
    if (sg_set_binary_mode(fd) < 0) {
        perror("sg_set_binary_mode");
        goto bail;
    }

    if (write(fd, buf, len) != len) {
        ret = sg_convert_errno(errno);
        perror(filename ? filename : "stdout");
        goto bail;
    }

    ret = 0;

bail:
    if (fd >= 0 && fd != STDOUT_FILENO)
        close(fd);
    return ret;
}


int
main(int argc, char *argv[])
{
    bool is_scsi_cdb = true;
    bool do_scan = false;
    int ret = 0;
    int err = 0;
    int res_cat, status, s_len, k, ret2;
    int sg_fd = -1;
    uint16_t sct_sc;
    uint32_t result;
    struct sg_pt_base *ptvp = NULL;
    uint8_t sense_buffer[32];
    uint8_t * dinp = NULL;
    uint8_t * doutp = NULL;
    uint8_t * free_buf_out = NULL;
    uint8_t * wrkBuf = NULL;
    struct opts_t opts;
    struct opts_t * op;
    char b[128];
    const int b_len = sizeof(b);

    op = &opts;
    memset(op, 0, sizeof(opts));
    op->timeout = DEFAULT_TIMEOUT;
    ret = parse_cmd_line(op, argc, argv);
#ifdef DEBUG
    pr2serr("In DEBUG mode, ");
    if (op->verbose_given && op->version_given) {
        pr2serr("but override: '-vV' given, zero verbose and continue\n");
        op->verbose_given = false;
        op->version_given = false;
        op->verbose = 0;
    } else if (! op->verbose_given) {
        pr2serr("set '-vv'\n");
        op->verbose = 2;
    } else
        pr2serr("keep verbose=%d\n", op->verbose);
#else
    if (op->verbose_given && op->version_given)
        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
#endif
    if (op->version_given) {
        pr_version();
        goto done;
    }

    if (ret != 0) {
        pr2serr("\n");  /* blank line before outputting usage */
        usage();
        goto done;
    } else if (op->do_help) {
        usage();
        goto done;
    } else if (op->do_enumerate)
        goto done;

    sg_fd = scsi_pt_open_device(op->device_name, op->readonly,
                                op->verbose);
    if (sg_fd < 0) {
        pr2serr("%s: %s\n", op->device_name, safe_strerror(-sg_fd));
        ret = sg_convert_errno(-sg_fd);
        goto done;
    }

    ptvp = construct_scsi_pt_obj_with_fd(sg_fd, op->verbose);
    if (ptvp == NULL) {
        pr2serr("construct_scsi_pt_obj_with_fd() failed\n");
        ret = SG_LIB_CAT_OTHER;
        goto done;
    }

    if (op->scan_first < op->scan_last)
        do_scan = true;

and_again:
    if (do_scan) {
        op->cdb[0] = op->scan_first;
        printf("Command bytes in hex:");
        for (k = 0; k < op->cdb_length; ++k)
            printf(" %02x", op->cdb[k]);
        printf("\n");
    }

    is_scsi_cdb = sg_is_scsi_cdb(op->cdb, op->cdb_length);
    if ((1 == op->cmdset) && !is_scsi_cdb)
        is_scsi_cdb = true;
    else if ((2 == op->cmdset) && is_scsi_cdb)
        is_scsi_cdb = false;

    if (op->do_dataout) {
        uint32_t dout_len;

        doutp = fetch_dataout(op, &free_buf_out, &err);
        if (doutp == NULL) {
            ret = err;
            goto done;
        }
        dout_len = op->dataout_len;
        if (op->verbose > 2)
            pr2serr("dxfer_buffer_out=%p, length=%d\n",
                    (void *)doutp, dout_len);
        set_scsi_pt_data_out(ptvp, doutp, dout_len);
        if (op->cmdfile_given) {
            if (NVME_ADDR_DATA_OUT ==
                sg_get_unaligned_le64(op->cdb + SG_NVME_PT_ADDR))
                sg_put_unaligned_le64((uint64_t)(sg_uintptr_t)doutp,
                                      op->cdb + SG_NVME_PT_ADDR);
            if (NVME_DATA_LEN_DATA_OUT ==
                sg_get_unaligned_le32(op->cdb + SG_NVME_PT_DATA_LEN))
                sg_put_unaligned_le32(dout_len,
                                      op->cdb + SG_NVME_PT_DATA_LEN);
        }
    }
    if (op->do_datain) {
        uint32_t din_len = op->datain_len;

        dinp = sg_memalign(din_len, 0 /* page_size */, &wrkBuf, false);
        if (dinp == NULL) {
            pr2serr("sg_memalign: failed to get %d bytes of memory\n",
                    din_len);
            ret = sg_convert_errno(ENOMEM);
            goto done;
        }
        if (op->verbose > 2)
            pr2serr("dxfer_buffer_in=%p, length=%d\n", (void *)dinp, din_len);
        set_scsi_pt_data_in(ptvp, dinp, din_len);
        if (op->cmdfile_given) {
            if (NVME_ADDR_DATA_IN ==
                sg_get_unaligned_le64(op->cdb + SG_NVME_PT_ADDR))
                sg_put_unaligned_le64((uint64_t)(sg_uintptr_t)dinp,
                                      op->cdb + SG_NVME_PT_ADDR);
            if (NVME_DATA_LEN_DATA_IN ==
                sg_get_unaligned_le32(op->cdb + SG_NVME_PT_DATA_LEN))
                sg_put_unaligned_le32(din_len,
                                      op->cdb + SG_NVME_PT_DATA_LEN);
        }
    }
    if (op->verbose) {
        char d[128];

        pr2serr("    %s to send: ", is_scsi_cdb ? "cdb" : "cmd");
        if (is_scsi_cdb) {
            pr2serr("%s\n", sg_get_command_str(op->cdb, op->cdb_length,
                                               op->verbose > 1,
                                               sizeof(d), d));
        } else {        /* If not SCSI cdb then treat as NVMe command */
            pr2serr("\n");
            hex2stderr(op->cdb, op->cdb_length, -1);
            if (op->verbose > 1)
                pr2serr("  Command name: %s\n",
                        sg_get_nvme_opcode_name(op->cdb[0], ! op->do_nvm,
                                                b_len, b));
        }
    }
    set_scsi_pt_cdb(ptvp, op->cdb, op->cdb_length);
    if (op->verbose > 2)
        pr2serr("sense_buffer=%p, length=%d\n", (void *)sense_buffer,
                (int)sizeof(sense_buffer));
    set_scsi_pt_sense(ptvp, sense_buffer, sizeof(sense_buffer));

    if (op->do_nvm)
        ret = do_nvm_pt(ptvp, 0, op->timeout, op->verbose);
    else
        ret = do_scsi_pt(ptvp, -1, op->timeout, op->verbose);
    if (ret > 0) {
        switch (ret) {
        case SCSI_PT_DO_BAD_PARAMS:
            pr2serr("do_scsi_pt: bad pass through setup\n");
            ret = SG_LIB_CAT_OTHER;
            break;
        case SCSI_PT_DO_TIMEOUT:
            pr2serr("do_scsi_pt: timeout\n");
            ret = SG_LIB_CAT_TIMEOUT;
            break;
        case SCSI_PT_DO_NVME_STATUS:
            sct_sc = (uint16_t)get_scsi_pt_status_response(ptvp);
            pr2serr("NVMe Status: %s [0x%x]\n",
                    sg_get_nvme_cmd_status_str(sct_sc, b_len, b), sct_sc);
            if (op->verbose) {
                result = get_pt_result(ptvp);
                pr2serr("NVMe Result=0x%x\n", result);
                s_len = get_scsi_pt_sense_len(ptvp);
                if ((op->verbose > 1) && (s_len > 0)) {
                    pr2serr("NVMe completion queue 4 DWords (as byte "
                            "string):\n");
                    hex2stderr(sense_buffer, s_len, -1);
                }
            }
            break;
        case SCSI_PT_DO_NOT_SUPPORTED:
            pr2serr("do_scsi_pt: not supported\n");
            ret = SG_LIB_CAT_TIMEOUT;
            break;
        default:
            pr2serr("do_scsi_pt: unknown error: %d\n", ret);
            ret = SG_LIB_CAT_OTHER;
            break;
        }
        goto done;
    } else if (ret < 0) {
        k = -ret;
        pr2serr("do_scsi_pt: %s\n", safe_strerror(k));
        err = get_scsi_pt_os_err(ptvp);
        if ((err != 0) && (err != k))
            pr2serr("    ... or perhaps: %s\n", safe_strerror(err));
        ret = sg_convert_errno(err);
        goto done;
    }

    s_len = get_scsi_pt_sense_len(ptvp);
    if (is_scsi_cdb) {
        res_cat = get_scsi_pt_result_category(ptvp);
        switch (res_cat) {
        case SCSI_PT_RESULT_GOOD:
            ret = 0;
            break;
        case SCSI_PT_RESULT_SENSE:
            ret = sg_err_category_sense(sense_buffer, s_len);
            break;
        case SCSI_PT_RESULT_TRANSPORT_ERR:
            get_scsi_pt_transport_err_str(ptvp, b_len, b);
            pr2serr(">>> transport error: %s\n", b);
            ret = SG_LIB_CAT_OTHER;
            break;
        case SCSI_PT_RESULT_OS_ERR:
            get_scsi_pt_os_err_str(ptvp, b_len, b);
            pr2serr(">>> os error: %s\n", b);
            ret = SG_LIB_CAT_OTHER;
            break;
        default:
            pr2serr(">>> unknown pass through result category (%d)\n",
                    res_cat);
            ret = SG_LIB_CAT_OTHER;
            break;
        }

        status = get_scsi_pt_status_response(ptvp);
        pr2serr("SCSI Status: ");
        sg_print_scsi_status(status);
        pr2serr("\n\n");
        if ((SAM_STAT_CHECK_CONDITION == status) && (! op->no_sense)) {
            if (0 == s_len)
                pr2serr(">>> Strange: status is CHECK CONDITION but no Sense "
                        "Information\n");
            else {
                pr2serr("Sense Information:\n");
                sg_print_sense(NULL, sense_buffer, s_len, (op->verbose > 0));
                pr2serr("\n");
            }
        }
        if (SAM_STAT_RESERVATION_CONFLICT == status)
            ret = SG_LIB_CAT_RES_CONFLICT;
    } else {    /* NVMe command */
        result = get_pt_result(ptvp);
        pr2serr("NVMe Result=0x%x\n", result);
        if (op->verbose && (s_len > 0)) {
            pr2serr("NVMe completion queue 4 DWords (as byte string):\n");
            hex2stderr(sense_buffer, s_len, -1);
        }
    }

    if (op->do_datain) {
        int data_len = op->datain_len - get_scsi_pt_resid(ptvp);

        if (ret && !(SG_LIB_CAT_RECOVERED == ret ||
                     SG_LIB_CAT_NO_SENSE == ret))
            pr2serr("Error %d occurred, no data received\n", ret);
        else if (data_len == 0) {
            pr2serr("No data received\n");
        } else {
            if (op->datain_file == NULL && !op->datain_binary) {
                pr2serr("Received %d bytes of data:\n", data_len);
                hex2stderr(dinp, data_len, 0);
            } else {
                const char * cp = "stdout";

                if (op->datain_file &&
                    ! ((1 == strlen(op->datain_file)) &&
                       ('-' == op->datain_file[0])))
                    cp = op->datain_file;
                pr2serr("Writing %d bytes of data to %s\n", data_len, cp);
                ret2 = write_dataout(op->datain_file, dinp,
                                     data_len);
                if (0 != ret2) {
                    if (0 == ret)
                        ret = ret2;
                    goto done;
                }
            }
        }
    }

done:
    if (do_scan) {
        ++op->scan_first;
        if (op->scan_first <= op->scan_last) {
            clear_scsi_pt_obj(ptvp);
            goto and_again;
        }
    }

    if (op->verbose && is_scsi_cdb) {
        sg_get_category_sense_str(ret, b_len, b, op->verbose - 1);
        pr2serr("%s\n", b);
    }
    if (wrkBuf)
        free(wrkBuf);
    if (free_buf_out)
        free(free_buf_out);
    if (ptvp)
        destruct_scsi_pt_obj(ptvp);
    if (sg_fd >= 0)
        scsi_pt_close_device(sg_fd);
    return ret >= 0 ? ret : SG_LIB_CAT_OTHER;
}
