/* 
 * (c) Secure Software Solutions, 2001.  Licensed under the GNU Public License.
 *
 * Make sure to define any of the following, if appropriate.
 * HAVE_STDINT_H
 * HAVE_WCHAR_H
 * HAVE_VSNPRINTF
 * HAVE_SNPRINTF
 * HAVE_VSAPRINTF
 * HAVE_SAPRINTF
 * 
 */

#include <ctype.h>
#include <errno.h>
#include <stddef.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>

#include "vsnprintf.h"


#ifdef HAVE_STDINT_H
#include <stdint.h>
#endif
#ifdef HAVE_WCHAR_H
#include <wchar.h>
#else
typedef struct mbstate_t
{
  int count;    /* Number of bytes needed for the current character. */
  int value;    /* Value so far. */
} mbstate_t;

/* This shouldn't actually be defined if it's not in wchar.h, but some
 * systems seem to have it defined already anyway.  Yay portability.
 */
#ifndef MB_CUR_MAX
#define MB_CUR_MAX  1
#endif
#define mbsinit(s)  memset((void *)s, 0, sizeof(*s))
#endif

/* Comformity: C99 - SUSv2 only when it's the same as C99 */

/* Limitations: No support for the *m$ or %m$ syntax */

#define FLAG_ALTERNATE_FORM   0x01
#define FLAG_ZERO_PAD         0x02
#define FLAG_LEFT_ADJUST      0x04
#define FLAG_POSITIVE_BLANK   0x08
#define FLAG_POSITIVE_SIGN    0x10

#define FLAG_HAVE_WIDTH       0x1000
#define FLAG_HAVE_PRECISION   0x2000

#define MODIFIER_CHAR         0x01	/* hh */
#define MODIFIER_SHORT        0x02	/* h  */
#define MODIFIER_LONG         0x04	/* l  */
#define MODIFIER_LONG_LONG    0x08	/* ll */
#define MODIFIER_LONG_DOUBLE  0x10	/* L  */
#define MODIFIER_INTMAX_T     0x20	/* j  */
#define MODIFIER_PTRDIFF_T    0x40	/* t  */
#define MODIFIER_SIZE_T       0x80	/* z  */

typedef struct _printf_buffer_t
{
  FILE           *stream;
  const char     *start;
  char           *outp;
  const char     *limit;
  int             nbytes;
  int             restricted;
}
printf_buffer_t;

static int 
internal_alloc(void **x, size_t size) {
  void   *m = (size_t *)malloc(size);
  if(!m) abort();
  return 0;
}

#define internal_new(x,y) (internal_alloc((void **)x, y))

static void
internal_freemem(void *x) {
  free(x);
}

static inline void
do_output(printf_buffer_t * buffer, const char *data, size_t count)
{
  buffer->nbytes += count;
  if (buffer->stream != NULL)
  {
    fwrite(data, 1, count, buffer->stream);
    return;
  }
  if (buffer->start != NULL)
  {
    while (count-- && buffer->outp < buffer->limit)
    {
      *(buffer->outp)++ = *data++;
    }
  }
}

static inline void
do_padding(printf_buffer_t * buffer, char pad, size_t count)
{
  buffer->nbytes += count;
  if (buffer->stream != NULL)
  {
    char           *tmp;

    internal_new(&tmp, count);
    memset(tmp, pad, count);
    fwrite(tmp, 1, count, buffer->stream);
    internal_freemem(tmp);
    return;
  }
  if (buffer->start != NULL)
  {
    while (count-- && buffer->outp < buffer->limit)
    {
      *(buffer->outp)++ = pad;
    }
  }
}

