/* 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 <https://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;
  char *version;
};

typedef struct engine_gpgconf *engine_gpgconf_t;


/* Return true if the engine's version is at least VERSION.  */
static int
have_gpgconf_version (engine_gpgconf_t gpgconf, const char *version)
{
  return _gpgme_compare_versions (gpgconf->version, version);
}


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);
  if (gpgconf->version)
    free (gpgconf->version);

  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;

  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 && version)
    {
      gpgconf->version = strdup (version);
      if (!gpgconf->version)
        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, const 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[6];
  int argc = 0;
  int rp[2];
  struct spawn_fd_item_s cfd[] = { {-1, 1 /* STDOUT_FILENO */, -1, 0},
				   {-1, -1} };
  int status;
  int nread;
  char *mark = NULL;

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

  if (gpgconf->home_dir && have_gpgconf_version (gpgconf, "2.1.13"))
    {
      argv[argc++] = (char*)"--homedir";
      argv[argc++] = gpgconf->home_dir;
    }

  argv[argc++] = (char*)arg1;
  argv[argc++] = arg2;
  argv[argc] = NULL;
  assert (argc < DIM (argv));

  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 = NULL;

  if (!line[0])
    return 0;

  while (line)
    {
      gpgme_conf_arg_t arg;

      if (opt->type != GPGME_CONF_STRING)
        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, const 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[7];
  int argc = 0;
  int rp[2] = { -1, -1 };
  int errp[2] = { -1, -1 };
  struct spawn_fd_item_s cfd[] = { {-1, 0 /* STDIN_FILENO */},
                                   {-1, 2 /* STDERR_FILENO */, -1},
                                   {-1, -1} };
  int status;
  int nwrite;

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

  if (gpgconf->home_dir && have_gpgconf_version (gpgconf, "2.1.13"))
    {
      argv[argc++] = (char*)"--homedir";
      argv[argc++] = gpgconf->home_dir;
    }

  argv[argc++] = (char*)"--runtime";
  argv[argc++] = (char*)arg1;
  argv[argc++] = arg2;
  argv[argc] = NULL;
  assert (argc < DIM (argv));

  if (_gpgme_io_pipe (rp, 0) < 0)
    {
      err = gpg_error_from_syserror ();
      goto leave;
    }

  if (_gpgme_io_pipe (errp, 1) < 0)
    {
      err = gpg_error_from_syserror ();
      goto leave;
    }

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

  status = _gpgme_io_spawn (gpgconf->file_name, argv,
                            IOSPAWN_FLAG_DETACHED, cfd, NULL, NULL, NULL);
  if (status < 0)
    {
      err = gpg_error_from_syserror ();
      goto leave;
    }

  rp[0] = -1;
  errp[1] = -1;

  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 ();
              goto leave;
	    }
	  else if (buflen == 0)
	    {
	      /* All is written.  */
	      _gpgme_io_close (rp[1]);
              rp[1] = -1;

              for (;;)
                {
                  do
                    {
                      buflen = _gpgme_io_read (errp[0], buf, BUFLEN);
                    }
                  while (buflen < 0 && errno == EAGAIN);

                  if (buflen == 0)
                    {
                      err = 0;
                      goto leave;
                    }
                  /* XXX: Do something useful with BUF.  */
                }
	    }
	}

      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)
	{
	  err = gpg_error_from_syserror ();
          goto leave;
	}
    }

  assert (! "reached");

 leave:
  if (rp[0] != -1)
    _gpgme_io_close (rp[0]);
  if (rp[1] != -1)
  _gpgme_io_close (rp[1]);
  if (errp[0] != -1)
    _gpgme_io_close (errp[0]);
  if (errp[1] != -1)
  _gpgme_io_close (errp[1]);

  return err;
}


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;
}


struct gpgconf_config_dir_s
{
  const char *what;
  char *result;
};

/* Called for each line in the gpgconf --list-dirs output.  Searches
   for the desired line and returns the result, indicating success by
   a special error value GPG_ERR_USER_1 (which terminates the
   operation immediately).  */
static gpgme_error_t
gpgconf_config_dir_cb (void *hook, char *line)
{
  /* This is an input- and output-parameter.  */
  struct gpgconf_config_dir_s *data = (struct gpgconf_config_dir_s *) hook;
  int len = strlen(data->what);

  if (!strncmp(line, data->what, len) && line[len] == ':')
    {
      char *result = strdup(&line[len + 1]);
      if (!result)
	return gpg_error_from_syserror ();
      data->result = result;
      return gpg_error(GPG_ERR_USER_1);
    }
  return 0;
}


/* Like gpgme_get_dirinfo, but uses the home directory of ENGINE and
   does not cache the result.  */
