/*
 * zipl - zSeries Initial Program Loader tool
 *
 * Functions to handle the boot loader data
 *
 * Copyright IBM Corp. 2001, 2017
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */

#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "lib/util_libc.h"
#include "boot/loaders_layout.h"

#include "stage2dump.h"
#include "stage3.h"

#include "boot.h"
#include "bootmap.h"
#include "error.h"
#include "misc.h"

/* Import a binary file */
/* clang-format off */
#define DATA_NAME(SYM, SUFFIX) _binary_##SYM##_bin##SUFFIX
#define DATA_SIZE(SYM)	       ((size_t)(DATA_NAME(SYM, _end) - DATA_NAME(SYM, _start)))
#define DATA_ADDR(SYM)	       (&DATA_NAME(SYM, _start))
#define BIN_FILE_PATH(FILE_NAME) STRINGIFY(BUILD_PATH) "/" STRINGIFY(FILE_NAME) ".bin"
#define IMPORT_DATA(SYM)						\
	extern const uint8_t DATA_NAME(SYM, _start)[];			\
	extern const uint8_t DATA_NAME(SYM, _end)[];			\
	asm(".section \".rodata\", \"a\", @progbits\n"			\
	    ".balign 4\n"						\
	    ".global " STRINGIFY(DATA_NAME(SYM, _start)) "\n"		\
	    STRINGIFY(DATA_NAME(SYM, _start)) ":\n"			\
	    ".incbin \"" BIN_FILE_PATH(SYM) "\"\n"			\
	    ".global " STRINGIFY(DATA_NAME(SYM, _end)) "\n"		\
	    STRINGIFY(DATA_NAME(SYM, _end)) ":\n"			\
	    ".balign 4\n"						\
	    ".previous\n")
/* clang-format on */

/* Stage 0 Loader */
IMPORT_DATA(eckd0_cdl);
IMPORT_DATA(eckd0_ldl);
IMPORT_DATA(fba0);
IMPORT_DATA(tape0);
/* Stage 1 Loader */
IMPORT_DATA(eckd1);
/* Stage 1b Loader */
IMPORT_DATA(eckd1b);
IMPORT_DATA(fba1b);
/* Stage 2 Loader */
IMPORT_DATA(eckd2);
IMPORT_DATA(fba2);
/* Stage 2 Dump Loader */
IMPORT_DATA(eckd2dump_mv);
IMPORT_DATA(eckd2dump_sv);
IMPORT_DATA(fba2dump);
IMPORT_DATA(tape2dump);

#define CCW_FLAG_CC		0x40
#define CCW_FLAG_SLI		0x20
#define FBA_BLK_SIZE		512

static struct boot_ccw0 tic_to_stage1b = {
	.cmd = 0x08, /* tic */
	.address_lo = STAGE1B_LOAD_ADDRESS,
};

/* Check sizes of internal objects. Return 0 if everything is correct,
 * non-zero otherwise. */
int
boot_check_data(void)
{
	if (DATA_SIZE(fba0) != sizeof(struct boot_fba_stage0)) {
		error_reason("Size mismatch of FBA stage 0 loader");
		return -1;
	}
	if (DATA_SIZE(fba1b) != sizeof(struct boot_fba_stage1b)) {
		error_reason("Size mismatch of FBA stage 1b loader");
		return -1;
	}
	if (DATA_SIZE(eckd0_ldl) !=
	    sizeof(struct boot_eckd_ldl_stage0)) {
		error_reason("Size mismatch of ECKD LDL stage 0 loader");
		return -1;
	}
	if (DATA_SIZE(eckd0_cdl) != sizeof(struct boot_eckd_cdl_stage0)) {
		error_reason("Size mismatch of ECKD CDL stage 0 loader");
		return -1;
	}
	if (DATA_SIZE(eckd1) != sizeof(struct boot_eckd_stage1)) {
		error_reason("Size mismatch of ECKD stage 1 loader");
		return -1;
	}
	if (DATA_SIZE(eckd1b) != sizeof(struct boot_eckd_stage1b)) {
		error_reason("Size mismatch of ECKD stage 1b loader");
		return -1;
	}
	return 0;
}

