/*

  Author: Tomi Salo <ttsalo@ssh.fi>

  Copyright (C) 1996 SSH Communications Security Oy, Espoo, Finland
  All rights reserved.

  Implementation of functions snprintf() and vsnprintf()

  */

/*
 * $Id: snprintf.c,v 1.8 2000/05/15 14:25:38 santtu Exp $
 * $Log: snprintf.c,v $
 * $EndLog$
 */

#include "sshincludes.h"
#include "sshsnprintf.h"
#include "sshdebug.h"

#ifdef SSH_MAKING_EPRINTF
#include "ssheprintf.h"
#endif

#ifndef NO_SNPRINTF

#ifdef SSH_MAKING_EPRINTF
#define SSH_DEBUG_MODULE "SshEPrintf"
#else
#define SSH_DEBUG_MODULE "SshSPrintf"
#endif


#undef isdigit
#define isdigit(ch) ((ch) >= '0' && (ch) <= '9')

#define MINUS_FLAG 0x1
#define PLUS_FLAG 0x2
#define SPACE_FLAG 0x4
#define HASH_FLAG 0x8
#define CONV_TO_SHORT 0x10
#define IS_LONG_INT 0x20
#define IS_LONG_DOUBLE 0x40
#define X_UPCASE 0x80
#define IS_NEGATIVE 0x100
#define UNSIGNED_DEC 0x200
#define ZERO_PADDING 0x400

#undef sprintf


/* Convert a integer from unsigned long int representation 
   to string representation. This will insert prefixes if needed 
   (leading zero for octal and 0x or 0X for hexadecimal) and
   will write at most buf_size characters to buffer.
   tmp_buf is used because we want to get correctly truncated
   results.
   */

static int
snprintf_convert_ulong(char *buffer, size_t buf_size, int base, char *digits,
                       unsigned long int ulong_val, int flags, int width,
                       int precision)
{
  int tmp_buf_len = 100 + width, len, written = 0;
  char *tmp_buf, *tmp_buf_ptr, prefix[2];
#ifdef _KERNEL
  if (!(tmp_buf = ssh_kmalloc(tmp_buf_len)))
    return 0;
#else
  tmp_buf = ssh_xmalloc(tmp_buf_len);
#endif /* _KERNEL */

  prefix[0] = '\0';
  prefix[1] = '\0';
  
  /* Make tmp_buf_ptr point just past the last char of buffer */
  tmp_buf_ptr = tmp_buf + tmp_buf_len;

  if (precision < 0)
    precision = 0;

  /* Main conversion loop */
  do 
    {
      *--tmp_buf_ptr = digits[ulong_val % base];
      ulong_val /= base;
      precision--;
    } 
  while ((ulong_val != 0 || precision > 0) && tmp_buf_ptr > tmp_buf);
 
  /* Get the prefix */
  if (!(flags & IS_NEGATIVE))
    {
      if (base == 16 && (flags & HASH_FLAG))
        {
          if (flags && X_UPCASE)
            {
              prefix[0] = 'x';
              prefix[1] = '0';
            }
          else
            {
              prefix[0] = 'X';
              prefix[1] = '0';
            }
        }
      
      if (base == 8 && (flags & HASH_FLAG))
        prefix[0] = '0';
      
      if (base == 10 && !(flags & UNSIGNED_DEC) && (flags & PLUS_FLAG))
        prefix[0] = '+';
      else
        {
          if (base == 10 && !(flags & UNSIGNED_DEC) && (flags & SPACE_FLAG))
            prefix[0] = ' ';
        }
    }
  else
      prefix[0] = '-';

  if (prefix[0] != '\0' && tmp_buf_ptr > tmp_buf)
    {
      *--tmp_buf_ptr = prefix[0];
      if (prefix[1] != '\0' && tmp_buf_ptr > tmp_buf)
        *--tmp_buf_ptr = prefix[1];
    }
  
  len = (tmp_buf + tmp_buf_len) - tmp_buf_ptr;

  /* Now:
     - len is the length of the actual converted number,
       which is pointed to by tmp_buf_ptr.
     - buf_size is how much space we have.
     - width is the minimum width requested by the user.
     The following code writes the number and padding into
     the buffer and returns the number of characters written.
     If the MINUS_FLAG is set, the number will be left-justified,
     and if it is not set, the number will be right-justified.
     */

  while (buf_size - written > 0)
    {
      /* Write until the buffer is full. If stuff to write is exhausted
         first, return straight from the loop. */
      if (flags & MINUS_FLAG)
        {
          if (written < len)
            buffer[written] = tmp_buf_ptr[written];
          else
            {
              if (written >= width)
                {
#ifdef _KERNEL
                  ssh_kfree(tmp_buf);
#else
                  ssh_xfree(tmp_buf);
#endif /* _KERNEL */
                  return written;
                }
              buffer[written] = (flags & ZERO_PADDING) ? '0': ' ';
            }
          written++;
        }
      else
        {
          if (width > len && written < width - len)
            buffer[written] = (flags & ZERO_PADDING) ? '0': ' ';
          else
            {
              if (width > len)
                buffer[written] = tmp_buf_ptr[written - (width - len)];
              else
                buffer[written] = tmp_buf_ptr[written];
            }
          written++;
          if (written >= width && written >= len)
            {
#ifdef _KERNEL
              ssh_kfree(tmp_buf);
#else
              ssh_xfree(tmp_buf);
#endif /* _KERNEL */
              return written;
            }
        }
    }
#ifdef _KERNEL
  ssh_kfree(tmp_buf);
#else
  ssh_xfree(tmp_buf);
#endif /* _KERNEL */
  return written;
}

