/*
 * Copyright 2017 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include <asm/byteorder.h>
#include <endian.h>
#include <fcntl.h>
#include <getopt.h>
#include <libusb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#include <fmap.h>

#ifndef __packed
#define __packed __attribute__((packed))
#endif

#include "compile_time_macros.h"
#include "misc_util.h"
#include "usb_descriptor.h"
#include "update_fw.h"
#include "vb21_struct.h"

#ifdef DEBUG
#define debug printf
#else
#define debug(fmt, args...)
#endif

/*
 * This file contains the source code of a Linux application used to update
 * EC device firmware (common code only, gsctool takes care of cr50).
 */

#define VID USB_VID_GOOGLE
#define PID 0x5022
#define SUBCLASS USB_SUBCLASS_GOOGLE_UPDATE
#define PROTOCOL USB_PROTOCOL_GOOGLE_UPDATE

enum exit_values {
	noop = 0,	  /* All up to date, no update needed. */
	all_updated = 1,  /* Update completed, reboot required. */
	rw_updated  = 2,  /* RO was not updated, reboot required. */
	update_error = 3  /* Something went wrong. */
};

struct usb_endpoint {
	struct libusb_device_handle *devh;
	uint8_t ep_num;
	int     chunk_len;
};

struct transfer_descriptor {
	/*
	 * offsets of section available for update (not currently active).
	 */
	uint32_t offset;

	struct usb_endpoint uep;
};

/* Information about the target */
static struct first_response_pdu targ;

static uint16_t protocol_version;
static uint16_t header_type;
static char *progname;
static char *short_opts = "bd:efg:hjlnp:rsS:tuw";
static const struct option long_opts[] = {
	/* name    hasarg *flag val */
	{"binvers",	1,   NULL, 'b'},
	{"device",	1,   NULL, 'd'},
	{"entropy",	0,   NULL, 'e'},
	{"fwver",	0,   NULL, 'f'},
	{"tp_debug",	1,   NULL, 'g'},
	{"help",	0,   NULL, 'h'},
	{"jump_to_rw",	0,   NULL, 'j'},
	{"follow_log",	0,   NULL, 'l'},
	{"no_reset",	0,   NULL, 'n'},
	{"tp_update",	1,   NULL, 'p'},
	{"reboot",	0,   NULL, 'r'},
	{"stay_in_ro",	0,   NULL, 's'},
	{"serial",	1,   NULL, 'S'},
	{"tp_info",	0,   NULL, 't'},
	{"unlock_rollback",	0,   NULL, 'u'},
	{"unlock_rw",	0,   NULL, 'w'},
	{},
};

/* Release USB device and return error to the OS. */
static void shut_down(struct usb_endpoint *uep)
{
	libusb_close(uep->devh);
	libusb_exit(NULL);
	exit(update_error);
}

static void usage(int errs)
{
	printf("\nUsage: %s [options] <binary image>\n"
	       "\n"
	       "This updates EC firmware over USB (common code EC, no cr50).\n"
	       "The required argument is the full RO+RW image.\n"
	       "\n"
	       "Options:\n"
	       "\n"
	       "  -b,--binvers             Report versions of image's "
				"RW and RO, do not update\n"
	       "  -d,--device  VID:PID     USB device (default %04x:%04x)\n"
	       "  -e,--entropy             Add entropy to device secret\n"
	       "  -f,--fwver               Report running firmware versions.\n"
	       "  -g,--tp_debug <hex data> Touchpad debug command\n"
	       "  -h,--help                Show this message\n"
	       "  -j,--jump_to_rw          Tell EC to jump to RW\n"
	       "  -l,--follow_log          Get console log\n"
	       "  -p,--tp_update file      Update touchpad FW\n"
	       "  -r,--reboot              Tell EC to reboot\n"
	       "  -s,--stay_in_ro          Tell EC to stay in RO\n"
	       "  -S,--serial              Device serial number\n"
	       "  -t,--tp_info             Get touchpad information\n"
	       "  -u,--unlock_rollback     Tell EC to unlock the rollback region\n"
	       "  -w,--unlock_rw           Tell EC to unlock the RW region\n"
	       "\n", progname, VID, PID);

	exit(errs ? update_error : noop);
}

static void str2hex(const char *str, uint8_t *data, int *len)
{
	int i;
	int slen = strlen(str);

	if (slen/2 > *len) {
		fprintf(stderr, "Hex string too long.\n");
		exit(update_error);
	}

	if (slen % 2 != 0) {
		fprintf(stderr, "Hex string length not a multiple of 2.\n");
		exit(update_error);
	}

	for (i = 0, *len = 0; i < slen; i += 2, (*len)++) {
		char *end;
		char tmp[3];

		tmp[0] = str[i];
		tmp[1] = str[i+1];
		tmp[2] = 0;

		data[*len] = strtol(tmp, &end, 16);

		if (*end != 0) {
			fprintf(stderr, "Invalid hex string.\n");
			exit(update_error);
		}
	}
}

