/*
 * Utility program for the Linux OS SCSI generic ("sg") device driver.
 *     Copyright (C) 2000-2023 D. Gilbert
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later

   This shows the mapping from "sg" devices to other scsi devices
   (i.e. sd, scd or st) if any.

   Note: This program requires sg version 2 or better.

   Version 0.19 20041203

   Version 1.02 20050511
        - allow for sparse disk name with up to 3 letter SCSI
          disk device node names (e.g. /dev/sdaaa)
          [Nate Dailey < Nate dot Dailey at stratus dot com >]
*/

#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 <errno.h>
#include <dirent.h>
#include <libgen.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "sg_lib.h"
#include "sg_cmds_basic.h"
#include "sg_io_linux.h"


static const char * version_str = "1.14 20230130";

static const char * devfs_id = "/dev/.devfsd";

#define NUMERIC_SCAN_DEF true /* set to false to make alpha scan default */

#define INQUIRY_RESP_INITIAL_LEN 36
#define MAX_SG_DEVS 4096
#define PRESENT_ARRAY_SIZE MAX_SG_DEVS

static const char * sysfs_sg_dir = "/sys/class/scsi_generic";
static char gen_index_arr[PRESENT_ARRAY_SIZE];
static int has_sysfs_sg = 0;


typedef struct my_map_info
{
    int active;
    int lin_dev_type;
    int oth_dev_num;
    struct sg_scsi_id sg_dat;
    char vendor[8];
    char product[16];
    char revision[4];
} my_map_info_t;


#define MAX_SD_DEVS (26 + 26*26 + 26*26*26) /* sdX, sdXX, sdXXX */
                 /* (26 + 676 + 17576) = 18278 */
#define MAX_SR_DEVS 128
#define MAX_ST_DEVS 128
#define MAX_OSST_DEVS 128
#define MAX_ERRORS 5

static my_map_info_t map_arr[MAX_SG_DEVS];

#define LIN_DEV_TYPE_UNKNOWN 0
#define LIN_DEV_TYPE_SD 1
#define LIN_DEV_TYPE_SR 2
#define LIN_DEV_TYPE_ST 3
#define LIN_DEV_TYPE_SCD 4
#define LIN_DEV_TYPE_OSST 5


typedef struct my_scsi_idlun {
/* why can't userland see this structure ??? */
    int dev_id;
    int host_unique_id;
} My_scsi_idlun;


#define EBUFF_SZ 256
static char ebuff[EBUFF_SZ];

static void scan_dev_type(const char * leadin, int max_dev, bool do_numeric,
                          int lin_dev_type, int last_sg_ind);

static void usage()
{
    printf("Usage: sg_map [-a] [-h] [-i] [-n] [-sd] [-scd or -sr] [-st] "
           "[-V] [-x]\n");
    printf("  where:\n");
    printf("    -a      do alphabetic scan (ie sga, sgb, sgc)\n");
    printf("    -h or -?    show this usage message then exit\n");
    printf("    -i      also show device INQUIRY strings\n");
    printf("    -n      do numeric scan (i.e. sg0, sg1, sg2) "
           "(default)\n");
    printf("    -sd     show mapping to disks\n");
    printf("    -scd    show mapping to cdroms (look for /dev/scd<n>\n");
    printf("    -sr     show mapping to cdroms (look for /dev/sr<n>\n");
    printf("    -st     show mapping to tapes (st and osst devices)\n");
    printf("    -V      print version string then exit\n");
    printf("    -x      also show bus,chan,id,lun and type\n\n");
    printf("If no '-s*' arguments given then show all mappings. This "
           "utility\nis DEPRECATED, do not use in Linux 2.6 series or "
           "later.\n");
}

static int scandir_select(const struct dirent * s)
{
    int k;

    if (1 == sscanf(s->d_name, "sg%d", &k)) {
        if ((k >= 0) && (k < PRESENT_ARRAY_SIZE)) {
            gen_index_arr[k] = 1;
            return 1;
        }
    }
    return 0;
}

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

    num = scandir(dir_name, &namelist, scandir_select, NULL);
    if (num < 0)
        return -errno;
    for (k = 0; k < num; ++k)
        free(namelist[k]);
    free(namelist);
    return num;
}

