/*
   +----------------------------------------------------------------------+
   | Copyright (c) The PHP Group                                          |
   +----------------------------------------------------------------------+
   | This source file is subject to version 3.01 of the PHP license,      |
   | that is bundled with this package in the file LICENSE, and is        |
   | available through the world-wide-web at the following url:           |
   | https://www.php.net/license/3_01.txt                                 |
   | If you did not receive a copy of the PHP license and are unable to   |
   | obtain it through the world-wide-web, please send a note to          |
   | license@php.net so we can mail you a copy immediately.               |
   +----------------------------------------------------------------------+
   | Author: Stig S�ther Bakken <ssb@php.net>                             |
   +----------------------------------------------------------------------+
 */

#include <math.h>				/* modf() */
#include "php.h"
#include "zend_execute.h"

#include <locale.h>
#ifdef ZTS
#include "ext/standard/php_string.h" /* for localeconv_r() */
#define LCONV_DECIMAL_POINT (*lconv.decimal_point)
#else
#define LCONV_DECIMAL_POINT (*lconv->decimal_point)
#endif

#define ALIGN_LEFT 0
#define ALIGN_RIGHT 1
#define ADJ_WIDTH 1
#define ADJ_PRECISION 2
#define NUM_BUF_SIZE 500
#define FLOAT_PRECISION 6
#define MAX_FLOAT_PRECISION 53

#if 0
/* trick to control varargs functions through cpp */
# define PRINTF_DEBUG(arg) php_printf arg
#else
# define PRINTF_DEBUG(arg)
#endif

static const char hexchars[] = "0123456789abcdef";
static const char HEXCHARS[] = "0123456789ABCDEF";

/* php_spintf_appendchar() {{{ */
inline static void
php_sprintf_appendchar(zend_string **buffer, size_t *pos, char add)
{
	if ((*pos + 1) >= ZSTR_LEN(*buffer)) {
		PRINTF_DEBUG(("%s(): ereallocing buffer to %zu bytes\n", get_active_function_name(), ZSTR_LEN(*buffer)));
		*buffer = zend_string_extend(*buffer, ZSTR_LEN(*buffer) << 1, 0);
	}
	PRINTF_DEBUG(("sprintf: appending '%c', pos=%zu\n", add, *pos));
	ZSTR_VAL(*buffer)[(*pos)++] = add;
}
/* }}} */

/* php_spintf_appendchar() {{{ */
inline static void
php_sprintf_appendchars(zend_string **buffer, size_t *pos, char *add, size_t len)
{
	if ((*pos + len) >= ZSTR_LEN(*buffer)) {
		size_t nlen = ZSTR_LEN(*buffer);

		PRINTF_DEBUG(("%s(): ereallocing buffer to %zu bytes\n", get_active_function_name(), ZSTR_LEN(*buffer)));
		do {
			nlen = nlen << 1;
		} while ((*pos + len) >= nlen);
		*buffer = zend_string_extend(*buffer, nlen, 0);
	}
	PRINTF_DEBUG(("sprintf: appending \"%s\", pos=%zu\n", add, *pos));
	memcpy(ZSTR_VAL(*buffer) + (*pos), add, len);
	*pos += len;
}
/* }}} */

