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

/* A utility program for the Linux OS SCSI subsystem.
 *
 *
 * This program maps a primary SCSI device node name to the corresponding
 * SCSI generic device node name (or vice versa). Targets Linux
 * kernel 2.6, 3 and 4 series. Sysfs device names can also be mapped.
 */

/* #define _XOPEN_SOURCE 500 */
/* needed to see DT_REG and friends when compiled with: c99 pedantic */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdbool.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <getopt.h>
#include <dirent.h>
#include <libgen.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>      /* new location for major + minor */
#ifndef major
#include <sys/types.h>
#endif

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

#ifdef HAVE_LINUX_MAJOR_H
#include <linux/major.h>
#else
#include "sg_pt_linux_missing.h"
#endif

#include "sg_lib.h"

static const char * version_str = "1.23 20230717";

#define ME "sg_map26: "

#define NT_NO_MATCH 0
#define NT_SD 1
#define NT_SR 2
#define NT_HD 3
#define NT_ST 4
#define NT_OSST 5
#define NT_SG 6
#define NT_CH 7
#define NT_BSG 8
#define NT_NVME 9
#define NT_NVME_GEN 10  /* e.g.  /dev/ng0n1 */
#define NT_REG 11
#define NT_DIR 12

#define NAME_LEN_MAX 256
#define D_NAME_LEN_MAX 520

#ifndef SCSI_CHANGER_MAJOR
#define SCSI_CHANGER_MAJOR 86
#endif
#ifndef OSST_MAJOR
#define OSST_MAJOR 206
#endif

/* scandir() and stat() categories, file_type enumeration */
#define FT_OTHER 0
#define FT_REGULAR 1
#define FT_BLOCK 2
#define FT_CHAR 3
#define FT_DIR 4

/* older major.h headers may not have these */
#ifndef SCSI_DISK8_MAJOR
#define SCSI_DISK8_MAJOR        128
#define SCSI_DISK9_MAJOR        129
#define SCSI_DISK10_MAJOR       130
#define SCSI_DISK11_MAJOR       131
#define SCSI_DISK12_MAJOR       132
#define SCSI_DISK13_MAJOR       133
#define SCSI_DISK14_MAJOR       134
#define SCSI_DISK15_MAJOR       135
#endif

/* st minor decodes from Kai Makisara 20081008 */
#define ST_NBR_MODE_BITS 2
#define ST_MODE_SHIFT (7 - ST_NBR_MODE_BITS)
#define TAPE_NR(minor) ( (((minor) & ~255) >> (ST_NBR_MODE_BITS + 1)) | \
    ((minor) & ~(UINT_MAX << ST_MODE_SHIFT)) )

static const char * sys_sg_dir = "/sys/class/scsi_generic/";
static const char * sys_sd_dir = "/sys/block/";
static const char * sys_sr_dir = "/sys/block/";
static const char * sys_hd_dir = "/sys/block/";
static const char * sys_scsi_dev_dir = "/sys/class/scsi_device/";
static const char * sys_st_dir = "/sys/class/scsi_tape/";
static const char * sys_sch_dir = "/sys/class/scsi_changer/";
static const char * sys_osst_dir = "/sys/class/onstream_tape/";
static const char * def_dev_dir = "/dev";


static const struct option long_options[] = {
    {"dev_dir", required_argument, 0, 'd'},
    {"given_is", required_argument, 0, 'g'},
    {"help", no_argument, 0, 'h'},
    {"result", required_argument, 0, 'r'},
    {"symlink", no_argument, 0, 's'},
    {"verbose", no_argument, 0, 'v'},
    {"version", no_argument, 0, 'V'},
    {0, 0, 0, 0},
};

static const char * nt_names[] = {
    "No matching",
    "disk",
    "cd/dvd",
    "hd",
    "tape",
    "tape (osst)",
    "generic (sg)",
    "changer",
    "regular file",
    "directory",
};


#if defined(__GNUC__) || defined(__clang__)
static int pr2serr(const char * fmt, ...)
        __attribute__ ((format (printf, 1, 2)));
#else
static int pr2serr(const char * fmt, ...);
#endif


static int
pr2serr(const char * fmt, ...)
{
    va_list args;
    int n;

    va_start(args, fmt);
    n = vfprintf(stderr, fmt, args);
    va_end(args);
    return n;
}

static void
usage()
{
        pr2serr("Usage: sg_map26 [--dev_dir=DIR] [--given_is=0..1] [--help] "
                "[--result=0..3]\n"
                "                [--symlink] [--verbose] [--version] "
                "DEVICE\n"
                "  where:\n"
                "    --dev_dir=DIR | -d DIR    search in DIR for "
                "resulting special\n"
                "                            (def: directory of DEVICE "
                "or '/dev')\n"
                "    --given_is=0..1 | -g 0..1    variety of given "
                "DEVICE\n"
                "                                 0->block or char special "
                "(or symlink to)\n"
                "                                   1->sysfs device, 'dev' or "
                "parent\n"
                "    --help | -h       print out usage message\n"
                "    --result=0..3 | -r 0..3    variety of file(s) to "
                "find\n"
                "                               0->mapped block or char "
                "special(def)\n"
                "                               1->mapped sysfs path\n"
                "                               2->matching block or "
                "char special\n"
                "                               3->matching sysfs "
                "path\n"
                "    --symlink | -s    symlinks to special included in "
                "result\n"
                "    --verbose | -v    increase verbosity of output\n"
                "    --version | -V    print version string and exit\n\n"
                "Maps SCSI device node to corresponding generic node (and "
                "vv). Users may\nfind the lsscsi utility more convenient "
                "as it doesn't need root\npermissions.\n"
                );
}


/* ssafe_strerror() contributed by Clayton Weaver <cgweav at email dot com>
   Allows for situation in which strerror() is given a wild value (or the
   C library is incomplete) and returns NULL. Still not thread safe.
 */

