/*
 * Copyright (c) 2016 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.
 */

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

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "sg_lib.h"
#include "sg_lib_data.h"
#include "sg_pt.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 issues the SCSI READ ATTRIBUTE command to the given SCSI device
 * and decodes the response. Based on spc5r08.pdf
 */

static const char * version_str = "1.00 20160207";

#define MAX_RATTR_BUFF_LEN (1024 * 1024)
#define DEF_RATTR_BUFF_LEN (1024 * 8)

#define SG_READ_ATTRIBUTE_CMD 0x8c
#define SG_READ_ATTRIBUTE_CMDLEN 16

#define RA_ATTR_VAL_SA 0x0
#define RA_ATTR_LIST_SA 0x1
#define RA_LV_LIST_SA 0x2
#define RA_PART_LIST_SA 0x3
#define RA_SMC2_SA 0x4
#define RA_SUP_ATTR_SA 0x5
#define RA_HIGHEST_SA 0x5

#define RA_FMT_BINARY 0x0
#define RA_FMT_ASCII 0x1
#define RA_FMT_TEXT 0x2         /* takes into account locale */
#define RA_FMT_RES 0x3          /* reserved */


#define SENSE_BUFF_LEN 64       /* Arbitrary, could be larger */
#define DEF_PT_TIMEOUT  60      /* 60 seconds */

struct opts_t {
    int cache;
    int ea;
    int enumerate;
    int filter;
    int fai;
    int do_hex;
    int lvn;
    int maxlen;
    int pn;
    int quiet;
    int do_raw;
    int o_readonly;
    int sa;
    int verbose;
};

struct acron_nv_t {
    const char * acron;
    const char * name;
    int val;
};

struct attr_name_info_t {
    int id;
    const char * name;  /* tab ('\t') suggest line break */
    int format;         /* RA_FMT_BINARY and friends, -1 --> unknown */
    int len;            /* -1 --> not fixed (variable) */
    int process;        /* 0 --> print decimal if binary, 1 --> print hex,
                         * 2 --> further processing */
};

static struct option long_options[] = {
    {"cache", no_argument, 0, 'c'},
    {"enumerate", no_argument, 0, 'e'},
    {"element", required_argument, 0, 'E'},   /* SMC-3 element address */
    {"filter", required_argument, 0, 'f'},
    {"first", required_argument, 0, 'F'},
    {"help", no_argument, 0, 'h'},
    {"hex", no_argument, 0, 'H'},
    {"in", required_argument, 0, 'i'},
    {"lvn", required_argument, 0, 'l'},
    {"maxlen", required_argument, 0, 'm'},
    {"partition", required_argument, 0, 'p'},
    {"quiet", required_argument, 0, 'q'},
    {"raw", no_argument, 0, 'r'},
    {"readonly", no_argument, 0, 'R'},
    {"sa", required_argument, 0, 's'},
    {"verbose", no_argument, 0, 'v'},
    {"version", no_argument, 0, 'V'},
    {0, 0, 0, 0},   /* sentinel */
};

static struct acron_nv_t sa_acron_arr[] = {
    {"av", "attribute values", 0},
    {"al", "attribute list", 1},
    {"lvl", "logical volume list", 2},
    {"pl", "partition list", 3},
    {"smc", "SMC-2 should define this", 4},
    {"sa", "supported attributes", 5},
    {NULL, NULL, -1},           /* sentinel */
};

