/*
 * Copyright (C) 2020 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <fwupdplugin.h>

#include "fu-efi-image.h"

struct _FuEfiImage {
	GObject parent_instance;
	gchar *checksum;
};

typedef struct {
	gsize offset;
	gsize size;
	gchar *name;
} FuEfiImageRegion;

typedef struct __attribute__((packed)) {
	guint32 addr;
	guint32 size;
} FuEfiImageDataDirEntry;

G_DEFINE_TYPE(FuEfiImage, fu_efi_image, G_TYPE_OBJECT)

#define _DOS_OFFSET_SIGNATURE	 0x00
#define _DOS_OFFSET_TO_PE_HEADER 0x3c

#define _PEI_OFFSET_SIGNATURE		 0x00
#define _PEI_OFFSET_MACHINE		 0x04
#define _PEI_OFFSET_NUMBER_OF_SECTIONS	 0x06
#define _PEI_OFFSET_OPTIONAL_HEADER_SIZE 0x14
#define _PEI_HEADER_SIZE		 0x18

#define _PE_OFFSET_SIZE_OF_HEADERS    0x54
#define _PE_OFFSET_CHECKSUM	      0x58
#define _PE_OFFSET_DEBUG_TABLE_OFFSET 0x98

#define _PEP_OFFSET_SIZE_OF_HEADERS    0x54
#define _PEP_OFFSET_CHECKSUM	       0x58
#define _PEP_OFFSET_DEBUG_TABLE_OFFSET 0xa8

#define _SECTION_HEADER_OFFSET_NAME 0x0
#define _SECTION_HEADER_OFFSET_SIZE 0x10
#define _SECTION_HEADER_OFFSET_PTR  0x14
#define _SECTION_HEADER_SIZE	    0x28

#define IMAGE_FILE_MACHINE_AMD64   0x8664
#define IMAGE_FILE_MACHINE_I386	   0x014c
#define IMAGE_FILE_MACHINE_THUMB   0x01c2
#define IMAGE_FILE_MACHINE_AARCH64 0xaa64

static gint
fu_efi_image_region_sort_cb(gconstpointer a, gconstpointer b)
{
	const FuEfiImageRegion *r1 = *((const FuEfiImageRegion **)a);
	const FuEfiImageRegion *r2 = *((const FuEfiImageRegion **)b);
	if (r1->offset < r2->offset)
		return -1;
	if (r1->offset > r2->offset)
		return 1;
	return 0;
}

static FuEfiImageRegion *
fu_efi_image_add_region(GPtrArray *checksum_regions,
			const gchar *name,
			gsize offset_start,
			gsize offset_end)
{
	FuEfiImageRegion *r = g_new0(FuEfiImageRegion, 1);
	r->name = g_strdup(name);
	r->offset = offset_start;
	r->size = offset_end - offset_start;
	g_ptr_array_add(checksum_regions, r);
	return r;
}

static void
fu_efi_image_region_free(FuEfiImageRegion *r)
{
	g_free(r->name);
	g_free(r);
}

FuEfiImage *
fu_efi_image_new(GBytes *data, GError **error)
{
	FuEfiImageRegion *r;
	const guint8 *buf;
	gsize bufsz;
	gsize image_bytes = 0;
	gsize checksum_offset;
	gsize data_dir_debug_offset;
	gsize offset_tmp;
	guint16 dos_sig = 0;
	guint16 machine = 0;
	guint16 opthdrsz;
	guint16 sections;
	guint32 baseaddr = 0;
	guint32 cert_table_size;
	guint32 header_size;
	guint32 nt_sig = 0;
	g_autoptr(FuEfiImage) self = g_object_new(FU_TYPE_EFI_IMAGE, NULL);
	g_autoptr(GChecksum) checksum = g_checksum_new(G_CHECKSUM_SHA256);
	g_autoptr(GPtrArray) checksum_regions = NULL;

	/* verify this is a DOS file */
	buf = fu_bytes_get_data_safe(data, &bufsz, error);
	if (buf == NULL)
		return NULL;
	if (!fu_common_read_uint16_safe(buf,
					bufsz,
					_DOS_OFFSET_SIGNATURE,
					&dos_sig,
					G_LITTLE_ENDIAN,
					error))
		return NULL;
	if (dos_sig != 0x5a4d) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_NOT_SUPPORTED,
			    "Invalid DOS header magic %04x",
			    dos_sig);
		return NULL;
	}

	/* verify the PE signature */
	if (!fu_common_read_uint32_safe(buf,
					bufsz,
					_DOS_OFFSET_TO_PE_HEADER,
					&baseaddr,
					G_LITTLE_ENDIAN,
					error))
		return NULL;
	if (!fu_common_read_uint32_safe(buf,
					bufsz,
					baseaddr + _PEI_OFFSET_SIGNATURE,
					&nt_sig,
					G_LITTLE_ENDIAN,
					error))
		return NULL;
	if (nt_sig != 0x4550) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_NOT_SUPPORTED,
			    "Invalid PE header signature %08x",
			    nt_sig);
		return NULL;
	}

	/* which machine type are we reading */
	if (!fu_common_read_uint16_safe(buf,
					bufsz,
					baseaddr + _PEI_OFFSET_MACHINE,
					&machine,
					G_LITTLE_ENDIAN,
					error))
		return NULL;
	if (machine == IMAGE_FILE_MACHINE_AMD64 || machine == IMAGE_FILE_MACHINE_AARCH64) {
		/* a.out header directly follows PE header */
		if (!fu_common_read_uint16_safe(buf,
						bufsz,
						baseaddr + _PEI_HEADER_SIZE,
						&machine,
						G_LITTLE_ENDIAN,
						error))
			return NULL;
		if (machine != 0x020b) {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_NOT_SUPPORTED,
				    "Invalid a.out machine type %04x",
				    machine);
			return NULL;
		}
		if (!fu_common_read_uint32_safe(buf,
						bufsz,
						baseaddr + _PEP_OFFSET_SIZE_OF_HEADERS,
						&header_size,
						G_LITTLE_ENDIAN,
						error))
			return NULL;

		checksum_offset = baseaddr + _PEP_OFFSET_CHECKSUM;

		/* now, this is odd. sbsigntools seems to think that we're
		 * skipping the CertificateTable -- but we actually seems to be
		 * ignoring Debug instead */
		data_dir_debug_offset = baseaddr + _PEP_OFFSET_DEBUG_TABLE_OFFSET;

	} else if (machine == IMAGE_FILE_MACHINE_I386 || machine == IMAGE_FILE_MACHINE_THUMB) {
		/* a.out header directly follows PE header */
		if (!fu_common_read_uint16_safe(buf,
						bufsz,
						baseaddr + _PEI_HEADER_SIZE,
						&machine,
						G_LITTLE_ENDIAN,
						error))
			return NULL;
		if (machine != 0x010b) {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_NOT_SUPPORTED,
				    "Invalid a.out machine type %04x",
				    machine);
			return NULL;
		}
		if (!fu_common_read_uint32_safe(buf,
						bufsz,
						baseaddr + _PE_OFFSET_SIZE_OF_HEADERS,
						&header_size,
						G_LITTLE_ENDIAN,
						error))
			return NULL;

		checksum_offset = baseaddr + _PE_OFFSET_CHECKSUM;
		data_dir_debug_offset = baseaddr + _PE_OFFSET_DEBUG_TABLE_OFFSET;

	} else {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_NOT_SUPPORTED,
			    "Invalid PE header machine %04x",
			    machine);
		return NULL;
	}

	/* get sections */
	if (!fu_common_read_uint32_safe(buf,
					bufsz,
					data_dir_debug_offset + sizeof(guint32),
					&cert_table_size,
					G_LITTLE_ENDIAN,
					error))
		return NULL;
	if (!fu_common_read_uint16_safe(buf,
					bufsz,
					baseaddr + _PEI_OFFSET_NUMBER_OF_SECTIONS,
					&sections,
					G_LITTLE_ENDIAN,
					error))
		return NULL;
	g_debug("number_of_sections: %u", sections);

	/* get header size */
	if (!fu_common_read_uint16_safe(buf,
					bufsz,
					baseaddr + _PEI_OFFSET_OPTIONAL_HEADER_SIZE,
					&opthdrsz,
					G_LITTLE_ENDIAN,
					error))
		return NULL;
	g_debug("optional_header_size: 0x%x", opthdrsz);

	/* first region: beginning to checksum_offset field */
	checksum_regions = g_ptr_array_new_with_free_func((GDestroyNotify)fu_efi_image_region_free);
	r = fu_efi_image_add_region(checksum_regions, "begin->cksum", 0x0, checksum_offset);
	image_bytes += r->size + sizeof(guint32);

	/* second region: end of checksum_offset to certificate table entry */
	r = fu_efi_image_add_region(checksum_regions,
				    "cksum->datadir[DEBUG]",
				    checksum_offset + sizeof(guint32),
				    data_dir_debug_offset);
	image_bytes += r->size + sizeof(FuEfiImageDataDirEntry);

	/* third region: end of checksum_offset to end of headers */
	r = fu_efi_image_add_region(checksum_regions,
				    "datadir[DEBUG]->headers",
				    data_dir_debug_offset + sizeof(FuEfiImageDataDirEntry),
				    header_size);
	image_bytes += r->size;

	/* add COFF sections */
	offset_tmp = baseaddr + _PEI_HEADER_SIZE + opthdrsz;
	for (guint i = 0; i < sections; i++) {
		guint32 file_offset = 0;
		guint32 file_size = 0;
		gchar name[9] = {'\0'};

		if (!fu_common_read_uint32_safe(buf,
						bufsz,
						offset_tmp + _SECTION_HEADER_OFFSET_PTR,
						&file_offset,
						G_LITTLE_ENDIAN,
						error))
			return NULL;
		if (!fu_common_read_uint32_safe(buf,
						bufsz,
						offset_tmp + _SECTION_HEADER_OFFSET_SIZE,
						&file_size,
						G_LITTLE_ENDIAN,
						error))
			return NULL;
		if (file_size == 0)
			continue;
		if (!fu_memcpy_safe((guint8 *)name,
				    sizeof(name),
				    0x0, /* dst */
				    buf,
				    bufsz,
				    offset_tmp + _SECTION_HEADER_OFFSET_NAME, /* src */
				    sizeof(name) - 1,
				    error))
			return NULL;
		r = fu_efi_image_add_region(checksum_regions,
					    name,
					    file_offset,
					    file_offset + file_size);
		image_bytes += r->size;

		if (file_offset + r->size > bufsz) {
			g_set_error(error,
				    G_IO_ERROR,
				    G_IO_ERROR_INVALID_DATA,
				    "file-aligned section %s extends beyond end of file",
				    r->name);
			return NULL;
		}
		offset_tmp += _SECTION_HEADER_SIZE;
	}

	/* make sure in order */
	g_ptr_array_sort(checksum_regions, fu_efi_image_region_sort_cb);

	/* for the data at the end of the image */
	if (image_bytes + cert_table_size < bufsz) {
		fu_efi_image_add_region(checksum_regions,
					"endjunk",
					image_bytes,
					bufsz - cert_table_size);
	} else if (image_bytes + cert_table_size > bufsz) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "checksum_offset areas outside image size");
		return NULL;
	}

	/* calculate the checksum we would find in the dbx */
	for (guint i = 0; i < checksum_regions->len; i++) {
		r = g_ptr_array_index(checksum_regions, i);
		g_debug("region %s: 0x%04x -> 0x%04x [0x%04x]",
			r->name,
			(guint)r->offset,
			(guint)(r->offset + r->size - 1),
			(guint)r->size);
		g_checksum_update(checksum, (const guchar *)buf + r->offset, (gssize)r->size);
	}
	self->checksum = g_strdup(g_checksum_get_string(checksum));
	return g_steal_pointer(&self);
}

const gchar *
fu_efi_image_get_checksum(FuEfiImage *self)
{
	return self->checksum;
}

static void
fu_efi_image_finalize(GObject *obj)
{
	FuEfiImage *self = FU_EFI_IMAGE(obj);
	g_free(self->checksum);
	G_OBJECT_CLASS(fu_efi_image_parent_class)->finalize(obj);
}

static void
fu_efi_image_class_init(FuEfiImageClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
	object_class->finalize = fu_efi_image_finalize;
}

static void
fu_efi_image_init(FuEfiImage *self)
{
}
