/*
 * Copyright (c) 2004-2023 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 <string.h>
#include <getopt.h>
#include <ctype.h>

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

#include "sg_lib.h"
#include "sg_cmds_basic.h"
#include "sg_unaligned.h"
#include "sg_pr2serr.h"

/* A utility program originally written for the Linux OS SCSI subsystem.
 *
 * This program writes the given mode page contents to the corresponding
 * mode page on the given device.
 */

static const char * version_str = "1.30 20230623";

#define ME "sg_wr_mode: "

#define MX_ALLOC_LEN 2048
#define SHORT_ALLOC_LEN 252

#define EBUFF_SZ 256


static const struct option long_options[] = {
    {"cfile", required_argument, 0, 'C'},
    {"contents", required_argument, 0, 'c'},
    {"dbd", no_argument, 0, 'd'},
    {"force", no_argument, 0, 'f'},
    {"help", no_argument, 0, 'h'},
    {"len", required_argument, 0, 'l'},
    {"mask", required_argument, 0, 'm'},
    {"page", required_argument, 0, 'p'},
    {"raw", no_argument, 0, 'r'},
    {"rtd", no_argument, 0, 'R'},
    {"save", no_argument, 0, 's'},
    {"six", no_argument, 0, '6'},
    {"verbose", no_argument, 0, 'v'},
    {"version", no_argument, 0, 'V'},
    {0, 0, 0, 0},
};


static void
usage()
{
    pr2serr("Usage: sg_wr_mode [--cfile=CF] [--contents=H,H...] [--dbd] "
            "[--force]\n"
            "                  [--help] [--len=10|6] [--mask=M,M...]\n"
            "                  [--page=PG_H[,SPG_H]] [--raw] [--rtd] "
            "[--save]\n"
            "                  [--six] [--verbose] [--version] DEVICE\n"
            "  where:\n"
            "    --cfile=CF | -C CF    contents in a file called CF\n"
            "    --contents=H,H... | -c H,H...    comma separated string "
            "of hex numbers\n"
            "                                     that is mode page contents "
            "to write\n"
            "    --contents=- | -c -   read stdin for mode page contents"
            " to write\n"
            "    --dbd | -d            disable block descriptors (DBD bit"
            " in cdb)\n"
            "    --force | -f          force the contents to be written\n"
            "    --help | -h           print out usage message\n"
            "    --len=10|6 | -l 10|6    use 10 byte (def) or 6 byte "
            "variants of\n"
            "                            SCSI MODE SENSE/SELECT commands\n"
            "    --mask=M,M... | -m M,M...   comma separated "
            "string of hex\n"
            "                                numbers that mask contents"
            " to write\n"
            "    --page=PG_H | -p PG_H     page_code to be written (in hex)\n"
            "    --page=PG_H,SPG_H | -p PG_H,SPG_H    page and subpage code "
            "to be\n"
            "                                         written (in hex)\n"
            "    --raw | -r            contents of CF file decoded as "
            "binary\n"
            "    --rtd | -R            set RTD bit (revert to defaults) in "
            "cdb\n"
            "    --save | -s           set 'save page' (SP) bit; default "
            "don't so\n"
            "                          only 'current' values changed\n"
            "    --six | -6            do SCSI MODE SENSE/SELECT(6) "
            "commands\n"
            "    --verbose | -v        increase verbosity\n"
            "    --version | -V        print version string and exit\n\n"
            "writes given mode page with SCSI MODE SELECT (10 or 6) "
            "command\n");
}


/* Read hex numbers from command line or stdin. On the command line can
 * either be comma or space separated list. Space separated list need to be
 * quoted. For stdin (indicated by *inp=='-') there should be either
 * one entry per line, a comma separated list or space separated list.
 * Returns 0 if ok, or sg3_utils error code if error. */
