/*
  * Copyright (C) 1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013  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 <config.h>

#include <grub/util/install.h>
#include <grub/util/misc.h>
#include <grub/emu/config.h>

#include <string.h>

#pragma GCC diagnostic ignored "-Wmissing-prototypes"
#pragma GCC diagnostic ignored "-Wmissing-declarations"
#include <argp.h>
#pragma GCC diagnostic error "-Wmissing-prototypes"
#pragma GCC diagnostic error "-Wmissing-declarations"

static char *output_image;
static char **files;
static int nfiles;
const struct grub_install_image_target_desc *format;
static FILE *memdisk;

enum
  {
    OPTION_OUTPUT = 'o',
    OPTION_FORMAT = 'O'
  };

static struct argp_option options[] = {
  GRUB_INSTALL_OPTIONS,
  {"output", 'o', N_("FILE"),
   0, N_("save output in FILE [required]"), 2},
  {"format", 'O', N_("FILE"), 0, 0, 2},
  {"compression", 'C', "xz|none|auto", OPTION_HIDDEN, 0, 2},
  {0, 0, 0, 0, 0, 0}
};

static char *
help_filter (int key, const char *text, void *input __attribute__ ((unused)))
{
  switch (key)
    {
    case 'O':
      {
	char *formats = grub_install_get_image_targets_string (), *ret;
	ret = xasprintf ("%s\n%s %s", _("generate an image in FORMAT"),
			 _("available formats:"), formats);
	free (formats);
	return ret;
      }
    default:
      return grub_install_help_filter (key, text, input);
    }
}

static error_t
argp_parser (int key, char *arg, struct argp_state *state)
{
  if (key == 'C')
    key = GRUB_INSTALL_OPTIONS_INSTALL_CORE_COMPRESS;

  if (grub_install_parse (key, arg))
    return 0;

  switch (key)
    {

    case 'o':
      if (output_image)
	free (output_image);

      output_image = xstrdup (arg);
      break;

    case 'O':
      {
	format = grub_install_get_image_target (arg);
	if (!format)
	  {
	    printf (_("unknown target format %s\n"), arg);
	    argp_usage (state);
	    exit (1);
	  }
	break;
      }

    case ARGP_KEY_ARG:
      files[nfiles++] = xstrdup (arg);
      break;

    default:
      return ARGP_ERR_UNKNOWN;
    }
  return 0;
}

struct argp argp = {
  options, argp_parser, N_("[OPTION] SOURCE..."),
  N_("Generate a standalone image (containing all modules) in the selected format")"\v"N_("Graft point syntax (E.g. /boot/grub/grub.cfg=./grub.cfg) is accepted"), 
  NULL, help_filter, NULL
};

/* tar support */
#define MAGIC	"ustar"
struct head
{
  char name[100];
  char mode[8];
  char uid[8];
  char gid[8];
  char size[12];
  char mtime[12];
  char chksum[8];
  char typeflag;
  char linkname[100];
  char magic[6];
  char version[2];
  char uname[32];
  char gname[32];
  char devmajor[8];
  char devminor[8];
  char prefix[155];
  char pad[12];
} GRUB_PACKED;

static void
write_zeros (unsigned rsz)
{
  char buf[512];

  memset (buf, 0, 512);
  fwrite (buf, 1, rsz, memdisk);
}

static void
write_pad (unsigned sz)
{
  write_zeros ((~sz + 1) & 511);
}

static void
set_tar_value (char *field, grub_uint32_t val,
	       unsigned len)
{
  unsigned i;
  for (i = 0; i < len - 1; i++)
    field[len - 2 - i] = '0' + ((val >> (3 * i)) & 7);
}

static void
compute_checksum (struct head *hd)
{
  unsigned int chk = 0;
  unsigned char *ptr;
  memset (hd->chksum, ' ', 8);
  for (ptr = (unsigned char *) hd; ptr < (unsigned char *) (hd + 1); ptr++)
    chk += *ptr;
  set_tar_value (hd->chksum, chk, 8);
}