/* php_spintf_appendstring() {{{ */
inline static void
php_sprintf_appendstring(zend_string **buffer, size_t *pos, char *add,
						   size_t min_width, size_t max_width, char padding,
						   size_t alignment, size_t len, bool neg, int expprec, int always_sign)
{
	size_t npad;
	size_t req_size;
	size_t copy_len;
	size_t m_width;

	copy_len = (expprec ? MIN(max_width, len) : len);
	npad = (min_width < copy_len) ? 0 : min_width - copy_len;

	PRINTF_DEBUG(("sprintf: appendstring(%p, %zu, %zu, \"%s\", %zu, '%c', %zu)\n",
				  *buffer, *pos, ZSTR_LEN(*buffer), add, min_width, padding, alignment));
	m_width = MAX(min_width, copy_len);

	if(m_width > INT_MAX - *pos - 1) {
		zend_error_noreturn(E_ERROR, "Field width %zd is too long", m_width);
	}

	req_size = *pos + m_width + 1;

	if (req_size > ZSTR_LEN(*buffer)) {
		size_t size = ZSTR_LEN(*buffer);
		while (req_size > size) {
			if (size > ZEND_SIZE_MAX/2) {
				zend_error_noreturn(E_ERROR, "Field width %zd is too long", req_size);
			}
			size <<= 1;
		}
		PRINTF_DEBUG(("sprintf ereallocing buffer to %zu bytes\n", size));
		*buffer = zend_string_extend(*buffer, size, 0);
	}
	if (alignment == ALIGN_RIGHT) {
		if ((neg || always_sign) && padding=='0') {
			ZSTR_VAL(*buffer)[(*pos)++] = (neg) ? '-' : '+';
			add++;
			len--;
			copy_len--;
		}
		while (npad-- > 0) {
			ZSTR_VAL(*buffer)[(*pos)++] = padding;
		}
	}
	PRINTF_DEBUG(("sprintf: appending \"%s\"\n", add));
	memcpy(&ZSTR_VAL(*buffer)[*pos], add, copy_len + 1);
	*pos += copy_len;
	if (alignment == ALIGN_LEFT) {
		while (npad--) {
			ZSTR_VAL(*buffer)[(*pos)++] = padding;
		}
	}
}
/* }}} */

/* php_spintf_appendint() {{{ */
inline static void
php_sprintf_appendint(zend_string **buffer, size_t *pos, zend_long number,
						size_t width, char padding, size_t alignment,
						int always_sign)
{
	char numbuf[NUM_BUF_SIZE];
	zend_ulong magn, nmagn;
	unsigned int i = NUM_BUF_SIZE - 1, neg = 0;

	PRINTF_DEBUG(("sprintf: appendint(%p, %zu, %zu, " ZEND_LONG_FMT ", %zu, '%c', %zu)\n",
				  *buffer, *pos, ZSTR_LEN(*buffer), number, width, padding, alignment));
	if (number < 0) {
		neg = 1;
		magn = ((zend_ulong) -(number + 1)) + 1;
	} else {
		magn = (zend_ulong) number;
	}

	/* Can't right-pad 0's on integers */
	if(alignment==0 && padding=='0') padding=' ';

	numbuf[i] = '\0';

	do {
		nmagn = magn / 10;

		numbuf[--i] = (unsigned char)(magn - (nmagn * 10)) + '0';
		magn = nmagn;
	}
	while (magn > 0 && i > 1);
	if (neg) {
		numbuf[--i] = '-';
	} else if (always_sign) {
		numbuf[--i] = '+';
	}
	PRINTF_DEBUG(("sprintf: appending " ZEND_LONG_FMT " as \"%s\", i=%u\n",
				  number, &numbuf[i], i));
	php_sprintf_appendstring(buffer, pos, &numbuf[i], width, 0,
							 padding, alignment, (NUM_BUF_SIZE - 1) - i,
							 neg, 0, always_sign);
}
/* }}} */

/* php_spintf_appenduint() {{{ */
inline static void
php_sprintf_appenduint(zend_string **buffer, size_t *pos,
					   zend_ulong number,
					   size_t width, char padding, size_t alignment)
{
	char numbuf[NUM_BUF_SIZE];
	zend_ulong magn, nmagn;
	unsigned int i = NUM_BUF_SIZE - 1;

	PRINTF_DEBUG(("sprintf: appenduint(%p, %zu, %zu, " ZEND_LONG_FMT ", %zu, '%c', %zu)\n",
				  *buffer, *pos, ZSTR_LEN(*buffer), number, width, padding, alignment));
	magn = (zend_ulong) number;

	/* Can't right-pad 0's on integers */
	if (alignment == 0 && padding == '0') padding = ' ';

	numbuf[i] = '\0';

	do {
		nmagn = magn / 10;

		numbuf[--i] = (unsigned char)(magn - (nmagn * 10)) + '0';
		magn = nmagn;
	} while (magn > 0 && i > 0);

	PRINTF_DEBUG(("sprintf: appending " ZEND_LONG_FMT " as \"%s\", i=%d\n", number, &numbuf[i], i));
	php_sprintf_appendstring(buffer, pos, &numbuf[i], width, 0,
							 padding, alignment, (NUM_BUF_SIZE - 1) - i, /* neg */ false, 0, 0);
}
/* }}} */