/*
 * Create a stage 3 parameter block in memory.
 * Upon success, return 0 and set BUFFER to point to the data buffer and set
 * BYTECOUNT to contain the parameter block size in bytes.
 * Return non-zero otherwise.
 */
int
boot_get_stage3_parms(void **buffer, size_t *bytecount, address_t parm_addr,
		      address_t initrd_addr, size_t initrd_len,
		      address_t entry, int extra_parm, uint64_t flags,
		      address_t image_addr, size_t image_len,
		      address_t envblk_addr, size_t envblk_len)
{
	struct stage3_parms params;
	void* data;

	if (entry != (entry & PSW32_ADDR_MASK)) {
		error_reason("Kernel image entry point to high (31 bit "
			     "addressing mode)");
		return -1;
	}
	/* Get memory */
	data = misc_malloc(sizeof(params));
	if (data == NULL)
		return -1;
	memset(data, 0, sizeof(params));
	/* Prepare params section */
	params.parm_addr = (uint64_t) parm_addr;
	params.initrd_addr = (uint64_t) initrd_addr;
	params.initrd_len = (uint64_t) initrd_len;
	params.load_psw = (uint64_t)(entry | PSW_LOAD);
	params.extra_parm = (uint64_t) extra_parm;
	params.flags = flags;
	params.image_len = (uint64_t) image_len;
	params.image_addr = (uint64_t) image_addr;
	params.envblk_addr = (uint64_t) envblk_addr;
	params.envblk_len = (uint64_t) envblk_len;
	/* Initialize buffer */
	memcpy(data, &params, sizeof(params));
	*buffer = data;
	*bytecount = sizeof(params);
	return 0;
}

int
boot_init_fba_stage0(struct boot_fba_stage0 *stage0,
		     disk_blockptr_t *stage1b_list, blocknum_t stage1b_count)
{
	blocknum_t i;

	/* Initialize stage 0 data */
	memcpy(stage0, DATA_ADDR(fba0), DATA_SIZE(fba0));
	/* Fill in blocklist for stage 2 loader */
	if (stage1b_count > STAGE1B_BLK_CNT_MAX) {
		error_reason("Not enough room for FBA stage 1b loader");
		return -1;
	}
	for (i = 0; i < stage1b_count; i++) {
		stage0->locdata[i].blocknr =
			(uint32_t) stage1b_list[i].linear.block;
		stage0->locread[i].read.address_lo = STAGE1B_LOAD_ADDRESS + i * FBA_BLK_SIZE;
	}
	/* Terminate CCW chain: Tic to stage 1b */
	memcpy(&stage0->locread[i], &tic_to_stage1b, sizeof(tic_to_stage1b));
	return 0;
}

void
boot_init_eckd_ldl_stage0(struct boot_eckd_ldl_stage0 *stage0)
{
	memcpy(stage0, DATA_ADDR(eckd0_ldl), DATA_SIZE(eckd0_ldl));
	/* Fill in size of stage 1 plus stage 0 loader */
	stage0->read_r1.count = sizeof(struct boot_eckd_stage1) +
		sizeof(struct boot_eckd_ldl_stage0);
}

void
boot_init_eckd_cdl_stage0(struct boot_eckd_cdl_stage0 *stage0)
{
	memcpy(stage0, DATA_ADDR(eckd0_cdl), DATA_SIZE(eckd0_cdl));
	/* Fill in size of stage 1 loader */
	stage0->read.count = sizeof(struct boot_eckd_stage1);
}

