/*
 * libefiboot - library for the manipulation of EFI boot variables
 * Copyright 2012-2015 Red Hat, Inc.
 * Copyright (C) 2001 Dell Computer Corporation <Matt_Domsch@dell.com>
 *
 * This library 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; either version 2.1 of the
 * License, or (at your option) any later version.
 *
 * This library 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 library; if not, see
 * <http://www.gnu.org/licenses/>.
 *
 */

#include <efivar/efivar.h>
#include <stddef.h>

#include "dp.h"
#include "include/efivar/efiboot-loadopt.h"

typedef struct efi_load_option_s {
	uint32_t attributes;
	uint16_t file_path_list_length;
	uint16_t description[];
	// uint8_t file_path_list[];
	// uint8_t optional_data[];
}
__attribute__((packed))
efi_load_option;

ssize_t
__attribute__((__nonnull__ (6)))
__attribute__((__visibility__ ("default")))
efi_loadopt_create(uint8_t *buf, ssize_t size, uint32_t attributes,
		   efidp dp, ssize_t dp_size, unsigned char *description,
		   uint8_t *optional_data, size_t optional_data_size)
{
	ssize_t desc_len = utf8len((uint8_t *)description, 1024) * 2 + 2;
	ssize_t sz = sizeof (attributes)
		     + sizeof (uint16_t) + desc_len
		     + dp_size + optional_data_size;

	if (size == 0)
		return sz;

	if (size < sz) {
		errno = ENOSPC;
		return -1;
	}

	if (!buf) {
invalid:
		errno = EINVAL;
		return -1;
	}

	if (!optional_data && optional_data_size != 0)
		goto invalid;

	if ((!dp && dp_size == 0) || dp_size < 0)
		goto invalid;

	if (dp) {
		if (!efidp_is_valid(dp, dp_size))
			goto invalid;

		if (efidp_size(dp) != dp_size)
			goto invalid;
	}

	if (buf) {
		uint8_t *pos = buf;
		*(uint32_t *)pos = attributes;
		pos += sizeof (attributes);

		*(uint16_t *)pos = dp_size;
		pos += sizeof (uint16_t);

		utf8_to_ucs2((uint16_t *)pos, desc_len, 1,
			     (uint8_t *)description);
		pos += desc_len;

		if (dp)
			memcpy(pos, dp, dp_size);
		pos += dp_size;

		if (optional_data && optional_data_size > 0)
			memcpy(pos, optional_data, optional_data_size);
	}

	return sz;
}

ssize_t
__attribute__((__nonnull__ (1)))
__attribute__((__visibility__ ("default")))
efi_loadopt_optional_data_size(efi_load_option *opt, size_t size)
{
	ssize_t sz;
	ssize_t ret;
	uint8_t *p;

	ret = size;
	if (ret < (ssize_t)sizeof(*opt)) {
		efi_error("load option size is too small for header (%zd/%zd)",
			  ret, sizeof(*opt));
		return -1;
	}
	ret -= sizeof(*opt);
	if (ret < opt->file_path_list_length) {
		efi_error("load option size is too small for path (%zd/%d)",
			  size, opt->file_path_list_length);
		return -1;
	}
	ret -= opt->file_path_list_length;
	if (ret < 0) {
		efi_error("leftover size is negative (%zd)", ret);
		return -1;
	}
	/* "size" as the limit means sz will be size or less in all cases; no
	 * need to test it.  if it /is/ size, there's no optional data. */
	sz = ucs2size(opt->description, ret);
	p = (uint8_t *)(opt->description) + sz;
	ret -= sz;
	if (ret < 0) {
		efi_error("leftover size is negative (%zd)", ret);
		return -1;
	}

	if (!efidp_is_valid((const_efidp)p, opt->file_path_list_length)) {
		efi_error("efi device path is not valid");
		return -1;
	}
	sz = efidp_size((const_efidp)p);
	if (sz != opt->file_path_list_length) {
		efi_error("size does not match file path size (%zd/%d)",
			  sz, opt->file_path_list_length);
		return -1;
	}

	return ret;
}

int
__attribute__((__nonnull__ (1)))
__attribute__((__visibility__ ("default")))
efi_loadopt_is_valid(efi_load_option *opt, size_t size)
{
	ssize_t rc;

	rc = efi_loadopt_optional_data_size(opt, size);
	return (rc >= 0);
}

uint32_t
__attribute__((__nonnull__ (1)))
__attribute__((__visibility__ ("default")))
efi_loadopt_attrs(efi_load_option *opt)
{
	return opt->attributes;
}

void
__attribute__((__nonnull__ (1)))
__attribute__((__visibility__ ("default")))
efi_loadopt_attr_set(efi_load_option *opt, uint16_t attr)
{
	opt->attributes |= attr;
}

void
__attribute__((__nonnull__ (1)))
__attribute__((__visibility__ ("default")))
efi_loadopt_attr_clear(efi_load_option *opt, uint16_t attr)
{
	opt->attributes &= ~attr;
}

