/* recfix.c - Fix and analyze a recfile.  */

/* Copyright (C) 2010-2022 Jose E. Marchesi */

/* This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <config.h>

#include <getopt.h>
#include <stdlib.h>
#include <xalloc.h>
#include <gettext.h>
#define _(str) gettext (str)

#include <rec.h>
#include <recutl.h>

/*
 * Data types.
 */

/* recfix supports several operations, enumerated in the following
   type.  */

enum recfix_op
{
  RECFIX_OP_INVALID,
  RECFIX_OP_CHECK,
#if defined REC_CRYPT_SUPPORT
  RECFIX_OP_ENCRYPT,
  RECFIX_OP_DECRYPT,
#endif
  RECFIX_OP_SORT,
  RECFIX_OP_AUTO
};

/*
 * Global variables.
 */

bool  recfix_external = true;
char *recfix_file     = NULL;
int   recfix_op       = RECFIX_OP_INVALID;
char *recfix_password = NULL;
bool  recfix_force    = false;

/*
 * Command line options management.
 */

enum
{
  COMMON_ARGS,
  NO_EXTERNAL_ARG,
  FORCE_ARG,
  OP_SORT_ARG,
#if defined REC_CRYPT_SUPPORT
  PASSWORD_ARG,
  OP_ENCRYPT_ARG,
  OP_DECRYPT_ARG,
#endif
  OP_CHECK_ARG,
  OP_AUTO_ARG
};

static const struct option GNU_longOptions[] =
  {
    COMMON_LONG_ARGS,
    {"no-external", no_argument, NULL, NO_EXTERNAL_ARG},
    {"force", no_argument, NULL, FORCE_ARG},
    {"check", no_argument, NULL, OP_CHECK_ARG},
    {"sort", no_argument, NULL, OP_SORT_ARG},
#if defined REC_CRYPT_SUPPORT
    {"password", required_argument, NULL, PASSWORD_ARG},
    {"encrypt", no_argument, NULL, OP_ENCRYPT_ARG},
    {"decrypt", no_argument, NULL, OP_DECRYPT_ARG},
#endif
    {"auto", no_argument, NULL, OP_AUTO_ARG},
    {NULL, 0, NULL, 0}
  };

void
recutl_print_help (void)
{
  /* TRANSLATORS: --help output, recfix synopsis.
     no-wrap */
  printf (_("\
Usage: recfix [OPTION]... [OPERATION] [OP_OPTION]... [FILE]\n"));

  /* TRANSLATORS: --help output, recfix short description.
     no-wrap */
  fputs (_("\
Check and fix rec files.\n"),
         stdout);

  puts ("");
  /* TRANSLATORS: --help output, recfix global arguments.
     no-wrap */
  fputs (_("\
      --no-external                   don't use external descriptors.\n\
      --force                         force the requested operation.\n"),
         stdout);

  recutl_print_help_common ();

  puts("");
  /* TRANSLATORS: --help output, recfix operations.
     no-wrap */
  fputs (_("\
Operations:\n\
      --check                         check integrity of the specified file.  Default.\n\
      --sort                          sort the records in the specified file.\n\
      --auto                          insert auto-generated fields in records missing them.\n"),
         stdout);

#if defined REC_CRYPT_SUPPORT
    /* TRANSLATORS: --help output, recfix operations related with encryption.
       no-wrap */
    fputs (_("\
      --encrypt                       encrypt confidential fields in the specified file.\n\
      --decrypt                       decrypt confidential fields in the specified file.\n"),
         stdout);

  puts("");
  /* TRANSLATORS: --help output, recfix encryption and decryption
     options.
     no-wrap */
  fputs (_("\
De/Encryption options:\n\
  -s, --password=PASSWORD             encrypt/decrypt with this password.\n"),
         stdout);
#endif /* REC_CRYPT_SUPPORT */

  puts("");
  /* TRANSLATORS: --help output, notes on recfix.
     no-wrap */
  fputs (_("\
If no FILE is specified then the command acts like a filter, getting\n\
the data from standard input and writing the result to standard output.\n"), stdout);

  puts("");
  recutl_print_help_footer ();
}