static struct attr_name_info_t attr_name_arr[] = {
/* Device type attributes */
    {0x0, "Remaining capacity in partition [MiB]", RA_FMT_BINARY, 8, 0},
    {0x1, "Maximum capacity in partition [MiB]", RA_FMT_BINARY, 8, 0},
    {0x2, "TapeAlert flags", RA_FMT_BINARY, 8, 0},   /* SSC-4 */
    {0x3, "Load count", RA_FMT_BINARY, 8, 0},
    {0x4, "MAM space remaining [B]", RA_FMT_BINARY, 8, 0},
    {0x5, "Assigning organization", RA_FMT_ASCII, 8, 0}, /* SSC-4 */
    {0x6, "Format density code", RA_FMT_BINARY, 1, 1},    /* SSC-4 */
    {0x7, "Initialization count", RA_FMT_BINARY, 2, 0},
    {0x8, "Volume identifier", RA_FMT_ASCII, 32, 0},
    {0x9, "Volume change reference", RA_FMT_BINARY, -1, 1}, /* SSC-4 */
    {0x20a, "Density vendor/serial number at last load", RA_FMT_ASCII, 40, 0},
    {0x20b, "Density vendor/serial number at load-1", RA_FMT_ASCII, 40, 0},
    {0x20c, "Density vendor/serial number at load-2", RA_FMT_ASCII, 40, 0},
    {0x20d, "Density vendor/serial number at load-3", RA_FMT_ASCII, 40, 0},
    {0x220, "Total MiB written in medium life", RA_FMT_BINARY, 8, 0},
    {0x221, "Total MiB read in medium life", RA_FMT_BINARY, 8, 0},
    {0x222, "Total MiB written in current/last load", RA_FMT_BINARY, 8, 0},
    {0x223, "Total MiB read in current/last load", RA_FMT_BINARY, 8, 0},
    {0x224, "Logical position of first encrypted block", RA_FMT_BINARY, 8, 2},
    {0x225, "Logical position of first unencrypted block\tafter first "
     "encrypted block", RA_FMT_BINARY, 8, 2},
    {0x340, "Medium usage history", RA_FMT_BINARY, 90, 2},
    {0x341, "Partition usage history", RA_FMT_BINARY, 60, 2},

/* Medium type attributes */
    {0x400, "Medium manufacturer", RA_FMT_ASCII, 8, 0},
    {0x401, "Medium serial number", RA_FMT_ASCII, 32, 0},
    {0x402, "Medium length [m]", RA_FMT_BINARY, 4, 0},      /* SSC-4 */
    {0x403, "Medium width [0.1 mm]", RA_FMT_BINARY, 4, 0},  /* SSC-4 */
    {0x404, "Assigning organization", RA_FMT_ASCII, 8, 0},  /* SSC-4 */
    {0x405, "Medium density code", RA_FMT_BINARY, 1, 1},    /* SSC-4 */
    {0x406, "Medium manufacture date", RA_FMT_ASCII, 8, 0},
    {0x407, "MAM capacity [B]", RA_FMT_BINARY, 8, 0},
    {0x408, "Medium type", RA_FMT_BINARY, 1, 1},
    {0x409, "Medium type information", RA_FMT_BINARY, 2, 1},
    {0x40a, "Numeric medium serial number", -1, -1, 1},

/* Host type attributes */
    {0x800, "Application vendor", RA_FMT_ASCII, 8, 0},
    {0x801, "Application name", RA_FMT_ASCII, 32, 0},
    {0x802, "Application version", RA_FMT_ASCII, 8, 0},
    {0x803, "User medium text label", RA_FMT_TEXT, 160, 0},
    {0x804, "Date and time last written", RA_FMT_ASCII, 12, 0},
    {0x805, "Text localization identifier", RA_FMT_BINARY, 1, 0},
    {0x806, "Barcode", RA_FMT_ASCII, 32, 0},
    {0x807, "Owning host textual name", RA_FMT_TEXT, 80, 0},
    {0x808, "Media pool", RA_FMT_TEXT, 160, 0},
    {0x809, "Partition user text label", RA_FMT_ASCII, 16, 0},
    {0x80a, "Load/unload at partition", RA_FMT_BINARY, 1, 0},
    {0x80a, "Application format version", RA_FMT_ASCII, 16, 0},
    {0x80c, "Volume coherency information", RA_FMT_BINARY, -1, 1},
     /* SSC-5 */
    {0x820, "Medium globally unique identifier", RA_FMT_BINARY, 36, 1},
    {0x821, "Media pool globally unique identifier", RA_FMT_BINARY, 36, 1},

    {-1, NULL, -1, -1, 0},
};


static void
usage()
{
    pr2serr("Usage: sg_read_attr [--cache] [--element=EA] [--enumerate] "
            "[--filter=FL]\n"
            "                    [--first=FAI] [--help] [--hex] [--in=FN] "
            "[--lvn-LVN]\n"
            "                    [--maxlen=LEN] [--partition=PN] [--quiet] "
            "[--raw]\n"
            "                    [--readonly] [--sa=SA] [--verbose] "
            "[--version]\n"
            "                    DEVICE\n");
    pr2serr("  where:\n"
            "    --cache|-c         set CACHE bit in cdn (def: clear)\n"
            "    --enumerate|-e     enumerate known attributes and service "
            "actions\n"
            "    --element=EA|-E EA    EA is placed in 'element address' "
            "field in\n"
            "                          cdb [SMC-3] (def: 0)\n"
            "    --filter=FL|-f FL    FL is parameter code to match (def: "
            "-1 -> all)\n"
            "    --first=FAI|-F FAI    FAI is placed in 'first attribute "
            "identifier'\n"
            "                          field in cdb (def: 0)\n"
            "    --help|-h          print out usage message\n"
            "    --hex|-H           output response in hexadecimal; used "
            "twice\n"
            "                       shows decoded values in hex\n"
            "    --in=FN|-i FN      FN is a filename containing attribute "
            "values in\n"
            "                       ASCII hex or binary if --raw also "
            "given\n"
            "    --lvn=LVN|-l LVN    logical volume number (LVN) (def:0)\n"
            "    --maxlen=LEN|-m LEN    max response length (allocation "
            "length in cdb)\n"
            "                           (def: 0 -> 8192 bytes)\n"
            "    --partition=PN|-p PN    partition number (PN) (def:0)\n"
            "    --quiet|-q         reduce the amount of output, can use "
            "more than once\n"
            "    --raw|-r           output response in binary\n"
            "    --readonly|-R      open DEVICE read-only (def: read-write)\n"
            "    --sa=SA|-s SA      SA is service action (def: 0)\n"
            "    --verbose|-v       increase verbosity\n"
            "    --version|-V       print version string and exit\n\n"
            "Performs a SCSI READ ATTRIBUTE command. It is typically used "
            "on tape\nsystems.\n");
}

