/* Export pnvram and some variables for runtime */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2009  Free Software Foundation, Inc.
 *
 *  GRUB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/file.h>
#include <grub/err.h>
#include <grub/normal.h>
#include <grub/mm.h>
#include <grub/misc.h>
#include <grub/efiemu/efiemu.h>
#include <grub/efiemu/runtime.h>
#include <grub/extcmd.h>

/* Place for final location of variables */
static int nvram_handle = 0;
static int nvramsize_handle = 0;
static int high_monotonic_count_handle = 0;
static int timezone_handle = 0;
static int accuracy_handle = 0;
static int daylight_handle = 0;

/* Temporary place */
static grub_uint8_t *nvram;
static grub_size_t nvramsize;
static grub_uint32_t high_monotonic_count;
static grub_int16_t timezone;
static grub_uint8_t daylight;
static grub_uint32_t accuracy;

static const struct grub_arg_option options[] = {
  {"size", 's', 0, "number of bytes to reserve for pseudo NVRAM", 0,
   ARG_TYPE_INT},
  {"high-monotonic-count", 'm', 0, 
   "Initial value of high monotonic count", 0, ARG_TYPE_INT},
  {"timezone", 't', 0, 
   "Timezone, offset in minutes from GMT", 0, ARG_TYPE_INT},
  {"accuracy", 'a', 0, 
   "Accuracy of clock, in 1e-12 units", 0, ARG_TYPE_INT},
  {"daylight", 'd', 0, 
   "Daylight value, as per EFI specifications", 0, ARG_TYPE_INT},
  {0, 0, 0, 0, 0, 0}
};

/* Parse signed value */
static int
grub_strtosl (char *arg, char **end, int base)
{
  if (arg[0] == '-')
    return -grub_strtoul (arg + 1, end, base);
  return grub_strtoul (arg, end, base);
}

/* Export stuff for efiemu */
static grub_err_t
nvram_set (void * data __attribute__ ((unused)))
{      
  /* Take definitive pointers */
  grub_uint8_t *nvram_def = grub_efiemu_mm_obtain_request (nvram_handle);
  grub_uint32_t *nvramsize_def 
    = grub_efiemu_mm_obtain_request (nvramsize_handle);
  grub_uint32_t *high_monotonic_count_def
    = grub_efiemu_mm_obtain_request (high_monotonic_count_handle);
  grub_int16_t *timezone_def 
    = grub_efiemu_mm_obtain_request (timezone_handle);
  grub_uint8_t *daylight_def
    = grub_efiemu_mm_obtain_request (daylight_handle);
  grub_uint32_t *accuracy_def
    = grub_efiemu_mm_obtain_request (accuracy_handle);

  /* Copy to definitive loaction */
  grub_dprintf ("efiemu", "preparing pnvram\n");
  grub_memcpy (nvram_def, nvram, nvramsize);
  *nvramsize_def = nvramsize;
  *high_monotonic_count_def = high_monotonic_count;
  *timezone_def = timezone;
  *daylight_def = daylight;
  *accuracy_def = accuracy;

  /* Register symbols */
  grub_efiemu_register_symbol ("efiemu_variables", nvram_handle, 0);
  grub_efiemu_register_symbol ("efiemu_varsize", nvramsize_handle, 0);
  grub_efiemu_register_symbol ("efiemu_high_monotonic_count", 
			       high_monotonic_count_handle, 0);
  grub_efiemu_register_symbol ("efiemu_time_zone", timezone_handle, 0);
  grub_efiemu_register_symbol ("efiemu_time_daylight", daylight_handle, 0);
  grub_efiemu_register_symbol ("efiemu_time_accuracy", 
			       accuracy_handle, 0);

  return GRUB_ERR_NONE;
}

static void
nvram_unload (void * data __attribute__ ((unused)))
{
  grub_efiemu_mm_return_request (nvram_handle);
  grub_efiemu_mm_return_request (nvramsize_handle);
  grub_efiemu_mm_return_request (high_monotonic_count_handle);
  grub_efiemu_mm_return_request (timezone_handle);
  grub_efiemu_mm_return_request (accuracy_handle);
  grub_efiemu_mm_return_request (daylight_handle);

  grub_free (nvram);
  nvram = 0;
}

