text: /*-
   * Copyright (c) 2000 Kelly Yancey <kbyanc@posi.net>
   * Derived from work done by Julian Elischer <julian@tfs.com,
   * julian@dialix.oz.au>, 1993, and Peter Dufault <dufault@hda.com>, 1994.
   * All rights reserved.
   *
   * 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,
   *    without modification, immediately at the beginning of the file.
   * 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 AUTHOR ``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 AUTHOR 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.
   */
  
  #include <sys/cdefs.h>
  __FBSDID("$FreeBSD: soc2013/dpl/head/sbin/camcontrol/modeedit.c 242917 2012-10-19 14:49:42Z ed $");
  
  #include <sys/queue.h>
  #include <sys/types.h>
  
  #include <assert.h>
  #include <ctype.h>
  #include <err.h>
  #include <errno.h>
  #include <stdlib.h>
  #include <string.h>
  #include <stdio.h>
  #include <sysexits.h>
  #include <unistd.h>
  
  #include <cam/scsi/scsi_all.h>
  #include <cam/cam.h>
  #include <cam/cam_ccb.h>
  #include <camlib.h>
  #include "camcontrol.h"
  
  #define	DEFAULT_SCSI_MODE_DB	"/usr/share/misc/scsi_modes"
  #define	DEFAULT_EDITOR		"vi"
  #define	MAX_FORMAT_SPEC		4096	/* Max CDB format specifier. */
  #define	MAX_PAGENUM_LEN		10	/* Max characters in page num. */
  #define	MAX_PAGENAME_LEN	64	/* Max characters in page name. */
  #define	PAGEDEF_START		'{'	/* Page definition delimiter. */
  #define	PAGEDEF_END		'}'	/* Page definition delimiter. */
  #define	PAGENAME_START		'"'	/* Page name delimiter. */
  #define	PAGENAME_END		'"'	/* Page name delimiter. */
  #define	PAGEENTRY_END		';'	/* Page entry terminator (optional). */
  #define	MAX_COMMAND_SIZE	255	/* Mode/Log sense data buffer size. */
  #define PAGE_CTRL_SHIFT		6	/* Bit offset to page control field. */
  
  
  /* Macros for working with mode pages. */
  #define	MODE_PAGE_HEADER(mh)						\
  	(struct scsi_mode_page_header *)find_mode_page_6(mh)
  
  #define	MODE_PAGE_DATA(mph)						\
  	(u_int8_t *)(mph) + sizeof(struct scsi_mode_page_header)
  
  
  struct editentry {
  	STAILQ_ENTRY(editentry) link;
  	char	*name;
  	char	type;
  	int	editable;
  	int	size;
  	union {
  		int	ivalue;
  		char	*svalue;
  	} value;
  };
  static STAILQ_HEAD(, editentry) editlist; /* List of page entries. */
  static int editlist_changed = 0;	/* Whether any entries were changed. */
  
  struct pagename {
  	SLIST_ENTRY(pagename) link;
  	int pagenum;
  	char *name;
  };
  static SLIST_HEAD(, pagename) namelist;	/* Page number to name mappings. */
  
  static char format[MAX_FORMAT_SPEC];	/* Buffer for scsi cdb format def. */
  
  static FILE *edit_file = NULL;		/* File handle for edit file. */
  static char edit_path[] = "/tmp/camXXXXXX";
  
  
  /* Function prototypes. */
  static void		 editentry_create(void *hook, int letter, void *arg,
  					  int count, char *name);
  static void		 editentry_update(void *hook, int letter, void *arg,
  					  int count, char *name);
  static int		 editentry_save(void *hook, char *name);
  static struct editentry	*editentry_lookup(char *name);
  static int		 editentry_set(char *name, char *newvalue,
  				       int editonly);
  static void		 editlist_populate(struct cam_device *device,
  					   int modepage, int page_control,
  					   int dbd, int retries, int timeout);
  static void		 editlist_save(struct cam_device *device, int modepage,
  				       int page_control, int dbd, int retries,
  				       int timeout);
  static void		 nameentry_create(int pagenum, char *name);
  static struct pagename	*nameentry_lookup(int pagenum);
  static int		 load_format(const char *pagedb_path, int page);
  static int		 modepage_write(FILE *file, int editonly);
  static int		 modepage_read(FILE *file);
  static void		 modepage_edit(void);
  static void		 modepage_dump(struct cam_device *device, int page,
  				       int page_control, int dbd, int retries,
  				       int timeout);
  static void		 cleanup_editfile(void);
  
  
  #define	returnerr(code) do {						\
  	errno = code;							\
  	return (-1);							\
  } while (0)
  
  
  #define	RTRIM(string) do {						\
  	int _length;							\
  	while (isspace(string[_length = strlen(string) - 1]))		\
  		string[_length] = '\0';					\
  } while (0)
  
  
  static void
  editentry_create(void *hook __unused, int letter, void *arg, int count,
  		 char *name)
  {
  	struct editentry *newentry;	/* Buffer to hold new entry. */
  
  	/* Allocate memory for the new entry and a copy of the entry name. */
  	if ((newentry = malloc(sizeof(struct editentry))) == NULL ||
  	    (newentry->name = strdup(name)) == NULL)
  		err(EX_OSERR, NULL);
  
  	/* Trim any trailing whitespace for the entry name. */
  	RTRIM(newentry->name);
  
  	newentry->editable = (arg != NULL);
  	newentry->type = letter;
  	newentry->size = count;		/* Placeholder; not accurate. */
  	newentry->value.svalue = NULL;
  
  	STAILQ_INSERT_TAIL(&editlist, newentry, link);
  }
  
  static void
  editentry_update(void *hook __unused, int letter, void *arg, int count,
  		 char *name)
  {
  	struct editentry *dest;		/* Buffer to hold entry to update. */
  
  	dest = editentry_lookup(name);
  	assert(dest != NULL);
  
  	dest->type = letter;
  	dest->size = count;		/* We get the real size now. */
  
  	switch (dest->type) {
  	case 'i':			/* Byte-sized integral type. */
  	case 'b':			/* Bit-sized integral types. */
  	case 't':
  		dest->value.ivalue = (intptr_t)arg;
  		break;
  
  	case 'c':			/* Character array. */
  	case 'z':			/* Null-padded string. */
  		editentry_set(name, (char *)arg, 0);
  		break;
  	default:
  		; /* NOTREACHED */
  	}
  }
  
  static int
  editentry_save(void *hook __unused, char *name)
  {
  	struct editentry *src;		/* Entry value to save. */
  
  	src = editentry_lookup(name);
  	assert(src != NULL);
  
  	switch (src->type) {
  	case 'i':			/* Byte-sized integral type. */
  	case 'b':			/* Bit-sized integral types. */
  	case 't':
  		return (src->value.ivalue);
  		/* NOTREACHED */
  
  	case 'c':			/* Character array. */
  	case 'z':			/* Null-padded string. */
  		return ((intptr_t)src->value.svalue);
  		/* NOTREACHED */
  
  	default:
  		; /* NOTREACHED */
  	}
  
  	return (0);			/* This should never happen. */
  }
  
  static struct editentry *
  editentry_lookup(char *name)
  {
  	struct editentry *scan;
  
  	assert(name != NULL);
  
  	STAILQ_FOREACH(scan, &editlist, link) {
  		if (strcasecmp(scan->name, name) == 0)
  			return (scan);
  	}
  
  	/* Not found during list traversal. */
  	return (NULL);
  }
  
  static int
  editentry_set(char *name, char *newvalue, int editonly)
  {
  	struct editentry *dest;	/* Modepage entry to update. */
  	char *cval;		/* Pointer to new string value. */
  	char *convertend;	/* End-of-conversion pointer. */
  	int ival;		/* New integral value. */
  	int resolution;		/* Resolution in bits for integer conversion. */
  
  /*
   * Macro to determine the maximum value of the given size for the current
   * resolution.
   * XXX Lovely x86's optimize out the case of shifting by 32 and gcc doesn't
   *     currently workaround it (even for int64's), so we have to kludge it.
   */
  #define	RESOLUTION_MAX(size) ((resolution * (size) == 32)? 		\
  	(int)0xffffffff: (1 << (resolution * (size))) - 1)
  
  	assert(newvalue != NULL);
  	if (*newvalue == '\0')
  		return (0);	/* Nothing to do. */
  
  	if ((dest = editentry_lookup(name)) == NULL)
  		returnerr(ENOENT);
  	if (!dest->editable && editonly)
  		returnerr(EPERM);
  
  	switch (dest->type) {
  	case 'i':		/* Byte-sized integral type. */
  	case 'b':		/* Bit-sized integral types. */
  	case 't':
  		/* Convert the value string to an integer. */
  		resolution = (dest->type == 'i')? 8: 1;
  		ival = (int)strtol(newvalue, &convertend, 0);
  		if (*convertend != '\0')
  			returnerr(EINVAL);
  		if (ival > RESOLUTION_MAX(dest->size) || ival < 0) {
  			int newival = (ival < 0)? 0: RESOLUTION_MAX(dest->size);
  			warnx("value %d is out of range for entry %s; clipping "
  			    "to %d", ival, name, newival);
  			ival = newival;
  		}
  		if (dest->value.ivalue != ival)
  			editlist_changed = 1;
  		dest->value.ivalue = ival;
  		break;
  
  	case 'c':		/* Character array. */
  	case 'z':		/* Null-padded string. */
  		if ((cval = malloc(dest->size + 1)) == NULL)
  			err(EX_OSERR, NULL);
  		bzero(cval, dest->size + 1);
  		strncpy(cval, newvalue, dest->size);
  		if (dest->type == 'z') {
  			/* Convert trailing spaces to nulls. */
  			char *convertend2;
  
  			for (convertend2 = cval + dest->size;
  			    convertend2 >= cval; convertend2--) {
  				if (*convertend2 == ' ')
  					*convertend2 = '\0';
  				else if (*convertend2 != '\0')
  					break;
  			}
  		}
  		if (strncmp(dest->value.svalue, cval, dest->size) == 0) {
  			/* Nothing changed, free the newly allocated string. */
  			free(cval);
  			break;
  		}
  		if (dest->value.svalue != NULL) {
  			/* Free the current string buffer. */
  			free(dest->value.svalue);
  			dest->value.svalue = NULL;
  		}
  		dest->value.svalue = cval;
  		editlist_changed = 1;
  		break;
  
  	default:
  		; /* NOTREACHED */
  	}
  
  	return (0);
  #undef RESOLUTION_MAX
  }
  
  static void
  nameentry_create(int pagenum, char *name) {
  	struct pagename *newentry;
  
  	if (pagenum < 0 || name == NULL || name[0] == '\0')
  		return;
  
  	/* Allocate memory for the new entry and a copy of the entry name. */
  	if ((newentry = malloc(sizeof(struct pagename))) == NULL ||
  	    (newentry->name = strdup(name)) == NULL)
  		err(EX_OSERR, NULL);
  
  	/* Trim any trailing whitespace for the page name. */
  	RTRIM(newentry->name);
  
  	newentry->pagenum = pagenum;
  	SLIST_INSERT_HEAD(&namelist, newentry, link);
  }
  
  static struct pagename *
  nameentry_lookup(int pagenum) {
  	struct pagename *scan;
  
  	SLIST_FOREACH(scan, &namelist, link) {
  		if (pagenum == scan->pagenum)
  			return (scan);
  	}
  
  	/* Not found during list traversal. */
  	return (NULL);
  }
  
  static int
  load_format(const char *pagedb_path, int page)
  {
  	FILE *pagedb;
  	char str_pagenum[MAX_PAGENUM_LEN];
  	char str_pagename[MAX_PAGENAME_LEN];
  	int pagenum;
  	int depth;			/* Quoting depth. */
  	int found;
  	int lineno;
  	enum { LOCATE, PAGENAME, PAGEDEF } state;
  	int ch;
  	char c;
  
  #define	SETSTATE_LOCATE do {						\
  	str_pagenum[0] = '\0';						\
  	str_pagename[0] = '\0';						\
  	pagenum = -1;							\
  	state = LOCATE;							\
  } while (0)
  
  #define	SETSTATE_PAGENAME do {						\
  	str_pagename[0] = '\0';						\
  	state = PAGENAME;						\
  } while (0)
  
  #define	SETSTATE_PAGEDEF do {						\
  	format[0] = '\0';						\
  	state = PAGEDEF;						\
  } while (0)
  
  #define	UPDATE_LINENO do {						\
  	if (c == '\n')							\
  		lineno++;						\
  } while (0)
  
  #define	BUFFERFULL(buffer)	(strlen(buffer) + 1 >= sizeof(buffer))
  
  	if ((pagedb = fopen(pagedb_path, "r")) == NULL)
  		returnerr(ENOENT);
  
  	SLIST_INIT(&namelist);
  
  	c = '\0';
  	depth = 0;
  	lineno = 0;
  	found = 0;
  	SETSTATE_LOCATE;
  	while ((ch = fgetc(pagedb)) != EOF) {
  
  		/* Keep a line count to make error messages more useful. */
  		UPDATE_LINENO;
  
  		/* Skip over comments anywhere in the mode database. */
  		if (ch == '#') {
  			do {
  				ch = fgetc(pagedb);
  			} while (ch != '\n' && ch != EOF);
  			UPDATE_LINENO;
  			continue;
  		}
  		c = ch;
  
  		/* Strip out newline characters. */
  		if (c == '\n')
  			continue;
  
  		/* Keep track of the nesting depth for braces. */
  		if (c == PAGEDEF_START)
  			depth++;
  		else if (c == PAGEDEF_END) {
  			depth--;
  			if (depth < 0) {
  				errx(EX_OSFILE, "%s:%d: %s", pagedb_path,
  				    lineno, "mismatched bracket");
  			}
  		}
  
  		switch (state) {
  		case LOCATE:
  			/*
  			 * Locate the page the user is interested in, skipping
  			 * all others.
  			 */
  			if (isspace(c)) {
  				/* Ignore all whitespace between pages. */
  				break;
  			} else if (depth == 0 && c == PAGEENTRY_END) {
  				/*
  				 * A page entry terminator will reset page
  				 * scanning (useful for assigning names to
  				 * modes without providing a mode definition).
  				 */
  				/* Record the name of this page. */
  				pagenum = strtol(str_pagenum, NULL, 0);
  				nameentry_create(pagenum, str_pagename);
  				SETSTATE_LOCATE;
  			} else if (depth == 0 && c == PAGENAME_START) {
  				SETSTATE_PAGENAME;
  			} else if (c == PAGEDEF_START) {
  				pagenum = strtol(str_pagenum, NULL, 0);
  				if (depth == 1) {
  					/* Record the name of this page. */
  					nameentry_create(pagenum, str_pagename);
  					/*
  					 * Only record the format if this is
  					 * the page we are interested in.
  					 */
  					if (page == pagenum && !found)
  						SETSTATE_PAGEDEF;
  				}
  			} else if (c == PAGEDEF_END) {
  				/* Reset the processor state. */
  				SETSTATE_LOCATE;
  			} else if (depth == 0 && ! BUFFERFULL(str_pagenum)) {
  				strncat(str_pagenum, &c, 1);
  			} else if (depth == 0) {
  				errx(EX_OSFILE, "%s:%d: %s %zd %s", pagedb_path,
  				    lineno, "page identifier exceeds",
  				    sizeof(str_pagenum) - 1, "characters");
  			}
  			break;
  
  		case PAGENAME:
  			if (c == PAGENAME_END) {
  				/*
  				 * Return to LOCATE state without resetting the
  				 * page number buffer.
  				 */
  				state = LOCATE;
  			} else if (! BUFFERFULL(str_pagename)) {
  				strncat(str_pagename, &c, 1);
  			} else {
  				errx(EX_OSFILE, "%s:%d: %s %zd %s", pagedb_path,
  				    lineno, "page name exceeds",
  				    sizeof(str_pagenum) - 1, "characters");
  			}
  			break;
  
  		case PAGEDEF:
  			/*
  			 * Transfer the page definition into a format buffer
  			 * suitable for use with CDB encoding/decoding routines.
  			 */
  			if (depth == 0) {
  				found = 1;
  				SETSTATE_LOCATE;
  			} else if (! BUFFERFULL(format)) {
  				strncat(format, &c, 1);
  			} else {
  				errx(EX_OSFILE, "%s:%d: %s %zd %s", pagedb_path,
  				    lineno, "page definition exceeds",
  				    sizeof(format) - 1, "characters");
  			}
  			break;
  
  		default:
  			; /* NOTREACHED */
  		}
  
  		/* Repeat processing loop with next character. */
  	}
  
  	if (ferror(pagedb))
  		err(EX_OSFILE, "%s", pagedb_path);
  
  	/* Close the SCSI page database. */
  	fclose(pagedb);
  
  	if (!found)			/* Never found a matching page. */
  		returnerr(ESRCH);
  
  	return (0);
  }
  
  static void
  editlist_populate(struct cam_device *device, int modepage, int page_control,
  		  int dbd, int retries, int timeout)
  {
  	u_int8_t data[MAX_COMMAND_SIZE];/* Buffer to hold sense data. */
  	u_int8_t *mode_pars;		/* Pointer to modepage params. */
  	struct scsi_mode_header_6 *mh;	/* Location of mode header. */
  	struct scsi_mode_page_header *mph;
  
  	STAILQ_INIT(&editlist);
  
  	/* Fetch changeable values; use to build initial editlist. */
  	mode_sense(device, modepage, 1, dbd, retries, timeout, data,
  		   sizeof(data));
  
  	mh = (struct scsi_mode_header_6 *)data;
  	mph = MODE_PAGE_HEADER(mh);
  	mode_pars = MODE_PAGE_DATA(mph);
  
  	/* Decode the value data, creating edit_entries for each value. */
  	buff_decode_visit(mode_pars, mh->data_length, format,
  	    editentry_create, 0);
  
  	/* Fetch the current/saved values; use to set editentry values. */
  	mode_sense(device, modepage, page_control, dbd, retries, timeout, data,
  		   sizeof(data));
  	buff_decode_visit(mode_pars, mh->data_length, format,
  	    editentry_update, 0);
  }
  
  static void
  editlist_save(struct cam_device *device, int modepage, int page_control,
  	      int dbd, int retries, int timeout)
  {
  	u_int8_t data[MAX_COMMAND_SIZE];/* Buffer to hold sense data. */
  	u_int8_t *mode_pars;		/* Pointer to modepage params. */
  	struct scsi_mode_header_6 *mh;	/* Location of mode header. */
  	struct scsi_mode_page_header *mph;
  
  	/* Make sure that something changed before continuing. */
  	if (! editlist_changed)
  		return;
  
  	/*
  	 * Preload the CDB buffer with the current mode page data.
  	 * XXX If buff_encode_visit would return the number of bytes encoded
  	 *     we *should* use that to build a header from scratch. As it is
  	 *     now, we need mode_sense to find out the page length.
  	 */
  	mode_sense(device, modepage, page_control, dbd, retries, timeout, data,
  		   sizeof(data));
  
  	/* Initial headers & offsets. */
  	mh = (struct scsi_mode_header_6 *)data;
  	mph = MODE_PAGE_HEADER(mh);
  	mode_pars = MODE_PAGE_DATA(mph);
  
  	/* Encode the value data to be passed back to the device. */
  	buff_encode_visit(mode_pars, mh->data_length, format,
  	    editentry_save, 0);
  
  	/* Eliminate block descriptors. */
  	bcopy(mph, ((u_int8_t *)mh) + sizeof(*mh),
  	    sizeof(*mph) + mph->page_length);
  
  	/* Recalculate headers & offsets. */
  	mh->blk_desc_len = 0;		/* No block descriptors. */
  	mh->dev_spec = 0;		/* Clear device-specific parameters. */
  	mph = MODE_PAGE_HEADER(mh);
  	mode_pars = MODE_PAGE_DATA(mph);
  
  	mph->page_code &= SMS_PAGE_CODE;/* Isolate just the page code. */
  	mh->data_length = 0;		/* Reserved for MODE SELECT command. */
  
  	/*
  	 * Write the changes back to the device. If the user editted control
  	 * page 3 (saved values) then request the changes be permanently
  	 * recorded.
  	 */
  	mode_select(device,
  	    (page_control << PAGE_CTRL_SHIFT == SMS_PAGE_CTRL_SAVED),
  	    retries, timeout, (u_int8_t *)mh,
  	    sizeof(*mh) + mh->blk_desc_len + sizeof(*mph) + mph->page_length);
  }
  
  static int
  modepage_write(FILE *file, int editonly)
  {
  	struct editentry *scan;
  	int written = 0;
  
  	STAILQ_FOREACH(scan, &editlist, link) {
  		if (scan->editable || !editonly) {
  			written++;
  			if (scan->type == 'c' || scan->type == 'z') {
  				fprintf(file, "%s:  %s\n", scan->name,
  				    scan->value.svalue);
  			} else {
  				fprintf(file, "%s:  %d\n", scan->name,
  				    scan->value.ivalue);
  			}
  		}
  	}
  	return (written);
  }
  
  static int
  modepage_read(FILE *file)
  {
  	char *buffer;			/* Pointer to dynamic line buffer.  */
  	char *line;			/* Pointer to static fgetln buffer. */
  	char *name;			/* Name portion of the line buffer. */
  	char *value;			/* Value portion of line buffer.    */
  	size_t length;			/* Length of static fgetln buffer.  */
  
  #define	ABORT_READ(message, param) do {					\
  	warnx(message, param);						\
  	free(buffer);							\
  	returnerr(EAGAIN);						\
  } while (0)
  
  	while ((line = fgetln(file, &length)) != NULL) {
  		/* Trim trailing whitespace (including optional newline). */
  		while (length > 0 && isspace(line[length - 1]))
  			length--;
  
  	    	/* Allocate a buffer to hold the line + terminating null. */
  	    	if ((buffer = malloc(length + 1)) == NULL)
  			err(EX_OSERR, NULL);
  		memcpy(buffer, line, length);
  		buffer[length] = '\0';
  
  		/* Strip out comments. */
  		if ((value = strchr(buffer, '#')) != NULL)
  			*value = '\0';
  
  		/* The name is first in the buffer. Trim whitespace.*/
  		name = buffer;
  		RTRIM(name);
  		while (isspace(*name))
  			name++;
  
  		/* Skip empty lines. */
  		if (strlen(name) == 0)
  			continue;
  
  		/* The name ends at the colon; the value starts there. */
  		if ((value = strrchr(buffer, ':')) == NULL)
  			ABORT_READ("no value associated with %s", name);
  		*value = '\0';			/* Null-terminate name. */
  		value++;			/* Value starts afterwards. */
  
  		/* Trim leading and trailing whitespace. */
  		RTRIM(value);
  		while (isspace(*value))
  			value++;
  
  		/* Make sure there is a value left. */
  		if (strlen(value) == 0)
  			ABORT_READ("no value associated with %s", name);
  
  		/* Update our in-memory copy of the modepage entry value. */
  		if (editentry_set(name, value, 1) != 0) {
  			if (errno == ENOENT) {
  				/* No entry by the name. */
  				ABORT_READ("no such modepage entry \"%s\"",
  				    name);
  			} else if (errno == EINVAL) {
  				/* Invalid value. */
  				ABORT_READ("Invalid value for entry \"%s\"",
  				    name);
  			} else if (errno == ERANGE) {
  				/* Value out of range for entry type. */
  				ABORT_READ("value out of range for %s", name);
  			} else if (errno == EPERM) {
  				/* Entry is not editable; not fatal. */
  				warnx("modepage entry \"%s\" is read-only; "
  				    "skipping.", name);
  			}
  		}
  
  		free(buffer);
  	}
  	return (ferror(file)? -1: 0);
  
  #undef ABORT_READ
  }
  
  static void
  modepage_edit(void)
  {
  	const char *editor;
  	char *commandline;
  	int fd;
  	int written;
  
  	if (!isatty(fileno(stdin))) {
  		/* Not a tty, read changes from stdin. */
  		modepage_read(stdin);
  		return;
  	}
  
  	/* Lookup editor to invoke. */
  	if ((editor = getenv("EDITOR")) == NULL)
  		editor = DEFAULT_EDITOR;
  
  	/* Create temp file for editor to modify. */
  	if ((fd = mkstemp(edit_path)) == -1)
  		errx(EX_CANTCREAT, "mkstemp failed");
  
  	atexit(cleanup_editfile);
  
  	if ((edit_file = fdopen(fd, "w")) == NULL)
  		err(EX_NOINPUT, "%s", edit_path);
  
  	written = modepage_write(edit_file, 1);
  
  	fclose(edit_file);
  	edit_file = NULL;
  
  	if (written == 0) {
  		warnx("no editable entries");
  		cleanup_editfile();
  		return;
  	}
  
  	/*
  	 * Allocate memory to hold the command line (the 2 extra characters
  	 * are to hold the argument separator (a space), and the terminating
  	 * null character.
  	 */
  	commandline = malloc(strlen(editor) + strlen(edit_path) + 2);
  	if (commandline == NULL)
  		err(EX_OSERR, NULL);
  	sprintf(commandline, "%s %s", editor, edit_path);
  
  	/* Invoke the editor on the temp file. */
  	if (system(commandline) == -1)
  		err(EX_UNAVAILABLE, "could not invoke %s", editor);
  	free(commandline);
  
  	if ((edit_file = fopen(edit_path, "r")) == NULL)
  		err(EX_NOINPUT, "%s", edit_path);
  
  	/* Read any changes made to the temp file. */
  	modepage_read(edit_file);
  
  	cleanup_editfile();
  }
  
  static void
  modepage_dump(struct cam_device *device, int page, int page_control, int dbd,
  	      int retries, int timeout)
  {
  	u_int8_t data[MAX_COMMAND_SIZE];/* Buffer to hold sense data. */
  	u_int8_t *mode_pars;		/* Pointer to modepage params. */
  	struct scsi_mode_header_6 *mh;	/* Location of mode header. */
  	struct scsi_mode_page_header *mph;
  	int indx;			/* Index for scanning mode params. */
  
  	mode_sense(device, page, page_control, dbd, retries, timeout, data,
  		   sizeof(data));
  
  	mh = (struct scsi_mode_header_6 *)data;
  	mph = MODE_PAGE_HEADER(mh);
  	mode_pars = MODE_PAGE_DATA(mph);
  
  	/* Print the raw mode page data with newlines each 8 bytes. */
  	for (indx = 0; indx < mph->page_length; indx++) {
  		printf("%02x%c",mode_pars[indx],
  		    (((indx + 1) % 8) == 0) ? '\n' : ' ');
  	}
  	putchar('\n');
  }
  
  static void
  cleanup_editfile(void)
  {
  	if (edit_file == NULL)
  		return;
  	if (fclose(edit_file) != 0 || unlink(edit_path) != 0)
  		warn("%s", edit_path);
  	edit_file = NULL;
  }
  
  void
  mode_edit(struct cam_device *device, int page, int page_control, int dbd,
  	  int edit, int binary, int retry_count, int timeout)
  {
  	const char *pagedb_path;	/* Path to modepage database. */
  
  	if (edit && binary)
  		errx(EX_USAGE, "cannot edit in binary mode.");
  
  	if (! binary) {
  		if ((pagedb_path = getenv("SCSI_MODES")) == NULL)
  			pagedb_path = DEFAULT_SCSI_MODE_DB;
  
  		if (load_format(pagedb_path, page) != 0 && (edit || verbose)) {
  			if (errno == ENOENT) {
  				/* Modepage database file not found. */
  				warn("cannot open modepage database \"%s\"",
  				    pagedb_path);
  			} else if (errno == ESRCH) {
  				/* Modepage entry not found in database. */
  				warnx("modepage %d not found in database"
  				    "\"%s\"", page, pagedb_path);
  			}
  			/* We can recover in display mode, otherwise we exit. */
  			if (!edit) {
  				warnx("reverting to binary display only");
  				binary = 1;
  			} else
  				exit(EX_OSFILE);
  		}
  
  		editlist_populate(device, page, page_control, dbd, retry_count,
  			timeout);
  	}
  
  	if (edit) {
  		if (page_control << PAGE_CTRL_SHIFT != SMS_PAGE_CTRL_CURRENT &&
  		    page_control << PAGE_CTRL_SHIFT != SMS_PAGE_CTRL_SAVED)
  			errx(EX_USAGE, "it only makes sense to edit page 0 "
  			    "(current) or page 3 (saved values)");
  		modepage_edit();
  		editlist_save(device, page, page_control, dbd, retry_count,
  			timeout);
  	} else if (binary || STAILQ_EMPTY(&editlist)) {
  		/* Display without formatting information. */
  		modepage_dump(device, page, page_control, dbd, retry_count,
  		    timeout);
  	} else {
  		/* Display with format. */
  		modepage_write(stdout, 0);
  	}
  }
  
  void
  mode_list(struct cam_device *device, int page_control, int dbd,
  	  int retry_count, int timeout)
  {
  	u_int8_t data[MAX_COMMAND_SIZE];/* Buffer to hold sense data. */
  	struct scsi_mode_header_6 *mh;	/* Location of mode header. */
  	struct scsi_mode_page_header *mph;
  	struct pagename *nameentry;
  	const char *pagedb_path;
  	int len;
  
  	if ((pagedb_path = getenv("SCSI_MODES")) == NULL)
  		pagedb_path = DEFAULT_SCSI_MODE_DB;
  
  	if (load_format(pagedb_path, 0) != 0 && verbose && errno == ENOENT) {
  		/* Modepage database file not found. */
  		warn("cannot open modepage database \"%s\"", pagedb_path);
  	}
  
  	/* Build the list of all mode pages by querying the "all pages" page. */
  	mode_sense(device, SMS_ALL_PAGES_PAGE, page_control, dbd, retry_count,
  	    timeout, data, sizeof(data));
  
  	mh = (struct scsi_mode_header_6 *)data;
  	len = mh->blk_desc_len;		/* Skip block descriptors. */
  	/* Iterate through the pages in the reply. */
  	while (len < mh->data_length) {
  		/* Locate the next mode page header. */
  		mph = (struct scsi_mode_page_header *)
  		    ((intptr_t)mh + sizeof(*mh) + len);
  
  		mph->page_code &= SMS_PAGE_CODE;
  		nameentry = nameentry_lookup(mph->page_code);
  
  		if (nameentry == NULL || nameentry->name == NULL)
  			printf("0x%02x\n", mph->page_code);
  		else
  			printf("0x%02x\t%s\n", mph->page_code,
  			    nameentry->name); 
  		len += mph->page_length + sizeof(*mph);
  	}
  }
  