/* Edit translations using a subprocess.
   Copyright (C) 2001-2025 Free Software Foundation, Inc.
   Written by Bruno Haible <haible@clisp.cons.org>, 2001.

   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 <https://www.gnu.org/licenses/>.  */


#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <limits.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>

#include <textstyle.h>

#include <error.h>
#include "options.h"
#include "noreturn.h"
#include "closeout.h"
#include "dir-list.h"
#include "xvasprintf.h"
#include "error-progname.h"
#include "progname.h"
#include "relocatable.h"
#include "basename-lgpl.h"
#include "message.h"
#include "read-catalog-file.h"
#include "read-po.h"
#include "read-properties.h"
#include "read-stringtable.h"
#include "write-catalog.h"
#include "write-po.h"
#include "write-properties.h"
#include "write-stringtable.h"
#include "msgl-charset.h"
#include "xalloc.h"
#include "findprog.h"
#include "pipe-filter.h"
#include "xsetenv.h"
#include "filters.h"
#include "msgl-iconv.h"
#include "xerror-handler.h"
#include "po-charset.h"
#include "propername.h"
#include "gettext.h"

#define _(str) gettext (str)


/* We use a child process, and communicate through a bidirectional pipe.  */


/* Force output of PO file even if empty.  */
static int force_po;

/* Keep the header entry unmodified.  */
static int keep_header;

/* Name of the subprogram.  */
static const char *sub_name;

/* Pathname of the subprogram.  */
static const char *sub_path;

/* Argument list for the subprogram.  */
static const char **sub_argv;
static int sub_argc;

static bool newline;

/* Filter function.  */
static void (*filter) (const char *str, size_t len, char **resultp, size_t *lengthp);


/* Forward declaration of local functions.  */
_GL_NORETURN_FUNC static void usage (int status);
static void generic_filter (const char *str, size_t len, char **resultp, size_t *lengthp);
static msgdomain_list_ty *process_msgdomain_list (msgdomain_list_ty *mdlp);