/* Invokes a SCSI READ ATTRIBUTE command (SPC+SMC).  Return of 0 -> success,
 * various SG_LIB_CAT_* positive values or -1 -> other errors */
static int
sg_ll_read_attr(int sg_fd, void * resp, int * residp,
                const struct opts_t * op)
{
    int k, ret, res, sense_cat;
    int noisy = 1;
    unsigned char raCmdBlk[SG_READ_ATTRIBUTE_CMDLEN] =
          {SG_READ_ATTRIBUTE_CMD, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
           0, 0, 0, 0};
    unsigned char sense_b[SENSE_BUFF_LEN];
    struct sg_pt_base * ptvp;

    raCmdBlk[1] = 0x1f & op->sa;
    if (op->ea)
        sg_put_unaligned_be16(op->ea, raCmdBlk + 2);
    if (op->lvn)
        raCmdBlk[5] = 0xff & op->lvn;
    if (op->pn)
        raCmdBlk[7] = 0xff & op->pn;
    if (op->fai)
        sg_put_unaligned_be16(op->fai, raCmdBlk + 8);
    sg_put_unaligned_be32((uint32_t)op->maxlen, raCmdBlk + 10);
    if (op->cache)
        raCmdBlk[14] |= 0x1;
    if (op->verbose) {
        pr2serr("    Read attribute cdb: ");
        for (k = 0; k < SG_READ_ATTRIBUTE_CMDLEN; ++k)
            pr2serr("%02x ", raCmdBlk[k]);
        pr2serr("\n");
    }

    ptvp = construct_scsi_pt_obj();
    if (NULL == ptvp) {
        pr2serr("%s: out of memory\n", __func__);
        return -1;
    }
    set_scsi_pt_cdb(ptvp, raCmdBlk, sizeof(raCmdBlk));
    set_scsi_pt_sense(ptvp, sense_b, sizeof(sense_b));
    set_scsi_pt_data_in(ptvp, (unsigned char *)resp, op->maxlen);
    res = do_scsi_pt(ptvp, sg_fd, DEF_PT_TIMEOUT, op->verbose);
    ret = sg_cmds_process_resp(ptvp, "read attribute", res, op->maxlen,
                               sense_b, noisy, op->verbose, &sense_cat);
    if (-1 == ret)
        ;
    else if (-2 == ret) {
        switch (sense_cat) {
        case SG_LIB_CAT_RECOVERED:
        case SG_LIB_CAT_NO_SENSE:
            ret = 0;
            break;
        default:
            ret = sense_cat;
            break;
        }
    } else
        ret = 0;
    if (residp)
        *residp = get_scsi_pt_resid(ptvp);
    destruct_scsi_pt_obj(ptvp);
    return ret;
}

static void
dStrRaw(const char* str, int len)
{
    int k;

    for (k = 0 ; k < len; ++k)
        printf("%c", str[k]);
}

static int
find_sa_acron(const char * cp)
{
    int k;
    const struct acron_nv_t * anvp;
    const char * mp;

    for (anvp = sa_acron_arr; anvp->acron ; ++anvp) {
        for (mp = cp, k = 0; *mp; ++mp, ++k) {
            if (0 == anvp->acron[k])
                return anvp->val;
            if (tolower(*mp) != anvp->acron[k])
                break;
        }
        if ((0 == *mp) && (0 == anvp->acron[k]))
            return anvp->val;
    }
    return -1;  /* not found */
}

const char * a_format[] = {
    "binary",
    "ascii",
    "text",
    "format[0x3]",
};

static void
enum_attributes(void)
{
    const struct attr_name_info_t * anip;
    const char * cp;
    char b[32];

    printf("Attribute ID\tLength\tFormat\tName\n");
    printf("------------------------------------------\n");
    for (anip = attr_name_arr; anip->name ; ++anip) {
        if (anip->format < 0)
            snprintf(b, sizeof(b), "unknown");
        else
            snprintf(b, sizeof(b), "%s", a_format[0x3 & anip->format]);
        printf("  0x%04x:\t%d\t%s\t", anip->id, anip->len, b);
        cp = strchr(anip->name, '\t');
        if (cp ) {
            printf("%.*s\n", (int)(cp - anip->name), anip->name);
            printf("\t\t\t\t%s\n", cp + 1);
        } else
            printf("%s\n", anip->name);
    }
}

