/*
 * Copyright (C) 2003 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

/* The interfaces in this file are subject to change at any time. */

#include "config.h"

#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <glib.h>
#include "buffer.h"
#include "vteconv.h"
#include "vtedefines.hh"

typedef size_t (*convert_func)(GIConv converter,
			  const guchar **inbuf,
			  gsize *inbytes_left,
			  guchar **outbuf,
			  gsize *outbytes_left);
struct _VteConv {
	GIConv conv;
	convert_func convert;
	gint (*close)(GIConv converter);
	gboolean in_unichar, out_unichar;
	VteByteArray *in_scratch, *out_scratch;
};

/* We can't use g_utf8_strlen as that's not nul-safe :( */
static gsize
_vte_conv_utf8_strlen(const gchar *p, gssize max)
{
	const gchar *q = p + max;
        gsize length = 0;
        while (p < q) {
		p = g_utf8_next_char(p);
                length++;
        }
	return length;
}

/* A variant of g_utf8_validate() that allows NUL characters.
 * Requires that max_len >= 0 && end != NULL. */
static gboolean
_vte_conv_utf8_validate(const gchar *str,
                        gssize max_len,
                        const gchar **end)
{
        gboolean ret;
        do {
                ret = g_utf8_validate(str, max_len, end);
                max_len -= *end - str;
                str = *end;
                /* Hitting a NUL is okay. Clear the error and iterate over them. */
                while (max_len > 0 && *str == '\0') {
                        ret = TRUE;
                        max_len--;
                        str++;
                        *end = str;
                }
        } while (ret && max_len > 0);
        return ret;
}

/* A variant of g_utf8_get_char_validated() that allows NUL characters.
 * Requires that max_len >= 0. */
static gunichar
_vte_conv_utf8_get_char_validated(const gchar *p,
                                  gssize max_len) {
        gunichar ret;
        /* Handle NUL at the beginning. */
        if (max_len > 0 && p[0] == '\0')
                return 0;
        ret = g_utf8_get_char_validated(p, max_len);
        /* If a partial match is returned but there's a NUL in the buffer
         * then this is a wrong error, we're facing an invalid character. */
        if (ret == (gunichar) -2 && memchr(p, '\0', max_len) != NULL)
                ret = (gunichar) -1;
        return ret;
}

/* A bogus UTF-8 to UTF-8 conversion function which attempts to provide the
 * same semantics as g_iconv(). */
static size_t
_vte_conv_utf8_utf8(GIConv converter,
		    const gchar **inbuf,
		    gsize *inbytes_left,
		    gchar **outbuf,
		    gsize *outbytes_left)
{
	gboolean validated;
	const gchar *endptr;
	size_t bytes;

	/* We don't tolerate shenanigans! */
	g_assert_cmpuint(*outbytes_left, >=, *inbytes_left);

	/* The only error we can throw is EILSEQ, so check for that here. */
        validated = _vte_conv_utf8_validate(*inbuf, *inbytes_left, &endptr);

	/* Copy whatever data was validated. */
	bytes = endptr - *inbuf;
	memcpy(*outbuf, *inbuf, bytes);
	*inbuf += bytes;
	*outbuf += bytes;
	*outbytes_left -= bytes;
	*inbytes_left -= bytes;

	/* Return 0 (number of non-reversible conversions performed) if everything
	 * looked good, else EILSEQ. */
	if (validated) {
		return 0;
	}

	/* Determine why the end of the string is not valid. */
        if (_vte_conv_utf8_get_char_validated(*inbuf, *inbytes_left) == (gunichar) -2) {
		/* Prefix of a valid UTF-8 */
		errno = EINVAL;
	} else {
		/* We had enough bytes to validate the character, and
		 * it failed.  It just doesn't look right. */
		errno = EILSEQ;
	}
	return (size_t) -1;
}

/* Open a conversion descriptor which, in addition to normal cases, provides
 * UTF-8 to UTF-8 conversions and a gunichar-compatible source and target
 * encoding. */