static void
add_tar_file (const char *from,
	      const char *to)
{
  char *tcn;
  const char *iptr;
  char *optr;
  struct head hd;
  grub_util_fd_t in;
  ssize_t r;
  grub_uint32_t mtime = 0;
  grub_uint32_t size;

  COMPILE_TIME_ASSERT (sizeof (hd) == 512);

  if (grub_util_is_special_file (from))
    return;

  mtime = grub_util_get_mtime (from);

  optr = tcn = xmalloc (strlen (to) + 1);
  for (iptr = to; *iptr == '/'; iptr++);
  for (; *iptr; iptr++)
    if (!(iptr[0] == '/' && iptr[1] == '/'))
      *optr++ = *iptr;
  *optr = '\0';

  if (grub_util_is_directory (from))
    {
      grub_util_fd_dir_t d;
      grub_util_fd_dirent_t de;

      d = grub_util_fd_opendir (from);

      while ((de = grub_util_fd_readdir (d)))
	{
	  char *fp, *tfp;
	  if (strcmp (de->d_name, ".") == 0)
	    continue;
	  if (strcmp (de->d_name, "..") == 0)
	    continue;
	  fp = grub_util_path_concat (2, from, de->d_name);
	  tfp = xasprintf ("%s/%s", to, de->d_name);
	  add_tar_file (fp, tfp);
	  free (fp);
	}
      grub_util_fd_closedir (d);
      free (tcn);
      return;
    }

  if (optr - tcn > 99)
    {
      memset (&hd, 0, sizeof (hd));
      memcpy (hd.name, tcn, 99);
      memcpy (hd.mode, "0000600", 7);
      memcpy (hd.uid, "0001750", 7);
      memcpy (hd.gid, "0001750", 7);

      set_tar_value (hd.size, optr - tcn, 12);
      set_tar_value (hd.mtime, mtime, 12);
      hd.typeflag = 'L';
      memcpy (hd.magic, MAGIC, sizeof (hd.magic));
      memcpy (hd.uname, "grub", 4);
      memcpy (hd.gname, "grub", 4);

      compute_checksum (&hd);

      fwrite (&hd, 1, sizeof (hd), memdisk);
      fwrite (tcn, 1, optr - tcn, memdisk);

      write_pad (optr - tcn);
    }

  in = grub_util_fd_open (from, GRUB_UTIL_FD_O_RDONLY);
  if (!GRUB_UTIL_FD_IS_VALID (in))
    grub_util_error (_("cannot open `%s': %s"), from, grub_util_fd_strerror ());

  if (!grub_install_copy_buffer)
    grub_install_copy_buffer = xmalloc (GRUB_INSTALL_COPY_BUFFER_SIZE);

  size = grub_util_get_fd_size (in, from, NULL);

  memset (&hd, 0, sizeof (hd));
  memcpy (hd.name, tcn, optr - tcn < 99 ? optr - tcn : 99);
  memcpy (hd.mode, "0000600", 7);
  memcpy (hd.uid, "0001750", 7);
  memcpy (hd.gid, "0001750", 7);

  set_tar_value (hd.size, size, 12);
  set_tar_value (hd.mtime, mtime, 12);
  hd.typeflag = '0';
  memcpy (hd.magic, MAGIC, sizeof (hd.magic));
  memcpy (hd.uname, "grub", 4);
  memcpy (hd.gname, "grub", 4);

  compute_checksum (&hd);

  fwrite (&hd, 1, sizeof (hd), memdisk);
 
  while (1)
    {
      r = grub_util_fd_read (in, grub_install_copy_buffer, GRUB_INSTALL_COPY_BUFFER_SIZE);
      if (r <= 0)
	break;
      fwrite (grub_install_copy_buffer, 1, r, memdisk);
    }
  grub_util_fd_close (in);

  write_pad (size);
  free (tcn);
}

int
main (int argc, char *argv[])
{
  const char *pkglibdir;
  int i;

  grub_util_host_init (&argc, &argv);
  grub_util_disable_fd_syncs ();

  files = xmalloc ((argc + 1) * sizeof (files[0]));

  argp_parse (&argp, argc, argv, 0, 0, 0);

  pkglibdir = grub_util_get_pkglibdir ();

  if (!output_image)
    grub_util_error ("%s", _("output file must be specified"));

  if (!format)
    grub_util_error ("%s", _("Target format not specified (use the -O option)."));

  if (!grub_install_source_directory)
    grub_install_source_directory = grub_util_path_concat (2, pkglibdir, grub_util_get_target_dirname (format));

  enum grub_install_plat plat = grub_install_get_target (grub_install_source_directory);

  char *memdisk_dir = grub_util_make_temporary_dir ();
  char *boot_grub = grub_util_path_concat (3, memdisk_dir, "boot", "grub");
  grub_install_copy_files (grub_install_source_directory,
			   boot_grub, plat);

  char *memdisk_img = grub_util_make_temporary_file ();

  memdisk = grub_util_fopen (memdisk_img, "wb");

  add_tar_file (memdisk_dir, "");
  for (i = 0; i < nfiles; i++)
    {
      char *eq = grub_strchr (files[i], '=');
      char *from, *to;
      if (!eq)
	{
	  from = files[i];
	  to = files[i];
	}
      else
	{
	  *eq = '\0';
	  to = files[i];
	  from = eq + 1;
	}
      while (*to == '/')
	to++;
      add_tar_file (from, to);
    }
  write_zeros (512);

  fclose (memdisk);

  grub_util_unlink_recursive (memdisk_dir);

  grub_install_push_module ("memdisk");
  grub_install_push_module ("tar");

  grub_install_make_image_wrap (grub_install_source_directory,
				"(memdisk)/boot/grub", output_image,
				memdisk_img, NULL,
				grub_util_get_target_name (format), 0);

  grub_util_unlink (memdisk_img);
  return 0;
}