/* php_spintf_appenddouble() {{{ */
inline static void
php_sprintf_appenddouble(zend_string **buffer, size_t *pos,
						 double number,
						 size_t width, char padding,
						 size_t alignment, int precision,
						 int adjust, char fmt,
						 int always_sign
						)
{
	char num_buf[NUM_BUF_SIZE];
	char *s = NULL;
	size_t s_len = 0;
	bool is_negative = false;
#ifdef ZTS
	struct lconv lconv;
#else
	struct lconv *lconv;
#endif

	PRINTF_DEBUG(("sprintf: appenddouble(%p, %zu, %zu, %f, %zu, '%c', %zu, %c)\n",
				  *buffer, *pos, ZSTR_LEN(*buffer), number, width, padding, alignment, fmt));
	if ((adjust & ADJ_PRECISION) == 0) {
		precision = FLOAT_PRECISION;
	} else if (precision > MAX_FLOAT_PRECISION) {
		php_error_docref(NULL, E_NOTICE, "Requested precision of %d digits was truncated to PHP maximum of %d digits", precision, MAX_FLOAT_PRECISION);
		precision = MAX_FLOAT_PRECISION;
	}

	if (zend_isnan(number)) {
		is_negative = (number<0);
		php_sprintf_appendstring(buffer, pos, "NaN", 3, 0, padding,
								 alignment, 3, is_negative, 0, always_sign);
		return;
	}

	if (zend_isinf(number)) {
		is_negative = (number<0);		
		char *str = is_negative ? "-INF" : "INF";
		php_sprintf_appendstring(buffer, pos, str, strlen(str), 0, padding,
								alignment, strlen(str), is_negative, 0, always_sign);
		return;
	}

	switch (fmt) {
		case 'e':
		case 'E':
		case 'f':
		case 'F':
#ifdef ZTS
			localeconv_r(&lconv);
#else
			lconv = localeconv();
#endif
			s = php_conv_fp((fmt == 'f')?'F':fmt, number, 0, precision,
						(fmt == 'f')?LCONV_DECIMAL_POINT:'.',
						&is_negative, &num_buf[1], &s_len);
			if (is_negative) {
				num_buf[0] = '-';
				s = num_buf;
				s_len++;
			} else if (always_sign) {
				num_buf[0] = '+';
				s = num_buf;
				s_len++;
			}
			break;

		case 'g':
		case 'G':
		case 'h':
		case 'H':
		{
			if (precision == 0)
				precision = 1;

			char decimal_point = '.';
			if (fmt == 'g' || fmt == 'G') {
#ifdef ZTS
				localeconv_r(&lconv);
#else
				lconv = localeconv();
#endif
				decimal_point = LCONV_DECIMAL_POINT;
			}

			char exp_char = fmt == 'G' || fmt == 'H' ? 'E' : 'e';
			/* We use &num_buf[ 1 ], so that we have room for the sign. */
			s = zend_gcvt(number, precision, decimal_point, exp_char, &num_buf[1]);
			is_negative = 0;
			if (*s == '-') {
				is_negative = 1;
				s = &num_buf[1];
			} else if (always_sign) {
				num_buf[0] = '+';
				s = num_buf;
			}

			s_len = strlen(s);
			break;
		}
	}

	php_sprintf_appendstring(buffer, pos, s, width, 0, padding,
							 alignment, s_len, is_negative, 0, always_sign);
}
/* }}} */

/* php_spintf_appendd2n() {{{ */
inline static void
php_sprintf_append2n(zend_string **buffer, size_t *pos, zend_long number,
					 size_t width, char padding, size_t alignment, int n,
					 const char *chartable, int expprec)
{
	char numbuf[NUM_BUF_SIZE];
	zend_ulong num;
	zend_ulong  i = NUM_BUF_SIZE - 1;
	int andbits = (1 << n) - 1;

	PRINTF_DEBUG(("sprintf: append2n(%p, %zu, %zu, " ZEND_LONG_FMT ", %zu, '%c', %zu, %d, %p)\n",
				  *buffer, *pos, ZSTR_LEN(*buffer), number, width, padding, alignment, n,
				  chartable));
	PRINTF_DEBUG(("sprintf: append2n 2^%d andbits=%x\n", n, andbits));

	num = (zend_ulong) number;
	numbuf[i] = '\0';

	do {
		numbuf[--i] = chartable[(num & andbits)];
		num >>= n;
	}
	while (num > 0);

	php_sprintf_appendstring(buffer, pos, &numbuf[i], width, 0,
							 padding, alignment, (NUM_BUF_SIZE - 1) - i,
							 /* neg */ false, expprec, 0);
}
/* }}} */

