/* Copyright (c) 2002,2004,2005 Joerg Wunsch
   All rights reserved.

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions are met:

   * Redistributions of source code must retain the above copyright
     notice, this list of conditions and the following disclaimer.

   * Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in
     the documentation and/or other materials provided with the
     distribution.

   * Neither the name of the copyright holders nor the names of
     contributors may be used to endorse or promote products derived
     from this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  POSSIBILITY OF SUCH DAMAGE.
*/

/* $Id: vfscanf.c,v 1.13 2005/11/09 21:54:33 joerg_wunsch Exp $ */

#include <avr/pgmspace.h>
#include <ctype.h>
#include <stdint.h>
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "stdio_private.h"

/*
 * This file can be compiled into more than one flavour.  The default
 * is to offer the usual modifiers and integer formatting support
 * (level 2).  Level 1 maintains a minimal version that just offers
 * integer formatting, but no modifier support except "h" and "l".
 * Level 3 is intented for floating point support, and also adds the
 * %[ conversion.
 */

#ifndef SCANF_LEVEL
#  define SCANF_LEVEL SCANF_STD
#endif

#if SCANF_LEVEL == SCANF_MIN || SCANF_LEVEL == SCANF_STD \
|| SCANF_LEVEL == SCANF_FLT
/* OK */
#else
#  error "Not a known scanf level."
#endif

#if SCANF_LEVEL >= SCANF_MIN
#define FLHASPERCENT	0x01	/* first % found */
#define	FLUNSIGNED	0x02	/* arg is unsinged (long) * */
#define FLLONG		0x04	/* arg is long int * */
#define FLMINUS		0x08	/* minus sign scanned */
#endif

#if SCANF_LEVEL >= SCANF_STD
#define FLSTAR		0x10	/* suppress assingment (* fmt) */
#endif

#if SCANF_LEVEL >= SCANF_FLT
#define FLBRACKET	0x20	/* %[ found, now collecting the set */
#define FLNEGATE	0x40	/* negate %[ set */

/* bit set macros for %[ format */
#define vscanf_set_bit(i) \
	buf[(unsigned char)(i) / 8] |= (1 << (unsigned char)(i) % 8)
#define vscanf_bit_is_set(i) \
	(buf[(unsigned char)(i) / 8] & (1 << (unsigned char)(i) % 8))

/*
 * 40 Bytes is sufficient for 32-bit floating point values in `f'
 * style notation.  Note that this buffer is also used to record the
 * bit vector for %[ formats, so it must be at least 256/8 = 32 bytes
 * long.
 */
#define FLTBUF 40
#endif

