/*
* libslack - http://libslack.org/
*
* Copyright (C) 1999-2002, 2004, 2010, 2020 raf <raf@raf.org>
*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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/>.
*
* 20201111 raf <raf@raf.org>
*/

/*

=head1 NAME

I<vsscanf(3)> - I<sscanf(3)> with a I<va_list> parameter

=head1 SYNOPSIS

    #include <slack/std.h>
    #ifndef HAVE_VSSCANF
    #include <slack/vsscanf.h>
    #endif

    int vsscanf(const char *str, const char *format, va_list args);

=head1 DESCRIPTION

Similar to I<sscanf(3)> with the variable argument list specified directly
as for I<vprintf(3)>.

Note that this may not be identical in behaviour to the I<sscanf(3)> on your
system because this was implemented from scratch for systems that lack
I<vsscanf(3)>. So your I<sscanf(3)> and this I<vsscanf(3)> share no common
code. Your I<sscanf(3)> may support extensions that I<vsscanf(3)> does not
support. I<vsscanf(3)> complies with all of the relevant ISO C requirements
for I<sscanf(3)> except:

=over 4

=item *

C<format> may not be a multibyte character string; and

=item *

Scanning a pointer (C<"%p">) may not exactly match the format that your
I<printf(3)> uses to print pointers on your system. This version accepts
pointers as a hexadecimal number with or without a preceding C<0x>.

=back

=head1 MT-Level

MT-Safe if and only if no thread calls I<setlocale(3)>. Since locales are
inherently non-threadsafe as they are currently defined, this shouldn't be a
problem. Just call C<setlocale(LC_ALL, "")> once after program
initialisation and never again (at least not after creating any threads). If
it is a problem, just change C<localeconv()-E<gt>decimal_point[0]> in the source
to C<'.'> and it will be MT-Safe at the expense of losing locale support.

=head1 EXAMPLE

    #include <slack/std.h>
    #ifndef HAVE_VSSCANF
    #include <slack/vsscanf.h>
    #endif

    int fdscanf(int fd, const char *format, ...)
    {
        va_list args;
        char buf[BUFSIZ];
        ssize_t bytes;
        int rc;
        
        if ((bytes = read(fd, buf, BUFSIZ)) <= 0)
            return bytes;

        buf[bytes] = '\0';

        va_start(args, format);
        rc = vsscanf(buf, format, args);
        va_end(args);

        return rc;
    }

    int main()
    {
        int rc, a = 0, b = 0;

        rc = fdscanf(STDIN_FILENO, "%d %d", &a, &b);
        printf("rc = %d a = %d b = %d\n", rc, a, b);

        return (rc == 2) ? EXIT_SUCCESS : EXIT_FAILURE;
    }

=head1 NOTE

I<gcc(1)> warns that:

    warning: ANSI C does not support the `L' length modifier
    warning: use of `l' length character with `e' type character

However, the ANSI C standard (Section 7.9.6.2) states that:

"Finally, the conversion specifiers C<e>, C<f>, and C<g> shall be preceded
by C<l> if the corresponding argument is a pointer to I<double> rather than
a pointer to C<float>, or by C<L> if it is a pointer to I<long double>."

I have chosen to disregard the I<gcc(1)> warnings in favour of the standard.
If you see the above warnings when compiling the unit tests for
I<vsscanf(3)>, just ignore them.

=head1 SEE ALSO

I<libslack(3)>,
I<sscanf(3)>

=head1 AUTHOR

20201111 raf <raf@raf.org>

=cut

*/

#include "config.h"
#include "std.h"

#include <locale.h>

