/*
 * vsscanf.c
 *
 * vsscanf(), from which the rest of the scanf()
 * family is built
 */

#include <ctype.h>
#include <stdarg.h>
#include <stddef.h>
#include <inttypes.h>
#include <string.h>
#include <limits.h>
#include <stdio.h>

#ifndef LONG_BIT
#define LONG_BIT (CHAR_BIT*sizeof(long))
#endif

enum flags {
  FL_SPLAT  = 0x01,		/* Drop the value, do not assign */
  FL_INV    = 0x02,		/* Character-set with inverse */
  FL_WIDTH  = 0x04,		/* Field width specified */
  FL_MINUS  = 0x08,		/* Negative number */
};

enum ranks {
  rank_char	= -2,
  rank_short	= -1,
  rank_int	= 0,
  rank_long	= 1,
  rank_longlong	= 2,
  rank_ptr      = INT_MAX	/* Special value used for pointers */
};

#define MIN_RANK	rank_char
#define MAX_RANK	rank_longlong

#define INTMAX_RANK	rank_longlong
#define SIZE_T_RANK	rank_long
#define PTRDIFF_T_RANK	rank_long

enum bail {
  bail_none = 0,		/* No error condition */
  bail_eof,			/* Hit EOF */
  bail_err			/* Conversion mismatch */
};

static inline const char *
skipspace(const char *p)
{
  while ( isspace((unsigned char)*p) ) p++;
  return p;
}

#undef set_bit
static inline void
set_bit(unsigned long *bitmap, unsigned int bit)
{
  bitmap[bit/LONG_BIT] |= 1UL << (bit%LONG_BIT);
}

#undef test_bit
static inline int
test_bit(unsigned long *bitmap, unsigned int bit)
{
  return (int)(bitmap[bit/LONG_BIT] >> (bit%LONG_BIT)) & 1;
}

