/*++
/* NAME
/*	quote_for_json 3
/* SUMMARY
/*	quote UTF-8 string value for JSON
/* SYNOPSIS
/*	#include <quote_for_json.h>
/*
/*	char	*quote_for_json(
/*	VSTRING	*result,
/*	const char *in,
/*	ssize_t	len)
/*
/*	char	*quote_for_json_append(
/*	VSTRING	*result,
/*	const char *in,
/*	ssize_t	len)
/*
/*	char	*quote_for_json_var(
/*	VSTRING	*result,
/*	const char *in)
/* DESCRIPTION
/*	quote_for_json() takes well-formed UTF-8 encoded text,
/*	quotes that text compliant with RFC 4627, and returns a
/*	pointer to the resulting text. The input may contain null
/*	bytes, but the output will not.
/*
/*	quote_for_json() produces short (two-letter) escape sequences
/*	for common control characters, double quote and backslash.
/*	It will not quote "/" (0x2F), and will quote DEL (0x7f) as
/*	\u007F to make it printable. The input byte sequence "\uXXXX"
/*	is quoted like any other text (the "\" is escaped as "\\").
/*
/*	quote_for_json() does not perform UTF-8 validation. The caller
/*	should use valid_utf8_string() or printable() as appropriate.
/*
/*	quote_for_json_append() appends the output to the result buffer.
/*
/*	quote_for_json_var() takes a null-terminated sequence of
/*	null-terminated arguments and formats them with quote_for_json().
*
/*	Arguments:
/* .IP result
/*	Storage for the result, resized automatically.
/* .IP in
/*	Pointer to the input byte sequence.
/* .IP len
/*	The length of the input byte sequence, or a negative number
/*	when the byte sequence is null-terminated.
/* DIAGNOSTICS
/*	Fatal error: memory allocation error.
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	Wietse Venema
/*	Google, Inc.
/*	111 8th Avenue
/*	New York, NY 10011, USA
/*
/*	Wietse Venema
/*	porcupine.org
/*--*/

 /*
  * System library.
  */
#include <sys_defs.h>
#include <ctype.h>
#include <stdarg.h>
#include <string.h>

 /*
  * Utility library.
  */
#include <stringops.h>
#include <vstring.h>

#define STR(x) vstring_str(x)

/* quote_for_json_append - quote JSON string, append result */

char   *quote_for_json_append(VSTRING *result, const char *text, ssize_t len)
{
    const char *cp;
    int     ch;

    if (len < 0)
	len = strlen(text);

    for (cp = text; len > 0; len--, cp++) {
	ch = *(const unsigned char *) cp;
	if (UNEXPECTED(ISCNTRL(ch))) {
	    switch (ch) {
	    case '\b':
		VSTRING_ADDCH(result, '\\');
		VSTRING_ADDCH(result, 'b');
		break;
	    case '\f':
		VSTRING_ADDCH(result, '\\');
		VSTRING_ADDCH(result, 'f');
		break;
	    case '\n':
		VSTRING_ADDCH(result, '\\');
		VSTRING_ADDCH(result, 'n');
		break;
	    case '\r':
		VSTRING_ADDCH(result, '\\');
		VSTRING_ADDCH(result, 'r');
		break;
	    case '\t':
		VSTRING_ADDCH(result, '\\');
		VSTRING_ADDCH(result, 't');
		break;
	    default:
		/* All other controls including DEL and NUL. */
		vstring_sprintf_append(result, "\\u%04X", ch);
		break;
	    }
	} else {
	    switch (ch) {
	    case '\\':
	    case '"':
		VSTRING_ADDCH(result, '\\');
		/* FALLTHROUGH */
	    default:
		/* Includes malformed UTF-8. */
		VSTRING_ADDCH(result, ch);
		break;
	    }
	}
    }
    VSTRING_TERMINATE(result);
    return (STR(result));
}

/* quote_for_json - quote JSON string */

char   *quote_for_json(VSTRING *result, const char *text, ssize_t len)
{
    VSTRING_RESET(result);
    return (quote_for_json_append(result, text, len));
}


/* quote_for_json_var - quote null-terminated list of null-terminated strings */

