/* $Id$ */
/* Copyright (c) 2011-2017 Pierre Pronchery <khorben@defora.org> */
/* This file is part of DeforaOS Devel Asm */
/* This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. */



#include <System.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include "Asm.h"


/* PE */
/* private */
/* types */
/* plug-in */
struct _AsmFormatPlugin
{
	AsmFormatPluginHelper * helper;
};

#pragma pack(1)
struct pe_export_directory
{
	uint32_t flags;
	uint32_t time;
	uint16_t major;
	uint16_t minor;
	uint32_t name;
	uint32_t base;
	uint32_t functions_cnt;
	uint32_t names_cnt;
	uint32_t functions_addr;
	uint32_t names_addr;
	uint32_t ordinals_addr;
};

struct pe_header
{
	uint16_t machine;
	uint16_t section_cnt;
	uint32_t timestamp;
	uint32_t symbol_offset;
	uint32_t symbol_cnt;
	uint16_t opthdr_size;
	uint16_t flags;
};

struct pe_image_header
{
	uint16_t signature;
	uint8_t major;
	uint8_t minor;
	uint32_t code_size;
	uint32_t code_initialized;
	uint32_t code_uninitialized;
	uint32_t entrypoint;
	uint32_t code_base;
};

struct pe_image_header_data
{
	uint32_t vaddr;
	uint32_t size;
};

struct pe_image_header_pe32
{
	uint32_t data_base;
	uint32_t image_base;
	uint32_t section_alignment;
	uint32_t file_alignment;
	uint16_t os_major;
	uint16_t os_minor;
	uint16_t image_major;
	uint16_t image_minor;
	uint16_t subsys_major;
	uint16_t subsys_minor;
	uint32_t win32_version;
	uint32_t image_size;
	uint32_t headers_size;
	uint32_t checksum;
	uint16_t subsys;
	uint16_t dll_flags;
	uint32_t stack_reserved;
	uint32_t stack_commit;
	uint32_t heap_reserved;
	uint32_t heap_commit;
	uint32_t loader_flags;
	uint32_t rvasizes_cnt;
};

struct pe_image_header_pe32_plus
{
	uint64_t image_base;
	uint32_t section_alignment;
	uint32_t file_alignment;
	uint16_t os_major;
	uint16_t os_minor;
	uint16_t image_major;
	uint16_t image_minor;
	uint16_t subsys_major;
	uint16_t subsys_minor;
	uint32_t win32_version;
	uint32_t image_size;
	uint32_t headers_size;
	uint32_t checksum;
	uint16_t subsys;
	uint16_t dll_flags;
	uint32_t stack_reserved;
	uint64_t stack_commit;
	uint64_t heap_reserved;
	uint64_t heap_commit;
	uint32_t loader_flags;
	uint32_t rvasizes_cnt;
};

struct pe_msdos
{
	char signature[2];
	char _padding[0x3a];
	uint16_t offset;
};

struct pe_section_header
{
	char name[8];
	union
	{
		uint32_t paddr;
		uint32_t vsize;
	} misc;
	uint32_t vaddr;
	uint32_t raw_size;
	uint32_t raw_offset;
	uint32_t raw_reloc;
	uint32_t lines_offset;
	uint16_t reloc_cnt;
	uint16_t lines_cnt;
	uint32_t flags;
};

struct pe_symbol
{
	union
	{
		struct
		{
			char name[8];
		} _short;
		struct
		{
			uint32_t zero;
			uint32_t offset;
		} _long;
	} name;
	uint32_t value;
	uint16_t section;
	uint16_t type;
	uint8_t storage_class;
	uint8_t aux_cnt;
};
#pragma pack()


/* constants */
/* program header machine types */
#define PE_IMAGE_FILE_MACHINE_AMD64	0x8664
#define PE_IMAGE_FILE_MACHINE_ARM	0x1c00
#define PE_IMAGE_FILE_MACHINE_I386	0x014c
#define PE_IMAGE_FILE_MACHINE_UNKNOWN	0x0000

