/* GNU m4 -- A simple macro processor

   Copyright (C) 1989-1994, 2006-2014, 2016-2017, 2020-2021 Free
   Software Foundation, Inc.

   This file is part of GNU M4.

   GNU M4 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.

   GNU M4 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/>.
*/

/* printf like formatting for m4.  */

#include "m4.h"
#include "xvasprintf.h"

/* Simple varargs substitute.  We assume int and unsigned int are the
   same size; likewise for long and unsigned long.  */

/* Parse STR as an integer, reporting warnings.  */
static int
arg_int (const char *str)
{
  char *endp;
  long value;
  size_t len = strlen (str);

  if (!len)
    {
      M4ERROR ((warning_status, 0, _("empty string treated as 0")));
      return 0;
    }
  errno = 0;
  value = strtol (str, &endp, 10);
  if (endp - str - len)
    M4ERROR ((warning_status, 0, _("non-numeric argument %s"), str));
  else if (c_isspace (*str))
    M4ERROR ((warning_status, 0, _("leading whitespace ignored")));
  else if (errno == ERANGE || (int) value != value)
    M4ERROR ((warning_status, 0, _("numeric overflow detected")));
  return value;
}

/* Parse STR as a long, reporting warnings.  */
static long
arg_long (const char *str)
{
  char *endp;
  long value;
  size_t len = strlen (str);

  if (!len)
    {
      M4ERROR ((warning_status, 0, _("empty string treated as 0")));
      return 0L;
    }
  errno = 0;
  value = strtol (str, &endp, 10);
  if (endp - str - len)
    M4ERROR ((warning_status, 0, _("non-numeric argument %s"), str));
  else if (c_isspace (*str))
    M4ERROR ((warning_status, 0, _("leading whitespace ignored")));
  else if (errno == ERANGE)
    M4ERROR ((warning_status, 0, _("numeric overflow detected")));
  return value;
}

/* Parse STR as a double, reporting warnings.  */
static double
arg_double (const char *str)
{
  char *endp;
  double value;
  size_t len = strlen (str);

  if (!len)
    {
      M4ERROR ((warning_status, 0, _("empty string treated as 0")));
      return 0.0;
    }
  errno = 0;
  value = strtod (str, &endp);
  if (endp - str - len)
    M4ERROR ((warning_status, 0, _("non-numeric argument %s"), str));
  else if (c_isspace (*str))
    M4ERROR ((warning_status, 0, _("leading whitespace ignored")));
  else if (errno == ERANGE)
    M4ERROR ((warning_status, 0, _("numeric overflow detected")));
  return value;
}

#define ARG_INT(argc, argv) \
        ((argc == 0) ? 0 : \
         (--argc, argv++, arg_int (TOKEN_DATA_TEXT (argv[-1]))))

#define ARG_LONG(argc, argv) \
        ((argc == 0) ? 0 : \
         (--argc, argv++, arg_long (TOKEN_DATA_TEXT (argv[-1]))))

#define ARG_STR(argc, argv) \
        ((argc == 0) ? "" : \
         (--argc, argv++, TOKEN_DATA_TEXT (argv[-1])))

#define ARG_DOUBLE(argc, argv) \
        ((argc == 0) ? 0 : \
         (--argc, argv++, arg_double (TOKEN_DATA_TEXT (argv[-1]))))


/*------------------------------------------------------------------.
| The main formatting function.  Output is placed on the obstack    |
| OBS, the first argument in ARGV is the formatting string, and the |
| rest is arguments for the string.  Warn rather than invoke        |
| unspecified behavior in the underlying printf when we do not      |
| recognize a format.                                               |
`------------------------------------------------------------------*/