#ifndef KERNEL

static int
snprintf_convert_float(char *buffer, size_t buf_size,
                       double dbl_val, int flags, int width,
                       int precision, char format_char)
{
  char print_buf[160], print_buf_len = 0;
  char format_str[80], *format_str_ptr;

  format_str_ptr = format_str;

  if (width > 155) width = 155;
  if (precision < 0)
    precision = 6;
  if (precision > 120)
    precision = 120;

  /* Construct the formatting string and let system's sprintf
     do the real work. */
  
  *format_str_ptr++ = '%';

  if (flags & MINUS_FLAG)
    *format_str_ptr++ = '-';
  if (flags & PLUS_FLAG)
    *format_str_ptr++ = '+';
  if (flags & SPACE_FLAG)
    *format_str_ptr++ = ' ';
  if (flags & ZERO_PADDING)
    *format_str_ptr++ = '0';
  if (flags & HASH_FLAG)
    *format_str_ptr++ = '#';
    
  sprintf(format_str_ptr, "%d.%d", width, precision);
  format_str_ptr += strlen(format_str_ptr);

  if (flags & IS_LONG_DOUBLE)
    *format_str_ptr++ = 'L';
  *format_str_ptr++ = format_char;
  *format_str_ptr++ = '\0';

  sprintf(print_buf, format_str, dbl_val);
  print_buf_len = strlen(print_buf);

  if (print_buf_len > buf_size)
    print_buf_len = buf_size;
  strncpy(buffer, print_buf, print_buf_len);
  return print_buf_len;
}

#endif /* KERNEL */

int
#ifdef SSH_MAKING_EPRINTF
ssh_esnprintf(char *str, size_t size, const char *format, ...)
#else
snprintf(char *str, size_t size, const char *format, ...)
#endif /* SSH_MAKING_EPRINTF */
{
  int ret;
  va_list ap;
  va_start(ap, format);
#ifdef SSH_MAKING_EPRINTF
  ret = ssh_evsnprintf(str, size, format, ap);
#else
  ret = vsnprintf(str, size, format, ap);
#endif
  va_end(ap);

  return ret;
}

