/*
 * firmware handling tools
 *
 * Copyright (C) 2001 Kees Cook
 * cook@cpoint.net, http://outflux.net/
 *
 * 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
 * of the License, 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <fcntl.h>
#include "scsi_iface.h"
#include "scsi_tools.h"
#include "firmware.h"


int write_buffer(int fd, int timeout,
		 u_char lun, u_char mode, u_char buffer_id, 
		 u_char bufoff1, u_char bufoff2, u_char bufoff3,
		 u_char paraml1, u_char paraml2, u_char paraml3,
		 u_char control, int datalen, u_char * data) {

	static u_char buf[SG_LEN + MAX_PACKET];

	/* copy SCSI cmd parameters into buffer */
	buf[SG_LEN+0]=0x3b;
	buf[SG_LEN+1]=(lun<<5) + mode;
	buf[SG_LEN+2]=buffer_id;
	buf[SG_LEN+3]=bufoff1;
	buf[SG_LEN+4]=bufoff2;
	buf[SG_LEN+5]=bufoff3;
	buf[SG_LEN+6]=paraml1;
	buf[SG_LEN+7]=paraml2;
	buf[SG_LEN+8]=paraml3;
	buf[SG_LEN+9]=control;
	/* copy to-SCSI-device data into buffer */
	if (datalen && data) {
		memcpy(buf+SG_LEN+10,data,datalen);
	}

	/* off it goes! */
	return (waitfor_scsi_cmd(fd,10,datalen,buf,0,NULL)==0) ? OKAY : FAILED;
}

int prog_mode(int fd) {
	return write_buffer(fd, 1, /* timeout */
				0,5,0, /* lun, mode, buff id */
	                        0,0,0,  /* buffer offset */
				0,0,0,  /* param list */
				0x80,   /* control */
				0, NULL); /* data */
}

int restart_fas(int fd) {
	return write_buffer(fd, 1, /* timeout */
				0,5,4, /* lun, mode, buff id */
	                        0,0,0,  /* buffer offset */
				0,0,0,  /* param list */
				0x80,   /* control */
				0, NULL); /* data */
}

int select_unit(int fd, u_char uflags, int len) {
	return write_buffer(fd, 1, /* timeout */
				0,5,1, /* lun, mode, buff id */
	                        uflags,0,0,  /* buffer offset */
	                        (u_char)(len >> 16),
				(u_char)(len >> 8),
				(u_char)(len),  /* param list*/
				0x80,   /* control */
				0, NULL); /* data */
}

int send_chunk(int fd, int offset, int len, u_char * data) {
	return write_buffer(fd, 180, /* timeout */
				0,5,2, /* lun, mode, buff id */
	                        (u_char)(offset >> 16),
				(u_char)(offset >> 8),
				(u_char)(offset),  /* buffer offset */
	                        (u_char)(len >> 16),
				(u_char)(len >> 8),
				(u_char)(len),  /* param list*/
				0x80,   /* control */
				len, data+offset); /* data */
}

/* returns 3 states:
	OKAY    = success
	FAILED  = no operation
	BADSTATE= interrupted
*/
#define MIN(x,y)	((x)<(y) ? (x) : (y))
int program_unit(int fd, int imagelen, u_char * image) {
	int remaining, send, offset;

	int chunks, sent;

	if (!select_unit(fd,0,imagelen)) {
		fprintf(stderr,"FAILED: selecting unit\n");
		return FAILED;	
	}

	remaining=imagelen;
	offset=0;
	
	chunks=imagelen/MAX_PUSH;
	sent=1;

	while (remaining>0) {
		if (sent==chunks) {
			/* sending last block */
			printf("\tCommitting firmware ...\n");
		} else {
			switch (sent) {
			case 1:
				/* first block starts erase */
				printf("\tErasing firmware ...\n");
				break;
			case 2:
				/* start counting up the blocks */
				printf("\tUploading firmware chunks ...\n");
			default:
				printf("\t\t%d/%d\r",sent,chunks);
				break;
			}
		}
		fflush(NULL);

		send=MIN(remaining,MAX_PUSH);
		if (send_chunk(fd,offset,send,image)!=OKAY) {
			fprintf(stderr,"\nFAILED: firmware upload\n");
			return BADSTATE;
		}
		remaining-=send;
		offset+=send;
		sent++;
	}

	return OKAY;
}

