/* ----------------------------------------------------------------------- *
 *
 *   Copyright 2008 rPath, Inc. - All Rights Reserved
 *
 *   Permission is hereby granted, free of charge, to any person
 *   obtaining a copy of this software and associated documentation
 *   files (the "Software"), to deal in the Software without
 *   restriction, including without limitation the rights to use,
 *   copy, modify, merge, publish, distribute, sublicense, and/or
 *   sell copies of the Software, and to permit persons to whom
 *   the Software is furnished to do so, subject to the following
 *   conditions:
 *
 *   The above copyright notice and this permission notice shall
 *   be included in all copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 *   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 *   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 *   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 *   OTHER DEALINGS IN THE SOFTWARE.
 *
 * ----------------------------------------------------------------------- */

/*
 * reloc_linux.c
 *
 * This is the initial program run before the Linux kernel.
 * It serves to hoist the initrd as high as permitted.
 *
 * This runs in protected mode, but in low memory, and should be kept
 * as small as possible.
 */

#include "reloc/reloc.h"
#include "setup.h"

static uint32_t initrd_len, initrd_addr;
static uint64_t max_addr;

static int initrd_fit(uint32_t base, uint32_t end)
{
	uint64_t ibase;

	if (end <= base)
		return -1;	/* Invalid */

	if (base > max_addr)
		return -1;	/* Not accessible */

	if (end > max_addr)
		end = max_addr;

	if (end < initrd_len)
		return -1;

	ibase = (end - initrd_len) & ~0xfff;
	if (ibase < base)
		return -1;

	if (initrd_addr < ibase) {
		initrd_addr = ibase; /* This is a better one... */
	}

	return 0;
}

static int probe_memory_e820(void)
{
	com32sys_t regs;
	struct e820_info {
		uint64_t base;
		uint64_t len;
		uint32_t type;
	} buf;
	int rv = -1;
	uint32_t copied;

	memset(&regs, 0, sizeof regs);

	do {
		regs.eax.l = 0x0000e820;
		regs.ecx.l = sizeof buf;
		regs.edx.l = 0x534d4150;
		regs.edi.w[0] = OFFS(&buf);
		regs.es = SEG(&buf);

		intcall(0x15, &regs, &regs);
		copied = (regs.eflags.l & EFLAGS_CF)
			? 0 : regs.ecx.l;

		if ( regs.eax.l != 0x534d4150 || copied < 20 )
			break;

		if (buf.type != 1)
			continue; /* Not memory */

		rv &= initrd_fit(buf.base, buf.base+buf.len);
	} while (regs.ebx.l);

	return rv;
}

static int probe_memory_e801(void)
{
	com32sys_t regs;
	uint64_t end;

	memset(&regs, 0, sizeof regs);
	regs.eax.w[0] = 0xe801;
	intcall(0x15, &regs, &regs);

	if (regs.eflags.l & EFLAGS_CF)
		return -1;	/* No e801 */

	if (regs.eax.w[0] < 15*1024)
		end = (uint64_t)(regs.eax.w[0] << 10) + 0x100000;
	else
		end = (uint64_t)(regs.ebx.w[0] << 16) + 0x1000000;

	return initrd_fit(0x100000, end);
}

static int probe_memory_88(void)
{
	com32sys_t regs;

	memset(&regs, 0, sizeof regs);
	regs.eax.b[1] = 0x88;
	intcall(0x15, &regs, &regs);

	if (regs.eflags.l & EFLAGS_CF)
		return -1;

	return initrd_fit(0x100000, (regs.eax.w[0] << 10)+0x100000);
}

static int place_initrd(void)
{
	int rv;

	rv = probe_memory_e820();
	if (!rv)
		return 0;

	rv = probe_memory_e801();
	if (!rv)
		return 0;

	return probe_memory_88();
}

extern char _end[];
/* This structure is hacked by the installer */
static const struct startup_info startup_info
__attribute__((section(".startupinfo"))) =
{
	.reloc_size = (uint32_t)&_end,
};

int main(void)
{
	struct setup_header *hdr = (void *)(startup_info.setup_addr + 0x1f1);

	if (startup_info.rd_len) {
		initrd_len = startup_info.rd_len;
		max_addr   = startup_info.rd_maxaddr;

		if (place_initrd())
			return -1;

		/* Move the initrd into place */
		memmove((void *)initrd_addr, (void *)startup_info.rd_addr,
			startup_info.rd_len);

		hdr->ramdisk_image = initrd_addr;
	}

	jump_to_kernel(startup_info.setup_addr >> 4,
		       startup_info.cmdline_addr - startup_info.setup_addr);
	return -1;		/* Shouldn't return... */
}
