/*
** blk512-linux.c : resize a 520 byte/sector drive to 512 byte/sector.
**
** Copyright (C) 2003  Grant Grundler <grundler@parisc-linux.org>
**
**   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.
**
**
** http://www.t10.org/scsi-3.htm
** http://www.tldp.org/HOWTO/SCSI-Generic-HOWTO
**
**	Inquiry		Block Count (@512 byte blocks)
**	ST150150N	8388315
**	IBM_DCHS04F	8888543
**	IBM_DGHS09Y	17916240
**	ST318304FC	35145034  (Factory spec is 35885167 sectors)
**	ST336605FC	???
**	ST336753FC	71132960  (Factory spec is 71687372 sectors) 
*/

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <scsi/scsi.h>
#include <scsi/scsi_ioctl.h>
#include <scsi/sg.h>
#include <sys/errno.h>

#define FORMAT_DEV_PAGE	3	/* Format Device Mode Page */

#define CDB_SIZE		6	/* SCSI Command Block */
#define MODE_HDR_SIZE		4	/* Mode Sense Header */
#define BLOCK_DESCR_SIZE	8	/* Block Descriptor Header */

#define LOGICAL_UNIT_NOT_READY	4 /* ASC */
#define FORMAT_IN_PROGRESS	4 /* ASCQ */
 

#define MAX_SENSE_SZ	32
unsigned char sbuff[MAX_SENSE_SZ];

#define MAX_BUFF_SZ	0xFF
char dbuff[MAX_BUFF_SZ];


void static
dump_buffer(char *ident, unsigned char *buf, int cnt)
{
		int i;
		putchar('\n');
		printf(ident);
		for  (i=0; i< cnt; i++)
		{
			if ((i & 0xf) == 0) printf("\n0x%04x: ", i);
			if ((i & 0x7) == 0) printf(" ");
			printf(" %02x", buf[i]);
		}
		putchar('\n');
}


static int
scsi_mode_sense(int fd, char page, char *buff, char *sense_buff)
{
	unsigned char mode_cdb[CDB_SIZE] = {0x00, 0, 0, 0, 0, 0};
	sg_io_hdr_t io_hdr;

	/* Prepare Mode Sense command */
	mode_cdb[0] = MODE_SENSE;	/* command */
	mode_cdb[1] = 0x00;		/* LUN / DBD */
	mode_cdb[2] = page;		/* Mode page code */
	mode_cdb[3] = 0x00;		/* reserved */
	mode_cdb[4] = MAX_BUFF_SZ;	/* allocation length */
	mode_cdb[5] = 0x00;		/* control : get current values */

	memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
	memset(sense_buff, 0, MAX_SENSE_SZ);

	io_hdr.interface_id = 'S';
	io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
	io_hdr.cmd_len = CDB_SIZE;
	io_hdr.mx_sb_len = MAX_SENSE_SZ;
	io_hdr.iovec_count = 0;		/* no scatter gather */
	io_hdr.dxfer_len = MAX_BUFF_SZ;
	io_hdr.dxferp = buff;
	io_hdr.cmdp = mode_cdb;
	io_hdr.sbp = sense_buff;
	io_hdr.timeout = 1000;	/* 1 second */
	io_hdr.flags = SG_FLAG_LUN_INHIBIT;	/* don't clobber LUN field */ 

        if (ioctl(fd, SG_IO, &io_hdr) < 0) {
		perror("MODE Sense SG_IO ioctl error");
		close(fd);
		exit(1);
        }

	if (io_hdr.host_status || io_hdr.driver_status)
		printf("UHOH! page 0x%x  HBA Stat 0x%x  Drv Stat 0x%x\n",
			 page, io_hdr.host_status, io_hdr.driver_status);

	if (io_hdr.sb_len_wr)
		dump_buffer("Request Sense Data:\n", sense_buff, io_hdr.sb_len_wr); 

	return (MAX_BUFF_SZ - io_hdr.resid);
}