VteConv
_vte_conv_open(const char *target, const char *source)
{
	VteConv ret;
	GIConv conv;
	gboolean in_unichar, out_unichar, utf8;
	const char *real_target, *real_source;

	/* No shenanigans. */
	g_assert(target != NULL);
	g_assert(source != NULL);
	g_assert(strlen(target) > 0);
	g_assert(strlen(source) > 0);

	/* Assume normal iconv usage. */
	in_unichar = FALSE;
	out_unichar = FALSE;
	real_source = source;
	real_target = target;

	/* Determine if we need to convert gunichars to UTF-8 on input. */
	if (strcmp(target, VTE_CONV_GUNICHAR_TYPE) == 0) {
		real_target = "UTF-8";
		out_unichar = TRUE;
	}

	/* Determine if we need to convert UTF-8 to gunichars on output. */
	if (strcmp(source, VTE_CONV_GUNICHAR_TYPE) == 0) {
		real_source = "UTF-8";
		in_unichar = TRUE;
	}

	/* Determine if this is a UTF-8 to UTF-8 conversion. */
	utf8 = ((g_ascii_strcasecmp(real_target, "UTF-8") == 0) &&
		(g_ascii_strcasecmp(real_source, "UTF-8") == 0));

	/* If we're doing UTF-8 to UTF-8, just use a dummy function which
	 * checks for bad data. */
	conv = NULL;
	if (!utf8) {
		char *translit_target = g_strdup_printf ("%s//translit", real_target);
		conv = g_iconv_open(translit_target, real_source);
		g_free (translit_target);
		if (conv == ((GIConv) -1)) {
			conv = g_iconv_open(real_target, real_source);
		}
		if (conv == ((GIConv) -1)) {
			return VTE_INVALID_CONV;
		}
	}

	/* Set up the descriptor. */
	ret = g_slice_new0(struct _VteConv);
	if (utf8) {
		ret->conv = NULL;
		ret->convert = (convert_func) _vte_conv_utf8_utf8;
		ret->close = NULL;
	} else {
		g_assert((conv != NULL) && (conv != ((GIConv) -1)));
		ret->conv = conv;
		ret->convert = (convert_func) g_iconv;
		ret->close = g_iconv_close;
	}

	/* Initialize other elements. */
	ret->in_unichar = in_unichar;
	ret->out_unichar = out_unichar;

	/* Create scratch buffers. */
	ret->in_scratch = _vte_byte_array_new();
	ret->out_scratch = _vte_byte_array_new();

	return ret;
}

gint
_vte_conv_close(VteConv converter)
{
	g_assert(converter != NULL);
	g_assert(converter != VTE_INVALID_CONV);

	/* Close the underlying descriptor, if there is one. */
	if (converter->conv != NULL) {
		g_assert(converter->close != NULL);
		converter->close(converter->conv);
	}

	/* Free the scratch buffers. */
	_vte_byte_array_free(converter->in_scratch);
	_vte_byte_array_free(converter->out_scratch);

	/* Free the structure itself. */
	g_slice_free(struct _VteConv, converter);

	return 0;
}

