/*  Copyright 1996 Grant R. Guenther,  based on work of Itai Nahshon
 *   http://www.torque.net/ziptool.html
 *  Copyright 1997-2002,2007-2009 Alain Knaff.
 *  This file is part of mtools.
 *
 *  Mtools 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  Mtools 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 Mtools.  If not, see <http://www.gnu.org/licenses/>.
 *
 * mzip.c
 * Iomega Zip/Jaz drive tool
 * change protection mode and eject disk
 */

/* mzip.c by Markus Gyger <mgyger@itr.ch> */
/* This code is based on ftp://gear.torque.net/pub/ziptool.c */
/* by Grant R. Guenther with the following copyright notice: */

/*  (c) 1996   Grant R. Guenther,  based on work of Itai Nahshon  */
/*  http://www.torque.net/ziptool.html  */


/* Unprotect-till-eject modes and mount tests added
 * by Ilya Ovchinnikov <ilya@socio.msu.su>
 */

#include "sysincludes.h"
#include "mtools.h"
#include "scsi.h"

#ifndef _PASSWORD_LEN
#define _PASSWORD_LEN 33
#endif

#ifdef OS_linux

#if __GLIBC__ >=2
#include <sys/mount.h>
#else
#define _LINUX_KDEV_T_H 1  /* don't redefine MAJOR/MINOR */
#include <linux/fs.h>
#endif

#include "devices.h"

#endif


static int zip_cmd(int priv, int fd, unsigned char cdb[6], uint8_t clen,
		   scsi_io_mode_t mode, void *data, uint32_t len,
		   void *extra_data)
{
	int r;

	if(priv)
		reclaim_privs();
	r = scsi_cmd(fd, cdb, clen,  mode, data, len, extra_data);
	if(priv)
		drop_privs();
	return r;
}

static int test_mounted ( char *dev UNUSEDP)
{
#ifdef HAVE_MNTENT_H
	struct mntent	*mnt;
	struct MT_STAT	st_dev, st_mnt;
	FILE		*mtab;
/*
 * Now check if any partition of this device is already mounted (this
 * includes checking if the device is mounted under a different name).
 */

	if (MT_STAT (dev, &st_dev)) {
		fprintf (stderr, "%s: stat(%s) failed: %s.\n",
			 progname, dev, strerror (errno));
		exit(1);
	}

	if (!S_ISBLK (st_dev.st_mode)) /* not a block device, cannot
					* be mounted */
		return 0;

#ifndef _PATH_MOUNTED
# define _PATH_MOUNTED "/etc/mtab"
#endif

	if ((mtab = setmntent (_PATH_MOUNTED, "r")) == NULL) {
		fprintf (stderr, "%s: can't open %s.\n",
			 progname, _PATH_MOUNTED);
		exit(1);
	}

	while ( ( mnt = getmntent (mtab) ) ) {
		if (!mnt->mnt_fsname

#ifdef MNTTYPE_SWAP
		    || !strcmp (mnt->mnt_type, MNTTYPE_SWAP)
#endif
#ifdef MNTTYPE_NFS
		    || !strcmp (mnt->mnt_type, MNTTYPE_NFS)
#endif
		    ||  !strcmp (mnt->mnt_type, "proc")
		    ||  !strcmp (mnt->mnt_type, "smbfs")
#ifdef MNTTYPE_IGNORE
		    ||  !strcmp (mnt->mnt_type, MNTTYPE_IGNORE)
#endif
			)
			continue;

		if (MT_STAT (mnt->mnt_fsname, &st_mnt)) {
			continue;
		}

		if (S_ISBLK (st_mnt.st_mode)) {
#ifdef OS_linux
			/* on Linux, warn also if the device is on the same
			 * partition */
			if (MAJOR(st_mnt.st_rdev) == MAJOR(st_dev.st_rdev) &&
			    MINOR(st_mnt.st_rdev) >= MINOR(st_dev.st_rdev) &&
			    MINOR(st_mnt.st_rdev) <= MINOR(st_dev.st_rdev)+15){
				fprintf (stderr,
					 "Device %s%d is mounted on %s.\n",
					 dev,
					 MINOR(st_mnt.st_rdev) -
					 MINOR(st_dev.st_rdev),
					 mnt->mnt_dir);
#else
				if(st_mnt.st_rdev != st_dev.st_rdev) {
#endif
					endmntent (mtab);
					return 1;
				}
#if 0
			} /* keep Emacs indentation happy */
#endif
		}
	}
	endmntent (mtab);
#endif
	return 0;
}