/* php_spintf_getnumber() {{{ */
inline static int
php_sprintf_getnumber(char **buffer, size_t *len)
{
	char *endptr;
	zend_long num = ZEND_STRTOL(*buffer, &endptr, 10);
	size_t i;

	if (endptr != NULL) {
		i = (endptr - *buffer);
		*len -= i;
		*buffer = endptr;
	}
	PRINTF_DEBUG(("sprintf_getnumber: number was %zu bytes long\n", i));

	if (num >= INT_MAX || num < 0) {
		return -1;
	} else {
		return (int) num;
	}
}
/* }}} */

#define ARG_NUM_NEXT -1
#define ARG_NUM_INVALID -2

int php_sprintf_get_argnum(char **format, size_t *format_len) {
	char *temppos = *format;
	while (isdigit((int) *temppos)) temppos++;
	if (*temppos != '$') {
		return ARG_NUM_NEXT;
	}

	int argnum = php_sprintf_getnumber(format, format_len);
	if (argnum <= 0) {
		zend_value_error("Argument number specifier must be greater than zero and less than %d", INT_MAX);
		return ARG_NUM_INVALID;
	}

	(*format)++;  /* skip the '$' */
	(*format_len)--;
	return argnum - 1;
}

/* php_formatted_print() {{{
 * New sprintf implementation for PHP.
 *
 * Modifiers:
 *
 *  " "   pad integers with spaces
 *  "-"   left adjusted field
 *   n    field size
 *  "."n  precision (floats only)
 *  "+"   Always place a sign (+ or -) in front of a number
 *
 * Type specifiers:
 *
 *  "%"   literal "%", modifiers are ignored.
 *  "b"   integer argument is printed as binary
 *  "c"   integer argument is printed as a single character
 *  "d"   argument is an integer
 *  "f"   the argument is a float
 *  "o"   integer argument is printed as octal
 *  "s"   argument is a string
 *  "x"   integer argument is printed as lowercase hexadecimal
 *  "X"   integer argument is printed as uppercase hexadecimal
 *
 * nb_additional_parameters is used for throwing errors:
 *  - -1: ValueError is thrown (for vsprintf where args originates from an array)
 *  - 0 or more: ArgumentCountError is thrown
 */