size_t
_vte_conv(VteConv converter,
	  const guchar **inbuf, gsize *inbytes_left,
	  guchar **outbuf, gsize *outbytes_left)
{
	size_t ret, tmp;
	const guchar *work_inbuf_start, *work_inbuf_working;
	guchar *work_outbuf_start, *work_outbuf_working;
	gsize work_inbytes, work_outbytes;

	g_assert(converter != NULL);
	g_assert(converter != VTE_INVALID_CONV);

	work_inbuf_start = work_inbuf_working = *inbuf;
	work_outbuf_start = work_outbuf_working = *outbuf;
	work_inbytes = *inbytes_left;
	work_outbytes = *outbytes_left;

	/* Possibly convert the input data from gunichars to UTF-8. */
	if (converter->in_unichar) {
		int i, char_count;
		guchar *p, *end;
		gunichar *g;
		/* Make sure the scratch buffer has enough space. */
		char_count = *inbytes_left / sizeof(gunichar);
		_vte_byte_array_set_minimum_size(converter->in_scratch,
					     (char_count + 1) * VTE_UTF8_BPC);
		/* Convert the incoming text. */
		g = (gunichar*) *inbuf;
		p = converter->in_scratch->data;
		end = p + (char_count + 1) * VTE_UTF8_BPC;
		for (i = 0; i < char_count; i++) {
			p += g_unichar_to_utf8(g[i], (gchar *)p);
			g_assert(p <= end);
		}
		/* Update our working pointers. */
		work_inbuf_start = converter->in_scratch->data;
		work_inbuf_working = work_inbuf_start;
		work_inbytes = p - work_inbuf_start;
	}

	/* Possibly set the output pointers to point at our scratch buffer. */
	if (converter->out_unichar) {
		work_outbytes = *outbytes_left * VTE_UTF8_BPC;
		_vte_byte_array_set_minimum_size(converter->out_scratch,
					     work_outbytes);
		work_outbuf_start = converter->out_scratch->data;
		work_outbuf_working = work_outbuf_start;
	}

	/* Call the underlying conversion. */
	ret = 0;
	do {
		tmp = converter->convert(converter->conv,
					 &work_inbuf_working,
					 &work_inbytes,
					 &work_outbuf_working,
					 &work_outbytes);
		if (tmp == (size_t) -1) {
			/* Check for zero bytes, which we pass right through. */
			if (errno == EILSEQ) {
				if ((work_inbytes > 0) &&
				    (work_inbuf_working[0] == '\0') &&
				    (work_outbytes > 0)) {
					work_outbuf_working[0] = '\0';
					work_outbuf_working++;
					work_inbuf_working++;
					work_outbytes--;
					work_inbytes--;
					ret++;
				} else {
					/* No go. */
					ret = -1;
					break;
				}
			} else {
				ret = -1;
				break;
			}
		} else {
			ret += tmp;
			break;
		}
	} while (work_inbytes > 0);

	/* We can't handle this particular failure, and it should
	 * never happen.  (If it does, our caller needs fixing.)  */
	g_assert((ret != (size_t)-1) || (errno != E2BIG));

	/* Possibly convert the output from UTF-8 to gunichars. */
	if (converter->out_unichar) {
		int  left = *outbytes_left;
		gunichar *g;
		gchar *p;

		g = (gunichar*) *outbuf;
		for(p = (gchar *)work_outbuf_start;
				p < (gchar *)work_outbuf_working;
				p = g_utf8_next_char(p)) {
		       g_assert(left>=0);
		       *g++ = g_utf8_get_char(p);
		       left -= sizeof(gunichar);
		}
		*outbytes_left = left;
		*outbuf = (guchar*) g;
	} else {
		/* Pass on the output results. */
		*outbuf = work_outbuf_working;
		*outbytes_left -= (work_outbuf_working - work_outbuf_start);
	}

	/* Advance the input pointer to the right place. */
	if (converter->in_unichar) {
		/* Get an idea of how many characters were converted, and
		 * advance the pointer as required. */
		gsize chars;
		chars = _vte_conv_utf8_strlen((const gchar *)work_inbuf_start,
					      work_inbuf_working - work_inbuf_start);
		*inbuf += (sizeof(gunichar) * chars);
		*inbytes_left -= (sizeof(gunichar) * chars);
	} else {
		/* Pass on the input results. */
		*inbuf = work_inbuf_working;
		*inbytes_left -= (work_inbuf_working - work_inbuf_start);
	}

	return ret;
}

size_t
_vte_conv_cu(VteConv converter,
	     const guchar **inbuf, gsize *inbytes_left,
	     gunichar **outbuf, gsize *outbytes_left)
{
	return _vte_conv(converter,
			 inbuf, inbytes_left,
			 (guchar**)outbuf, outbytes_left);
}