int
boot_init_eckd_stage1(struct boot_eckd_stage1 *stage1,
		      disk_blockptr_t *stage1b_list, blocknum_t stage1b_count)
{
	blocknum_t i;

	memcpy(stage1, DATA_ADDR(eckd1), DATA_SIZE(eckd1));
	/* Fill in blocklist for stage 1b  loader */
	if (stage1b_count > STAGE1B_BLK_CNT_MAX) {
		error_reason("Not enough room for ECKD stage 1b loader "
			     "(try larger block size)");
		return -1;
	}
	for (i = 0; i < stage1b_count; i++) {
		stage1->ssrt[i].read.count = stage1b_list[i].chs.size;
		stage1->seek[i].cyl = stage1b_list[i].chs.cyl;
		stage1->seek[i].head = stage1b_list[i].chs.head |
			((stage1b_list[i].chs.cyl >> 12) & 0xfff0);
		stage1->seek[i].sec = stage1b_list[i].chs.sec;
		stage1->ssrt[i].read.address_lo =
			STAGE1B_LOAD_ADDRESS + i * stage1b_list[i].chs.size;
		stage1->ssrt[i].read.flags = CCW_FLAG_CC | CCW_FLAG_SLI;
	}
	/* Terminate CCW chain: Tic to stage 1b */
	memcpy(&stage1->ssrt[i], &tic_to_stage1b, sizeof(tic_to_stage1b));
	return 0;
}

int
boot_init_fba_stage1b(struct boot_fba_stage1b *stage1b,
		      disk_blockptr_t *stage2_list, blocknum_t stage2_count)
{
	blocknum_t i;

	memcpy(stage1b, DATA_ADDR(fba1b), DATA_SIZE(fba1b));
	if (stage2_count > STAGE2_BLK_CNT_MAX) {
		error_reason("Not enough room for FBA stage 2 loader");
		return -1;
	}
	for (i = 0; i < stage2_count; i++) {
		stage1b->locdata[i].blocknr =
			(uint32_t) stage2_list[i].linear.block;
		stage1b->locread[i].read.address_lo =
			STAGE2_LOAD_ADDRESS + i * FBA_BLK_SIZE;
	}
	/* Terminate CCW chain */
	stage1b->locread[i - 1].read.flags &= ~CCW_FLAG_CC;
	return 0;
}

int
boot_init_eckd_stage1b(struct boot_eckd_stage1b *stage1b,
		       disk_blockptr_t *stage2_list, blocknum_t stage2_count)
{
	blocknum_t i;

	memcpy(stage1b, DATA_ADDR(eckd1b), DATA_SIZE(eckd1b));
	if (stage2_count > STAGE2_BLK_CNT_MAX) {
		error_reason("Not enough room for ECKD stage 2 loader "
			     "(try larger block size)");
		return -1;
	}
	for (i = 0; i < stage2_count; i++) {
		stage1b->ssrt[i].read.count = stage2_list[i].chs.size;
		stage1b->seek[i].cyl = stage2_list[i].chs.cyl;
		stage1b->seek[i].head = stage2_list[i].chs.head |
			((stage2_list[i].chs.cyl >> 12) & 0xfff0);
		stage1b->seek[i].sec = stage2_list[i].chs.sec;
		stage1b->ssrt[i].read.address_lo = STAGE2_LOAD_ADDRESS +
			i * stage2_list[i].chs.size;
		stage1b->ssrt[i].read.flags = CCW_FLAG_CC | CCW_FLAG_SLI;
	}
	/* Terminate CCW chain */
	stage1b->ssrt[i - 1].read.flags &= ~CCW_FLAG_CC;
	return 0;
}

int
boot_get_tape_ipl(void** data, size_t* size, address_t parm_addr,
		  address_t initrd_addr, address_t image_addr)
{
	struct boot_tape_ipl_params params;
	void* buffer;

	if (image_addr != (image_addr & PSW32_ADDR_MASK)) {
		error_reason("Kernel image load address to high (31 bit "
			     "addressing mode)");
		return -1;
	}
	buffer = misc_malloc(DATA_SIZE(tape0));
	if (buffer == NULL)
		return -1;
	/* Prepare params section */
	params.parm_addr = (uint64_t) parm_addr;
	params.initrd_addr = (uint64_t) initrd_addr;
	params.load_psw = (uint64_t) (image_addr | PSW_LOAD);
	/* Initialize buffer */
	memcpy(buffer, DATA_ADDR(tape0), DATA_SIZE(tape0));
	memcpy(VOID_ADD(buffer, BOOT_TAPE_IPL_PARAMS_OFFSET), &params,
	       sizeof(struct boot_tape_ipl_params));
	*data = buffer;
	*size = DATA_SIZE(tape0);
	return 0;
}