static zend_string *
php_formatted_print(char *format, size_t format_len, zval *args, int argc, int nb_additional_parameters)
{
	size_t size = 240, outpos = 0;
	int alignment, currarg, adjusting, argnum, width, precision;
	char *temppos, padding;
	zend_string *result;
	int always_sign;
	int max_missing_argnum = -1;

	/* For debugging */
	const char *format_orig = format;
	ZEND_IGNORE_VALUE(format_orig);

	result = zend_string_alloc(size, 0);

	currarg = 0;
	argnum = 0;

	while (format_len) {
		int expprec;
		zval *tmp;

		temppos = memchr(format, '%', format_len);
		if (!temppos) {
			php_sprintf_appendchars(&result, &outpos, format, format_len);
			break;
		} else if (temppos != format) {
			php_sprintf_appendchars(&result, &outpos, format, temppos - format);
			format_len -= temppos - format;
			format = temppos;
		}
		format++;			/* skip the '%' */
		format_len--;

		if (*format == '%') {
			php_sprintf_appendchar(&result, &outpos, '%');
			format++;
			format_len--;
		} else {
			/* starting a new format specifier, reset variables */
			alignment = ALIGN_RIGHT;
			adjusting = 0;
			padding = ' ';
			always_sign = 0;
			expprec = 0;

			PRINTF_DEBUG(("sprintf: first looking at '%c', inpos=%zu\n",
						  *format, format - format_orig));
			if (isalpha((int)*format)) {
				width = precision = 0;
				argnum = ARG_NUM_NEXT;
			} else {
				/* first look for argnum */
				argnum = php_sprintf_get_argnum(&format, &format_len);
				if (argnum == ARG_NUM_INVALID) {
					goto fail;
				}

				/* after argnum comes modifiers */
				PRINTF_DEBUG(("sprintf: looking for modifiers\n"
							  "sprintf: now looking at '%c', inpos=%zu\n",
							  *format, format - format_orig));
				for (;; format++, format_len--) {
					if (*format == ' ' || *format == '0') {
						padding = *format;
					} else if (*format == '-') {
						alignment = ALIGN_LEFT;
						/* space padding, the default */
					} else if (*format == '+') {
						always_sign = 1;
					} else if (*format == '\'') {
						if (format_len > 1) {
							format++;
							format_len--;
							padding = *format;
						} else {
							zend_value_error("Missing padding character");
							goto fail;
						}
					} else {
						PRINTF_DEBUG(("sprintf: end of modifiers\n"));
						break;
					}
				}
				PRINTF_DEBUG(("sprintf: padding='%c'\n", padding));
				PRINTF_DEBUG(("sprintf: alignment=%s\n",
							  (alignment == ALIGN_LEFT) ? "left" : "right"));


				/* after modifiers comes width */
				if (*format == '*') {
					format++;
					format_len--;

					int width_argnum = php_sprintf_get_argnum(&format, &format_len);
					if (width_argnum == ARG_NUM_INVALID) {
						goto fail;
					}
					if (width_argnum == ARG_NUM_NEXT) {
						width_argnum = currarg++;
					}
					if (width_argnum >= argc) {
						max_missing_argnum = MAX(max_missing_argnum, width_argnum);
						continue;
					}
					tmp = &args[width_argnum];
					ZVAL_DEREF(tmp);
					if (Z_TYPE_P(tmp) != IS_LONG) {
						zend_value_error("Width must be an integer");
						goto fail;
					}
					if (Z_LVAL_P(tmp) < 0 || Z_LVAL_P(tmp) > INT_MAX) {
						zend_value_error("Width must be between 0 and %d", INT_MAX);
						goto fail;
					}
					width = Z_LVAL_P(tmp);
					adjusting |= ADJ_WIDTH;
				} else if (isdigit((int)*format)) {
					PRINTF_DEBUG(("sprintf: getting width\n"));
					if ((width = php_sprintf_getnumber(&format, &format_len)) < 0) {
						zend_value_error("Width must be between 0 and %d", INT_MAX);
						goto fail;
					}
					adjusting |= ADJ_WIDTH;
				} else {
					width = 0;
				}
				PRINTF_DEBUG(("sprintf: width=%d\n", width));

				/* after width and argnum comes precision */
				if (*format == '.') {
					format++;
					format_len--;
					PRINTF_DEBUG(("sprintf: getting precision\n"));
					if (*format == '*') {
						format++;
						format_len--;

						int prec_argnum = php_sprintf_get_argnum(&format, &format_len);
						if (prec_argnum == ARG_NUM_INVALID) {
							goto fail;
						}
						if (prec_argnum == ARG_NUM_NEXT) {
							prec_argnum = currarg++;
						}
						if (prec_argnum >= argc) {
							max_missing_argnum = MAX(max_missing_argnum, prec_argnum);
							continue;
						}
						tmp = &args[prec_argnum];
						ZVAL_DEREF(tmp);
						if (Z_TYPE_P(tmp) != IS_LONG) {
							zend_value_error("Precision must be an integer");
							goto fail;
						}
						if (Z_LVAL_P(tmp) < -1 || Z_LVAL_P(tmp) > INT_MAX) {
							zend_value_error("Precision must be between -1 and %d", INT_MAX);
							goto fail;
						}
						precision = Z_LVAL_P(tmp);
						adjusting |= ADJ_PRECISION;
						expprec = 1;
					} else if (isdigit((int)*format)) {
						if ((precision = php_sprintf_getnumber(&format, &format_len)) < 0) {
							zend_value_error("Precision must be between 0 and %d", INT_MAX);
							goto fail;
						}
						adjusting |= ADJ_PRECISION;
						expprec = 1;
					} else {
						precision = 0;
					}
				} else {
					precision = 0;
				}
				PRINTF_DEBUG(("sprintf: precision=%d\n", precision));
			}

			if (*format == 'l') {
				format++;
				format_len--;
			}
			PRINTF_DEBUG(("sprintf: format character='%c'\n", *format));

			if (argnum == ARG_NUM_NEXT) {
				argnum = currarg++;
			}
			if (argnum >= argc) {
				max_missing_argnum = MAX(max_missing_argnum, argnum);
				continue;
			}

			if (expprec && precision == -1
					&& *format != 'g' && *format != 'G' && *format != 'h' && *format != 'H') {
				zend_value_error("Precision -1 is only supported for %%g, %%G, %%h and %%H");
				goto fail;
			}

			/* now we expect to find a type specifier */
			tmp = &args[argnum];
			switch (*format) {
				case 's': {
					zend_string *t;
					zend_string *str = zval_get_tmp_string(tmp, &t);
					php_sprintf_appendstring(&result, &outpos,
											 ZSTR_VAL(str),
											 width, precision, padding,
											 alignment,
											 ZSTR_LEN(str),
											 /* neg */ false, expprec, 0);
					zend_tmp_string_release(t);
					break;
				}

				case 'd':
					php_sprintf_appendint(&result, &outpos,
										  zval_get_long(tmp),
										  width, padding, alignment,
										  always_sign);
					break;

				case 'u':
					php_sprintf_appenduint(&result, &outpos,
										  zval_get_long(tmp),
										  width, padding, alignment);
					break;

				case 'e':
				case 'E':
				case 'f':
				case 'F':
				case 'g':
				case 'G':
				case 'h':
				case 'H':
					php_sprintf_appenddouble(&result, &outpos,
											 zval_get_double(tmp),
											 width, padding, alignment,
											 precision, adjusting,
											 *format, always_sign
											);
					break;

				case 'c':
					php_sprintf_appendchar(&result, &outpos,
										(char) zval_get_long(tmp));
					break;

				case 'o':
					php_sprintf_append2n(&result, &outpos,
										 zval_get_long(tmp),
										 width, padding, alignment, 3,
										 hexchars, expprec);
					break;

				case 'x':
					php_sprintf_append2n(&result, &outpos,
										 zval_get_long(tmp),
										 width, padding, alignment, 4,
										 hexchars, expprec);
					break;

				case 'X':
					php_sprintf_append2n(&result, &outpos,
										 zval_get_long(tmp),
										 width, padding, alignment, 4,
										 HEXCHARS, expprec);
					break;

				case 'b':
					php_sprintf_append2n(&result, &outpos,
										 zval_get_long(tmp),
										 width, padding, alignment, 1,
										 hexchars, expprec);
					break;

				case '%':
					php_sprintf_appendchar(&result, &outpos, '%');

					break;

				case '\0':
					if (!format_len) {
						zend_value_error("Missing format specifier at end of string");
						goto fail;
					}
					ZEND_FALLTHROUGH;

				default:
					zend_value_error("Unknown format specifier \"%c\"", *format);
					goto fail;
			}
			format++;
			format_len--;
		}
	}

	if (max_missing_argnum >= 0) {
		if (nb_additional_parameters == -1) {
			zend_value_error("The arguments array must contain %d items, %d given", max_missing_argnum + 1, argc);
		} else {
			zend_argument_count_error("%d arguments are required, %d given", max_missing_argnum + nb_additional_parameters + 1, argc + nb_additional_parameters);
		}
		goto fail;
	}

	/* possibly, we have to make sure we have room for the terminating null? */
	ZSTR_VAL(result)[outpos]=0;
	ZSTR_LEN(result) = outpos;
	return result;

fail:
	zend_string_efree(result);
	return NULL;
}
/* }}} */