int
vfscanf(FILE *stream, const char *fmt, va_list ap) {
	union {
#if SCANF_LEVEL >= SCANF_FLT
		double	d;
#endif
		unsigned long ul;
		long	l;
		char	*cp;
	} a;
	char	c;	/* holds a char from the format string */
	uint8_t	base;
	int	nconvs, rv, i, j, olen;
#if SCANF_LEVEL > SCANF_MIN
	int8_t	width;
#endif
	uint8_t flags;
#if SCANF_LEVEL >= SCANF_FLT
	char	*bp;
	char	fltchars[] = "0123456789Ee.";
	char	buf[FLTBUF];
#endif

	flags = 0;
	nconvs = 0;
	i = 0;
	rv = EOF;
	olen = stream->len = 0;

	if ((stream->flags & __SRD) == 0)
		return EOF;

	/*
	 * Do not use fmt++ in the next line.  pgm_read_byte() is a
	 * macro, so it could evaluate its argument more than once.
	 */
	while ((c = ((stream->flags & __SPGM)? pgm_read_byte(fmt): *fmt))) {

		fmt++;

#if SCANF_LEVEL >= SCANF_FLT
		if (flags & FLBRACKET) {
			if (c == '^' && i == 0 && !(flags & FLNEGATE)) {
				flags |= FLNEGATE; /* negate set */
				continue; /* without bumping i */
			}
			if (c == '-') {
				if (i == 0) {
				  addbit:
					vscanf_set_bit(c);
					i++;
					continue;
				}
				flags |= FLMINUS;
				i++;
				continue;
			}
			if (c == ']') {
				if (i == 0)
					goto addbit;
				if (flags & FLMINUS) /* trailing - before ] */
					vscanf_set_bit('-');
				if (flags & FLNEGATE)
					for (i = 0; i < 256 / 8; i++)
						buf[i] = ~buf[i];
				if (!(flags & FLSTAR))
					a.cp = va_arg(ap, char *);
				while (width-- > 0) {
					if ((i = getc(stream)) == EOF)
						break;
					if (!vscanf_bit_is_set(i)) {
						ungetc(i, stream);
						break;
					}
					if (!(flags & FLSTAR))
						*a.cp++ = i;
				}
				if (!(flags & FLSTAR))
					*a.cp = '\0';
				goto nextconv;
			}
			if (flags & FLMINUS) {
				flags &= ~FLMINUS;
				while ((unsigned char)j < (unsigned char)c) {
					vscanf_set_bit(j);
					j++;
				}
			}
			j = (unsigned char)c; /* remember for x-y range */
			goto addbit;
		}
#endif /* SCANF_LEVEL >= SCANF_FLT */
		if (flags & FLHASPERCENT) {
			if (c == '%') {
				flags &= ~FLHASPERCENT;
#if SCANF_LEVEL > SCANF_MIN
				goto literal;
#else
				continue;
#endif
			}

			/*
			 * Modifiers go first.  They all end up in a
			 * "continue" statement so to fetch the next
			 * char from the format string.
			 */
#if SCANF_LEVEL > SCANF_MIN
			if (c >= '0' && c <= '9') {
				c -= '0';
				if (width == SCHAR_MAX)
					width = 0;
				else
					width *= 10;
				width += c;
				continue;
			}
#endif /* SCANF_LEVEL > SCANF_MIN */

			c = tolower(c);

			switch (c) {
#if SCANF_LEVEL > SCANF_MIN
			case '*':
				flags |= FLSTAR;
				continue;
#endif /* SCANF_LEVEL > SCANF_MIN */

			case 'h':
#if SHRT_MAX != INT_MAX
#error "SHRT_MAX != INT_MAX for target: not supported"
#endif
				/*
				 * short int and int are identical on
				 * our target platform, ignore.
				 */
				continue;

			case 'l':
				flags |= FLLONG;
				continue;

				/*
				 * Actual conversion specifications go
				 * here.
				 */
			case 'c':
#if SCANF_LEVEL > SCANF_MIN
				if (!(flags & FLSTAR))
#endif /* SCANF_LEVEL > SCANF_MIN */
					a.cp = va_arg(ap, char *);
#if SCANF_LEVEL > SCANF_MIN
				if (width == SCHAR_MAX)
					width = 1;
				while (width-- > 0) {
#endif /* SCANF_LEVEL > SCANF_MIN */
					if ((i = getc(stream)) == EOF)
						goto leave;
#if SCANF_LEVEL > SCANF_MIN
					if (!(flags & FLSTAR))
#endif /* SCANF_LEVEL > SCANF_MIN */
						*a.cp++ = i;
#if SCANF_LEVEL > SCANF_MIN
				}
#endif /* SCANF_LEVEL > SCANF_MIN */
				break;

			case 's':
#if SCANF_LEVEL > SCANF_MIN
				if (!(flags & FLSTAR))
#endif /* SCANF_LEVEL > SCANF_MIN */
					a.cp = va_arg(ap, char *);
				do {
					i = getc(stream);
				} while (isspace(i));
				if (i == EOF)
					goto leave;

#if SCANF_LEVEL > SCANF_MIN
				while (width-- > 0)
#else
				for (;;)
#endif /* SCANF_LEVEL > SCANF_MIN */
				{
					if (isspace(i)) {
						ungetc(i, stream);
						break;
					}
#if SCANF_LEVEL > SCANF_MIN
					if (!(flags & FLSTAR))
#endif /* SCANF_LEVEL > SCANF_MIN */
						*a.cp++ = i;
					if ((i = getc(stream)) == EOF)
						break;
				}
#if SCANF_LEVEL > SCANF_MIN
				if (!(flags & FLSTAR))
#endif /* SCANF_LEVEL > SCANF_MIN */
					*a.cp = '\0';
				break;

			case 'o':
				base = 8;
				flags |= FLUNSIGNED;
				goto dointeger;

			case 'p':
				/*
				 * Handle pointers as plain unsigned
				 * integers.  This assumes that
				 * sizeof(void *) == sizeof(unsigned int).
				 */
			case 'x':
				base = 16;
				/* FALLTHROUGH */

			case 'u':
				flags |= FLUNSIGNED;
				/* FALLTHROUGH */

			case 'd':
			case 'i':
			  dointeger:
				do {
					i = getc(stream);
				} while (isspace(i));
				if (i == EOF)
					goto leave;

				if ((char)i == '-' || (char)i == '+') {
#if SCANF_LEVEL > SCANF_MIN
					if (--width <= 0)
						/*
						 * Incomplete conversion
						 * due to field width
						 * truncation.
						 */
						goto leave;
#endif /* SCANF_LEVEL > SCANF_MIN */
					if ((char)i == '-')
						flags |= FLMINUS;
					if ((i = getc(stream)) == EOF)
						goto leave;
				}

				if ((char)i == '0') {
					/*
					 * %i conversions default to base
					 * 10, but allow for base 8
					 * indicated by a leading 0 in
					 * input, or base 16 indicated by
					 * leading 0x/0X.
					 *
					 * For %x (and %p) conversions, the
					 * leading 0x/0X is explicitly
					 * allowable.
					 *
					 * If we fail the conversion here,
					 * it is a mismatch condition, but
					 * since we already saw a zero,
					 * this means the current
					 * conversion succeeded, assigning
					 * 0.
					 */
					a.ul = 0;

#if SCANF_LEVEL > SCANF_MIN
					if (--width <= 0)
						goto intdone;
#endif /* SCANF_LEVEL > SCANF_MIN */
					if ((i = getc(stream)) == EOF)
						goto intdone;
					if ((char)tolower(i) == 'x') {
						if (c == 'o' ||
						    c == 'd' || c == 'u') {
							/*
							 * Invalid 0x in
							 * %d/%u/%o
							 */
							ungetc(i, stream);
							goto intdone;
						}
						base = 16;
						if ((i = getc(stream)) == EOF)
							goto intdone;
					} else if (c == 'i')
						base = 8;
				}

				a.ul = 0;
				for (;;) {
					j = tolower(i);
					/*
					 * First, assume it is a decimal
					 * digit.
					 */
					j -= '0';
					if (j > 9) {
						/*
						 * Not a decimal digit.
						 * Try hex next.
						 */
						j += '0'; /* undo "- '0'"
							   * above */
						j -= 'a'; /* 'a' is first
							   * hex digit */
						if (j >= 0)
							/* 'a' has value
							 * 10 */
							j += 10;
						/*
						 * else: not a hex digit,
						 * gets caught below.
						 */
					}
					if (j < 0 || j >= base) {
						ungetc(i, stream);
						break;
					}
					a.ul *= base;
					a.ul += j;
#if SCANF_LEVEL > SCANF_MIN
					if (--width <= 0)
						break;
#endif /* SCANF_LEVEL > SCANF_MIN */
					if ((i = getc(stream)) == EOF)
						break;
				}
				/*
				 * This is a bit of a hack: while we
				 * collect all integer digits in an
				 * unsigned long number in order to be
				 * safe for unsigned conversions, the
				 * standard allows for optional signs.
				 * We are thus faced to the concept of
				 * possibly negating an unsigned
				 * number. :-/  We rely here on union a
				 * mapping the signed and unsigned
				 * fields suitably.
				 */
				if (flags & FLMINUS)
					a.l = -a.l;
			  intdone:
#if SCANF_LEVEL > SCANF_MIN
				if (!(flags & FLSTAR)) {
#endif /* SCANF_LEVEL > SCANF_MIN */
					if ((flags & (FLLONG | FLUNSIGNED))
					    == (FLLONG | FLUNSIGNED))
						*(va_arg(ap, unsigned long *)) =
							a.ul;
					else if (flags & (FLUNSIGNED))
						*(va_arg(ap, unsigned *)) =
							(unsigned)a.ul;
					else if (flags & FLLONG)
						*(va_arg(ap, long *)) = a.l;
					else
						*(va_arg(ap, int *)) =
							(int)a.l;
#if SCANF_LEVEL > SCANF_MIN
				}
#endif /* SCANF_LEVEL > SCANF_MIN */
				break;

#if SCANF_LEVEL > SCANF_MIN
			case 'n':
				if (!(flags & FLSTAR))
					*(va_arg(ap, int *)) = stream->len;
				break;
#endif /* SCANF_LEVEL > SCANF_MIN */

#if SCANF_LEVEL >= SCANF_FLT
			case 'e':
			case 'f':
			case 'g':
				do {
					i = getc(stream);
				} while (isspace(i));
				if (i == EOF)
					goto leave;

				if ((char)i == '-' || (char)i == '+') {
					if ((char)i == '-')
						flags |= FLMINUS;
					if ((i = getc(stream)) == EOF)
						goto leave;
				}

				a.d = 0.0;
				for (bp = buf;
				     bp < buf + FLTBUF - 1 && width > 0;
				     width--) {
					if (strchr(fltchars, i) == 0) {
						ungetc(i, stream);
						break;
					}
					if ((char)i == 'e' ||
					    (char)i == 'E') {
						/*
						 * Prevent another 'E'
						 * from being recognized.
						 */
						fltchars[10] = 0;
						*bp++ = i;
						if ((i = getc(stream)) == EOF)
							break;
						if ((char)i != '-' &&
						    (char)i != '+')
							continue;
					} else if ((char)i == '.')
						/*
						 * Prevent another dot from
						 * being recognized.  If we
						 * already saw an 'E'
						 * above, we could not get
						 * here at all.
						 */
						fltchars[12] = 0;
					*bp++ = i;
					if ((i = getc(stream)) == EOF)
						break;
				}
				*bp++ = 0;
				a.d = strtod(buf, 0);
				if (flags & FLMINUS)
					a.d = -a.d;
				*(va_arg(ap, double *)) = a.d;
				/*
				 * Restore the 'E' and '.' chars that
				 * might have been clobbered above.
				 */
				fltchars[10] = 'E';
				fltchars[12] = '.';
				break;

			case '[':
				flags |= FLBRACKET;
				i = j = 0;
				memset(buf, 0, 256 / 8);
				continue;
#endif /* SCANF_LEVEL >= SCANF_FLT */
			}

#if SCANF_LEVEL >= SCANF_FLT
		  nextconv:
#endif
			if (stream->len > olen) {
#if SCANF_LEVEL >= SCANF_STD
				if (!(flags & FLSTAR))
#endif
					nconvs++;
				rv = 0;
			} else if (c != 'n' || i == EOF)
				/*
				 * If one conversion failed completely,
				 * punt.
				 */
				goto leave;
			flags = 0;
		} else if (c == '%') {
			flags = FLHASPERCENT;
			base = 10;
			olen = stream->len;
#if SCANF_LEVEL > SCANF_MIN
			width = SCHAR_MAX;
		} else if (isspace(c)) {
			/* match against any whitspace */
			do {
				i = getc(stream);
			} while (isspace(i));
			if (i == EOF)
				goto leave;
			ungetc(i, stream);
		} else {
			/* literal character in format, match it */
		  literal:
			if ((i = getc(stream)) == EOF)
				goto leave;
			if (i != c)
				goto leave;
#endif /* SCANF_LEVEL > SCANF_MIN */
		}
	}
leave:
	/*
	 * If a conversion was aborted (usually due to input failure
	 * or end-of-file), adjust the total number of conversions
	 * done if at least one char could be read from the stream.
	 */
	if ((flags & FLHASPERCENT) && stream->len > olen) {
#if SCANF_LEVEL >= SCANF_STD
		if (!(flags & FLSTAR))
#endif
			nconvs++;
		rv = 0;
	}
	/*
	 * If an error occurs before the first successful conversion,
	 * we ought to return EOF.  Before getting here, all
	 * conversions maintain the last character read from the
	 * stream (or EOF) within variable `i'.
	 */
	if (i == EOF && nconvs == 0)
		return rv;

	return nconvs;
}