struct menu_buffer {
	void *start;
	size_t size;
	size_t off;
};

static void
mb_init(struct menu_buffer *buffer, void *data, size_t size)
{
	buffer-> start = data;
	buffer->size = size;
	buffer->off = 0;
	memset(data, 0, size);
}


static void *
mb_alloc(struct menu_buffer *buffer, size_t len)
{
	void *result;

	if (buffer->off + len > buffer->size)
		return NULL;
	result = VOID_ADD(buffer->start, buffer->off);
	buffer->off += len;

	return result;
}


static void*
mb_sprintf(struct menu_buffer *buffer, const char *fmt, ...)
{
	va_list args;
	int size;
	int len;
	char *str;

	str = VOID_ADD(buffer->start, buffer->off);
	size = buffer->size - buffer->off;
	va_start(args, fmt);
	len = vsnprintf(str, size, fmt, args);
	va_end(args);
	if (len < 0 || len >= size)
		return NULL;
	misc_ascii_to_ebcdic((unsigned char *) str,
			     (unsigned char *) str + len);
	mb_alloc(buffer, len + 1);

	return str;
}


static int
store_stage2_menu(void* data, size_t size, struct job_data* job)
{
	struct boot_stage2_params* params;
	char* name;
	int flag;
	int timeout;
	int i;
	struct menu_buffer mb;
	void *str;
	uint64_t config_kdump = 0;

	mb_init(&mb, data, size);
	if (job->id == job_menu) {
		name = "-";
		for (i = 0; i < job->data.menu.num; i++) {
			if (job->data.menu.entry[i].id == job_ipl &&
			    job->data.menu.entry[i].data.ipl.is_kdump)
				/* we start with 2nd bit */
				config_kdump |= (0x1 << (i + 1));
			if (job->data.menu.entry[i].pos ==
			    job->data.menu.default_pos) {
				if (job->data.menu.entry[i].id == job_ipl &&
				    job->data.menu.entry[i].data.ipl.is_kdump)
					/* default entry is first bit */
					config_kdump |= 0x1;
				name = job->data.menu.entry[i].name;
			}
		}
		flag = (job->data.menu.prompt != 0);
		timeout = job->data.menu.timeout;
		/* Be verbose */
		if (verbose) {
			printf("Preparing boot menu\n");
			printf("  Interactive prompt......: %s\n",
			       job->data.menu.prompt ? "enabled" : "disabled");
			if (job->data.menu.timeout == 0)
				printf("  Menu timeout............: "
				       "disabled\n");
			else
				printf("  Menu timeout............: %d "
				       "seconds\n", job->data.menu.timeout);
			printf("  Default configuration...: '%s'\n", name);
		}
	} else {
		if (job->id == job_ipl && job->data.ipl.is_kdump)
			config_kdump |= 0x1;
		name = job->name;
		flag = 0;
		timeout = 0;
	}
	/* Header */
	params = mb_alloc(&mb, sizeof(struct boot_stage2_params));
	if (!params)
		goto err_nospace;
	params->flag = flag;
	params->config_kdump = config_kdump;
	params->timeout = timeout;
	/* Banner text */
	str = mb_sprintf(&mb, "zIPL v%s interactive boot menu\n ",
			   RELEASE_STRING);
	if (!str)
		goto err_nospace;
	params->banner = (uint16_t) ((unsigned long) str -
				     (unsigned long) data);
	/* Default config text */
	if (name != NULL)
		str = mb_sprintf(&mb, " 0. default (%s)", name);
	else
		str = mb_sprintf(&mb, " 0. default");
	if (!str)
		goto err_nospace;
	params->config[0] = (uint16_t) ((unsigned long) str -
					(unsigned long) data);
	/* Skip rest if job is not an actual menu */
	if (job->id != job_menu)
		return 0;
	/* Config texts */
	for (i = 0; i < job->data.menu.num; i++) {
		if (job->data.menu.entry[i].data.ipl.common.ignore) {
			params->config[job->data.menu.entry[i].pos] = 0;
			continue;
		}
		const char *kdump_str = "";
		if (job->data.menu.entry[i].data.ipl.is_kdump)
			kdump_str = " (kdump)";
		str = mb_sprintf(&mb, "%2d. %s%s",
				 job->data.menu.entry[i].pos,
				 job->data.menu.entry[i].name,
				 kdump_str);
		if (!str)
			goto err_nospace_user;
		params->config[job->data.menu.entry[i].pos] = (uint16_t)
			((unsigned long) str - (unsigned long) data);
	}
	return 0;

err_nospace:
	error_reason("Not enough room for menu data");
	return -1;
err_nospace_user:
	error_reason("Not enough room for menu data (try fewer sections or "
		     "shorter names)");
	return -1;
}