static void make_dev_name(char * fname, const char * leadin, int k,
                          bool do_numeric)
{
    char buff[64];
    int  ones,tens,hundreds; /* for lack of a better name */
    int  buff_idx;

    strcpy(fname, leadin ? leadin : "/dev/sg");
    if (do_numeric) {
        sprintf(buff, "%d", k);
        strcat(fname, buff);
    }
    else if (k >= (26 + 26*26 + 26*26*26)) {
        strcat(fname, "xxxx");
    }
    else {
        ones = k % 26;

        if ((k - 26) >= 0)
            tens = ((k-26)/26) % 26;
        else tens = -1;

        if ((k - (26 + 26*26)) >= 0)
             hundreds = ((k - (26 + 26*26))/(26*26)) % 26;
        else hundreds = -1;

        buff_idx = 0;
        if (hundreds >= 0) buff[buff_idx++] = 'a' + (char)hundreds;
        if (tens >= 0) buff[buff_idx++] = 'a' + (char)tens;
        buff[buff_idx++] = 'a' + (char)ones;
        buff[buff_idx] = '\0';
        strcat(fname, buff);
    }
}


int main(int argc, char * argv[])
{
    bool do_all_s = true;
    bool do_extra = false;
    bool do_inquiry = false;
    bool do_numeric = NUMERIC_SCAN_DEF;
    bool do_osst = false;
    bool do_scd = false;
    bool do_sd = false;
    bool do_sr = false;
    bool do_st = false;
    bool eacces_err = false;
    int sg_fd, res, k;
    int num_errors = 0;
    int num_silent = 0;
    int last_sg_ind = -1;
    char fname[64];
    struct stat a_stat;

    for (k = 1; k < argc; ++k) {
        if (0 == strcmp("-n", argv[k]))
            do_numeric = true;
        else if (0 == strcmp("-a", argv[k]))
            do_numeric = false;
        else if (0 == strcmp("-x", argv[k]))
            do_extra = true;
        else if (0 == strcmp("-i", argv[k]))
            do_inquiry = true;
        else if (0 == strcmp("-sd", argv[k])) {
            do_sd = true;
            do_all_s = false;
        } else if (0 == strcmp("-st", argv[k])) {
            do_st = true;
            do_osst = true;
            do_all_s = false;
        } else if (0 == strcmp("-sr", argv[k])) {
            do_sr = true;
            do_all_s = false;
        } else if (0 == strcmp("-scd", argv[k])) {
            do_scd = true;
            do_all_s = false;
        } else if (0 == strcmp("-V", argv[k])) {
            fprintf(stderr, "Version string: %s\n", version_str);
            exit(0);
        } else if ((0 == strcmp("-?", argv[k])) ||
                   (0 == strncmp("-h", argv[k], 2))) {
            printf(
            "Show mapping from sg devices to other scsi device names\n\n");
            usage();
            return SG_LIB_SYNTAX_ERROR;
        } else if (*argv[k] == '-') {
            printf("Unknown switch: %s\n", argv[k]);
            usage();
            return SG_LIB_SYNTAX_ERROR;
        } else if (*argv[k] != '-') {
            printf("Unknown argument\n");
            usage();
            return SG_LIB_SYNTAX_ERROR;
        }
    }

    if ((stat(sysfs_sg_dir, &a_stat) >= 0) && (S_ISDIR(a_stat.st_mode)))
        has_sysfs_sg = sysfs_sg_scan(sysfs_sg_dir);

    if (stat(devfs_id, &a_stat) == 0)
        printf("# Note: the devfs pseudo file system is present\n");

    for (k = 0, res = 0; (k < MAX_SG_DEVS) && (num_errors < MAX_ERRORS);
         ++k, res = (sg_fd >= 0) ? close(sg_fd) : 0) {
        if (res < 0) {
            snprintf(ebuff, EBUFF_SZ, "Error closing %s ", fname);
            perror("sg_map: close error");
            return SG_LIB_FILE_ERROR;
        }
        if (has_sysfs_sg) {
           if (0 == gen_index_arr[k]) {
                sg_fd = -1;
                continue;
            }
            make_dev_name(fname, "/dev/sg", k, true);
        } else
            make_dev_name(fname, "/dev/sg", k, do_numeric);

        sg_fd = open(fname, O_RDONLY | O_NONBLOCK);
        if (sg_fd < 0) {
            if (EBUSY == errno) {
                map_arr[k].active = -2;
                continue;
            }
            else if ((ENODEV == errno) || (ENOENT == errno) ||
                     (ENXIO == errno)) {
                ++num_errors;
                ++num_silent;
                map_arr[k].active = -1;
                continue;
            }
            else {
                if (EACCES == errno)
                    eacces_err = true;
                snprintf(ebuff, EBUFF_SZ, "Error opening %s ", fname);
                perror(ebuff);
                ++num_errors;
                continue;
            }
        }
        res = ioctl(sg_fd, SG_GET_SCSI_ID, &map_arr[k].sg_dat);
        if (res < 0) {
            snprintf(ebuff, EBUFF_SZ,
                     "device %s failed on sg ioctl, skip", fname);
            perror(ebuff);
            ++num_errors;
            continue;
        }
        if (do_inquiry) {
            char buff[INQUIRY_RESP_INITIAL_LEN];

            if (0 == sg_ll_inquiry(sg_fd, false, false, 0, buff, sizeof(buff),
                                   true, 0)) {
                memcpy(map_arr[k].vendor, &buff[8], 8);
                memcpy(map_arr[k].product, &buff[16], 16);
                memcpy(map_arr[k].revision, &buff[32], 4);
            }
        }
        map_arr[k].active = 1;
        map_arr[k].oth_dev_num = -1;
        last_sg_ind = k;
    }
    if ((num_errors >= MAX_ERRORS) && (num_silent < num_errors)) {
        printf("Stopping because there are too many error\n");
        printf("[number silent: %d, number of errors: %d]\n",
               num_silent, num_errors);
        if (eacces_err)
            printf("    root access may be required\n");
        return SG_LIB_FILE_ERROR;
    }
    if (last_sg_ind < 0) {
        printf("Stopping because no sg devices found\n");
    }

    if (do_all_s || do_sd)
        scan_dev_type("/dev/sd", MAX_SD_DEVS, 0, LIN_DEV_TYPE_SD, last_sg_ind);
    if (do_all_s || do_sr)
        scan_dev_type("/dev/sr", MAX_SR_DEVS, 1, LIN_DEV_TYPE_SR, last_sg_ind);
    if (do_all_s || do_scd)
        scan_dev_type("/dev/scd", MAX_SR_DEVS, 1, LIN_DEV_TYPE_SCD,
                      last_sg_ind);
    if (do_all_s || do_st)
        scan_dev_type("/dev/nst", MAX_ST_DEVS, 1, LIN_DEV_TYPE_ST,
                      last_sg_ind);
    if (do_all_s || do_osst)
        scan_dev_type("/dev/osst", MAX_OSST_DEVS, 1, LIN_DEV_TYPE_OSST,
                      last_sg_ind);

    for (k = 0; k <= last_sg_ind; ++k) {
        if (has_sysfs_sg) {
           if (0 == gen_index_arr[k]) {
                continue;
            }
            make_dev_name(fname, "/dev/sg", k, true);
        } else
            make_dev_name(fname, "/dev/sg", k, do_numeric);
        printf("%s", fname);
        switch (map_arr[k].active)
        {
        case -2:
            printf(do_extra ? "  -2 -2 -2 -2  -2" : "  busy");
            break;
        case -1:
            printf(do_extra ? "  -1 -1 -1 -1  -1" : "  not present");
            break;
        case 0:
            printf(do_extra ? "  -3 -3 -3 -3  -3" : "  some error");
            break;
        case 1:
            if (do_extra)
                printf("  %d %d %d %d  %d", map_arr[k].sg_dat.host_no,
                       map_arr[k].sg_dat.channel, map_arr[k].sg_dat.scsi_id,
                       map_arr[k].sg_dat.lun, map_arr[k].sg_dat.scsi_type);
            switch (map_arr[k].lin_dev_type)
            {
            case LIN_DEV_TYPE_SD:
                make_dev_name(fname, "/dev/sd" , map_arr[k].oth_dev_num, 0);
                printf("  %s", fname);
                break;
            case LIN_DEV_TYPE_ST:
                make_dev_name(fname, "/dev/nst" , map_arr[k].oth_dev_num, 1);
                printf("  %s", fname);
                break;
            case LIN_DEV_TYPE_OSST:
                make_dev_name(fname, "/dev/osst" , map_arr[k].oth_dev_num, 1);
                printf("  %s", fname);
                break;
            case LIN_DEV_TYPE_SR:
                make_dev_name(fname, "/dev/sr" , map_arr[k].oth_dev_num, 1);
                printf("  %s", fname);
                break;
            case LIN_DEV_TYPE_SCD:
                make_dev_name(fname, "/dev/scd" , map_arr[k].oth_dev_num, 1);
                printf("  %s", fname);
                break;
            default:
                break;
            }
            if (do_inquiry)
                printf("  %.8s  %.16s  %.4s", map_arr[k].vendor,
                       map_arr[k].product, map_arr[k].revision);
            break;
        default:
            printf("  bad logic\n");
            break;
        }
        printf("\n");
    }
    return 0;
}

