/* engine-gpgconf.c - gpg-conf engine.
   Copyright (C) 2000 Werner Koch (dd9jn)
   Copyright (C) 2001, 2002, 2003, 2004, 2005, 2007, 2008,
                 2013 g10 Code GmbH

   This file is part of GPGME.

   GPGME 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.

   GPGME 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 program; if not, see <http://www.gnu.org/licenses/>.
 */

#if HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdlib.h>
#include <string.h>
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#include <assert.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
#include <fcntl.h> /* FIXME */
#include <errno.h>

#include "gpgme.h"
#include "util.h"
#include "ops.h"
#include "wait.h"
#include "priv-io.h"
#include "sema.h"

#include "assuan.h"
#include "debug.h"

#include "engine-backend.h"


struct engine_gpgconf
{
  char *file_name;
  char *home_dir;
};

typedef struct engine_gpgconf *engine_gpgconf_t;


static char *
gpgconf_get_version (const char *file_name)
{
  return _gpgme_get_program_version (file_name ? file_name
				     : _gpgme_get_default_gpgconf_name ());
}


static const char *
gpgconf_get_req_version (void)
{
  return "2.0.4";
}


static void
gpgconf_release (void *engine)
{
  engine_gpgconf_t gpgconf = engine;

  if (!gpgconf)
    return;

  if (gpgconf->file_name)
    free (gpgconf->file_name);
  if (gpgconf->home_dir)
    free (gpgconf->home_dir);

  free (gpgconf);
}


static gpgme_error_t
gpgconf_new (void **engine, const char *file_name, const char *home_dir,
             const char *version)
{
  gpgme_error_t err = 0;
  engine_gpgconf_t gpgconf;

  (void)version; /* Not yet used.  */

  gpgconf = calloc (1, sizeof *gpgconf);
  if (!gpgconf)
    return gpg_error_from_syserror ();

  gpgconf->file_name = strdup (file_name ? file_name
			       : _gpgme_get_default_gpgconf_name ());
  if (!gpgconf->file_name)
    err = gpg_error_from_syserror ();

  if (!err && home_dir)
    {
      gpgconf->home_dir = strdup (home_dir);
      if (!gpgconf->home_dir)
	err = gpg_error_from_syserror ();
    }

  if (err)
    gpgconf_release (gpgconf);
  else
    *engine = gpgconf;

  return err;
}


static void
release_arg (gpgme_conf_arg_t arg, gpgme_conf_type_t alt_type)
{
  while (arg)
    {
      gpgme_conf_arg_t next = arg->next;

      if (alt_type == GPGME_CONF_STRING)
	free (arg->value.string);
      free (arg);
      arg = next;
    }
}


static void
release_opt (gpgme_conf_opt_t opt)
{
  if (opt->name)
    free (opt->name);
  if (opt->description)
    free (opt->description);
  if (opt->argname)
    free (opt->argname);

  release_arg (opt->default_value, opt->alt_type);
  if (opt->default_description)
    free (opt->default_description);

  release_arg (opt->no_arg_value, opt->alt_type);
  release_arg (opt->value, opt->alt_type);
  release_arg (opt->new_value, opt->alt_type);

  free (opt);
}


static void
release_comp (gpgme_conf_comp_t comp)
{
  gpgme_conf_opt_t opt;

  if (comp->name)
    free (comp->name);
  if (comp->description)
    free (comp->description);
  if (comp->program_name)
    free (comp->program_name);

  opt = comp->options;
  while (opt)
    {
      gpgme_conf_opt_t next = opt->next;
      release_opt (opt);
      opt = next;
    }

  free (comp);
}


static void
gpgconf_config_release (gpgme_conf_comp_t conf)
{
  while (conf)
    {
      gpgme_conf_comp_t next = conf->next;
      release_comp (conf);
      conf = next;
    }
}

/* Read from gpgconf and pass line after line to the hook function.
   We put a limit of 64 k on the maximum size for a line.  This should
   allow for quite a long "group" line, which is usually the longest
   line (mine is currently ~3k).  */
