/*
 * Apple iSight (the one built in the screen of Macbooks) firmware loader
 * Copyright (C) 2006 Ronald S. Bultje <rbultje@ronald.bitfreak.net>
 *
 * Special thanks to Johannes Berg <johannes@sipsolutions.net> for helping
 * to find out how to load the firmware; see his website on
 * http://johannes.sipsolutions.net/MacBook/iSight for details.
 * Basic structure of firmware data: <len:2><off:2><data:len>, where
 * the $data of size $len is to be put at position $off in the device.
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA
 */

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

#include <usb.h>
#define TIMEOUT 300

static int
read_fw (struct usb_dev_handle *dev, char *filename, long off)
{
	int fd, len, req, llen, res, ret = -1;
	unsigned char data[4], rdata[1024], *ptr;

	if ((fd = open (filename, O_RDONLY)) == -1) {
		perror ("Opening file");
		return -1;
	} else if (lseek (fd, off, SEEK_SET) != off) {
		perror ("Seeking");
		close (fd);
		return -1;
	}

	if ((res = usb_control_msg (dev, 0x40, 0xA0, 0xe600, 0,
				    "\1", 1, TIMEOUT)) != 1) {
		perror ("Firmware load init failed");
		close (fd);
		return -1;
	}
	while (1) {
		if ((len = read (fd, data, 4)) != 4) {
			if (len == 0) {
				fprintf (stderr,
					 "Unexpected eos - corrupt driver?\n");
				goto end;
			} else {
				perror("Reading firmware header chunk failed");
				goto end;
			}
		}
		len = (data[0] << 8) | data[1];
		req = (data[2] << 8) | data[3];
		if (len == 0x8001)
			break; /* success */
		else if (len == 0)
			continue;
		else if (len < 0 || len >= 1024) {
			fprintf (stderr,
				 "Invalid firmware data_length %d, load aborted\n",
				 len);
			goto end;
		} else if (read (fd, rdata, len) != len) {
			perror ("Error reading firmware data");
			goto end;
		}

		/* upload to usb bus */
		for (ptr = rdata; len > 0; req += 50, ptr += 50) {
			llen = len > 50 ? 50 : len;
			len -= llen;

			if ((res = usb_control_msg (dev, 0x40, 0xA0, req, 0,
						    (char *) ptr, llen,
						    TIMEOUT)) != llen) {
				fprintf (stderr,
					 "firmware load req=0x%x failed: %s\n",
					 req, strerror (errno));
				goto end;
			}
		}
	}

	ret = 0;
end:
	if ((res = usb_control_msg (dev, 0x40, 0xA0, 0xe600, 0,
				    "\0", 1, TIMEOUT)) != 1) {
		perror ("Firmware finish-up failed");
		ret = -1;
	}

	close (fd);

	return ret;
}

static int
probe_dev (struct usb_device *dev, char *fw_filename, long off)
{
	int n, total = 0, res;

	if (dev->descriptor.idVendor == 0x05ac &&
	    dev->descriptor.idProduct == 0x8300 &&
	    dev->descriptor.bDeviceClass == 0xff &&
	    dev->descriptor.bDeviceSubClass == 0xff &&
	    dev->descriptor.bDeviceProtocol == 0xff) {
		usb_dev_handle *h;

		/* load firmware */
		if (!(h = usb_open (dev))) {
			perror ("Opening iSight");
			return -1;
		}
		printf ("Loading firmware for iSight...\n");
		if (read_fw (h, fw_filename, off) == 0) {
			printf ("done\n");
			total++;
		}
		usb_close (h);
	} else if (dev->descriptor.idVendor == 0x05ac &&
		   dev->descriptor.idProduct == 0x8501) {
		printf ("Apple iSight with firmware already loaded found\n");
		total++;
	}

	for (n = 0; n < dev->num_children; n++) {
		res = probe_dev (dev->children[n], fw_filename, off);
		if (res != -1)
			total += res;
	}

	return total;
}

int
main (int argc, char *argv[])
{
	int n, found = 0, res, err = 0;
	char command[1024];
	struct usb_bus *bus;
	struct usb_device *dev;
	static const struct {
		char *sha1sum;
		long off;
	} offsets[] = {
		{ "86430c04f9b67c5c3d84409138a7679827025ec2", 5172 /* 0x1434 */ },
		{ "a14c159b176d27a6e98dcb5dea5d78b81e15ad41", 9176 /* 0x23D8 */ },
		{ "c6c94dd77b864f8bd231abf3cb2de4c9d139e1bf", 0x1434 },
		{ "01e291d529e7c18deea2eba252d18114e096276e", 0x2060 },
		{ NULL }
	}, *offset;

	if (argc != 2) {
		fprintf(stderr, "Usage: %s <firmware file>\n", argv[0]);
		fprintf(stderr, "Firmware can usually be found on your Mac "
			"partition in /System/Library/Extensions/"
			"IOUSBFamily.kext/Contents/PlugIns/"
			"AppleUSBVideoSupport.kext/Contents/MacOS/"
			"AppleUSBVideoSupport\n");
		return -1;
	}
	
	/* check sha1sum on firmware, to prevent loading crap into the
	 * iSight and thus possibly damaging it. */
	for (n = 0; offsets[n].sha1sum != NULL; n++) {
		snprintf (command, sizeof (command) - 1,
			  "test \"x`sha1sum %s`\" == \"x%s  %s\" &> /dev/null",
			  argv[1], offsets[n].sha1sum, argv[1]);
		if (system (command) == 0)
			break;
	}
	if (offsets[n].sha1sum == NULL) {
		fprintf (stderr, "Sha1sum check on firmware file failed\n");
		return -1;
	}
	offset = &offsets[n];

	/* init usb */
	usb_init ();
	if (usb_find_busses () == 0) {
		fprintf (stderr, "No USB busses found\n");
		return -1;
	} else if (usb_find_devices () == 0) {
		fprintf (stderr, "No USB devices found\n");
		return -1;
	}

	/* find iSight */
	for (bus = usb_busses; bus != NULL; bus = bus->next) {
		if (bus->devices != NULL) {
			for (dev = bus->devices; dev != NULL;
			     dev = dev->next) {
				res += probe_dev (dev, argv[1], offset->off);
				if (res == -1)
					err++;
				else
					found += res;
			}
		}
	}
	if (found == 0 && err == 0) {
		fprintf (stderr, "No Apple iSight found!\n");
		return -1;
	}

	return 0;
}