static char safe_errbuf[64] = {'u', 'n', 'k', 'n', 'o', 'w', 'n', ' ',
                               'e', 'r', 'r', 'n', 'o', ':', ' ', 0};

static char *
ssafe_strerror(int errnum)
{
        size_t len;
        char * errstr;

        errstr = strerror(errnum);
        if (NULL == errstr) {
                len = strlen(safe_errbuf);
                snprintf(safe_errbuf + len, sizeof(safe_errbuf) - len, "%i",
                         errnum);
                safe_errbuf[sizeof(safe_errbuf) - 1] = '\0';  /* bombproof */
                return safe_errbuf;
        }
        return errstr;
}

/* This code is borrowed from lib/sg_pt_linux.c and a stripped down version
 * is here so sg_map26 continues to not depend on libsgutils . */
static void
find_bsg_nvme_char_major(int * bsg_majp, int * nvme_majp, int * nvme_gen_majp,
                         int vb)
{
    bool got_one = false;
    int n;
    char * cp;
    FILE *fp;
    char a[128];
    char b[128];
    static const int blen = sizeof(b);
    static const char * proc_devices = "/proc/devices";

    if (bsg_majp)
        *bsg_majp = 0;
    if (nvme_majp)
        *nvme_majp = 0;
    if (nvme_gen_majp)
        *nvme_gen_majp = 0;
    if (NULL == (fp = fopen(proc_devices, "r"))) {
        if (vb)
            pr2serr("fopen %s failed: %s\n", proc_devices, strerror(errno));
        return;
    }
    while ((cp = fgets(b, blen, fp))) {
        if ((1 == sscanf(b, "%126s", a)) &&
            (0 == memcmp(a, "Character", 9)))
            break;
    }
    while (cp && (cp = fgets(b, blen, fp))) {
        if (2 == sscanf(b, "%d %126s", &n, a)) {
            if (0 == strcmp("bsg", a)) {
                if (bsg_majp)
                    *bsg_majp = n;
                if (vb > 3)
                    pr2serr("found bsg_major=%d\n", n);
                if (got_one)
                    break;
                got_one = true;
            } else if (0 == memcmp("nvme", a, 4)) {
                if (0 == strcmp("nvme-generic", a)) {
                    if (nvme_gen_majp)
                        *nvme_gen_majp = n;
                    if (vb > 3)
                        pr2serr("found nvme_gen_char_major=%d\n", n);
                } else {
                    if (nvme_majp)
                        *nvme_majp = n;
                    if (vb > 3)
                        pr2serr("found nvme_char_major=%d\n", n);
                }
                got_one = true;
            }
        } else
            break;
    }
    if ((vb > 3) && (! got_one))
        pr2serr("found no bsg nor nvme char device in %s\n", proc_devices);
    fclose(fp);
}

static int
nt_typ_from_filename(const char * filename, int * majj, int * minn)
{
        int ma, mi, res, bsg_maj, nvme_maj, nvme_gen_maj;
        struct stat st;

        if (stat(filename, &st) < 0)
                return -errno;
        ma = major(st.st_rdev);
        mi = minor(st.st_rdev);
        if (majj)
                *majj = ma;
        if (minn)
                *minn = mi;
        if (S_ISCHR(st.st_mode)) {
                switch(ma) {
                case OSST_MAJOR:
                        return NT_OSST;
                case SCSI_GENERIC_MAJOR:
                        return NT_SG;
                case SCSI_TAPE_MAJOR:
                        return NT_ST;
                case SCSI_CHANGER_MAJOR:
                        return NT_CH;
                default:
                        res = NT_NO_MATCH;
                        find_bsg_nvme_char_major(&bsg_maj, &nvme_maj,
                                                 &nvme_gen_maj, 0);
                        if (ma == bsg_maj)
                            res = NT_BSG;
                        else if (ma == nvme_maj)
                            res = NT_NVME;
                        else if (ma == nvme_gen_maj)
                            res = NT_NVME_GEN;
                        return res;
                }
        } else if (S_ISBLK(st.st_mode)) {
                switch(ma) {
                case SCSI_DISK0_MAJOR: case SCSI_DISK1_MAJOR:
                case SCSI_DISK2_MAJOR: case SCSI_DISK3_MAJOR:
                case SCSI_DISK4_MAJOR: case SCSI_DISK5_MAJOR:
                case SCSI_DISK6_MAJOR: case SCSI_DISK7_MAJOR:
                case SCSI_DISK8_MAJOR: case SCSI_DISK9_MAJOR:
                case SCSI_DISK10_MAJOR: case SCSI_DISK11_MAJOR:
                case SCSI_DISK12_MAJOR: case SCSI_DISK13_MAJOR:
                case SCSI_DISK14_MAJOR: case SCSI_DISK15_MAJOR:
                        return NT_SD;
                case SCSI_CDROM_MAJOR:
                        return NT_SR;
                case IDE0_MAJOR: case IDE1_MAJOR:
                case IDE2_MAJOR: case IDE3_MAJOR:
                case IDE4_MAJOR: case IDE5_MAJOR:
                case IDE6_MAJOR: case IDE7_MAJOR:
                case IDE8_MAJOR: case IDE9_MAJOR:
                        return NT_HD;
                default:
                        return NT_NO_MATCH;
                }
        } else if (S_ISREG(st.st_mode))
                return NT_REG;
        else if (S_ISDIR(st.st_mode))
                return NT_DIR;
        return NT_NO_MATCH;
}