void
expand_format (struct obstack *obs, int argc, token_data **argv)
{
  const char *f;                        /* format control string */
  const char *fmt;                      /* position within f */
  char fstart[] = "%'+- 0#*.*hhd";      /* current format spec */
  char *p;                              /* position within fstart */
  unsigned char c;                      /* a simple character */

  /* Flags.  */
  char flags;                           /* flags to use in fstart */
  enum {
    THOUSANDS   = 0x01, /* ' */
    PLUS        = 0x02, /* + */
    MINUS       = 0x04, /* - */
    SPACE       = 0x08, /*   */
    ZERO        = 0x10, /* 0 */
    ALT         = 0x20, /* # */
    DONE        = 0x40  /* no more flags */
  };

  /* Precision specifiers.  */
  int width;                    /* minimum field width */
  int prec;                     /* precision */
  char lflag;                   /* long flag */

  /* Specifiers we are willing to accept.  ok['x'] implies %x is ok.
     Various modifiers reduce the set, in order to avoid undefined
     behavior in printf.  */
  char ok[128];

  /* Buffer and stuff.  */
  char *str;                    /* malloc'd buffer of formatted text */
  enum {CHAR, INT, LONG, DOUBLE, STR} datatype;

  f = fmt = ARG_STR (argc, argv);
  memset (ok, 0, sizeof ok);
  while (1)
    {
      const char *percent = strchr (fmt, '%');
      if (!percent)
        {
          obstack_grow (obs, fmt, strlen (fmt));
          return;
        }
      obstack_grow (obs, fmt, percent - fmt);
      fmt = percent + 1;

      if (*fmt == '%')
        {
          obstack_1grow (obs, '%');
          fmt++;
          continue;
        }

      p = fstart + 1; /* % */
      lflag = 0;
      ok['a'] = ok['A'] = ok['c'] = ok['d'] = ok['e'] = ok['E']
        = ok['f'] = ok['F'] = ok['g'] = ok['G'] = ok['i'] = ok['o']
        = ok['s'] = ok['u'] = ok['x'] = ok['X'] = 1;

      /* Parse flags.  */
      flags = 0;
      do
        {
          switch (*fmt)
            {
            case '\'': /* thousands separator */
              ok['a'] = ok['A'] = ok['c'] = ok['e'] = ok['E']
                = ok['o'] = ok['s'] = ok['x'] = ok['X'] = 0;
              flags |= THOUSANDS;
              break;

            case '+': /* mandatory sign */
              ok['c'] = ok['o'] = ok['s'] = ok['u'] = ok['x'] = ok['X'] = 0;
              flags |= PLUS;
              break;

            case ' ': /* space instead of positive sign */
              ok['c'] = ok['o'] = ok['s'] = ok['u'] = ok['x'] = ok['X'] = 0;
              flags |= SPACE;
              break;

            case '0': /* zero padding */
              ok['c'] = ok['s'] = 0;
              flags |= ZERO;
              break;

            case '#': /* alternate output */
              ok['c'] = ok['d'] = ok['i'] = ok['s'] = ok['u'] = 0;
              flags |= ALT;
              break;

            case '-': /* left justification */
              flags |= MINUS;
              break;

            default:
              flags |= DONE;
              break;
            }
        }
      while (!(flags & DONE) && fmt++);
      if (flags & THOUSANDS)
        *p++ = '\'';
      if (flags & PLUS)
        *p++ = '+';
      if (flags & MINUS)
        *p++ = '-';
      if (flags & SPACE)
        *p++ = ' ';
      if (flags & ZERO)
        *p++ = '0';
      if (flags & ALT)
        *p++ = '#';

      /* Minimum field width; an explicit 0 is the same as not giving
         the width.  */
      width = 0;
      *p++ = '*';
      if (*fmt == '*')
        {
          width = ARG_INT (argc, argv);
          fmt++;
        }
      else
        while (c_isdigit (*fmt))
          {
            width = 10 * width + *fmt - '0';
            fmt++;
          }

      /* Maximum precision; an explicit negative precision is the same
         as not giving the precision.  A lone '.' is a precision of 0.  */
      prec = -1;
      *p++ = '.';
      *p++ = '*';
      if (*fmt == '.')
        {
          ok['c'] = 0;
          if (*(++fmt) == '*')
            {
              prec = ARG_INT (argc, argv);
              ++fmt;
            }
          else
            {
              prec = 0;
              while (c_isdigit (*fmt))
                {
                  prec = 10 * prec + *fmt - '0';
                  fmt++;
                }
            }
        }

      /* Length modifiers.  We don't yet recognize ll, j, t, or z.  */
      if (*fmt == 'l')
        {
          *p++ = 'l';
          lflag = 1;
          fmt++;
          ok['c'] = ok['s'] = 0;
        }
      else if (*fmt == 'h')
        {
          *p++ = 'h';
          fmt++;
          if (*fmt == 'h')
            {
              *p++ = 'h';
              fmt++;
            }
          ok['a'] = ok['A'] = ok['c'] = ok['e'] = ok['E'] = ok['f'] = ok['F']
            = ok['g'] = ok['G'] = ok['s'] = 0;
        }

      c = *fmt++;
      if (sizeof ok <= c || !ok[c])
        {
          M4ERROR ((warning_status, 0,
                    _("Warning: unrecognized specifier in `%s'"), f));
          if (c == '\0')
            fmt--;
          continue;
        }

      /* Specifiers.  We don't yet recognize C, S, n, or p.  */
      switch (c)
        {
        case 'c':
          datatype = CHAR;
          p -= 2; /* %.*c is undefined, so undo the '.*'.  */
          break;

        case 's':
          datatype = STR;
          break;

        case 'd':
        case 'i':
        case 'o':
        case 'x':
        case 'X':
        case 'u':
          datatype = lflag ? LONG : INT;
          break;

        case 'a':
        case 'A':
        case 'e':
        case 'E':
        case 'f':
        case 'F':
        case 'g':
        case 'G':
          datatype = DOUBLE;
          break;

        default:
          abort ();
        }
      *p++ = c;
      *p = '\0';

      /* Our constructed format string in fstart is safe.  */
#if 4 < __GNUC__ + (6 <= __GNUC_MINOR__)
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif

      switch (datatype)
        {
        case CHAR:
          str = xasprintf (fstart, width, ARG_INT(argc, argv));
          break;

        case INT:
          str = xasprintf (fstart, width, prec, ARG_INT(argc, argv));
          break;

        case LONG:
          str = xasprintf (fstart, width, prec, ARG_LONG(argc, argv));
          break;

        case DOUBLE:
          str = xasprintf (fstart, width, prec, ARG_DOUBLE(argc, argv));
          break;

        case STR:
          str = xasprintf (fstart, width, prec, ARG_STR(argc, argv));
          break;

        default:
          abort();
        }
#if 4 < __GNUC__ + (6 <= __GNUC_MINOR__)
# pragma GCC diagnostic pop
#endif

      /* NULL was returned on failure, such as invalid format string.  For
         now, just silently ignore that bad specifier.  */
      if (str == NULL)
        continue;

      obstack_grow (obs, str, strlen (str));
      free (str);
    }
}