/* program image header signatures */
#define PE_IMAGE_HEADER_ROM		0x0107
#define PE_IMAGE_HEADER_PE32		0x010b
#define PE_IMAGE_HEADER_PE32_PLUS	0x020b

/* section header flags */
#define PE_IMAGE_SCN_CNT_CODE		0x00000020


/* variables */
static const struct
{
	char const * arch;
	uint16_t machine;
} _pe_arch[] =
{
	{ "amd64",	PE_IMAGE_FILE_MACHINE_AMD64	},
	{ "arm",	PE_IMAGE_FILE_MACHINE_ARM	},
	{ "i386",	PE_IMAGE_FILE_MACHINE_I386	},
	{ "i486",	PE_IMAGE_FILE_MACHINE_I386	},
	{ "i586",	PE_IMAGE_FILE_MACHINE_I386	},
	{ "i686",	PE_IMAGE_FILE_MACHINE_I386	},
	{ NULL,		PE_IMAGE_FILE_MACHINE_UNKNOWN	}
};

static char const _pe_msdos_signature[2] = "MZ";
static char const _pe_header_signature[4] = "PE\0\0";


/* prototypes */
/* plug-in */
static AsmFormatPlugin * _pe_init(AsmFormatPluginHelper * helper,
		char const * arch);
static char const * _pe_guess(AsmFormatPlugin * format, char const * hint);
static char const * _pe_detect(AsmFormatPlugin * format);
static int _pe_decode(AsmFormatPlugin * format, int raw);
static int _pe_decode_section(AsmFormatPlugin * format, AsmSection * section,
		AsmArchInstructionCall ** calls, size_t * calls_cnt);

/* useful */
static char const * _pe_get_arch(uint16_t machine);
static int _pe_get_machine(char const * arch);


/* public */
/* variables */
AsmFormatPluginDefinition format_plugin =
{
	"pe",
	"Portable Executable (PE)",
	LICENSE_GNU_LGPL3_FLAGS,
	_pe_msdos_signature,
	sizeof(_pe_msdos_signature),
	_pe_init,
	NULL,
	_pe_guess,
	NULL,
	NULL,
	NULL,
	_pe_detect,
	_pe_decode,
	_pe_decode_section
};


/* private */
/* functions */
/* pe_init */
static AsmFormatPlugin * _pe_init(AsmFormatPluginHelper * helper,
		char const * arch)
{
	AsmFormatPlugin * pe;
	int machine;
	struct pe_msdos pm;
	struct pe_header ph;

	if((pe = object_new(sizeof(*pe))) == NULL)
		return NULL;
	pe->helper = helper;
	if(arch == NULL)
		return pe;
	if((machine = _pe_get_machine(arch)) < 0)
	{
		object_delete(pe);
		return NULL;
	}
	/* output the MS-DOS header */
	memset(&pm, 0, sizeof(pm));
	memcpy(pm.signature, _pe_msdos_signature, sizeof(pm.signature));
	pm.offset = sizeof(pm);
	if(helper->write(helper->format, &pm, sizeof(pm)) != sizeof(pm))
	{
		object_delete(pe);
		return NULL;
	}
	/* output the PE signature */
	if(helper->write(helper->format, _pe_header_signature,
				sizeof(_pe_header_signature))
			!= sizeof(_pe_header_signature))
	{
		object_delete(pe);
		return NULL;
	}
	/* output the PE header */
	memset(&ph, 0, sizeof(ph));
	ph.machine = _htol16(machine);
	ph.timestamp = _htol32(time(NULL));
	/* FIXME update the section and symbol lists */
	if(helper->write(helper->format, &ph, sizeof(ph)) != sizeof(ph))
	{
		object_delete(pe);
		return NULL;
	}
	return pe;
}


