/*
 * set_density - read and optionally set the SCSI CD-ROM drive's Density Code
 *
 * Copyright (c) 1994 by Andreas Haumer (andreas@vlsivie.tuwien.ac.at)
 *
 * This program is a quick hack to solve the problem of unmountable
 * Photo-CDs on some SCSI CD-ROM drives under Linux.
 *
 * Usage:
 * To mount a Photo-CD on a SCSI CD-ROM drive where you get an "Unable to
 * identify CD-ROM format" error you can try the following command (as root):
 *
 *          set_density [-i] [-f device-file] -d density
 *
 * and then
 *          mount -tiso9660 <device-file> <mount-point>
 *
 *
 * NOTE 1: The value of <density> is vendor-specific!
 *         On my Toshiba XM-3401TA drive I have to use a density code of
 *         <0x81> or <0x83> (a density code of 0x83 works for both Photo-
 *         CDs and "normal" CD-ROMs).
 *         The XM-3401TA is also the only device I have actually tested this
 *         program!
 *         If you've found a working density code, please report it together
 *         with your drive's specification (use the "-i" option). I will
 *         collect this and maybe we can set up a list of certain drive /
 *         density code combinations useful for other Photo-CD users.
 *
 * NOTE 2: One problem remains: If you have already tried to mount the
 *         Photo-CD with incorrect density and then set the new one with
 *         this program, you should eject the CD and insert it *_before_*
 *         you issue the mount command again.
 *         This is because the buffers read with the incorrect density
 *         remain in the buffer-cache and I don't know how to invalidate
 *         them program-controlled! (Is there a "BLKRRPART" or "BLKFLSBUF"
 *         ioctl() for SCSI CD-ROM drives? Any hints?)
 *
 * NOTE 3: Be very careful when using this program! Don't change the
 *         density code of your mounted HD drives!
 *
 * The functionality of this program is based on source code and information
 * I got from:
 *             - Eric Youngdale (scsiinfo.c)
 *             - ANSI X3.131-199X (draft SCSI-2, Oct. 1991)
 *             - Toshiba SCSI-2 Interface Specifications Ver. 6.0, Jul. 1992
 *
 *
 * Author:  Andreas Haumer (andreas@vlsivie.tuwien.ac.at)
 *          Buchengasse 67/8
 *          A-1100 Vienna
 *          Austria
 *
 *
 * V1.1, 94/12/31 - created (is there something better to do on this evening?)
 * V1.2, 94/01/01 - display device information ("-i") option
 * V1.3, 94/06/06 - prepared for the public (added more comments)
 *
 * -------------------------------------------------------------------------
 *
 *   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.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#ifndef lint
static char *rcsid = "$Id: set_density.c,v 1.3 1994/06/06 20:11:59 andreas Exp $";
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <fcntl.h>

/* SCSI Operation Codes */
#define INQUIRY      0x12
#define MODE_SENSE   0x1a
#define MODE_SELECT  0x15

#define SCSI_IOCTL_SEND_COMMAND 1

#define CDROM_DEV "/dev/scd0"	/* my default CD-ROM device file */

#define BLEN 128

unsigned char buffer[BLEN];

typedef struct {
  unsigned char device_type;
  char vendor[9];
  char product[17];
  char revision[5];
} InquiryData;

char *device_type_str[] = {
  "Direct-access",
  "Sequential-access",
  "Printer",
  "Processor",
  "Write-once",
  "CD-ROM",
  "Scanner",
  "Optical memory",
  "Medium Changer",
  "Communications"
};


/*
 * get_density - return the CD-ROM drive's current Density Code.
 *               In order to do this, we read one of the supported
 *               mode pages (e.g. Error Recovery Parameters) and extract
 *               the Density Code from the Block Descriptor.
 */