static void
recfix_parse_args (int argc,
                   char **argv)
{
  char c;
  int ret;

  while ((ret = getopt_long (argc,
                             argv,
                             ENCRYPTION_SHORT_ARGS,
                             GNU_longOptions,
                             NULL)) != -1)
    {
      c = ret;
      switch (c)
        {
          COMMON_ARGS_CASES
        case NO_EXTERNAL_ARG:
          recfix_external = false;
          break;
        case FORCE_ARG:
          recfix_force = true;
          break;
#if defined REC_CRYPT_SUPPORT
        case 's':
        case PASSWORD_ARG:
          if (recfix_op == RECFIX_OP_INVALID)
            recutl_fatal (_("--password|-s must be used as an operation argument.\n"));

          if ((recfix_op != RECFIX_OP_ENCRYPT)
              && (recfix_op != RECFIX_OP_DECRYPT))
            recutl_fatal (_("the specified operation does not require a password.\n"));

          if (recfix_password != NULL)
            recutl_fatal (_("please specify just one password.\n"));

          recfix_password = xstrdup (optarg);
          break;
#endif /* REC_CRYPT_SUPPORT */
        case OP_CHECK_ARG:
          if (recfix_op != RECFIX_OP_INVALID)
            recutl_fatal (_("please specify just one operation.\n"));

          recfix_op = RECFIX_OP_CHECK;
          break;
        case OP_SORT_ARG:
          if (recfix_op != RECFIX_OP_INVALID)
            recutl_fatal (_("please specify just one operation.\n"));

          recfix_op = RECFIX_OP_SORT;
          break;
        case OP_AUTO_ARG:
          if (recfix_op != RECFIX_OP_INVALID)
            recutl_fatal (_("please specify just one operation.\n"));

          recfix_op = RECFIX_OP_AUTO;
          break;
#if defined REC_CRYPT_SUPPORT
        case OP_ENCRYPT_ARG:
          if (recfix_op != RECFIX_OP_INVALID)
            recutl_fatal (_("please specify just one operation.\n"));

          recfix_op = RECFIX_OP_ENCRYPT;
          break;
        case OP_DECRYPT_ARG:
          if (recfix_op != RECFIX_OP_INVALID)
            recutl_fatal (_("please specify just one operation.\n"));

          recfix_op = RECFIX_OP_DECRYPT;
          break;
#endif /* REC_CRYPT_SUPPORT */
        default:
          exit (EXIT_FAILURE);
        }
    }

  /* The default operation is check, in case the user did not specify
     any in the command line.  */
  if (recfix_op == RECFIX_OP_INVALID)
    recfix_op = RECFIX_OP_CHECK;

#if defined REC_CRYPT_SUPPORT
  /* The encrypt and decrypt operations require the user to specify a
     password.  If no password was specified with -s and the program
     is running in a terminal, prompt the user to provide the
     password.  */

  if (((recfix_op == RECFIX_OP_ENCRYPT)
       || (recfix_op == RECFIX_OP_DECRYPT))
      && (recfix_password == NULL))
    {
      if (recutl_interactive ())
        {
          if (recfix_op == RECFIX_OP_ENCRYPT)
            recfix_password = recutl_getpass (true);
          else
            recfix_password = recutl_getpass (false);
        }

      if (!recfix_password || (strlen (recfix_password) == 0))
        recutl_fatal ("please specify a password.\n");
    }
#endif /* REC_CRYPT_SUPPORT */

  /* Read the name of the file to work on.  */
  if (optind < argc)
    {
      if ((argc - optind) != 1)
        {
          recutl_print_help ();
          exit (EXIT_FAILURE);
        }

      recfix_file = argv[optind++];
    }
}

static bool
recfix_check_database (rec_db_t db)
{
  bool ret;
  char *errors;
  size_t errors_size;
  rec_buf_t buf;

  buf = rec_buf_new (&errors, &errors_size);
  ret = (rec_int_check_db (db,
                           true,            /* Check descriptors.  */
                           recfix_external, /* Use external descriptors.  */
                           buf) == 0);
  rec_buf_close (buf);
  fprintf (stderr, "%s", errors);

  return ret;
}

static int
recfix_do_check ()
{
  rec_db_t db;

  /* Read the database from the specified file and check its
     integrity.  */

  db = recutl_read_db_from_file (recfix_file);
  if (!db)
    {
      fprintf (stderr, _("recfix: error: cannot read file %s\n"), recfix_file);
      return EXIT_FAILURE;
    }

  if (!recfix_check_database (db))
    return EXIT_FAILURE;

  return EXIT_SUCCESS;
}

static int
recfix_do_sort ()
{
  rec_db_t db     = NULL;
  size_t n_rset   = 0;
  rec_rset_t rset = NULL;

  /* Read the database from the specified file.  */

  db = recutl_read_db_from_file (recfix_file);
  if (!db)
    {
      fprintf (stderr, _("recfix: error: cannot read file %s\n"), recfix_file);
      return EXIT_FAILURE;
    }

  /* Sort all the record sets contained in the database.  */

  for (n_rset = 0; n_rset < rec_db_size (db); n_rset++)
    {
      rset = rec_db_get_rset (db, n_rset);
      if (!rec_rset_sort (rset, NULL))
        recutl_out_of_memory ();
    }

  if (!recfix_check_database (db))
    return EXIT_FAILURE;

  if (!recutl_file_is_writable (recfix_file))
    {
      recutl_error (_("file %s is not writable.\n"), recfix_file);
      return EXIT_FAILURE;
    }

  recutl_write_db_to_file (db, recfix_file);
  return EXIT_SUCCESS;
}