static void
enum_sa_acrons(void)
{
    const struct acron_nv_t * anvp;

    printf("SA_value\tAcronym\tDescription\n");
    printf("------------------------------------------\n");
    for (anvp = sa_acron_arr; anvp->acron ; ++anvp)
        printf("  %d:\t\t%s\t%s\n", anvp->val, anvp->acron, anvp->name);
}

/* Read ASCII hex bytes or binary from fname (a file named '-' taken as
 * stdin). If reading ASCII hex then there should be either one entry per
 * line or a comma, space or tab separated list of bytes. If no_space is
 * set then a string of ACSII hex digits is expected, 2 per byte. Everything
 * from and including a '#' on a line is ignored. Returns 0 if ok, or 1 if
 * error. */
static int
f2hex_arr(const char * fname, int as_binary, int no_space,
          uint8_t * mp_arr, int * mp_arr_len, int max_arr_len)
{
    int fn_len, in_len, k, j, m, split_line, fd, has_stdin;
    unsigned int h;
    const char * lcp;
    FILE * fp;
    char line[512];
    char carry_over[4];
    int off = 0;

    if ((NULL == fname) || (NULL == mp_arr) || (NULL == mp_arr_len))
        return 1;
    fn_len = strlen(fname);
    if (0 == fn_len)
        return 1;
    has_stdin = ((1 == fn_len) && ('-' == fname[0]));  /* read from stdin */
    if (as_binary) {
        if (has_stdin) {
            fd = STDIN_FILENO;
                if (sg_set_binary_mode(STDIN_FILENO) < 0)
                    perror("sg_set_binary_mode");
        } else {
            fd = open(fname, O_RDONLY);
            if (fd < 0) {
                pr2serr("unable to open binary file %s: %s\n", fname,
                         safe_strerror(errno));
                return 1;
            } else if (sg_set_binary_mode(fd) < 0)
                perror("sg_set_binary_mode");
        }
        k = read(fd, mp_arr, max_arr_len);
        if (k <= 0) {
            if (0 == k)
                pr2serr("read 0 bytes from binary file %s\n", fname);
            else
                pr2serr("read from binary file %s: %s\n", fname,
                        safe_strerror(errno));
            if (! has_stdin)
                close(fd);
            return 1;
        }
        *mp_arr_len = k;
        if (! has_stdin)
            close(fd);
        return 0;
    } else {    /* So read the file as ASCII hex */
        if (has_stdin)
            fp = stdin;
        else {
            fp = fopen(fname, "r");
            if (NULL == fp) {
                pr2serr("Unable to open %s for reading\n", fname);
                return 1;
            }
        }
    }

    carry_over[0] = 0;
    for (j = 0; j < 512; ++j) {
        if (NULL == fgets(line, sizeof(line), fp))
            break;
        in_len = strlen(line);
        if (in_len > 0) {
            if ('\n' == line[in_len - 1]) {
                --in_len;
                line[in_len] = '\0';
                split_line = 0;
            } else
                split_line = 1;
        }
        if (in_len < 1) {
            carry_over[0] = 0;
            continue;
        }
        if (carry_over[0]) {
            if (isxdigit(line[0])) {
                carry_over[1] = line[0];
                carry_over[2] = '\0';
                if (1 == sscanf(carry_over, "%4x", &h))
                    mp_arr[off - 1] = h;       /* back up and overwrite */
                else {
                    pr2serr("%s: carry_over error ['%s'] around line %d\n",
                            __func__, carry_over, j + 1);
                    goto bad;
                }
                lcp = line + 1;
                --in_len;
            } else
                lcp = line;
            carry_over[0] = 0;
        } else
            lcp = line;

        m = strspn(lcp, " \t");
        if (m == in_len)
            continue;
        lcp += m;
        in_len -= m;
        if ('#' == *lcp)
            continue;
        k = strspn(lcp, "0123456789aAbBcCdDeEfF ,\t");
        if ((k < in_len) && ('#' != lcp[k]) && ('\r' != lcp[k])) {
            pr2serr("%s: syntax error at line %d, pos %d\n", __func__,
                    j + 1, m + k + 1);
            goto bad;
        }
        if (no_space) {
            for (k = 0; isxdigit(*lcp) && isxdigit(*(lcp + 1));
                 ++k, lcp += 2) {
                if (1 != sscanf(lcp, "%2x", &h)) {
                    pr2serr("%s: bad hex number in line %d, pos %d\n",
                            __func__, j + 1, (int)(lcp - line + 1));
                    goto bad;
                }
                if ((off + k) >= max_arr_len) {
                    pr2serr("%s: array length exceeded\n", __func__);
                    goto bad;
                }
                mp_arr[off + k] = h;
            }
            if (isxdigit(*lcp) && (! isxdigit(*(lcp + 1))))
                carry_over[0] = *lcp;
            off += k;
        } else {
            for (k = 0; k < 1024; ++k) {
                if (1 == sscanf(lcp, "%4x", &h)) {
                    if (h > 0xff) {
                        pr2serr("%s: hex number larger than 0xff in line %d, "
                                "pos %d\n", __func__, j + 1,
                                (int)(lcp - line + 1));
                        goto bad;
                    }
                    if (split_line && (1 == strlen(lcp))) {
                        /* single trailing hex digit might be a split pair */
                        carry_over[0] = *lcp;
                    }
                    if ((off + k) >= max_arr_len) {
                        pr2serr("%s: array length exceeded\n", __func__);
                        goto bad;
                    }
                    mp_arr[off + k] = h;
                    lcp = strpbrk(lcp, " ,\t");
                    if (NULL == lcp)
                        break;
                    lcp += strspn(lcp, " ,\t");
                    if ('\0' == *lcp)
                        break;
                } else {
                    if (('#' == *lcp) || ('\r' == *lcp)) {
                        --k;
                        break;
                    }
                    pr2serr("%s: error in line %d, at pos %d\n", __func__,
                            j + 1, (int)(lcp - line + 1));
                    goto bad;
                }
            }
            off += (k + 1);
        }
    }
    *mp_arr_len = off;
    if (stdin != fp)
        fclose(fp);
    return 0;
bad:
    if (stdin != fp)
        fclose(fp);
    return 1;
}