int get_density (int fd)
{
  int status, PC, page;
  unsigned char *cmd, *data;

  memset (buffer, 0, (size_t)BLEN);
  
  PC   = 0;			/* return current values */
  page = 1;			/* Error Recovery Parameters page */
  
  *((int *)buffer)     = 0;	/* length of input data */
  *(((int *)buffer)+1) = BLEN;	/* length of output buffer */
  
  cmd = (unsigned char *)(((int *)buffer) + 2);
  
/* Field                           Field Description */
/* Command */
  cmd[0] = MODE_SENSE;		/* Operation Code */
  cmd[1] = 0x00;		/* Enable Block Descriptors */
  cmd[2] = (PC << 6) | page;	/* Page Control, Page Code */
  cmd[3] = 0x00;		/* reserved */
  cmd[4] = BLEN;		/* Allocation Length */
  cmd[5] = 0x00;		/* Control */
  
  if ((status = ioctl (fd, SCSI_IOCTL_SEND_COMMAND, buffer)) != 0) {
    printf ("ioctl(SCSI_IOCTL_SEND_COMMAND) status = %d\n", status);
    return (-1);
  }
  
  data = (unsigned char *)(((int *)buffer) + 2);
  
  return ((int)data[4]);
} /* get_density */


/*
 * set_density - set the CD-ROM drive's Density Code to <density>.
 */
int set_density (int fd, unsigned char density)
{
  int status;
  unsigned char PF, *cmd;

  memset (buffer, 0, (size_t)BLEN);
  
  PF = 1;			/* Parameter Page Format: SCSI-2  */
  
  *((int *)buffer)	 = 12;	/* length of input data */
  *(((int *)buffer) + 1) = 0;	/* length of output buffer */
  
  cmd = (unsigned char *)(((int *)buffer) + 2);

/* Field                           Field Description */
/* Command */
  cmd[0] = MODE_SELECT;		/* Operation Code */
  cmd[1] = PF << 4;		/* Page Format, Save Page = 0 */
  cmd[2] = 0x00;		/* reserved */
  cmd[3] = 0x00;		/* reserved */
  cmd[4] = 12;			/* Parameter List Length */
  cmd[5] = 0x00;		/* Control */

/* Parameter List */
  cmd[6] = 0;			/* reserved */
  cmd[7] = 0;			/* Medium Type */
  cmd[8] = 0;			/* Device Specific Parameter */
  cmd[9] = 8;			/* Block Descriptor Length */

/* Block Descriptor */  
  cmd[10] = density;		/* Density Code */
  cmd[11] = 0;			/* Number of Blocks = 0 */
  cmd[12] = 0;
  cmd[13] = 0;
  cmd[14] = 0;			/* reserved */
  cmd[15] = 0;			/* Block Length = 0x000800 */
  cmd[16] = 8;			/* ( = 2048 Bytes) */
  cmd[17] = 0;
  
  if ((status = ioctl (fd, SCSI_IOCTL_SEND_COMMAND, buffer)) != 0) {
    printf ("ioctl(SCSI_IOCTL_SEND_COMMAND) status = %d\n", status);
    return (-1);
  }

  return (0);
} /* set_density */


/*
 * do_inquiry - get inquiry-information from the SCSI device and
 *              save it in <*result>.
 */
int do_inquiry (int fd, InquiryData *result)
{
  int status;
  unsigned char *cmd, *data;
  
  memset (buffer, 0, (size_t)BLEN);
  
  *((int *)buffer)     = 0;	/* length of input data */
  *(((int *)buffer)+1) = BLEN;	/* length of output buffer */

  cmd = (unsigned char *)(((int *)buffer) + 2);
  
/* Field                           Field Description */
/* Command */
  cmd[0] = INQUIRY;		/* Operation Code */
  cmd[1] = 0x00;		/* lun=0, evpd=0 */
  cmd[2] = 0x00;		/* page code = 0 */
  cmd[3] = 0x00;		/* reserved */
  cmd[4] = BLEN;		/* Allocation Length */
  cmd[5] = 0x00;		/* Control */

  if ((status = ioctl (fd, SCSI_IOCTL_SEND_COMMAND, buffer)) != 0) {
    printf ("ioctl(SCSI_IOCTL_SEND_COMMAND) status = %d\n", status);
    return (-1);
  }

  data = (unsigned char *)(((int *)buffer) + 2);

  result->device_type = data[0] & 0x1f;
  strncpy (result->vendor,   data +  8,  8); result->vendor[8]   = '\0';
  strncpy (result->product,  data + 16, 16); result->product[16] = '\0';
  strncpy (result->revision, data + 32,  4); result->revision[4] = '\0';
  
  return (0);
} /* do_inquiry */