char   *quote_for_json_var(VSTRING *result,...)
{
    const char *in;
    va_list ap;

    VSTRING_RESET(result);
    va_start(ap, result);
    while ((in = va_arg(ap, const char *)) != 0)
	quote_for_json_append(result, in, -1);
    return (STR(result));
}

#ifdef TEST

 /*
  * System library.
  */
#include <stdlib.h>

 /*
  * Utility library.
  */
#include <argv.h>
#include <msg.h>
#include <msg_vstream.h>

typedef struct TEST_CASE {
    const char *label;			/* identifies test case */
    int     (*action) (const struct TEST_CASE *);
    union {
	struct {
	    char   *(*fn) (VSTRING *, const char *, ssize_t);
	    const char *input;		/* input string */
	    ssize_t input_len;		/* -1 or input length */
	}       fixed;
	struct {
	    char   *(*fn) (VSTRING *,...);
	    const char *input;
	}       variadic;
    }       u;
    const char *exp_res;		/* expected result */
} TEST_CASE;

#define PASS	(0)
#define FAIL	(1)

static VSTRING *res_buf;

static int run_fixed_test(const TEST_CASE *tp)
{
    int     test_fail = 0;
    char   *res;

    res = tp->u.fixed.fn(res_buf, tp->u.fixed.input, tp->u.fixed.input_len);
    if (strcmp(res, tp->exp_res) != 0) {
	msg_warn("test case '%s': got '%s', want '%s'",
		 tp->label, res, tp->exp_res);
	test_fail = 1;
    }
    return (test_fail);
}

static int run_variadic_test(const TEST_CASE *tp)
{
    int     test_fail = 0;
    char   *res;
    ARGV   *argv = argv_split(tp->u.variadic.input, CHARS_SPACE);

    res = tp->u.variadic.fn(res_buf, argv->argv[0], argv->argv[1],
			    argv->argv[2], argv->argv[3]);
    if (strcmp(res, tp->exp_res) != 0) {
	msg_warn("test case '%s': got '%s', want '%s'",
		 tp->label, res, tp->exp_res);
	test_fail = 1;
    }
    argv_free(argv);
    return (test_fail);
}

 /*
  * The test cases.
  */
static const TEST_CASE test_cases[] = {
    {"ordinary ASCII text", run_fixed_test,
	.u.fixed = {quote_for_json,
	" abcABC012.,[]{}/", -1}, " abcABC012.,[]{}/",
    },
    {"quote_for_json_append", run_fixed_test,
	.u.fixed = {quote_for_json_append,
	"foo", -1}, " abcABC012.,[]{}/foo",
    },
    {"common control characters", run_fixed_test,
	.u.fixed = {quote_for_json,
	"\b\f\r\n\t", -1}, "\\b\\f\\r\\n\\t",
    },
    {"uncommon control characters and DEL", run_fixed_test,
	.u.fixed = {quote_for_json,
	"\0\01\037\040\176\177", 6}, "\\u0000\\u0001\\u001F ~\\u007F",
    },
    {"malformed UTF-8", run_fixed_test,
	.u.fixed = {quote_for_json,
	"\\*\\uasd\\u007F\x80", -1}, "\\\\*\\\\uasd\\\\u007F\x80",
    },
    {"multiple input strings", run_variadic_test,
	.u.variadic = {quote_for_json_var, "one - two"},
	"one-two",
    },
    0,
};

int     main(int argc, char **argv)
{
    const TEST_CASE *tp;
    int     pass = 0;
    int     fail = 0;

    msg_vstream_init(sane_basename((VSTRING *) 0, argv[0]), VSTREAM_ERR);
    res_buf = vstring_alloc(100);

    for (tp = test_cases; tp->label != 0; tp++) {
	int     test_fail = 0;

	msg_info("RUN  %s", tp->label);
	test_fail = tp->action(tp);
	if (test_fail) {
	    fail++;
	    msg_info("FAIL %s", tp->label);
	} else {
	    msg_info("PASS %s", tp->label);
	    pass++;
	}
    }
    msg_info("PASS=%d FAIL=%d", pass, fail);
    exit(fail != 0);
}

#endif
