/* -*- linux-c -*-
 * vsprintf.c
 * Copyright (C) 2006, 2008-2019 Red Hat Inc.
 * Based on code from the Linux kernel
 * Copyright (C) 1991, 1992  Linus Torvalds
 *
 * This file is part of systemtap, and is free software.  You can
 * redistribute it and/or modify it under the terms of the GNU General
 * Public License (GPL); either version 2, or (at your option) any
 * later version.
 */
#ifndef _VSPRINTF_C_
#define _VSPRINTF_C_

#include "print.h"

static int skip_atoi(const char **s)
{
	int i=0;
	while (isdigit(**s))
		i = i*10 + *((*s)++) - '0';
	return i;
}

/*
 * Changes to number() will require a corresponding change to number_size below,
 * to ensure proper buffer allocation for _stp_printf.
 */
noinline static char * 
number(char * buf, char * end, uint64_t num, int base, int size, int precision, enum print_flag type)
{
	char c,sign,tmp[66];
	const char *digits;
	static const char small_digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
	static const char large_digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
	int i;

	digits = (type & STP_LARGE) ? large_digits : small_digits;
	if (type & STP_LEFT)
		type &= ~STP_ZEROPAD;
	if (base < 2 || base > 36)
		return NULL;
	c = (type & STP_ZEROPAD) ? '0' : ' ';
	sign = 0;
	if (type & STP_SIGN) {
		if ((int64_t) num < 0) {
			sign = '-';
			num = - (int64_t) num;
			size--;
		} else if (type & STP_PLUS) {
			sign = '+';
			size--;
		} else if (type & STP_SPACE) {
			sign = ' ';
			size--;
		}
	}
	if (type & STP_SPECIAL) {
		if (base == 16)
			size -= 2;
		else if (base == 8)
			size--;
	}
	i = 0;
	if (num == 0)
		tmp[i++]='0';
	else while (num != 0)
		tmp[i++] = digits[do_div(num,base)];
	if (i > precision)
		precision = i;
	size -= precision;
	if (!(type&(STP_ZEROPAD+STP_LEFT))) {
		while(size-->0) {
			if (buf <= end)
				*buf = ' ';
			++buf;
		}
	}
	if (sign) {
		if (buf <= end)
			*buf = sign;
		++buf;
	}
	if (type & STP_SPECIAL) {
		if (base==8) {
			if (buf <= end)
				*buf = '0';
			++buf;
		} else if (base==16) {
			if (buf <= end)
				*buf = '0';
			++buf;
			if (buf <= end)
				*buf = digits[33];
			++buf;
		}
	}
	if (!(type & STP_LEFT)) {
		while (size-- > 0) {
			if (buf <= end)
				*buf = c;
			++buf;
		}
	}
	while (i < precision--) {
		if (buf <= end)
			*buf = '0';
		++buf;
	}
	while (i-- > 0) {
		if (buf <= end)
			*buf = tmp[i];
		++buf;
	}
	while (size-- > 0) {
		if (buf <= end)
			*buf = ' ';
		++buf;
	}
	return buf;
}

/*
 * Calculates the number of bytes required to print the paramater num. A change to 
 * number() requires a corresponding change here, and vice versa, to ensure the 
 * calculated size and printed size match.
 */
