/*
 * Copyright (c) 2004-2020 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.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <getopt.h>
#define __STDC_FORMAT_MACROS 1
#include <inttypes.h>

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "sg_lib.h"
#include "sg_cmds_basic.h"
#include "sg_cmds_extra.h"
#include "sg_pr2serr.h"

/* A utility program for the Linux OS SCSI subsystem.
 *
 * This program issues the SCSI VERIFY(10) or VERIFY(16) command to the given
 * SCSI block device.
 *
 * N.B. This utility should, but doesn't, check the logical block size with
 * the SCSI READ CAPACITY command. It is up to the user to make sure that
 * the count of blocks requested and the number of bytes transferred (when
 * BYTCHK>0) are "in sync". That caclculation is somewhat complicated by
 * the possibility of protection data (DIF).
 */

static const char * version_str = "1.27 20201029";    /* sbc4r17 */

#define ME "sg_verify: "

#define EBUFF_SZ 256


static struct option long_options[] = {
        {"0", no_argument, 0, '0'},
        {"16", no_argument, 0, 'S'},
        {"bpc", required_argument, 0, 'b'},
        {"bytchk", required_argument, 0, 'B'},  /* 4 backward compatibility */
        {"count", required_argument, 0, 'c'},
        {"dpo", no_argument, 0, 'd'},
        {"ebytchk", required_argument, 0, 'E'}, /* extended bytchk (2 bits) */
        {"group", required_argument, 0, 'g'},
        {"help", no_argument, 0, 'h'},
        {"in", required_argument, 0, 'i'},
        {"lba", required_argument, 0, 'l'},
        {"nbo", required_argument, 0, 'n'},     /* misspelling, legacy */
        {"ndo", required_argument, 0, 'n'},
        {"quiet", no_argument, 0, 'q'},
        {"readonly", no_argument, 0, 'r'},
        {"verbose", no_argument, 0, 'v'},
        {"version", no_argument, 0, 'V'},
        {"vrprotect", required_argument, 0, 'P'},
        {0, 0, 0, 0},
};

static void
usage()
{
    pr2serr("Usage: sg_verify [--0] [--16] [--bpc=BPC] [--count=COUNT] "
            "[--dpo]\n"
            "                 [--ebytchk=BCH] [--ff] [--group=GN] [--help] "
            "[--in=IF]\n"
            "                 [--lba=LBA] [--ndo=NDO] [--quiet] "
            "[--readonly]\n"
            "                 [--verbose] [--version] [--vrprotect=VRP] "
            "DEVICE\n"
            "  where:\n"
            "    --0|-0              fill buffer with zeros (don't read "
            "stdin)\n"
            "    --16|-S             use VERIFY(16) (def: use "
            "VERIFY(10) )\n"
            "    --bpc=BPC|-b BPC    max blocks per verify command "
            "(def: 128)\n"
            "    --count=COUNT|-c COUNT    count of blocks to verify "
            "(def: 1).\n"
            "    --dpo|-d            disable page out (cache retention "
            "priority)\n"
            "    --ebytchk=BCH|-E BCH    sets BYTCHK value, either 1, 2 "
            "or 3 (def: 0).\n"
            "                            BCH overrides BYTCHK=1 set by "
            "'--ndo='. If\n"
            "                            BCH is 3 then NDO must be the LBA "
            "size\n"
            "                            (plus protection size if DIF "
            "active)\n"
            "    --ff|-f             fill buffer with 0xff bytes (don't read "
            "stdin)\n"
            "    --group=GN|-g GN    set group number field to GN (def: 0)\n"
            "    --help|-h           print out usage message\n"
            "    --in=IF|-i IF       input from file called IF (def: "
            "stdin)\n"
            "                        only active if --ebytchk=BCH given\n"
            "    --lba=LBA|-l LBA    logical block address to start "
            "verify (def: 0)\n"
            "    --ndo=NDO|-n NDO    NDO is number of bytes placed in "
            "data-out buffer.\n"
            "                        These are fetched from IF (or "
            "stdin) and used\n"
            "                        to verify the device data against. "
            "Forces\n"
            "                        --bpc=COUNT. Sets BYTCHK (byte check) "
            "to 1\n"
            "    --quiet|-q          suppress miscompare report to stderr, "
            "still\n"
            "                        causes an exit status of 14\n"
            "    --readonly|-r       open DEVICE read-only (def: open it "
            "read-write)\n"
            "    --verbose|-v        increase verbosity\n"
            "    --version|-V        print version string and exit\n"
            "    --vrprotect=VRP|-P VRP    set vrprotect field to VRP "
            "(def: 0)\n\n"
            "Performs one or more SCSI VERIFY(10) or SCSI VERIFY(16) "
            "commands. sbc3r34\nmade the BYTCHK field two bits wide "
            "(it was a single bit).\n");
}