/* pe_guess */
static char const * _pe_guess(AsmFormatPlugin * format, char const * hint)
{
	struct
	{
		char const * quirk;
		char const * arch;
	} quirks[] =
	{
		{ "arm", "armel" },
		{ "mips", "mipsel" },
		{ "x86", "i686" },
		{ "x86-64", "amd64" },
		{ "x86_64", "amd64" }
	};
	size_t i;
	(void) format;

	if(hint == NULL)
		return NULL;
	for(i = 0; i < sizeof(quirks) / sizeof(*quirks); i++)
		if(string_compare(hint, quirks[i].quirk) == 0)
			return quirks[i].arch;
	for(i = 0; i < sizeof(_pe_arch) / sizeof(*_pe_arch); i++)
		if(string_compare(_pe_arch[i].arch, hint) == 0)
			return hint;
	return NULL;
}


/* pe_detect */
static char const * _pe_detect(AsmFormatPlugin * format)
{
	AsmFormatPluginHelper * helper = format->helper;
	struct pe_msdos pm;
	struct pe_header ph;

	if(helper->seek(helper->format, 0, SEEK_SET) != 0)
		return NULL;
	if(helper->read(helper->format, &pm, sizeof(pm)) != sizeof(pm))
		return NULL;
	pm.offset = _htol16(pm.offset) + 4;
	if(helper->seek(helper->format, pm.offset, SEEK_SET) != pm.offset)
		return NULL;
	if(helper->read(helper->format, &ph, sizeof(ph)) != sizeof(ph))
		return NULL;
	ph.machine = _htol16(ph.machine);
	return _pe_get_arch(ph.machine);
}


/* pe_decode */
static int _decode_data(AsmFormatPlugin * format, uint32_t vaddr, uint32_t base,
		struct pe_image_header_data * pid, size_t i);
static int _decode_data_export_directory(AsmFormatPlugin * format, uint32_t vaddr,
		uint32_t base, struct pe_image_header_data * pid);
static int _decode_error(AsmFormatPlugin * format);
static char * _decode_string(AsmFormatPlugin * format, off_t offset);