/* Load the variables file It's in format
   guid1:attr1:name1:data1;
   guid2:attr2:name2:data2;
   ...
   Where all fields are in hex
*/
static grub_err_t
read_pnvram (char *filename)
{
  char *buf, *ptr, *ptr2;
  grub_file_t file;
  grub_size_t size;
  grub_uint8_t *nvramptr = nvram;
  struct efi_variable *efivar;
  grub_size_t guidlen, datalen;
  unsigned i, j;
  
  file = grub_file_open (filename);
  if (!file)
    return grub_error (GRUB_ERR_BAD_OS, "couldn't read pnvram");
  size = grub_file_size (file);
  buf = grub_malloc (size + 1);
  if (!buf)
    return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't read pnvram");
  if (grub_file_read (file, buf, size) != (grub_ssize_t) size)
    return grub_error (GRUB_ERR_BAD_OS, "couldn't read pnvram");
  buf[size] = 0;
  grub_file_close (file);

  for (ptr = buf; *ptr; )
    {
      if (grub_isspace (*ptr))
	{
	  ptr++;
	  continue;
	}

      efivar = (struct efi_variable *) nvramptr;
      if (nvramptr - nvram + sizeof (struct efi_variable) > nvramsize)
	return grub_error (GRUB_ERR_OUT_OF_MEMORY, 
			   "file is too large for reserved variable space");

      nvramptr += sizeof (struct efi_variable);

      /* look ahow long guid field is*/
      guidlen = 0;      
      for (ptr2 = ptr; (grub_isspace (*ptr2) 
			|| (*ptr2 >= '0' && *ptr2 <= '9')
			|| (*ptr2 >= 'a' && *ptr2 <= 'f')
			|| (*ptr2 >= 'A' && *ptr2 <= 'F')); 
	   ptr2++)
	if (!grub_isspace (*ptr2))
	  guidlen++;
      guidlen /= 2;

      /* Read guid */
      if (guidlen != sizeof (efivar->guid))
	{   
	  grub_free (buf);
	  return grub_error (GRUB_ERR_BAD_OS, "can't parse %s", filename);
	}
      for (i = 0; i < 2 * sizeof (efivar->guid); i++)
	{
	  int hex = 0;
	  while (grub_isspace (*ptr))
	    ptr++;
	  if (*ptr >= '0' && *ptr <= '9')
	    hex = *ptr - '0';
	  if (*ptr >= 'a' && *ptr <= 'f')
	    hex = *ptr - 'a' + 10;
	  if (*ptr >= 'A' && *ptr <= 'F')
	    hex = *ptr - 'A' + 10;
	  
	  if (i%2 == 0)
	    ((grub_uint8_t *)&(efivar->guid))[i/2] = hex << 4;
	  else
	    ((grub_uint8_t *)&(efivar->guid))[i/2] |= hex;
	  ptr++;
	}
      
      while (grub_isspace (*ptr))
	ptr++;
      if (*ptr != ':')
	{
	  grub_dprintf ("efiemu", "Not colon\n");
	  grub_free (buf);
	  return grub_error (GRUB_ERR_BAD_OS, "can't parse %s", filename);
	}
      ptr++;
      while (grub_isspace (*ptr))
	ptr++;

      /* Attributes can be just parsed by existing functions */
      efivar->attributes = grub_strtoul (ptr, &ptr, 16);

      while (grub_isspace (*ptr))
	ptr++;
      if (*ptr != ':')
	{
	  grub_dprintf ("efiemu", "Not colon\n");
	  grub_free (buf);
	  return grub_error (GRUB_ERR_BAD_OS, "can't parse %s", filename);
	}
      ptr++;
      while (grub_isspace (*ptr))
	ptr++;

      /* Read name and value */
      for (j = 0; j < 2; j++)
	{
	  /* Look the length */
	  datalen = 0;
	  for (ptr2 = ptr; *ptr2 && (grub_isspace (*ptr2) 
				     || (*ptr2 >= '0' && *ptr2 <= '9')
				     || (*ptr2 >= 'a' && *ptr2 <= 'f')
				     || (*ptr2 >= 'A' && *ptr2 <= 'F')); 
	       ptr2++)
	    if (!grub_isspace (*ptr2))
	      datalen++;
	  datalen /= 2;
	 
	  if (nvramptr - nvram + datalen > nvramsize)
	    {
	      grub_free (buf);
	      return grub_error (GRUB_ERR_OUT_OF_MEMORY, 
				 "file is too large for reserved "
				 " variable space");
	    }
	  
	  for (i = 0; i < 2 * datalen; i++)
	    {
	      int hex = 0;
	      while (grub_isspace (*ptr))
		ptr++;
	      if (*ptr >= '0' && *ptr <= '9')
		hex = *ptr - '0';
	      if (*ptr >= 'a' && *ptr <= 'f')
		hex = *ptr - 'a' + 10;
	      if (*ptr >= 'A' && *ptr <= 'F')
		hex = *ptr - 'A' + 10;
	      
	      if (i%2 == 0)
		nvramptr[i/2] = hex << 4;
	      else
		nvramptr[i/2] |= hex;
	      ptr++;
	    }
	  nvramptr += datalen;
	  while (grub_isspace (*ptr))
	    ptr++;
	  if (*ptr != (j ? ';' : ':'))
	    {
	      grub_free (buf);
	      grub_dprintf ("efiemu", j?"Not semicolon\n":"Not colon\n");
	      return grub_error (GRUB_ERR_BAD_OS, "can't parse %s", filename);
	    }
	  if (j)
	    efivar->size = datalen;
	  else
	    efivar->namelen = datalen;
	  
	  ptr++;
	}
    }
  grub_free (buf);
  return GRUB_ERR_NONE;
}