int
boot_get_fba_stage2(void** data, size_t* size, struct job_data* job)
{
	void* buffer;
	int rc;

	buffer = misc_malloc(STAGE2_MAX_SIZE);
	if (buffer == NULL)
		return -1;
	memcpy(buffer, DATA_ADDR(fba2), DATA_SIZE(fba2));
	rc = store_stage2_menu(VOID_ADD(buffer, DATA_SIZE(fba2)),
			       STAGE2_MAX_SIZE - DATA_SIZE(fba2),
			       job);
	if (rc) {
		free(buffer);
		return rc;
	}
	*data = buffer;
	*size = STAGE2_MAX_SIZE;
	return 0;
}


int
boot_get_eckd_stage2(void** data, size_t* size, struct job_data* job)
{
	void* buffer;
	int rc;

	buffer = misc_malloc(STAGE2_MAX_SIZE);
	if (buffer == NULL)
		return -1;
	memcpy(buffer, DATA_ADDR(eckd2), DATA_SIZE(eckd2));
	rc = store_stage2_menu(VOID_ADD(buffer, DATA_SIZE(eckd2)),
			       STAGE2_MAX_SIZE - DATA_SIZE(eckd2),
			       job);
	if (rc) {
		free(buffer);
		return rc;
	}
	*data = buffer;
	*size = STAGE2_MAX_SIZE;
	return 0;
}


int
boot_get_tape_dump(void **data, size_t *size,
		   const struct stage2dump_parm_tail *stage2dump_parms)
{
	void *buffer;
	size_t offset;

	buffer = misc_malloc(DATA_SIZE(tape2dump));
	if (buffer == NULL)
		return -1;
	memcpy(buffer, DATA_ADDR(tape2dump), DATA_SIZE(tape2dump));
	/* Write mem size to end of dump record */
	offset = DATA_SIZE(tape2dump) - sizeof(struct stage2dump_parm_tail);
	memcpy(VOID_ADD(buffer, offset), stage2dump_parms, sizeof(struct stage2dump_parm_tail));
	*data = buffer;
	*size = DATA_SIZE(tape2dump);
	return 0;
}


int
boot_get_eckd_dump_stage2(void **data, size_t *size,
			  const struct stage2dump_parm_tail *stage2dump_parms)
{
	void *buffer;
	size_t offset;

	buffer = misc_malloc(DATA_SIZE(eckd2dump_sv));
	if (buffer == NULL)
		return -1;
	memcpy(buffer, DATA_ADDR(eckd2dump_sv), DATA_SIZE(eckd2dump_sv));
	/* Write mem size and no-compression indicator to end of dump record */
	offset = DATA_SIZE(eckd2dump_sv) - sizeof(struct stage2dump_parm_tail);
	memcpy(VOID_ADD(buffer, offset), stage2dump_parms,
	       sizeof(struct stage2dump_parm_tail));
	*data = buffer;
	*size = DATA_SIZE(eckd2dump_sv);
	return 0;
}