uint16_t
__attribute__((__nonnull__ (1)))
__attribute__((__visibility__ ("default")))
/* limit here is the /whole/ load option */
efi_loadopt_pathlen(efi_load_option *opt, ssize_t limit)
{
	uint16_t len = opt->file_path_list_length;
	if (limit >= 0) {
		if (len > limit)
			return 0;
		if (limit - offsetof(efi_load_option, file_path_list_length) < len)
			return 0;
	}
	return len;
}

efidp
__attribute__((__nonnull__ (1)))
__attribute__((__visibility__ ("default")))
/* limit here is the /whole/ load option */
efi_loadopt_path(efi_load_option *opt, ssize_t limit)
{
	char *p = (char *)opt;
	size_t sz;
	size_t left;
	efidp dp;

	left = (size_t)limit;
	if (left <= offsetof(efi_load_option, description))
		return NULL;
	left -= offsetof(efi_load_option, description);
	p += offsetof(efi_load_option, description);

	sz = ucs2size(opt->description, left);
	if (sz >= left) // since there's no room for a file path...
		return NULL;
	p += sz;
	left -= sz;

	if (left < opt->file_path_list_length)
		return NULL;

	dp = (efidp)p;
	if (!efidp_is_valid(dp, opt->file_path_list_length))
		return NULL;
	return dp;
}

int
__attribute__((__nonnull__ (1,3)))
__attribute__((__visibility__ ("default")))
efi_loadopt_optional_data(efi_load_option *opt, size_t opt_size,
			  unsigned char **datap, size_t *len)
{
	unsigned char *p = (unsigned char *)opt;
	size_t l = sizeof (opt->attributes)
		    + sizeof (opt->file_path_list_length);
	size_t ul;

	if (l > opt_size) {
over_limit:
		*len = 0;
		errno = EINVAL;
		return -1;
	}

	ul = ucs2size(opt->description, opt_size - l);
	if (opt->file_path_list_length > opt_size)
		goto over_limit;
	if (ul > opt_size)
		goto over_limit;
	if (opt_size - ul < opt->file_path_list_length)
		goto over_limit;
	l += ul + opt->file_path_list_length;
	if (l > opt_size)
		goto over_limit;

	*datap = (unsigned char *)(p + l);
	if (len && opt_size > 0)
		*len = (p+opt_size) - *datap;
	return 0;
}

ssize_t
__attribute__((__nonnull__ (3)))
__attribute__((__visibility__ ("default")))
efi_loadopt_args_from_file(uint8_t *buf, ssize_t size, char *filename)
{
	int rc;
	ssize_t ret = -1;
	struct stat statbuf = { 0, };
	int saved_errno;
	FILE *f;

	if (!buf && size != 0) {
		errno = -EINVAL;
		return -1;
	}

	f = fopen(filename, "r");
	if (!f)
		return -1;

	rc = fstat(fileno(f), &statbuf);
	if (rc < 0)
		goto err;

	if (size == 0) {
		fclose(f);
		return statbuf.st_size;
	}

	if (size < statbuf.st_size) {
		errno = ENOSPC;
		goto err;
	}

	ret = fread(buf, 1, statbuf.st_size, f);
	if (ret < statbuf.st_size)
		ret = -1;
err:
	saved_errno = errno;
	if (f)
		fclose(f);
	errno = saved_errno;
	return ret;
}

ssize_t
__attribute__((__nonnull__ (3)))
__attribute__((__visibility__ ("default")))
efi_loadopt_args_as_utf8(uint8_t *buf, ssize_t size, uint8_t *utf8)
{
	ssize_t req;
	if (!buf && size != 0) {
		errno = EINVAL;
		return -1;
	}

	/* we specifically want the storage size without NUL here,
	 * not the size with NUL or the number of utf8 characters */
	req = strlen((char *)utf8);

	if (size == 0)
		return req;

	if (size < req) {
		errno = ENOSPC;
		return -1;
	}

	memcpy(buf, utf8, req);
	return req;
}

ssize_t
__attribute__((__nonnull__ (3)))
__attribute__((__visibility__ ("default")))
efi_loadopt_args_as_ucs2(uint16_t *buf, ssize_t size, uint8_t *utf8)
{
	ssize_t req;
	if (!buf && size > 0) {
		errno = EINVAL;
		return -1;
	}

	req = utf8len(utf8, -1) * sizeof(uint16_t);
	if (size == 0)
		return req;

	if (size < req) {
		errno = ENOSPC;
		return -1;
	}

	return utf8_to_ucs2(buf, size, 0, utf8);
}

static unsigned char *last_desc;

static void
__attribute__((destructor))
teardown(void)
{
	if (last_desc)
		free(last_desc);
	last_desc = NULL;
}

__attribute__((__nonnull__ (1)))
__attribute__((__visibility__ ("default")))
const unsigned char *
efi_loadopt_desc(efi_load_option *opt, ssize_t limit)
{
	if (last_desc) {
		free(last_desc);
		last_desc = NULL;
	}

	last_desc = ucs2_to_utf8(opt->description, limit);
	return last_desc;
}