static gpgme_error_t
gpgconf_read (void *engine, char *arg1, char *arg2,
	      gpgme_error_t (*cb) (void *hook, char *line),
	      void *hook)
{
  struct engine_gpgconf *gpgconf = engine;
  gpgme_error_t err = 0;
  char *linebuf;
  size_t linebufsize;
  int linelen;
  char *argv[4] = { NULL /* file_name */, NULL, NULL, NULL };
  int rp[2];
  struct spawn_fd_item_s cfd[] = { {-1, 1 /* STDOUT_FILENO */, -1, 0},
				   {-1, -1} };
  int status;
  int nread;
  char *mark = NULL;

  argv[1] = arg1;
  argv[2] = arg2;


  /* FIXME: Deal with engine->home_dir.  */

  /* _gpgme_engine_new guarantees that this is not NULL.  */
  argv[0] = gpgconf->file_name;

  if (_gpgme_io_pipe (rp, 1) < 0)
    return gpg_error_from_syserror ();

  cfd[0].fd = rp[1];

  status = _gpgme_io_spawn (gpgconf->file_name, argv,
                            IOSPAWN_FLAG_DETACHED, cfd, NULL, NULL, NULL);
  if (status < 0)
    {
      _gpgme_io_close (rp[0]);
      _gpgme_io_close (rp[1]);
      return gpg_error_from_syserror ();
    }

  linebufsize = 1024; /* Usually enough for conf lines.  */
  linebuf = malloc (linebufsize);
  if (!linebuf)
    {
      err = gpg_error_from_syserror ();
      goto leave;
    }
  linelen = 0;

  while ((nread = _gpgme_io_read (rp[0], linebuf + linelen,
                                  linebufsize - linelen - 1)))
    {
      char *line;
      const char *lastmark = NULL;
      size_t nused;

      if (nread < 0)
        {
          err = gpg_error_from_syserror ();
          goto leave;
        }

      linelen += nread;
      linebuf[linelen] = '\0';

      for (line=linebuf; (mark = strchr (line, '\n')); line = mark+1 )
        {
          lastmark = mark;
          if (mark > line && mark[-1] == '\r')
            mark[-1] = '\0';
          else
            mark[0] = '\0';

          /* Got a full line.  Due to the CR removal code (which
             occurs only on Windows) we might be one-off and thus
             would see empty lines.  Don't pass them to the
             callback. */
          err = *line? (*cb) (hook, line) : 0;
          if (err)
            goto leave;
        }

      nused = lastmark? (lastmark + 1 - linebuf) : 0;
      memmove (linebuf, linebuf + nused, linelen - nused);
      linelen -= nused;

      if (!(linelen < linebufsize - 1))
        {
          char *newlinebuf;

          if (linelen <  8 * 1024 - 1)
            linebufsize = 8 * 1024;
          else if (linelen < 64 * 1024 - 1)
            linebufsize = 64 * 1024;
          else
            {
              /* We reached our limit - give up.  */
              err = gpg_error (GPG_ERR_LINE_TOO_LONG);
              goto leave;
            }

          newlinebuf = realloc (linebuf, linebufsize);
          if (!newlinebuf)
            {
              err = gpg_error_from_syserror ();
              goto leave;
            }
          linebuf = newlinebuf;
        }
    }

 leave:
  free (linebuf);
  _gpgme_io_close (rp[0]);
  return err;
}


static gpgme_error_t
gpgconf_config_load_cb (void *hook, char *line)
{
  gpgme_conf_comp_t *comp_p = hook;
  gpgme_conf_comp_t comp = *comp_p;
#define NR_FIELDS 16
  char *field[NR_FIELDS];
  int fields = 0;

  while (line && fields < NR_FIELDS)
    {
      field[fields++] = line;
      line = strchr (line, ':');
      if (line)
	*(line++) = '\0';
    }

  /* We require at least the first 3 fields.  */
  if (fields < 2)
    return trace_gpg_error (GPG_ERR_INV_ENGINE);

  /* Find the pointer to the new component in the list.  */
  while (comp && comp->next)
    comp = comp->next;
  if (comp)
    comp_p = &comp->next;

  comp = calloc (1, sizeof (*comp));
  if (!comp)
    return gpg_error_from_syserror ();
  /* Prepare return value.  */
  comp->_last_opt_p = &comp->options;
  *comp_p = comp;

  comp->name = strdup (field[0]);
  if (!comp->name)
    return gpg_error_from_syserror ();

  comp->description = strdup (field[1]);
  if (!comp->description)
    return gpg_error_from_syserror ();

  if (fields >= 3)
    {
      comp->program_name = strdup (field[2]);
      if (!comp->program_name)
	return gpg_error_from_syserror ();
    }

  return 0;
}