int
main (int argc, char **argv)
{
  bool do_help;
  bool do_version;
  char *output_file;
  const char *input_file;
  msgdomain_list_ty *result;
  catalog_input_format_ty input_syntax = &input_format_po;
  catalog_output_format_ty output_syntax = &output_format_po;
  bool sort_by_filepos = false;
  bool sort_by_msgid = false;
  int i;

  /* Set program name for messages.  */
  set_program_name (argv[0]);
  error_print_progname = maybe_print_progname;
  gram_max_allowed_errors = 20;

  /* Set locale via LC_ALL.  */
  setlocale (LC_ALL, "");

  /* Set the text message domain.  */
  bindtextdomain (PACKAGE, relocate (LOCALEDIR));
  bindtextdomain ("gnulib", relocate (GNULIB_LOCALEDIR));
  bindtextdomain ("bison-runtime", relocate (BISON_LOCALEDIR));
  textdomain (PACKAGE);

  /* Ensure that write errors on stdout are detected.  */
  atexit (close_stdout);

  /* Set default values for variables.  */
  do_help = false;
  do_version = false;
  output_file = NULL;
  input_file = NULL;

  /* Parse command line options.  */
  BEGIN_ALLOW_OMITTING_FIELD_INITIALIZERS
  static const struct program_option options[] =
  {
    { "add-location",      CHAR_MAX + 'n', optional_argument },
    { NULL,                'n',            no_argument       },
    { "color",             CHAR_MAX + 6,   optional_argument },
    { "directory",         'D',            required_argument },
    { "escape",            'E',            no_argument       },
    { "force-po",           0,             no_argument,      &force_po, 1 },
    { "help",               'h',           no_argument       },
    { "indent",             CHAR_MAX + 1,  no_argument       },
    { "input",              'i',           required_argument },
    { "keep-header",        0,             no_argument,      &keep_header, 1 },
    { "newline",            CHAR_MAX + 9,  no_argument       },
    { "no-escape",          CHAR_MAX + 2,  no_argument       },
    { "no-location",        CHAR_MAX + 8,  no_argument       },
    { "no-wrap",            CHAR_MAX + 3,  no_argument       },
    { "output-file",        'o',           required_argument },
    { "properties-input",   'P',           no_argument       },
    { "properties-output",  'p',           no_argument       },
    { "sort-by-file",       'F',           no_argument       },
    { "sort-output",        's',           no_argument       },
    { "strict",             CHAR_MAX + 10, no_argument       },
    { "stringtable-input",  CHAR_MAX + 4,  no_argument       },
    { "stringtable-output", CHAR_MAX + 5,  no_argument       },
    { "style",              CHAR_MAX + 7,  required_argument },
    { "version",            'V',           no_argument       },
    { "width",              'w',           required_argument },
  };
  END_ALLOW_OMITTING_FIELD_INITIALIZERS
  /* The flag NON_OPTION_TERMINATES_OPTIONS causes option parsing to terminate
     when the first non-option, i.e. the subprogram name, is encountered.  */
  start_options (argc, argv, options, NON_OPTION_TERMINATES_OPTIONS, 0);
  int opt;
  while ((opt = get_next_option ()) != -1)
    switch (opt)
      {
      case '\0':                /* Long option with key == 0.  */
        break;

      case 'D':
        dir_list_append (optarg);
        break;

      case 'E':
        message_print_style_escape (true);
        break;

      case 'F':
        sort_by_filepos = true;
        break;

      case 'h':
        do_help = true;
        break;

      case 'i':
        if (input_file != NULL)
          {
            error (EXIT_SUCCESS, 0, _("at most one input file allowed"));
            usage (EXIT_FAILURE);
          }
        input_file = optarg;
        break;

      case 'n':            /* -n */
      case CHAR_MAX + 'n': /* --add-location[={full|yes|file|never|no}] */
        if (handle_filepos_comment_option (optarg))
          usage (EXIT_FAILURE);
        break;

      case 'o':
        output_file = optarg;
        break;

      case 'p':
        output_syntax = &output_format_properties;
        break;

      case 'P':
        input_syntax = &input_format_properties;
        break;

      case 's':
        sort_by_msgid = true;
        break;

      case CHAR_MAX + 10: /* --strict */
        message_print_style_uniforum ();
        break;

      case 'V':
        do_version = true;
        break;

      case 'w':
        {
          int value;
          char *endp;
          value = strtol (optarg, &endp, 10);
          if (endp != optarg)
            message_page_width_set (value);
        }
        break;

      case CHAR_MAX + 1:
        message_print_style_indent ();
        break;

      case CHAR_MAX + 2:
        message_print_style_escape (false);
        break;

      case CHAR_MAX + 3: /* --no-wrap */
        message_page_width_ignore ();
        break;

      case CHAR_MAX + 4: /* --stringtable-input */
        input_syntax = &input_format_stringtable;
        break;

      case CHAR_MAX + 5: /* --stringtable-output */
        output_syntax = &output_format_stringtable;
        break;

      case CHAR_MAX + 6: /* --color */
        if (handle_color_option (optarg) || color_test_mode)
          usage (EXIT_FAILURE);
        break;

      case CHAR_MAX + 7: /* --style */
        handle_style_option (optarg);
        break;

      case CHAR_MAX + 8: /* --no-location */
        message_print_style_filepos (filepos_comment_none);
        break;

      case CHAR_MAX + 9: /* --newline */
        newline = true;
        break;

      default:
        usage (EXIT_FAILURE);
        break;
      }

  /* Version information is requested.  */
  if (do_version)
    {
      printf ("%s (GNU %s) %s\n", last_component (program_name),
              PACKAGE, VERSION);
      /* xgettext: no-wrap */
      printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\
License GPLv3+: GNU GPL version 3 or later <%s>\n\
This is free software: you are free to change and redistribute it.\n\
There is NO WARRANTY, to the extent permitted by law.\n\
"),
              "2001-2025", "https://gnu.org/licenses/gpl.html");
      printf (_("Written by %s.\n"), proper_name ("Bruno Haible"));
      exit (EXIT_SUCCESS);
    }

  /* Help is requested.  */
  if (do_help)
    usage (EXIT_SUCCESS);

  /* Test for the subprogram name.  */
  if (optind == argc)
    error (EXIT_FAILURE, 0, _("missing filter name"));
  sub_name = argv[optind];

  /* Verify selected options.  */
  if (sort_by_msgid && sort_by_filepos)
    error (EXIT_FAILURE, 0, _("%s and %s are mutually exclusive"),
           "--sort-output", "--sort-by-file");

  /* Build argument list for the program.  */
  sub_argc = argc - optind;
  sub_argv = XNMALLOC (sub_argc + 1, const char *);
  for (i = 0; i < sub_argc; i++)
    sub_argv[i] = argv[optind + i];
  sub_argv[i] = NULL;

  /* Extra checks for sed scripts.  */
  if (strcmp (sub_name, "sed") == 0)
    {
      if (sub_argc == 1)
        error (EXIT_FAILURE, 0,
               _("at least one sed script must be specified"));

      /* Replace GNU sed specific options with portable sed options.  */
      for (i = 1; i < sub_argc; i++)
        {
          if (strcmp (sub_argv[i], "--expression") == 0)
            sub_argv[i] = "-e";
          else if (strcmp (sub_argv[i], "--file") == 0)
            sub_argv[i] = "-f";
          else if (strcmp (sub_argv[i], "--quiet") == 0
                   || strcmp (sub_argv[i], "--silent") == 0)
            sub_argv[i] = "-n";

          if (strcmp (sub_argv[i], "-e") == 0
              || strcmp (sub_argv[i], "-f") == 0)
            i++;
        }
    }

  /* By default, input comes from standard input.  */
  if (input_file == NULL)
    input_file = "-";

  /* Read input file.  */
  result = read_catalog_file (input_file, input_syntax);

  /* Recognize special programs as built-ins.  */
  if (strcmp (sub_name, "recode-sr-latin") == 0 && sub_argc == 1)
    {
      filter = serbian_to_latin;

      /* Convert the input to UTF-8 first.  */
      result = iconv_msgdomain_list (result, po_charset_utf8, true, input_file,
                                     textmode_xerror_handler);
    }
  else if (strcmp (sub_name, "quot") == 0 && sub_argc == 1)
    {
      filter = ascii_quote_to_unicode;

      /* Convert the input to UTF-8 first.  */
      result = iconv_msgdomain_list (result, po_charset_utf8, true, input_file,
                                     textmode_xerror_handler);
    }
  else if (strcmp (sub_name, "boldquot") == 0 && sub_argc == 1)
    {
      filter = ascii_quote_to_unicode_bold;

      /* Convert the input to UTF-8 first.  */
      result = iconv_msgdomain_list (result, po_charset_utf8, true, input_file,
                                     textmode_xerror_handler);
    }
  else
    {
      filter = generic_filter;

      /* Warn if the current locale is not suitable for this PO file.  */
      compare_po_locale_charsets (result);

      /* Attempt to locate the program.
         This is an optimization, to avoid that spawn/exec searches the PATH
         on every call.  */
      sub_path = find_in_path (sub_name);

      /* Finish argument list for the program.  */
      sub_argv[0] = sub_path;
    }

  /* Apply the subprogram.  */
  result = process_msgdomain_list (result);

  /* Sort the results.  */
  if (sort_by_filepos)
    msgdomain_list_sort_by_filepos (result);
  else if (sort_by_msgid)
    msgdomain_list_sort_by_msgid (result);

  /* Write the merged message list out.  */
  msgdomain_list_print (result, output_file, output_syntax,
                        textmode_xerror_handler, force_po, false);

  exit (EXIT_SUCCESS);
}