/* php_formatted_print_get_array() {{{ */
static zval *php_formatted_print_get_array(zend_array *array, int *argc)
{
	zval *args, *zv;
	int n;

	n = zend_hash_num_elements(array);
	args = (zval *)safe_emalloc(n, sizeof(zval), 0);
	n = 0;
	ZEND_HASH_FOREACH_VAL(array, zv) {
		ZVAL_COPY_VALUE(&args[n], zv);
		n++;
	} ZEND_HASH_FOREACH_END();

	*argc = n;
	return args;
}
/* }}} */

/* {{{ Return a formatted string */
PHP_FUNCTION(sprintf)
{
	zend_string *result;
	char *format;
	size_t format_len;
	zval *args;
	int argc;

	ZEND_PARSE_PARAMETERS_START(1, -1)
		Z_PARAM_STRING(format, format_len)
		Z_PARAM_VARIADIC('*', args, argc)
	ZEND_PARSE_PARAMETERS_END();

	result = php_formatted_print(format, format_len, args, argc, 1);
	if (result == NULL) {
		RETURN_THROWS();
	}
	RETVAL_STR(result);
}
/* }}} */

/* {{{ Return a formatted string */
PHP_FUNCTION(vsprintf)
{
	zend_string *result;
	char *format;
	size_t format_len;
	zval *args;
	zend_array *array;
	int argc;

	ZEND_PARSE_PARAMETERS_START(2, 2)
		Z_PARAM_STRING(format, format_len)
		Z_PARAM_ARRAY_HT(array)
	ZEND_PARSE_PARAMETERS_END();

	args = php_formatted_print_get_array(array, &argc);

	result = php_formatted_print(format, format_len, args, argc, -1);
	efree(args);
	if (result == NULL) {
		RETURN_THROWS();
	}
	RETVAL_STR(result);
}
/* }}} */