size_t
_vte_conv_uu(VteConv converter,
	     const gunichar **inbuf, gsize *inbytes_left,
	     gunichar **outbuf, gsize *outbytes_left)
{
	return _vte_conv(converter,
			 (const guchar**)inbuf, inbytes_left,
			 (guchar**)outbuf, outbytes_left);
}

size_t
_vte_conv_uc(VteConv converter,
	     const gunichar **inbuf, gsize *inbytes_left,
	     guchar **outbuf, gsize *outbytes_left)
{
	return _vte_conv(converter,
			 (const guchar**)inbuf, inbytes_left,
			 outbuf, outbytes_left);
}

#ifdef VTECONV_MAIN

static gsize
ucs4_strlen(const gunichar *p,
            gsize max_len)
{
        const gunichar *q = p + max_len;
        gsize length = 0;
        while (p < q && *p++ != 0)
                length++;
	return length;
}
static void
clear(gunichar wide[5], gchar narrow[5])
{
	wide[0] = 'T';
	wide[1] = 'E';
	wide[2] = 'S';
	wide[3] = 'T';
	wide[4] = '\0';
	strcpy(narrow, "test");
}

static int
mixed_strcmp(const gunichar *wide, const guchar *narrow)
{
	while (*wide && *narrow) {
                if (*wide != (gunichar)*narrow) {
			return -1;
		}
		wide++;
		narrow++;
	}
	return 0;
}

/* Test _vte_conv_utf8_strlen, especially where it differs from g_utf8_strlen. */
static void
test_utf8_strlen (void)
{
        g_assert_cmpuint(_vte_conv_utf8_strlen("", 0), ==, 0);
	g_assert_cmpuint(_vte_conv_utf8_strlen("\0\0\0\0", 4), ==, 4);
	g_assert_cmpuint(_vte_conv_utf8_strlen("\0A\0\0", 4), ==, 4);
	g_assert_cmpuint(_vte_conv_utf8_strlen("\0A\0B", 4), ==, 4);
	g_assert_cmpuint(_vte_conv_utf8_strlen("A\0B\0", 4), ==, 4);
        g_assert_cmpuint(_vte_conv_utf8_strlen("ABCD", 4), ==, 4);
	g_assert_cmpuint(_vte_conv_utf8_strlen("ABCDE", 4), ==, 4);
        g_assert_cmpuint(_vte_conv_utf8_strlen("\xC2\xA0\xC2\xA0", 4), ==, 2);
}

static void
test_utf8_validate (void)
{
        static const struct {
                char input[16];
                gsize ilen;
                gsize endlen;
                gboolean validates;
        } tests[] = {
                { "\0\0\0", 0, 0, TRUE },
                { "\0\0\0", 1, 1, TRUE },
                { "\0\0\0", 3, 3, TRUE },

                { "ab\0cd\0\0ef", 6, 6, TRUE },
                { "ab\0cd\0\0ef", 7, 7, TRUE },
                { "ab\0cd\0\0ef", 9, 9, TRUE },

                { "ab\xE2\x94\x80\0\xE2\x94\x80yz", 11, 11, TRUE },

                { "ab\x80\0cd", 6, 2, FALSE },

                { "ab\xE2\0cd", 6, 2, FALSE },
        };
        guint i;
        const char *end;

        for (i = 0; i < G_N_ELEMENTS (tests); i++) {
                g_assert(_vte_conv_utf8_validate(tests[i].input, tests[i].ilen, &end) == tests[i].validates);
                g_assert_cmpuint((gsize)(end - tests[i].input), ==, tests[i].endlen);
        }
}