static void
output_int(printf_buffer_t * buffer, const char *prepend,
	   const char *data, int flags, int width, int precision)
{
  char            padchar = 0;
  size_t          len, padlen = 0, zerolen = 0;

  if (!*prepend)
  {
    if (*data != '-')
    {
      if (flags & FLAG_POSITIVE_SIGN)
      {
	prepend = "+";
      } else if (flags & FLAG_POSITIVE_BLANK)
      {
	prepend = " ";
      }
    }
  }

  len = strlen(data);
  if (!(flags & FLAG_HAVE_PRECISION))
  {
    precision = 1;
  }
  if (!precision && !atoi(data))
  {
    data = "";
  } else if (precision && len < precision)
  {
    zerolen = precision - len;
    len = precision;
  }
  len += strlen(prepend);
  if (width && len < width)
  {
    padchar = ((flags & FLAG_LEFT_ADJUST) || !(flags & FLAG_ZERO_PAD) ? ' '
	       : '0');
    padlen = width - len;
  }
  if (!(flags & FLAG_LEFT_ADJUST) && padlen > 0)
  {
    do_padding(buffer, padchar, padlen);
  }
  do_output(buffer, prepend, strlen(prepend));
  do_padding(buffer, '0', zerolen);
  do_output(buffer, data, strlen(data));
  if ((flags & FLAG_LEFT_ADJUST) && padlen > 0)
  {
    do_padding(buffer, padchar, padlen);
  }
}

static const char *
find_format(const char *str, mbstate_t * ps)
{
  while (*str && *str != '%')
  {
#ifdef HAVE_WCHAR_H
    size_t          len, max;

    mbsinit(ps);
    if (!(*str & 0x80) || (len = mbrlen(str, MB_CUR_MAX, ps)) == (size_t) - 1)
    {
      str++;
      continue;
    }

    for (max = MB_CUR_MAX * 2; len == (size_t) - 2; max += MB_CUR_MAX)
    {
      len = mbrlen(str, max, ps);
    }

    if (!len)
    {
      return NULL;
    }
    str += (len == (size_t) - 1 ? 1 : len);
#else
    str++;
#endif
  }
  return (*str ? str : NULL);
}

