/* $NetBSD: uscsi_subr.c,v 1.5 2022/05/28 21:14:57 andvar Exp $ */

/*-
* Copyright (c) 1998 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Charles M. Hannum; Jason R. Thorpe of the Numerical Aerospace
* Simulation Facility, NASA Ames Research Center.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* Small changes, generalisations and Linux support by Reinoud Zandijk
* <[email protected]>.
*
*/


/*
* SCSI support subroutines.
*/

#include <sys/param.h>
#include <sys/ioctl.h>
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <inttypes.h>
#include <assert.h>

#include "uscsilib.h"


int uscsilib_verbose = 0;


#ifdef USCSI_SCSIPI
       /*
        * scsipi is a integrated SCSI and ATAPI layer under NetBSD and exists
        * in a modified form under OpenBSD and possibly also under other
        * operating systems.
        */


#include <sys/scsiio.h>
#ifdef __OpenBSD__
#include <scsi/uscsi_all.h>
#else
#include <dev/scsipi/scsipi_all.h>
#endif


int
uscsi_open(struct uscsi_dev *disc)
{
       struct stat dstat;

       disc->fhandle = open(disc->dev_name, O_RDWR, 0); /* no create */
       if (disc->fhandle<0) {
               perror("Failure to open device or file");
               return ENODEV;
       }

       if (fstat(disc->fhandle, &dstat) < 0) {
               perror("Can't stat device or file");
               uscsi_close(disc);
               return ENODEV;
       }

       return 0;
}


int
uscsi_close(struct uscsi_dev * disc)
{
       close(disc->fhandle);
       disc->fhandle = -1;

       return 0;
}


int
uscsi_command(int flags, struct uscsi_dev *disc,
       void *cmd, size_t cmdlen, void *data, size_t datalen,
       uint32_t timeout, struct uscsi_sense *uscsi_sense)
{
       scsireq_t req;

       memset(&req, 0, sizeof(req));
       if (uscsi_sense)
               bzero(uscsi_sense, sizeof(struct uscsi_sense));

       memcpy(req.cmd, cmd, cmdlen);
       req.cmdlen = cmdlen;
       req.databuf = data;
       req.datalen = datalen;
       req.timeout = timeout;
       req.flags = flags;
       req.senselen = SENSEBUFLEN;

       if (ioctl(disc->fhandle, SCIOCCOMMAND, &req) == -1)
               err(1, "SCIOCCOMMAND");

       if (req.retsts == SCCMD_OK)
               return 0;

       /* Some problem; report it and exit. */
       if (req.retsts == SCCMD_TIMEOUT) {
               if (uscsilib_verbose)
                       fprintf(stderr, "%s: SCSI command timed out\n",
                               disc->dev_name);
               return EAGAIN;
       } else if (req.retsts == SCCMD_BUSY) {
               if (uscsilib_verbose)
                       fprintf(stderr, "%s: device is busy\n",
                               disc->dev_name);
               return EBUSY;
       } else if (req.retsts == SCCMD_SENSE) {
               if (uscsi_sense) {
                       uscsi_sense->asc        =  req.sense[12];
                       uscsi_sense->ascq       =  req.sense[13];
                       uscsi_sense->skey_valid =  req.sense[15] & 128;
                       uscsi_sense->sense_key  = (req.sense[16] << 8) |
                                                 (req.sense[17]);
               }
               if (uscsilib_verbose)
                       uscsi_print_sense((char *) disc->dev_name,
                               req.cmd, req.cmdlen,
                               req.sense, req.senselen_used, 1);
               return EIO;
       } else
               if (uscsilib_verbose)
                       fprintf(stderr, "%s: device had unknown status %x\n",
                               disc->dev_name,
                         req.retsts);

       return EFAULT;
}