static gpgme_error_t
gpgconf_parse_option (gpgme_conf_opt_t opt,
		      gpgme_conf_arg_t *arg_p, char *line)
{
  gpgme_error_t err;
  char *mark;

  if (!line[0])
    return 0;

  while (line)
    {
      gpgme_conf_arg_t arg;

      mark = strchr (line, ',');
      if (mark)
	*mark = '\0';

      arg = calloc (1, sizeof (*arg));
      if (!arg)
	return gpg_error_from_syserror ();
      *arg_p = arg;
      arg_p = &arg->next;

      if (*line == '\0')
	arg->no_arg = 1;
      else
	{
	  switch (opt->alt_type)
	    {
	      /* arg->value.count is an alias for arg->value.uint32.  */
	    case GPGME_CONF_NONE:
	    case GPGME_CONF_UINT32:
	      arg->value.uint32 = strtoul (line, NULL, 0);
	      break;

	    case GPGME_CONF_INT32:
	      arg->value.uint32 = strtol (line, NULL, 0);
	      break;

	    case GPGME_CONF_STRING:
              /* The complex types below are only here to silent the
                 compiler warning. */
            case GPGME_CONF_FILENAME:
            case GPGME_CONF_LDAP_SERVER:
            case GPGME_CONF_KEY_FPR:
            case GPGME_CONF_PUB_KEY:
            case GPGME_CONF_SEC_KEY:
            case GPGME_CONF_ALIAS_LIST:
	      /* Skip quote character.  */
	      line++;

	      err = _gpgme_decode_percent_string (line, &arg->value.string,
						  0, 0);
	      if (err)
		return err;
	      break;
	    }
	}

      /* Find beginning of next value.  */
      if (mark++ && *mark)
	line = mark;
      else
	line = NULL;
    }

  return 0;
}


static gpgme_error_t
gpgconf_config_load_cb2 (void *hook, char *line)
{
  gpgme_error_t err;
  gpgme_conf_comp_t comp = hook;
  gpgme_conf_opt_t *opt_p = comp->_last_opt_p;
  gpgme_conf_opt_t opt;
#define NR_FIELDS 16
  char *field[NR_FIELDS];
  int fields = 0;

  while (line && fields < NR_FIELDS)
    {
      field[fields++] = line;
      line = strchr (line, ':');
      if (line)
	*(line++) = '\0';
    }

  /* We require at least the first 10 fields.  */
  if (fields < 10)
    return trace_gpg_error (GPG_ERR_INV_ENGINE);

  opt = calloc (1, sizeof (*opt));
  if (!opt)
    return gpg_error_from_syserror ();

  comp->_last_opt_p = &opt->next;
  *opt_p = opt;

  if (field[0][0])
    {
      opt->name = strdup (field[0]);
      if (!opt->name)
	return gpg_error_from_syserror ();
    }

  opt->flags = strtoul (field[1], NULL, 0);

  opt->level = strtoul (field[2], NULL, 0);

  if (field[3][0])
    {
      opt->description = strdup (field[3]);
      if (!opt->description)
	return gpg_error_from_syserror ();
    }

  opt->type = strtoul (field[4], NULL, 0);

  opt->alt_type = strtoul (field[5], NULL, 0);

  if (field[6][0])
    {
      opt->argname = strdup (field[6]);
      if (!opt->argname)
	return gpg_error_from_syserror ();
    }

  if (opt->flags & GPGME_CONF_DEFAULT)
    {
      err = gpgconf_parse_option (opt, &opt->default_value, field[7]);
      if (err)
	return err;
    }
  else if ((opt->flags & GPGME_CONF_DEFAULT_DESC) && field[7][0])
    {
      opt->default_description = strdup (field[7]);
      if (!opt->default_description)
	return gpg_error_from_syserror ();
    }

  if (opt->flags & GPGME_CONF_NO_ARG_DESC)
    {
      opt->no_arg_description = strdup (field[8]);
      if (!opt->no_arg_description)
	return gpg_error_from_syserror ();
    }
  else
    {
      err = gpgconf_parse_option (opt, &opt->no_arg_value, field[8]);
      if (err)
	return err;
    }

  err = gpgconf_parse_option (opt, &opt->value, field[9]);
  if (err)
    return err;

  return 0;
}