/*
   takes a string to a product name, returns a string for a filename
   guess for the firmware.prm file
 */
char * build_firmware_filename(char * product_id) {
	static char buf[64];
	int i;
	char * ptr=product_id;
	char * slash;

	if (!product_id) return NULL;

	/*
	   If there is a slash in the product name, skip
	   past the slash
	 */
	slash=rindex(ptr,'/');
	if (slash) { ptr=slash+1; }

	/* copy all characters except '-', as lower case */
	i=0;
	while (*ptr && i<60 /* && *ptr!=' ' */) {
		if (*ptr == '-') {
			ptr++;
			continue;
		}
		buf[i++]=tolower(*ptr);
		ptr++;
	}

	/* add ".prm" to the end */
	buf[i++]='.';
	buf[i++]='p';
	buf[i++]='r';
	buf[i++]='m';
	buf[i++]='\0';

	return buf;
}

/*
   Copies a string until white space or out of chars
 */
void strnwcpy(char *dest, char *src, int n) {
	while(n-- && *src && *src != ' ')
		*dest++ = *src++;
	*dest=0;
}

/* 
   Takes length and image to loaded firmware
   Returns unit ID info from firmware file
 */
unit_id *verify_image(firmware_img * firmware) {
	char buf[128];
	char * ptr;
	int i;
	u_char last, sum=0;

	if (!firmware) {
		fprintf(stderr,"Verifying NULL firmware image?!\n");
		return NULL;
	}
	if (!(ptr=firmware->image)) {
		fprintf(stderr,"Firmware has NULL image?!\n");
		return NULL;
	}
	if (firmware->length < 128) {
		fprintf(stderr,"Firmware image less than 128 bytes?!\n");
		return NULL;
	}

	/* first, let's run a checksum on the image ... */
	/* This is an 8-bit checksum.  The last byte should make sure
	   that the chksum results in a zero. */
	for (i=0;i<firmware->length;i++) {
		last=firmware->image[i];
		sum+=last;
	}
	if (sum!=0) {
		fprintf(stderr,"Image fails checksum!  Got 0x%02x, "
			"expected 0x00 (last byte was 0x%02x)\n",sum,last);
		return NULL;
	}

	/*
	   rebuild the image product info area in the case of
	    double images from dual prom
	 */
	if ( ptr[0] == ptr[1] &&
	     ptr[2] == ptr[3] &&
	     ptr[4] == ptr[5]    ) {
		for (i=0; i < 128; i++)
			buf[i]=ptr[i*2];
		ptr=buf;
	}

	/* find firmware version */
	for (i=0; i<125; i++) {
		if (ptr[i] == 'V' && isdigit(ptr[i+1]) && ptr[i+2]=='.')
			break;
	}
	if (i==125) return NULL; /* failed to find a firmware image */

	/* copy the product name */
	strnwcpy(firmware->unit.name,ptr,NAMLEN);
	firmware->unit.name[NAMLEN]='\0';

	/* yank firmware version text */
	firmware->unit.fw_ver[0]=ptr[i++];
	firmware->unit.fw_ver[1]=ptr[i++];
	firmware->unit.fw_ver[2]=ptr[i++];
	firmware->unit.fw_ver[3]=ptr[i++];
	firmware->unit.fw_ver[4]='\0';

	return &firmware->unit;
}