/* Display usage information and exit.  */
static void
usage (int status)
{
  if (status != EXIT_SUCCESS)
    fprintf (stderr, _("Try '%s --help' for more information.\n"),
             program_name);
  else
    {
      printf (_("\
Usage: %s [OPTION] FILTER [FILTER-OPTION]\n\
"), program_name);
      printf ("\n");
      printf (_("\
Applies a filter to all translations of a translation catalog.\n\
"));
      printf ("\n");
      printf (_("\
Mandatory arguments to long options are mandatory for short options too.\n"));
      printf ("\n");
      printf (_("\
Input file location:\n"));
      printf (_("\
  -i, --input=INPUTFILE       input PO file\n"));
      printf (_("\
  -D, --directory=DIRECTORY   add DIRECTORY to list for input files search\n"));
      printf (_("\
If no input file is given or if it is -, standard input is read.\n"));
      printf ("\n");
      printf (_("\
Output file location:\n"));
      printf (_("\
  -o, --output-file=FILE      write output to specified file\n"));
      printf (_("\
The results are written to standard output if no output file is specified\n\
or if it is -.\n"));
      printf ("\n");
      printf (_("\
The FILTER can be any program that reads a translation from standard input\n\
and writes a modified translation to standard output.\n\
"));
      printf ("\n");
      printf (_("\
Filter input and output:\n"));
      printf (_("\
  --newline                   add a newline at the end of input and\n\
                                remove a newline from the end of output"));
      printf ("\n");
      printf (_("\
Useful FILTER-OPTIONs when the FILTER is 'sed':\n"));
      printf (_("\
  -e, --expression=SCRIPT     add SCRIPT to the commands to be executed\n"));
      printf (_("\
  -f, --file=SCRIPTFILE       add the contents of SCRIPTFILE to the commands\n\
                                to be executed\n"));
      printf (_("\
  -n, --quiet, --silent       suppress automatic printing of pattern space\n"));
      printf ("\n");
      printf (_("\
Input file syntax:\n"));
      printf (_("\
  -P, --properties-input      input file is in Java .properties syntax\n"));
      printf (_("\
      --stringtable-input     input file is in NeXTstep/GNUstep .strings syntax\n"));
      printf ("\n");
      printf (_("\
Output details:\n"));
      printf (_("\
      --color                 use colors and other text attributes always\n\
      --color=WHEN            use colors and other text attributes if WHEN.\n\
                              WHEN may be 'always', 'never', 'auto', or 'html'.\n"));
      printf (_("\
      --style=STYLEFILE       specify CSS style rule file for --color\n"));
      printf (_("\
      --no-escape             do not use C escapes in output (default)\n"));
      printf (_("\
  -E, --escape                use C escapes in output, no extended chars\n"));
      printf (_("\
      --force-po              write PO file even if empty\n"));
      printf (_("\
      --indent                indented output style\n"));
      printf (_("\
      --keep-header           keep header entry unmodified, don't filter it\n"));
      printf (_("\
      --no-location           suppress '#: filename:line' lines\n"));
      printf (_("\
  -n, --add-location          preserve '#: filename:line' lines (default)\n"));
      printf (_("\
      --strict                strict Uniforum output style\n"));
      printf (_("\
  -p, --properties-output     write out a Java .properties file\n"));
      printf (_("\
      --stringtable-output    write out a NeXTstep/GNUstep .strings file\n"));
      printf (_("\
  -w, --width=NUMBER          set output page width\n"));
      printf (_("\
      --no-wrap               do not break long message lines, longer than\n\
                              the output page width, into several lines\n"));
      printf (_("\
  -s, --sort-output           generate sorted output\n"));
      printf (_("\
  -F, --sort-by-file          sort output by file location\n"));
      printf ("\n");
      printf (_("\
Informative output:\n"));
      printf (_("\
  -h, --help                  display this help and exit\n"));
      printf (_("\
  -V, --version               output version information and exit\n"));
      printf ("\n");
      /* TRANSLATORS: The first placeholder is the web address of the Savannah
         project of this package.  The second placeholder is the bug-reporting
         email address for this package.  Please add _another line_ saying
         "Report translation bugs to <...>\n" with the address for translation
         bugs (typically your translation team's web or email address).  */
      printf(_("\
Report bugs in the bug tracker at <%s>\n\
or by email to <%s>.\n"),
             "https://savannah.gnu.org/projects/gettext",
             "bug-gettext@gnu.org");
    }

  exit (status);
}