int
internal_vsnprintf(printf_buffer_t * buffer, const char *format, va_list ap)
{
  mbstate_t       ps;
  const char     *c;

  memset(&ps, 0, sizeof(mbstate_t));
  for (c = find_format(format, &ps);
       c != NULL; c = find_format((format = c + 1), &ps))
  {
    int             done = 0, flags = 0, modifier = 0, precision = 0, width =

      0;

    /* output everything up to the % character */
    do_output(buffer, format, c - format);

    /* flags */
    for (format = c++; !done && *c; c++)
    {
      switch (*c)
      {
      case '#':
	flags |= FLAG_ALTERNATE_FORM;
	break;
      case '0':
	flags |= FLAG_ZERO_PAD;
	break;
      case '-':
	flags |= FLAG_LEFT_ADJUST;
	break;
      case ' ':
	flags |= FLAG_POSITIVE_BLANK;
	break;
      case '+':
	flags |= FLAG_POSITIVE_SIGN;
	break;
      default:
	done = 1;
	c--;
	break;
      }
    }

    /* minimum field width */
    if (*c == '*')
    {
      flags |= FLAG_HAVE_WIDTH;
      width = va_arg(ap, int);

      c++;
    } else if (isdigit(*c))
    {
      flags |= FLAG_HAVE_WIDTH;
      while (isdigit(*c))
      {
	width = (width * 10) + (*c++ - '0');
      }
    }

    /* precision */
    if (*c == '.')
    {
      flags |= FLAG_HAVE_PRECISION;
      if (*++c == '*')
      {
	precision = va_arg(ap, int);

	c++;
      } else
      {
	while (isdigit(*c))
	{
	  precision = (precision * 10) + (*c++ - '0');
	}
      }
    }

    /* length modifier */
    switch (*c)
    {
    case 'L':
      c++;
      modifier = MODIFIER_LONG_DOUBLE;
      break;
    case 'j':
      c++;
      modifier = MODIFIER_INTMAX_T;
      break;
    case 't':
      c++;
      modifier = MODIFIER_PTRDIFF_T;
      break;
    case 'z':
      c++;
      modifier = MODIFIER_SIZE_T;
      break;

    case 'h':
      if (*++c != 'h')
      {
	modifier = MODIFIER_SHORT;
      } else
      {
	modifier = MODIFIER_CHAR;
	c++;
      }
      break;
    case 'l':
      if (*++c != 'l')
      {
	modifier = MODIFIER_LONG;
      } else
      {
	modifier = MODIFIER_LONG_LONG;
	c++;
      }
      break;
    }

    /* conversion specifier */
    switch (*c)
    {
    case '%':
      if (c != format + 1)
      {
	do_output(buffer, format, c - format + 1);
      }
      do_output(buffer, "%", 1);
      break;

    case 'd':
    case 'i':
      {
	char            buf[24], *prepend = "";

	switch (modifier)
	{
	case MODIFIER_CHAR:
	  sprintf(buf, "%hhd", (char)va_arg(ap, int));

	  break;
	case MODIFIER_SHORT:
	  sprintf(buf, "%hd", (short)va_arg(ap, int));

	  break;
	case MODIFIER_LONG:
	  sprintf(buf, "%ld", va_arg(ap, long));

	  break;
	case MODIFIER_LONG_LONG:
	  sprintf(buf, "%lld", va_arg(ap, long long));

	  break;
#ifdef HAVE_STDINT_H
	case MODIFIER_INTMAX_T:
	  /* this will likely emit a warning, but it's valid */
	  sprintf(buf, "%jd", va_arg(ap, intmax_t));
	  break;
#endif
	case MODIFIER_PTRDIFF_T:
	  /* this will likely emit a warning, but it's valid */
	  sprintf(buf, "%td", va_arg(ap, ptrdiff_t));
	  break;
	case MODIFIER_SIZE_T:
	  /* this will likely emit a warning, but it's valid */
	  sprintf(buf, "%zd", va_arg(ap, ssize_t));
	  break;
	default:
	  sprintf(buf, "%d", va_arg(ap, int));

	  break;
	}
	output_int(buffer, prepend, buf, flags, width, precision);
      }
      break;

    case 'o':
    case 'u':
    case 'x':
    case 'X':
      {
	char            buf[24], fmt[8], *prepend = "";

	switch (modifier)
	{
	case MODIFIER_CHAR:
	  sprintf(fmt, "%%hh%c", *c);
	  sprintf(buf, fmt, (unsigned char)va_arg(ap, unsigned int));

	  break;
	case MODIFIER_SHORT:
	  sprintf(fmt, "%%h%c", *c);
	  sprintf(buf, fmt, (unsigned short)va_arg(ap, unsigned int));

	  break;
	case MODIFIER_LONG:
	  sprintf(fmt, "%%l%c", *c);
	  sprintf(buf, fmt, va_arg(ap, unsigned long));

	  break;
	case MODIFIER_LONG_LONG:
	  sprintf(fmt, "%%ll%c", *c);
	  sprintf(buf, fmt, va_arg(ap, unsigned long long));

	  break;
#ifdef HAVE_STDINT_H
	case MODIFIER_INTMAX_T:
	  sprintf(fmt, "%%j%c", *c);
	  sprintf(buf, fmt, va_arg(ap, uintmax_t));
	  break;
#endif
	case MODIFIER_PTRDIFF_T:
	  sprintf(fmt, "%%t%c", *c);
	  sprintf(buf, fmt, va_arg(ap, ptrdiff_t));
	  break;
	case MODIFIER_SIZE_T:
	  sprintf(fmt, "%%z%c", *c);
	  sprintf(buf, fmt, va_arg(ap, size_t));
	  break;
	default:
	  sprintf(fmt, "%%%c", *c);
	  sprintf(buf, fmt, va_arg(ap, unsigned int));

	  break;
	}

	if (flags & FLAG_ALTERNATE_FORM)
	{
	  if (*c == 'o')
	  {
	    prepend = "0";
	  } else if (*c == 'x')
	  {
	    prepend = "0x";
	  } else if (*c == 'X')
	  {
	    prepend = "0X";
	  }
	}

	output_int(buffer, prepend, buf, flags, width, precision);
      }
      break;

    case 'e':
    case 'E':
    case 'f':
    case 'F':
    case 'g':
    case 'G':
      {
	char           *buf, fmt[24];
	size_t          len;

	precision = ((flags & FLAG_HAVE_PRECISION) ? precision : 6);
	internal_new(&buf, precision + 24);
	sprintf(fmt, "%%%s.%d%c", ((flags & FLAG_ALTERNATE_FORM) ? "#" : ""),
		precision, *c);
	if (modifier & MODIFIER_LONG_DOUBLE)
	{
	  len = sprintf(buf, fmt, va_arg(ap, long double));
	} else
	{
	  len = sprintf(buf, fmt, va_arg(ap, double));
	}
	if ((flags & (FLAG_POSITIVE_SIGN | FLAG_POSITIVE_BLANK)) &&
	    *buf != '-')
	{
	  width--;
	}
	if (!(flags & FLAG_LEFT_ADJUST) && len < width)
	{
	  do_padding(buffer, ' ', width - len);
	}
	if ((flags & FLAG_POSITIVE_SIGN) && *buf != '-')
	{
	  do_output(buffer, "+", 1);
	} else if ((flags & FLAG_POSITIVE_BLANK) && *buf != '-')
	{
	  do_output(buffer, " ", 1);
	}
	do_output(buffer, buf, strlen(buf));
	if ((flags & FLAG_LEFT_ADJUST) && len < width)
	{
	  do_padding(buffer, ' ', width - len);
	}
	internal_freemem(buf);
      }
      break;

    case 'a':
    case 'A':
      {
	char           *buf, fmt[24];
	size_t          len;

	/* XXX: I'm a bit shaky here on the lengths and default precision */
	precision = ((flags & FLAG_HAVE_PRECISION) ? precision : 6);
	internal_new(&buf, precision + 9);
	sprintf(fmt, "%%%s.%d%c", ((flags & FLAG_ALTERNATE_FORM) ? "#" : ""),
		precision, *c);
	if (modifier & MODIFIER_LONG_DOUBLE)
	{
	  len = sprintf(buf, fmt, va_arg(ap, long double));
	} else
	{
	  len = sprintf(buf, fmt, va_arg(ap, double));
	}
	if ((flags & (FLAG_POSITIVE_SIGN | FLAG_POSITIVE_BLANK)) &&
	    *buf != '-')
	{
	  width--;
	}
	if (!(flags & FLAG_LEFT_ADJUST) && len < width)
	{
	  do_padding(buffer, ' ', width - len);
	}
	if ((flags & FLAG_POSITIVE_SIGN) && *buf != '-')
	{
	  do_output(buffer, "+", 1);
	} else if ((flags & FLAG_POSITIVE_BLANK) && *buf != '-')
	{
	  do_output(buffer, " ", 1);
	}
	do_output(buffer, buf, strlen(buf));
	if ((flags & FLAG_LEFT_ADJUST) && len < width)
	{
	  do_padding(buffer, ' ', width - len);
	}
	internal_freemem(buf);
      }
      break;

    case 'c':
      {
	char            buf[MB_CUR_MAX];
	size_t          len;

#ifdef HAVE_WCHAR_H
	if (modifier & MODIFIER_LONG)
	{
	  wint_t          ch = va_arg(ap, wint_t);
	  mbstate_t       ps;

	  memset(&ps, 0, sizeof(mbstate_t));
	  len = wcrtomb(buf, ch, &ps);
	} else
#endif
	{
	  buf[0] = (char)(unsigned char)va_arg(ap, int);

	  len = 1;
	}

	if (!(flags & FLAG_LEFT_ADJUST))
	{
	  do_padding(buffer, ' ', width - len);
	}
	do_output(buffer, buf, len);
	if (flags & FLAG_LEFT_ADJUST)
	{
	  do_padding(buffer, ' ', width - len);
	}
      }
      break;

    case 's':
      {
	char           *freeme = NULL, *string;
	size_t          len = 0;

	string = va_arg(ap, char *);

	if (string == NULL)
	{
	  if ((flags & FLAG_HAVE_PRECISION) && precision >= 6)
	  {
	    string = "(null)";
	    len = 6;
	  } else
	  {
	    string = "";
	  }
	} else if (!(modifier & MODIFIER_LONG))
	{
	  len = strlen(string);
	} else
#ifdef HAVE_WCHAR_H
	{
	  const wchar_t  *s = (const wchar_t *)string;
	  mbstate_t       ps;

	  memset(&ps, 0, sizeof(mbstate_t));
	  if ((flags & FLAG_HAVE_PRECISION) && precision > 0)
	  {
	    internal_new(&freeme, precision + 1);
	    string = freeme;
	    len = wcsrtombs(string, &s, precision, &ps);
	  } else
	  {
	    len = wcsrtombs(NULL, &s, 0, &ps);
	    if (len != (size_t) - 1)
	    {
	      s = (const wchar_t *)string;
	      mbsinit(&ps);
	      internal_new(&freeme, len + 1);
	      string = freeme;
	      wcsrtombs(string, &s, len + 1, &ps);
	    }
	  }
	}
#else
        {
          char *s = string;

          if ((flags & FLAG_HAVE_PRECISION) && precision > 0)
          {
	    internal_new(&freeme, precision + 1);
	    string = freeme;
            strncpy(string, s, precision);
            *(string + precision) = 0;
            len = precision;
          } else
          {
            len = strlen(s);
	    internal_new(&freeme, len + 1);
	    string = freeme;
            memcpy(string, s, len + 1);
          }
        }
#endif

	if ((flags & FLAG_HAVE_PRECISION) && len > precision)
	{
	  len = precision;
	}

	if (!(flags & FLAG_LEFT_ADJUST) && len < width)
	{
	  do_padding(buffer, ' ', width - len);
	}
	do_output(buffer, string, len);
	if ((flags & FLAG_LEFT_ADJUST) && len < width)
	{
	  do_padding(buffer, ' ', width - len);
	}

	if (freeme != NULL)
	{
	  internal_freemem(freeme);
	}
      }
      break;

    case 'p':
      {
	char            buf[20];

	sprintf(buf, "%p", va_arg(ap, void *));

	do_output(buffer, buf, strlen(buf));
      }
      break;

    case 'n':
      if (!buffer->restricted)
      {
	if (modifier & MODIFIER_LONG_LONG)
	{
	  *(long long *)va_arg(ap, void *) = buffer->nbytes;
	} else if (modifier & MODIFIER_LONG)
	{
	  *(long *)va_arg(ap, void *) = buffer->nbytes;
	} else if (modifier & MODIFIER_SHORT)
	{
	  *(short *)va_arg(ap, void *) = buffer->nbytes;
	} else if (modifier & MODIFIER_CHAR)
	{
	  *(char *)va_arg(ap, void *) = buffer->nbytes;
	} else
	{
	  *(int *)va_arg(ap, void *) = buffer->nbytes;
	}
	break;
      } else
      {
        void *junk = va_arg(ap, void *);    /* skip over the argument */
        junk = junk;                        /* shut up compiler warning */
      }

      /* FALL THROUGH */

    default:
      do_output(buffer, format, c - format + 1);
      break;
    }
  }

  /* write the rest of the string including null terminator */
  do_output(buffer, format, strlen(format) + 1);

  /* return the number of characters that would be written if there were
   * enough room.  note that there may have been enough room here and we
   * would then be returning the actual number of characters written.
   * Note: don't include the null terminator
   */
  return buffer->nbytes - 1;
}