static int
build_mode_page(const char * inp, bool is_file, bool as_binary,
                uint8_t * mp_arr, int * mp_arr_len, int max_arr_len)
{
    int in_len, k;
    unsigned int h;
    const char * lcp;
    char * cp;
    char * c2p;

    if ((NULL == inp) || (NULL == mp_arr) ||
        (NULL == mp_arr_len))
        return SG_LIB_LOGIC_ERROR;
    lcp = inp;
    in_len = strlen(inp);
    if (0 == in_len)
        *mp_arr_len = 0;
    if (is_file || ('-' == inp[0])) {
        return sg_f2hex_arr(inp, as_binary, false, mp_arr, mp_arr_len,
                            max_arr_len);
    } else {        /* hex string on command line */
        k = strspn(inp, "0123456789aAbBcCdDeEfF, ");
        if (in_len != k) {
            pr2serr("%s: error at pos %d\n", __func__, k + 1);
            return SG_LIB_SYNTAX_ERROR;
        }
        for (k = 0; k < max_arr_len; ++k) {
            if (1 == sscanf(lcp, "%x", &h)) {
                if (h > 0xff) {
                    pr2serr("%s: hex number larger than 0xff at pos %d\n",
                            __func__, (int)(lcp - inp + 1));
                    return SG_LIB_SYNTAX_ERROR;
                }
                mp_arr[k] = h;
                cp = (char *)strchr(lcp, ',');
                c2p = (char *)strchr(lcp, ' ');
                if (NULL == cp)
                    cp = c2p;
                if (NULL == cp)
                    break;
                if (c2p && (c2p < cp))
                    cp = c2p;
                lcp = cp + 1;
            } else {
                pr2serr("%s: error at pos %d\n", __func__,
                        (int)(lcp - inp + 1));
                return SG_LIB_SYNTAX_ERROR;
            }
        }
        *mp_arr_len = k + 1;
        if (k == max_arr_len) {
            pr2serr("%s: array length exceeded\n", __func__);
            return SG_LIB_SYNTAX_ERROR;
        }
    }
    return 0;
}

/* Read hex numbers from command line (comma separated list).
 * Can also be (single) space separated list but needs to be quoted on the
 * command line. Returns 0 if ok, or 1 if error. */
static int
build_mask(const char * inp, uint8_t * mask_arr, int * mask_arr_len,
           int max_arr_len)
{
    int in_len, k;
    unsigned int h;
    const char * lcp;
    char * cp;
    char * c2p;

    if ((NULL == inp) || (NULL == mask_arr) ||
        (NULL == mask_arr_len))
        return 1;
    lcp = inp;
    in_len = strlen(inp);
    if (0 == in_len)
        *mask_arr_len = 0;
    if ('-' == inp[0]) {        /* read from stdin */
        pr2serr("'--mask' does not accept input from stdin\n");
        return 1;
    } else {        /* hex string on command line */
        k = strspn(inp, "0123456789aAbBcCdDeEfF, ");
        if (in_len != k) {
            pr2serr("%s: error at pos %d\n", __func__, k + 1);
            return 1;
        }
        for (k = 0; k < max_arr_len; ++k) {
            if (1 == sscanf(lcp, "%x", &h)) {
                if (h > 0xff) {
                    pr2serr("%s: hex number larger than 0xff at pos %d\n",
                            __func__, (int)(lcp - inp + 1));
                    return 1;
                }
                mask_arr[k] = h;
                cp = (char *)strchr(lcp, ',');
                c2p = (char *)strchr(lcp, ' ');
                if (NULL == cp)
                    cp = c2p;
                if (NULL == cp)
                    break;
                if (c2p && (c2p < cp))
                    cp = c2p;
                lcp = cp + 1;
            } else {
                pr2serr("%s: error at pos %d\n", __func__,
                        (int)(lcp - inp + 1));
                return 1;
            }
        }
        *mask_arr_len = k + 1;
        if (k == max_arr_len) {
            pr2serr("%s: array length exceeded\n", __func__);
            return 1;
        }
    }
    return 0;
}