static gpgme_error_t
gpgconf_conf_load (void *engine, gpgme_conf_comp_t *comp_p)
{
  gpgme_error_t err;
  gpgme_conf_comp_t comp = NULL;
  gpgme_conf_comp_t cur_comp;

  *comp_p = NULL;

  err = gpgconf_read (engine, "--list-components", NULL,
		      gpgconf_config_load_cb, &comp);
  if (err)
    {
      gpgconf_release (comp);
      return err;
    }

  cur_comp = comp;
  while (!err && cur_comp)
    {
      err = gpgconf_read (engine, "--list-options", cur_comp->name,
			  gpgconf_config_load_cb2, cur_comp);
      cur_comp = cur_comp->next;
    }

  if (err)
    {
      gpgconf_release (comp);
      return err;
    }

  *comp_p = comp;
  return 0;
}



gpgme_error_t
_gpgme_conf_arg_new (gpgme_conf_arg_t *arg_p,
		     gpgme_conf_type_t type, const void *value)
{
  gpgme_conf_arg_t arg;

  arg = calloc (1, sizeof (*arg));
  if (!arg)
    return gpg_error_from_syserror ();

  if (!value)
    arg->no_arg = 1;
  else
    {
      /* We need to switch on type here because the alt-type is not
         yet known.  */
      switch (type)
	{
	case GPGME_CONF_NONE:
	case GPGME_CONF_UINT32:
	  arg->value.uint32 = *((unsigned int *) value);
	  break;

	case GPGME_CONF_INT32:
	  arg->value.int32 = *((int *) value);
	  break;

	case GPGME_CONF_STRING:
	case GPGME_CONF_FILENAME:
	case GPGME_CONF_LDAP_SERVER:
        case GPGME_CONF_KEY_FPR:
        case GPGME_CONF_PUB_KEY:
        case GPGME_CONF_SEC_KEY:
        case GPGME_CONF_ALIAS_LIST:
	  arg->value.string = strdup (value);
	  if (!arg->value.string)
	    {
	      free (arg);
	      return gpg_error_from_syserror ();
	    }
	  break;

	default:
	  free (arg);
	  return gpg_error (GPG_ERR_INV_VALUE);
	}
    }

  *arg_p = arg;
  return 0;
}


void
_gpgme_conf_arg_release (gpgme_conf_arg_t arg, gpgme_conf_type_t type)
{
  /* Lacking the alt_type we need to switch on type here.  */
  switch (type)
    {
    case GPGME_CONF_NONE:
    case GPGME_CONF_UINT32:
    case GPGME_CONF_INT32:
    case GPGME_CONF_STRING:
    default:
      break;

    case GPGME_CONF_FILENAME:
    case GPGME_CONF_LDAP_SERVER:
    case GPGME_CONF_KEY_FPR:
    case GPGME_CONF_PUB_KEY:
    case GPGME_CONF_SEC_KEY:
    case GPGME_CONF_ALIAS_LIST:
      type = GPGME_CONF_STRING;
      break;
    }

  release_arg (arg, type);
}


gpgme_error_t
_gpgme_conf_opt_change (gpgme_conf_opt_t opt, int reset, gpgme_conf_arg_t arg)
{
  if (reset)
    {
      if (opt->new_value)
	release_arg (opt->new_value, opt->alt_type);
      opt->new_value = NULL;
      opt->change_value = 0;
    }
  else
    {
      /* Support self-assignment, for example for adding an item to an
	 existing list.  */
      if (opt->new_value && arg != opt->new_value)
	release_arg (opt->new_value, opt->alt_type);
      opt->new_value = arg;
      opt->change_value = 1;
    }
  return 0;
}