static void
scsi_format(int fd)
{
	int ret = 0;
	const char FORMAT_HEADER_SIZE = 4;
	unsigned char cdb[CDB_SIZE], fmt_hdr[FORMAT_HEADER_SIZE];
	sg_io_hdr_t io_hdr;

	cdb[0] = FORMAT_UNIT;
	cdb[1] = 0x10;		/* defect list follows, short format */
	cdb[2] = 0;		/* vendor specific */
	cdb[3] = 0;		/* interleave MSB */
	cdb[4] = 0;		/* interleave LSB */
	cdb[5] = 0;		/* control */

	fmt_hdr[0] = 0;		/* reserved */
	fmt_hdr[1] = 0x02;	/* use device defaults, IMMED return */
	fmt_hdr[2] = 0;		/* defect list length MSB */
	fmt_hdr[3] = 0;		/* defect list length LSB */

	memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
	memset(sbuff, 0, MAX_SENSE_SZ);
	io_hdr.interface_id = 'S';
	io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
	io_hdr.cmd_len = CDB_SIZE;
	io_hdr.mx_sb_len = MAX_SENSE_SZ;
	io_hdr.iovec_count = 0;		/* no scatter gather */
	io_hdr.dxfer_len = FORMAT_HEADER_SIZE;
	io_hdr.dxferp = fmt_hdr;
	io_hdr.cmdp = cdb;
	io_hdr.sbp = sbuff;
	io_hdr.timeout = 10000;	/* 10 seconds */
	io_hdr.flags = SG_FLAG_LUN_INHIBIT;
        if (ioctl(fd, SG_IO, &io_hdr) < 0) {
		perror("FORMAT UNIT ioctl error");
		close(fd);
		exit(1);
        }

	printf("\n\nBeginning Format\n");

	for(;;) {
		float progress;

		sleep(30);
		cdb[0] = TEST_UNIT_READY;
		cdb[1] = 0;
		cdb[2] = 0;
		cdb[3] = 0;
		cdb[4] = 0;
		cdb[5] = 0;

		memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
		memset(sbuff, 0, MAX_SENSE_SZ);

		io_hdr.interface_id = 'S';
		io_hdr.dxfer_direction = SG_DXFER_NONE;
		io_hdr.cmd_len = CDB_SIZE;
		io_hdr.mx_sb_len = MAX_SENSE_SZ;
		io_hdr.iovec_count = 0;		/* no scatter gather */
		io_hdr.dxfer_len = 0;
		io_hdr.dxferp = NULL;
		io_hdr.cmdp = cdb;
		io_hdr.sbp = sbuff;
		io_hdr.timeout = 10000;	/* 1 second */
		io_hdr.flags = SG_FLAG_LUN_INHIBIT;	/* don't clobber LUN field */ 

	
		if ((ret = ioctl(fd, SG_IO, &io_hdr)) < 0) {
			perror("Test Unit Ready SG_IO ioctl error");
			close(fd);
			exit(1);
		}

		if (io_hdr.status == 0)
			break;

		if(sbuff[0] != 0x70 && sbuff[0] != 0x71) {
			printf("ERROR: invalid sense 0x%x\n", sbuff[0]);
			continue;
		}

		if((sbuff[2] & 0x0f) != NOT_READY) {
			printf("ERROR: sense is 0x%x, should be NOT READY\n",
			       sbuff[2] & 0x0f);
			continue;
		}

		if(sbuff[12] != LOGICAL_UNIT_NOT_READY
		   && sbuff[13] != FORMAT_IN_PROGRESS) {
			printf("ERROR: ASC 0x%x, ASCQ 0x%x unexpected\n",
			       sbuff[12], sbuff[13]);
			continue;
		}

		if((sbuff[15] & 0x80) != 0x80) {
			printf("ERROR: SKSV not set\n");
			continue;
		}

		progress = sbuff[16]*256 + sbuff[17];
		progress /= 65536;
		progress *= 100;

		printf("Format in progress %2.2f%% done\n", progress);
	}

	printf("FORMAT Complete\n");
}