static gpgme_error_t
gpgconf_conf_dir (void *engine, const char *what, char **result)
{
  gpgme_error_t err;
  struct gpgconf_config_dir_s data;

  data.what = what;
  data.result = NULL;
  err = gpgconf_read (engine, "--list-dirs", NULL,
		      gpgconf_config_dir_cb, &data);
  if (gpg_err_code (err) == GPG_ERR_USER_1)
    {
      /* This signals to us that a result was found.  */
      *result = data.result;
      return 0;
    }

  if (!err)
    err = gpg_error(GPG_ERR_NOT_FOUND);
  return 0;
}


/* Parse a line received from gpgconf --query-swdb.  This function may
 * modify LINE.  The result is stored at RESUL.  */
static gpg_error_t
parse_swdb_line (char *line, gpgme_query_swdb_result_t result)
{
  char *field[9];
  int fields = 0;
  gpg_err_code_t ec;

  while (line && fields < DIM (field))
    {
      field[fields++] = line;
      line = strchr (line, ':');
      if (line)
	*line++ = 0;
    }
  /* We require that all fields exists - gpgme emits all these fields
   * even on error.  They might be empty, though. */
  if (fields < 9)
    return gpg_error (GPG_ERR_INV_ENGINE);

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

  free (result->iversion);
  result->iversion = strdup (field[1]);
  if (!result->iversion)
    return gpg_error_from_syserror ();

  result->urgent = (strtol (field[3], NULL, 10) > 0);

  ec = gpg_err_code (strtoul (field[4], NULL, 10));

  result->created  = _gpgme_parse_timestamp (field[5], NULL);
  result->retrieved= _gpgme_parse_timestamp (field[6], NULL);

  free (result->version);
  result->version  = strdup (field[7]);
  if (!result->version)
    return gpg_error_from_syserror ();

  result->reldate  = _gpgme_parse_timestamp (field[8], NULL);

  /* Set other flags.  */
  result->warning = !!ec;
  result->update = 0;
  result->noinfo = 0;
  result->unknown = 0;
  result->tooold = 0;
  result->error = 0;

  switch (*field[2])
    {
    case '-': result->warning = 1; break;
    case '?': result->unknown = result->warning = 1; break;
    case 'u': result->update = 1; break;
    case 'c': break;
    case 'n': break;
    default:
      result->warning = 1;
      if (!ec)
        ec = GPG_ERR_INV_ENGINE;
      break;
    }

  if (ec == GPG_ERR_TOO_OLD)
    result->tooold = 1;
  else if (ec == GPG_ERR_ENOENT)
    result->noinfo = 1;
  else if (ec)
    result->error = 1;


  return 0;
}


static gpgme_error_t
gpgconf_query_swdb (void *engine,
                    const char *name, const char *iversion,
                    gpgme_query_swdb_result_t result)
{
  struct engine_gpgconf *gpgconf = engine;
  gpgme_error_t err = 0;
  char *linebuf;
  size_t linebufsize;
  int linelen;
  char *argv[7];
  int argc = 0;
  int rp[2];
  struct spawn_fd_item_s cfd[] = { {-1, 1 /* STDOUT_FILENO */, -1, 0},
				   {-1, -1} };
  int status;
  int nread;
  char *mark = NULL;

  if (!have_gpgconf_version (gpgconf, "2.1.16"))
    return gpg_error (GPG_ERR_ENGINE_TOO_OLD);

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

  if (gpgconf->home_dir)
    {
      argv[argc++] = (char*)"--homedir";
      argv[argc++] = gpgconf->home_dir;
    }

  argv[argc++] = (char*)"--query-swdb";
  argv[argc++] = (char*)name;
  argv[argc++] = (char*)iversion;
  argv[argc] = NULL;
  assert (argc < DIM (argv));

  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 = 2048; /* Same as used by gpgconf.  */
  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.  */
          if (*line)
            {
              err = parse_swdb_line (line, result);
              goto leave; /* Ready.  */
            }
          else /* empty line.  */
            err = 0;
        }

      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 void
gpgconf_set_io_cbs (void *engine, gpgme_io_cbs_t io_cbs)
{
  (void)engine;
  (void)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,		/* delete */
    NULL,		/* edit */
    NULL,		/* encrypt */
    NULL,		/* encrypt_sign */
    NULL,		/* export */
    NULL,		/* export_ext */
    NULL,		/* genkey */
    NULL,		/* import */
    NULL,		/* keylist */
    NULL,		/* keylist_ext */
    NULL,               /* keylist_data */
    NULL,               /* keysign */
    NULL,               /* tofu_policy */
    NULL,		/* sign */
    NULL,		/* trustlist */
    NULL,		/* verify */
    NULL,		/* getauditlog */
    NULL,               /* opassuan_transact */
    gpgconf_conf_load,
    gpgconf_conf_save,
    gpgconf_conf_dir,
    gpgconf_query_swdb,
    gpgconf_set_io_cbs,
    NULL,		/* io_event */
    NULL,		/* cancel */
    NULL,               /* cancel_op */
    NULL,               /* passwd */
    NULL,               /* set_pinentry_mode */
    NULL                /* opspawn */
  };