/*
* The reasoning behind this explicit copy is for compatibility with changes
* in our uscsi_addr structure.
*/
int
uscsi_identify(struct uscsi_dev *disc, struct uscsi_addr *saddr)
{
       struct scsi_addr raddr;
       int error;

       bzero(saddr, sizeof(struct scsi_addr));
       error = ioctl(disc->fhandle, SCIOCIDENTIFY, &raddr);
       if (error) return error;

#ifdef __NetBSD__
       /* scsi and atapi are split up like in uscsi_addr */
       if (raddr.type == 0) {
               saddr->type = USCSI_TYPE_SCSI;
               saddr->addr.scsi.scbus  = raddr.addr.scsi.scbus;
               saddr->addr.scsi.target = raddr.addr.scsi.target;
               saddr->addr.scsi.lun    = raddr.addr.scsi.lun;
       } else {
               saddr->type = USCSI_TYPE_ATAPI;
               saddr->addr.atapi.atbus = raddr.addr.atapi.atbus;
               saddr->addr.atapi.drive = raddr.addr.atapi.drive;
       }
#endif
#ifdef __OpenBSD__
       /* atapi's are shown as SCSI devices */
       if (raddr.type == 0) {
               saddr->type = USCSI_TYPE_SCSI;
               saddr->addr.scsi.scbus  = raddr.scbus;
               saddr->addr.scsi.target = raddr.target;
               saddr->addr.scsi.lun    = raddr.lun;
       } else {
               saddr->type = USCSI_TYPE_ATAPI;
               saddr->addr.atapi.atbus = raddr.scbus;  /* overload */
               saddr->addr.atapi.drive = raddr.target; /* overload */
       }
#endif

       return 0;
}


int
uscsi_check_for_scsi(struct uscsi_dev *disc)
{
       struct uscsi_addr       saddr;

       return uscsi_identify(disc, &saddr);
}
#endif  /* SCSILIB_SCSIPI */




#ifdef USCSI_LINUX_SCSI
       /*
        * Support code for Linux SCSI code. It uses the ioctl() way of
        * communicating since this is more close to the original NetBSD
        * scsipi implementation.
        */
#include <scsi/sg.h>
#include <scsi/scsi.h>

#define SENSEBUFLEN 48


int
uscsi_open(struct uscsi_dev * disc)
{
       int flags;
       struct stat stat;

       /* in Linux we are NOT allowed to open it blocking */
       /* no create! */
       disc->fhandle = open(disc->dev_name, O_RDWR | O_NONBLOCK, 0);
       if (disc->fhandle<0) {
               perror("Failure to open device or file");
               return ENODEV;
       }

       /* explicitly mark it non blocking (again) (silly Linux) */
       flags = fcntl(disc->fhandle, F_GETFL);
       flags &= ~O_NONBLOCK;
       fcntl(disc->fhandle, F_SETFL, flags);

       if (fstat(disc->fhandle, &stat) < 0) {
               perror("Can't stat device or file");
               uscsi_close(disc);
               return ENODEV;
       }

       return 0;
}


int
uscsi_close(struct uscsi_dev * disc)
{
       close(disc->fhandle);
       disc->fhandle = -1;

       return 0;
}


int
uscsi_command(int flags, struct uscsi_dev *disc,
       void *cmd, size_t cmdlen,
       void *data, size_t datalen,
       uint32_t timeout, struct uscsi_sense *uscsi_sense)
{
       struct sg_io_hdr req;
       uint8_t sense_buffer[SENSEBUFLEN];
       int error;

       bzero(&req, sizeof(req));
       if (flags == SG_DXFER_FROM_DEV) bzero(data, datalen);

       req.interface_id    = 'S';
       req.dxfer_direction = flags;
       req.cmd_len         = cmdlen;
       req.mx_sb_len       = SENSEBUFLEN;
       req.iovec_count     = 0;
       req.dxfer_len       = datalen;
       req.dxferp          = data;
       req.cmdp            = cmd;
       req.sbp             = sense_buffer;
       req.flags           = 0;
       req.timeout         = timeout;

       error = ioctl(disc->fhandle, SG_IO, &req);

       if (req.status) {
               /* Is this OK? */
               if (uscsi_sense) {
                       uscsi_sense->asc        =  sense_buffer[12];
                       uscsi_sense->ascq       =  sense_buffer[13];
                       uscsi_sense->skey_valid =  sense_buffer[15] & 128;
                       uscsi_sense->sense_key  = (sense_buffer[16] << 8) |
                                                 (sense_buffer[17]);
               }
               if (uscsilib_verbose) {
                       uscsi_print_sense((char *) disc->dev_name,
                               cmd, cmdlen, sense_buffer, req.sb_len_wr, 1);
               }
       }

       return error;
}