/* Callbacks called from pipe_filter_ii_execute.  */

struct locals
{
  /* String being written.  */
  const char *str;
  size_t len;
  /* String being read and accumulated.  */
  char *result;
  size_t allocated;
  size_t length;
};

static const void *
prepare_write (size_t *num_bytes_p, void *private_data)
{
  struct locals *l = (struct locals *) private_data;

  if (l->len > 0)
    {
      *num_bytes_p = l->len;
      return l->str;
    }
  else
    return NULL;
}

static void
done_write (void *data_written, size_t num_bytes_written, void *private_data)
{
  struct locals *l = (struct locals *) private_data;

  l->str += num_bytes_written;
  l->len -= num_bytes_written;
}

static void *
prepare_read (size_t *num_bytes_p, void *private_data)
{
  struct locals *l = (struct locals *) private_data;

  if (l->length == l->allocated)
    {
      l->allocated = l->allocated + (l->allocated >> 1) + 1;
      l->result = (char *) xrealloc (l->result, l->allocated);
    }
  *num_bytes_p = l->allocated - l->length;
  return l->result + l->length;
}

static void
done_read (void *data_read, size_t num_bytes_read, void *private_data)
{
  struct locals *l = (struct locals *) private_data;

  l->length += num_bytes_read;
}


/* Process a string STR of size LEN bytes through the subprogram.
   Store the freshly allocated result at *RESULTP and its length at *LENGTHP.
 */