static void usage(int ret) NORETURN;
static void usage(int ret)
{
	fprintf(stderr,
		"Mtools version %s, dated %s\n",
		mversion, mdate);
	fprintf(stderr,
		"Usage: %s [-V] [-q] [-e] [-u] [-r|-w|-p|-x] [drive:]\n"
		"\t-q print status\n"
		"\t-e eject disk\n"
		"\t-f eject disk even when mounted\n"
		"\t-r write protected (read-only)\n"
		"\t-w not write-protected (read-write)\n"
		"\t-p password write protected\n"
		"\t-x password protected\n"
		"\t-u unprotect till disk ejecting\n",
		progname);
	exit(ret);
}

#define ZIP_RW (0)
#define ZIP_RO (2)
#define ZIP_RO_PW (3)
#define ZIP_PW (5)
#define ZIP_UNLOCK_TIL_EJECT (8)

static uint8_t get_zip_status(int priv, int fd, void *extra_data)
{
	unsigned char status[128];
	unsigned char cdb[6] = { 0x06, 0, 0x02, 0, sizeof status, 0 };

	if (zip_cmd(priv, fd, cdb, 6, SCSI_IO_READ,
		    status, sizeof status, extra_data) == -1) {
		perror("status: ");
		exit(1);
	}
	return status[21] & 0xf;
}


static int short_command(int priv, int fd, uint8_t cmd1, uint8_t cmd2,
			 uint8_t cmd3, const char *data, void *extra_data)
{
	uint8_t cdb[6] = { 0, 0, 0, 0, 0, 0 };

	cdb[0] = cmd1;
	cdb[1] = cmd2;
	cdb[4] = cmd3;

	return zip_cmd(priv, fd, cdb, 6, SCSI_IO_WRITE,
		       (char *) data, data ? (uint32_t) strlen(data) : 0,
		       extra_data);
}


static int iomega_command(int priv, int fd, uint8_t mode, const char *data,
			  void *extra_data)
{
	return short_command(priv, fd,
			     SCSI_IOMEGA, mode,
			     /* Do we really need strlen(data) in here? */
			     data ? (uint8_t) strlen(data) : 0,
			     data, extra_data);
}

static int door_command(int priv, int fd, uint8_t cmd1, uint8_t cmd2,
			void *extra_data)
{
	return short_command(priv, fd, cmd1, 0, cmd2, 0, extra_data);
}