static void hexdump(const uint8_t *data, int len)
{
	int i;

	for (i = 0; i < len; i++) {
		printf("%02x", data[i]);
		if ((i % 16) == 15)
			printf("\n");
	}

	if ((len % 16) != 0)
		printf("\n");
}

static void dump_touchpad_info(const uint8_t *data, int len)
{
	const struct touchpad_info *info = (const struct touchpad_info *)data;

	if (len != sizeof(struct touchpad_info)) {
		fprintf(stderr, "Hex string length is not %zu",
			sizeof(struct touchpad_info));
		hexdump(data, len);
		return;
	}

	printf("\n");
	printf("status:         0x%02x\n", info->status);
	printf("vendor:         0x%04x\n", info->vendor);
	printf("fw_address:     0x%08x\n", info->fw_address);
	printf("fw_size:        0x%08x\n", info->fw_size);

	printf("allowed_fw_hash:\n");
	hexdump(info->allowed_fw_hash, sizeof(info->allowed_fw_hash));

	switch (info->vendor) {
	case 0x04f3: /* ELAN */
	case 0x0483: /* ST */
		printf("id:             0x%04x\n", info->elan.id);
		printf("fw_version:     0x%04x\n", info->elan.fw_version);
		printf("fw_fw_checksum: 0x%04x\n", info->elan.fw_checksum);
		break;
	default:
		fprintf(stderr, "Unknown vendor, vendor specific data:\n");
		hexdump((const uint8_t *)&info->elan, sizeof(info->elan));
		break;
	}
}

/* Read file into buffer */
static uint8_t *get_file_or_die(const char *filename, size_t *len_ptr)
{
	FILE *fp;
	struct stat st;
	uint8_t *data;
	size_t len;

	fp = fopen(filename, "rb");
	if (!fp) {
		perror(filename);
		exit(update_error);
	}
	if (fstat(fileno(fp), &st)) {
		perror("stat");
		exit(update_error);
	}

	len = st.st_size;

	data = malloc(len);
	if (!data) {
		perror("malloc");
		exit(update_error);
	}

	if (fread(data, st.st_size, 1, fp) != 1) {
		perror("fread");
		exit(update_error);
	}

	fclose(fp);

	*len_ptr = len;
	return data;
}

#define USB_ERROR(m, r) \
	fprintf(stderr, "%s:%d, %s returned %d (%s)\n", __FILE__, __LINE__, \
		m, r, libusb_strerror(r))

/*
 * Actual USB transfer function, the 'allow_less' flag indicates that the
 * valid response could be shortef than allotted memory, the 'rxed_count'
 * pointer, if provided along with 'allow_less' lets the caller know how mavy
 * bytes were received.
 */
static void do_xfer(struct usb_endpoint *uep, void *outbuf, int outlen,
		    void *inbuf, int inlen, int allow_less,
		    size_t *rxed_count)
{

	int r, actual;

	/* Send data out */
	if (outbuf && outlen) {
		actual = 0;
		r = libusb_bulk_transfer(uep->devh, uep->ep_num,
					 outbuf, outlen,
					 &actual, 2000);
		if (r < 0) {
			USB_ERROR("libusb_bulk_transfer", r);
			exit(update_error);
		}
		if (actual != outlen) {
			fprintf(stderr, "%s:%d, only sent %d/%d bytes\n",
				__FILE__, __LINE__, actual, outlen);
			shut_down(uep);
		}
	}

	/* Read reply back */
	if (inbuf && inlen) {

		actual = 0;
		r = libusb_bulk_transfer(uep->devh, uep->ep_num | 0x80,
					 inbuf, inlen,
					 &actual, 5000);
		if (r < 0) {
			USB_ERROR("libusb_bulk_transfer", r);
			exit(update_error);
		}
		if ((actual != inlen) && !allow_less) {
			fprintf(stderr, "%s:%d, only received %d/%d bytes\n",
				__FILE__, __LINE__, actual, inlen);
			hexdump(inbuf, actual);
			shut_down(uep);
		}

		if (rxed_count)
			*rxed_count = actual;
	}
}

static void xfer(struct usb_endpoint *uep, void *outbuf,
		 size_t outlen, void *inbuf, size_t inlen, int allow_less)
{
	do_xfer(uep, outbuf, outlen, inbuf, inlen, allow_less, NULL);
}