/* takes a filename, returns a ptr to the firmware image */
firmware_img * load_firmware(char * filename) {
	struct stat stats;
	firmware_img * firmware;

	if (!(firmware=(firmware_img*)malloc(sizeof(firmware_img)))) {
		perror("load_firmware malloc");
		return NULL;
	}
	/* clear the name space */
	firmware->unit.name[0]='\0';
	firmware->unit.fw_ver[0]='\0';

	/* open the file */
	if ((firmware->fd=open(filename,O_RDONLY))<0) {
		fprintf(stderr,"open('%s'): %s\n",filename,strerror(errno));
		free(firmware);
		return NULL;
	}
		
	/* find out file length.  could also lseek */
	if (fstat(firmware->fd,&stats)) {
		fprintf(stderr,"fstat('%s'): %s\n",filename,strerror(errno));
		free(firmware);
		return NULL;
	}

	/* mmap the memory */
	firmware->length=stats.st_size;
	if ((int)(firmware->image=(u_char*)mmap(NULL,firmware->length,
					PROT_READ,MAP_PRIVATE,
					firmware->fd,0))==-1) {
		fprintf(stderr,"mmap('%s'): %s\n",filename,strerror(errno));
		free(firmware);
		return NULL;
	}

	return firmware;
}

void unload_firmware(firmware_img * firmware) {
	if (!firmware) {
		fprintf(stderr,"unload_firmware got NULL pointer?!\n");
		return;
	}
	if (munmap(firmware->image,firmware->length)) {
		perror("munmap");
	}
	if (close(firmware->fd)) {
		perror("close");
	}

	/* free the memory */
	free(firmware);
}

/* this function is ONLY to be run by "upload_now" */
int run_upload(scsi_info *dev, firmware_img *firmware) {
	int rc, i;

	sleep(1); /* because that's what cdupdate does... */

	/* make sure our device is ready again */
	for (i=0;i<4;i++) {
		if (scsi_unitready(dev)) break;
	}
	if (i==4) {
		printf("FAILURE: unit did not become ready\n");
		/* didn't recover from setting programming mode */
		return FAILED;
	}

	if ((rc=program_unit(dev->fd,firmware->length,firmware->image))!=OKAY) {
		printf("FAILURE: unit did not take firmware\n");
		return rc;
	}
	return OKAY;
}


/* This function assumes everthing has passed verification with
   "choose_device" and "choose_firmware".   DO NOT call this without
   image checking! */
int upload_now(scsi_info *dev, firmware_img *firmware) {
	int rc,i;

	if (!dev) {
		fprintf(stderr,"AGH: upload_now had a NULL dev?!\n");
		return FAILED;
	}

	if (!firmware) {
		fprintf(stderr,"AGH: upload_now had a NULL firmware?!\n");
		return FAILED;
	}

	printf("\tPreparing for update...\n");

	/* make sure our device is ready */
	for (i=0;i<4;i++) {
		if (scsi_unitready(dev)) break;
	}
	if (i==4) {
		printf("FAILURE: unit did not become ready\n");
		return FAILED; /* unit never came ready */
	}

	/* kick it into programming mode */
	if (prog_mode(dev->fd)!=OKAY) {
		printf("FAILURE: could not enter programming mode\n");
		return FAILED; /* unit didn't go into programming mode */
	}

	if ((rc=run_upload(dev,firmware))!=BADSTATE) {
		if (rc!=OKAY) printf("FAILURE: run_upload croaked\n");
		/* go back to fas mode */
		printf("\tFinishing update ...\n");
		restart_fas(dev->fd);
		sleep(2);
		/* are we back? */
		for (i=0;i<4;i++) {
			if (scsi_unitready(dev)) break;
		}
		if (i==4) {
			printf("FAILURE: unit never came ready\n");
			/* didn't recover.  on a success, that's a failure,
			   on an upload failure, that's real bad */
			return (rc==FAILED ? BADSTATE : FAILED);
		}
	}
	return rc;
}