int
uscsi_identify(struct uscsi_dev *disc, struct uscsi_addr *saddr)
{
       struct sg_scsi_id sg_scsi_id;
       struct sg_id {
               /* target | lun << 8 | channel << 16 | low_ino << 24 */
               uint32_t tlci;
               uint32_t uniq_id;
       } sg_id;
       int emulated;
       int error;

       /* clean result */
       bzero(saddr, sizeof(struct uscsi_addr));

       /* check if its really SCSI or emulated SCSI (ATAPI f.e.) */
       saddr->type = USCSI_TYPE_SCSI;
       ioctl(disc->fhandle, SG_EMULATED_HOST, &emulated);
       if (emulated) saddr->type = USCSI_TYPE_ATAPI;

       /* try 2.4 kernel or older */
       error = ioctl(disc->fhandle, SG_GET_SCSI_ID, &sg_scsi_id);
       if (!error) {
               saddr->addr.scsi.target = sg_scsi_id.scsi_id;
               saddr->addr.scsi.lun    = sg_scsi_id.lun;
               saddr->addr.scsi.scbus  = sg_scsi_id.channel;

               return 0;
       }

       /* 2.6 kernel or newer */
       error = ioctl(disc->fhandle, SCSI_IOCTL_GET_IDLUN, &sg_id);
       if (error) return error;

       saddr->addr.scsi.target = (sg_id.tlci      ) & 0xff;
       saddr->addr.scsi.lun    = (sg_id.tlci >>  8) & 0xff;
       saddr->addr.scsi.scbus  = (sg_id.tlci >> 16) & 0xff;

       return 0;
}


int uscsi_check_for_scsi(struct uscsi_dev *disc) {
       struct uscsi_addr saddr;

       return uscsi_identify(disc, &saddr);
}
#endif  /* USCSI_LINUX_SCSI */




#ifdef USCSI_FREEBSD_CAM

int
uscsi_open(struct uscsi_dev *disc)
{
       disc->devhandle = cam_open_device(disc->dev_name, O_RDWR);

       if (disc->devhandle == NULL) {
               disc->fhandle = open(disc->dev_name, O_RDWR | O_NONBLOCK, 0);
               if (disc->fhandle < 0) {
                       perror("Failure to open device or file");
                       return ENODEV;
               }
       }

       return 0;
}


int
uscsi_close(struct uscsi_dev *disc)
{
       if (disc->devhandle != NULL) {
               cam_close_device(disc->devhandle);
               disc->devhandle = NULL;
       } else {
               close(disc->fhandle);
               disc->fhandle = -1;
       }

       return 0;
}