/* Return 0 on error, since it's never gonna be EP 0 */
static int find_endpoint(const struct libusb_interface_descriptor *iface,
			 struct usb_endpoint *uep)
{
	const struct libusb_endpoint_descriptor *ep;

	if (iface->bInterfaceClass == 255 &&
	    iface->bInterfaceSubClass == SUBCLASS &&
	    iface->bInterfaceProtocol == PROTOCOL &&
	    iface->bNumEndpoints) {
		ep = &iface->endpoint[0];
		uep->ep_num = ep->bEndpointAddress & 0x7f;
		uep->chunk_len = ep->wMaxPacketSize;
		return 1;
	}

	return 0;
}

/* Return -1 on error */
static int find_interface(struct usb_endpoint *uep)
{
	int iface_num = -1;
	int r, i, j;
	struct libusb_device *dev;
	struct libusb_config_descriptor *conf = 0;
	const struct libusb_interface *iface0;
	const struct libusb_interface_descriptor *iface;

	dev = libusb_get_device(uep->devh);
	r = libusb_get_active_config_descriptor(dev, &conf);
	if (r < 0) {
		USB_ERROR("libusb_get_active_config_descriptor", r);
		goto out;
	}

	for (i = 0; i < conf->bNumInterfaces; i++) {
		iface0 = &conf->interface[i];
		for (j = 0; j < iface0->num_altsetting; j++) {
			iface = &iface0->altsetting[j];
			if (find_endpoint(iface, uep)) {
				iface_num = i;
				goto out;
			}
		}
	}

out:
	libusb_free_config_descriptor(conf);
	return iface_num;
}

/* Returns true if parsed. */
static int parse_vidpid(const char *input, uint16_t *vid_ptr, uint16_t *pid_ptr)
{
	char *copy, *s, *e = 0;

	copy = strdup(input);

	s = strchr(copy, ':');
	if (!s)
		return 0;
	*s++ = '\0';

	*vid_ptr = (uint16_t) strtoul(copy, &e, 16);
	if (!*optarg || (e && *e))
		return 0;

	*pid_ptr = (uint16_t) strtoul(s, &e, 16);
	if (!*optarg || (e && *e))
		return 0;

	return 1;
}

static libusb_device_handle *check_device(libusb_device *dev,
	uint16_t vid, uint16_t pid, char *serialno)
{
	struct libusb_device_descriptor desc;
	libusb_device_handle *handle = NULL;
	char sn[256];
	int ret;
	int match = 1;
	int snvalid = 0;

	ret = libusb_get_device_descriptor(dev, &desc);
	if (ret < 0)
		return NULL;

	ret = libusb_open(dev, &handle);

	if (ret != LIBUSB_SUCCESS)
		return NULL;

	if (desc.iSerialNumber) {
		ret = libusb_get_string_descriptor_ascii(handle,
			desc.iSerialNumber, (unsigned char *)sn, sizeof(sn));
		if (ret > 0)
			snvalid = 1;
	}

	if (vid != 0 && vid != desc.idVendor)
		match = 0;
	if (pid != 0 && pid != desc.idProduct)
		match = 0;
	if (serialno != NULL && (!snvalid || strstr(sn, serialno) == NULL))
		match = 0;

	if (match)
		return handle;

	libusb_close(handle);
	return NULL;
}

static void usb_findit(uint16_t vid, uint16_t pid,
		char *serialno, struct usb_endpoint *uep)
{
	int iface_num, r, i;
	libusb_device **devs;
	libusb_device_handle *devh = NULL;
	ssize_t count;

	memset(uep, 0, sizeof(*uep));

	r = libusb_init(NULL);
	if (r < 0) {
		USB_ERROR("libusb_init", r);
		exit(update_error);
	}

	count = libusb_get_device_list(NULL, &devs);
	if (count < 0)
		return;

	for (i = 0; devs[i]; i++) {
		devh = check_device(devs[i], vid, pid, serialno);
		if (devh) {
			printf("Found device.\n");
			break;
		}
	}

	libusb_free_device_list(devs, 1);

	if (!devh) {
		fprintf(stderr, "Can't find device\n");
		exit(update_error);
	}

	uep->devh = devh;

	iface_num = find_interface(uep);
	if (iface_num < 0) {
		fprintf(stderr, "USB FW update not supported by that device\n");
		shut_down(uep);
	}
	if (!uep->chunk_len) {
		fprintf(stderr, "wMaxPacketSize isn't valid\n");
		shut_down(uep);
	}

	printf("found interface %d endpoint %d, chunk_len %d\n",
	       iface_num, uep->ep_num, uep->chunk_len);

	libusb_set_auto_detach_kernel_driver(uep->devh, 1);
	r = libusb_claim_interface(uep->devh, iface_num);
	if (r < 0) {
		USB_ERROR("libusb_claim_interface", r);
		shut_down(uep);
	}

	printf("READY\n-------\n");
}