static int _pe_decode(AsmFormatPlugin * format, int raw)
{
	AsmFormatPluginHelper * helper = format->helper;
	struct pe_msdos pm;
	char buf[sizeof(_pe_header_signature)];
	struct pe_header ph;
	size_t i;
	size_t cnt;
	struct pe_section_header psh;
	char * p = NULL;
	char * q;
	struct pe_image_header * pih;
	struct pe_image_header_pe32 * pih32;
	struct pe_image_header_pe32_plus * pih32p;
	struct pe_image_header_data * pid = NULL;
	off_t base = 0;
	off_t offset;
	struct pe_symbol ps;

	/* read the MS-DOS header */
	if(helper->seek(helper->format, 0, SEEK_SET) != 0)
		return -1;
	if(helper->read(helper->format, &pm, sizeof(pm)) != sizeof(pm))
		return -1;
	/* check the PE signature */
	if(helper->seek(helper->format, pm.offset, SEEK_SET) != pm.offset)
		return -1;
	if(helper->read(helper->format, &buf, sizeof(buf)) != sizeof(buf))
		return -1;
	if(memcmp(buf, _pe_header_signature, sizeof(buf)) != 0)
		return -1;
	/* read the PE header */
	if(helper->read(helper->format, &ph, sizeof(ph)) != sizeof(ph))
		return _decode_error(format);
	ph.section_cnt = _htol16(ph.section_cnt);
	ph.opthdr_size = _htol16(ph.opthdr_size);
	/* read the optional header if available, skip it if bogus */
	if(ph.opthdr_size >= sizeof(*pih))
	{
		if((p = malloc(ph.opthdr_size)) == NULL)
			return _decode_error(format);
		if(helper->read(helper->format, p, ph.opthdr_size)
				!= ph.opthdr_size)
		{
			free(p);
			return _decode_error(format);
		}
		pih = (struct pe_image_header *)p;
		pih->signature = _htol16(pih->signature);
		pih->code_base = _htol32(pih->code_base);
		/* read any additional part of the optional header */
		cnt = 0;
		if(pih->signature == PE_IMAGE_HEADER_PE32
				&& ph.opthdr_size >= sizeof(*pih)
				+ sizeof(*pih32))
		{
			/* PE32 executable */
			pih32 = (struct pe_image_header_pe32 *)(pih + 1);
			pih32->image_base = _htol32(pih32->image_base);
			pih32->rvasizes_cnt = _htol32(pih32->rvasizes_cnt);
			base = pih32->image_base;
			pid = (struct pe_image_header_data *)(pih32 + 1);
			cnt = pih32->rvasizes_cnt;
		}
		else if(pih->signature == PE_IMAGE_HEADER_PE32_PLUS
				&& ph.opthdr_size >= sizeof(*pih)
				+ sizeof(*pih32p))
		{
			/* PE32+ executable */
			pih32p = (struct pe_image_header_pe32_plus *)(pih + 1);
			pih32p->image_base = _htol64(pih32p->image_base);
			pih32p->rvasizes_cnt = _htol32(pih32p->rvasizes_cnt);
			base = pih32p->image_base;
			pid = (struct pe_image_header_data *)(pih32p + 1);
			cnt = pih32p->rvasizes_cnt;
		}
		/* read the data directories */
		for(i = 0; pid != NULL && (char *)(&pid[i + 1]) < p
				+ ph.opthdr_size && i < cnt; i++)
			if(_decode_data(format, base, pih->code_base, &pid[i],
						i) != 0)
				break; /* XXX report error */
	}
	/* read and record each section */
	offset = pm.offset + sizeof(_pe_header_signature) + sizeof(ph)
		+ ph.opthdr_size;
	if(ph.opthdr_size != 0 && helper->seek(helper->format, offset, SEEK_SET)
			!= offset)
		return _decode_error(format);
	for(i = 0; i < ph.section_cnt; i++)
	{
		if(helper->read(helper->format, &psh, sizeof(psh))
				!= sizeof(psh))
			break;
		offset += sizeof(psh);
		psh.name[sizeof(psh.name) - 1] = '\0';
		psh.vaddr = _htol32(psh.vaddr);
		psh.raw_size = _htol32(psh.raw_size);
		psh.raw_offset = _htol32(psh.raw_offset);
		psh.flags = _htol32(psh.flags);
		/* decode non-executable sections only if requested */
		if((psh.flags & PE_IMAGE_SCN_CNT_CODE) != PE_IMAGE_SCN_CNT_CODE
				&& !raw)
			continue;
		/* the $ sign has a special meaning for the linker */
		if((q = strchr(psh.name, '$')) != NULL)
			*q = '\0';
		if(helper->set_section(helper->format, i, 0, psh.name,
					psh.raw_offset, psh.raw_size,
					psh.vaddr + base) == NULL)
			break;
		if(helper->seek(helper->format, offset, SEEK_SET) != offset)
			break;
	}
	if(i != ph.section_cnt)
	{
		free(p);
		return -1;
	}
	/* read symbols (deprecated COFF debugging information) */
	if(ph.symbol_offset != 0 && helper->seek(helper->format,
				ph.symbol_offset, SEEK_SET) == ph.symbol_offset)
	{
		for(i = 0; i < ph.symbol_cnt; i++)
		{
			if(helper->read(helper->format, &ps, sizeof(ps))
					!= sizeof(ps))
				break;
			/* FIXME implement */
		}
	}
	free(p);
	return 0;
}

static int _decode_data(AsmFormatPlugin * format, uint32_t vaddr, uint32_t base,
		struct pe_image_header_data * pid, size_t i)
{
	pid->vaddr = _htol32(pid->vaddr);
	pid->size = _htol32(pid->size);
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() pid[%lu] 0x%08x, 0x%08x\n", __func__, i,
			pid->vaddr, pid->size);
#endif
	switch(i)
	{
		case 0:
			return _decode_data_export_directory(format, vaddr,
					base, pid);
		default:
			/* FIXME implement the rest */
			break;
	}
	return 0;
}