int vsscanf(const char *str, const char *format, va_list args)
{
	const char *f, *s;
	const char point = localeconv()->decimal_point[0];
	int cnv = 0;

	for (s = str, f = format; *f; ++f)
	{
		if (*f == '%')
		{
			int size = 0;
			int width = 0;
			int do_cnv = 1;

			if (*++f == '*')
				++f, do_cnv = 0;

			for (; isdigit((int)(unsigned int)*f); ++f)
				width *= 10, width += *f - '0';

			if (*f == 'h' || *f == 'l' || *f == 'L')
				size = *f++;

			if (*f != '[' && *f != 'c' && *f != 'n')
				while (isspace((int)(unsigned int)*s))
					++s;

#define COPY                         *b++ = *s++, --width
#define MATCH(cond)                  if (width && (cond)) COPY;
#define MATCH_ACTION(cond, action)   if (width && (cond)) { COPY; action; }
#define MATCHES_ACTION(cond, action) while (width && (cond)) { COPY; action; }
#define FAIL                         (cnv) ? cnv : EOF
			switch (*f)
			{
				case 'd': case 'i': case 'o': case 'u': case 'x': case 'X':
				case 'p':
				{
					static const char types[] = "diouxXp";
					static const int bases[] = { 10, 0, 8, 10, 16, 16, 16 };
					static const char digitset[] = "0123456789abcdefABCDEF";
					static const int setsizes[] = { 10, 0, 0, 0, 0, 0, 0, 0, 8, 0, 10, 0, 0, 0, 0, 0, 22 };
					int base = bases[strchr(types, *f) - types];
					int setsize;
					char buf[513];
					char *b = buf;
					int digit = 0;
					if (width <= 0 || width > 512) width = 512;
					MATCH(*s == '+' || *s == '-')
					MATCH_ACTION(*s == '0',
						digit = 1;
						MATCH_ACTION((*s == 'x' || *s == 'X') && (base == 0 || base == 16), base = 16) else base = 8;
					)
					setsize = setsizes[base];
					MATCHES_ACTION(memchr(digitset, *s, setsize), digit = 1)
					if (!digit) return FAIL;
					*b = '\0';
					if (do_cnv)
					{
						if (*f == 'd' || *f == 'i')
						{
							long data = strtol(buf, NULL, base);
							if (size == 'h')
								*va_arg(args, short *) = (short)data;
							else if (size == 'l')
								*va_arg(args, long *) = data;
							else
								*va_arg(args, int *) = (int)data;
						}
						else
						{
							unsigned long data = strtoul(buf, NULL, base);
							if (size == 'p')
								*va_arg(args, void **) = (void *)data;
							else if (size == 'h')
								*va_arg(args, unsigned short *) = (unsigned short)data;
							else if (size == 'l')
								*va_arg(args, unsigned long *) = data;
							else
								*va_arg(args, unsigned int *) = (unsigned int)data;
						}
						++cnv;
					}
					break;
				}

				case 'e': case 'E': case 'f': case 'g': case 'G':
				{
					char buf[513];
					char *b = buf;
					int digit = 0;
					if (width <= 0 || width > 512) width = 512;
					MATCH(*s == '+' || *s == '-')
					MATCHES_ACTION(isdigit((int)(unsigned int)*s), digit = 1)
					MATCH(*s == point)
					MATCHES_ACTION(isdigit((int)(unsigned int)*s), digit = 1)
					MATCHES_ACTION(digit && (*s == 'e' || *s == 'E'),
						MATCH(*s == '+' || *s == '-')
						digit = 0;
						MATCHES_ACTION(isdigit((int)(unsigned int)*s), digit = 1)
					)
					if (!digit) return FAIL;
					*b = '\0';
					if (do_cnv)
					{
						double data = strtod(buf, NULL);
						if (size == 'l')
							*va_arg(args, double *) = data;
						else if (size == 'L')
							*va_arg(args, long double *) = (long double)data;
						else
							*va_arg(args, float *) = (float)data;
						++cnv;
					}
					break;
				}

				case 's':
				{
					char *arg = va_arg(args, char *);
					if (width <= 0) width = INT_MAX;
					while (width-- && *s && !isspace((int)(unsigned int)*s))
						if (do_cnv) *arg++ = *s++;
					if (do_cnv) *arg = '\0', ++cnv;
					break;
				}

				case '[':
				{
					char *arg = va_arg(args, char *);
					int setcomp = 0;
					size_t setsize;
					const char *end;
					if (width <= 0) width = INT_MAX;
					if (*++f == '^') setcomp = 1, ++f;
					end = strchr((*f == ']') ? f + 1 : f, ']');
					if (!end) return FAIL; /* Could be cnv to match glibc-2.2 */
					setsize = end - f;     /* But FAIL matches the C standard */
					if (!*s) return FAIL;
					while (width-- && *s)
					{
						if (!setcomp && !memchr(f, *s, setsize)) break;
						if (setcomp && memchr(f, *s, setsize)) break;
						if (do_cnv) *arg++ = *s++;
					}
					if (do_cnv) *arg = '\0', ++cnv;
					f = end;
					break;
				}

				case 'c':
				{
					char *arg = va_arg(args, char *);
					if (width <= 0) width = 1;
					while (width--)
					{
						if (!*s) return FAIL;
						if (do_cnv) *arg++ = *s++;
					}
					if (do_cnv) ++cnv;
					break;
				}

				case 'n':
				{
					if (size == 'h')
						*va_arg(args, short *) = (short)(s - str);
					else if (size == 'l')
						*va_arg(args, long *) = (long)(s - str);
					else
						*va_arg(args, int *) = (int)(s - str);
					break;
				}

				case '%':
				{
					if (*s++ != '%') return cnv;
					break;
				}

				default:
					return FAIL;
			}
		}
		else if (isspace((int)(unsigned int)*f))
		{
			while (isspace((int)(unsigned int)f[1]))
				++f;
			while (isspace((int)(unsigned int)*s))
				++s;
		}
		else
		{
			if (*s++ != *f)
				return cnv;
		}
	}

	return cnv;
}