static int transfer_block(struct usb_endpoint *uep,
			  struct update_frame_header *ufh,
			  uint8_t *transfer_data_ptr, size_t payload_size)
{
	size_t transfer_size;
	uint32_t reply;
	int actual;
	int r;

	/* First send the header. */
	xfer(uep, ufh, sizeof(*ufh), NULL, 0, 0);

	/* Now send the block, chunk by chunk. */
	for (transfer_size = 0; transfer_size < payload_size;) {
		int chunk_size;

		chunk_size = MIN(uep->chunk_len, payload_size - transfer_size);
		xfer(uep, transfer_data_ptr, chunk_size, NULL, 0, 0);
		transfer_data_ptr += chunk_size;
		transfer_size += chunk_size;
	}

	/* Now get the reply. */
	r = libusb_bulk_transfer(uep->devh, uep->ep_num | 0x80,
				 (void *) &reply, sizeof(reply),
				 &actual, 5000);
	if (r) {
		if (r == -7) {
			fprintf(stderr, "Timeout!\n");
			return r;
		}
		USB_ERROR("libusb_bulk_transfer", r);
		shut_down(uep);
	}

	reply = *((uint8_t *)&reply);
	if (reply) {
		fprintf(stderr, "Error: status %#x\n", reply);
		exit(update_error);
	}

	return 0;
}

/**
 * Transfer an image section (typically RW or RO).
 *
 * td           - transfer descriptor to use to communicate with the target
 * data_ptr     - pointer at the section base in the image
 * section_addr - address of the section in the target memory space
 * data_len     - section size
 * smart_update - non-zero to enable the smart trailing of 0xff.
 */
static void transfer_section(struct transfer_descriptor *td,
			     uint8_t *data_ptr,
			     uint32_t section_addr,
			     size_t data_len,
			     uint8_t smart_update)
{
	/*
	 * Actually, we can skip trailing chunks of 0xff, as the entire
	 * section space must be erased before the update is attempted.
	 *
	 * FIXME: We can be smarter than this and skip blocks within the image.
	 */
	if (smart_update)
		while (data_len && (data_ptr[data_len - 1] == 0xff))
			data_len--;

	printf("sending 0x%zx bytes to %#x\n", data_len, section_addr);
	while (data_len) {
		size_t payload_size;
		uint32_t block_base;
		int max_retries;

		/* prepare the header to prepend to the block. */
		payload_size = MIN(data_len, targ.common.maximum_pdu_size);

		block_base = htobe32(section_addr);

		struct update_frame_header ufh;

		ufh.block_size = htobe32(payload_size +
					sizeof(struct update_frame_header));
		ufh.cmd.block_base = block_base;
		ufh.cmd.block_digest = 0;
		for (max_retries = 10; max_retries; max_retries--)
			if (!transfer_block(&td->uep, &ufh,
						data_ptr, payload_size))
				break;

		if (!max_retries) {
			fprintf(stderr,
				"Failed to transfer block, %zd to go\n",
				data_len);
			exit(update_error);
		}
		data_len -= payload_size;
		data_ptr += payload_size;
		section_addr += payload_size;
	}
}

/*
 * Each RO or RW section of the new image can be in one of the following
 * states.
 */
enum upgrade_status {
	not_needed = 0,  /* Version below or equal that on the target. */
	not_possible,    /*
			  * RO is newer, but can't be transferred due to
			  * target RW shortcomings.
			  */
	needed            /*
			   * This section needs to be transferred to the
			   * target.
			   */
};

/* This array describes all sections of the new image. */
static struct {
	const char *name;
	uint32_t    offset;
	uint32_t    size;
	enum upgrade_status  ustatus;
	char version[32];
	int32_t rollback;
	uint32_t key_version;
} sections[] = {
	{"RO"},
	{"RW"}
};

static const struct fmap_area *fmap_find_area_or_die(const struct fmap *fmap,
						     const char *name)
{
	const struct fmap_area *fmaparea;

	fmaparea = fmap_find_area(fmap, name);
	if (!fmaparea) {
		fprintf(stderr, "Cannot find FMAP area %s\n", name);
		exit(update_error);
	}

	return fmaparea;
}