#ifndef HAVE_VSNPRINTF
int
vsnprintf(char *str, size_t size, const char *format, va_list ap)
{
  printf_buffer_t buffer;

  buffer.stream = NULL;
  buffer.start = buffer.outp = str;
  buffer.limit = buffer.start + size;
  buffer.nbytes = 0;
  buffer.restricted = 0;

  return internal_vsnprintf(&buffer, format, ap);
}
#endif

#ifndef HAVE_SNPRINTF
int
snprintf(char *str, size_t size, const char *format, ...)
{
  int             i;
  va_list         ap;

  va_start(ap, format);
  i = vsnprintf(str, size, format, ap);
  va_end(ap);

  return i;
}
#endif

#ifndef HAVE_VSAPRINTF
int
vsaprintf(char **str, const char *format, va_list ap)
{
  int             need;

  need = vsnprintf(NULL, 0, format, ap);
  if(internal_new(str, need + 1))
  {
    errno = ENOMEM;
    return -1;
  }
  return vsnprintf(*str, need + 1, format, ap);
}
#endif

#ifndef HAVE_SAPRINTF
int
saprintf(char **str, const char *format, ...)
{
  int             i;
  va_list         ap;

  va_start(ap, format);
  i = vsaprintf(str, format, ap);
  va_end(ap);

  return i;
}
#endif