void mzip(int argc, char **argv, int type UNUSEDP) NORETURN;
void mzip(int argc, char **argv, int type UNUSEDP)
{
	void *extra_data = NULL;
	int c;
	char drive;
	device_t *dev;
	int fd = -1;
	char name[EXPAND_BUF];
#define ZIP_NIX (0)
#define ZIP_STATUS (1 << 0)
#define ZIP_EJECT  (1 << 1)
#define ZIP_MODE_CHANGE (1 << 2)
#define ZIP_FORCE  (1 << 3)
	int request = ZIP_NIX;

	uint8_t newMode = ZIP_RW;
	uint8_t oldMode = ZIP_RW;

#define setMode(x) \
	if(request & ZIP_MODE_CHANGE) usage(1); \
	request |= ZIP_MODE_CHANGE; \
	newMode = x; \
	break

	/* get command line options */
	if(helpFlag(argc, argv))
		usage(0);
	while ((c = getopt(argc, argv, "i:efpqrwxuh")) != EOF) {
		switch (c) {
			case 'i':
				set_cmd_line_image(optarg);
				break;
			case 'f':
				if (get_real_uid()) {
					fprintf(stderr,
						"Only root can use force. Sorry.\n");
					exit(1);
				}
				request |= ZIP_FORCE;
				break;
			case 'e': /* eject */
				request |= ZIP_EJECT;
				break;
			case 'q': /* status query */
				request |= ZIP_STATUS;
				break;

			case 'p': /* password read-only */
				setMode(ZIP_RO_PW);
			case 'r': /* read-only */
				setMode(ZIP_RO);
			case 'w': /* read-write */
				setMode(ZIP_RW);
			case 'x': /* password protected */
				setMode(ZIP_PW);
			case 'u': /* password protected */
				setMode(ZIP_UNLOCK_TIL_EJECT);
			case 'h':
				usage(0);
			default:  /* unrecognized */
				usage(1);

		}
	}

	if (request == ZIP_NIX) request = ZIP_STATUS;  /* default action */

	if (argc - optind > 1 ||
	    (argc - optind == 1 &&
	     (!argv[optind][0] || argv[optind][1] != ':')))
		usage(1);

	drive = ch_toupper(argc - optind == 1 ? argv[argc - 1][0] : ':');

	for (dev = devices; dev->name; dev++) {
		unsigned char cdb[6] = { 0, 0, 0, 0, 0, 0 };
		struct {
			char    type,
				type_modifier,
				scsi_version,
				data_format,
				length,
				reserved1[2],
				capabilities,
				vendor[8],
				product[16],
				revision[4],
				vendor_specific[20],
				reserved2[40];
		} inq_data;

		if (dev->drive != drive)
			continue;
		expand(dev->name, name);
		if ((request & (ZIP_MODE_CHANGE | ZIP_EJECT)) &&
		    !(request & ZIP_FORCE) &&
		    test_mounted(name)) {
			fprintf(stderr,
				"Can\'t change status of/eject mounted device\n");
			exit(1);
		}
		precmd(dev);

		if(IS_PRIVILEGED(dev))
			reclaim_privs();
		fd = scsi_open(name, O_RDONLY
#ifdef O_NDELAY
			       | O_NDELAY
#endif
			       , 0644,
			       &extra_data);
		if(IS_PRIVILEGED(dev))
			drop_privs();

				/* need readonly, else we can't
				 * open the drive on Solaris if
				 * write-protected */
		if (fd == -1)
			continue;
		closeExec(fd);

		if (!(request & (ZIP_MODE_CHANGE | ZIP_STATUS)))
			/* if no mode change or ZIP specific status is
			 * involved, the command (eject) is applicable
			 * on all drives */
			break;

		cdb[0] = SCSI_INQUIRY;
		cdb[4] = sizeof inq_data;
		if (zip_cmd(IS_PRIVILEGED(dev), fd, cdb, 6, SCSI_IO_READ,
			    &inq_data, sizeof inq_data, extra_data) != 0) {
			close(fd);
			continue;
		}

#ifdef DEBUG
		fprintf(stderr, "device: %s\n\tvendor: %.8s\n\tproduct: %.16s\n"
			"\trevision: %.4s\n", name, inq_data.vendor,
			inq_data.product, inq_data.revision);
#endif /* DEBUG */

		if (strncasecmp("IOMEGA  ", inq_data.vendor,
				sizeof inq_data.vendor) ||
		    (strncasecmp("ZIP 100         ",
				 inq_data.product, sizeof inq_data.product) &&
		     strncasecmp("ZIP 100 PLUS    ",
				 inq_data.product, sizeof inq_data.product) &&
		     strncasecmp("ZIP 250         ",
				 inq_data.product, sizeof inq_data.product) &&
		     strncasecmp("ZIP 750         ",
				 inq_data.product, sizeof inq_data.product) &&
		     strncasecmp("JAZ 1GB         ",
				 inq_data.product, sizeof inq_data.product) &&
		     strncasecmp("JAZ 2GB         ",
				 inq_data.product, sizeof inq_data.product))) {

			/* debugging */
			fprintf(stderr,"Skipping drive with vendor='");
			fwrite(inq_data.vendor,1, sizeof(inq_data.vendor),
			       stderr);
			fprintf(stderr,"' product='");
			fwrite(inq_data.product,1, sizeof(inq_data.product),
			       stderr);
			fprintf(stderr,"'\n");
			/* end debugging */
			close(fd);
			continue;
		}
		break;  /* found Zip/Jaz drive */
	}

	if (dev->drive == 0) {
		fprintf(stderr, "%s: drive '%c:' is not a Zip or Jaz drive\n",
			argv[0], drive);
		exit(1);
	}

	if (request & (ZIP_MODE_CHANGE | ZIP_STATUS))
		oldMode = get_zip_status(IS_PRIVILEGED(dev), fd, extra_data);

	if (request & ZIP_MODE_CHANGE) {
				/* request temp unlock, and disk is already unlocked */
		if(newMode == ZIP_UNLOCK_TIL_EJECT &&
		   (oldMode & ZIP_UNLOCK_TIL_EJECT))
			request &= ~ZIP_MODE_CHANGE;

				/* no password change requested, and disk is already
				 * in the requested state */
		if(!(newMode & 0x01) && newMode == oldMode)
			request &= ~ZIP_MODE_CHANGE;
	}

	if (request & ZIP_MODE_CHANGE) {
		int ret;
		uint8_t unlockMode, unlockMask;
		const char *passwd;
		char dummy[1];

		if(newMode == ZIP_UNLOCK_TIL_EJECT) {
			unlockMode = newMode | oldMode;
			unlockMask = 9;
		} else {
			unlockMode = newMode & ~0x5;
			unlockMask = 1;
		}

		if ((oldMode & unlockMask) == 1) {  /* unlock first */
			char *s;
			passwd = "APlaceForYourStuff";
			if ((s = strchr(passwd, '\n'))) *s = '\0';  /* chomp */
			iomega_command(IS_PRIVILEGED(dev), fd, unlockMode,
				       passwd, extra_data);
		}

		if ((get_zip_status(IS_PRIVILEGED(dev), fd, extra_data) &
		     unlockMask) == 1) {
			/* unlock first */
			char *s;
			passwd = getpass("Password: ");
			if ((s = strchr(passwd, '\n'))) *s = '\0';  /* chomp */
			if((ret=iomega_command(IS_PRIVILEGED(dev), fd,
					       unlockMode, passwd,
					       extra_data))){
				if (ret == -1) perror("passwd: ");
				else fprintf(stderr, "wrong password\n");
				exit(1);
			}
			if((get_zip_status(IS_PRIVILEGED(dev),
					   fd, extra_data) &
			    unlockMask) == 1) {
				fprintf(stderr, "wrong password\n");
				exit(1);
			}
		}

		if (newMode & 0x1) {
			char first_try[_PASSWORD_LEN+1];

			passwd = getpass("Enter new password:");
			strncpy(first_try, passwd,_PASSWORD_LEN);
			passwd = getpass("Re-type new password:");
			if(strncmp(first_try, passwd, _PASSWORD_LEN)) {
				fprintf(stderr,
					"You misspelled it. Password not set.\n");
				exit(1);
			}
		} else {
			passwd = dummy;
			dummy[0] = '\0';
		}

		if(newMode == ZIP_UNLOCK_TIL_EJECT)
			newMode |= oldMode;

		if((ret=iomega_command(IS_PRIVILEGED(dev), fd,
				       newMode, passwd, extra_data))){
			if (ret == -1) perror("set passwd: ");
			else fprintf(stderr, "password not changed\n");
			exit(1);
		}
#ifdef OS_linux
		ioctl(fd, BLKRRPART); /* revalidate the disk, so that the
					 kernel notices that its writable
					 status has changed */
#endif
	}

	if (request & ZIP_STATUS) {
		const char *unlocked;

		if(oldMode & 8)
			unlocked = " and unlocked until eject";
		else
			unlocked = "";
		switch (oldMode & ~8) {
			case ZIP_RW:
				printf("Drive '%c:' is not write-protected\n",
				       drive);
				break;
			case ZIP_RO:
				printf("Drive '%c:' is write-protected%s\n",
				       drive, unlocked);
				break;
			case ZIP_RO_PW:
				printf("Drive '%c:' is password write-protected%s\n",
				       drive, unlocked);
				break;
			case ZIP_PW:
				printf("Drive '%c:' is password protected%s\n",
				       drive, unlocked);
				break;
			default:
				printf("Unknown protection mode %d of drive '%c:'\n",
				       oldMode, drive);
				break;
		}
	}

	if (request & ZIP_EJECT) {
		if(request & ZIP_FORCE)
			if(door_command(IS_PRIVILEGED(dev), fd,
					SCSI_ALLOW_MEDIUM_REMOVAL, 0,
					extra_data) < 0) {
				perror("door unlock: ");
				exit(1);
			}

		if(door_command(IS_PRIVILEGED(dev), fd,
				SCSI_START_STOP, 1,
				extra_data) < 0) {
			perror("stop motor: ");
			exit(1);
		}

		if(door_command(IS_PRIVILEGED(dev), fd,
				SCSI_START_STOP, 2, extra_data) < 0) {
			perror("eject: ");
			exit(1);
		}
		if(door_command(IS_PRIVILEGED(dev), fd,
				SCSI_START_STOP, 2, extra_data) < 0) {
			perror("second eject: ");
			exit(1);
		}
	}

	close(fd);
	exit(0);
}