static int
nt_typ_from_major(int ma)
{
        switch(ma) {
        case SCSI_DISK0_MAJOR: case SCSI_DISK1_MAJOR:
        case SCSI_DISK2_MAJOR: case SCSI_DISK3_MAJOR:
        case SCSI_DISK4_MAJOR: case SCSI_DISK5_MAJOR:
        case SCSI_DISK6_MAJOR: case SCSI_DISK7_MAJOR:
        case SCSI_DISK8_MAJOR: case SCSI_DISK9_MAJOR:
        case SCSI_DISK10_MAJOR: case SCSI_DISK11_MAJOR:
        case SCSI_DISK12_MAJOR: case SCSI_DISK13_MAJOR:
        case SCSI_DISK14_MAJOR: case SCSI_DISK15_MAJOR:
                return NT_SD;
        case SCSI_CDROM_MAJOR:
                return NT_SR;
        case IDE0_MAJOR: case IDE1_MAJOR:
        case IDE2_MAJOR: case IDE3_MAJOR:
        case IDE4_MAJOR: case IDE5_MAJOR:
        case IDE6_MAJOR: case IDE7_MAJOR:
        case IDE8_MAJOR: case IDE9_MAJOR:
                return NT_HD;
        case OSST_MAJOR:
                return NT_OSST;
        case SCSI_GENERIC_MAJOR:
                return NT_SG;
        case SCSI_TAPE_MAJOR:
                return NT_ST;
        case SCSI_CHANGER_MAJOR:
                return NT_CH;
        default:
                return NT_NO_MATCH;
        }
        return NT_NO_MATCH;
}


struct node_match_item {
        bool follow_symlink;
        int file_type;
        int majj;
        int minn;
        char dir_name[D_NAME_LEN_MAX];
};

static struct node_match_item nd_match;

static int
nd_match_scandir_select(const struct dirent * s)
{
        bool symlnk = false;
        struct stat st;
        char name[D_NAME_LEN_MAX];

        switch (s->d_type) {
        case DT_BLK:
                if (FT_BLOCK != nd_match.file_type)
                        return 0;
                break;
        case DT_CHR:
                if (FT_CHAR != nd_match.file_type)
                        return 0;
                break;
        case DT_DIR:
                return (FT_DIR == nd_match.file_type) ? 1 : 0;
        case DT_REG:
                return (FT_REGULAR == nd_match.file_type) ? 1 : 0;
        case DT_LNK:    /* follow symlinks */
                if (! nd_match.follow_symlink)
                        return 0;
                symlnk = true;
                break;
        default:
                return 0;
        }
        if ((! symlnk) && (-1 == nd_match.majj) && (-1 == nd_match.minn))
                return 1;
        snprintf(name, sizeof(name), "%.*s/%.*s", NAME_LEN_MAX,
                 nd_match.dir_name, NAME_LEN_MAX, s->d_name);
        memset(&st, 0, sizeof(st));
        if (stat(name, &st) < 0)
                return 0;
        if (symlnk) {
                if (S_ISCHR(st.st_mode)) {
                        if (FT_CHAR != nd_match.file_type)
                                return 0;
                } else if (S_ISBLK(st.st_mode)) {
                        if (FT_BLOCK != nd_match.file_type)
                                return 0;
                } else
                        return 0;
        }
        return (((-1 == nd_match.majj) ||
                 ((unsigned)major(st.st_rdev) == (unsigned)nd_match.majj)) &&
                ((-1 == nd_match.minn) ||
                 ((unsigned)minor(st.st_rdev) == (unsigned)nd_match.minn)))
               ? 1 : 0;
}

static int
list_matching_nodes(const char * dir_name, int file_type, int majj, int minn,
                    bool follow_symlink, int verbose)
{
        struct dirent ** namelist;
        int num, k;

        strncpy(nd_match.dir_name, dir_name, D_NAME_LEN_MAX - 1);
        nd_match.file_type = file_type;
        nd_match.majj = majj;
        nd_match.minn = minn;
        nd_match.follow_symlink = follow_symlink;
        num = scandir(dir_name, &namelist, nd_match_scandir_select, NULL);
        if (num < 0) {
                if (verbose)
                        pr2serr("scandir: %s %s\n", dir_name,
                                ssafe_strerror(errno));
                return -errno;
        }
        for (k = 0; k < num; ++k) {
                printf("%s/%s\n", dir_name, namelist[k]->d_name);
                free(namelist[k]);
        }
        free(namelist);
        return num;
}

struct sg_item_t {
        char name[NAME_LEN_MAX + 2];
        int ft;
        int nt;
        int d_type;
};

static struct sg_item_t for_first;

static int
first_scandir_select(const struct dirent * s)
{
        if (FT_OTHER != for_first.ft)
                return 0;
        if ((DT_LNK != s->d_type) &&
            ((DT_DIR != s->d_type) || ('.' == s->d_name[0])))
                return 0;
        strncpy(for_first.name, s->d_name, NAME_LEN_MAX);
        for_first.ft = FT_CHAR;  /* dummy */
        for_first.d_type =  s->d_type;
        return 1;
}

/* scan for directory entry that is either a symlink or a directory */
static int
scan_for_first(const char * dir_name, int verbose)
{
        char name[NAME_LEN_MAX];
        struct dirent ** namelist;
        int num, k;

        for_first.ft = FT_OTHER;
        num = scandir(dir_name, &namelist, first_scandir_select, NULL);
        if (num < 0) {
                if (verbose > 0) {
                        snprintf(name, NAME_LEN_MAX, "scandir: %s", dir_name);
                        perror(name);
                }
                return -1;
        }
        for (k = 0; k < num; ++k)
                free(namelist[k]);
        free(namelist);
        return num;
}

static struct sg_item_t from_sg;