/* Test _vte_conv_utf8_get_char_validated. */
static void
test_utf8_get_char_validated (void)
{
        static const guchar mbyte_test_u[] = { 0xe2, 0x94, 0x80 };
        static const guchar mbyte_test_break_u[] = { 0xe2, 0xe2, 0xe2 };
        const char *mbyte_test = (const char *)mbyte_test_u;
        const char *mbyte_test_break = (const char *)mbyte_test_break_u;

        g_assert_cmpuint(_vte_conv_utf8_get_char_validated("", 0), ==, (gunichar)-2);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated("\0", 1), ==, 0);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated(mbyte_test, 1), ==, (gunichar)-2);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated(mbyte_test, 2), ==, (gunichar)-2);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated(mbyte_test, 3), ==, 0x2500);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated(mbyte_test_break, 1), ==, (gunichar)-2);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated(mbyte_test_break, 2), ==, (gunichar)-1);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated(mbyte_test_break, 3), ==, (gunichar)-1);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated("\x80\0", 2), ==, (gunichar)-1);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated("\xE2\0", 2), ==, (gunichar)-1);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated("\xE2\x94\0", 3), ==, (gunichar)-1);
        g_assert_cmpuint(_vte_conv_utf8_get_char_validated("\xE2\x94\x80\0", 4), ==, 0x2500);
}

typedef struct {
        gunichar wide[8];
        gssize widelen;
        gchar narrow[8];
        gssize narrowlen;
        char target[16];
        char source[16];
} TestData;

static void
test_narrow_narrow (const TestData *tests,
                    gsize n_tests)
{
	VteConv conv;
	guchar buf[10];
	const guchar *inbuf;
	guchar *outbuf;
	gsize inbytes, outbytes, ret;
        gsize i;

        for (i = 0; i < n_tests; i++) {
                memset(buf, 0, sizeof(buf));
                inbuf = (const guchar *)tests[i].narrow;
                inbytes = tests[i].narrowlen >= 0 ? tests[i].narrowlen : strlen(tests[i].narrow);
                outbuf = buf;
                outbytes = sizeof(buf);
                conv = _vte_conv_open(tests[i].target, tests[i].source);
                ret = _vte_conv(conv, &inbuf, &inbytes, &outbuf, &outbytes);
                g_assert_cmpuint(ret, ==, 0);
                g_assert_cmpuint(inbytes, ==, 0);
                g_assert_cmpstr(tests[i].narrow, ==, (char *)buf);
                _vte_conv_close(conv);
        }
}

static void
test_narrow_to_wide (const TestData *tests,
                     gsize n_tests)
{
        gunichar widebuf[5];
	VteConv conv;
	const guchar *inbuf;
	guchar *outbuf;
	gsize inbytes, outbytes, ret;
        gsize i;

        for (i = 0; i < n_tests; i++) {
                memset(widebuf, 0, sizeof(widebuf));
                inbuf = (const guchar *)tests[i].narrow;
                inbytes = tests[i].narrowlen >= 0 ? tests[i].narrowlen : strlen(tests[i].narrow);
                outbuf = (guchar*) widebuf;
                outbytes = sizeof(widebuf);
                conv = _vte_conv_open(VTE_CONV_GUNICHAR_TYPE, tests[i].source);
                ret = _vte_conv(conv, &inbuf, &inbytes, &outbuf, &outbytes);
                g_assert_cmpuint(ret, ==, 0);
                g_assert_cmpuint(inbytes, ==, 0);
                g_assert_cmpint(mixed_strcmp(widebuf, inbuf), ==, 0);
                _vte_conv_close(conv);
        }
}

static void
test_wide_to_narrow (const TestData *tests,
                     gsize n_tests)
{
        char buf[10];
	VteConv conv;
	const guchar *inbuf;
	guchar *outbuf;
	gsize inbytes, outbytes, ret;
        gsize i;

        for (i = 0; i < n_tests; i++) {
                memset(buf, 0, sizeof(buf));
                inbuf = (const guchar *)tests[i].wide;
                inbytes = tests[i].widelen >= 0 ? tests[i].widelen
                        : ucs4_strlen(tests[i].wide, sizeof(tests[i].wide)) * sizeof(gunichar);
                outbuf = (guchar *)buf;
                outbytes = sizeof(buf);
                conv = _vte_conv_open(tests[i].target, VTE_CONV_GUNICHAR_TYPE);
                ret = _vte_conv(conv, &inbuf, &inbytes, &outbuf, &outbytes);
                g_assert_cmpuint(ret, ==, 0);
                g_assert_cmpuint(inbytes, ==, 0);
                g_assert_cmpint(mixed_strcmp(tests[i].wide, outbuf), ==, 0);
                _vte_conv_close(conv);
        }
}

