/* Copyright 2016 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 <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "futility.h"

enum {
	OPT_HELP = 1000,
	OPT_OFFSET,
};

static const struct option long_opts[] = {
	{"help", 0, 0, OPT_HELP},
	{"offset", 1, 0, OPT_OFFSET},
	{NULL, 0, NULL, 0},
};

static void print_help(int argc, char *argv[])
{
	printf("\nUsage:  " MYNAME " %s FILE [OPTIONS]\n", argv[0]);
	printf("\nOptions:\n");
	printf(" --offset <offset>          Offset of cache within FILE\n");
	printf("\n");
}

struct mrc_metadata {
	uint32_t signature;
	uint32_t data_size;
	uint16_t data_checksum;
	uint16_t header_checksum;
	uint32_t version;
} __attribute__((packed));

#define MRC_DATA_SIGNATURE		(('M'<<0)|('R'<<8)|('C'<<16)|('D'<<24))
#define REGF_BLOCK_SHIFT		4
#define REGF_BLOCK_GRANULARITY		(1 << REGF_BLOCK_SHIFT)
#define REGF_METADATA_BLOCK_SIZE	REGF_BLOCK_GRANULARITY
#define REGF_UNALLOCATED_BLOCK		0xffff

static unsigned long compute_ip_checksum(const void *addr, unsigned long length)
{
	const uint8_t *ptr;
	volatile union {
		uint8_t  byte[2];
		uint16_t word;
	} value;
	unsigned long sum;
	unsigned long i;
	/* In the most straight forward way possible,
	 * compute an ip style checksum.
	 */
	sum = 0;
	ptr = addr;
	for(i = 0; i < length; i++) {
		unsigned long v;
		v = ptr[i];
		if (i & 1) {
			v <<= 8;
		}
		/* Add the new value */
		sum += v;
		/* Wrap around the carry */
		if (sum > 0xFFFF) {
			sum = (sum + (sum >> 16)) & 0xFFFF;
		}
	}
	value.byte[0] = sum & 0xff;
	value.byte[1] = (sum >> 8) & 0xff;
	return (~value.word) & 0xFFFF;
}

static int verify_mrc_slot(struct mrc_metadata *md, unsigned long slot_len)
{
	uint32_t header_checksum;

	if (slot_len < sizeof(*md)) {
		fprintf(stderr, "Slot too small!\n");
		return 1;
	}

	if (md->signature != MRC_DATA_SIGNATURE) {
		fprintf(stderr, "MRC signature mismatch\n");
		return 1;
	}

	fprintf(stderr, "MRC signature match.. successful\n");

	if (md->data_size > slot_len) {
		fprintf(stderr, "MRC cache size overflow\n");
		return 1;
	}

	header_checksum = md->header_checksum;
	md->header_checksum = 0;

	if (header_checksum != compute_ip_checksum(md, sizeof(*md))) {
		fprintf(stderr, "MRC metadata header checksum mismatch\n");
		return 1;
	}

	md->header_checksum = header_checksum;

	fprintf(stderr, "MRC metadata header checksum.. verified!\n");

	if (md->data_checksum != compute_ip_checksum(&md[1], md->data_size)) {
		fprintf(stderr, "MRC data checksum mismatch\n");
		return 1;
	}

	fprintf(stderr, "MRC data checksum.. verified!\n");
	return 0;
}

static int block_offset_unallocated(uint16_t offset)
{
	return offset == REGF_UNALLOCATED_BLOCK;
}

static uint8_t *get_next_mb(uint8_t *curr_mb)
{
	return curr_mb + REGF_METADATA_BLOCK_SIZE;
}

static int get_mrc_data_slot(uint16_t *mb, uint32_t *data_offset,
			     uint32_t *data_size)
{
	uint16_t num_metadata_blocks = *mb;

	if (block_offset_unallocated(*mb)) {
		fprintf(stderr, "MRC cache is empty!!\n");
		return 1;
	}

	/*
	 * First block offset in metadata block tells the total number of
	 * metadata blocks.
	 * Currently, we expect only 1 metadata block to be used.
	 */
	if (num_metadata_blocks != 1) {
		uint16_t *next_mb = (uint16_t *)get_next_mb((uint8_t *)mb);
		if (!block_offset_unallocated(*next_mb)) {
			fprintf(stderr, "More than 1 valid metadata block!!");
			return 1;
		}
	}

	/*
	 * RECOVERY_MRC_CACHE is expected to contain only one slot. Thus, there
	 * should be only one block offset present, indicating size of the MRC
	 * cache slot.
	 */
	mb++;
	*data_offset = (1 << REGF_BLOCK_SHIFT) * num_metadata_blocks;
	*data_size = (*mb - num_metadata_blocks) << REGF_BLOCK_SHIFT;

	mb++;
	if (!block_offset_unallocated(*mb)) {
		fprintf(stderr, "More than 1 slot in recovery mrc cache.\n");
		return 1;
	}

	return 0;
}

static int do_validate_rec_mrc(int argc, char *argv[])
{
	char *infile = NULL;
	int parse_error = 0;
	int fd, i, ret = 1;
	uint32_t file_size;
	uint8_t *buff;
	uint32_t offset = 0;
	uint32_t data_offset;
	uint32_t data_size;
	char *e;

	while (((i = getopt_long(argc, argv, ":", long_opts, NULL)) != -1) &&
	       !parse_error) {
		switch (i) {
		case OPT_HELP:
			print_help(argc, argv);
			return 0;
		case OPT_OFFSET:
			offset = strtoul(optarg, &e, 0);
			if (!*optarg || (e && *e)) {
				fprintf(stderr, "Invalid --offset\n");
				parse_error = 1;
			}
			break;
		default:
		case '?':
			parse_error = 1;
			break;
		}
	}

	if (parse_error) {
		print_help(argc, argv);
		return 1;
	}

	if ((argc - optind) < 1) {
		fprintf(stderr, "You must specify an input FILE!\n");
		print_help(argc, argv);
		return 1;
	} else if ((argc - optind) != 1) {
		fprintf(stderr, "Unexpected arguments!\n");
		print_help(argc, argv);
		return 1;
	}

	infile = argv[optind++];

	if (futil_open_and_map_file(infile, &fd, FILE_RO, &buff, &file_size) !=
	    FILE_ERR_NONE)
		return 1;

	if (offset > file_size) {
		fprintf(stderr, "File size(%#x) smaller than offset(%#x)\n",
			file_size, offset);
		futil_unmap_and_close_file(fd, FILE_RO, buff, file_size);
		return 1;
	}

	if (get_mrc_data_slot((uint16_t *)(buff + offset), &data_offset,
			      &data_size)) {
		fprintf(stderr, "Metadata block error\n");
		futil_unmap_and_close_file(fd, FILE_RO, buff, file_size);
		return 1;
	}
	offset += data_offset;

	if ((file_size > offset) && ((file_size - offset) >= data_size))
		ret = verify_mrc_slot((struct mrc_metadata *)(buff + offset),
				      data_size);
	else
		fprintf(stderr, "Offset or data size greater than file size: "
			"offset=%#x, file size=%#x, data_size=%#x\n",
			offset, file_size, data_size);

	if (futil_unmap_and_close_file(fd, FILE_RO, buff, file_size) !=
	    FILE_ERR_NONE)
		return 1;

	return ret;
}

DECLARE_FUTIL_COMMAND(validate_rec_mrc, do_validate_rec_mrc, VBOOT_VERSION_ALL,
		      "Validates content of Recovery MRC cache");
