/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "str.h"
#include "unichar.h"
#include "imap-arg.h"
#include "imap-quote.h"

/* If we have quoted-specials (<">, <\>) in a string, the minimum quoted-string
   overhead is 3 bytes ("\") while the minimum literal overhead is 5 bytes
   ("{n}\r\n"). But the literal overhead also depends on the string size. If
   the string length is less than 10, literal catches up to quoted-string after
   3 quoted-specials. If the string length is 10..99, it catches up after 4
   quoted-specials, and so on. We'll assume that the string lengths are usually
   in double digits, so we'll switch to literals after seeing 4
   quoted-specials. */
#define QUOTED_MAX_ESCAPE_CHARS 4

void imap_append_string(string_t *dest, const char *src,
			enum imap_quote_flags flags)
{
	i_assert(src != NULL);

	imap_append_nstring(dest, src, flags);
}

void imap_append_astring(string_t *dest, const char *src,
			 enum imap_quote_flags flags)
{
	unsigned int i;

	i_assert(src != NULL);

	for (i = 0; src[i] != '\0'; i++) {
		if (!IS_ASTRING_CHAR(src[i])) {
			imap_append_string(dest, src, flags);
			return;
		}
	}
	/* don't mix up NIL and "NIL"! */
	if (i == 0 || strcasecmp(src, "NIL") == 0)
		imap_append_string(dest, src, flags);
	else
		str_append(dest, src);
}

static void
imap_append_literal(string_t *dest, const char *src, unsigned int pos)
{
	size_t full_len = pos + strlen(src+pos);

	str_printfa(dest, "{%zu}\r\n", full_len);
	buffer_append(dest, src, full_len);
}

void imap_append_nstring(string_t *dest, const char *src,
			 enum imap_quote_flags flags)
{
	unsigned int escape_count = 0;
	size_t i;

	if (src == NULL) {
		str_append(dest, "NIL");
		return;
	}

	/* first check if we can (or want to) write this as quoted or
	   as literal.

	   quoted-specials = DQUOTE / "\"
	   QUOTED-CHAR     = <any TEXT-CHAR except quoted-specials> /
	                     "\" quoted-specials
	   TEXT-CHAR       = <any CHAR except CR and LF>
	*/
	for (i = 0; src[i] != '\0'; i++) {
		switch (src[i]) {
		case '"':
		case '\\':
			if (escape_count++ < QUOTED_MAX_ESCAPE_CHARS)
				break;
			/* fall through */
		case 13:
		case 10:
			imap_append_literal(dest, src, i);
			return;
		default:
			if (HAS_NO_BITS(flags, IMAP_QUOTE_FLAG_UTF8)) {
				if ((unsigned char)src[i] >= 0x80) {
					imap_append_literal(dest, src, i);
					return;
				}
			} else {
				unichar_t chr;
				int ret;

				ret = uni_utf8_get_char(&src[i], &chr);
				if (ret <= 0) {
					imap_append_literal(dest, src, i);
					return;
				}
				i += ret - 1;
			}
			break;
		}
	}
	imap_append_quoted(dest, src, flags);
}

static void
remove_newlines_and_append(string_t *dest, const char *src,
			   enum imap_quote_flags flags)
{
	size_t src_len;
	string_t *src_nolf;
	src_len = strlen(src);
	src_nolf = t_str_new(src_len);
	for (size_t i = 0; i < src_len; ++i) {
		if (src[i] != '\r' && src[i] != '\n') {
			/* other than CRs and LFS just copy */
			str_append_c(src_nolf, src[i]);
		} else if (src[i+1] != ' ' &&
			   src[i+1] != '\t' &&
			   src[i+1] != '\r' &&
			   src[i+1] != '\n' &&
			   src[i+1] != '\0') {
			/* CRs and LFs followed by a non-whitespace
			   are replaced themselves with a space */
			str_append_c(src_nolf, ' ');
		} else {
			/* CRs and LFs followed by a whitespace
			   OR at the end of the string are discarded */
		}
	}
	imap_append_nstring(dest, str_c(src_nolf), flags);
}