int
main(int argc, char * argv[])
{
    bool bpc_given = false;
    bool dpo = false;
    bool ff_given = false;
    bool got_stdin = false;
    bool quiet = false;
    bool readonly = false;
    bool verbose_given = false;
    bool verify16 = false;
    bool version_given = false;
    bool zero_given = false;
    int res, c, num, nread, infd;
    int sg_fd = -1;
    int bpc = 128;
    int group = 0;
    int bytchk = 0;
    int ndo = 0;        /* number of bytes in data-out buffer */
    int verbose = 0;
    int ret = 0;
    int vrprotect = 0;
    unsigned int info = 0;
    int64_t count = 1;
    int64_t ll;
    int64_t orig_count;
    uint64_t info64 = 0;
    uint64_t lba = 0;
    uint64_t orig_lba;
    uint8_t * ref_data = NULL;
    uint8_t * free_ref_data = NULL;
    const char * device_name = NULL;
    const char * file_name = NULL;
    const char * vc;
    char ebuff[EBUFF_SZ];

    while (1) {
        int option_index = 0;

        c = getopt_long(argc, argv, "0b:B:c:dE:fg:hi:l:n:P:qrSvV",
                        long_options, &option_index);
        if (c == -1)
            break;

        switch (c) {
        case '0':
            zero_given = true;
            break;
        case 'b':
            bpc = sg_get_num(optarg);
            if (bpc < 1) {
                pr2serr("bad argument to '--bpc'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            bpc_given = true;
            break;
        case 'c':
            count = sg_get_llnum(optarg);
            if (count < 0) {
                pr2serr("bad argument to '--count'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'd':
            dpo = true;
            break;
        case 'E':
            bytchk = sg_get_num(optarg);
            if ((bytchk < 0) || (bytchk > 3)) {
                pr2serr("bad argument to '--ebytchk'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'f':
            ff_given = true;
            break;
        case 'g':
            group = sg_get_num(optarg);
            if ((group < 0) || (group > 63)) {
                pr2serr("bad argument to '--group'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'h':
        case '?':
            usage();
            return 0;
        case 'i':
            file_name = optarg;
            break;
        case 'l':
            ll = sg_get_llnum(optarg);
            if (-1 == ll) {
                pr2serr("bad argument to '--lba'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            lba = (uint64_t)ll;
            break;
        case 'n':       /* number of bytes in data-out buffer */
        case 'B':       /* undocumented, old --bytchk=NDO option */
            ndo = sg_get_num(optarg);
            if (ndo < 1) {
                pr2serr("bad argument to '--ndo'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'P':
            vrprotect = sg_get_num(optarg);
            if (-1 == vrprotect) {
                pr2serr("bad argument to '--vrprotect'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            if ((vrprotect < 0) || (vrprotect > 7)) {
                pr2serr("'--vrprotect' requires a value from 0 to 7 "
                        "(inclusive)\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'q':
            quiet = true;
            break;
        case 'r':
            readonly = true;
            break;
        case 'S':
            verify16 = true;
            break;
        case 'v':
            verbose_given = true;
            ++verbose;
            break;
        case 'V':
            version_given = true;
            break;
        default:
            pr2serr("unrecognised option code 0x%x ??\n", c);
            usage();
            return SG_LIB_SYNTAX_ERROR;
        }
    }
    if (optind < argc) {
        if (NULL == device_name) {
            device_name = argv[optind];
            ++optind;
        }
        if (optind < argc) {
            for (; optind < argc; ++optind)
                pr2serr("Unexpected extra argument: %s\n", argv[optind]);
            usage();
            return SG_LIB_SYNTAX_ERROR;
        }
    }
#ifdef DEBUG
    pr2serr("In DEBUG mode, ");
    if (verbose_given && version_given) {
        pr2serr("but override: '-vV' given, zero verbose and continue\n");
        verbose_given = false;
        version_given = false;
        verbose = 0;
    } else if (! verbose_given) {
        pr2serr("set '-vv'\n");
        verbose = 2;
    } else
        pr2serr("keep verbose=%d\n", verbose);
#else
    if (verbose_given && version_given)
        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
#endif
    if (version_given) {
        pr2serr(ME "version: %s\n", version_str);
        return 0;
    }

    if (ndo > 0) {
        if (0 == bytchk)
            bytchk = 1;
        if (bpc_given && (bpc != count))
            pr2serr("'bpc' argument ignored, using --bpc=%" PRIu64 "\n",
                    count);
        if (count > 0x7fffffffLL) {
            pr2serr("count exceed 31 bits, way too large\n");
            return SG_LIB_SYNTAX_ERROR;
        }
#if 0
        if ((3 == bytchk) && (1 != count)) {
            pr2serr("count must be 1 when bytchk=3\n");
            return SG_LIB_SYNTAX_ERROR;
        }
        // bpc = (int)count;
#endif
    } else if (bytchk > 0) {
        pr2serr("when the 'ebytchk=BCH' option is given, then '--ndo=NDO' "
                "must also be given\n");
        return SG_LIB_CONTRADICT;
    }
    if ((zero_given || ff_given) && file_name) {
        pr2serr("giving --0 or --ff is not compatible with --if=%s\n",
                file_name);
        return SG_LIB_CONTRADICT;
    }

    if ((bpc > 0xffff) && (! verify16)) {
        pr2serr("'%s' exceeds 65535, so use VERIFY(16)\n",
                (ndo > 0) ? "count" : "bpc");
        verify16 = true;
    }
    if (((lba + count - 1) > 0xffffffffLLU) && (! verify16)) {
        pr2serr("'lba' exceed 32 bits, so use VERIFY(16)\n");
        verify16 = true;
    }
    if ((group > 0) && (! verify16))
        pr2serr("group number ignored with VERIFY(10) command, use the --16 "
                "option\n");

    orig_count = count;
    orig_lba = lba;

    if (ndo > 0) {
        ref_data = (uint8_t *)sg_memalign(ndo, 0, &free_ref_data, verbose > 4);
        if (NULL == ref_data) {
            pr2serr("failed to allocate %d byte buffer\n", ndo);
            ret = sg_convert_errno(ENOMEM);
            goto err_out;
        }
        if (ff_given)
            memset(ref_data, 0xff, ndo);
        if (zero_given || ff_given)
            goto skip;
        if ((NULL == file_name) || (0 == strcmp(file_name, "-"))) {
            got_stdin = true;
            infd = STDIN_FILENO;
            if (sg_set_binary_mode(STDIN_FILENO) < 0)
                perror("sg_set_binary_mode");
        } else {
            if ((infd = open(file_name, O_RDONLY)) < 0) {
                ret = sg_convert_errno(errno);
                snprintf(ebuff, EBUFF_SZ,
                         ME "could not open %s for reading", file_name);
                perror(ebuff);
                goto err_out;
            } else if (sg_set_binary_mode(infd) < 0)
                perror("sg_set_binary_mode");
        }
        if (verbose && got_stdin)
                pr2serr("about to wait on STDIN\n");
        for (nread = 0; nread < ndo; nread += res) {
            res = read(infd, ref_data + nread, ndo - nread);
            if (res <= 0) {
                ret = sg_convert_errno(errno);
                pr2serr("reading from %s failed at file offset=%d\n",
                        (got_stdin ? "stdin" : file_name), nread);
                goto err_out;
            }
        }
        if (! got_stdin)
            close(infd);
    }
skip:
    if (NULL == device_name) {
        pr2serr("missing device name!\n\n");
        usage();
        ret = SG_LIB_SYNTAX_ERROR;
        goto err_out;
    }
    sg_fd = sg_cmds_open_device(device_name, readonly, verbose);
    if (sg_fd < 0) {
        if (verbose)
            pr2serr(ME "open error: %s: %s\n", device_name,
                    safe_strerror(-sg_fd));
        ret = sg_convert_errno(-sg_fd);
        goto err_out;
    }

    vc = verify16 ? "VERIFY(16)" : "VERIFY(10)";
    for (; count > 0; count -= bpc, lba += bpc) {
        num = (count > bpc) ? bpc : count;
        if (verify16)
            res = sg_ll_verify16(sg_fd, vrprotect, dpo, bytchk,
                                 lba, num, group, ref_data,
                                 ndo, &info64, !quiet , verbose);
        else
            res = sg_ll_verify10(sg_fd, vrprotect, dpo, bytchk,
                                 (unsigned int)lba, num, ref_data,
                                 ndo, &info, !quiet, verbose);
        if (0 != res) {
            char b[80];

            ret = res;
            switch (res) {
            case SG_LIB_CAT_ILLEGAL_REQ:
                pr2serr("bad field in %s cdb, near lba=0x%" PRIx64 "\n", vc,
                        lba);
                break;
            case SG_LIB_CAT_MEDIUM_HARD:
                pr2serr("%s medium or hardware error near lba=0x%" PRIx64 "\n",
                        vc, lba);
                break;
            case SG_LIB_CAT_MEDIUM_HARD_WITH_INFO:
                if (verify16)
                    pr2serr("%s medium or hardware error, reported lba=0x%"
                            PRIx64 "\n", vc, info64);
                else
                    pr2serr("%s medium or hardware error, reported lba=0x%x\n",
                            vc, info);
                break;
            case SG_LIB_CAT_MISCOMPARE:
                if ((0 == quiet) || verbose)
                    pr2serr("%s MISCOMPARE: started at LBA 0x%" PRIx64 "\n",
                            vc, lba);
                break;
            default:
                sg_get_category_sense_str(res, sizeof(b), b, verbose);
                pr2serr("%s: %s\n", vc, b);
                pr2serr("    failed near lba=%" PRIu64 " [0x%" PRIx64 "]\n",
                        lba, lba);
                break;
            }
            break;
        }
    }

    if (verbose && (0 == ret) && (orig_count > 1))
        pr2serr("Verified %" PRId64 " [0x%" PRIx64 "] blocks from lba %" PRIu64
                " [0x%" PRIx64 "]\n    without error\n", orig_count,
                (uint64_t)orig_count, orig_lba, orig_lba);

 err_out:
    if (sg_fd >= 0) {
        res = sg_cmds_close_device(sg_fd);
        if (res < 0) {
            pr2serr("close error: %s\n", safe_strerror(-res));
            if (0 == ret)
                ret = sg_convert_errno(-res);
        }
    }
    if (free_ref_data)
        free(free_ref_data);
    if (0 == verbose) {
        if (! sg_if_can2stderr("sg_verify failed: ", ret))
            pr2serr("Some error occurred, try again with '-v' "
                    "or '-vv' for more information\n");
    }
    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
}