noinline static int
number_size(uint64_t num, int base, int size, int precision, enum print_flag type) {
    char c,sign,tmp[66];
    const char *digits;
    static const char small_digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
    static const char large_digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    int i, num_bytes = 0;

    digits = (type & STP_LARGE) ? large_digits : small_digits;
    if (type & STP_LEFT)
            type &= ~STP_ZEROPAD;
    if (base < 2 || base > 36)
            return 0;
    c = (type & STP_ZEROPAD) ? '0' : ' ';
    sign = 0;
    if (type & STP_SIGN) {
            if ((int64_t) num < 0) {
                    sign = '-';
                    num = - (int64_t) num;
                    size--;
            } else if (type & STP_PLUS) {
                    sign = '+';
                    size--;
            } else if (type & STP_SPACE) {
                    sign = ' ';
                    size--;
            }
    }
    if (type & STP_SPECIAL) {
            if (base == 16)
                    size -= 2;
            else if (base == 8)
                    size--;
    }
    i = 0;
    if (num == 0)
            tmp[i++]='0';
    else while (num != 0)
            tmp[i++] = digits[do_div(num,base)];
    if (i > precision)
            precision = i;
    size -= precision;
    if (!(type&(STP_ZEROPAD+STP_LEFT))) {
            while(size-->0) {
              num_bytes++;
            }
    }
    if (sign) {
      num_bytes++;
    }
    if (type & STP_SPECIAL) {
            if (base==8) {
                    num_bytes++;
            } else if (base==16) {
                    num_bytes+=2;
            }
    }
    if (!(type & STP_LEFT)) {
            while (size-- > 0) {
                    num_bytes++;
            }
    }
    while (i < precision--) {
            num_bytes++;
    }
    while (i-- > 0) {
            num_bytes++;
    }
    while (size-- > 0) {
            num_bytes++;
    }
    return num_bytes;

}


/*
 * Output one character into the buffer.  Usually this is just a
 * straight copy, padded left or right up to 'width', but if the user
 * gave the '#' flag then we need to escape special characters.
 */
noinline static char *
_stp_vsprint_char(char * str, char * end, char c,
		  int width, enum print_flag flags)
{
	int size = _stp_vsprint_char_size(c, 0, flags);

	if (!(flags & STP_LEFT)) {
		while (width-- > size) {
			if (str <= end)
				*str = ' ';
			++str;
		}
	}

	if (size == 1) {
		if (str <= end)
			*str = c;
		++str;
	}
	else {
		/* Other sizes mean this is not a printable character.
		 * First try to match up C escape characters: */
		char escape = 0;
		switch (c) {
			case '\a':
				escape = 'a';
				break;
			case '\b':
				escape = 'b';
				break;
			case '\f':
				escape = 'f';
				break;
			case '\n':
				escape = 'n';
				break;
			case '\r':
				escape = 'r';
				break;
			case '\t':
				escape = 't';
				break;
			case '\v':
				escape = 'v';
				break;
			case '\'':
				escape = '\'';
				break;
			case '\\':
				escape = '\\';
				break;
		}

		if (str <= end)
			*str = '\\';
		++str;
		if (escape) {
			if (str <= end)
				*str = escape;
			++str;
		}
		else {
			/* Fall back to octal for everything else */
			if (str <= end)
				*str = to_oct_digit((c >> 6) & 03);
			++str;
			if (str <= end)
				*str = to_oct_digit((c >> 3) & 07);
			++str;
			if (str <= end)
				*str = to_oct_digit(c & 07);
			++str;
		}
	}

	while (width-- > size) {
		if (str <= end)
			*str = ' ';
		++str;
	}

	return str;
}


/*
 * Compute the size of a given character in the buffer.  Usually this is
 * just 1 (padded up to 'width'), but if the user gave the '#' flag then
 * we need to escape special characters.
 */
noinline static int
_stp_vsprint_char_size(char c, int width, enum print_flag flags)
{
	int size = 1;

	/* look for quoteworthy characters */
	if ((flags & STP_SPECIAL) &&
	    (!(isprint(c) && isascii(c)) || c == '\'' || c == '\\'))
		switch (c) {
			case '\a':
			case '\b':
			case '\f':
			case '\n':
			case '\r':
			case '\t':
			case '\v':
			case '\'':
			case '\\':
				/* backslash and one escape character */
				size = 2;
				break;
			default:
				/* backslash and three octal digits */
				size = 4;
				break;
		}

	return max(size, width);
}