void imap_append_nstring_nolf(string_t *dest, const char *src,
			      enum imap_quote_flags flags)
{
	if (src == NULL || strpbrk(src, "\r\n") == NULL)
		imap_append_nstring(dest, src, flags);
	else if (buffer_get_pool(dest)->datastack_pool)
		remove_newlines_and_append(dest, src, flags);
	else T_BEGIN {
		remove_newlines_and_append(dest, src, flags);
	} T_END;
}

void imap_append_quoted(string_t *dest, const char *src,
			enum imap_quote_flags flags)
{
	str_append_c(dest, '"');
	for (; *src != '\0'; src++) {
		switch (*src) {
		case 13:
		case 10:
			/* not allowed */
			break;
		case '"':
		case '\\':
			str_append_c(dest, '\\');
			str_append_c(dest, *src);
			break;
		default:
			if (HAS_NO_BITS(flags, IMAP_QUOTE_FLAG_UTF8)) {
				if ((unsigned char)*src >= 0x80) {
					/* 8bit input not allowed in dquotes */
					break;
				}
				str_append_c(dest, *src);
			} else {
				unichar_t chr;
				int ret;

				ret = uni_utf8_get_char(src, &chr);
				if (ret <= 0)
					break;
				str_append_data(dest, src, ret);
				src += ret - 1;
			}
			break;
		}
	}
	str_append_c(dest, '"');
}

void imap_append_string_for_humans(string_t *dest,
				   const unsigned char *src, size_t size,
				   enum imap_quote_flags flags)
{
	size_t i, pos, remove_count = 0;
	bool whitespace_prefix = TRUE, last_lwsp = TRUE, modify = FALSE;

	/* first check if there is anything to change */
	for (i = 0; i < size; i++) {
		switch (src[i]) {
		case 0:
			/* convert NUL to #0x80 */
			last_lwsp = FALSE;
			modify = TRUE;
			break;
		case 13:
		case 10:
		case '\t':
			modify = TRUE;
			/* fall through */
		case ' ':
			if (last_lwsp) {
				modify = TRUE;
				remove_count++;
			}
			last_lwsp = TRUE;
			break;
		case '"':
		case '\\':
			modify = TRUE;
			last_lwsp = FALSE;
			break;
		default:
			if (HAS_NO_BITS(flags, IMAP_QUOTE_FLAG_UTF8)) {
				if ((src[i] & 0x80) != 0)
					modify = TRUE;
			} else {
				unichar_t chr;
				int ret;

				ret = uni_utf8_get_char_n(&src[i], size - i, &chr);
				if (ret <= 0)
					modify = TRUE;
				else
					i += ret - 1;
			}
			last_lwsp = FALSE;
			break;
		}
		if (!last_lwsp)
			whitespace_prefix = FALSE;
	}
	if (last_lwsp && i > 0 && !whitespace_prefix) {
		modify = TRUE;
		remove_count++;
	}
	if (!modify) {
		/* fast path: we can simply write it as quoted string
		   without any escaping */
		str_append_c(dest, '"');
		str_append_data(dest, src, size);
		str_append_c(dest, '"');
		return;
	}
	if (size == remove_count) {
		/* contained only whitespace */
		str_append(dest, "\"\"");
		return;
	}

	str_printfa(dest, "{%zu}\r\n", size - remove_count);
	pos = str_len(dest);

	last_lwsp = TRUE; whitespace_prefix = TRUE;
	for (i = 0; i < size; i++) {
		switch (src[i]) {
		case 0:
			str_append_c(dest, 128);
			last_lwsp = FALSE;
			break;
		case 13:
		case 10:
		case '\t':
		case ' ':
			if (!last_lwsp)
				str_append_c(dest, ' ');
			last_lwsp = TRUE;
			break;
		default:
			last_lwsp = FALSE;
			str_append_c(dest, src[i]);
			break;
		}
		if (!last_lwsp)
			whitespace_prefix = FALSE;
	}
	if (last_lwsp && i > 0 && !whitespace_prefix)
		str_truncate(dest, str_len(dest)-1);
	i_assert(str_len(dest) - pos == size - remove_count);
}