static void
scsi_mode_select(int fd, unsigned char page, char mlen, char *buff, char * sense_buff)
{
	int ret = 0;
	unsigned char mode_cdb[CDB_SIZE] = {0x00, 0, 0, 0, 0, 0};
	sg_io_hdr_t io_hdr;

	/* MODE SELECT command */
	mode_cdb[0] = MODE_SELECT;	/* command */
	mode_cdb[1] = 0x11;		/* LUN 0, PF (bit 4), SP (bit 0) */
	mode_cdb[2] = 0x00;		/* reserved */
	mode_cdb[3] = 0x00;		/* reserved */
	mode_cdb[4] = mlen;
	mode_cdb[5] = 0x00;		/* control */

	memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
	memset(sense_buff, 0, MAX_SENSE_SZ);
	io_hdr.interface_id = 'S';
	io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
	io_hdr.cmd_len = CDB_SIZE;
	io_hdr.mx_sb_len = MAX_SENSE_SZ;
	io_hdr.iovec_count = 0;		/* no scatter gather */
	io_hdr.dxfer_len = mlen;
	io_hdr.dxferp = buff;
	io_hdr.cmdp = mode_cdb;
	io_hdr.sbp = sense_buff;
	io_hdr.timeout = 1000;	/* 1 second */
	io_hdr.flags = SG_FLAG_LUN_INHIBIT;

	if (ioctl(fd, SG_IO, &io_hdr) < 0) {
		perror("MODE_SELECT ioctl failed");
		close(fd);
		exit(1);
	}

#if 0
	if (io_hdr.resid)
#endif
		printf("	mlen 0x%x  resid 0x%x duration %d  info 0x%x\n",
			 mlen, io_hdr.resid, io_hdr.duration, io_hdr.info);

	if (io_hdr.status || io_hdr.msg_status || 
				io_hdr.host_status || io_hdr.driver_status)
		printf("SCSI Status %d  HBA Stat 0x%x  Drv Stat 0x%x  MSG stat 0x%x\n",
				io_hdr.status, io_hdr.host_status,
				io_hdr.driver_status, io_hdr.msg_status);

	if (io_hdr.sb_len_wr)
		dump_buffer("Request Sense Data:\n", sense_buff, io_hdr.sb_len_wr); 

}