int
#ifdef SSH_MAKING_EPRINTF
ssh_evsnprintf(char *str, size_t size, const char *format, va_list ap)
#else
vsnprintf(char *str, size_t size, const char *format, va_list ap)
#endif
{
  int status, left = (int)size - 1;
  const char *format_ptr = format;
  int flags, width, precision, i;
  char format_char, *orig_str = str;
  int *int_ptr;
  long int long_val;
  unsigned long int ulong_val;
  char *str_val;
#ifndef KERNEL
  double dbl_val;
#endif /* KERNEL */

  while (format_ptr < format + strlen(format))
    {
      if (*format_ptr == '%')
        {
          if (format_ptr[1] == '%' && left > 0)
            {
              /* Format `%%' at format string as `%' */
              *str++ = '%';
              left--;
              format_ptr += 2;
            }
          else
            {
              /* Other format directive. */
              if (left <= 0)
                {
                  /* No space left on output */
                  str += left;
                  *str = '\0';
                  return size;
                }
              else
                {
                  int length, value;
                  const char *format_start = format_ptr;

                  flags = 0; 
                  width = 0;
                  precision = -1;
                  format_char = (char)0;

                  /* Get the flags */
                  format_ptr++;
                  while (*format_ptr == '-' || *format_ptr == '+' || 
                         *format_ptr == ' ' || *format_ptr == '#' ||
                         *format_ptr == '0')
                    {
                      switch (*format_ptr)
                        {
                        case '-':
                          flags |= MINUS_FLAG;
                          break;
                        case '+':
                          flags |= PLUS_FLAG;
                          break;
                        case ' ':
                          flags |= SPACE_FLAG;
                          break;
                        case '#':
                          flags |= HASH_FLAG;
                          break;
                        case '0':
                          flags |= ZERO_PADDING;
                          break;
                        }
                      format_ptr++;
                    }

                  /* Don't pad left-justified numbers withs zeros */
                  if ((flags & MINUS_FLAG) && (flags & ZERO_PADDING))
                    flags &= ~ZERO_PADDING;
        
                  /* Is width field present? */
                  if (isdigit(*format_ptr))
                    {
                      for (value = 0; 
                           *format_ptr && isdigit(*format_ptr); 
                           format_ptr++)
                        value = 10 * value + *format_ptr - '0';
                      
                      width = value;
                    }
                  else
                    {
                      if (*format_ptr == '*')
                        {
                          width = va_arg(ap, int);
                          format_ptr++;
                        }
                    }

                  /* Is the precision field present? */
                  if (*format_ptr == '.')
                    {
                      format_ptr++;
                      if (isdigit(*format_ptr))
                        {
                          for (value = 0; 
                               *format_ptr && isdigit(*format_ptr); 
                               format_ptr++)
                            value = 10 * value + *format_ptr - '0';

                          precision = value;
                        }
                      else
                        {
                          if (*format_ptr == '*')
                            {
                              precision = va_arg(ap, int);
                              format_ptr++;
                            }
                          else
                            precision = 0;
                        }
                    }

                  switch (*format_ptr)
                    {
                    case 'h':
                      flags |= CONV_TO_SHORT;
                      format_ptr++;
                      break;
                    case 'l':
                      flags |= IS_LONG_INT;
                      format_ptr++;
                      break;
                    case 'L':
                      flags |= IS_LONG_DOUBLE;
                      format_ptr++;
                      break;
                    default:
                      break;
                    }
      
                  /* Get and check the formatting character */
                  format_char = *format_ptr;
                  format_ptr++;
                  length = format_ptr - format_start;
                        
                  switch (format_char)
                    {
                    case 'c': case 's': case 'p': case 'n':
                    case 'd': case 'i': case 'o': 
                    case 'u': case 'x': case 'X': 
                    case 'f': case 'e': case 'E': case 'g': case 'G': 
#ifdef SSH_MAKING_EPRINTF
                    case '@':
#endif /* SSH_MAKING_EPRINTF */
                      if (format_char == 'X')
                        flags |= X_UPCASE;
                      if (format_char == 'o')
                        flags |= UNSIGNED_DEC;
                      status = length;
                      break;
                    
                    default:
                      status = 0;
                    }

                  if (status == 0)
                    {
                      /* Invalid format directive. Fail with zero return. */
                      *str = '\0';
                      return 0;
                    }
                  else
                    {
                      /* Print argument according to the directive. */
                      switch (format_char)
                        {
                        case 'i': case 'd':
                          /* Convert to unsigned long int before
                             actual conversion to string */
                          if (flags & IS_LONG_INT)
                            long_val = va_arg(ap, long int);
                          else
                            long_val = (long int) va_arg(ap, int);
                          
                          if (long_val < 0)
                            {
                              ulong_val = (unsigned long int) -long_val;
                              flags |= IS_NEGATIVE;
                            }
                          else
                            {
                              ulong_val = (unsigned long int) long_val;
                            }
                          status = 
                            snprintf_convert_ulong(str, left, 10, "0123456789",
                                                   ulong_val, flags,
                                                   width, precision);
                          str += status;
                          left -= status;
                          break;

                        case 'p':
                          ulong_val = (unsigned long int)
                            va_arg(ap, void *);
                          status =
                            snprintf_convert_ulong(str, left, 16,
                                                   "0123456789abcdef",
                                                   ulong_val, flags,
                                                   width, precision);
                          str += status;
                          left -= status;
                          break;                            

                        case 'x': case 'X':

                          if (flags & IS_LONG_INT)
                            ulong_val = va_arg(ap, unsigned long int);
                          else
                            ulong_val = 
                              (unsigned long int) va_arg(ap, unsigned int);
                          
                          status = 
                            snprintf_convert_ulong(str, left, 16, 
                                                   (format_char == 'x')   ? 
                                                       "0123456789abcdef" : 
                                                       "0123456789ABCDEF",
                                                   ulong_val, flags,
                                                   width, precision);
                          str += status;
                          left -= status;
                          
                          break;

                        case 'o':
                          if (flags & IS_LONG_INT)
                            ulong_val = va_arg(ap, unsigned long int);
                          else
                            ulong_val = 
                              (unsigned long int) va_arg(ap, unsigned int);

                          status = snprintf_convert_ulong(str, left, 8,
                                                          "01234567",
                                                          ulong_val, flags,
                                                          width, precision);
                          str += status;
                          left -= status;
                          break;

                        case 'u':
                          if (flags & IS_LONG_INT)
                            ulong_val = va_arg(ap, unsigned long int);
                          else
                            ulong_val = 
                              (unsigned long int) va_arg(ap, unsigned int);

                          status = snprintf_convert_ulong(str, left, 10,
                                                          "0123456789",
                                                          ulong_val, flags,
                                                          width, precision);
                          str += status;
                          left -= status;
                          break;

                        case 'c':
                          if (flags & IS_LONG_INT)
                            ulong_val = va_arg(ap, unsigned long int);
                          else
                            ulong_val = 
                              (unsigned long int) va_arg(ap, unsigned int);
                          *str++ = (unsigned char)ulong_val;
                          left--;
                          break;

#ifdef SSH_MAKING_EPRINTF
                        case '@':
                          {
                            SshEPrintfRenderer renderer =
                              va_arg(ap, SshEPrintfRenderer);
                            void *arg =
                              va_arg(ap, void *);
                            int return_value;
                            int unfit = 0;

                            return_value =
                              (* renderer)
                              (str, left, precision, arg);

                            SSH_ASSERT(return_value <= left + 1);

                            if (return_value == left + 1)
                              {
                                return_value = left;
                                unfit = 1;
                              }

                            if (width > left) width = left;

                            if (width > return_value)
                              {
                                if (flags & MINUS_FLAG)
                                  {
                                    memset(str + return_value,
                                           ' ', (width - return_value));
                                  }
                                else
                                  {
                                    memmove(str + (width - return_value),
                                            str, return_value);
                                    memset(str, ' ',
                                           (width - return_value));
                                  }
                                str += width;
                                left -= width;
                              }
                            else
                              {
                                str += return_value;
                                left -= return_value;
                              }

                            if (unfit)
                              {
                                str++; left--; left = -1;
                              }
                          }
                          break;
#endif /* SSH_MAKING_EPRINTF */

                        case 's':
                          str_val = va_arg(ap, char *);

                          if (str_val == NULL)
                            str_val = "(null)";
                          
                          if (precision == -1)
                            precision = strlen(str_val);
                          else
                            {
                              size_t len = strlen(str_val);
                              if (len <= precision)
                                precision = len;
                            }
                          if (precision > left)
                            precision = left;

                          if (width > left)
                            width = left;
                          if (width < precision)
                            width = precision;
                          i = width - precision;

                          if (flags & MINUS_FLAG)
                            {
                              strncpy(str, str_val, precision);
                              memset(str + precision,
                                     (flags & ZERO_PADDING)?'0':' ', i);
                            }
                          else
                            {
                              memset(str, (flags & ZERO_PADDING)?'0':' ', i);
                              strncpy(str + i, str_val, precision);
                            }
                          str += width;
                          left -= width;
                          break;

                        case 'n':
                          int_ptr = va_arg(ap, int *);
                          *int_ptr = str - orig_str;
                          break;

#ifndef KERNEL
                        case 'f': case 'e': case 'E': case 'g': case 'G':
                          if (flags & IS_LONG_DOUBLE)
                            dbl_val = (double) va_arg(ap, long double);
                          else
                            dbl_val = va_arg(ap, double);
                          status =
                            snprintf_convert_float(str, left, dbl_val, flags,
                                                   width, precision,
                                                   format_char);
                          str += status;
                          left -= status;
                          break;
#endif /* KERNEL */
                          
                        default:
                          break;
                        }
                    }
                }
            }
        }
      else
        {
          if (left > 0)
            {
              *str++ = *format_ptr++;
              left--;
            }
          else
            {
              *str = '\0';
              return size;
            }
        }
    }
  if (left < 0)
    str += left;
  *str = '\0';
  return size - left - 1;
}

#endif /* not NO_SNPRINTF */