/* Returns 1 if 'ucp' all 0xff bytes, returns 2 is all 0xff bytes apart
 * from last being 0xfe; otherwise returns 0. */
static int
all_ffs_or_last_fe(const unsigned char * ucp, int len)
{
    for ( ; len > 0; ++ucp, --len) {
        if (*ucp < 0xfe)
            return 0;
        if (0xfe == *ucp)
            return (1 == len) ? 2 : 0;

    }
    return 1;
}

static char *
attr_id_lookup(unsigned int id, const struct attr_name_info_t ** anipp,
               int blen, char * b)
{
    const struct attr_name_info_t * anip;

    for (anip = attr_name_arr; anip->name; ++anip) {
        if (id == (unsigned int)anip->id)
            break;
    }
    if (anip->name) {
        snprintf(b, blen, "%s", anip->name);
        if (anipp)
            *anipp = anip;
        return b;
    }
    if (anipp)
        *anipp = NULL;
    if (id < 0x400)
        snprintf(b, blen, "Unknown device attribute 0x%x", id);
    else if (id < 0x800)
        snprintf(b, blen, "Unknown medium attribute 0x%x", id);
    else if (id < 0xc00)
        snprintf(b, blen, "Unknown host attribute 0x%x", id);
    else if (id < 0x1000)
        snprintf(b, blen, "Vendor specific device attribute 0x%x", id);
    else if (id < 0x1400)
        snprintf(b, blen, "Vendor specific medium attribute 0x%x", id);
    else if (id < 0x1800)
        snprintf(b, blen, "Vendor specific host attribute 0x%x", id);
    else
        snprintf(b, blen, "Reserved attribute 0x%x", id);
    return b;
}

static void
decode_attr_list(const unsigned char * alp, int len, bool supported,
                 const struct opts_t * op)
{
    int id;
    char b[160];
    char * cp;
    char * c2p;
    const char * leadin = supported ? "Supported a" : "A";

    if (op->verbose)
        printf("%sttribute list: [len=%d]\n", leadin, len);
    else if (0 == op->quiet)
        printf("%sttribute list:\n", leadin);
    if (op->do_hex) {
        dStrHex((const char *)alp, len, 0);
        return;
    }
    for ( ; len > 0; alp += 2, len -= 2) {
        id = sg_get_unaligned_be16(alp + 0);
        if ((op->filter >= 0) && (op->filter != id))
            continue;
        if (op->verbose)
            printf("  0x%.4x:\t", id);
        cp = attr_id_lookup(id, NULL, sizeof(b), b);
        c2p = strchr(cp, '\t');
        if (c2p) {
            printf("  %.*s -\n", (int)(c2p - cp), cp);
            if (op->verbose)
                printf("\t\t      %s\n", c2p + 1);
            else
                printf("      %s\n", c2p + 1);
        } else
            printf("  %s\n", cp);
    }
}