/*
 * Scan the new image and retrieve versions of all sections.
 */
static void fetch_header_versions(const uint8_t *image, size_t len)
{
	const struct fmap *fmap;
	const struct fmap_area *fmaparea;
	long int offset;
	size_t i;

	offset = fmap_find(image, len);
	if (offset < 0) {
		fprintf(stderr, "Cannot find FMAP in image\n");
		exit(update_error);
	}
	fmap = (const struct fmap *)(image+offset);

	/* FIXME: validate fmap struct more than this? */
	if (fmap->size != len) {
		fprintf(stderr, "Mismatch between FMAP size and image size\n");
		exit(update_error);
	}

	for (i = 0; i < ARRAY_SIZE(sections); i++) {
		const char *fmap_name;
		const char *fmap_fwid_name;
		const char *fmap_rollback_name = NULL;
		const char *fmap_key_name = NULL;

		if (!strcmp(sections[i].name, "RO")) {
			fmap_name = "EC_RO";
			fmap_fwid_name = "RO_FRID";
		} else if (!strcmp(sections[i].name, "RW")) {
			fmap_name = "EC_RW";
			fmap_fwid_name = "RW_FWID";
			fmap_rollback_name = "RW_RBVER";
			/*
			 * Key version comes from key RO (RW signature does not
			 * contain the key version.
			 */
			fmap_key_name = "KEY_RO";
		} else {
			fprintf(stderr, "Invalid section name\n");
			exit(update_error);
		}

		fmaparea = fmap_find_area_or_die(fmap, fmap_name);

		/* FIXME: endianness? */
		sections[i].offset = fmaparea->offset;
		sections[i].size = fmaparea->size;

		fmaparea = fmap_find_area_or_die(fmap, fmap_fwid_name);

		if (fmaparea->size != sizeof(sections[i].version)) {
			fprintf(stderr, "Invalid fwid size\n");
			exit(update_error);
		}
		memcpy(sections[i].version, image+fmaparea->offset,
			fmaparea->size);

		sections[i].rollback = -1;
		if (fmap_rollback_name) {
			fmaparea = fmap_find_area(fmap, fmap_rollback_name);
			if (fmaparea)
				memcpy(&sections[i].rollback,
				       image+fmaparea->offset,
				       sizeof(sections[i].rollback));
		}

		sections[i].key_version = -1;
		if (fmap_key_name) {
			fmaparea = fmap_find_area(fmap, fmap_key_name);
			if (fmaparea) {
				const struct vb21_packed_key *key =
					(const void *)(image+fmaparea->offset);
				sections[i].key_version = key->key_version;
			}
		}
	}
}

static int show_headers_versions(const void *image)
{
	size_t i;

	for (i = 0; i < ARRAY_SIZE(sections); i++) {
		printf("%s off=%08x/%08x v=%.32s rb=%d kv=%d\n",
			sections[i].name, sections[i].offset, sections[i].size,
			sections[i].version, sections[i].rollback,
			sections[i].key_version);
	}
	return 0;
}

/*
 * Pick sections to transfer based on information retrieved from the target,
 * the new image, and the protocol version the target is running.
 */
static void pick_sections(struct transfer_descriptor *td)
{
	size_t i;

	for (i = 0; i < ARRAY_SIZE(sections); i++) {
		uint32_t offset = sections[i].offset;

		/* Skip currently active section. */
		if (offset != td->offset)
			continue;

		sections[i].ustatus = needed;
	}
}