static void
test_g_iconv_narrow_narrow (void)
{
        static const TestData tests[] = {
                { { 0, }, -1, "test", -1, "UTF-8", "ISO-8859-1" },
                { { 0, }, -1, "test", -1, "ISO-8859-1", "UTF-8" },
        };

        test_narrow_narrow (tests, G_N_ELEMENTS(tests));
}

static void
test_g_iconv_narrow_to_wide (void)
{
        static const TestData tests[] = {
                { { 0, }, -1, "test", -1, VTE_CONV_GUNICHAR_TYPE, "ISO-8859-1" },
        };

        test_narrow_to_wide (tests, G_N_ELEMENTS(tests));
}

static void
test_g_iconv_wide_to_narrow (void)
{
        static const TestData tests[] = {
                { { 'T', 'E', 'S', 'T', 0 }, -1, "", -1, "ISO-8859-1", VTE_CONV_GUNICHAR_TYPE },
        };

        test_wide_to_narrow (tests, G_N_ELEMENTS(tests));
}

static void
test_utf8_to_utf8 (void)
{
        static const TestData tests[] = {
                { { 0, }, -1, "test", -1, "UTF-8", "UTF-8" },
        };

        test_narrow_narrow (tests, G_N_ELEMENTS (tests));
}

static void
test_zero_byte_passthrough (void)
{
	gunichar wide_test[5];
	gchar narrow_test[5];
	VteConv conv;
	const guchar *inbuf;
	guchar *outbuf;
	gsize inbytes, outbytes;
        int i;

	/* Test zero-byte pass-through. */
	clear(wide_test, narrow_test);
	memset(wide_test, 0, sizeof(wide_test));
	inbuf = (guchar *)wide_test;
	inbytes = 3 * sizeof(gunichar);
	outbuf = (guchar *)narrow_test;
	outbytes = sizeof(narrow_test);
	conv = _vte_conv_open("UTF-8", VTE_CONV_GUNICHAR_TYPE);
	i = _vte_conv(conv, &inbuf, &inbytes, &outbuf, &outbytes);
	g_assert(inbytes == 0);
	if ((narrow_test[0] != 0) ||
	    (narrow_test[1] != 0) ||
	    (narrow_test[2] != 0)) {
		g_error("Conversion 6 failed.\n");
	}
	_vte_conv_close(conv);

	/* Test zero-byte pass-through. */
	clear(wide_test, narrow_test);
	memset(wide_test, 'A', sizeof(wide_test));
	memset(narrow_test, 0, sizeof(narrow_test));
	inbuf = (guchar *)narrow_test;
	inbytes = 3;
	outbuf = (guchar *)wide_test;
	outbytes = sizeof(wide_test);
	conv = _vte_conv_open(VTE_CONV_GUNICHAR_TYPE, "UTF-8");
	i = _vte_conv(conv, &inbuf, &inbytes, &outbuf, &outbytes);
	g_assert(inbytes == 0);
	if ((wide_test[0] != 0) ||
	    (wide_test[1] != 0) ||
	    (wide_test[2] != 0)) {
		g_error("Conversion 7 failed.\n");
	}
	_vte_conv_close(conv);

	/* Test zero-byte pass-through. */
	clear(wide_test, narrow_test);
	memset(wide_test, 'A', sizeof(wide_test));
	memset(narrow_test, 0, sizeof(narrow_test));
	inbuf = (guchar *)narrow_test;
	inbytes = 3;
	outbuf = (guchar *)wide_test;
	outbytes = sizeof(wide_test);
	conv = _vte_conv_open(VTE_CONV_GUNICHAR_TYPE, "ISO-8859-1");
	i = _vte_conv(conv, &inbuf, &inbytes, &outbuf, &outbytes);
	g_assert(inbytes == 0);
	if ((wide_test[0] != 0) ||
	    (wide_test[1] != 0) ||
	    (wide_test[2] != 0)) {
		g_error("Conversion 8 failed.\n");
	}
	_vte_conv_close(conv);
}