noinline static char *
_stp_vsprint_memory(char * str, char * end, const char * ptr,
		    int width, int precision,
		    char format, enum print_flag flags)
{
	int i, len = 0;
	struct context * __restrict__ c;

	if (format == 's') {
		if ((unsigned long)ptr < PAGE_SIZE)
			ptr = "<NULL>";
		len = strnlen(ptr, clamp_t(size_t, precision, 0, STP_BUFFER_SIZE));                
	}
	else if (precision > 0)
		len = precision;
	else
		len = 1;

	if (!(flags & STP_LEFT)) {
		int actlen = len;
		if (format == 'M')
			actlen = len * 2;
		while (actlen < width-- && str <= end)
			*str++ = ' ';
	}

	if (format == 'M') { /* stolen from kernel: trace_seq_putmem_hex() */
		static const char _stp_hex_asc[] = "0123456789abcdef";

                /* PR13386: Skip if called with null context */
		c = _stp_runtime_get_context();
                if (c) for (i = 0; i < len && str < end; i++) {
		    unsigned char c_tmp;
			  if (flags & STP_SPECIAL)
			    c_tmp = uread((unsigned char *)(ptr));
			  else
			    c_tmp = kread((unsigned char *)(ptr));
			  ptr++;
			*str++ = _stp_hex_asc[(c_tmp & 0xf0) >> 4];
			*str++ = _stp_hex_asc[(c_tmp & 0x0f)];
		}
		len = len * 2; /* the actual length */
	}
	else if (format == 'm') {
                /* PR13386: Skip if called with null context */
		c = _stp_runtime_get_context();
		if (c) for (i = 0; i < len && str <= end; ++i) {
		    if (flags & STP_SPECIAL)
			*str++ = uread((unsigned char *)(ptr));
                    else
		        *str++ = kread((unsigned char *)(ptr));
		    ptr++;
		}
	}
	else				/* %s format */
		for (i = 0; i < len && str <= end; ++i)
			*str++ = *ptr++;

	while (len < width-- && str <= end)
		*str++ = ' ';

	if (flags & STP_ZEROPAD && str <= end)
		*str++ = '\0';

	return str;

	/* We've caught a deref fault.  Make sure the string is null
	 * terminated. and return. */
deref_fault:
	if (str <= end)
		*str = '\0';
	return NULL;
}

noinline static int
_stp_vsprint_memory_size(const char * ptr, int width, int precision,
			 char format, enum print_flag flags)
{
	int len = 0;

	if (format == 's') {
		if ((unsigned long)ptr < PAGE_SIZE)
			ptr = "<NULL>";
		len = strnlen(ptr, clamp_t(size_t, precision, 0, STP_BUFFER_SIZE));
	}
	else if (precision > 0)
		len = precision;
	else
		len = 1;

	if (format == 'M')
		len = len * 2; /* hex dump print size */

	len = max(len, width);

	if (flags & STP_ZEROPAD)
		len++;

	return len;
}

static int check_binary_precision (int precision) {
  /* precision can be unspecified (-1) or one of 1, 2, 4 or 8.  */
  switch (precision) {
  case -1:
  case 1:
  case 2:
  case 4:
  case 8:
    break;
  default:
    precision = -1;
    break;
  }
  return precision;
}

noinline static char *
_stp_vsprint_binary(char * str, char * end, int64_t num,
		    int width, int precision, enum print_flag flags)
{
	/* Only certain values are valid for the precision.  */
	precision = check_binary_precision (precision);

	/* Unspecified field width defaults to the specified
	   precision and vice versa. If neither is specified,
	   then both default to 8.  */
	if (width == -1) {
		if (precision == -1) {
			width = 8;
			precision = 8;
		}
		else
			width = precision;
	}
	else if (precision == -1) {
		precision = check_binary_precision (width);
		if (precision == -1)
			precision = 8;
	}

	if (!(flags & STP_LEFT))
		while (precision < width-- && str <= end)
			*str++ = '\0';

	if ((str + precision - 1) <= end) {
#ifdef __ia64__
		memcpy(str, &num, precision); //to prevent unaligned access
#else
		switch(precision) {
			case 1:
				*(int8_t *)str = (int8_t)num;
				break;
			case 2:
				*(int16_t *)str = (int16_t)num;
				break;
			case 4:
				*(int32_t *)str = (int32_t)num;
				break;
			default: // "%.8b" by default
			case 8:
				*(int64_t *)str = num;
				break;
		}
#endif
		str += precision;
	}

        /* >= 0 tests to avoid compiler worrying about signed-overflows */
        while (precision >= 0 && width >= 0 && precision < width-- && str <= end)
		*str++ = '\0';

	return str;
}