static void setup_connection(struct transfer_descriptor *td)
{
	size_t rxed_size;
	size_t i;
	uint32_t error_code;

	/*
	 * Need to be backwards compatible, communicate with targets running
	 * different protocol versions.
	 */
	union {
		struct first_response_pdu rpdu;
		uint32_t legacy_resp;
	} start_resp;

	/* Send start request. */
	printf("start\n");

	struct update_frame_header ufh;
	uint8_t inbuf[td->uep.chunk_len];
	int actual = 0;

	/* Flush all data from endpoint to recover in case of error. */
	while (!libusb_bulk_transfer(td->uep.devh,
					td->uep.ep_num | 0x80,
					(void *)&inbuf, td->uep.chunk_len,
					&actual, 10)) {
		printf("flush\n");
	}

	memset(&ufh, 0, sizeof(ufh));
	ufh.block_size = htobe32(sizeof(ufh));
	do_xfer(&td->uep, &ufh, sizeof(ufh), &start_resp,
		sizeof(start_resp), 1, &rxed_size);

	/* We got something. Check for errors in response */
	if (rxed_size < 8) {
		fprintf(stderr, "Unexpected response size %zd: ", rxed_size);
		for (i = 0; i < rxed_size; i++)
			fprintf(stderr, " %02x", ((uint8_t *)&start_resp)[i]);
		fprintf(stderr, "\n");
		exit(update_error);
	}

	protocol_version = be16toh(start_resp.rpdu.protocol_version);
	if (protocol_version < 5 || protocol_version > 6) {
		fprintf(stderr, "Unsupported protocol version %d\n",
			protocol_version);
		exit(update_error);
	}

	header_type = be16toh(start_resp.rpdu.header_type);

	printf("target running protocol version %d (type %d)\n",
		protocol_version, header_type);
	if (header_type != UPDATE_HEADER_TYPE_COMMON) {
		fprintf(stderr, "Unsupported header type %d\n",
			header_type);
		exit(update_error);
	}

	error_code = be32toh(start_resp.rpdu.return_value);

	if (error_code) {
		fprintf(stderr, "Target reporting error %d\n", error_code);
		shut_down(&td->uep);
		exit(update_error);
	}

	td->offset = be32toh(start_resp.rpdu.common.offset);
	memcpy(targ.common.version, start_resp.rpdu.common.version,
		sizeof(start_resp.rpdu.common.version));
	targ.common.maximum_pdu_size =
		be32toh(start_resp.rpdu.common.maximum_pdu_size);
	targ.common.flash_protection =
		be32toh(start_resp.rpdu.common.flash_protection);
	targ.common.min_rollback = be32toh(start_resp.rpdu.common.min_rollback);
	targ.common.key_version = be32toh(start_resp.rpdu.common.key_version);

	printf("maximum PDU size: %d\n", targ.common.maximum_pdu_size);
	printf("Flash protection status: %04x\n", targ.common.flash_protection);
	printf("version: %32s\n", targ.common.version);
	printf("key_version: %d\n", targ.common.key_version);
	printf("min_rollback: %d\n", targ.common.min_rollback);
	printf("offset: writable at %#x\n", td->offset);

	pick_sections(td);
}

/*
 * Channel TPM extension/vendor command over USB. The payload of the USB frame
 * in this case consists of the 2 byte subcommand code concatenated with the
 * command body. The caller needs to indicate if a response is expected, and
 * if it is - of what maximum size.
 */
static int ext_cmd_over_usb(struct usb_endpoint *uep, uint16_t subcommand,
			    void *cmd_body, size_t body_size,
			    void *resp, size_t *resp_size,
			    int allow_less)
{
	struct update_frame_header *ufh;
	uint16_t *frame_ptr;
	size_t usb_msg_size;

	usb_msg_size = sizeof(struct update_frame_header) +
		sizeof(subcommand) + body_size;

	ufh = malloc(usb_msg_size);
	if (!ufh) {
		printf("%s: failed to allocate %zd bytes\n",
		       __func__, usb_msg_size);
		return -1;
	}

	ufh->block_size = htobe32(usb_msg_size);
	ufh->cmd.block_digest = 0;
	ufh->cmd.block_base = htobe32(UPDATE_EXTRA_CMD);
	frame_ptr = (uint16_t *)(ufh + 1);
	*frame_ptr = htobe16(subcommand);

	if (body_size)
		memcpy(frame_ptr + 1, cmd_body, body_size);

	xfer(uep, ufh, usb_msg_size, resp, resp_size ? *resp_size : 0,
	     allow_less);

	free(ufh);
	return 0;
}

/*
 * Indicate to the target that update image transfer has been completed. Upon
 * receiveing of this message the target state machine transitions into the
 * 'rx_idle' state. The host may send an extension command to reset the target
 * after this.
 */
static void send_done(struct usb_endpoint *uep)
{
	uint32_t out;

	/* Send stop request, ignoring reply. */
	out = htobe32(UPDATE_DONE);
	xfer(uep, &out, sizeof(out), &out, 1, 0);
}

static void send_subcommand(struct transfer_descriptor *td, uint16_t subcommand,
			    void *cmd_body, size_t body_size,
			    uint8_t *response, size_t response_size)
{
	send_done(&td->uep);

	ext_cmd_over_usb(&td->uep, subcommand,
			cmd_body, body_size,
			response, &response_size, 0);
	printf("sent command %x, resp %x\n", subcommand, response[0]);
}