#ifdef TEST

#undef _ISOC9X_SOURCE
#undef __USE_ISOC9X
#include <math.h>
#include <float.h>

int test_sscanf(const char *str, const char *format, ...)
{
	int rc;
	va_list args;
	va_start(args, format);
	rc = vsscanf(str, format, args);
	va_end(args);
	return rc;
}

int main(int ac, char **av)
{
	int errors = 0;
	short si1, si2;
	int i1, i2;
	long li1, li2;
	float f1, f2;
	double d1, d2;
	long double ld1, ld2;
	void *p1, *p2;
	char b1[128], b2[128];
	char c1[128], c2[128];
	char s1[128], s2[128];
	short sn1, sn2;
	int in1, in2;
	long ln1, ln2;
	unsigned short su1, su2;
	unsigned int u1, u2;
	unsigned long lu1, lu2;
	char str[512];
	int rc1, rc2;

	if (ac == 2 && !strcmp(av[1], "help"))
	{
		printf("usage: %s [show]\n", *av);
		return EXIT_SUCCESS;
	}

	printf("Testing: %s\n", "vsscanf");

	/* Test one of everything */

	sprintf(str, " abc -12 37 101 3.4e-1 12.34 102.23 xyz %p def ghi jkl %% ",
		p1 = (void *)0xdeadbeef
	);

	rc1 = sscanf(str, " abc %hd %d %ld %e %le %Le xyz %p %127[^abc ] %3c %127s %hn %n%% %ln",
		&si1, &i1, &li1, &f1, &d1, &ld1, &p1, b1, c1, s1, &sn1, &in1, &ln1
	);

	rc2 = test_sscanf(str, " abc %hd %d %ld %e %le %Le xyz %p %127[^abc ] %3c %127s %hn %n%% %ln",
		&si2, &i2, &li2, &f2, &d2, &ld2, &p2, b2, c2, s2, &sn2, &in2, &ln2
	);

	if (ac >= 2 && !strcmp(av[1], "show"))
	{
		printf("in: %s\n", str);
		printf("rc1=%d, rc2=%d\n", rc1, rc2);
		printf("out1: si1<%hd> i1<%d> li1<%ld> f1<%e> d1<%le> ld1<%Le> p1<%p> b1<%s> c1<%s> s1<%s> sn1<%hd> in1<%d> ln1<%ld>\n",
			si1, i1, li1, f1, d1, ld1, p1, b1, c1, s1, sn1, in1, ln1
		);
		printf("out2: si2<%hd> i2<%d> li2<%ld> f2<%e> d2<%le> ld2<%Le> p2<%p> b2<%s> c2<%s> s2<%s> sn2<%hd> in2<%d> ln2<%ld>\n",
			si2, i2, li2, f2, d2, ld2, p2, b2, c2, s2, sn2, in2, ln2
		);
	}

	if (rc1 != rc2)
		++errors, printf("Test1: failed (returned %d, not %d)\n", rc2, rc1);
	if (si1 != si2)
		++errors, printf("Test2: failed (%%hd scanned %hd, not %hd)\n", si2, si1);
	if (i1 != i2)
		++errors, printf("Test3: failed (%%d scanned %d, not %d)\n", i2, i1);
	if (li1 != li2)
		++errors, printf("Test4: failed (%%ld scanned %ld, not %ld)\n", li2, li1);
	if (fabs(f2 - 3.4e-1) / 3.4e-1 >= 4 * FLT_EPSILON)
		++errors, printf("Test5: failed (%%e scanned %e, not %e)\n", f2, f1);
	if (fabs(d2 - 12.34) / 12.34 >= 4 * DBL_EPSILON)
		++errors, printf("Test6: failed (%%le scanned %le, not %le)\n", d2, d1);
	if (fabs(ld2 - 102.23) / 102.23 >= 4 * LDBL_EPSILON)
		++errors, printf("Test7: failed (%%Le scanned %Le, not %Le)\n", ld2, ld1);
	if (p1 != p2)
		++errors, printf("Test8: failed (%%p scanned %p, not %p)\n", p2, p1);
	if (strcmp(b1, b2))
		++errors, printf("Test9: failed (%%[^abc ] scanned \"%s\", not \"%s\")\n", b2, b1);
	if (memcmp(c1, c2, 3))
		++errors, printf("Test10: failed (%%3c scanned \"%3.3s\", not \"%3.3s\")\n", c2, c1);
	if (strcmp(s1, s2))
		++errors, printf("Test11: failed (%%s scanned \"%s\", not \"%s\")\n", s2, s1);
	if (sn1 != sn2)
		++errors, printf("Test12: failed (%%hn scanned %hd, not %hd)\n", sn2, sn1);
	if (in1 != in2)
		++errors, printf("Test13: failed (%%n scanned %d, not %d)\n", in2, in1);
	if (ln1 != ln2)
		++errors, printf("Test14: failed (%%ln scanned %ld, not %ld)\n", ln2, ln1);

	/* Test different numeric bases */

#define TEST_NUM(i, var, tst, str, format) \
	rc1 = sscanf(str, format, &s##var##1, &var##1, &l##var##1); \
	rc2 = test_sscanf(str, format, &s##var##2, &var##2, &l##var##2); \
	if (rc1 != rc2) \
		++errors, printf("Test%d: failed (returned %d, not %d)\n", (i), rc2, rc1); \
	if (s##var##1 != s##var##2) \
		++errors, printf("Test%d: failed (%%h%c scanned %hd, not %hd)\n", (i), tst, s##var##2, s##var##1); \
	if (var##1 != var##2) \
		++errors, printf("Test%d: failed (%%%c scanned %d, not %d)\n", (i), tst, var##2, var##1); \
	if (l##var##1 != l##var##2) \
		++errors, printf("Test%d: failed (%%l%c scanned %ld, not %ld)\n", (i), tst, l##var##2, l##var##1)

#define TEST_STR(i, len, str, format) \
	rc1 = sscanf(str, format, b1, c1, s1); \
	rc2 = test_sscanf(str, format, b2, c2, s2); \
	if (rc1 != rc2) \
		++errors, printf("Test%d: failed (returned %d, not %d)\n", (i), rc2, rc1); \
	if (strcmp(b1, b2)) \
		++errors, printf("Test%d: failed (%%%d[ scanned \"%s\", not \"%s\")\n", (i), (len), b2, b1); \
	if (memcmp(c1, c2, len)) \
		++errors, printf("Test%d: failed (%%%dc scanned \"%*.*s\", not \"%*.*s\")\n", (i), (len), (len), (len), c2, (len), (len), c1); \
	if (strcmp(s1, s2)) \
		++errors, printf("Test%d: failed (%%%ds scanned \"%s\", not \"%s\")\n", (i), (len), s2, s1)

	TEST_NUM(15, i, 'i', "37 21 53", "%hi %i %li");
	TEST_NUM(16, i, 'i', "037 021 053", "%hi %i %li");
	TEST_NUM(17, i, 'i', "0x37 0x21 0x53", "%hi %i %li");
	TEST_NUM(18, u, 'o', "037 021 053", "%ho %o %lo");
	TEST_NUM(19, u, 'u', "37 21 53", "%hu %u %lu");
	TEST_NUM(20, u, 'x', "0x37 0x21 0x53", "%hx %x %lx");
	TEST_NUM(21, u, 'X', "0x37 0x21 0x53", "%hx %x %lx");

	/* Test field width handling */

	TEST_NUM(22, i, 'd', "123456789", "%3hd %2d %4ld");
	TEST_NUM(23, i, 'i', "123456789", "%3hi %2i %4li");
	TEST_NUM(24, i, 'i', "012340789", "%3hi %2i %4li");
	TEST_NUM(25, i, 'x', "123456789", "%3hi %2i %4li");
	TEST_NUM(26, u, 'o', "012340789", "%3ho %2o %4lo");
	TEST_NUM(27, u, 'u', "123456789", "%3hu %2u %4lu");
	TEST_NUM(28, u, 'x', "123456789", "%3hx %2x %4lx");
	TEST_NUM(29, u, 'X', "123456789", "%3hx %2X %4lX");
	TEST_STR(30, 1, "abcd", "%1[a]%c%1s");

	TEST_STR(31, 5, "abc\001d e f g h i", "%5[][abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~!@#$%^&*()_=+\\|{};':\",./<>? 	-]%5c%5s");
	TEST_STR(32, 3, "abc\001d e f g h i", "%3[][abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~!@#$%^&*()_=+\\|{};':\",./<>? 	-]%3c%3s");
	TEST_STR(33, 7, "abc\001d e f g h i", "%7[][abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~!@#$%^&*()_=+\\|{};':\",./<>? 	-]%7c%7s");
	TEST_STR(34, 2, "abc\001d e f g h i", "%2[][abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`~!@#$%^&*()_=+\\|{};':\",./<>? 	-]%2c%2s");

	/* Test error reporting */

#define TEST_ERR(i, str, format) \
	rc1 = sscanf(str, format); \
	rc2 = test_sscanf(str, format); \
	if (rc1 != rc2) \
		++errors, printf("Test%d: failed (returned %d, not %d)\n", (i), rc2, rc1)

#define TEST_ERR_ARG(i, str, format, var) \
	rc1 = sscanf(str, format, &var##1); \
	rc2 = test_sscanf(str, format, &var##1); \
	if (rc1 != rc2) \
		++errors, printf("Test%d: failed (returned %d, not %d)\n", (i), rc2, rc1)

	TEST_ERR_ARG(35, "", "%d", i);
	TEST_ERR_ARG(36, "", "%i", i);
	TEST_ERR_ARG(37, "", "%o", u);
	TEST_ERR_ARG(38, "", "%u", u);
	TEST_ERR_ARG(39, "", "%x", u);
	TEST_ERR_ARG(40, "", "%X", u);
	TEST_ERR_ARG(41, "", "%p", p);
	TEST_ERR_ARG(42, "", "%e", f);
	TEST_ERR_ARG(43, "", "%E", f);
	TEST_ERR_ARG(44, "", "%f", f);
	TEST_ERR_ARG(45, "", "%g", f);
	TEST_ERR_ARG(46, "", "%G", f);
	TEST_ERR_ARG(47, "", "%[abc]", *b); /* This fails when comparing against glibc */
	TEST_ERR_ARG(48, "", "%c", *c);
	TEST_ERR(49, "a", "%%");
	TEST_ERR(50, "a", "b");

	if (errors)
		printf("%d/50 tests failed (This system's sscanf(3) might be wrong))\n", errors);
	else
		printf("All tests passed\n");

	return (errors == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}

#endif

/* vi:set ts=4 sw=4: */