static int
from_sg_scandir_select(const struct dirent * s)
{
        int len;

        if (FT_OTHER != from_sg.ft)
                return 0;
        if ((DT_LNK != s->d_type) &&
            ((DT_DIR != s->d_type) || ('.' == s->d_name[0])))
                return 0;
        from_sg.d_type = s->d_type;
        if (0 == strncmp("scsi_changer", s->d_name, 12)) {
                strncpy(from_sg.name, s->d_name, NAME_LEN_MAX);
                from_sg.ft = FT_CHAR;
                from_sg.nt = NT_CH;
                return 1;
        } else if (0 == strncmp("block", s->d_name, 5)) {
                strncpy(from_sg.name, s->d_name, NAME_LEN_MAX);
                from_sg.ft = FT_BLOCK;
                return 1;
        } else if (0 == strcmp("tape", s->d_name)) {
                strcpy(from_sg.name, s->d_name);
                from_sg.ft = FT_CHAR;
                from_sg.nt = NT_ST;
                return 1;
        } else if (0 == strncmp("scsi_tape:st", s->d_name, 12)) {
                len = strlen(s->d_name);
                if (isdigit(s->d_name[len - 1])) {
                        /* want 'st<num>' symlink only */
                        strcpy(from_sg.name, s->d_name);
                        from_sg.ft = FT_CHAR;
                        from_sg.nt = NT_ST;
                        return 1;
                } else
                        return 0;
        } else if (0 == strncmp("onstream_tape:os", s->d_name, 16)) {
                strcpy(from_sg.name, s->d_name);
                from_sg.ft = FT_CHAR;
                from_sg.nt = NT_OSST;
                return 1;
        } else
                return 0;
}

static int
from_sg_scan(const char * dir_name, int verbose)
{
        struct dirent ** namelist;
        int num, k;

        from_sg.ft = FT_OTHER;
        from_sg.nt = NT_NO_MATCH;
        num = scandir(dir_name, &namelist, from_sg_scandir_select, NULL);
        if (num < 0) {
                if (verbose)
                        pr2serr("scandir: %s %s\n", dir_name,
                                ssafe_strerror(errno));
                return -errno;
        }
        if (verbose) {
                for (k = 0; k < num; ++k)
                        pr2serr("    %s/%s\n", dir_name,
                                namelist[k]->d_name);
        }
        for (k = 0; k < num; ++k)
                free(namelist[k]);
        free(namelist);
        return num;
}

static struct sg_item_t to_sg;

static int
to_sg_scandir_select(const struct dirent * s)
{
        if (FT_OTHER != to_sg.ft)
                return 0;
        if (DT_LNK != s->d_type)
                return 0;
        if (0 == strncmp("scsi_generic", s->d_name, 12)) {
                strncpy(to_sg.name, s->d_name, NAME_LEN_MAX);
                to_sg.ft = FT_CHAR;
                to_sg.nt = NT_SG;
                return 1;
        } else
                return 0;
}

static int
to_sg_scan(const char * dir_name)
{
        struct dirent ** namelist;
        int num, k;

        to_sg.ft = FT_OTHER;
        to_sg.nt = NT_NO_MATCH;
        num = scandir(dir_name, &namelist, to_sg_scandir_select, NULL);
        if (num < 0)
                return -errno;
        for (k = 0; k < num; ++k)
                free(namelist[k]);
        free(namelist);
        return num;
}

/* Return 1 if directory, else 0 */
static int
if_directory_chdir(const char * dir_name, const char * base_name)
{
        char buff[D_NAME_LEN_MAX];
        struct stat a_stat;

        strcpy(buff, dir_name);
        strcat(buff, "/");
        strcat(buff, base_name);
        if (stat(buff, &a_stat) < 0)
                return 0;
        if (S_ISDIR(a_stat.st_mode)) {
                if (chdir(buff) < 0)
                        return 0;
                return 1;
        }
        return 0;
}

/* Return 1 if directory, else 0 */
static int
if_directory_ch2generic(const char * dir_name)
{
        char buff[NAME_LEN_MAX];
        struct stat a_stat;
        const char * old_name = "generic";

        strcpy(buff, dir_name);
        strcat(buff, "/");
        strcat(buff, old_name);
        if ((stat(buff, &a_stat) >= 0) && S_ISDIR(a_stat.st_mode)) {
                if (chdir(buff) < 0)
                        return 0;
                return 1;
        }
        /* No "generic", so now look for "scsi_generic:sg<n>" */
        if (1 != to_sg_scan(dir_name))
                return 0;
        strcpy(buff, dir_name);
        strcat(buff, "/");
        strcat(buff, to_sg.name);
        if (stat(buff, &a_stat) < 0)
                return 0;
        if (S_ISDIR(a_stat.st_mode)) {
                if (chdir(buff) < 0)
                        return 0;
                return 1;
        }
        return 0;
}

/* Return 1 if found, else 0 if problems */
static int
get_value(const char * dir_name, const char * base_name, char * value,
          int max_value_len)
{
        char buff[D_NAME_LEN_MAX];
        FILE * f;
        int len;

        if ((NULL == dir_name) && (NULL == base_name))
                return 0;
        if (dir_name) {
                strcpy(buff, dir_name);
                if (base_name && (strlen(base_name) > 0)) {
                        strcat(buff, "/");
                        strcat(buff, base_name);
                }
        } else
                strcpy(buff, base_name);
        if (NULL == (f = fopen(buff, "r"))) {
                return 0;
        }
        if (NULL == fgets(value, max_value_len, f)) {
                fclose(f);
                return 0;
        }
        len = strlen(value);
        if ((len > 0) && (value[len - 1] == '\n'))
                value[len - 1] = '\0';
        fclose(f);
        return 1;
}