int
boot_get_eckd_mvdump_stage2(void **data, size_t *size,
			    const struct stage2dump_parm_tail *stage2dump_parms,
			    const struct mvdump_parm_table *mv_parm_table)
{
	void *buffer;
	size_t offset;

	buffer = misc_malloc(DATA_SIZE(eckd2dump_mv));
	if (buffer == NULL)
		return -1;
	memcpy(buffer, DATA_ADDR(eckd2dump_mv), DATA_SIZE(eckd2dump_mv));
	/*
	 * Write stage2 dump parameters to end of dump record, right before
	 * 512-byte parameter table
	 */
	offset = DATA_SIZE(eckd2dump_mv) - sizeof(struct mvdump_parm_table);
	memcpy(VOID_ADD(buffer, offset), mv_parm_table,
	       sizeof(struct mvdump_parm_table));
	offset = offset - sizeof(struct stage2dump_parm_tail);
	memcpy(VOID_ADD(buffer, offset), stage2dump_parms,
	       sizeof(struct stage2dump_parm_tail));
	*data = buffer;
	*size = DATA_SIZE(eckd2dump_mv);
	return 0;
}


int
boot_get_fba_dump_stage2(void **data, size_t *size,
			 const struct stage2dump_parm_tail *stage2dump_parms)
{
	void *buffer;
	size_t offset;

	buffer = misc_malloc(DATA_SIZE(fba2dump));
	if (buffer == NULL)
		return -1;
	memcpy(buffer, DATA_ADDR(fba2dump), DATA_SIZE(fba2dump));
	/* Write mem size to end of dump record */
	offset = DATA_SIZE(fba2dump) - sizeof(struct stage2dump_parm_tail);
	memcpy(VOID_ADD(buffer, offset), stage2dump_parms,
	       sizeof(struct stage2dump_parm_tail));
	*data = buffer;
	*size = DATA_SIZE(fba2dump);
	return 0;
}

void
boot_get_dump_info(struct boot_info *boot_info, uint8_t dev_type, void *param)
{
	memset(boot_info, 0, sizeof(*boot_info));
	memcpy(&boot_info->magic, BOOT_INFO_MAGIC, sizeof(boot_info->magic));
	boot_info->flags |= BOOT_INFO_FLAGS_ARCH;
	boot_info->dev_type = dev_type;
	boot_info->bp_type = BOOT_INFO_BP_TYPE_DUMP;
	boot_info->version = BOOT_INFO_VERSION;
	memcpy(&boot_info->bp.dump.param, param,
	       sizeof(boot_info->bp.dump.param));
}

/*
 * Helper function to install a boot record for CCW-type IPL from DASD
 */
void
boot_get_ipl_info_ccw(struct boot_info *boot_info, uint8_t dev_type,
		      disk_blockptr_t *bm_ptr, struct disk_info *info)
{
	memset(boot_info, 0, sizeof(*boot_info));
	memcpy(&boot_info->magic, BOOT_INFO_MAGIC, sizeof(boot_info->magic));
	boot_info->flags |= BOOT_INFO_FLAGS_ARCH;
	boot_info->dev_type = dev_type;
	boot_info->bp_type = BOOT_INFO_BP_TYPE_IPL;
	boot_info->version = BOOT_INFO_VERSION;
	bootmap_store_blockptr(&boot_info->bp, bm_ptr, info,
			       LEGACY_BLKPTR_FORMAT_ID);
}

/**
 * Allocate and initialize a boot record for List-Directed IPL from DASD
 *
 * TABLE: address of an actual program table
 */
int boot_get_eckd_ld_ipl_br(void **br_ptr, size_t *size_ptr,
			    disk_blockptr_t *table, struct disk_info *info)
{
	struct eckd_boot_record *br;

	br = util_zalloc(info->phy_block_size);
	if (!br)
		return -1;

	memcpy(&br->magic, ZIPL_MAGIC, ZIPL_MAGIC_SIZE);
	br->version_id = DISK_LAYOUT_ID;
	bootmap_store_blockptr(&br->program_table_pointer, table, info,
			       BLKPTR_FORMAT_ID);
	br->os_id = 0x55aa; /* LINUX */
	*br_ptr = br;
	*size_ptr = info->phy_block_size;
	return 0;
}