/* Returns number of successfully transmitted image sections. */
static int transfer_image(struct transfer_descriptor *td,
			       uint8_t *data, size_t data_len)
{
	size_t i;
	int num_txed_sections = 0;

	for (i = 0; i < ARRAY_SIZE(sections); i++)
		if (sections[i].ustatus == needed) {
			transfer_section(td,
					 data + sections[i].offset,
					 sections[i].offset,
					 sections[i].size, 1);
			num_txed_sections++;
		}

	/*
	 * Move USB receiver sate machine to idle state so that vendor
	 * commands can be processed later, if any.
	 */
	send_done(&td->uep);

	if (!num_txed_sections)
		printf("nothing to do\n");
	else
		printf("-------\nupdate complete\n");
	return num_txed_sections;
}

static void generate_reset_request(struct transfer_descriptor *td)
{
	size_t response_size;
	uint8_t response;
	uint16_t subcommand;
	uint8_t command_body[2]; /* Max command body size. */
	size_t command_body_size;

	if (protocol_version < 6) {
		/*
		 * Send a second stop request, which should reboot
		 * without replying.
		 */
		send_done(&td->uep);
		/* Nothing we can do over /dev/tpm0 running versions below 6. */
		return;
	}

	/*
	 * If the user explicitly wants it, request post reset instead of
	 * immediate reset. In this case next time the target reboots, the h1
	 * will reboot as well, and will consider running the uploaded code.
	 *
	 * In case target RW version is 19 or above, to reset the target the
	 * host is supposed to send the command to enable the uploaded image
	 * disabled by default.
	 *
	 * Otherwise the immediate reset command would suffice.
	 */
	/* Most common case. */
	command_body_size = 0;
	response_size = 1;
	subcommand = UPDATE_EXTRA_CMD_IMMEDIATE_RESET;
	ext_cmd_over_usb(&td->uep, subcommand,
			command_body, command_body_size,
			&response, &response_size, 0);

	printf("reboot not triggered\n");
}

static void get_random(uint8_t *data, int len)
{
	FILE *fp;
	int i = 0;

	fp = fopen("/dev/random", "rb");
	if (!fp) {
		perror("Can't open /dev/random");
		exit(update_error);
	}

	while (i < len) {
		int ret = fread(data+i, len-i, 1, fp);

		if (ret < 0) {
			perror("fread");
			exit(update_error);
		}

		i += ret;
	}

	fclose(fp);
}

static void read_console(struct transfer_descriptor *td)
{
	uint8_t payload[] = { 0x1 };
	uint8_t response[64];
	size_t response_size = 64;
	struct timespec sleep_duration = {  /* 100 ms */
		.tv_sec = 0,
		.tv_nsec = 100l * 1000l * 1000l,
	};

	send_done(&td->uep);

	printf("\n");
	while (1) {
		response_size = 1;
		ext_cmd_over_usb(&td->uep,
				 UPDATE_EXTRA_CMD_CONSOLE_READ_INIT,
				 NULL, 0,
				 response, &response_size, 0);

		while (1) {
			response_size = 64;
			ext_cmd_over_usb(&td->uep,
					 UPDATE_EXTRA_CMD_CONSOLE_READ_NEXT,
					 payload, sizeof(payload),
					 response, &response_size, 1);
			if (response[0] == 0)
				break;
			/* make sure it's null-terminated. */
			response[response_size - 1] = 0;
			printf("%s", (const char *)response);
		}
		nanosleep(&sleep_duration, NULL);
	}
}