/*
 * display_inquiry - display inquiry-information in <*info>
 */
void display_inquiry (InquiryData *info)
{
  printf ("SCSI Device Information\n");
  printf ("Peripheral Device Type:\t");
  if (info->device_type > 9)
    printf ("0x%02x\n", (int)info->device_type);
  else
    printf ("%s\n", device_type_str[info->device_type]);
  printf ("Vendor Identification:\t%s\n", info->vendor);
  printf ("Product Identification:\t%s\n", info->product);
  printf ("Product Revision Level:\t%s\n", info->revision);
  printf ("-----------------------\n");

  return;
} /* display_inquiry */


/*
 * usage - print a short program-usage information
 */
void usage (char *progname)
{
  printf ("usage: %s [-f devicefile] [-d density] [-i]\n", progname);
  printf ("\t<devicefile> ... SCSI CD-ROM device special file\n");
  printf ("\t<density> ...... Density Code, 0x00 <= density <= 0xff\n");
  printf ("\t-i ............. display device information\n");
} /* usage */


int main (int argc, char *argv[])
{
  extern int getopt();
  extern char *optarg;
  int cdfile, density, new_density, inquiry_info, doit;
  char c, *fname;
  InquiryData inquiry_data;
  
  inquiry_info =  0;		/* don't display inquiry information */
  new_density  = -1;		/* default: just read current density */
  fname = CDROM_DEV;		/* default: the SCSI-device special-file */

  while ((c = getopt (argc, argv, "f:d:i")) != EOF)
    switch (c) {
    case 'i':
      inquiry_info = 1;
      break;
    case 'f':
      fname = optarg;
      break;
    case 'd':
      new_density = (int)strtol (optarg, NULL, 0);
      if ((new_density < 0) || (new_density > 0xff)) {
	usage (argv[0]);
	exit (-1);
      }
      break;
    default:
      usage (argv[0]);
      exit (-1);
    }

/*
 * open the device-file
 */
  if ((cdfile = open (fname, O_RDONLY)) == -1) {
    perror (fname);
    exit (-1);
  }

/*
 * get SCSI inquiry data
 */
  if (do_inquiry (cdfile, &inquiry_data) != 0)
    perror ("inquiry");

/*
 * print inquiry-data if requested
 */
  if (inquiry_info)
    display_inquiry (&inquiry_data);

/*
 * get and display current density value
 */
  if ((density = get_density (cdfile)) >= 0) {
    printf ("%s ", (new_density >= 0) ? "Old" : "Current");
    printf ("Density Code:\t0x%02x\n", density);
  }

/*
 * set new density value if requested.
 * ask for confirmation if device is not a CD-ROM.
 */
  if (new_density >= 0) {
    doit = 1;
    if (inquiry_data.device_type != 5) {
      printf ("\nWARNING: \"%s\" is not a CD-ROM device!\n", fname);
      printf ("Do you really want to set the Density Code (y/n)? ");
      fflush (stdout);
      if (getchar () != 'y')
	doit = 0;
    }
    
    if (doit) {
      set_density (cdfile, new_density);
/*
 * code to revalidate buffers should go here ?
 */
      if ((density = get_density (cdfile)) >= 0)
	printf ("New Density Code:\t0x%02x\n", density);
    }
  }
  
  close (cdfile);
  
  return (0);
}