main(int argc, char **argv)
{
	int fd;
	int mode_len;

	int blk_cnt = 0;	/* -c value */
	int blk_size = 512;	/* -s value */
	int show_mask = 0;	/* -M */
	int show_saved = 0;	/* -S */
	int format = 0;		/* -F */

	char c;
	char *device_name;

	while ((c = getopt(argc, argv, "SMc:s:F")) != EOF) {
		switch (c) {
		case 'c':  blk_cnt = atoi(optarg);   break;
		case 's':  blk_size = atoi(optarg);  break;
		case 'M':  show_mask = 1;            break;
		case 'S':  show_saved = 1;           break;
		case 'F':  format = 1;		     break;
		default:
			printf("usage: %s [-MSF] [-c <block count>] [-s <block size>] </dev/sg<N>\n"
			"\t(eg %s -c 8813870 /dev/sg17)\n", argv[0], argv[0]);
			printf("WARNING: This program will destroy all the data on the target device.\n"
			"\tUse scsiinfo to verify you have the correct device.\n");
			exit(1);
		}
	}

	device_name = argv[optind];

	/* FIXME: add more sanity checks:
	** o block size/count might already be set...don't repeat
	** o verify SCSI device is a disk (get inquiry data first)
	*/

	if ((fd = open(device_name, O_RDWR)) < 0) {
		char ebuff[128];
		sprintf(ebuff, "error opening device file: %s", device_name);
		perror(ebuff);
		exit(1);
	}

	if (show_mask) {
		/* Changeable Mask (1s are changeable) */
		mode_len = scsi_mode_sense(fd, 0x40 | FORMAT_DEV_PAGE,
							 dbuff, sbuff);
		dump_buffer("Mode Sense Mask:\n", dbuff, mode_len); 
	}

	if (show_saved) {
		/* Saved Values */
		mode_len = scsi_mode_sense(fd, 0xc0 | FORMAT_DEV_PAGE,
							 dbuff, sbuff);
		dump_buffer("Mode Sense Saved:\n", dbuff, mode_len); 
	}

	/* Current Values */
	mode_len = scsi_mode_sense(fd, FORMAT_DEV_PAGE, dbuff, sbuff);
	dump_buffer("Mode Sense Current:\n", dbuff, mode_len); 

/*
Mode Sense Current:

0x0000:   23 00 10 08 04 2c 1d 80  00 00 02 08 83 16 03 89
0x0010:   00 00 00 06 00 00 01 9e  02 08 00 01 00 44 00 48
0x0020:   40 00 00 00
*/

	/* Factory Default Values */
	mode_len = scsi_mode_sense(fd, 0x80 | FORMAT_DEV_PAGE, dbuff, sbuff);
	dump_buffer("Mode Sense Default:\n", dbuff, mode_len); 

/*
Mode Sense Default:

0x0000:   23 00 10 08 04 2c 1d 80  00 00 02 08 83 16 03 89
0x0010:   00 00 00 06 00 00 01 a8  02 00 00 01 00 55 00 5a
0x0020:   40 00 00 00

Used sbc2r08.pdf	(SCSI Block Commands - 2, Rev 08)

Format Device Mode page starts at +12:
 	0x83   +0  "Page Saveable" bit (0x80) + mode Page (0x3)
	0x16   +1  Page Length (0x16)	
	0x0389 +2  Tracks per Zone
	0x0000 +4  Alternate sectors per zone
	0x0006 +6  Alternate tracks per zone
	0x0000 +8  Alternate tracks per logical unit
	0x01a8 +a  sectors per track
	0x0200 +c  data bytes per physical sector
	0x0001 +e  interleave
	0x0055 +10 track skew factor
	0x005a +12 cylinder skew factor
	0x40   +14 SSEC + HSEC + RMB + SURF (upper bits, remainder is reserved)
	           (ie only support Hard Sectors and not soft sectors)
	0x000000 +15  last 3 bytes reserved.
*/

	if (blk_cnt) {
 		/*
	 	** T10/1416-D Revision 12
		** http://www.t10.org/scsi-3.htm
		*/

		/* Table 216  Mode Parameter Header(6) */
		dbuff[0] = 0;	/* MODE SENSE only: Mode Data Length */
		dbuff[1] = 0;	/* medium type */
		dbuff[2] = 0;   /* MODE SENSE only: Device Specific Parameter */
		dbuff[3] = BLOCK_DESCR_SIZE;	/* block descriptor Length */


/* Working Draft SCSI Primary Commands - 3 (SPC-3)    pg 255
**
** If the SCSI device doesn't support changing its capacity by changing
** the NUMBER OF BLOCKS field using the MODE SELECT command, the value
** in the NUMBER OF BLOCKS field is ignored. If the device supports changing
** its capacity by changing the NUMBER OF BLOCKS field, then the
** NUMBER OF BLOCKS field is interpreted as follows:
**	a) If the number of blocks is set to zero, the device shall retain
**	   its current capacity if the block size has not changed. If the
**	   number of blocks is set to zero and the block size has changed,
**	   the device shall be set to its maximum capacity when the new
**         block size takes effect;
**
**	b) If the number of blocks is greater than zero and less than or
**	   equal to its maximum capacity, the device shall be set to that
**	   number of blocks. If the block size has not changed, the device
**	   shall not become format corrupted. This capacity setting shall be
**	   retained through power cycles, hard resets, logical unit resets,
**	   and I_T nexus losses;
**
**	c) If the number of blocks field is set to a value greater than the
**	   maximum capacity of the device and less than FFFF FFFFh, then the
**	   command is terminated with a CHECK CONDITION status. The sense key
**	   is set to ILLEGAL REQUEST. The device shall retain its previous
**	   block descriptor settings; or
**
**	d) If the number of blocks is set to FFFF FFFFh, the device shall be
**	   set to its maximum capacity. If the block size has not changed,
**	   the device shall not become format corrupted. This capacity setting
**	   shall be retained through power cycles, hard resets, logical unit
**	   resets, and I_T nexus losses.
*/

		/* Table 219  Direct-access device mode param blk descriptor */
		dbuff[4]  = (unsigned char) (blk_cnt >> 24); /* # of blk MSB */
		dbuff[5]  = (unsigned char) (blk_cnt >> 16); /* # of blk */
		dbuff[6]  = (unsigned char) (blk_cnt >> 8);  /* # of blk */
		dbuff[7]  = (unsigned char)  blk_cnt;        /* # of blk LSB */

		dbuff[8]  = 0x00;                            /* Density Code */
		dbuff[9]  = (unsigned char) (blk_size >> 16); /* bytes/blk */
		dbuff[10] = (unsigned char) (blk_size >> 8);  /* bytes/blk */
		dbuff[11] = (unsigned char)  blk_size;        /* bytes/blk */

#if 1
		dbuff[12] = FORMAT_DEV_PAGE;

		/* adjust default Format page data */
		dbuff[0x18] = (unsigned char) (blk_size >> 8); /* bytes/blk */
		dbuff[0x19] = (unsigned char)  blk_size;       /* bytes/blk */

		mode_len = MODE_HDR_SIZE + BLOCK_DESCR_SIZE;
#endif
		dump_buffer("Mode Select Data:\n", dbuff, mode_len); 

		/* write the mode page back out */
		scsi_mode_select(fd, FORMAT_DEV_PAGE, mode_len, dbuff, sbuff);
	}

	if (blk_cnt)
		printf("Done setting %s to %d blocks/%d bytes_per_block\n",
				 device_name, blk_cnt, blk_size);

	if(format)
		scsi_format(fd);

	close(fd);

	return (0);
}