noinline static int
_stp_vsprint_binary_size(int64_t num, int width, int precision)
{
	/* Only certain values are valid for the precision.  */
	precision = check_binary_precision (precision);

	/* Unspecified field width defaults to the specified
	   precision and vice versa. If neither is specified,
	   then both default to 8.  */
	if (width == -1) {
		if (precision == -1) {
			width = 8;
			precision = 8;
		}
		else
			width = precision;
	}
	else if (precision == -1) {
		precision = check_binary_precision (width);
		if (precision == -1)
			precision = 8;
	}

	return max(precision, width);
}

noinline static int
_stp_vsnprintf(char *buf, size_t size, const char *fmt, va_list args)
{
	int len;
	uint64_t num;
	int i, base;
	char *str = NULL, *end = NULL, c;
	const char *s;
	enum print_flag flags;		/* flags to number() */
	int field_width;	/* width of output field */
	int precision;		/* min. # of digits for integers; max
				   number of chars for from string */
	int qualifier;		/* 'h', 'l', or 'L' for integer fields */
	int num_bytes = 0;
	unsigned long irqflags = 0;
	bool got_print_lock = false;

	/* Reject out-of-range values early */
	if (unlikely((int) size < 0))
		return 0;

	/*
	 * buf will be NULL when this function is called from _stp_printf.
	 * This branch calculates the exact size print buffer required for 
	 * the string and allocates it with _stp_reserve_bytes. A change
	 * to this branch requires a corresponding change to the same 
	 * section of code below.
	 */
	if (buf == NULL) {
	  const char* fmt_copy = fmt;
          va_list args_copy;

          va_copy(args_copy, args);

          for (; *fmt_copy ; ++fmt_copy) {
                    if (*fmt_copy != '%') {
                            num_bytes++;
                            continue;
                    }

                    /* process flags */
                    flags = 0;
            repeat_copy:
                    ++fmt_copy;          /* this also skips first '%' */
                    switch (*fmt_copy) {
                    case '-': flags |= STP_LEFT; goto repeat_copy;
                    case '+': flags |= STP_PLUS; goto repeat_copy;
                    case ' ': flags |= STP_SPACE; goto repeat_copy;
                    case '#': flags |= STP_SPECIAL; goto repeat_copy;
                    case '0': flags |= STP_ZEROPAD; goto repeat_copy;
                    }

                    /* get field width */
                    field_width = -1;
                    if (isdigit(*fmt_copy))
                            field_width = clamp(skip_atoi(&fmt_copy), 0, STP_BUFFER_SIZE);
                    else if (*fmt_copy == '*') {
                            ++fmt_copy;
                            /* it's the next argument */
                            field_width = va_arg(args_copy, int);
                            if (field_width < 0) {
                                    field_width = -field_width;
                                    flags |= STP_LEFT;
                            }
                            field_width = clamp(field_width, 0, STP_BUFFER_SIZE);
                    }

                    /* get the precision */
                    precision = -1;
                    if (*fmt_copy == '.') {
                            ++fmt_copy;
                            if (isdigit(*fmt_copy))
                                    precision = skip_atoi(&fmt_copy);
                            else if (*fmt_copy == '*') {
                                    ++fmt_copy;
                                    /* it's the next argument */
                                    precision = va_arg(args_copy, int);
                            }
                            precision = clamp(precision, 0, STP_BUFFER_SIZE);
                    }

                    /* get the conversion qualifier */
                    qualifier = -1;
                    if (*fmt_copy == 'h' || *fmt_copy == 'l' || *fmt_copy == 'L') {
                            qualifier = *fmt_copy;
                            ++fmt_copy;
                            if (qualifier == 'l' && *fmt_copy == 'l') {
                                    qualifier = 'L';
                                    ++fmt_copy;
                            }
                    }

                    /* default base */
                    base = 10;

                    switch (*fmt_copy) {
                    case 'b':
                            num = va_arg(args_copy, int64_t);
                            num_bytes += _stp_vsprint_binary_size(num,
                                            field_width, precision);
                            continue;

                    case 's':
                    case 'M':
                    case 'm':
                            s = va_arg(args_copy, char *);
                            num_bytes += _stp_vsprint_memory_size(s,
                                            field_width, precision,
                                            *fmt_copy, flags);
                            continue;

                    case 'X':
                            flags |= STP_LARGE;
			    fallthrough;
                    case 'x':
                            base = 16;
                            break;

                    case 'd':
                    case 'i':
                            flags |= STP_SIGN;
			    fallthrough;
                    case 'u':
                            break;

                    case 'p':
                            /* Note that %p takes an int64_t argument. */
                            qualifier = 'L';
                            /* Since stap 1.3, %p == %#x. */
                            flags |= STP_SPECIAL;
                            base = 16;

#if STAP_COMPAT_VERSION < STAP_VERSION(1,3)
                            /* Before 1.3, %p was an odd child; see below. */
                            qualifier = 'P';
                            if (field_width == -1)
                                    field_width = 2 + 2*sizeof(void*);
                            precision = field_width - 2;
                            if (!(flags & STP_LEFT))
                                    precision = min_t(int, precision, 2*sizeof(void*));
#endif

                            break;

                    case '%':
                            num_bytes++;
                            continue;

                            /* integer number formats - set up the flags and "break" */
                    case 'o':
                            base = 8;
                            break;

                    case 'c':
                            c = (unsigned char) va_arg(args_copy, int);
                            num_bytes += _stp_vsprint_char_size(c, field_width, flags);
                            continue;

                    default:
                            num_bytes++;
                            if (*fmt_copy) {
                              num_bytes++;
                            } else {
                              --fmt_copy;
                            }
                            continue;
                    }

                    if (qualifier == 'L')
                            num = va_arg(args_copy, int64_t);
                    else if (qualifier == 'P') // fake, just for compat %p
                            num = (unsigned long) va_arg(args_copy, int64_t);
                    else if (qualifier == 'l') {
                            num = va_arg(args_copy, unsigned long);
                            if (flags & STP_SIGN)
                                    num = (signed long) num;
                    } else if (qualifier == 'h') {
                            num = (unsigned short) va_arg(args_copy, int);
                            if (flags & STP_SIGN)
                                    num = (signed short) num;
                    } else {
                            num = va_arg(args_copy, unsigned int);
                            if (flags & STP_SIGN)
                                    num = (signed int) num;
                    }
                    num_bytes += number_size(num, base, field_width, precision, flags);
            }

	  va_end(args_copy);

	  if (num_bytes == 0)
	    return 0;

	  //max print buffer size
	  if (num_bytes > STP_BUFFER_SIZE) {
	    num_bytes = STP_BUFFER_SIZE;
	  }

	  if (!_stp_print_trylock_irqsave(&irqflags))
	    return 0;
	  str = (char*)_stp_reserve_bytes(num_bytes);
	  if (str == NULL) {
	    goto err_unlock;
	  }
	  got_print_lock = true;
	  size = num_bytes;
	  end = str + size - 1;

	} else {
          str = buf;
          end = buf + size - 1;
	}

	/*
	 * Note that a change to code below requires a corresponding
	 * change in the code above to properly calculate the bytes
	 * required in the output buffer.
	 */
	for (; *fmt ; ++fmt) {
		if (*fmt != '%') {
			if (str <= end)
				*str = *fmt;
			++str;
			continue;
		}

		/* process flags */
		flags = 0;
	repeat:
		++fmt;          /* this also skips first '%' */
		switch (*fmt) {
		case '-': flags |= STP_LEFT; goto repeat;
		case '+': flags |= STP_PLUS; goto repeat;
		case ' ': flags |= STP_SPACE; goto repeat;
		case '#': flags |= STP_SPECIAL; goto repeat;
		case '0': flags |= STP_ZEROPAD; goto repeat;
		}

		/* get field width */
		field_width = -1;
		if (isdigit(*fmt))
			field_width = clamp(skip_atoi(&fmt), 0, (int)size);
		else if (*fmt == '*') {
			++fmt;
			/* it's the next argument */
			field_width = va_arg(args, int);
			if (field_width < 0) {
				field_width = -field_width;
				flags |= STP_LEFT;
			}
			field_width = clamp(field_width, 0, (int)size);
		}

		/* get the precision */
		precision = -1;
		if (*fmt == '.') {
			++fmt;
			if (isdigit(*fmt))
				precision = skip_atoi(&fmt);
			else if (*fmt == '*') {
				++fmt;
				/* it's the next argument */
				precision = va_arg(args, int);
			}
			precision = clamp(precision, 0, (int)size);
		}

		/* get the conversion qualifier */
		qualifier = -1;
		if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L') {
			qualifier = *fmt;
			++fmt;
			if (qualifier == 'l' && *fmt == 'l') {
				qualifier = 'L';
				++fmt;
			}
		}

		/* default base */
		base = 10;

		switch (*fmt) {
		case 'b':
			num = va_arg(args, int64_t);
			str = _stp_vsprint_binary(str, end, num,
					field_width, precision, flags);
			continue;

		case 's':
	        case 'M':
		case 'm':
			s = va_arg(args, char *);
			str = _stp_vsprint_memory(str, end, s,
					field_width, precision,
					*fmt, flags);
			if (unlikely(str == NULL)) {
				if (num_bytes > 0) {
					_stp_unreserve_bytes(num_bytes);
					goto err_unlock;
				}
				return 0;
			}
			continue;

		case 'X':
			flags |= STP_LARGE;
                        fallthrough;
		case 'x':
			base = 16;
			break;

		case 'd':
		case 'i':
			flags |= STP_SIGN;
                        fallthrough;
		case 'u':
			break;

		case 'p':
			/* Note that %p takes an int64_t argument. */
			qualifier = 'L';
			/* Since stap 1.3, %p == %#x. */
			flags |= STP_SPECIAL;
			base = 16;

#if STAP_COMPAT_VERSION < STAP_VERSION(1,3)
			/* Before 1.3, %p was an odd child:
			 * - the value is truncated to unsigned long
			 * - the specified precision is ignored
			 * - the default field_width is 2+2*sizeof(void*)
			 * - the effective precision is field_width - 2, except
			 *   if !STP_LEFT, where it is at most 2*sizeof(void*)
			 */
			qualifier = 'P';
			if (field_width == -1)
				field_width = 2 + 2*sizeof(void*);
			precision = field_width - 2;
			if (!(flags & STP_LEFT))
				precision = min_t(int, precision, 2*sizeof(void*));
#endif

			break;

		case '%':
			if (str <= end)
				*str = '%';
			++str;
			continue;

			/* integer number formats - set up the flags and "break" */
		case 'o':
			base = 8;
			break;

		case 'c':
			c = (unsigned char) va_arg(args, int);
			str = _stp_vsprint_char(str, end, c, field_width, flags);
			continue;

		default:
			if (str <= end)
				*str = '%';
			++str;
			if (*fmt) {
				if (str <= end)
					*str = *fmt;
				++str;
			} else {
				--fmt;
			}
			continue;
		}

		if (qualifier == 'L')
			num = va_arg(args, int64_t);
		else if (qualifier == 'P') // fake, just for compat %p
			num = (unsigned long) va_arg(args, int64_t);
		else if (qualifier == 'l') {
			num = va_arg(args, unsigned long);
			if (flags & STP_SIGN)
				num = (signed long) num;
		} else if (qualifier == 'h') {
			num = (unsigned short) va_arg(args, int);
			if (flags & STP_SIGN)
				num = (signed short) num;
		} else {
			num = va_arg(args, unsigned int);
			if (flags & STP_SIGN)
				num = (signed int) num;
		}
		str = number(str, end, num, base,
			     field_width, precision, flags);
	}

	if (buf != NULL) {
          if (likely(str <= end))
                  *str = '\0';
          else if (size > 0)
                  /* don't write out a null byte if the buf size is zero */
                  *end = '\0';
	}

	if (got_print_lock)
		_stp_print_unlock_irqrestore(&irqflags);
	return str-buf;

err_unlock:
	_stp_print_unlock_irqrestore(&irqflags);
	return 0;
}

#endif /* _VSPRINTF_C_ */