int main(int argc, char *argv[])
{
	struct transfer_descriptor td;
	int errorcnt;
	uint8_t *data = 0;
	size_t data_len = 0;
	uint16_t vid = VID, pid = PID;
	char *serialno = NULL;
	int i;
	size_t j;
	int transferred_sections = 0;
	int binary_vers = 0;
	int show_fw_ver = 0;
	int no_reset_request = 0;
	int touchpad_update = 0;
	int extra_command = -1;
	uint8_t extra_command_data[50];
	int extra_command_data_len = 0;
	uint8_t extra_command_answer[64];
	int extra_command_answer_len = 1;

	progname = strrchr(argv[0], '/');
	if (progname)
		progname++;
	else
		progname = argv[0];

	/* Usb transfer - default mode. */
	memset(&td, 0, sizeof(td));

	errorcnt = 0;
	opterr = 0;				/* quiet, you */
	while ((i = getopt_long(argc, argv, short_opts, long_opts, 0)) != -1) {
		switch (i) {
		case 'b':
			binary_vers = 1;
			break;
		case 'd':
			if (!parse_vidpid(optarg, &vid, &pid)) {
				printf("Invalid argument: \"%s\"\n", optarg);
				errorcnt++;
			}
			break;
		case 'e':
			get_random(extra_command_data, 32);
			extra_command_data_len = 32;
			extra_command = UPDATE_EXTRA_CMD_INJECT_ENTROPY;
			break;
		case 'f':
			show_fw_ver = 1;
			break;
		case 'g':
			extra_command = UPDATE_EXTRA_CMD_TOUCHPAD_DEBUG;
			/* Maximum length. */
			extra_command_data_len = 50;
			str2hex(optarg,
				extra_command_data, &extra_command_data_len);
			hexdump(extra_command_data, extra_command_data_len);
			extra_command_answer_len = 64;
			break;
		case 'h':
			usage(errorcnt);
			break;
		case 'j':
			extra_command = UPDATE_EXTRA_CMD_JUMP_TO_RW;
			break;
		case 'l':
			extra_command = UPDATE_EXTRA_CMD_CONSOLE_READ_INIT;
			break;
		case 'n':
			no_reset_request = 1;
			break;
		case 'p':
			touchpad_update = 1;

			data = get_file_or_die(optarg, &data_len);
			printf("read %zd(%#zx) bytes from %s\n",
				data_len, data_len, argv[optind]);

			break;
		case 'r':
			extra_command = UPDATE_EXTRA_CMD_IMMEDIATE_RESET;
			break;
		case 's':
			extra_command = UPDATE_EXTRA_CMD_STAY_IN_RO;
			break;
		case 'S':
			serialno = optarg;
			break;
		case 't':
			extra_command = UPDATE_EXTRA_CMD_TOUCHPAD_INFO;
			extra_command_answer_len =
				sizeof(struct touchpad_info);
			break;
		case 'u':
			extra_command = UPDATE_EXTRA_CMD_UNLOCK_ROLLBACK;
			break;
		case 'w':
			extra_command = UPDATE_EXTRA_CMD_UNLOCK_RW;
			break;
		case 0:				/* auto-handled option */
			break;
		case '?':
			if (optopt)
				printf("Unrecognized option: -%c\n", optopt);
			else
				printf("Unrecognized option: %s\n",
				       argv[optind - 1]);
			errorcnt++;
			break;
		case ':':
			printf("Missing argument to %s\n", argv[optind - 1]);
			errorcnt++;
			break;
		default:
			printf("Internal error at %s:%d\n", __FILE__, __LINE__);
			exit(update_error);
		}
	}

	if (errorcnt)
		usage(errorcnt);

	if (!show_fw_ver && extra_command == -1 && !touchpad_update) {
		if (optind >= argc) {
			fprintf(stderr,
				"\nERROR: Missing required <binary image>\n\n");
			usage(1);
		}

		data = get_file_or_die(argv[optind], &data_len);
		printf("read %zd(%#zx) bytes from %s\n",
		       data_len, data_len, argv[optind]);

		fetch_header_versions(data, data_len);

		if (binary_vers)
			exit(show_headers_versions(data));
	} else {
		if (optind < argc)
			printf("Ignoring binary image %s\n", argv[optind]);
	}

	usb_findit(vid, pid, serialno, &td.uep);

	setup_connection(&td);

	if (show_fw_ver) {
		printf("Current versions:\n");
		printf("Writable %32s\n", targ.common.version);
	}

	if (data) {
		if (touchpad_update) {
			transfer_section(&td,
					data,
					0x80000000,
					data_len, 0);
			free(data);

			send_done(&td.uep);
		} else {
			transferred_sections = transfer_image(&td,
							data, data_len);
			free(data);

			if (transferred_sections && !no_reset_request)
				generate_reset_request(&td);
		}
	} else if (extra_command == UPDATE_EXTRA_CMD_CONSOLE_READ_INIT) {
		read_console(&td);
	} else if (extra_command > -1) {
		send_subcommand(&td, extra_command,
				extra_command_data, extra_command_data_len,
				extra_command_answer, extra_command_answer_len);

		switch (extra_command) {
		case UPDATE_EXTRA_CMD_TOUCHPAD_INFO:
			dump_touchpad_info(extra_command_answer,
					   extra_command_answer_len);
			break;
		case  UPDATE_EXTRA_CMD_TOUCHPAD_DEBUG:
			hexdump(extra_command_answer, extra_command_answer_len);
			break;
		}
	}

	libusb_close(td.uep.devh);
	libusb_exit(NULL);

	if (!transferred_sections)
		return noop;
	/*
	 * We should indicate if RO update was not done because of the
	 * insufficient RW version.
	 */
	for (j = 0; j < ARRAY_SIZE(sections); j++)
		if (sections[j].ustatus == not_possible) {
			/* This will allow scripting repeat attempts. */
			printf("Failed to update RO, run the command again\n");
			return rw_updated;
		}

	printf("image updated\n");
	return all_updated;
}