static void
generic_filter (const char *str, size_t len, char **resultp, size_t *lengthp)
{
  struct locals l;

  l.str = str;
  l.len = len;
  l.allocated = len + (len >> 2) + 1;
  l.result = XNMALLOC (l.allocated, char);
  l.length = 0;

  pipe_filter_ii_execute (sub_name, sub_path, sub_argv, false, true,
                          prepare_write, done_write, prepare_read, done_read,
                          &l);

  *resultp = l.result;
  *lengthp = l.length;
}


/* Process a string STR of size LEN bytes, then remove NUL bytes.
   Store the freshly allocated result at *RESULTP and its length at *LENGTHP.
 */
static void
process_string (const char *str, size_t len, char **resultp, size_t *lengthp)
{
  char *result;
  size_t length;

  filter (str, len, &result, &length);

  /* Remove NUL bytes from result.  */
  {
    char *p = result;
    char *pend = result + length;

    for (; p < pend; p++)
      if (*p == '\0')
        {
          char *q;

          q = p;
          for (; p < pend; p++)
            if (*p != '\0')
              *q++ = *p;
          length = q - result;
          break;
        }
  }

  *resultp = result;
  *lengthp = length;
}


/* Do the same thing as process_string but append a newline to STR
   before processing, and remove a newline from the result.
 */
static void
process_string_with_newline (const char *str, size_t len, char **resultp,
                             size_t *lengthp)
{
  char *newstr;
  char *result;
  size_t length;

  newstr = XNMALLOC (len + 1, char);
  memcpy (newstr, str, len);
  newstr[len] = '\n';

  process_string (newstr, len + 1, &result, &length);

  free (newstr);

  if (length > 0 && result[length - 1] == '\n')
    result[--length] = '\0';
  else
    error (0, 0, _("filter output is not terminated with a newline"));

  *resultp = result;
  *lengthp = length;
}