static void
test_utf8_to_utf8_error (void)
{
	static const guchar mbyte_test[] = { 0xe2, 0x94, 0x80 };
	static const guchar mbyte_test_break[] = { 0xe2, 0xe2, 0xe2 };
	gchar buf[10];
	VteConv conv;
	const guchar *inbuf;
	guchar *outbuf;
	gsize inbytes, outbytes;
	gsize i;

	/* Test UTF-8 to UTF-8 error reporting, valid multibyte. */
	for (i = 0; i < sizeof(mbyte_test); i++) {
		int ret;
		inbuf = mbyte_test;
		inbytes = i + 1;
		outbuf = (guchar *)buf;
		outbytes = sizeof(buf);
		conv = _vte_conv_open("UTF-8", "UTF-8");
		ret = _vte_conv(conv, &inbuf, &inbytes, &outbuf, &outbytes);
		switch (i) {
		case 0:
                        g_assert_cmpint(ret, ==, -1);
                        g_assert_cmpint(errno, ==, EINVAL);
			break;
		case 1:
                        g_assert_cmpint(ret, ==, -1);
                        g_assert_cmpint(errno, ==, EINVAL);
			break;
		case 2:
                        g_assert_cmpint(ret, !=, -1);
			break;
		default:
			g_assert_not_reached();
			break;
		}
		_vte_conv_close(conv);
	}

	/* Test UTF-8 to UTF-8 error reporting, invalid multibyte. */
	for (i = 0; i < sizeof(mbyte_test_break); i++) {
		int ret;
		inbuf = mbyte_test_break;
		inbytes = i + 1;
		outbuf = (guchar *)buf;
		outbytes = sizeof(buf);
		conv = _vte_conv_open("UTF-8", "UTF-8");
		ret = _vte_conv(conv, &inbuf, &inbytes, &outbuf, &outbytes);
		_vte_conv_close(conv);
		switch (i) {
		case 0:
                        g_assert_cmpint(ret, ==, -1);
                        g_assert_cmpint(errno, ==, EINVAL);
			break;
		case 1:
                        g_assert_cmpint(ret, ==, -1);
                        g_assert_cmpint(errno, ==, EILSEQ);
			break;
		case 2:
                        g_assert_cmpint(ret, ==, -1);
                        g_assert_cmpint(errno, ==, EILSEQ);
			break;
		default:
			g_assert_not_reached();
			break;
		}
	}
}

int
main (int argc,
      char *argv[])
{
        g_test_init (&argc, &argv, nullptr);

        g_test_add_func ("/vte/conv/utf8/strlen", test_utf8_strlen);
        g_test_add_func ("/vte/conv/utf8/validate", test_utf8_validate);
        g_test_add_func ("/vte/conv/utf8/get-char", test_utf8_get_char_validated);
        g_test_add_func ("/vte/conv/utf8/conversion", test_utf8_to_utf8);
        g_test_add_func ("/vte/conv/utf8/conversion-with-error", test_utf8_to_utf8_error);
        g_test_add_func ("/vte/conv/narrow-narrow", test_g_iconv_narrow_narrow);
        g_test_add_func ("/vte/conv/narrow-to-wide", test_g_iconv_narrow_to_wide);
        g_test_add_func ("/vte/conv/wide-to-narrow", test_g_iconv_wide_to_narrow);
        g_test_add_func ("/vte/conv/zero-byte-passthrough", test_zero_byte_passthrough);

	return g_test_run ();
}
#endif