#if defined REC_CRYPT_SUPPORT

static int
recfix_do_crypt ()
{
  rec_db_t db;
  size_t n_rset;

  /* Read the database from the specified file. */

  db = recutl_read_db_from_file (recfix_file);
  if (!db)
    {
      fprintf (stderr, _("recfix: error: cannot read file %s\n"), recfix_file);
      return EXIT_FAILURE;
    }

  /* Encrypt/decrypt any unencrypted/encrypted field marked as
     "confidential" using the given password.  */

  for (n_rset = 0; n_rset < rec_db_size (db); n_rset++)
    {
      rec_mset_iterator_t iter;
      rec_fex_t confidential_fields;
      rec_record_t record;
      rec_rset_t rset =
        rec_db_get_rset (db, n_rset);

      /* Skip record sets not having any confidential fields.  */

      confidential_fields = rec_rset_confidential (rset);
      if (confidential_fields == NULL)
        continue;

      /* Process every record of the record set.  */

      iter = rec_mset_iterator (rec_rset_mset (rset));
      while (rec_mset_iterator_next (&iter, MSET_RECORD, (const void **) &record, NULL))
        {
          if (recfix_op == RECFIX_OP_ENCRYPT)
            {
              /* Encrypt any unencrypted confidential field in this
                 record.  */

              if (!rec_encrypt_record (rset, record, recfix_password)
                  && !recfix_force)
                {
                  recutl_error (_("the database contains already encrypted fields\n"));
                  recutl_fatal (_("please use --force or --decrypt\n"));
                }
            }
          else
            {
              /* Decrypt any encrypted confidential field in this
                 record.  */

              rec_decrypt_record (rset, record, recfix_password);
            }
        }

      rec_mset_iterator_free (&iter);
    }

  /* Write the modified database back to the file.  */

  recutl_write_db_to_file (db, recfix_file);

  return EXIT_SUCCESS;
}

#endif /* REC_CRYPT_SUPPORT */

static int
recfix_do_auto ()
{
  rec_db_t db    = NULL;
  size_t n_rset  = 0;

  /* Read the database from the especified file.  */

  db = recutl_read_db_from_file (recfix_file);
  if (!db)
    {
      fprintf (stderr, _("recfix: error: cannot read file %s\n"), recfix_file);
      return EXIT_FAILURE;
    }

  /* Add auto fields to any record in the database not having it, in
     record sets featuring auto fields.  */

  for (n_rset = 0; n_rset < rec_db_size (db); n_rset++)
    {
      rec_mset_iterator_t iter;
      rec_record_t record;
      rec_rset_t rset = rec_db_get_rset (db, n_rset);

      iter = rec_mset_iterator (rec_rset_mset (rset));
      while (rec_mset_iterator_next (&iter, MSET_RECORD, (const void**) &record, NULL))
        {
          if (!rec_rset_add_auto_fields (rset, record))
            recutl_out_of_memory ();
        }

      rec_mset_iterator_free (&iter);
    }

  if (!recfix_check_database (db))
    return EXIT_FAILURE;

  recutl_write_db_to_file (db, recfix_file);
  return EXIT_SUCCESS;
}

int
main (int argc, char *argv[])
{
  int res     = EXIT_SUCCESS;

  recutl_init ("recfix");

  /* Parse arguments.  */

  recfix_parse_args (argc, argv);

  /* Execute the proper operation as specified in the recfix_op
     variable.  */

  switch (recfix_op)
    {
    case RECFIX_OP_CHECK:
      res = recfix_do_check ();
      break;
    case RECFIX_OP_SORT:
      res = recfix_do_sort ();
      break;
    case RECFIX_OP_AUTO:
      res = recfix_do_auto ();
      break;
#if defined REC_CRYPT_SUPPORT
    case RECFIX_OP_ENCRYPT:
    case RECFIX_OP_DECRYPT:
      res = recfix_do_crypt ();
      break;
#endif /* REC_CRYPT_SUPPORT */
    default:
      /* This point shall not be reached.  */
      res = EXIT_FAILURE;
      recutl_fatal (_("unknown operation in recfix: please report this as a bug.\n"));
    }

  return res;
}