static void
process_message (message_ty *mp)
{
  const char *msgstr = mp->msgstr;
  size_t msgstr_len = mp->msgstr_len;
  char *location;
  size_t nsubstrings;
  char **substrings;
  size_t total_len;
  char *total_str;
  const char *p;
  char *q;
  size_t k;

  /* Keep the header entry unmodified, if --keep-header was given.  */
  if (is_header (mp) && keep_header)
    return;

  /* Set environment variables for the subprocess.
     Note: These environment variables, especially MSGFILTER_MSGCTXT and
     MSGFILTER_MSGID, may contain non-ASCII characters.  The subprocess
     may not interpret these values correctly if the locale encoding is
     different from the PO file's encoding.  We want about this situation,
     above.
     On Unix, this problem is often harmless.  On Windows, however, - both
     native Windows and Cygwin - the values of environment variables *must*
     be in the encoding that is the value of GetACP(), because the system
     may convert the environment from char** to wchar_t** before spawning
     the subprocess and back from wchar_t** to char** in the subprocess,
     and it does so using the GetACP() codepage.  */
  if (mp->msgctxt != NULL)
    xsetenv ("MSGFILTER_MSGCTXT", mp->msgctxt, 1);
  else
    unsetenv ("MSGFILTER_MSGCTXT");
  xsetenv ("MSGFILTER_MSGID", mp->msgid, 1);
  if (mp->msgid_plural != NULL)
    xsetenv ("MSGFILTER_MSGID_PLURAL", mp->msgid_plural, 1);
  else
    unsetenv ("MSGFILTER_MSGID_PLURAL");
  location = xasprintf ("%s:%ld", mp->pos.file_name,
                        (long) mp->pos.line_number);
  xsetenv ("MSGFILTER_LOCATION", location, 1);
  free (location);
  if (mp->prev_msgctxt != NULL)
    xsetenv ("MSGFILTER_PREV_MSGCTXT", mp->prev_msgctxt, 1);
  else
    unsetenv ("MSGFILTER_PREV_MSGCTXT");
  if (mp->prev_msgid != NULL)
    xsetenv ("MSGFILTER_PREV_MSGID", mp->prev_msgid, 1);
  else
    unsetenv ("MSGFILTER_PREV_MSGID");
  if (mp->prev_msgid_plural != NULL)
    xsetenv ("MSGFILTER_PREV_MSGID_PLURAL", mp->prev_msgid_plural, 1);
  else
    unsetenv ("MSGFILTER_PREV_MSGID_PLURAL");

  /* Count NUL delimited substrings.  */
  for (p = msgstr, nsubstrings = 0;
       p < msgstr + msgstr_len;
       p += strlen (p) + 1, nsubstrings++);

  /* Process each NUL delimited substring separately.  */
  substrings = XNMALLOC (nsubstrings, char *);
  for (p = msgstr, k = 0, total_len = 0; k < nsubstrings; k++)
    {
      char *result;
      size_t length;

      if (mp->msgid_plural != NULL)
        {
          char *plural_form_string = xasprintf ("%lu", (unsigned long) k);

          xsetenv ("MSGFILTER_PLURAL_FORM", plural_form_string, 1);
          free (plural_form_string);
        }
      else
        unsetenv ("MSGFILTER_PLURAL_FORM");

      if (newline)
        process_string_with_newline (p, strlen (p), &result, &length);
      else
        process_string (p, strlen (p), &result, &length);

      result = (char *) xrealloc (result, length + 1);
      result[length] = '\0';
      substrings[k] = result;
      total_len += length + 1;

      p += strlen (p) + 1;
    }

  /* Concatenate the results, including the NUL after each.  */
  total_str = XNMALLOC (total_len, char);
  for (k = 0, q = total_str; k < nsubstrings; k++)
    {
      size_t length = strlen (substrings[k]);

      memcpy (q, substrings[k], length + 1);
      free (substrings[k]);
      q += length + 1;
    }
  free (substrings);

  mp->msgstr = total_str;
  mp->msgstr_len = total_len;
}


static void
process_message_list (message_list_ty *mlp)
{
  size_t j;

  for (j = 0; j < mlp->nitems; j++)
    process_message (mlp->item[j]);
}


static msgdomain_list_ty *
process_msgdomain_list (msgdomain_list_ty *mdlp)
{
  size_t k;

  for (k = 0; k < mdlp->nitems; k++)
    process_message_list (mdlp->item[k]->messages);

  return mdlp;
}