/* {{{ Output a formatted string */
PHP_FUNCTION(printf)
{
	zend_string *result;
	size_t rlen;
	char *format;
	size_t format_len;
	zval *args;
	int argc;

	ZEND_PARSE_PARAMETERS_START(1, -1)
		Z_PARAM_STRING(format, format_len)
		Z_PARAM_VARIADIC('*', args, argc)
	ZEND_PARSE_PARAMETERS_END();

	result = php_formatted_print(format, format_len, args, argc, 1);
	if (result == NULL) {
		RETURN_THROWS();
	}
	rlen = PHPWRITE(ZSTR_VAL(result), ZSTR_LEN(result));
	zend_string_efree(result);
	RETURN_LONG(rlen);
}
/* }}} */

/* {{{ Output a formatted string */
PHP_FUNCTION(vprintf)
{
	zend_string *result;
	size_t rlen;
	char *format;
	size_t format_len;
	zval *args;
	zend_array *array;
	int argc;

	ZEND_PARSE_PARAMETERS_START(2, 2)
		Z_PARAM_STRING(format, format_len)
		Z_PARAM_ARRAY_HT(array)
	ZEND_PARSE_PARAMETERS_END();

	args = php_formatted_print_get_array(array, &argc);

	result = php_formatted_print(format, format_len, args, argc, -1);
	efree(args);
	if (result == NULL) {
		RETURN_THROWS();
	}
	rlen = PHPWRITE(ZSTR_VAL(result), ZSTR_LEN(result));
	zend_string_efree(result);
	RETURN_LONG(rlen);
}
/* }}} */

/* {{{ Output a formatted string into a stream */
PHP_FUNCTION(fprintf)
{
	php_stream *stream;
	char *format;
	size_t format_len;
	zval *arg1, *args;
	int argc;
	zend_string *result;

	ZEND_PARSE_PARAMETERS_START(2, -1)
		Z_PARAM_RESOURCE(arg1)
		Z_PARAM_STRING(format, format_len)
		Z_PARAM_VARIADIC('*', args, argc)
	ZEND_PARSE_PARAMETERS_END();

	php_stream_from_zval(stream, arg1);

	result = php_formatted_print(format, format_len, args, argc, 2);
	if (result == NULL) {
		RETURN_THROWS();
	}

	php_stream_write(stream, ZSTR_VAL(result), ZSTR_LEN(result));

	RETVAL_LONG(ZSTR_LEN(result));
	zend_string_efree(result);
}
/* }}} */

/* {{{ Output a formatted string into a stream */
PHP_FUNCTION(vfprintf)
{
	php_stream *stream;
	char *format;
	size_t format_len;
	zval *arg1, *args;
	zend_array *array;
	int argc;
	zend_string *result;

	ZEND_PARSE_PARAMETERS_START(3, 3)
		Z_PARAM_RESOURCE(arg1)
		Z_PARAM_STRING(format, format_len)
		Z_PARAM_ARRAY_HT(array)
	ZEND_PARSE_PARAMETERS_END();

	php_stream_from_zval(stream, arg1);

	args = php_formatted_print_get_array(array, &argc);

	result = php_formatted_print(format, format_len, args, argc, -1);
	efree(args);
	if (result == NULL) {
		RETURN_THROWS();
	}

	php_stream_write(stream, ZSTR_VAL(result), ZSTR_LEN(result));

	RETVAL_LONG(ZSTR_LEN(result));
	zend_string_efree(result);
}
/* }}} */