/* FIXME: Major problem: We don't get errors from gpgconf.  */

static gpgme_error_t
gpgconf_write (void *engine, char *arg1, char *arg2, gpgme_data_t conf)
{
  struct engine_gpgconf *gpgconf = engine;
  gpgme_error_t err = 0;
#define BUFLEN 1024
  char buf[BUFLEN];
  int buflen = 0;
  char *argv[] = { NULL /* file_name */, arg1, arg2, 0 };
  int rp[2];
  struct spawn_fd_item_s cfd[] = { {-1, 0 /* STDIN_FILENO */}, {-1, -1} };
  int status;
  int nwrite;

  /* FIXME: Deal with engine->home_dir.  */

  /* _gpgme_engine_new guarantees that this is not NULL.  */
  argv[0] = gpgconf->file_name;

  if (_gpgme_io_pipe (rp, 0) < 0)
    return gpg_error_from_syserror ();

  cfd[0].fd = rp[0];

  status = _gpgme_io_spawn (gpgconf->file_name, argv,
                            IOSPAWN_FLAG_DETACHED, cfd, NULL, NULL, NULL);
  if (status < 0)
    {
      _gpgme_io_close (rp[0]);
      _gpgme_io_close (rp[1]);
      return gpg_error_from_syserror ();
    }

  for (;;)
    {
      if (buflen == 0)
	{
	  do
	    {
	      buflen = gpgme_data_read (conf, buf, BUFLEN);
	    }
	  while (buflen < 0 && errno == EAGAIN);

	  if (buflen < 0)
	    {
	      err = gpg_error_from_syserror ();
	      _gpgme_io_close (rp[1]);
	      return err;
	    }
	  else if (buflen == 0)
	    {
	      /* All is written.  */
	      _gpgme_io_close (rp[1]);
	      return 0;
	    }
	}

      do
	{
	  nwrite = _gpgme_io_write (rp[1], buf, buflen);
	}
      while (nwrite < 0 && errno == EAGAIN);

      if (nwrite > 0)
	{
	  buflen -= nwrite;
	  if (buflen > 0)
	    memmove (&buf[0], &buf[nwrite], buflen);
	}
      else if (nwrite < 0)
	{
	  _gpgme_io_close (rp[1]);
	  return gpg_error_from_syserror ();
	}
    }

  return 0;
}


static gpgme_error_t
arg_to_data (gpgme_data_t conf, gpgme_conf_opt_t option, gpgme_conf_arg_t arg)
{
  gpgme_error_t err = 0;
  int amt = 0;
  char buf[16];

  while (amt >= 0 && arg)
    {
      switch (option->alt_type)
	{
	case GPGME_CONF_NONE:
	case GPGME_CONF_UINT32:
	default:
	  snprintf (buf, sizeof (buf), "%u", arg->value.uint32);
	  buf[sizeof (buf) - 1] = '\0';
	  amt = gpgme_data_write (conf, buf, strlen (buf));
	  break;

	case GPGME_CONF_INT32:
	  snprintf (buf, sizeof (buf), "%i", arg->value.uint32);
	  buf[sizeof (buf) - 1] = '\0';
	  amt = gpgme_data_write (conf, buf, strlen (buf));
	  break;


	case GPGME_CONF_STRING:
          /* The complex types below are only here to silent the
             compiler warning. */
        case GPGME_CONF_FILENAME:
        case GPGME_CONF_LDAP_SERVER:
        case GPGME_CONF_KEY_FPR:
        case GPGME_CONF_PUB_KEY:
        case GPGME_CONF_SEC_KEY:
        case GPGME_CONF_ALIAS_LIST:
          if (arg->value.string)
            {
              /* One quote character, and three times to allow for
                 percent escaping.  */
              char *ptr = arg->value.string;
              amt = gpgme_data_write (conf, "\"", 1);
              if (amt < 0)
                break;

              while (!err && *ptr)
                {
                  switch (*ptr)
                    {
                    case '%':
                      amt = gpgme_data_write (conf, "%25", 3);
                      break;

                    case ':':
                      amt = gpgme_data_write (conf, "%3a", 3);
                      break;

                    case ',':
                      amt = gpgme_data_write (conf, "%2c", 3);
                      break;

                    default:
                      amt = gpgme_data_write (conf, ptr, 1);
                    }
                  ptr++;
                }
            }
	  break;
	}

      if (amt < 0)
	break;

      arg = arg->next;
      /* Comma separator.  */
      if (arg)
	amt = gpgme_data_write (conf, ",", 1);
    }

  if (amt < 0)
    return gpg_error_from_syserror ();

  return 0;
}