static grub_err_t
grub_efiemu_make_nvram (void)
{
  grub_err_t err;

  err = grub_efiemu_autocore ();
  if (err)
    {
      grub_free (nvram);
      return err;
    }

  err = grub_efiemu_register_prepare_hook (nvram_set, nvram_unload, 0);
  if (err)
    {
      grub_free (nvram);
      return err;
    }
  nvram_handle 
    = grub_efiemu_request_memalign (1, nvramsize, 
				    GRUB_EFI_RUNTIME_SERVICES_DATA);
  nvramsize_handle 
    = grub_efiemu_request_memalign (1, sizeof (grub_uint32_t), 
				    GRUB_EFI_RUNTIME_SERVICES_DATA);
  high_monotonic_count_handle
    = grub_efiemu_request_memalign (1, sizeof (grub_uint32_t), 
				    GRUB_EFI_RUNTIME_SERVICES_DATA);
  timezone_handle
    = grub_efiemu_request_memalign (1, sizeof (grub_uint16_t), 
				    GRUB_EFI_RUNTIME_SERVICES_DATA);
  daylight_handle
    = grub_efiemu_request_memalign (1, sizeof (grub_uint8_t), 
				    GRUB_EFI_RUNTIME_SERVICES_DATA);
  accuracy_handle
    = grub_efiemu_request_memalign (1, sizeof (grub_uint32_t), 
				    GRUB_EFI_RUNTIME_SERVICES_DATA);

  grub_efiemu_request_symbols (6);
  return GRUB_ERR_NONE;
}

grub_err_t
grub_efiemu_pnvram (void)
{
  if (nvram)
    return GRUB_ERR_NONE;

  nvramsize = 2048;
  high_monotonic_count = 1;
  timezone = GRUB_EFI_UNSPECIFIED_TIMEZONE;
  accuracy = 50000000;
  daylight = 0;

  nvram = grub_malloc (nvramsize);
  if (!nvram)
    return grub_error (GRUB_ERR_OUT_OF_MEMORY, 
		       "Couldn't allocate space for temporary pnvram storage");
  grub_memset (nvram, 0, nvramsize);

  return grub_efiemu_make_nvram ();
}

static grub_err_t
grub_cmd_efiemu_pnvram (struct grub_extcmd *cmd,
			int argc, char **args)
{
  struct grub_arg_list *state = cmd->state;
  grub_err_t err;

  if (argc > 1)
    return grub_error (GRUB_ERR_BAD_ARGUMENT, "only one argument expected");

  nvramsize = state[0].set ? grub_strtoul (state[0].arg, 0, 0) : 2048;
  high_monotonic_count = state[1].set ? grub_strtoul (state[1].arg, 0, 0) : 1;
  timezone = state[2].set ? grub_strtosl (state[2].arg, 0, 0) 
    : GRUB_EFI_UNSPECIFIED_TIMEZONE;
  accuracy = state[3].set ? grub_strtoul (state[3].arg, 0, 0) : 50000000;
  daylight = state[4].set ? grub_strtoul (state[4].arg, 0, 0) : 0;
  
  nvram = grub_malloc (nvramsize);
  if (!nvram)
    return grub_error (GRUB_ERR_OUT_OF_MEMORY, 
		       "Couldn't allocate space for temporary pnvram storage");
  grub_memset (nvram, 0, nvramsize);  

  if (argc == 1 && (err = read_pnvram (args[0])))
    {
      grub_free (nvram);
      return err;
    }
  return grub_efiemu_make_nvram ();
}

static grub_extcmd_t cmd;

void grub_efiemu_pnvram_cmd_register (void);
void grub_efiemu_pnvram_cmd_unregister (void);

void
grub_efiemu_pnvram_cmd_register (void)
{
  cmd = grub_register_extcmd ("efiemu_pnvram", grub_cmd_efiemu_pnvram, 
			      GRUB_COMMAND_FLAG_BOTH,
			      "efiemu_pnvram [FILENAME]",
			      "Initialise pseudo-NVRAM and load variables "
			      "from FILE", 
			      options);
}

void
grub_efiemu_pnvram_cmd_unregister (void)
{
  grub_unregister_extcmd (cmd);
}