static int
map_hd(const char * device_dir, int ma, int mi, int op_result,
       bool follow_symlink, int verbose)
{
        char c, num;

        if (2 == op_result) {
                num = list_matching_nodes(device_dir, FT_BLOCK,
                                          ma, mi, follow_symlink,
                                          verbose);
                return (num > 0) ? 0 : 1;
        }
        switch (ma) {
        case IDE0_MAJOR: c = 'a'; break;
        case IDE1_MAJOR: c = 'c'; break;
        case IDE2_MAJOR: c = 'e'; break;
        case IDE3_MAJOR: c = 'g'; break;
        case IDE4_MAJOR: c = 'i'; break;
        case IDE5_MAJOR: c = 'k'; break;
        case IDE6_MAJOR: c = 'm'; break;
        case IDE7_MAJOR: c = 'o'; break;
        case IDE8_MAJOR: c = 'q'; break;
        case IDE9_MAJOR: c = 's'; break;
        default: c = '?'; break;
        }
        if (mi > 63)
                ++c;
        printf("%shd%c\n", sys_hd_dir, c);
        return 0;
}

static int
map_sd(const char * device_name, const char * device_dir, int ma, int mi,
       int op_result, bool follow_symlink, int verbose)
{
        int index, m_mi, m_ma, num, disc, disc2;
        char value[D_NAME_LEN_MAX];
        char name[D_NAME_LEN_MAX];

        if (2 == op_result) {
                num = list_matching_nodes(device_dir, FT_BLOCK, ma, mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        }
        /* see sd_major() in drivers/scsi/sd.c lk 6.2 */
        disc2 = 0xfff00 & mi;
        disc = 0xf & (mi >> 4);
        if (SCSI_DISK0_MAJOR == ma)
                index = disc + disc2;
        else if (ma >= SCSI_DISK8_MAJOR)
                index = disc + 128 +
                        ((ma - SCSI_DISK8_MAJOR) * 16) + disc2;
        else
                index = disc + 16 +
                        ((ma - SCSI_DISK1_MAJOR) * 16) + disc2;
        if (index < 26)
                snprintf(name, sizeof(name), "%ssd%c",
                         sys_sd_dir, 'a' + index % 26);
        else if (index < (26 + 1) * 26)
                snprintf(name, sizeof(name), "%ssd%c%c",
                         sys_sd_dir,
                         'a' + index / 26 - 1,'a' + index % 26);
        else {
                const unsigned int m1 = (index / 26 - 1) / 26 - 1;
                const unsigned int m2 = (index / 26 - 1) % 26;
                const unsigned int m3 =  index % 26;

                snprintf(name, sizeof(name), "%ssd%c%c%c",
                         sys_sd_dir, 'a' + m1, 'a' + m2, 'a' + m3);
        }
        if (3 == op_result) {
                printf("%s\n", name);
                return 0;
        }
        if (! get_value(name, "dev", value, sizeof(value))) {
                pr2serr("Couldn't find sysfs match for device: %s\n",
                        device_name);
                return 1;
        }
        if (verbose)
                pr2serr("sysfs sd dev: %s\n", value);
        if (! if_directory_chdir(name, "device")) {
                pr2serr("sysfs problem with device: %s\n", device_name);
                return 1;
        }
        if (if_directory_ch2generic(".")) {
                if (1 == op_result) {
                        if (NULL == getcwd(value, sizeof(value)))
                                value[0] = '\0';
                        printf("%s\n", value);
                        return 0;
                }
                if (! get_value(".", "dev", value, sizeof(value))) {
                        pr2serr("Couldn't find sysfs generic dev\n");
                        return 1;
                }
                if (verbose)
                        printf("matching dev: %s\n", value);
                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
                        pr2serr("Couldn't decode mapped dev\n");
                        return 1;
                }
                num = list_matching_nodes(device_dir, FT_CHAR, m_ma, m_mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        } else {
                pr2serr("sd device: %s does not match any SCSI generic "
                        "device\n", device_name);
                pr2serr("    perhaps sg module is not loaded\n");
                return 1;
        }
}

static int
map_sr(const char * device_name, const char * device_dir, int ma, int mi,
       int op_result, bool follow_symlink, int verbose)
{
        int m_mi, m_ma, num;
        char value[D_NAME_LEN_MAX];
        char name[D_NAME_LEN_MAX];

        if (2 == op_result) {
                num = list_matching_nodes(device_dir, FT_BLOCK, ma, mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        }
        snprintf(name, sizeof(name), "%ssr%d", sys_sr_dir, mi);
        if (3 == op_result) {
                printf("%s\n", name);
                return 0;
        }
        if (! get_value(name, "dev", value, sizeof(value))) {
                pr2serr("Couldn't find sysfs match for device: %s\n",
                        device_name);
                return 1;
        }
        if (verbose)
                pr2serr("sysfs sr dev: %s\n", value);
        if (! if_directory_chdir(name, "device")) {
                pr2serr("sysfs problem with device: %s\n", device_name);
                return 1;
        }
        if (if_directory_ch2generic(".")) {
                if (1 == op_result) {
                        if (NULL == getcwd(value, sizeof(value)))
                                value[0] = '\0';
                        printf("%s\n", value);
                        return 0;
                }
                if (! get_value(".", "dev", value, sizeof(value))) {
                        pr2serr("Couldn't find sysfs generic dev\n");
                        return 1;
                }
                if (verbose)
                        printf("matching dev: %s\n", value);
                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
                        pr2serr("Couldn't decode mapped dev\n");
                        return 1;
                }
                num = list_matching_nodes(device_dir, FT_BLOCK, m_ma, m_mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        } else {
                pr2serr("sr device: %s does not match any SCSI generic "
                        "device\n", device_name);
                pr2serr("    perhaps sg module is not loaded\n");
                return 1;
        }
}

static int
map_st(const char * device_name, const char * device_dir, int ma, int mi,
       int op_result, bool follow_symlink, int verbose)
{
        int m_mi, m_ma, num;
        char value[D_NAME_LEN_MAX];
        char name[D_NAME_LEN_MAX];

        if (2 == op_result) {
                num = list_matching_nodes(device_dir, FT_CHAR, ma, mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        }
        snprintf(name, sizeof(name), "%sst%d", sys_st_dir,
                 TAPE_NR(mi));
        if (3 == op_result) {
                printf("%s\n", name);
                return 0;
        }
        if (! get_value(name, "dev", value, sizeof(value))) {
                pr2serr("Couldn't find sysfs match for device: %s\n",
                        device_name);
                return 1;
        }
        if (verbose)
                pr2serr("sysfs st dev: %s\n", value);
        if (! if_directory_chdir(name, "device")) {
                pr2serr("sysfs problem with device: %s\n", device_name);
                return 1;
        }
        if (if_directory_ch2generic(".")) {
                if (1 == op_result) {
                        if (NULL == getcwd(value, sizeof(value)))
                                value[0] = '\0';
                        printf("%s\n", value);
                        return 0;
                }
                if (! get_value(".", "dev", value, sizeof(value))) {
                        pr2serr("Couldn't find sysfs generic dev\n");
                        return 1;
                }
                if (verbose)
                        printf("matching dev: %s\n", value);
                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
                        pr2serr("Couldn't decode mapped dev\n");
                        return 1;
                }
                num = list_matching_nodes(device_dir, FT_CHAR, m_ma, m_mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        } else {
                pr2serr("st device: %s does not match any SCSI generic "
                        "device\n", device_name);
                pr2serr("    perhaps sg module is not loaded\n");
                return 1;
        }
}

static int
map_osst(const char * device_name, const char * device_dir, int ma, int mi,
         int op_result, bool follow_symlink, int verbose)
{
        int m_mi, m_ma, num;
        char value[D_NAME_LEN_MAX];
        char name[D_NAME_LEN_MAX];

        if (2 == op_result) {
                num = list_matching_nodes(device_dir, FT_CHAR, ma, mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        }
        snprintf(name, sizeof(name), "%sosst%d", sys_osst_dir,
                 TAPE_NR(mi));
        if (3 == op_result) {
                printf("%s\n", name);
                return 0;
        }
        if (! get_value(name, "dev", value, sizeof(value))) {
                pr2serr("Couldn't find sysfs match for device: %s\n",
                        device_name);
                return 1;
        }
        if (verbose)
                pr2serr("sysfs osst dev: %s\n", value);
        if (! if_directory_chdir(name, "device")) {
                pr2serr("sysfs problem with device: %s\n", device_name);
                return 1;
        }
        if (if_directory_ch2generic(".")) {
                if (1 == op_result) {
                        if (NULL == getcwd(value, sizeof(value)))
                                value[0] = '\0';
                        printf("%s\n", value);
                        return 0;
                }
                if (! get_value(".", "dev", value, sizeof(value))) {
                        pr2serr("Couldn't find sysfs generic dev\n");
                        return 1;
                }
                if (verbose)
                        printf("matching dev: %s\n", value);
                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
                        pr2serr("Couldn't decode mapped dev\n");
                        return 1;
                }
                num = list_matching_nodes(device_dir, FT_CHAR, m_ma, m_mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        } else {
                pr2serr("osst device: %s does not match any SCSI generic "
                        "device\n", device_name);
                pr2serr("    perhaps sg module is not loaded\n");
                return 1;
        }
}

static int
map_ch(const char * device_name, const char * device_dir, int ma, int mi,
       int op_result, bool follow_symlink, int verbose)
{
        int m_mi, m_ma, num;
        char value[D_NAME_LEN_MAX];
        char name[D_NAME_LEN_MAX];

        if (2 == op_result) {
                num = list_matching_nodes(device_dir, FT_CHAR, ma, mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        }
        snprintf(name, sizeof(name), "%ssch%d", sys_sch_dir, mi);
        if (3 == op_result) {
                printf("%s\n", name);
                return 0;
        }
        if (! get_value(name, "dev", value, sizeof(value))) {
                pr2serr("Couldn't find sysfs match for device: %s\n",
                        device_name);
                return 1;
        }
        if (verbose)
                pr2serr("sysfs sch dev: %s\n", value);
        if (! if_directory_chdir(name, "device")) {
                pr2serr("sysfs problem with device: %s\n", device_name);
                return 1;
        }
        if (if_directory_ch2generic(".")) {
                if (1 == op_result) {
                        if (NULL == getcwd(value, sizeof(value)))
                                value[0] = '\0';
                        printf("%s\n", value);
                        return 0;
                }
                if (! get_value(".", "dev", value, sizeof(value))) {
                        pr2serr("Couldn't find sysfs generic dev\n");
                        return 1;
                }
                if (verbose)
                        printf("matching dev: %s\n", value);
                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
                        pr2serr("Couldn't decode mapped dev\n");
                        return 1;
                }
                num = list_matching_nodes(device_dir, FT_CHAR, m_ma, m_mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        } else {
                pr2serr("sch device: %s does not match any SCSI generic "
                        "device\n", device_name);
                pr2serr("    perhaps sg module is not loaded\n");
                return 1;
        }
}

static int
map_sg(const char * device_name, const char * device_dir, int ma, int mi,
       int op_result, bool follow_symlink, int verbose)
{
        int m_mi, m_ma, num;
        char value[D_NAME_LEN_MAX];
        char name[D_NAME_LEN_MAX];

        if (2 == op_result) {
                num = list_matching_nodes(device_dir, FT_CHAR, ma, mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        }
        snprintf(name, sizeof(name), "%ssg%d", sys_sg_dir, mi);
        if (3 == op_result) {
                printf("%s\n", name);
                return 0;
        }
        if (! get_value(name, "dev", value, sizeof(value))) {
                pr2serr("Couldn't find sysfs match for device: %s\n",
                        device_name);
                return 1;
        }
        if (verbose)
                pr2serr("sysfs sg dev: %s\n", value);
        if (! if_directory_chdir(name, "device")) {
                pr2serr("sysfs problem with device: %s\n", device_name);
                return 1;
        }
        if ((1 == from_sg_scan(".", verbose)) &&
            (if_directory_chdir(".", from_sg.name))) {
                if (DT_DIR == from_sg.d_type) {
                        if ((1 == scan_for_first(".", verbose)) &&
                            (if_directory_chdir(".", for_first.name))) {
                                ;
                        } else {
                                pr2serr("unexpected scan_for_first error\n");
                        }
                }
                if (1 == op_result) {
                        if (NULL == getcwd(value, sizeof(value)))
                                value[0] = '\0';
                        printf("%s\n", value);
                        return 0;
                }
                if (! get_value(".", "dev", value, sizeof(value))) {
                        pr2serr("Couldn't find sysfs block dev\n");
                        return 1;
                }
                if (verbose)
                        printf("matching dev: %s\n", value);
                if (2 != sscanf(value, "%d:%d", &m_ma, &m_mi)) {
                        pr2serr("Couldn't decode mapped dev\n");
                        return 1;
                }
                num = list_matching_nodes(device_dir, from_sg.ft, m_ma, m_mi,
                                          follow_symlink, verbose);
                return (num > 0) ? 0 : 1;
        } else {
                pr2serr("sg device: %s does not match any other SCSI "
                        "device\n", device_name);
                return 1;
        }
}

/* Attempts to map bsg device of the form /dev/bsg/h:c:t:l to the
 * corresponding device. If 2!=op_result, first looks for a matching sd
 * device (for disks), then for a sr device (for bd/dvd/cd), then for a st
 * device (for tape drives). If no match is found, or 2==op_result then
 * checks for a matching sg device. Always returns 0 (probably shouldn't). */
static int
map_bsg(const char * device_name, const char * device_dir, int ma, int mi,
        int op_result, bool follow_symlink, int vb)
{
        bool found = false;
        int n;
        char * cp;
        char bsname[D_NAME_LEN_MAX];
        char b[196];
        char value[D_NAME_LEN_MAX];
        static const int bsname_len = sizeof(bsname);
        static const int blen = sizeof(b);
        static const int vlen = sizeof(value);

        if (vb > 3) {
                pr2serr("%s: device_name: %s, device_dir: %s\n", __func__,
                        device_name, device_dir);
                pr2serr("   major=%d, minor=%d, op_result=%d, follow_symlink"
                        "=%d\n", ma, mi, op_result, (int)follow_symlink);
        }
        strncpy(bsname, device_name, bsname_len);
        cp = basename(bsname);
        if (vb > 2)
                pr2serr("%s: basename: %s\n", __func__, cp);
        snprintf(b, blen, "%s%s/device", sys_scsi_dev_dir, cp);
        if (2 == op_result)
            goto skip;
        if (if_directory_chdir(b, "block")) {
                /* should get both sd and sr driver devices */
                n = scan_for_first(".", vb);
                if (1 == n)
                        printf("/dev/%s\n", for_first.name);
                if (vb > 1)
                        pr2serr("%s: expected 1, found %d\n", __func__, n);
                found = true;
        } else if (if_directory_chdir(b, "tape")) {
                snprintf(bsname, bsname_len, "%s/tape", b);
                if (get_value(bsname, "dev", value, vlen)) {
                        int maj, min;

                        if (2 == sscanf(value, "%d:%d", &maj, &min)) {
                                n = list_matching_nodes("/dev", FT_CHAR, maj,
                                                        min, false, vb);
                                if (vb > 2)
                                    pr2serr("%s: found tape matches: %d\n",
                                            __func__, n);
                        }
                } else if (vb > 0)
                        pr2serr("%s: tape failed to fetch ../dev\n",
                                __func__);
                found = true;
        }
skip:
        if ((! found) && if_directory_chdir(b, "scsi_generic")) {
                n = scan_for_first(".", vb);
                if (1 == n)
                        printf("/dev/%s\n", for_first.name);
                if (vb > 1)
                        pr2serr("%s: expected 1, found %d\n", __func__, n);
        }
        return 0;
}

int
main(int argc, char * argv[])
{
        bool cont;
        int c, num, tt, res;
        int given_is = -1;
        int opt_result = 0;
        int verbose = 0;
        int ret = 1;
        int ma, mi;
        bool do_dev_dir = false;
        bool follow_symlink = false;
        char device_name[D_NAME_LEN_MAX];
        char device_dir[D_NAME_LEN_MAX];
        char value[D_NAME_LEN_MAX];

        memset(device_name, 0, sizeof(device_name));
        memset(device_dir, 0, sizeof(device_dir));
        while (1) {
                int option_index = 0;

                c = getopt_long(argc, argv, "d:hg:r:svV", long_options,
                                &option_index);
                if (c == -1)
                        break;

                switch (c) {
                case 'd':
                        strncpy(device_dir, optarg, sizeof(device_dir) - 1);
                        do_dev_dir = true;
                        break;
                case 'g':
                        num = sscanf(optarg, "%d", &res);
                        if ((1 == num) && ((0 == res) || (1 == res)))
                                given_is = res;
                        else {
                                pr2serr("value for '--given_to=' must be 0 "
                                        "or 1\n");
                                return SG_LIB_SYNTAX_ERROR;
                        }
                        break;
                case 'h':
                case '?':
                        usage();
                        return 0;
                case 'r':
                        num = sscanf(optarg, "%d", &res);
                        if ((1 == num) && (res >= 0) && (res < 4))
                                opt_result = res;
                        else {
                                pr2serr("value for '--result=' must be "
                                        "0..3\n");
                                return SG_LIB_SYNTAX_ERROR;
                        }
                        break;
                case 's':
                        follow_symlink = true;
                        break;
                case 'v':
                        ++verbose;
                        break;
                case 'V':
                        pr2serr(ME "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 ('\0' == device_name[0]) {
                        strncpy(device_name, argv[optind],
                                sizeof(device_name) - 1);
                        device_name[sizeof(device_name) - 1] = '\0';
                        ++optind;
                }
                if (optind < argc) {
                        for (; optind < argc; ++optind)
                                pr2serr("Unexpected extra argument: %s\n",
                                        argv[optind]);
                        usage();
                        return SG_LIB_SYNTAX_ERROR;
                }
        }

        if (0 == device_name[0]) {
                pr2serr("missing device name!\n");
                usage();
                return SG_LIB_SYNTAX_ERROR;
        }

        ma = 0;
        mi = 0;
        if (do_dev_dir) {
                if (if_directory_chdir(".", device_dir)) {
                        if (getcwd(device_dir, sizeof(device_dir)))
                                device_dir[sizeof(device_dir) - 1] = '\0';
                        else
                                device_dir[0] = '\0';
                        if (verbose > 1)
                                pr2serr("Absolute path to dev_dir: %s\n",
                                        device_dir);
                } else {
                        pr2serr("dev_dir: %s invalid\n", device_dir);
                        return SG_LIB_FILE_ERROR;
                }
        } else {
                strcpy(device_dir, device_name);
                dirname(device_dir);
                if (0 == strcmp(device_dir, device_name)) {
                        if (NULL == getcwd(device_dir, sizeof(device_dir)))
                                device_dir[0] = '\0';
                }
        }
        ret = nt_typ_from_filename(device_name, &ma, &mi);
        if (ret < 0) {
                pr2serr("stat failed on %s: %s\n", device_name,
                        ssafe_strerror(-ret));
                return SG_LIB_FILE_ERROR;
        }
        if (verbose)
                pr2serr(" %s: %s device [maj=%d, min=%d]\n", device_name,
                        nt_names[ret], ma, mi);
        res = 0;
        switch (ret) {
        case NT_SD:
        case NT_SR:
        case NT_HD:
                if (given_is > 0) {
                        pr2serr("block special but '--given_is=' suggested "
                                "sysfs device\n");
                        return SG_LIB_FILE_ERROR;
                }
                break;
        case NT_ST:
        case NT_OSST:
        case NT_CH:
        case NT_SG:
        case NT_BSG:
        case NT_NVME:
        case NT_NVME_GEN:
                if (given_is > 0) {
                        pr2serr("character special but '--given_is=' "
                                "suggested sysfs device\n");
                        return SG_LIB_FILE_ERROR;
                }
                break;
        case NT_REG:
                if (0 == given_is) {
                        pr2serr("regular file but '--given_is=' suggested "
                                "block or char special\n");
                        return SG_LIB_FILE_ERROR;
                }
                strcpy(device_dir, def_dev_dir);
                break;
        case NT_DIR:
                if (0 == given_is) {
                        pr2serr("directory but '--given_is=' suggested "
                                "block or char special\n");
                        return SG_LIB_FILE_ERROR;
                }
                strcpy(device_dir, def_dev_dir);
                break;
        default:
                break;
        }

        tt = NT_NO_MATCH;
        do {
                cont = false;
                switch (ret) {
                case NT_NO_MATCH:
                        res = 1;
                        break;
                case NT_SD:
                        res = map_sd(device_name, device_dir, ma, mi,
                                     opt_result, follow_symlink, verbose);
                        break;
                case NT_SR:
                        res = map_sr(device_name, device_dir, ma, mi,
                                     opt_result, follow_symlink, verbose);
                        break;
                case NT_HD:
                        if (opt_result < 2) {
                                pr2serr("a hd device does not map to a sg "
                                        "device\n");
                                return SG_LIB_FILE_ERROR;
                        }
                        res = map_hd(device_dir, ma, mi, opt_result,
                                     follow_symlink, verbose);
                        break;
                case NT_ST:
                        res = map_st(device_name, device_dir, ma, mi,
                                     opt_result, follow_symlink, verbose);
                        break;
                case NT_OSST:
                        res = map_osst(device_name, device_dir, ma, mi,
                                       opt_result, follow_symlink, verbose);
                        break;
                case NT_CH:
                        res = map_ch(device_name, device_dir, ma, mi,
                                     opt_result, follow_symlink, verbose);
                        break;
                case NT_SG:
                        res = map_sg(device_name, device_dir, ma, mi,
                                     opt_result, follow_symlink, verbose);
                        break;
                case NT_BSG:
                        res = map_bsg(device_name, device_dir, ma, mi,
                                      opt_result, follow_symlink, verbose);
                        break;
                case NT_NVME_GEN:
                        pr2serr("Can't map NVMe generic device: %s\n",
                                device_name);
                        break;
                case NT_NVME:
                        pr2serr("Can't map NVMe device: %s\n", device_name);
                        break;
                case NT_REG:
                        if (! get_value(NULL, device_name, value,
                                        sizeof(value))) {
                                pr2serr("Couldn't fetch value from: %s\n",
                                        device_name);
                                return SG_LIB_FILE_ERROR;
                        }
                        if (verbose)
                                pr2serr("value: %s\n", value);
                        if (2 != sscanf(value, "%d:%d", &ma, &mi)) {
                                pr2serr("Couldn't decode value\n");
                                return SG_LIB_FILE_ERROR;
                        }
                        tt = nt_typ_from_major(ma);
                        cont = true;
                        break;
                case NT_DIR:
                        if (! get_value(device_name, "dev", value,
                                        sizeof(value))) {
                                pr2serr("Couldn't fetch value from: %s/dev\n",
                                        device_name);
                                return SG_LIB_FILE_ERROR;
                        }
                        if (verbose)
                                pr2serr("value: %s\n", value);
                        if (2 != sscanf(value, "%d:%d", &ma, &mi)) {
                                pr2serr("Couldn't decode value\n");
                                return SG_LIB_FILE_ERROR;
                        }
                        tt = nt_typ_from_major(ma);
                        cont = true;
                        break;
                default:
                        break;
                }
                ret = tt;
        } while (cont);
        return res;
}