static int find_dev_in_sg_arr(My_scsi_idlun * my_idlun, int host_no,
                              int last_sg_ind)
{
    int k;
    struct sg_scsi_id * sidp;

    for (k = 0; k <= last_sg_ind; ++k) {
        sidp = &(map_arr[k].sg_dat);
        if ((host_no == sidp->host_no) &&
            ((my_idlun->dev_id & 0xff) == sidp->scsi_id) &&
            (((my_idlun->dev_id >> 8) & 0xff) == sidp->lun) &&
            (((my_idlun->dev_id >> 16) & 0xff) == sidp->channel))
            return k;
    }
    return -1;
}

static void scan_dev_type(const char * leadin, int max_dev, bool do_numeric,
                          int lin_dev_type, int last_sg_ind)
{
    int k, res, ind, sg_fd = 0;
    int num_errors = 0;
    int num_silent = 0;
    int host_no = -1;
    My_scsi_idlun my_idlun;
    char fname[64];

    for (k = 0, res = 0; (k < max_dev)  && (num_errors < MAX_ERRORS);
         ++k, res = (sg_fd >= 0) ? close(sg_fd) : 0) {

/* ignore close() errors */
#if 0
        if (res < 0) {
            snprintf(ebuff, EBUFF_SZ, "Error closing %s ", fname);
            perror("sg_map: close error");
#ifndef IGN_CLOSE_ERR
            return;
#else
            ++num_errors;
            sg_fd = 0;
#endif
        }
#else
            if (res) { }        /* dummy to stop res unused warning */
#endif
        make_dev_name(fname, leadin, k, do_numeric);
#ifdef DEBUG
        printf ("Trying %s: ", fname);
#endif

        sg_fd = open(fname, O_RDONLY | O_NONBLOCK);
        if (sg_fd < 0) {
#ifdef DEBUG
            printf ("ERROR %i\n", errno);
#endif
            if (EBUSY == errno) {
                printf("Device %s is busy\n", fname);
                ++num_errors;
            } else if ((ENODEV == errno) || (ENXIO == errno)) {
                ++num_errors;
                ++num_silent;
            } else if (ENOENT != errno) { /* ignore ENOENT for sparse names */
                snprintf(ebuff, EBUFF_SZ, "Error opening %s ", fname);
                perror(ebuff);
                ++num_errors;
            }
            continue;
        }

        res = ioctl(sg_fd, SCSI_IOCTL_GET_IDLUN, &my_idlun);
        if (res < 0) {
            snprintf(ebuff, EBUFF_SZ,
                     "device %s failed on scsi ioctl(idlun), skip", fname);
            perror(ebuff);
            ++num_errors;
#ifdef DEBUG
            printf ("Couldn't get IDLUN!\n");
#endif
            continue;
        }
        res = ioctl(sg_fd, SCSI_IOCTL_GET_BUS_NUMBER, &host_no);
        if (res < 0) {
            snprintf(ebuff, EBUFF_SZ,
                 "device %s failed on scsi ioctl(bus_number), skip", fname);
            perror(ebuff);
            ++num_errors;
#ifdef DEBUG
            printf ("Couldn't get BUS!\n");
#endif
            continue;
        }
#ifdef DEBUG
        printf ("%i(%x) %i %i %i %i\n", host_no, my_idlun.host_unique_id,
                (my_idlun.dev_id>>24)&0xff, (my_idlun.dev_id>>16)&0xff,
                (my_idlun.dev_id>>8)&0xff, my_idlun.dev_id&0xff);
#endif
        ind = find_dev_in_sg_arr(&my_idlun, host_no, last_sg_ind);
        if (ind >= 0) {
            map_arr[ind].oth_dev_num = k;
            map_arr[ind].lin_dev_type = lin_dev_type;
        } else {
            printf("Strange, could not find device %s mapped to sg device??\n",
                   fname);
            printf("[number silent: %d, number of errors: %d]\n",
                   num_silent, num_errors);
        }
    }
}