int
main(int argc, char * argv[])
{
    bool dbd = false;
    bool do_raw = false;
    bool force = false;
    bool got_contents = false;
    bool got_mask = false;
    bool mode_6 = false;        /* so default is mode_10 */
    bool rtd = false;   /* added in spc5r11 */
    bool save = false;
    bool verbose_given = false;
    bool version_given = false;
    int res, c, num, alloc_len, off, pdt, k, md_len, hdr_len, bd_len;
    int mask_in_len;
    int sg_fd = -1;
    int pg_code = -1;
    int sub_pg_code = 0;
    int verbose = 0;
    int read_in_len = 0;
    int ret = 0;
    unsigned u, uu;
    const char * device_name = NULL;
    const char * cfile_arg = NULL;
    const char * contents_arg = NULL;
    uint8_t read_in[MX_ALLOC_LEN];
    uint8_t mask_in[MX_ALLOC_LEN];
    uint8_t ref_md[MX_ALLOC_LEN];
    char ebuff[EBUFF_SZ];
    char errStr[128];
    char b[80];
    struct sg_simple_inquiry_resp inq_data;
    static const int read_in_sz = sizeof(read_in);
    static const int mask_in_sz = sizeof(mask_in);
    static const int ref_md_sz = sizeof(ref_md);

    while (1) {
        int option_index = 0;

        c = getopt_long(argc, argv, "6c:C:dfhl:m:p:rRsvV", long_options,
                        &option_index);
        if (c == -1)
            break;

        switch (c) {
        case '6':
            mode_6 = true;
            break;
        case 'c':
            contents_arg = optarg;
            break;
        case 'C':
            cfile_arg = optarg;
            break;
        case 'd':
            dbd = true;
            break;
        case 'f':
            force = true;
            break;
        case 'h':
        case '?':
            usage();
            return 0;
        case 'l':
            num = sscanf(optarg, "%d", &res);
            if ((1 == num) && ((6 == res) || (10 == res)))
                mode_6 = (6 == res);
            else {
                pr2serr("length (of cdb) must be 6 or 10\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'm':
            memset(mask_in, 0xff, mask_in_sz);
            if (0 != build_mask(optarg, mask_in, &mask_in_len,
                                mask_in_sz)) {
                pr2serr("bad argument to '--mask'\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            got_mask = true;
            break;
        case 'p':
           if (NULL == strchr(optarg, ',')) {
                num = sscanf(optarg, "%x", &u);
                if ((1 != num) || (u > 62)) {
                    pr2serr("Bad hex page code value after '--page' "
                            "switch\n");
                    return SG_LIB_SYNTAX_ERROR;
                }
                pg_code = u;
            } else if (2 == sscanf(optarg, "%x,%x", &u, &uu)) {
                if (uu > 254) {
                    pr2serr("Bad hex sub page code value after '--page' "
                            "switch\n");
                    return SG_LIB_SYNTAX_ERROR;
                }
                pg_code = u;
                sub_pg_code = uu;
            } else {
                pr2serr("Bad hex page code, subpage code sequence after "
                        "'--page' switch\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'r':
            do_raw = true;
            break;
        case 'R':
            rtd = true;
            break;
        case 's':
            save = 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;
        }
    }
    if (cfile_arg || contents_arg) {
        if (cfile_arg && contents_arg) {
            pr2serr("Cannot have both --contents= and --cfile= options\n");
            return SG_LIB_SYNTAX_ERROR;
        }
        memset(read_in, 0, read_in_sz);
        if ((ret = build_mode_page(optarg, !! cfile_arg, do_raw, read_in,
                                   &read_in_len, read_in_sz))) {
            pr2serr("bad argument to '%s'\n", cfile_arg ? "--cfile=" :
                                                          "--contents=");
            return ret;
        }
        got_contents = true;
    }

#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 (NULL == device_name) {
        pr2serr("missing device name!\n\n");
        usage();
        return SG_LIB_SYNTAX_ERROR;
    }
    if ((pg_code < 0) && (! rtd)) {
        pr2serr("need page code (see '--page=')\n\n");
        usage();
        return SG_LIB_SYNTAX_ERROR;
    }
    if (got_mask && force) {
        pr2serr("cannot use both '--force' and '--mask'\n\n");
        usage();
        return SG_LIB_CONTRADICT;
    }

    sg_fd = sg_cmds_open_device(device_name, false /* rw */, 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 fini;
    }
    if (rtd)
        goto revert_to_defaults;

    if (0 == sg_simple_inquiry(sg_fd, &inq_data, false, verbose))
        pdt = inq_data.peripheral_type;
    else
        pdt = PDT_UNKNOWN;

    /* do MODE SENSE to fetch current values */
    memset(ref_md, 0, ref_md_sz);
    snprintf(errStr, sizeof(errStr), "MODE SENSE (%d): ", mode_6 ? 6 : 10);
    alloc_len = mode_6 ? SHORT_ALLOC_LEN : MX_ALLOC_LEN;
    if (mode_6)
        res = sg_ll_mode_sense6(sg_fd, dbd, 0 /*current */, pg_code,
                                sub_pg_code, ref_md, alloc_len, true,
                                verbose);
     else
        res = sg_ll_mode_sense10(sg_fd, false /* llbaa */, dbd,
                                 0 /* current */, pg_code, sub_pg_code,
                                 ref_md, alloc_len, true, verbose);
    ret = res;
    if (res) {
        if (SG_LIB_CAT_INVALID_OP == res)
            pr2serr("%snot supported, try '--len=%d'\n", errStr,
                    (mode_6 ? 10 : 6));
        else {
            sg_get_category_sense_str(res, sizeof(b), b, verbose);
            pr2serr("%s%s\n", errStr, b);
        }
        goto fini;
    }
    off = sg_mode_page_offset(ref_md, alloc_len, mode_6, ebuff, EBUFF_SZ);
    if (off < 0) {
        pr2serr("%s%s\n", errStr, ebuff);
        goto fini;
    }
    md_len = sg_msense_calc_length(ref_md, alloc_len, mode_6, &bd_len);
    if (md_len < 0) {
        pr2serr("%ssg_msense_calc_length() failed\n", errStr);
        goto fini;
    }
    hdr_len = mode_6 ? 4 : 8;
    if (got_contents) {
        if (read_in_len < 2) {
            pr2serr("contents length=%d too short\n", read_in_len);
            goto fini;
        }
        ref_md[0] = 0;  /* mode data length reserved for mode select */
        if (! mode_6)
            ref_md[1] = 0;    /* mode data length reserved for mode select */
        if (0 == pdt)   /* for disks mask out DPOFUA bit */
            ref_md[mode_6 ? 2 : 3] &= 0xef;
        if (md_len > alloc_len) {
            pr2serr("mode data length=%d exceeds allocation length=%d\n",
                    md_len, alloc_len);
            goto fini;
        }
        if (got_mask) {
            for (k = 0; k < (md_len - off); ++k) {
                if ((0x0 == mask_in[k]) || (k > read_in_len))
                   read_in[k] = ref_md[off + k];
                else if (mask_in[k] < 0xff) {
                   c = (ref_md[off + k] & (0xff & ~mask_in[k]));
                   read_in[k] = (c | (read_in[k] & mask_in[k]));
                }
            }
            read_in_len = md_len - off;
        }
        if (! force) {
            if ((! (ref_md[off] & 0x80)) && save) {
                pr2serr("PS bit in existing mode page indicates that it is "
                        "not saveable\n    but '--save' option given\n");
                goto fini;
            }
            read_in[0] &= 0x7f; /* mask out PS bit, reserved in mode select */
            if ((md_len - off) != read_in_len) {
                pr2serr("contents length=%d but reference mode page "
                        "length=%d\n", read_in_len, md_len - off);
                goto fini;
            }
            if (pg_code != (read_in[0] & 0x3f)) {
                pr2serr("contents page_code=0x%x but reference "
                        "page_code=0x%x\n", (read_in[0] & 0x3f), pg_code);
                goto fini;
            }
            if ((read_in[0] & 0x40) != (ref_md[off] & 0x40)) {
                pr2serr("contents flags subpage but reference page does not "
                        "(or vice versa)\n");
                goto fini;
            }
            if ((read_in[0] & 0x40) && (read_in[1] != sub_pg_code)) {
                pr2serr("contents subpage_code=0x%x but reference "
                        "sub_page_code=0x%x\n", read_in[1], sub_pg_code);
                goto fini;
            }
        } else
            md_len = off + read_in_len; /* force length */

        memcpy(ref_md + off, read_in, read_in_len);
        if (mode_6)
            res = sg_ll_mode_select6_v2(sg_fd, true /* PF */, rtd, save,
                                        ref_md, md_len, true, verbose);
        else
            res = sg_ll_mode_select10_v2(sg_fd, true /* PF */, rtd, save,
                                         ref_md, md_len, true, verbose);
        ret = res;
        if (res)
            goto fini;
    } else {
        printf(">>> No contents given, so show current mode page data:\n");
        printf("  header:\n");
        hex2stdout(ref_md, hdr_len, -1);
        if (bd_len) {
            printf("  block descriptor(s):\n");
            hex2stdout(ref_md + hdr_len, bd_len, -1);
        } else
            printf("  << no block descriptors >>\n");
        printf("  mode page:\n");
        hex2stdout(ref_md + off, md_len - off, -1);
    }
    ret = 0;
    goto fini;

revert_to_defaults:
    if (verbose)
        pr2serr("Doing MODE SELECT(%d) with revert to defaults (RTD) set "
                "and SP=%d\n", mode_6 ? 6 : 10, !! save);
    if (mode_6)
        res = sg_ll_mode_select6_v2(sg_fd, false /* PF */, true /* rtd */,
                                    save, NULL, 0, true, verbose);
    else
        res = sg_ll_mode_select10_v2(sg_fd, false /* PF */, true /* rtd */,
                                     save, NULL, 0, true, verbose);
    ret = res;
fini:
    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 (0 == verbose) {
        if (! sg_if_can2stderr("sg_wr_mode failed: ", ret))
            pr2serr("Some error occurred, try again with '-v' or '-vv' for "
                    "more information\n");
    }
    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
}