static int _decode_data_export_directory(AsmFormatPlugin * format, uint32_t vaddr,
		uint32_t base, struct pe_image_header_data * pid)
{
	AsmFormatPluginHelper * helper = format->helper;
	struct pe_export_directory ped;
	size_t j;
	uint32_t f;
	uint32_t n;
	char * p;

	if(base > pid->vaddr || helper->seek(helper->format, pid->vaddr - base,
				SEEK_SET) < 0
			|| helper->read(helper->format, &ped, sizeof(ped))
			!= sizeof(ped))
		return -1;
	ped.major = _htol16(ped.major);
	ped.minor = _htol16(ped.minor);
	ped.name = _htol32(ped.name);
	ped.base = _htol32(ped.base);
	ped.functions_cnt = _htol32(ped.functions_cnt);
	ped.names_cnt = _htol32(ped.names_cnt);
	ped.functions_addr = _htol32(ped.functions_addr);
	ped.names_addr = _htol32(ped.names_addr);
	ped.ordinals_addr = _htol32(ped.ordinals_addr);
	ped.time = _htol32(ped.time);
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() M:0x%04x m:0x%04x n:0x%08x f:0x%08x@0x%08x"
			" n:0x%0x8@0x%08x 0x%08x t:0x%08x\n", __func__,
			ped.major, ped.minor, ped.name, ped.functions_cnt,
			ped.functions_addr, ped.names_cnt, ped.names_addr,
			ped.base, ped.time);
#endif
	for(j = 0; j < ped.functions_cnt && j < ped.names_cnt; j++)
	{
		if(helper->seek(helper->format, ped.functions_addr - base
					+ j * sizeof(f), SEEK_SET) < 0)
			return -1;
		if(helper->read(helper->format, &f, sizeof(f)) != sizeof(f))
			return -1;
		if(helper->seek(helper->format, ped.names_addr - base
					+ j * sizeof(n), SEEK_SET) < 0)
			return -1;
		if(helper->read(helper->format, &n, sizeof(n)) != sizeof(n))
			return -1;
		if((p = _decode_string(format, n - base)) == NULL)
			continue;
#ifdef DEBUG
		fprintf(stderr, "DEBUG: %s() 0x%08x@0x%08x \"%s\"\n", __func__,
				n - base, f + vaddr, p);
#endif
		/* XXX report errors */
		helper->set_function(helper->format, f + vaddr, p, f, -1);
		free(p);
	}
	return 0;
}

static int _decode_error(AsmFormatPlugin * format)
{
	return -error_set_code(1, "%s: %s", format->helper->get_filename(
				format->helper->format), strerror(errno));
}

static char * _decode_string(AsmFormatPlugin * format, off_t offset)
{
	AsmFormatPluginHelper * helper = format->helper;
	char * ret = NULL;
	char * p;
	size_t len = 0;
	ssize_t s;
	size_t i;
	const int inc = 32;

	if(helper->seek(helper->format, offset, SEEK_SET) != offset)
		return NULL;
	for(;;)
	{
		if((p = realloc(ret, len + inc)) == NULL)
		{
			free(ret);
			return NULL;
		}
		ret = p;
		if((s = helper->read(helper->format, &ret[len], inc)) < 0)
		{
			free(ret);
			return NULL;
		}
		for(i = len; i < len + inc; i++)
			if(ret[i] == '\0')
				return ret;
		len += s;
	}
	return ret;
}


/* pe_decode_section */
static int _pe_decode_section(AsmFormatPlugin * format, AsmSection * section,
		AsmArchInstructionCall ** calls, size_t * calls_cnt)
{
	AsmFormatPluginHelper * helper = format->helper;

	return helper->decode(helper->format, section->offset, section->size,
			section->base, calls, calls_cnt);
}


/* accessors */
/* pe_get_arch */
static char const * _pe_get_arch(uint16_t machine)
{
	size_t i;

	for(i = 0; _pe_arch[i].arch != NULL; i++)
		if(_pe_arch[i].machine == machine)
			return _pe_arch[i].arch;
	error_set_code(1, "%s: %s 0x%x", "pe", "Unknown architecture", machine);
	return NULL;
}


/* pe_get_machine */
static int _pe_get_machine(char const * arch)
{
	size_t i;

	for(i = 0; _pe_arch[i].arch != NULL; i++)
		if(strcmp(_pe_arch[i].arch, arch) == 0)
			return _pe_arch[i].machine;
	return -error_set_code(1, "%s: %s", arch,
			"Unsupported architecture for PE");
}