static void
helper_full_attr(const unsigned char * alp, int len, int id,
                 const struct attr_name_info_t * anip,
                 const struct opts_t * op)
{
    int k;
    const unsigned char * ucp;

    if (op->verbose)
        printf("[r%c] ", (0x80 & alp[2]) ? 'o' : 'w');
    if (op->verbose > 3)
        pr2serr("%s: id=0x%x, len=%d, anip->format=%d, anip->len=%d\n",
                __func__, id, len, anip->format, anip->len);
    switch (id) {
    case 0x224:         /* logical position of first encrypted block */
        k = all_ffs_or_last_fe(alp + 5, len - 5);
        if (1 == k)
            printf("<unknown> [ff]\n");
        else if (2 == k)
            printf("<unknown [fe]>\n");
        else {
            if ((len - 5) <= 8)
                printf("%" PRIx64, sg_get_unaligned_be(len - 5, alp + 5));
            else {
                printf("\n");
                dStrHex((const char *)(alp + 5), len - 5, 0);
            }
        }
        break;
    case 0x225:         /* logical position of first unencrypted block
                         * after first encrypted block */
        k = all_ffs_or_last_fe(alp + 5, len - 5);
        if (1 == k)
            printf("<unknown> [ff]\n");
        else if (2 == k)
            printf("<unknown [fe]>\n");
        else {
            if ((len - 5) <= 8)
                printf("%" PRIx64, sg_get_unaligned_be(len - 5, alp + 5));
            else {
                printf("\n");
                dStrHex((const char *)(alp + 5), len - 5, 0);
            }
        }
        break;
    case 0x340:         /* Medium Usage history */
        ucp = alp + 5;
        printf("\n");
        if ((len - 5) < 90) {
            pr2serr("%s: expected 90 bytes, got %d\n", __func__, len - 5);
            break;
        }
        printf("    Current amount of data written [MiB]: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 0));
        printf("    Current write retry count: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 6));
        printf("    Current amount of data read [MiB]: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 12));
        printf("    Current read retry count: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 18));
        printf("    Previous amount of data written [MiB]: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 24));
        printf("    Previous write retry count: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 30));
        printf("    Previous amount of data read [MiB]: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 36));
        printf("    Previous read retry count: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 42));
        printf("    Total amount of data written [MiB]: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 48));
        printf("    Total write retry count: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 54));
        printf("    Total amount of data read [MiB]: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 60));
        printf("    Total read retry count: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 66));
        printf("    Load count: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 72));
        printf("    Total change partition count: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 78));
        printf("    Total partition initialization count: %" PRIu64 "\n",
               sg_get_unaligned_be48(ucp + 84));
        break;
    case 0x341:         /* Partition Usage history */
        ucp = alp + 5;
        printf("\n");
        if ((len - 5) < 60) {
            pr2serr("%s: expected 60 bytes, got %d\n", __func__, len - 5);
            break;
        }
        printf("    Current amount of data written [MiB]: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 0));
        printf("    Current write retry count: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 4));
        printf("    Current amount of data read [MiB]: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 8));
        printf("    Current read retry count: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 12));
        printf("    Previous amount of data written [MiB]: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 16));
        printf("    Previous write retry count: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 20));
        printf("    Previous amount of data read [MiB]: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 24));
        printf("    Previous read retry count: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 28));
        printf("    Total amount of data written [MiB]: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 32));
        printf("    Total write retry count: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 36));
        printf("    Total amount of data read [MiB]: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 40));
        printf("    Total read retry count: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 44));
        printf("    Load count: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 48));
        printf("    change partition count: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 52));
        printf("    partition initialization count: %" PRIu32 "\n",
               sg_get_unaligned_be32(ucp + 56));
        break;
    default:
        pr2serr("%s: unknown attribute id: 0x%x\n", __func__, id);
        printf("  In hex:\n");
        dStrHex((const char *)alp, len, 0);
        break;
    }
}

static void
decode_attr_vals(const unsigned char * alp, int len, const struct opts_t * op)
{
    int bump, id, alen;
    uint64_t ull;
    char * cp;
    char * c2p;
    const struct attr_name_info_t * anip;
    char b[160];

    if (op->verbose)
        printf("Attribute values: [len=%d]\n", len);
    else if (op->filter < 0) {
        if (0 == op->quiet)
            printf("Attribute values:\n");
        if (op->do_hex) {       /* only expect -HH to get through here */
            dStrHex((const char *)alp, len, 0);
            return;
        }
    }
    for ( ; len > 4; alp += bump, len -= bump) {
        id = sg_get_unaligned_be16(alp + 0);
        bump = sg_get_unaligned_be16(alp + 3) + 5;
        alen = bump - 5;
        if ((op->filter >= 0) && (op->filter != id)) {
            if (id < op->filter)
                continue;
            else
                break;  /* Assume array is ascending id order */
        }
        anip = NULL;
        cp = attr_id_lookup(id, &anip, sizeof(b), b);
        if (op->quiet < 2) {
            c2p = strchr(cp, '\t');
            if (c2p) {
                printf("  %.*s -\n", (int)(c2p - cp), cp);
                printf("      %s: ", c2p + 1);
            } else
                printf("  %s: ", cp);
        }
        if (op->verbose)
            printf("[r%c] ", (0x80 & alp[2]) ? 'o' : 'w');
        if (anip) {
            if ((RA_FMT_BINARY == anip->format) && (bump <= 13)) {
                ull = sg_get_unaligned_be(alen, alp + 5);
                if (0 == anip->process)
                    printf("%" PRIu64 "\n", ull);
                else if (1 == anip->process)
                    printf("0x%" PRIx64 "\n", ull);
                else
                    helper_full_attr(alp, bump, id, anip, op);
                if (op->verbose) {
                    if ((anip->len > 0) && (alen > 0) && (alen != anip->len))
                        printf(" <<< T10 length (%d) differs from length in "
                               "response (%d) >>>\n", anip->len, alen);
                }
            } else if (RA_FMT_BINARY == anip->format) {
                if (2 == anip->process)
                    helper_full_attr(alp, bump, id, anip, op);
                else {
                    printf("\n");
                    dStrHex((const char *)(alp + 5), alen, 0);
                }
           } else {
                if (2 == anip->process)
                    helper_full_attr(alp, bump, id, anip, op);
                else {
                    printf("%.*s\n", alen, alp + 5);
                    if (op->verbose) {
                        if ((anip->len > 0) && (alen > 0) &&
                            (alen != anip->len))
                            printf(" <<< T10 length (%d) differs from length "
                                   "in response (%d) >>>\n", anip->len, alen);
                    }
                }
            }
        } else {
            if (op->verbose > 1)
                printf("Attribute id lookup failed, in hex:\n");
            else
                printf("\n");
            dStrHex((const char *)(alp + 5), alen, 0);
        }
    }
    if (op->verbose && (len > 0) && (len <= 4))
        pr2serr("warning: iterate of attributes should end a residual of "
                "%d\n", len);
}

static void
decode_all_sa_s(const unsigned char * rabp, int len, const struct opts_t * op)
{
    if (op->do_hex && (2 != op->do_hex)) {
        dStrHex((const char *)rabp, len, ((1 == op->do_hex) ? 1 : -1));
        return;
    }
    switch (op->sa) {
    case RA_ATTR_VAL_SA:
        decode_attr_vals(rabp + 4, len - 4, op);
        break;
    case RA_ATTR_LIST_SA:
        decode_attr_list(rabp + 4, len - 4, false, op);
        break;
    case RA_LV_LIST_SA:
        if ((0 == op->quiet) || op->verbose)
            printf("Logical volume list:\n");
        if (len < 4) {
            pr2serr(">>> response length unexpectedly short: %d bytes\n",
                    len);
            break;
        }
        printf("  First logical volume number: %u\n", rabp[2]);
        printf("  Number of logical volumes available: %u\n", rabp[3]);
        break;
    case RA_PART_LIST_SA:
        if ((0 == op->quiet) || op->verbose)
            printf("Partition number list:\n");
        if (len < 4) {
            pr2serr(">>> response length unexpectedly short: %d bytes\n",
                    len);
            break;
        }
        printf("  First partition number: %u\n", rabp[2]);
        printf("  Number of partitions available: %u\n", rabp[3]);
        break;
    case RA_SMC2_SA:
        printf("Used by SMC-2, not information, output in hex:\n");
        dStrHex((const char *)rabp, len, 0);
        break;
    case RA_SUP_ATTR_SA:
        decode_attr_list(rabp + 4, len - 4, true, op);
        break;
    default:
        printf("Unrecognized service action [0x%x], response in hex:\n",
               op->sa);
        dStrHex((const char *)rabp, len, 0);
        break;
    }
}

int
main(int argc, char * argv[])
{
    int sg_fd, res, c, len, resid, rlen, in_len;
    unsigned int ra_len;
    int ret = 0;
    const char * device_name = NULL;
    const char * fname = NULL;
    unsigned char * rabp = NULL;
    struct opts_t opts;
    struct opts_t * op;
    char b[80];

    op = &opts;
    memset(op, 0, sizeof(opts));
    op->filter = -1;
    while (1) {
        int option_index = 0;

        c = getopt_long(argc, argv, "ceE:f:F:hHi:l:m:p:qrRs:vV",
                        long_options, &option_index);
        if (c == -1)
            break;

        switch (c) {
        case 'c':
            ++op->cache;
            break;
        case 'e':
            ++op->enumerate;
            break;
        case 'E':
           op->ea = sg_get_num(optarg);
           if ((op->ea < 0) || (op->ea > 65535)) {
                pr2serr("bad argument to '--ea=EA', expect 0 to 65535\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'f':
           op->filter = sg_get_num(optarg);
           if ((op->filter < -3) || (op->filter > 65535)) {
                pr2serr("bad argument to '--filter=FL', expect -3 to "
                        "65535\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'F':
           op->fai = sg_get_num(optarg);
           if ((op->fai < 0) || (op->fai > 65535)) {
                pr2serr("bad argument to '--first=FAI', expect 0 to 65535\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'h':
        case '?':
            usage();
            return 0;
        case 'H':
            ++op->do_hex;
            break;
        case 'i':
            fname = optarg;
            break;
        case 'l':
           op->lvn = sg_get_num(optarg);
           if ((op->lvn < 0) || (op->lvn > 255)) {
                pr2serr("bad argument to '--lvn=LVN', expect 0 to 255\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'm':
            op->maxlen = sg_get_num(optarg);
            if ((op->maxlen < 0) || (op->maxlen > MAX_RATTR_BUFF_LEN)) {
                pr2serr("argument to '--maxlen' should be %d or "
                        "less\n", MAX_RATTR_BUFF_LEN);
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'p':
           op->pn = sg_get_num(optarg);
           if ((op->pn < 0) || (op->pn > 255)) {
                pr2serr("bad argument to '--pn=PN', expect 0 to 255\n");
                return SG_LIB_SYNTAX_ERROR;
            }
            break;
        case 'q':
            ++op->quiet;
            break;
        case 'r':
            ++op->do_raw;
            break;
        case 'R':
            ++op->o_readonly;
            break;
        case 's':
           if (isdigit(*optarg)) {
               op->sa = sg_get_num(optarg);
               if ((op->sa < 0) || (op->sa > 63)) {
                    pr2serr("bad argument to '--sa=SA', expect 0 to 63\n");
                    return SG_LIB_SYNTAX_ERROR;
                }
            } else {
                res = find_sa_acron(optarg);
                if (res < 0) {
                    enum_sa_acrons();
                    return SG_LIB_SYNTAX_ERROR;
                }
                op->sa = res;
            }
            break;
        case 'v':
            ++op->verbose;
            break;
        case 'V':
            pr2serr("version: %s\n", version_str);
            return 0;
        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 (op->enumerate) {
        enum_attributes();
        printf("\n");
        enum_sa_acrons();
        return 0;
    }

    if (fname && device_name) {
        pr2serr("since '--in=FN' given, ignoring DEVICE\n");
        device_name = NULL;
    }

    if (0 == op->maxlen)
        op->maxlen = DEF_RATTR_BUFF_LEN;
    rabp = (unsigned char *)calloc(1, op->maxlen);
    if (NULL == rabp) {
        pr2serr("unable to calloc %d bytes\n", op->maxlen);
        return SG_LIB_CAT_OTHER;
    }

    if (NULL == device_name) {
        if (fname) {
            if (f2hex_arr(fname, op->do_raw, 0, rabp, &in_len, op->maxlen))
                return SG_LIB_FILE_ERROR;
            if (op->do_raw)
                op->do_raw = 0;    /* can interfere on decode */
            if (in_len < 4) {
                pr2serr("--in=%s only decoded %d bytes (needs 4 at least)\n",
                        fname, in_len);
                return SG_LIB_SYNTAX_ERROR;
            }
            decode_all_sa_s(rabp, in_len, op);
            goto clean_up;
        }
        pr2serr("missing device name!\n");
        usage();
        return SG_LIB_SYNTAX_ERROR;
    }

    if (op->do_raw) {
        if (sg_set_binary_mode(STDOUT_FILENO) < 0) {
            perror("sg_set_binary_mode");
            return SG_LIB_FILE_ERROR;
        }
    }

    sg_fd = sg_cmds_open_device(device_name, op->o_readonly, op->verbose);
    if (sg_fd < 0) {
        pr2serr("open error: %s: %s\n", device_name,
                safe_strerror(-sg_fd));
        return SG_LIB_FILE_ERROR;
    }

    res = sg_ll_read_attr(sg_fd, rabp, &resid, op);
    ret = res;
    if (0 == res) {
        rlen = op->maxlen - resid;
        if (rlen < 4) {
            pr2serr("Response length (%d) too short\n", rlen);
            ret = SG_LIB_CAT_MALFORMED;
            goto close_then_end;
        }
        if ((op->sa <= RA_HIGHEST_SA) && (op->sa != RA_SMC2_SA)) {
            ra_len = ((RA_LV_LIST_SA == op->sa) ||
                      (RA_PART_LIST_SA == op->sa)) ?
                        (unsigned int)sg_get_unaligned_be16(rabp + 0) :
                        sg_get_unaligned_be32(rabp + 0) + 2;
            ra_len += 2;
        } else
            ra_len = rlen;
        if ((int)ra_len > rlen) {
            if (op->verbose)
                pr2serr("ra_len available is %d, response length is %d\n",
                        ra_len, rlen);
            len = rlen;
        } else
            len = (int)ra_len;
        if (op->do_raw) {
            dStrRaw((const char *)rabp, len);
            goto close_then_end;
        }
        decode_all_sa_s(rabp, len, op);
    } else if (SG_LIB_CAT_INVALID_OP == res)
        pr2serr("Read attribute command not supported\n");
    else {
        sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
        pr2serr("Read attribute command: %s\n", b);
    }

close_then_end:
    res = sg_cmds_close_device(sg_fd);
    if (res < 0) {
        pr2serr("close error: %s\n", safe_strerror(-res));
        if (0 == ret)
            ret = SG_LIB_FILE_ERROR;
    }
clean_up:
    if (rabp)
        free(rabp);
    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
}