int vsscanf(const char *buffer, const char *format, va_list ap)
{
  const char *p = format;
  char ch;
  const char *q = buffer;
  const char *qq;
  uintmax_t val = 0;
  int rank = rank_int;		/* Default rank */
  unsigned int width = UINT_MAX;
  int base;
  enum flags flags = 0;
  enum {
    st_normal,			/* Ground state */
    st_flags,			/* Special flags */
    st_width,			/* Field width */
    st_modifiers,		/* Length or conversion modifiers */
    st_match_init,		/* Initial state of %[ sequence */
    st_match,			/* Main state of %[ sequence */
    st_match_range,		/* After - in a %[ sequence */
  } state = st_normal;
  char *sarg = NULL;		/* %s %c or %[ string argument */
  enum bail bail = bail_none;
  int sign;
  int converted = 0;		/* Successful conversions */
  unsigned long matchmap[((1 << CHAR_BIT)+(LONG_BIT-1))/LONG_BIT];
  int matchinv = 0;		/* Is match map inverted? */
  unsigned char range_start = 0;

  while ( (ch = *p++) && !bail ) {
    switch ( state ) {
    case st_normal:
      if ( ch == '%' ) {
	state = st_flags;
	flags = 0; rank = rank_int; width = UINT_MAX;
      } else if ( isspace((unsigned char)ch) ) {
	q = skipspace(q);
      } else {
	if ( *q == ch )
	  q++;
	else
	  bail = bail_err;	/* Match failure */
      }
      break;

    case st_flags:
      switch ( ch ) {
      case '*':
	flags |= FL_SPLAT;
	break;
      case '0' ... '9':
	width = (ch-'0');
	state = st_width;
	flags |= FL_WIDTH;
	break;
      default:
	state = st_modifiers;
	p--;			/* Process this character again */
	break;
      }
      break;

    case st_width:
      if ( ch >= '0' && ch <= '9' ) {
	width = width*10+(ch-'0');
      } else {
	state = st_modifiers;
	p--;			/* Process this character again */
      }
      break;

    case st_modifiers:
      switch ( ch ) {
	/* Length modifiers - nonterminal sequences */
      case 'h':
	rank--;			/* Shorter rank */
	break;
      case 'l':
	rank++;			/* Longer rank */
	break;
      case 'j':
	rank = INTMAX_RANK;
	break;
      case 'z':
	rank = SIZE_T_RANK;
	break;
      case 't':
	rank = PTRDIFF_T_RANK;
	break;
      case 'L':
      case 'q':
	rank = rank_longlong;	/* long double/long long */
	break;

      default:
	/* Output modifiers - terminal sequences */
	state = st_normal;	/* Next state will be normal */
	if ( rank < MIN_RANK )	/* Canonicalize rank */
	  rank = MIN_RANK;
	else if ( rank > MAX_RANK )
	  rank = MAX_RANK;

	switch ( ch ) {
	case 'P':		/* Upper case pointer */
	case 'p':		/* Pointer */
#if 0	/* Enable this to allow null pointers by name */
	  q = skipspace(q);
	  if ( !isdigit((unsigned char)*q) ) {
	    static const char * const nullnames[] =
	    { "null", "nul", "nil", "(null)", "(nul)", "(nil)", 0 };
	    const char * const *np;

	    /* Check to see if it's a null pointer by name */
	    for ( np = nullnames ; *np ; np++ ) {
	      if ( !strncasecmp(q, *np, strlen(*np)) ) {
		val = (uintmax_t)((void *)NULL);
		goto set_integer;
	      }
	    }
	    /* Failure */
	    bail = bail_err;
	    break;
	  }
	  /* else */
#endif
	  rank = rank_ptr;
	  base = 0; sign = 0;
	  goto scan_int;

	case 'i':		/* Base-independent integer */
	  base = 0; sign = 1;
	  goto scan_int;

	case 'd':		/* Decimal integer */
	  base = 10; sign = 1;
	  goto scan_int;

	case 'o':		/* Octal integer */
	  base = 8; sign = 0;
	  goto scan_int;

	case 'u':		/* Unsigned decimal integer */
	  base = 10; sign = 0;
	  goto scan_int;

	case 'x':		/* Hexadecimal integer */
	case 'X':
	  base = 16; sign = 0;
	  goto scan_int;

	case 'n':		/* Number of characters consumed */
	  val = (q-buffer);
	  goto set_integer;

	scan_int:
	  q = skipspace(q);
	  if ( !*q ) {
	    bail = bail_eof;
	    break;
	  }
	  val = strntoumax(q, (char **)&qq, base, width);
	  if ( qq == q ) {
	    bail = bail_err;
	    break;
	  }
	  q = qq;
	  converted++;
	  /* fall through */

	set_integer:
	  if ( !(flags & FL_SPLAT) ) {
	    switch(rank) {
	    case rank_char:
	      *va_arg(ap, unsigned char *) = (unsigned char)val;
	      break;
	    case rank_short:
	      *va_arg(ap, unsigned short *) = (unsigned short)val;
	      break;
	    case rank_int:
	      *va_arg(ap, unsigned int *) = (unsigned int)val;
	      break;
	    case rank_long:
	      *va_arg(ap, unsigned long *) = (unsigned long)val;
	      break;
	    case rank_longlong:
	      *va_arg(ap, unsigned long long *) = (unsigned long long)val;
	      break;
	    case rank_ptr:
	      *va_arg(ap, void **) = (void *)(uintptr_t)val;
	      break;
	    }
	  }
	  break;

	case 'c':               /* Character */
          width = (flags & FL_WIDTH) ? width : 1; /* Default width == 1 */
          sarg = va_arg(ap, char *);
          while ( width-- ) {
            if ( !*q ) {
              bail = bail_eof;
              break;
            }
            *sarg++ = *q++;
          }
          if ( !bail )
            converted++;
          break;

        case 's':               /* String */
	  {
	    char *sp;
	    sp = sarg = va_arg(ap, char *);
	    while ( width-- && *q && !isspace((unsigned char)*q) ) {
	      *sp++ = *q++;
	    }
	    if ( sarg != sp ) {
	      *sp = '\0';	/* Terminate output */
	      converted++;
	    } else {
	      bail = bail_eof;
	    }
	  }
	  break;

	case '[':		/* Character range */
	  sarg = va_arg(ap, char *);
	  state = st_match_init;
	  matchinv = 0;
	  memset(matchmap, 0, sizeof matchmap);
	  break;

	case '%':		/* %% sequence */
	  if ( *q == '%' )
	    q++;
	  else
	    bail = bail_err;
	  break;

	default:		/* Anything else */
	  bail = bail_err;	/* Unknown sequence */
	  break;
	}
      }
      break;

    case st_match_init:		/* Initial state for %[ match */
      if ( ch == '^' && !(flags & FL_INV) ) {
	matchinv = 1;
      } else {
	set_bit(matchmap, (unsigned char)ch);
	state = st_match;
      }
      break;

    case st_match:		/* Main state for %[ match */
      if ( ch == ']' ) {
	goto match_run;
      } else if ( ch == '-' ) {
	range_start = (unsigned char)ch;
	state = st_match_range;
      } else {
	set_bit(matchmap, (unsigned char)ch);
      }
      break;

    case st_match_range:		/* %[ match after - */
      if ( ch == ']' ) {
	set_bit(matchmap, (unsigned char)'-'); /* - was last character */
	goto match_run;
      } else {
	int i;
	for ( i = range_start ; i < (unsigned char)ch ; i++ )
	  set_bit(matchmap, i);
	state = st_match;
      }
      break;

    match_run:			/* Match expression finished */
      qq = q;
      while ( width && *q && test_bit(matchmap, (unsigned char)*q)^matchinv ) {
	*sarg++ = *q++;
      }
      if ( q != qq ) {
	*sarg = '\0';
	converted++;
      } else {
	bail = *q ? bail_err : bail_eof;
      }
      break;
    }
  }

  if ( bail == bail_eof && !converted )
    converted = -1;		/* Return EOF (-1) */

  return converted;
}