int
uscsi_command(int flags, struct uscsi_dev *disc,
       void *cmd, size_t cmdlen,
       void *data, size_t datalen,
       uint32_t timeout, struct uscsi_sense *uscsi_sense)
{
       struct cam_device *cam_dev;
       struct scsi_sense_data *cam_sense_data;
       union ccb ccb;
       uint32_t cam_sense;
       uint8_t *keypos;
       int camflags;

       memset(&ccb, 0, sizeof(ccb));
       cam_dev = (struct cam_device *) disc->devhandle;

       if (datalen == 0) flags = SCSI_NODATACMD;
       /* optional : */
       /* if (data) assert(flags == SCSI_NODATACMD); */

       camflags = CAM_DIR_NONE;
       if (flags & SCSI_READCMD)
               camflags = CAM_DIR_IN;
       if (flags & SCSI_WRITECMD)
               camflags = CAM_DIR_OUT;

       cam_fill_csio(
               &ccb.csio,
               0,                      /* retries */
               NULL,                   /* cbfcnp */
               camflags,               /* flags */
               MSG_SIMPLE_Q_TAG,       /* tag_action */
               (u_int8_t *) data,      /* data_ptr */
               datalen,                /* dxfer_len */
               SSD_FULL_SIZE,          /* sense_len */
               cmdlen,                 /* cdb_len */
               timeout                 /* timeout */
       );

       /* Disable freezing the device queue */
       ccb.ccb_h.flags |= CAM_DEV_QFRZDIS;

       memcpy(ccb.csio.cdb_io.cdb_bytes, cmd, cmdlen);

       /* Send the command down via the CAM interface */
       if (cam_send_ccb(cam_dev, &ccb) < 0) {
               err(1, "cam_send_ccb");
       }

       if ((ccb.ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP)
               return 0;

       /* print error using the uscsi_sense routines? */

       cam_sense = (ccb.ccb_h.status & (CAM_STATUS_MASK | CAM_AUTOSNS_VALID));
       if (cam_sense != (CAM_SCSI_STATUS_ERROR | CAM_AUTOSNS_VALID))
               return EFAULT;

       /* drive responds with sense information */
       if (!uscsilib_verbose)
               return EFAULT;

       /* print sense info */
       cam_sense_data = &ccb.csio.sense_data;
       if (uscsi_sense) {
               uscsi_sense->asc  = cam_sense_data->add_sense_code;
               uscsi_sense->ascq = cam_sense_data->add_sense_code_qual;
               keypos  = cam_sense_data->sense_key_spec;
               uscsi_sense->skey_valid =  keypos[0] & 128;
               uscsi_sense->sense_key  = (keypos[1] << 8) | (keypos[2]);
       }

       uscsi_print_sense((char *) disc->dev_name,
               cmd, cmdlen,
               (uint8_t *) cam_sense_data, 8 + cam_sense_data->extra_len, 1);

       return EFAULT;
}


int
uscsi_identify(struct uscsi_dev *disc, struct uscsi_addr *saddr)
{
       struct cam_device *cam_dev;

       /* clean result */
       bzero(saddr, sizeof(struct uscsi_addr));

       cam_dev = (struct cam_device *) disc->devhandle;
       if (!cam_dev) return ENODEV;

       /* check if its really SCSI or emulated SCSI (ATAPI f.e.) ? */
       saddr->type = USCSI_TYPE_SCSI;
       saddr->addr.scsi.target = cam_dev->target_id;
       saddr->addr.scsi.lun    = cam_dev->target_lun;
       saddr->addr.scsi.scbus  = cam_dev->bus_id;

       return 0;
}


int
uscsi_check_for_scsi(struct uscsi_dev *disc)
{
       struct uscsi_addr saddr;

       return uscsi_identify(disc, &saddr);
}

#endif  /* USCSI_FREEBSD_CAM */



/*
* Generic SCSI functions also used by the sense printing functionality.
* FreeBSD support has it already asked for by the CAM.
*/

int
uscsi_mode_sense(struct uscsi_dev *dev,
       uint8_t pgcode, uint8_t pctl, void *buf, size_t len)
{
       scsicmd cmd;

       bzero(buf, len);                /* initialise receiving buffer  */

       bzero(cmd, SCSI_CMD_LEN);
       cmd[ 0] = 0x1a;                 /* MODE SENSE                   */
       cmd[ 1] = 0;                    /* -                            */
       cmd[ 2] = pgcode | pctl;        /* page code and control flags  */
       cmd[ 3] = 0;                    /* -                            */
       cmd[ 4] = len;                  /* length of receive buffer     */
       cmd[ 5] = 0;                    /* control                      */

       return uscsi_command(SCSI_READCMD, dev, &cmd, 6, buf, len, 10000, NULL);
}


int
uscsi_mode_select(struct uscsi_dev *dev,
       uint8_t byte2, void *buf, size_t len)
{
       scsicmd cmd;

       bzero(cmd, SCSI_CMD_LEN);
       cmd[ 0] = 0x15;                 /* MODE SELECT                  */
       cmd[ 1] = 0x10 | byte2;         /* SCSI-2 page format select    */
       cmd[ 4] = len;                  /* length of page settings      */
       cmd[ 5] = 0;                    /* control                      */

       return uscsi_command(SCSI_WRITECMD, dev, &cmd, 6, buf, len,
                       10000, NULL);
}


int
uscsi_request_sense(struct uscsi_dev *dev, void *buf, size_t len)
{
       scsicmd cmd;

       bzero(buf, len);                /* initialise receiving buffer  */

       bzero(cmd, SCSI_CMD_LEN);
       cmd[ 0] = 0x03;                 /* REQUEST SENSE                */
       cmd[ 4] = len;                  /* length of data to be read    */
       cmd[ 5] = 0;                    /* control                      */

       return uscsi_command(SCSI_WRITECMD, dev, &cmd, 6, buf, len,
                       10000, NULL);
}


/* end of uscsi_subr.c */