static gpgme_error_t
gpgconf_conf_save (void *engine, gpgme_conf_comp_t comp)
{
  gpgme_error_t err;
  int amt = 0;
  /* We use a data object to store the new configuration.  */
  gpgme_data_t conf;
  gpgme_conf_opt_t option;
  int something_changed = 0;

  err = gpgme_data_new (&conf);
  if (err)
    return err;

  option = comp->options;
  while (!err && amt >= 0 && option)
    {
      if (option->change_value)
	{
	  unsigned int flags = 0;
	  char buf[16];

	  something_changed = 1;

	  amt = gpgme_data_write (conf, option->name, strlen (option->name));
	  if (amt >= 0)
	    amt = gpgme_data_write (conf, ":", 1);
	  if (amt < 0)
	    break;

	  if (!option->new_value)
	    flags |= GPGME_CONF_DEFAULT;
	  snprintf (buf, sizeof (buf), "%u", flags);
	  buf[sizeof (buf) - 1] = '\0';

	  amt = gpgme_data_write (conf, buf, strlen (buf));
	  if (amt >= 0)
	    amt = gpgme_data_write (conf, ":", 1);
	  if (amt < 0)
	    break;

	  if (option->new_value)
	    {
	      err = arg_to_data (conf, option, option->new_value);
	      if (err)
		break;
	    }
	  amt = gpgme_data_write (conf, "\n", 1);
	}
      option = option->next;
    }
  if (!err && amt < 0)
    err = gpg_error_from_syserror ();
  if (err || !something_changed)
    goto bail;

  err = gpgme_data_seek (conf, 0, SEEK_SET);
  if (err)
    goto bail;

  err = gpgconf_write (engine, "--change-options", comp->name, conf);
 bail:
  gpgme_data_release (conf);
  return err;
}


static void
gpgconf_set_io_cbs (void *engine, gpgme_io_cbs_t io_cbs)
{
  /* Nothing to do.  */
}


/* Currently, we do not use the engine interface for the various
   operations.  */
void
_gpgme_conf_release (gpgme_conf_comp_t conf)
{
  gpgconf_config_release (conf);
}


struct engine_ops _gpgme_engine_ops_gpgconf =
  {
    /* Static functions.  */
    _gpgme_get_default_gpgconf_name,
    NULL,
    gpgconf_get_version,
    gpgconf_get_req_version,
    gpgconf_new,

    /* Member functions.  */
    gpgconf_release,
    NULL,		/* reset */
    NULL,               /* set_status_cb */
    NULL,		/* set_status_handler */
    NULL,		/* set_command_handler */
    NULL,		/* set_colon_line_handler */
    NULL,		/* set_locale */
    NULL,		/* set_protocol */
    NULL,		/* decrypt */
    NULL,		/* decrypt_verify */
    NULL,		/* delete */
    NULL,		/* edit */
    NULL,		/* encrypt */
    NULL,		/* encrypt_sign */
    NULL,		/* export */
    NULL,		/* export_ext */
    NULL,		/* genkey */
    NULL,		/* import */
    NULL,		/* keylist */
    NULL,		/* keylist_ext */
    NULL,		/* sign */
    NULL,		/* trustlist */
    NULL,		/* verify */
    NULL,		/* getauditlog */
    NULL,               /* opassuan_transact */
    gpgconf_conf_load,
    gpgconf_conf_save,
    gpgconf_set_io_cbs,
    NULL,		/* io_event */
    NULL,		/* cancel */
    NULL,               /* cancel_op */
    NULL,               /* passwd */
    NULL,               /* set_pinentry_mode */
    NULL                /* opspawn */
  };
