]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-json: Implement low-level JSON generator
authorStephan Bosch <stephan.bosch@open-xchange.com>
Wed, 7 Aug 2019 18:52:24 +0000 (20:52 +0200)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Sat, 18 Nov 2023 18:58:04 +0000 (18:58 +0000)
src/lib-json/Makefile.am
src/lib-json/json-generator.c [new file with mode: 0644]
src/lib-json/json-generator.h [new file with mode: 0644]
src/lib-json/test-json-generator.c [new file with mode: 0644]
src/lib-json/test-json-io.c [new file with mode: 0644]

index 6ce494ea9f4481ea54d392e35be624558ba17ee1..e170e8c8595d62a573b3be504fb908f8aff6a422 100644 (file)
@@ -7,16 +7,20 @@ AM_CPPFLAGS = \
 libjson_la_SOURCES = \
        json-syntax.c \
        json-types.c \
-       json-parser.new.c
+       json-parser.new.c \
+       json-generator.c
 libjson_la_LIBADD = -lm
 
 headers = \
        json-syntax.h \
        json-types.h \
-       json-parser.new.h
+       json-parser.new.h \
+       json-generator.h
 
 test_programs = \
-       test-json-parser
+       test-json-parser \
+       test-json-generator \
+       test-json-io
 
 noinst_PROGRAMS = $(test_programs)
 
@@ -39,6 +43,20 @@ test_json_parser_LDADD = \
 test_json_parser_DEPENDENCIES = \
        $(test_deps)
 
+test_json_generator_SOURCE = \
+       test-json-generator.c
+test_json_generator_LDADD = \
+       $(test_libs)
+test_json_generator_DEPENDENCIES = \
+       $(test_deps)
+
+test_json_io_SOURCE = \
+       test-json-io.c
+test_json_io_LDADD = \
+       $(test_libs)
+test_json_io_DEPENDENCIES = \
+       $(test_deps)
+
 pkginc_libdir=$(pkgincludedir)
 pkginc_lib_HEADERS = $(headers)
 
diff --git a/src/lib-json/json-generator.c b/src/lib-json/json-generator.c
new file mode 100644 (file)
index 0000000..858bfb9
--- /dev/null
@@ -0,0 +1,1034 @@
+/* Copyright (c) 2017-2023 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "hex-dec.h"
+#include "ostream-private.h"
+
+#include "json-syntax.h"
+#include "json-generator.h"
+
+#include <math.h>
+
+enum json_generator_state {
+       JSON_GENERATOR_STATE_VALUE = 0,
+       JSON_GENERATOR_STATE_VALUE_END,
+       JSON_GENERATOR_STATE_VALUE_NEXT,
+       JSON_GENERATOR_STATE_OBJECT_MEMBER,
+       JSON_GENERATOR_STATE_OBJECT_VALUE,
+       JSON_GENERATOR_STATE_STRING,
+       JSON_GENERATOR_STATE_TEXT,
+       JSON_GENERATOR_STATE_END,
+};
+
+struct json_generator_level {
+       bool object:1;
+};
+
+struct json_generator {
+       struct ostream *output;
+       enum json_generator_flags flags;
+
+       /* Buffer for elements that the generator has assumed responsibility for
+          by returning > 0, but could not be written to the output stream right
+          away. */
+       string_t *buf;
+       /* Write position */
+       size_t buf_pos;
+
+       /* API state: based on called API functions */
+       enum json_generator_state state;
+       /* Write state: based on what is written to the output so far */
+       enum json_generator_state write_state;
+
+       /* Stack of syntax levels */
+       ARRAY(struct json_generator_level) level_stack;
+       /* API state: stack position of opened syntax levels */
+       unsigned int level_stack_pos;
+       /* Write state: stack position of written syntax levels */
+       unsigned int level_stack_written;
+
+       /* We are in an object */
+       bool object_level_written:1;  /* write state */
+       bool object_level:1;          /* API state */
+       /* We closed an empty string */
+       bool string_empty:1;          /* API state */
+};
+
+static struct json_generator *
+json_generator_new(enum json_generator_flags flags)
+{
+       struct json_generator *generator;
+
+       generator = i_new(struct json_generator, 1);
+       generator->flags = flags;
+       i_array_init(&generator->level_stack, 16);
+
+       return generator;
+}
+
+struct json_generator *
+json_generator_init(struct ostream *output, enum json_generator_flags flags)
+{
+       struct json_generator *generator;
+
+       generator = json_generator_new(flags);
+       generator->buf = str_new(default_pool, 128);
+       generator->output = output;
+       o_stream_ref(output);
+
+       return generator;
+}
+
+struct json_generator *
+json_generator_init_str(string_t *buf, enum json_generator_flags flags)
+{
+       struct json_generator *generator;
+
+       generator = json_generator_new(flags);
+       generator->buf = buf;
+
+       return generator;
+}
+
+void json_generator_deinit(struct json_generator **_generator)
+{
+       struct json_generator *generator = *_generator;
+
+       if (generator == NULL)
+               return;
+       *_generator = NULL;
+
+       if (generator->output != NULL) {
+               o_stream_unref(&generator->output);
+               str_free(&generator->buf);
+       }
+       array_free(&generator->level_stack);
+       i_free(generator);
+}
+
+static inline size_t
+json_generator_bytes_available(struct json_generator *generator)
+{
+       if (generator->output == NULL || generator->output->blocking)
+               return SIZE_MAX;
+       return o_stream_get_buffer_avail_size(generator->output);
+}
+
+static int
+json_generator_make_space(struct json_generator *generator, size_t space,
+                         size_t *avail_r)
+{
+       *avail_r = json_generator_bytes_available(generator);
+       if (*avail_r >= space)
+               return 1;
+       if (o_stream_flush(generator->output) < 0)
+               return -1;
+       *avail_r = json_generator_bytes_available(generator);
+       return (*avail_r >= space ? 1 : 0);
+}
+
+static int
+json_generator_write(struct json_generator *generator,
+                    const void *data, size_t size)
+{
+       ssize_t ret;
+
+       if (generator->output == NULL) {
+               str_append_data(generator->buf, data, size);
+               return 1;
+       }
+       ret = o_stream_send(generator->output, data, size);
+       if (ret < 0)
+               return -1;
+       i_assert((size_t)ret == size);
+       return 1;
+}
+
+static inline int
+json_generator_write_all(struct json_generator *generator,
+                        const void *data, size_t size)
+{
+       size_t avail;
+       int ret;
+
+       ret = json_generator_make_space(generator, size, &avail);
+       if (ret <= 0)
+               return ret;
+
+       return json_generator_write(generator, data, size);
+}
+
+static int
+json_generator_write_buffered(struct json_generator *generator,
+                             const void *data, size_t size, bool continued)
+{
+       size_t avail, write;
+
+       if (!continued || generator->output == NULL ||
+           str_len(generator->buf) == 0) {
+               /* Try to write to output first */
+               if (json_generator_make_space(generator, size, &avail) < 0)
+                       return -1;
+               write = (avail < size ? avail : size);
+               if (write > 0) {
+                       i_assert(generator->output == NULL ||
+                                str_len(generator->buf) == 0);
+                       if (json_generator_write(generator, data, write) < 0)
+                               return -1;
+                       data = PTR_OFFSET(data, write);
+                       size -= write;
+               }
+       }
+
+       if (size > 0) {
+               i_assert(generator->output != NULL);
+               /* Prevent buffer from growing needlessly */
+               if (str_len(generator->buf) + size > 1024 &&
+                   generator->buf_pos > 0)
+                       str_delete(generator->buf, 0, generator->buf_pos);
+               /* Append data to buffer */
+               str_append_data(generator->buf, data, size);
+       }
+       return 1;
+}
+
+static int json_generator_flush_buffer(struct json_generator *generator)
+{
+       const unsigned char *data;
+       size_t size, avail;
+
+       if (generator->output == NULL)
+               return 1;
+       if (str_len(generator->buf) == 0)
+               return 1;
+
+       data = str_data(generator->buf);
+       size = str_len(generator->buf);
+       i_assert(generator->buf_pos < size);
+
+       data += generator->buf_pos;
+       size -= generator->buf_pos;
+
+       if (json_generator_make_space(generator, size, &avail) < 0)
+               return -1;
+       if (avail == 0)
+               return 0;
+       if (avail < size) {
+               if (json_generator_write(generator, data, avail) < 0)
+                       return -1;
+               generator->buf_pos += avail;
+               return 0;
+       }
+       if (json_generator_write(generator, data, size) < 0)
+               return -1;
+       generator->buf_pos = 0;
+       str_truncate(generator->buf, 0);
+       return 1;
+}
+
+int json_generator_flush(struct json_generator *generator)
+{
+       bool hide_root = HAS_ALL_BITS(generator->flags,
+                                     JSON_GENERATOR_FLAG_HIDE_ROOT);
+       int ret;
+
+       /* Flush buffer */
+       ret = json_generator_flush_buffer(generator);
+       if (ret <= 0)
+               return ret;
+       /* Flush closing string */
+       if (generator->write_state == JSON_GENERATOR_STATE_STRING &&
+           generator->state != JSON_GENERATOR_STATE_STRING) {
+               ret = json_generator_write_all(generator, "\"", 1);
+               if (ret <= 0)
+                       return ret;
+               generator->write_state = JSON_GENERATOR_STATE_VALUE_END;
+       }
+       /* Flush object member */
+       if (generator->write_state == JSON_GENERATOR_STATE_OBJECT_VALUE) {
+               ret = json_generator_write_all(generator, ":", 1);
+               if (ret <= 0)
+                       return ret;
+               generator->write_state = JSON_GENERATOR_STATE_VALUE;
+       }
+       /* Flush opening objects/arrays */
+       for (;;) {
+               struct json_generator_level *level;
+
+               i_assert(generator->level_stack_written <=
+                        generator->level_stack_pos);
+               if (generator->level_stack_written == generator->level_stack_pos)
+                       break;
+
+               i_assert(generator->write_state != JSON_GENERATOR_STATE_STRING &&
+                        generator->write_state != JSON_GENERATOR_STATE_TEXT);
+               if (generator->write_state == JSON_GENERATOR_STATE_VALUE_END)
+                       generator->write_state = JSON_GENERATOR_STATE_VALUE_NEXT;
+               if (generator->write_state == JSON_GENERATOR_STATE_VALUE_NEXT) {
+                       ret = json_generator_write_all(generator, ",", 1);
+                       if (ret <= 0)
+                               return ret;
+                       generator->write_state = JSON_GENERATOR_STATE_VALUE;
+               }
+
+               // FIXME: add indent
+
+               level = array_idx_get_space(&generator->level_stack,
+                                           generator->level_stack_written);
+               if (level->object) {
+                       if (!hide_root || generator->level_stack_written > 0) {
+                               ret = json_generator_write_all(generator, "{", 1);
+                               if (ret <= 0)
+                                       return ret;
+                       }
+                       generator->level_stack_written++;
+                       generator->write_state = JSON_GENERATOR_STATE_OBJECT_MEMBER;
+                       generator->object_level_written = TRUE;
+               } else {
+                       if (!hide_root || generator->level_stack_written > 0) {
+                               ret = json_generator_write_all(generator, "[", 1);
+                               if (ret <= 0)
+                                       return ret;
+                       }
+                       generator->level_stack_written++;
+                       generator->object_level_written = FALSE;
+                       generator->write_state = JSON_GENERATOR_STATE_VALUE;
+               }
+       }
+       /* Flush separator */
+       switch (generator->write_state) {
+       /* Flush comma */
+       case JSON_GENERATOR_STATE_VALUE_END:
+               if (generator->level_stack_pos == 0) {
+                       generator->write_state = JSON_GENERATOR_STATE_END;
+                       break;
+               }
+               if (generator->state != JSON_GENERATOR_STATE_STRING &&
+                       generator->state != JSON_GENERATOR_STATE_TEXT)
+                       break;
+               generator->write_state = JSON_GENERATOR_STATE_VALUE_NEXT;
+               /* Fall through */
+       case JSON_GENERATOR_STATE_VALUE_NEXT:
+               ret = json_generator_write_all(generator, ",", 1);
+               if (ret <= 0)
+                       return ret;
+               if (generator->object_level_written) {
+                       generator->write_state = JSON_GENERATOR_STATE_OBJECT_MEMBER;
+               } else {
+                       generator->write_state = JSON_GENERATOR_STATE_VALUE;
+               }
+               break;
+       /* Flush colon */
+       case JSON_GENERATOR_STATE_OBJECT_VALUE:
+               ret = json_generator_write_all(generator, ":", 1);
+               if (ret <= 0)
+                       return ret;
+               generator->write_state = JSON_GENERATOR_STATE_VALUE;
+               break;
+       default:
+               break;
+       }
+       /* Flush opening empty string */
+       if (generator->string_empty &&
+           generator->write_state != JSON_GENERATOR_STATE_STRING) {
+               i_assert(generator->write_state == JSON_GENERATOR_STATE_VALUE ||
+                        generator->write_state == JSON_GENERATOR_STATE_OBJECT_VALUE);
+               ret = json_generator_write_all(generator, "\"", 1);
+               if (ret <= 0)
+                       return ret;
+               generator->string_empty = FALSE;
+               ret = json_generator_write_all(generator, "\"", 1);
+               if (ret < 0)
+                       return -1;
+               if (ret == 0) {
+                       generator->write_state = JSON_GENERATOR_STATE_STRING;
+                       return 0;
+               }
+               generator->write_state = JSON_GENERATOR_STATE_VALUE_END;
+       /* Flush opening string */
+       } else if (generator->state == JSON_GENERATOR_STATE_STRING &&
+                  generator->write_state != JSON_GENERATOR_STATE_STRING) {
+               i_assert(generator->write_state == JSON_GENERATOR_STATE_VALUE ||
+                        generator->write_state == JSON_GENERATOR_STATE_OBJECT_VALUE);
+               ret = json_generator_write_all(generator, "\"", 1);
+               if (ret <= 0)
+                       return ret;
+               generator->write_state = JSON_GENERATOR_STATE_STRING;
+       }
+       /* Flush opening text */
+       if (generator->state == JSON_GENERATOR_STATE_TEXT &&
+           generator->write_state != JSON_GENERATOR_STATE_TEXT)
+               generator->write_state = JSON_GENERATOR_STATE_TEXT;
+       return 1;
+}
+
+/*
+ * value begin/end
+ */
+
+static inline void
+json_generator_value_begin(struct json_generator *generator)
+{
+       i_assert(generator->state == JSON_GENERATOR_STATE_VALUE);
+}
+
+static inline int
+json_generator_value_begin_flushed(struct json_generator *generator)
+{
+       int ret;
+
+       json_generator_value_begin(generator);
+       if (generator->write_state == JSON_GENERATOR_STATE_VALUE_END)
+               generator->write_state = JSON_GENERATOR_STATE_VALUE_NEXT;
+       ret = json_generator_flush(generator);
+       if (ret <= 0)
+               return ret;
+       i_assert(generator->write_state == generator->state);
+       return 1;
+}
+
+static inline void
+json_generator_value_end(struct json_generator *generator)
+{
+       if (generator->level_stack_pos == 0)
+               generator->state = JSON_GENERATOR_STATE_END;
+       else if (generator->object_level)
+               generator->state = JSON_GENERATOR_STATE_OBJECT_MEMBER;
+       else
+               generator->state = JSON_GENERATOR_STATE_VALUE;
+       generator->write_state = JSON_GENERATOR_STATE_VALUE_END;
+}
+
+/*
+ * number
+ */
+
+int json_generate_number(struct json_generator *generator, intmax_t number)
+{
+       int ret;
+
+       ret = json_generator_value_begin_flushed(generator);
+       if (ret <= 0)
+               return ret;
+
+       str_printfa(generator->buf, "%"PRIdMAX, number);
+
+       json_generator_value_end(generator);
+       return (json_generator_flush(generator) < 0 ? -1 : 1);
+}
+
+int json_generate_number_raw(struct json_generator *generator,
+                             const char *number)
+{
+       int ret;
+
+       ret = json_generator_value_begin_flushed(generator);
+       if (ret <= 0)
+               return ret;
+       if (json_generator_write_buffered(generator, number,
+                                         strlen(number), FALSE) < 0)
+               return -1;
+       json_generator_value_end(generator);
+       return 1;
+}
+
+/*
+ * string
+ */
+
+void json_generate_string_open(struct json_generator *generator)
+{
+       json_generator_value_begin(generator);
+       generator->state = JSON_GENERATOR_STATE_STRING;
+}
+
+static ssize_t
+json_generate_string_write_data(struct json_generator *generator,
+                               const void *data, size_t size,
+                               bool buffered, bool last)
+{
+       const unsigned char *p, *pbegin, *poffset, *pend;
+       size_t avail;
+       int ret;
+
+       p = pbegin = poffset = data;
+       pend = p + size;
+       while (p < pend) {
+               unsigned char esc_hex[6];
+               const char *esc = NULL;
+               unsigned int esc_len = 2;
+               int octets = 0;
+               unichar_t ch;
+
+               if (buffered)
+                       avail = SIZE_MAX;
+               else {
+                       ret = json_generator_make_space(generator, pend - p,
+                                                       &avail);
+                       if (ret < 0)
+                               return -1;
+               }
+               if (avail == 0)
+                       break;
+
+               poffset = p;
+               while (avail > 0 && p < pend && esc == NULL) {
+                       octets = uni_utf8_get_char_n(p, (pend - p), &ch);
+                       if (octets < 0 || (octets == 0 && last) ||
+                           (octets > 0  && !uni_is_valid_ucs4(ch))) {
+                               /* Replace invalid UTF-8/Unicode with the
+                                  replacement character. */
+                               esc = UNICODE_REPLACEMENT_CHAR_UTF8;
+                               esc_len = UTF8_REPLACEMENT_CHAR_LEN;
+                               octets = (octets <= 0 ? 1 : octets);
+                               break;
+                       }
+                       if (octets == 0 || (size_t)octets > avail)
+                               break;
+                       switch (ch) {
+                       /* %x22 /          ; "    quotation mark  U+0022 */
+                       case '"':
+                               esc = "\\\"";
+                               break;
+                       /* %x5C /          ; \    reverse solidus U+005C */
+                       case '\\':
+                               esc = "\\\\";
+                               break;
+                       /* %x62 /          ; b    backspace       U+0008 */
+                       case '\b':
+                               esc = "\\b";
+                               break;
+                       /* %x66 /          ; f    form feed       U+000C */
+                       case '\f':
+                               esc = "\\f";
+                               break;
+                       /* %x6E /          ; n    line feed       U+000A */
+                       case '\n':
+                               esc = "\\n";
+                               break;
+                       /* %x72 /          ; r    carriage return U+000D */
+                       case '\r':
+                               esc = "\\r";
+                               break;
+                       /* %x74 /          ; t    tab             U+0009 */
+                       case '\t':
+                               esc = "\\t";
+                               break;
+                       default:
+                               if (ch < 0x20 || ch == 0x2028 || ch == 0x2029) {
+                                       esc_hex[0] = '\\';
+                                       esc_hex[1] = 'u';
+                                       dec2hex(&esc_hex[2], (uintmax_t)ch, 4);
+                                       esc = (const char *)esc_hex;
+                                       esc_len = sizeof(esc_hex);
+                               } else {
+                                       p += octets;
+                                       avail -= octets;
+                               }
+                       }
+               }
+
+               if ((p - poffset) > 0) {
+                       if (buffered) {
+                               if (json_generator_write_buffered(
+                                       generator, poffset, p -poffset,
+                                       TRUE) < 0)
+                                       return -1;
+                       } else {
+                               if (json_generator_write(
+                                       generator, poffset, p - poffset) < 0)
+                                       return -1;
+                       }
+               }
+               if (esc != NULL) {
+                       if (esc_len > avail) {
+                               break;
+                       } else {
+                               if (buffered) {
+                                       if (json_generator_write_buffered(
+                                               generator, esc, esc_len,
+                                               TRUE) < 0)
+                                               return -1;
+                               } else {
+                                       if (json_generator_write(
+                                               generator, esc, esc_len) < 0)
+                                               return -1;
+                               }
+                               p += octets;
+                       }
+               }
+               if (octets == 0 || (size_t)octets > avail)
+                       break;
+       }
+
+       return (ssize_t)(p - pbegin);
+}
+
+ssize_t json_generate_string_more(struct json_generator *generator,
+                                 const void *data, size_t size, bool last)
+{
+       int ret;
+
+       i_assert(generator->state == JSON_GENERATOR_STATE_STRING);
+       ret = json_generator_flush(generator);
+       if (ret <= 0)
+               return (ssize_t)ret;
+       i_assert(generator->write_state == JSON_GENERATOR_STATE_STRING);
+
+       return json_generate_string_write_data(generator, data, size,
+                                              FALSE, last);
+}
+
+void json_generate_string_close(struct json_generator *generator)
+{
+       i_assert(generator->state == JSON_GENERATOR_STATE_STRING);
+       if (generator->write_state != JSON_GENERATOR_STATE_STRING) {
+               /* This function does not flush first before changing state, nor
+                  does the string_open() function. So, we need to remember
+                  closing the an empty string, because otherwise nothing will
+                  be emitted. */
+               generator->string_empty = TRUE;
+       }
+       if (generator->level_stack_pos == 0)
+               generator->state = JSON_GENERATOR_STATE_END;
+       else if (generator->object_level)
+               generator->state = JSON_GENERATOR_STATE_OBJECT_MEMBER;
+       else
+               generator->state = JSON_GENERATOR_STATE_VALUE;
+}
+
+int json_generate_string_write_close(struct json_generator *generator)
+{
+       if (generator->state == JSON_GENERATOR_STATE_STRING)
+               json_generate_string_close(generator);
+       return json_generator_flush(generator);
+}
+
+int json_generate_string_data(struct json_generator *generator,
+                             const void *data, size_t size)
+{
+       int ret;
+
+       ret = json_generator_value_begin_flushed(generator);
+       if (ret <= 0)
+               return ret;
+
+       if (json_generator_write_buffered(generator, "\"", 1, FALSE) < 0)
+               return -1;
+       if (json_generate_string_write_data(generator, data, size,
+                                           TRUE, TRUE) < 0)
+               return -1;
+       if (json_generator_write_buffered(generator, "\"", 1, TRUE) < 0)
+               return -1;
+
+       json_generator_value_end(generator);
+       return 1;
+}
+
+int json_generate_string(struct json_generator *generator, const char *str)
+{
+       return json_generate_string_data(generator,
+                                        (const unsigned char *)str,
+                                        strlen(str));
+}
+
+/*
+ * null, true, false
+ */
+
+static int
+json_generate_literal(struct json_generator *generator, const char *literal)
+{
+       size_t lit_size = strlen(literal);
+       int ret;
+
+       ret = json_generator_value_begin_flushed(generator);
+       if (ret <= 0)
+               return ret;
+
+       ret = json_generator_write_all(generator, literal, lit_size);
+       if (ret <= 0)
+               return ret;
+
+       json_generator_value_end(generator);
+       return ret;
+}
+
+int json_generate_null(struct json_generator *generator)
+{
+       return json_generate_literal(generator, "null");
+}
+
+int json_generate_false(struct json_generator *generator)
+{
+       return json_generate_literal(generator, "false");
+}
+
+int json_generate_true(struct json_generator *generator)
+{
+       return json_generate_literal(generator, "true");
+}
+
+/*
+ * stack level
+ */
+
+static void
+json_generator_level_open(struct json_generator *generator, bool object)
+{
+       struct json_generator_level *level;
+
+       level = array_idx_get_space(&generator->level_stack,
+                                   generator->level_stack_pos++);
+       i_zero(level);
+       level->object = object;
+       generator->object_level = object;
+}
+
+static void
+json_generator_level_close(struct json_generator *generator, bool object)
+{
+       struct json_generator_level *level, *under_level;
+
+       i_assert(generator->level_stack_pos > 0);
+
+       i_assert(generator->level_stack_written == generator->level_stack_pos);
+       generator->level_stack_written--;
+
+       if (generator->level_stack_pos < 2) {
+               generator->object_level_written = FALSE;
+               generator->object_level = FALSE;
+       } else {
+               under_level = array_idx_modifiable(
+                       &generator->level_stack, generator->level_stack_pos-2);
+               generator->object_level_written = under_level->object;
+               generator->object_level = under_level->object;
+       }
+       level = array_idx_modifiable(&generator->level_stack,
+                                    --generator->level_stack_pos);
+       i_assert(level->object == object);
+}
+
+/*
+ * array
+ */
+
+void json_generate_array_open(struct json_generator *generator)
+{
+       json_generator_value_begin(generator);
+       json_generator_level_open(generator, FALSE);
+       generator->state = JSON_GENERATOR_STATE_VALUE;
+}
+
+int json_generate_array_close(struct json_generator *generator)
+{
+       bool hide_root = HAS_ALL_BITS(generator->flags,
+                                     JSON_GENERATOR_FLAG_HIDE_ROOT);
+       int ret;
+
+       i_assert(generator->state == JSON_GENERATOR_STATE_VALUE);
+       ret = json_generator_flush(generator);
+       if (ret <= 0)
+               return ret;
+       i_assert(generator->write_state == JSON_GENERATOR_STATE_VALUE ||
+                generator->write_state == JSON_GENERATOR_STATE_VALUE_END);
+
+       i_assert(generator->level_stack_written > 0);
+       if (!hide_root || generator->level_stack_written > 1) {
+               ret = json_generator_write_all(generator, "]", 1);
+               if (ret <= 0)
+                       return ret;
+       }
+       json_generator_level_close(generator, FALSE);
+       json_generator_value_end(generator);
+       return 1;
+}
+
+/*
+ * object
+ */
+
+void json_generate_object_open(struct json_generator *generator)
+{
+       json_generator_value_begin(generator);
+       json_generator_level_open(generator, TRUE);
+       generator->state = JSON_GENERATOR_STATE_OBJECT_MEMBER;
+}
+
+int json_generate_object_member(struct json_generator *generator,
+                               const char *name)
+{
+       int ret;
+
+       i_assert(generator->state == JSON_GENERATOR_STATE_OBJECT_MEMBER);
+       if (generator->write_state == JSON_GENERATOR_STATE_VALUE_END) {
+               generator->write_state = JSON_GENERATOR_STATE_VALUE_NEXT;
+       }
+       ret = json_generator_flush(generator);
+       if (ret <= 0)
+               return ret;
+       i_assert(generator->write_state == generator->state);
+       generator->state = JSON_GENERATOR_STATE_VALUE;
+
+       if (json_generator_write_buffered(generator, "\"", 1, FALSE) < 0)
+               return -1;
+       if (json_generate_string_write_data(
+               generator, name, strlen(name), TRUE, TRUE) < 0)
+               return -1;
+       if (json_generator_write_buffered(generator, "\"", 1, TRUE) < 0)
+               return -1;
+       generator->write_state = JSON_GENERATOR_STATE_OBJECT_VALUE;
+       return 1;
+}
+
+int json_generate_object_close(struct json_generator *generator)
+{
+       bool hide_root = HAS_ALL_BITS(generator->flags,
+                                     JSON_GENERATOR_FLAG_HIDE_ROOT);
+       int ret;
+
+       i_assert(generator->state == JSON_GENERATOR_STATE_OBJECT_MEMBER);
+       ret = json_generator_flush(generator);
+       if (ret <= 0)
+               return ret;
+       i_assert(generator->write_state == JSON_GENERATOR_STATE_OBJECT_MEMBER ||
+                generator->write_state == JSON_GENERATOR_STATE_VALUE_END);
+       i_assert(generator->level_stack_written > 0);
+       if (!hide_root || generator->level_stack_written > 1) {
+               ret = json_generator_write_all(generator, "}", 1);
+               if (ret <= 0)
+                       return ret;
+       }
+       json_generator_level_close(generator, TRUE);
+       json_generator_value_end(generator);
+       return 1;
+}
+
+/*
+ * JSON-text
+ */
+
+void json_generate_text_open(struct json_generator *generator)
+{
+       json_generator_value_begin(generator);
+       generator->state = JSON_GENERATOR_STATE_TEXT;
+}
+
+static ssize_t
+json_generate_text_write_data(struct json_generator *generator,
+                             const void *data, size_t size, bool buffered)
+{
+       int ret;
+
+       if (!buffered) {
+               size_t avail;
+
+               ret = json_generator_make_space(generator, size, &avail);
+               if (ret < 0)
+                       return -1;
+               if (avail == 0)
+                       return 0;
+               if (size > avail)
+                       size = avail;
+       }
+
+       if (buffered) {
+               if (json_generator_write_buffered(generator, data, size,
+                                                  FALSE) < 0)
+                       return -1;
+       } else {
+               if (json_generator_write(generator, data, size) < 0)
+                       return -1;
+       }
+       return (ssize_t)size;
+}
+
+ssize_t json_generate_text_more(struct json_generator *generator,
+                               const void *data, size_t size)
+{
+       int ret;
+
+       i_assert(generator->state == JSON_GENERATOR_STATE_TEXT);
+       ret = json_generator_flush(generator);
+       if (ret <= 0)
+               return (ssize_t)ret;
+       i_assert(generator->write_state == JSON_GENERATOR_STATE_TEXT);
+
+       return json_generate_text_write_data(generator, data, size, FALSE);
+}
+
+int json_generate_text_close(struct json_generator *generator)
+{
+       int ret;
+
+       i_assert(generator->state == JSON_GENERATOR_STATE_TEXT);
+       ret = json_generator_flush(generator);
+       if (ret <= 0)
+               return ret;
+       i_assert(generator->write_state == JSON_GENERATOR_STATE_TEXT);
+
+       json_generator_value_end(generator);
+       return 1;
+}
+
+int json_generate_text_data(struct json_generator *generator,
+                           const void *data, size_t size)
+{
+       int ret;
+
+       ret = json_generator_value_begin_flushed(generator);
+       if (ret <= 0)
+               return ret;
+
+       if (json_generate_text_write_data(generator, data, size, TRUE) < 0)
+               return -1;
+       json_generator_value_end(generator);
+       return 1;
+}
+
+int json_generate_text(struct json_generator *generator, const char *str)
+{
+       return json_generate_text_data(generator, (const unsigned char *)str,
+                                      strlen(str));
+}
+
+/*
+ * value
+ */
+
+int json_generate_value(struct json_generator *generator,
+                       enum json_type type,
+                       const struct json_value *value)
+{
+       switch (type) {
+       /* string */
+       case JSON_TYPE_STRING:
+               switch (value->content_type) {
+               case JSON_CONTENT_TYPE_STRING:
+                       return json_generate_string(generator,
+                                                   value->content.str);
+               case JSON_CONTENT_TYPE_DATA:
+                       return json_generate_string_data(
+                               generator, value->content.data->data,
+                               value->content.data->size);
+               default:
+                       break;
+               }
+               break;
+       /* number */
+       case JSON_TYPE_NUMBER:
+               switch (value->content_type) {
+               case JSON_CONTENT_TYPE_STRING:
+                       return json_generate_number_raw(generator,
+                                                       value->content.str);
+               case JSON_CONTENT_TYPE_INTEGER:
+                       return json_generate_number(generator,
+                                                   value->content.intnum);
+               default:
+                       break;
+               }
+               break;
+       /* true */
+       case JSON_TYPE_TRUE:
+               return json_generate_true(generator);
+       /* false */
+       case JSON_TYPE_FALSE:
+               return json_generate_false(generator);
+       /* null */
+       case JSON_TYPE_NULL:
+               return json_generate_null(generator);
+       /* JSON-text */
+       case JSON_TYPE_TEXT:
+               switch (value->content_type) {
+               case JSON_CONTENT_TYPE_STRING:
+                       return json_generate_text(generator,
+                                                 value->content.str);
+               case JSON_CONTENT_TYPE_DATA:
+                       return json_generate_text_data(
+                               generator, value->content.data->data,
+                               value->content.data->size);
+               default:
+                       break;
+               }
+               break;
+       /* ?? */
+       default:
+               break;
+       }
+       i_unreached();
+}
+
+/*
+ * Simple string output
+ */
+
+static void json_append_escaped_char(string_t *dest, unsigned char src)
+{
+       switch (src) {
+       case '\b':
+               str_append(dest, "\\b");
+               break;
+       case '\f':
+               str_append(dest, "\\f");
+               break;
+       case '\n':
+               str_append(dest, "\\n");
+               break;
+       case '\r':
+               str_append(dest, "\\r");
+               break;
+       case '\t':
+               str_append(dest, "\\t");
+               break;
+       case '"':
+               str_append(dest, "\\\"");
+               break;
+       case '\\':
+               str_append(dest, "\\\\");
+               break;
+       default:
+               if (src < 0x20 || src >= 0x80)
+                       str_printfa(dest, "\\u%04x", src);
+               else
+                       str_append_c(dest, src);
+               break;
+       }
+}
+
+static void json_append_escaped_ucs4(string_t *dest, unichar_t chr)
+{
+       if (chr < 0x80)
+               json_append_escaped_char(dest, (unsigned char)chr);
+       else if (chr == 0x2028 || chr == 0x2029)
+               str_printfa(dest, "\\u%04x", chr);
+       else
+               uni_ucs4_to_utf8_c(chr, dest);
+}
+
+
+void json_append_escaped(string_t *dest, const char *src)
+{
+       json_append_escaped_data(dest, (const unsigned char*)src, strlen(src));
+}
+
+void json_append_escaped_data(string_t *dest, const unsigned char *src,
+                             size_t size)
+{
+       size_t i;
+       int bytes = 0;
+       unichar_t chr;
+
+       for (i = 0; i < size;) {
+               bytes = uni_utf8_get_char_n(src+i, size-i, &chr);
+               if (bytes > 0 && uni_is_valid_ucs4(chr)) {
+                       json_append_escaped_ucs4(dest, chr);
+                       i += bytes;
+               } else {
+                       str_append_data(dest, UNICODE_REPLACEMENT_CHAR_UTF8,
+                                       UTF8_REPLACEMENT_CHAR_LEN);
+                       i++;
+               }
+       }
+}
diff --git a/src/lib-json/json-generator.h b/src/lib-json/json-generator.h
new file mode 100644 (file)
index 0000000..1670f99
--- /dev/null
@@ -0,0 +1,97 @@
+#ifndef JSON_GENERATOR_H
+#define JSON_GENERATOR_H
+
+#include "json-types.h"
+
+#define json_append_escaped json_append_escaped_new
+#define json_append_escaped_data json_append_escaped_data_new
+
+// FIXME: add settings for formatting/indenting the output
+
+struct json_generator;
+
+enum json_generator_flags {
+       /* Hide the root array or object node. So, the top-level '[' and ']' or
+          '{' and '}' will not be written to the output. Generating a nomal
+          value as root with this flag set will trigger an assertion failure.
+        */
+       JSON_GENERATOR_FLAG_HIDE_ROOT = BIT(0),
+};
+
+struct json_generator *
+json_generator_init(struct ostream *output, enum json_generator_flags flags);
+struct json_generator *
+json_generator_init_str(string_t *buf, enum json_generator_flags flags);
+
+void json_generator_deinit(struct json_generator **_generator);
+
+int json_generator_flush(struct json_generator *generator);
+
+/* number */
+
+int json_generate_number(struct json_generator *generator,
+                        intmax_t number);
+int json_generate_number_raw(struct json_generator *generator,
+                            const char *number);
+
+/* string */
+
+void json_generate_string_open(struct json_generator *generator);
+ssize_t json_generate_string_more(struct json_generator *generator,
+                                 const void *data, size_t size, bool last);
+void json_generate_string_close(struct json_generator *generator);
+int json_generate_string_write_close(struct json_generator *generator);
+
+int json_generate_string_data(struct json_generator *generator,
+                             const void *data, size_t size);
+int json_generate_string(struct json_generator *generator, const char *str);
+
+/* null */
+
+int json_generate_null(struct json_generator *generator);
+
+/* false */
+
+int json_generate_false(struct json_generator *generator);
+
+/* true */
+
+int json_generate_true(struct json_generator *generator);
+
+/* object */
+
+void json_generate_object_open(struct json_generator *generator);
+int json_generate_object_member(struct json_generator *generator,
+                               const char *name);
+int json_generate_object_close(struct json_generator *generator);
+
+/* array */
+
+void json_generate_array_open(struct json_generator *generator);
+int json_generate_array_close(struct json_generator *generator);
+
+/* JSON-text */
+
+void json_generate_text_open(struct json_generator *generator);
+ssize_t json_generate_text_more(struct json_generator *generator,
+                               const void *data, size_t size);
+int json_generate_text_close(struct json_generator *generator);
+
+int json_generate_text_data(struct json_generator *generator,
+                           const void *data, size_t size);
+int json_generate_text(struct json_generator *generator, const char *str);
+
+/* value */
+
+int json_generate_value(struct json_generator *generator,
+                       enum json_type type, const struct json_value *value);
+
+/*
+ * Simple string output
+ */
+
+void json_append_escaped(string_t *dest, const char *src);
+void json_append_escaped_data(string_t *dest, const unsigned char *src,
+                             size_t size);
+
+#endif
diff --git a/src/lib-json/test-json-generator.c b/src/lib-json/test-json-generator.c
new file mode 100644 (file)
index 0000000..c3f33fd
--- /dev/null
@@ -0,0 +1,1436 @@
+/* Copyright (c) 2017-2023 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "ostream.h"
+#include "unichar.h"
+#include "test-common.h"
+
+#include "json-generator.h"
+
+#include <unistd.h>
+
+static bool debug = FALSE;
+
+static void test_json_generate_buffer(void)
+{
+       string_t *buffer;
+       struct ostream *output;
+       struct json_generator *generator;
+       unsigned int state, pos;
+       ssize_t sret;
+       int ret;
+
+       buffer = str_new(default_pool, 256);
+       output = o_stream_create_buffer(buffer);
+       o_stream_set_no_error_handling(output, TRUE);
+
+       /* number - integer */
+       test_begin("json write number - integer");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_number(generator, 23423);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("23423", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* number - raw */
+       test_begin("json write number - raw");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_number_raw(generator, "23423");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("23423", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* false */
+       test_begin("json write false");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_false(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("false", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* false */
+       test_begin("json write null");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_null(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("null", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* true */
+       test_begin("json write true");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_true(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("true", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string */
+       test_begin("json write string");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_string(generator, "frop!");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"frop!\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string - TAB */
+       test_begin("json write string - TAB");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_string(generator, "frop\tfriep");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"frop\\tfriep\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string - LF */
+       test_begin("json write string - LF");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_string(generator, "frop\nfriep");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"frop\\nfriep\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string - LF,TAB */
+       test_begin("json write string - CR,LF,TAB");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_string(generator, "frop\r\n\tfriep");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"frop\\r\\n\\tfriep\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string - quotes */
+       test_begin("json write string - quotes");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_string(generator, "\"frop\"");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"\\\"frop\\\"\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string - slashes */
+       test_begin("json write string - slashes");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_string(generator, "frop\\friep/frml");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"frop\\\\friep/frml\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string - slashes */
+       test_begin("json write string - BS,FF");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_string(generator, "\x08\x0c");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"\\b\\f\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string - bad UTF-8 */
+       test_begin("json write string - bad UTF-8");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_string(generator, "\xc3\x28");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"\xEF\xBF\xBD(\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string - bad UTF-8 code point */
+       test_begin("json write string - bad UTF-8 code point");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_string(generator, "\xed\xa0\xbd");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       // FIXME: this should ideally produce just one replacement char
+       test_assert(strcmp("\"\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD\"",
+                   str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string - long */
+       test_begin("json write string - long");
+       generator = json_generator_init(output, 0);
+       json_generate_string_open(generator);
+       sret = (int)json_generate_string_more(generator,
+               "frop", strlen("frop"), FALSE);
+       test_assert(sret > 0);
+       sret = (int)json_generate_string_more(generator,
+               "frop", strlen("frop"), FALSE);
+       test_assert(sret > 0);
+       sret = (int)json_generate_string_more(generator,
+               "frop", strlen("frop"), TRUE);
+       test_assert(sret > 0);
+       json_generate_string_close(generator);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"fropfropfrop\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* <JSON-text> */
+       test_begin("json write <JSON-text>");
+       generator = json_generator_init(output, 0);
+       ret = json_generate_text(generator, "[\"frop!\"]");
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[\"frop!\"]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* <JSON-text> - long */
+       test_begin("json write <JSON-text> - long");
+       generator = json_generator_init(output, 0);
+       json_generate_text_open(generator);
+       sret = (int)json_generate_text_more(generator,
+               "\"frop", strlen("\"frop"));
+       test_assert(sret > 0);
+       sret = (int)json_generate_text_more(generator,
+               "frop", strlen("frop"));
+       test_assert(sret > 0);
+       sret = (int)json_generate_text_more(generator,
+               "frop\"", strlen("frop\""));
+       test_assert(sret > 0);
+       ret = json_generate_text_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"fropfropfrop\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ ] */
+       test_begin("json write array - [ ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ number ] */
+       test_begin("json write array - [ number ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_number(generator, 23423);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[23423]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ string ] */
+       test_begin("json write array - [ string ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_string(generator, "frop");
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[\"frop\"]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ false ] */
+       test_begin("json write array - [ false ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_false(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[false]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ null ] */
+       test_begin("json write array - [ null ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_null(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[null]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ true ] */
+       test_begin("json write array - [ true ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_true(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[true]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ [] ] */
+       test_begin("json write array - [ [] ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[[]]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ {} ] */
+       test_begin("json write array - [ {} ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       json_generate_object_open(generator);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[{}]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ <JSON-text> ] */
+       test_begin("json write array - [ <JSON-text> ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_text(generator, "{\"a\":1,\"b\":2,\"c\":3}");
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[{\"a\":1,\"b\":2,\"c\":3}]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ string, <JSON-text> ] */
+       test_begin("json write array - [ string, <JSON-text> ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       json_generate_string(generator, "frop");
+       ret = json_generate_text(generator, "{\"a\":1,\"b\":2,\"c\":3}");
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[\"frop\",{\"a\":1,\"b\":2,\"c\":3}]",
+                   str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ true, true ] */
+       test_begin("json write array - [ true, true ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_true(generator);
+       test_assert(ret > 0);
+       ret = json_generate_true(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[true,true]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ true, true, true ] */
+       test_begin("json write array - [ true, true, true ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_true(generator);
+       test_assert(ret > 0);
+       ret = json_generate_true(generator);
+       test_assert(ret > 0);
+       ret = json_generate_true(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[true,true,true]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ "frop", "friep", "frml" ] */
+       test_begin("json write array - [ \"frop\", \"friep\", \"frml\" ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_string(generator, "frop");
+       test_assert(ret > 0);
+       ret = json_generate_string(generator, "friep");
+       test_assert(ret > 0);
+       ret = json_generate_string(generator, "frml");
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[\"frop\",\"friep\",\"frml\"]",
+                   str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ 1, 2, 3 ] */
+       test_begin("json write array - [ 1, 2, 3 ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_number(generator, 1);
+       test_assert(ret > 0);
+       ret = json_generate_number(generator, 2);
+       test_assert(ret > 0);
+       ret = json_generate_number(generator, 3);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[1,2,3]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ [], [], [] ] */
+       test_begin("json write array - [ [], [], [] ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[[],[],[]]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ {}, {}, {} ] */
+       test_begin("json write array - [ {}, {}, {} ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       json_generate_object_open(generator);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[{},{},{}]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ [ [], [], [] ], [ [], [], [] ], [ [], [], [] ] ] */
+       test_begin("json write array - "
+               "[ [ [], [], [] ], [ [], [], [] ], [ [], [], [] ] ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       json_generate_array_open(generator);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[[[],[],[]],[[],[],[]],[[],[],[]]]",
+                   str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ <JSON-text>, <JSON-text>, <JSON-text> ] */
+       test_begin("json write array - "
+                  "[ <JSON-text>, <JSON-text>, <JSON-text> ]");
+       generator = json_generator_init(output, 0);
+       json_generate_array_open(generator);
+       ret = json_generate_text(generator, "true");
+       test_assert(ret > 0);
+       ret = json_generate_text(generator, "1234234");
+       test_assert(ret > 0);
+       ret = json_generate_text(generator, "\"frml\"");
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("[true,1234234,\"frml\"]", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* array - hidden root */
+       test_begin("json write array - hidden_root");
+       generator = json_generator_init(output, JSON_GENERATOR_FLAG_HIDE_ROOT);
+       json_generate_array_open(generator);
+       ret = json_generate_string(generator, "frop");
+       test_assert(ret > 0);
+       ret = json_generate_string(generator, "friep");
+       test_assert(ret > 0);
+       ret = json_generate_string(generator, "frml");
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"frop\",\"friep\",\"frml\"", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { } */
+       test_begin("json write object - { }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{}", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": 1 } */
+       test_begin("json write object - { \"frop\": 1 }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "frop");
+       test_assert(ret > 0);
+       ret = json_generate_number(generator, 1);
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{\"frop\":1}", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": "friep" } */
+       test_begin("json write object - { \"frop\": \"friep\" }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "frop");
+       test_assert(ret > 0);
+       ret = json_generate_string(generator, "friep");
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{\"frop\":\"friep\"}", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": false } */
+       test_begin("json write object - { \"frop\": false }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "frop");
+       test_assert(ret > 0);
+       ret = json_generate_false(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{\"frop\":false}", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": [] } */
+       test_begin("json write object - { \"frop\": [] }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "frop");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{\"frop\":[]}", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": {} } */
+       test_begin("json write object - { \"frop\": {} }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "frop");
+       test_assert(ret > 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{\"frop\":{}}", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": <JSON-text> } */
+       test_begin("json write object - { \"frop\": <JSON-text> }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "frop");
+       test_assert(ret > 0);
+       ret = json_generate_text(generator, "[\"friep\",1,true]");
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{\"frop\":[\"friep\",1,true]}",
+                   str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": {}, "friep": {} } */
+       test_begin("json write object - { \"frop\": {}, \"friep\": {} }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "frop");
+       test_assert(ret > 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_member(generator, "friep");
+       test_assert(ret > 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{\"frop\":{},\"friep\":{}}", str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": [], "friep": [], "frml": [] } */
+       test_begin("json write object - "
+                  "{ \"frop\": [], \"friep\": [], \"frml\": [] }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "frop");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_member(generator, "friep");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_member(generator, "frml");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{\"frop\":[],\"friep\":[],\"frml\":[]}",
+                   str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": [1], "friep": [true], "frml": ["a"] } */
+       test_begin("json write object - "
+                  "{ \"frop\": [1], \"friep\": [true], \"frml\": [\"a\"] }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "frop");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_number(generator, 1);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_member(generator, "friep");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_true(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_member(generator, "frml");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_string(generator, "a");
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{\"frop\":[1],\"friep\":[true],\"frml\":[\"a\"]}",
+                   str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "a": [{"d": 1}], "b": [{"e": 2}], "c": [{"f": 3}] } */
+       test_begin("json write object - "
+                  "{ \"a\": [{\"d\": 1}], \"b\": [{\"e\": 2}], "
+                  "\"c\": [{\"f\": 3}] }");
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "a");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "d");
+       test_assert(ret > 0);
+       ret = json_generate_number(generator, 1);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_member(generator, "b");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "e");
+       test_assert(ret > 0);
+       ret = json_generate_number(generator, 2);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_member(generator, "c");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "f");
+       test_assert(ret > 0);
+       ret = json_generate_number(generator, 3);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("{\"a\":[{\"d\":1}],"
+                            "\"b\":[{\"e\":2}],\"c\":[{\"f\":3}]}",
+                   str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* object - hidden root */
+       test_begin("json write object - hidden root");
+       generator = json_generator_init(output, JSON_GENERATOR_FLAG_HIDE_ROOT);
+       json_generate_object_open(generator);
+       ret = json_generate_object_member(generator, "frop");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_number(generator, 1);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_member(generator, "friep");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_true(generator);
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_member(generator, "frml");
+       test_assert(ret > 0);
+       json_generate_array_open(generator);
+       ret = json_generate_string(generator, "a");
+       test_assert(ret > 0);
+       ret = json_generate_array_close(generator);
+       test_assert(ret > 0);
+       ret = json_generate_object_close(generator);
+       test_assert(ret > 0);
+       json_generator_flush(generator);
+       test_assert(ret > 0);
+       json_generator_deinit(&generator);
+       test_assert(strcmp("\"frop\":[1],\"friep\":[true],\"frml\":[\"a\"]",
+                          str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* trickle [1] */
+       test_begin("json write object - trickle[1]");
+       o_stream_set_max_buffer_size(output, 0);
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       state = 0;
+       for (pos = 0; pos < 65535 && state <= 15; pos++) {
+               o_stream_set_max_buffer_size(output, pos);
+               switch (state) {
+               case 0:
+                       ret = json_generate_object_member(generator, "aaaaaa");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_array_open(generator);
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 1:
+                       ret = json_generate_object_member(generator, "dddddd");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 2:
+                       ret = json_generate_number(generator, 1);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 3:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 4:
+                       ret = json_generate_array_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 5:
+                       ret = json_generate_object_member(generator, "bbbbbb");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_array_open(generator);
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 6:
+                       ret = json_generate_object_member(generator, "eeeeee");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 7:
+                       ret = json_generate_number(generator, 2);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 8:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 9:
+                       ret = json_generate_array_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 10:
+                       ret = json_generate_object_member(generator, "cccccc");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_array_open(generator);
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 11:
+                       ret = json_generate_object_member(generator, "ffffff");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 12:
+                       ret = json_generate_number(generator, 3);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 13:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 14:
+                       ret = json_generate_array_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 15:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               }
+       }
+       json_generator_deinit(&generator);
+       test_assert(state == 16);
+       test_assert(strcmp("{\"aaaaaa\":[{\"dddddd\":1}],"
+                          "\"bbbbbb\":[{\"eeeeee\":2}],"
+                          "\"cccccc\":[{\"ffffff\":3}]}",
+                          str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* trickle [2] */
+       test_begin("json write object - trickle[2]");
+       o_stream_set_max_buffer_size(output, 0);
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       state = 0;
+       for (pos = 0; pos < 65535 && state <= 24; pos++) {
+               o_stream_set_max_buffer_size(output, pos);
+               switch (state) {
+               case 0:
+                       ret = json_generate_object_member(generator, "aaaaaa");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_array_open(generator);
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 1:
+                       ret = json_generate_object_member(generator, "dddddd");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 2:
+                       ret = json_generate_number(generator, 1);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 3:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 4:
+                       ret = json_generate_object_member(generator, "gggggg");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 5:
+                       ret = json_generate_number(generator, 4);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 6:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 7:
+                       ret = json_generate_array_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 8:
+                       ret = json_generate_object_member(generator, "bbbbbb");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_array_open(generator);
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 9:
+                       ret = json_generate_object_member(generator, "eeeeee");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 10:
+                       ret = json_generate_number(generator, 2);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 11:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 12:
+                       ret = json_generate_object_member(generator, "hhhhhh");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 13:
+                       ret = json_generate_number(generator, 5);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 14:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 15:
+                       ret = json_generate_array_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 16:
+                       ret = json_generate_object_member(generator, "cccccc");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_array_open(generator);
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 17:
+                       ret = json_generate_object_member(generator, "ffffff");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 18:
+                       ret = json_generate_number(generator, 3);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 19:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 20:
+                       ret = json_generate_object_member(generator, "iiiiii");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 21:
+                       ret = json_generate_number(generator, 6);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 22:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 23:
+                       ret = json_generate_array_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 24:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               }
+       }
+       json_generator_deinit(&generator);
+       test_assert(state == 25);
+       test_assert(strcmp("{\"aaaaaa\":[{\"dddddd\":1},{\"gggggg\":4}],"
+                          "\"bbbbbb\":[{\"eeeeee\":2},{\"hhhhhh\":5}],"
+                          "\"cccccc\":[{\"ffffff\":3},{\"iiiiii\":6}]}",
+                          str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* trickle[3] */
+       test_begin("json write object - trickle[3]");
+       o_stream_set_max_buffer_size(output, 0);
+       generator = json_generator_init(output, 0);
+       json_generate_object_open(generator);
+       state = 0;
+       for (pos = 0; pos < 65535 && state <= 15; pos++) {
+               o_stream_set_max_buffer_size(output, pos);
+               switch (state) {
+               case 0:
+                       ret = json_generate_object_member(generator, "aaaaaa");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_array_open(generator);
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 1:
+                       ret = json_generate_object_member(generator, "dddddd");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 2:
+                       ret = json_generate_text(generator, "1234567");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 3:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 4:
+                       ret = json_generate_array_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 5:
+                       ret = json_generate_object_member(generator, "bbbbbb");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_array_open(generator);
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 6:
+                       ret = json_generate_object_member(generator, "eeeeee");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 7:
+                       ret = json_generate_text(generator, "[1,2,3,4,5,6,7]");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 8:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 9:
+                       ret = json_generate_array_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 10:
+                       ret = json_generate_object_member(generator, "cccccc");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_generate_array_open(generator);
+                       json_generate_object_open(generator);
+                       state++;
+                       continue;
+               case 11:
+                       ret = json_generate_object_member(generator, "ffffff");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 12:
+                       ret = json_generate_text(generator, "\"1234567\"");
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 13:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 14:
+                       ret = json_generate_array_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 15:
+                       ret = json_generate_object_close(generator);
+                       test_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               }
+       }
+       json_generator_deinit(&generator);
+       test_assert(state == 16);
+       test_assert(strcmp("{\"aaaaaa\":[{\"dddddd\":1234567}],"
+                          "\"bbbbbb\":[{\"eeeeee\":[1,2,3,4,5,6,7]}],"
+                          "\"cccccc\":[{\"ffffff\":\"1234567\"}]}",
+                          str_c(buffer)) == 0);
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       o_stream_destroy(&output);
+       str_free(&buffer);
+}
+
+static void test_json_append_escaped(void)
+{
+       string_t *str = t_str_new(32);
+
+       test_begin("json_append_escaped()");
+       json_append_escaped(str, "\b\f\r\n\t\"\\\001\002-\xC3\xA4\xf0\x90"
+                                "\x90\xb7\xe2\x80\xa8\xe2\x80\xa9\xff");
+       test_assert(strcmp(str_c(str),
+                          "\\b\\f\\r\\n\\t\\\"\\\\\\u0001\\u0002-"
+                          "\xC3\xA4\xf0\x90\x90\xb7\\u2028\\u2029"
+                          ""UNICODE_REPLACEMENT_CHAR_UTF8) == 0);
+       test_end();
+}
+
+static void test_json_append_escaped_data(void)
+{
+       static const unsigned char test_input[] =
+               "\b\f\r\n\t\"\\\000\001\002-\xC3\xA4\xf0\x90"
+               "\x90\xb7\xe2\x80\xa8\xe2\x80\xa9\xff";
+       string_t *str = t_str_new(32);
+
+       test_begin("json_append_escaped_data()");
+       json_append_escaped_data(str, test_input, sizeof(test_input)-1);
+       test_assert(strcmp(str_c(str),
+                          "\\b\\f\\r\\n\\t\\\"\\\\\\u0000\\u0001\\u0002-"
+                          "\xC3\xA4\xf0\x90\x90\xb7\\u2028\\u2029"
+                          UNICODE_REPLACEMENT_CHAR_UTF8) == 0);
+       test_end();
+}
+
+int main(int argc, char *argv[])
+{
+       int c;
+
+       static void (*test_functions[])(void) = {
+               test_json_generate_buffer,
+               test_json_append_escaped,
+               test_json_append_escaped_data,
+               NULL
+       };
+
+       while ((c = getopt(argc, argv, "D")) > 0) {
+               switch (c) {
+               case 'D':
+                       debug = TRUE;
+                       break;
+               default:
+                       i_fatal("Usage: %s [-D]", argv[0]);
+               }
+       }
+
+       return test_run(test_functions);
+}
diff --git a/src/lib-json/test-json-io.c b/src/lib-json/test-json-io.c
new file mode 100644 (file)
index 0000000..31f8e6f
--- /dev/null
@@ -0,0 +1,1284 @@
+/* Copyright (c) 2017-2023 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "randgen.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-temp.h"
+#include "iostream-pump.h"
+#include "test-common.h"
+
+#include "json-parser.new.h"
+#include "json-generator.h"
+
+#include <stdio.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <ctype.h>
+
+static bool debug = FALSE;
+
+struct json_io_test {
+       const char *input;
+       const char *output;
+       struct json_limits limits;
+       enum json_parser_flags flags;
+};
+
+static const struct json_io_test
+tests[] = {
+       {
+               .input = "123456789"
+       },{
+               .input = "\"frop\""
+       },{
+               .input = "false"
+       },{
+               .input = "null"
+       },{
+               .input = "true"
+       },{
+               .input = "[]"
+       },{
+               .input = "[[]]"
+       },{
+               .input = "[[[[[[[[[[[[]]]]]]]]]]]]"
+       },{
+               .input = "[[],[],[]]"
+       },{
+               .input = "[[[],[],[]],[[],[],[]],[[],[],[]]]"
+       },{
+               .input = "{}"
+       },{
+               .input = "[\"frop\"]"
+       },{
+               .input = "[\"frop\",\"friep\"]"
+       },{
+               .input = "[\"frop\",\"friep\",\"frml\"]"
+       },{
+               .input = "[\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\"]"
+       },{
+               .input = "[true]"
+       },{
+               .input = "[null]"
+       },{
+               .input = "[true,false]"
+       },{
+               .input = "[true,true,false,false]"
+       },{
+               .input = "[1]"
+       },{
+               .input = "[1,12]"
+       },{
+               .input = "[1,12,123]"
+       },{
+               .input = "[1,12,123,1234]"
+       },{
+               .input = "[1,2,3,4,5,6,7]"
+       },{
+               .input = "{\"frop\":1}"
+       },{
+               .input = "{\"a\":\"1\",\"b\":\"2\",\"c\":\"3\",\"d\":\"4\",\"e\":\"5\",\"f\":\"6\",\"g\":\"7\"}"
+       },{
+               .input = "[{\"frop\":1},{\"frop\":1},{\"frop\":1},{\"frop\":1},{\"frop\":1}]"
+       },{
+               .input = "[[\"frop\",1],[\"frop\",1],[\"frop\",1],[\"frop\",1],[\"frop\",1]]"
+       },{
+               .input = "[[\"frop\",[]],[\"frop\",[]],[\"frop\",[]],[\"frop\",[]],[\"frop\",[]]]"
+       },{
+               .input = "[[\"frop\"],[1],[\"frop\"],[1],[\"frop\"],[1],[\"frop\"],[1],[\"frop\"],[1]]"
+       },{
+               .input = "[[\"frop\"],[1,2,false],[\"frop\"],[1,2,false],[\"frop\"],[1,2,false],[\"frop\"],[1,2,false],[\"frop\"],[1,2,false]]"
+       },{
+               .input = "[[\"frop\",{}],[\"frop\",{}],[\"frop\",{}],[\"frop\",{}],[\"frop\",{}]]"
+       },{
+               .input = "{\"a\":{\"b\":{\"c\":{\"d\":{\"e\":{\"f\":{\"g\":{\"h\":{}}}}}}}}}"
+       },{
+               .input =
+                       "{\n"
+                       "    \"glossary\": {\n"
+                       "        \"title\": \"example glossary\",\n"
+                       "               \"GlossDiv\": {\n"
+                       "            \"title\": \"S\",\n"
+                       "                       \"GlossList\": {\n"
+                       "                \"GlossEntry\": {\n"
+                       "                    \"ID\": \"SGML\",\n"
+                       "                                       \"SortAs\": \"SGML\",\n"
+                       "                                       \"GlossTerm\": \"Standard Generalized Markup Language\",\n"
+                       "                                       \"Acronym\": \"SGML\",\n"
+                       "                                       \"Abbrev\": \"ISO 8879:1986\",\n"
+                       "                                       \"GlossDef\": {\n"
+                       "                        \"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\n"
+                       "                                               \"GlossSeeAlso\": [\"GML\", \"XML\"]\n"
+                       "                    },\n"
+                       "                                       \"GlossSee\": \"markup\"\n"
+                       "                }\n"
+                       "            }\n"
+                       "        }\n"
+                       "    }\n"
+                       "}\n",
+               .output =
+                       "{\"glossary\":{\"title\":\"example glossary\",\"GlossDiv\":{"
+                       "\"title\":\"S\",\"GlossList\":{\"GlossEntry\":{\"ID\":\"SGML\","
+                       "\"SortAs\":\"SGML\",\"GlossTerm\":\"Standard Generalized Markup Language\","
+                       "\"Acronym\":\"SGML\",\"Abbrev\":\"ISO 8879:1986\",\"GlossDef\":{"
+                       "\"para\":\"A meta-markup language, used to create markup languages such as DocBook.\","
+                       "\"GlossSeeAlso\":[\"GML\",\"XML\"]},\"GlossSee\":\"markup\"}}}}}"
+       },{
+               .input =
+                       "{\"menu\": {\n"
+                       "  \"id\": \"file\",\n"
+                       "  \"value\": \"File\",\n"
+                       "  \"popup\": {\n"
+                       "    \"menuitem\": [\n"
+                       "      {\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n"
+                       "      {\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n"
+                       "      {\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n"
+                       "    ]\n"
+                       "  }\n"
+                       "}}\n",
+               .output =
+                       "{\"menu\":{\"id\":\"file\",\"value\":\"File\","
+                       "\"popup\":{\"menuitem\":[{\"value\":\"New\",\"onclick\":"
+                       "\"CreateNewDoc()\"},{\"value\":\"Open\",\"onclick\":\"OpenDoc()\"},"
+                       "{\"value\":\"Close\",\"onclick\":\"CloseDoc()\"}]}}}"
+       },{
+               .input =
+                       "{\"widget\": {\n"
+                       "    \"debug\": \"on\",\n"
+                       "    \"window\": {\n"
+                       "        \"title\": \"Sample Konfabulator Widget\",\n"
+                       "        \"name\": \"main_window\",\n"
+                       "        \"width\": 500,\n"
+                       "        \"height\": 500\n"
+                       "    },\n"
+                       "    \"image\": { \n"
+                       "        \"src\": \"Images/Sun.png\",\n"
+                       "        \"name\": \"sun1\",\n"
+                       "        \"hOffset\": 250,\n"
+                       "        \"vOffset\": 250,\n"
+                       "        \"alignment\": \"center\"\n"
+                       "    },\n"
+                       "    \"text\": {\n"
+                       "        \"data\": \"Click Here\",\n"
+                       "        \"size\": 36,\n"
+                       "        \"style\": \"bold\",\n"
+                       "        \"name\": \"text1\",\n"
+                       "        \"hOffset\": 250,\n"
+                       "        \"vOffset\": 100,\n"
+                       "        \"alignment\": \"center\",\n"
+                       "        \"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\"\n"
+                       "    }\n"
+                       "}}\n",
+               .output =
+                       "{\"widget\":{\"debug\":\"on\",\"window\":{"
+                       "\"title\":\"Sample Konfabulator Widget\","
+                       "\"name\":\"main_window\",\"width\":500,"
+                       "\"height\":500},\"image\":{\"src\":\"Images/Sun.png\","
+                       "\"name\":\"sun1\",\"hOffset\":250,\"vOffset\":250,"
+                       "\"alignment\":\"center\"},\"text\":{\"data\":\"Click Here\","
+                       "\"size\":36,\"style\":\"bold\",\"name\":\"text1\","
+                       "\"hOffset\":250,\"vOffset\":100,\"alignment\":\"center\","
+                       "\"onMouseUp\":\"sun1.opacity = (sun1.opacity / 100) * 90;\"}}}"
+       },{
+               .input =
+                       "{\"web-app\": {\r\n"
+                       "  \"servlet\": [   \r\n"
+                       "    {\r\n"
+                       "      \"servlet-name\": \"cofaxCDS\",\r\n"
+                       "      \"servlet-class\": \"org.cofax.cds.CDSServlet\",\r\n"
+                       "      \"init-param\": {\r\n"
+                       "        \"configGlossary:installationAt\": \"Philadelphia, PA\",\r\n"
+                       "        \"configGlossary:adminEmail\": \"ksm@pobox.com\",\r\n"
+                       "        \"configGlossary:poweredBy\": \"Cofax\",\r\n"
+                       "        \"configGlossary:poweredByIcon\": \"/images/cofax.gif\",\r\n"
+                       "        \"configGlossary:staticPath\": \"/content/static\",\r\n"
+                       "        \"templateProcessorClass\": \"org.cofax.WysiwygTemplate\",\r\n"
+                       "        \"templateLoaderClass\": \"org.cofax.FilesTemplateLoader\",\r\n"
+                       "        \"templatePath\": \"templates\",\r\n"
+                       "        \"templateOverridePath\": \"\",\r\n"
+                       "        \"defaultListTemplate\": \"listTemplate.htm\",\r\n"
+                       "        \"defaultFileTemplate\": \"articleTemplate.htm\",\r\n"
+                       "        \"useJSP\": false,\r\n"
+                       "        \"jspListTemplate\": \"listTemplate.jsp\",\r\n"
+                       "        \"jspFileTemplate\": \"articleTemplate.jsp\",\r\n"
+                       "        \"cachePackageTagsTrack\": 200,\r\n"
+                       "        \"cachePackageTagsStore\": 200,\r\n"
+                       "        \"cachePackageTagsRefresh\": 60,\r\n"
+                       "        \"cacheTemplatesTrack\": 100,\r\n"
+                       "        \"cacheTemplatesStore\": 50,\r\n"
+                       "        \"cacheTemplatesRefresh\": 15,\r\n"
+                       "        \"cachePagesTrack\": 200,\r\n"
+                       "        \"cachePagesStore\": 100,\r\n"
+                       "        \"cachePagesRefresh\": 10,\r\n"
+                       "        \"cachePagesDirtyRead\": 10,\r\n"
+                       "        \"searchEngineListTemplate\": \"forSearchEnginesList.htm\",\r\n"
+                       "        \"searchEngineFileTemplate\": \"forSearchEngines.htm\",\r\n"
+                       "        \"searchEngineRobotsDb\": \"WEB-INF/robots.db\",\r\n"
+                       "        \"useDataStore\": true,\r\n"
+                       "        \"dataStoreClass\": \"org.cofax.SqlDataStore\",\r\n"
+                       "        \"redirectionClass\": \"org.cofax.SqlRedirection\",\r\n"
+                       "        \"dataStoreName\": \"cofax\",\r\n"
+                       "        \"dataStoreDriver\": \"com.microsoft.jdbc.sqlserver.SQLServerDriver\",\r\n"
+                       "        \"dataStoreUrl\": \"jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon\",\r\n"
+                       "        \"dataStoreUser\": \"sa\",\r\n"
+                       "        \"dataStorePassword\": \"dataStoreTestQuery\",\r\n"
+                       "        \"dataStoreTestQuery\": \"SET NOCOUNT ON;select test='test';\",\r\n"
+                       "        \"dataStoreLogFile\": \"/usr/local/tomcat/logs/datastore.log\",\r\n"
+                       "        \"dataStoreInitConns\": 10,\r\n"
+                       "        \"dataStoreMaxConns\": 100,\r\n"
+                       "        \"dataStoreConnUsageLimit\": 100,\r\n"
+                       "        \"dataStoreLogLevel\": \"debug\",\r\n"
+                       "        \"maxUrlLength\": 500}},\r\n"
+                       "    {\r\n"
+                       "      \"servlet-name\": \"cofaxEmail\",\r\n"
+                       "      \"servlet-class\": \"org.cofax.cds.EmailServlet\",\r\n"
+                       "      \"init-param\": {\r\n"
+                       "      \"mailHost\": \"mail1\",\r\n"
+                       "      \"mailHostOverride\": \"mail2\"}},\r\n"
+                       "    {\r\n"
+                       "      \"servlet-name\": \"cofaxAdmin\",\r\n"
+                       "      \"servlet-class\": \"org.cofax.cds.AdminServlet\"},\r\n"
+                       " \r\n"
+                       "    {\r\n"
+                       "      \"servlet-name\": \"fileServlet\",\r\n"
+                       "      \"servlet-class\": \"org.cofax.cds.FileServlet\"},\r\n"
+                       "    {\r\n"
+                       "      \"servlet-name\": \"cofaxTools\",\r\n"
+                       "      \"servlet-class\": \"org.cofax.cms.CofaxToolsServlet\",\r\n"
+                       "      \"init-param\": {\r\n"
+                       "        \"templatePath\": \"toolstemplates/\",\r\n"
+                       "        \"log\": 1,\r\n"
+                       "        \"logLocation\": \"/usr/local/tomcat/logs/CofaxTools.log\",\r\n"
+                       "        \"logMaxSize\": \"\",\r\n"
+                       "        \"dataLog\": 1,\r\n"
+                       "        \"dataLogLocation\": \"/usr/local/tomcat/logs/dataLog.log\",\r\n"
+                       "        \"dataLogMaxSize\": \"\",\r\n"
+                       "        \"removePageCache\": \"/content/admin/remove?cache=pages&id=\",\r\n"
+                       "        \"removeTemplateCache\": \"/content/admin/remove?cache=templates&id=\",\r\n"
+                       "        \"fileTransferFolder\": \"/usr/local/tomcat/webapps/content/fileTransferFolder\",\r\n"
+                       "        \"lookInContext\": 1,\r\n"
+                       "        \"adminGroupID\": 4,\r\n"
+                       "        \"betaServer\": true}}],\r\n"
+                       "  \"servlet-mapping\": {\r\n"
+                       "    \"cofaxCDS\": \"/\",\r\n"
+                       "    \"cofaxEmail\": \"/cofaxutil/aemail/*\",\r\n"
+                       "    \"cofaxAdmin\": \"/admin/*\",\r\n"
+                       "    \"fileServlet\": \"/static/*\",\r\n"
+                       "    \"cofaxTools\": \"/tools/*\"},\r\n"
+                       " \r\n"
+                       "  \"taglib\": {\r\n"
+                       "    \"taglib-uri\": \"cofax.tld\",\r\n"
+                       "    \"taglib-location\": \"/WEB-INF/tlds/cofax.tld\"}}}",
+               .output =
+                       "{\"web-app\":{\"servlet\":[{\"servlet-name\":\"cofaxCDS\","
+                       "\"servlet-class\":\"org.cofax.cds.CDSServlet\","
+                       "\"init-param\":{\"configGlossary:installationAt\":\"Philadelphia, PA\","
+                       "\"configGlossary:adminEmail\":\"ksm@pobox.com\","
+                       "\"configGlossary:poweredBy\":\"Cofax\","
+                       "\"configGlossary:poweredByIcon\":\"/images/cofax.gif\","
+                       "\"configGlossary:staticPath\":\"/content/static\","
+                       "\"templateProcessorClass\":\"org.cofax.WysiwygTemplate\","
+                       "\"templateLoaderClass\":\"org.cofax.FilesTemplateLoader\","
+                       "\"templatePath\":\"templates\","
+                       "\"templateOverridePath\":\"\","
+                       "\"defaultListTemplate\":\"listTemplate.htm\","
+                       "\"defaultFileTemplate\":\"articleTemplate.htm\","
+                       "\"useJSP\":false,\"jspListTemplate\":\"listTemplate.jsp\","
+                       "\"jspFileTemplate\":\"articleTemplate.jsp\","
+                       "\"cachePackageTagsTrack\":200,\"cachePackageTagsStore\":200,"
+                       "\"cachePackageTagsRefresh\":60,\"cacheTemplatesTrack\":100,"
+                       "\"cacheTemplatesStore\":50,\"cacheTemplatesRefresh\":15,"
+                       "\"cachePagesTrack\":200,\"cachePagesStore\":100,"
+                       "\"cachePagesRefresh\":10,\"cachePagesDirtyRead\":10,"
+                       "\"searchEngineListTemplate\":\"forSearchEnginesList.htm\","
+                       "\"searchEngineFileTemplate\":\"forSearchEngines.htm\","
+                       "\"searchEngineRobotsDb\":\"WEB-INF/robots.db\","
+                       "\"useDataStore\":true,\"dataStoreClass\":\"org.cofax.SqlDataStore\","
+                       "\"redirectionClass\":\"org.cofax.SqlRedirection\","
+                       "\"dataStoreName\":\"cofax\","
+                       "\"dataStoreDriver\":\"com.microsoft.jdbc.sqlserver.SQLServerDriver\","
+                       "\"dataStoreUrl\":\"jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon\","
+                       "\"dataStoreUser\":\"sa\",\"dataStorePassword\":\"dataStoreTestQuery\","
+                       "\"dataStoreTestQuery\":\"SET NOCOUNT ON;select test='test';\","
+                       "\"dataStoreLogFile\":\"/usr/local/tomcat/logs/datastore.log\","
+                       "\"dataStoreInitConns\":10,\"dataStoreMaxConns\":100,"
+                       "\"dataStoreConnUsageLimit\":100,\"dataStoreLogLevel\":\"debug\","
+                       "\"maxUrlLength\":500}},{\"servlet-name\":\"cofaxEmail\","
+                       "\"servlet-class\":\"org.cofax.cds.EmailServlet\","
+                       "\"init-param\":{\"mailHost\":\"mail1\",\"mailHostOverride\":\"mail2\"}},"
+                       "{\"servlet-name\":\"cofaxAdmin\","
+                       "\"servlet-class\":\"org.cofax.cds.AdminServlet\"},{"
+                       "\"servlet-name\":\"fileServlet\","
+                       "\"servlet-class\":\"org.cofax.cds.FileServlet\"},{"
+                       "\"servlet-name\":\"cofaxTools\","
+                       "\"servlet-class\":\"org.cofax.cms.CofaxToolsServlet\","
+                       "\"init-param\":{\"templatePath\":\"toolstemplates/\","
+                       "\"log\":1,\"logLocation\":\"/usr/local/tomcat/logs/CofaxTools.log\","
+                       "\"logMaxSize\":\"\",\"dataLog\":1,"
+                       "\"dataLogLocation\":\"/usr/local/tomcat/logs/dataLog.log\","
+                       "\"dataLogMaxSize\":\"\","
+                       "\"removePageCache\":\"/content/admin/remove?cache=pages&id=\","
+                       "\"removeTemplateCache\":\"/content/admin/remove?cache=templates&id=\","
+                       "\"fileTransferFolder\":\"/usr/local/tomcat/webapps/content/fileTransferFolder\","
+                       "\"lookInContext\":1,\"adminGroupID\":4,\"betaServer\":true}}],"
+                       "\"servlet-mapping\":{\"cofaxCDS\":\"/\","
+                       "\"cofaxEmail\":\"/cofaxutil/aemail/*\","
+                       "\"cofaxAdmin\":\"/admin/*\",\"fileServlet\":\"/static/*\","
+                       "\"cofaxTools\":\"/tools/*\"},\"taglib\":{"
+                       "\"taglib-uri\":\"cofax.tld\","
+                       "\"taglib-location\":\"/WEB-INF/tlds/cofax.tld\"}}}"
+       },{
+               .input =
+                       "{\"menu\": {\r\n"
+                       "    \"header\": \"SVG Viewer\",\r\n"
+                       "    \"items\": [\r\n"
+                       "        {\"id\": \"Open\"},\r\n"
+                       "        {\"id\": \"OpenNew\", \"label\": \"Open New\"},\r\n"
+                       "        null,\r\n"
+                       "        {\"id\": \"ZoomIn\", \"label\": \"Zoom In\"},\r\n"
+                       "        {\"id\": \"ZoomOut\", \"label\": \"Zoom Out\"},\r\n"
+                       "        {\"id\": \"OriginalView\", \"label\": \"Original View\"},\r\n"
+                       "        null,\r\n"
+                       "        {\"id\": \"Quality\"},\r\n"
+                       "        {\"id\": \"Pause\"},\r\n"
+                       "        {\"id\": \"Mute\"},\r\n"
+                       "        null,\r\n"
+                       "        {\"id\": \"Find\", \"label\": \"Find...\"},\r\n"
+                       "        {\"id\": \"FindAgain\", \"label\": \"Find Again\"},\r\n"
+                       "        {\"id\": \"Copy\"},\r\n"
+                       "        {\"id\": \"CopyAgain\", \"label\": \"Copy Again\"},\r\n"
+                       "        {\"id\": \"CopySVG\", \"label\": \"Copy SVG\"},\r\n"
+                       "        {\"id\": \"ViewSVG\", \"label\": \"View SVG\"},\r\n"
+                       "        {\"id\": \"ViewSource\", \"label\": \"View Source\"},\r\n"
+                       "        {\"id\": \"SaveAs\", \"label\": \"Save As\"},\r\n"
+                       "        null,\r\n"
+                       "        {\"id\": \"Help\"},\r\n"
+                       "        {\"id\": \"About\", \"label\": \"About Adobe CVG Viewer...\"}\r\n"
+                       "    ]\r\n"
+                       "}}",
+               .output =
+                       "{\"menu\":{\"header\":\"SVG Viewer\",\"items\":["
+                       "{\"id\":\"Open\"},{\"id\":\"OpenNew\",\"label\":\"Open New\"},"
+                       "null,{\"id\":\"ZoomIn\",\"label\":\"Zoom In\"},"
+                       "{\"id\":\"ZoomOut\",\"label\":\"Zoom Out\"},"
+                       "{\"id\":\"OriginalView\",\"label\":\"Original View\"},"
+                       "null,{\"id\":\"Quality\"},{\"id\":\"Pause\"},"
+                       "{\"id\":\"Mute\"},null,{\"id\":\"Find\",\"label\":\"Find...\"},"
+                       "{\"id\":\"FindAgain\",\"label\":\"Find Again\"},"
+                       "{\"id\":\"Copy\"},{\"id\":\"CopyAgain\",\"label\":\"Copy Again\"},"
+                       "{\"id\":\"CopySVG\",\"label\":\"Copy SVG\"},"
+                       "{\"id\":\"ViewSVG\",\"label\":\"View SVG\"},"
+                       "{\"id\":\"ViewSource\",\"label\":\"View Source\"},"
+                       "{\"id\":\"SaveAs\",\"label\":\"Save As\"},"
+                       "null,{\"id\":\"Help\"},"
+                       "{\"id\":\"About\",\"label\":\"About Adobe CVG Viewer...\"}]}}"
+       },{
+               .input =
+                       "{\r\n"
+                       "    \"$schema\": \"http://json-schema.org/draft-06/schema#\",\r\n"
+                       "    \"$id\": \"http://json-schema.org/draft-06/schema#\",\r\n"
+                       "    \"title\": \"Core schema meta-schema\",\r\n"
+                       "    \"definitions\": {\r\n"
+                       "        \"schemaArray\": {\r\n"
+                       "            \"type\": \"array\",\r\n"
+                       "            \"minItems\": 1,\r\n"
+                       "            \"items\": { \"$ref\": \"#\" }\r\n"
+                       "        },\r\n"
+                       "        \"nonNegativeInteger\": {\r\n"
+                       "            \"type\": \"integer\",\r\n"
+                       "            \"minimum\": 0\r\n"
+                       "        },\r\n"
+                       "        \"nonNegativeIntegerDefault0\": {\r\n"
+                       "            \"allOf\": [\r\n"
+                       "                { \"$ref\": \"#/definitions/nonNegativeInteger\" },\r\n"
+                       "                { \"default\": 0 }\r\n"
+                       "            ]\r\n"
+                       "        },\r\n"
+                       "        \"simpleTypes\": {\r\n"
+                       "            \"enum\": [\r\n"
+                       "                \"array\",\r\n"
+                       "                \"boolean\",\r\n"
+                       "                \"integer\",\r\n"
+                       "                \"null\",\r\n"
+                       "                \"number\",\r\n"
+                       "                \"object\",\r\n"
+                       "                \"string\"\r\n"
+                       "            ]\r\n"
+                       "        },\r\n"
+                       "        \"stringArray\": {\r\n"
+                       "            \"type\": \"array\",\r\n"
+                       "            \"items\": { \"type\": \"string\" },\r\n"
+                       "            \"uniqueItems\": true,\r\n"
+                       "            \"default\": []\r\n"
+                       "        }\r\n"
+                       "    },\r\n"
+                       "    \"type\": [\"object\", \"boolean\"],\r\n"
+                       "    \"properties\": {\r\n"
+                       "        \"$id\": {\r\n"
+                       "            \"type\": \"string\",\r\n"
+                       "            \"format\": \"uri-reference\"\r\n"
+                       "        },\r\n"
+                       "        \"$schema\": {\r\n"
+                       "            \"type\": \"string\",\r\n"
+                       "            \"format\": \"uri\"\r\n"
+                       "        },\r\n"
+                       "        \"$ref\": {\r\n"
+                       "            \"type\": \"string\",\r\n"
+                       "            \"format\": \"uri-reference\"\r\n"
+                       "        },\r\n"
+                       "        \"title\": {\r\n"
+                       "            \"type\": \"string\"\r\n"
+                       "        },\r\n"
+                       "        \"description\": {\r\n"
+                       "            \"type\": \"string\"\r\n"
+                       "        },\r\n"
+                       "        \"default\": {},\r\n"
+                       "        \"multipleOf\": {\r\n"
+                       "            \"type\": \"number\",\r\n"
+                       "            \"exclusiveMinimum\": 0\r\n"
+                       "        },\r\n"
+                       "        \"maximum\": {\r\n"
+                       "            \"type\": \"number\"\r\n"
+                       "        },\r\n"
+                       "        \"exclusiveMaximum\": {\r\n"
+                       "            \"type\": \"number\"\r\n"
+                       "        },\r\n"
+                       "        \"minimum\": {\r\n"
+                       "            \"type\": \"number\"\r\n"
+                       "        },\r\n"
+                       "        \"exclusiveMinimum\": {\r\n"
+                       "            \"type\": \"number\"\r\n"
+                       "        },\r\n"
+                       "        \"maxLength\": { \"$ref\": \"#/definitions/nonNegativeInteger\" },\r\n"
+                       "        \"minLength\": { \"$ref\": \"#/definitions/nonNegativeIntegerDefault0\" },\r\n"
+                       "        \"pattern\": {\r\n"
+                       "            \"type\": \"string\",\r\n"
+                       "            \"format\": \"regex\"\r\n"
+                       "        },\r\n"
+                       "        \"additionalItems\": { \"$ref\": \"#\" },\r\n"
+                       "        \"items\": {\r\n"
+                       "            \"anyOf\": [\r\n"
+                       "                { \"$ref\": \"#\" },\r\n"
+                       "                { \"$ref\": \"#/definitions/schemaArray\" }\r\n"
+                       "            ],\r\n"
+                       "            \"default\": {}\r\n"
+                       "        },\r\n"
+                       "        \"maxItems\": { \"$ref\": \"#/definitions/nonNegativeInteger\" },\r\n"
+                       "        \"minItems\": { \"$ref\": \"#/definitions/nonNegativeIntegerDefault0\" },\r\n"
+                       "        \"uniqueItems\": {\r\n"
+                       "            \"type\": \"boolean\",\r\n"
+                       "            \"default\": false\r\n"
+                       "        },\r\n"
+                       "        \"contains\": { \"$ref\": \"#\" },\r\n"
+                       "        \"maxProperties\": { \"$ref\": \"#/definitions/nonNegativeInteger\" },\r\n"
+                       "        \"minProperties\": { \"$ref\": \"#/definitions/nonNegativeIntegerDefault0\" },\r\n"
+                       "        \"required\": { \"$ref\": \"#/definitions/stringArray\" },\r\n"
+                       "        \"additionalProperties\": { \"$ref\": \"#\" },\r\n"
+                       "        \"definitions\": {\r\n"
+                       "            \"type\": \"object\",\r\n"
+                       "            \"additionalProperties\": { \"$ref\": \"#\" },\r\n"
+                       "            \"default\": {}\r\n"
+                       "        },\r\n"
+                       "        \"properties\": {\r\n"
+                       "            \"type\": \"object\",\r\n"
+                       "            \"additionalProperties\": { \"$ref\": \"#\" },\r\n"
+                       "            \"default\": {}\r\n"
+                       "        },\r\n"
+                       "        \"patternProperties\": {\r\n"
+                       "            \"type\": \"object\",\r\n"
+                       "            \"additionalProperties\": { \"$ref\": \"#\" },\r\n"
+                       "            \"default\": {}\r\n"
+                       "        },\r\n"
+                       "        \"dependencies\": {\r\n"
+                       "            \"type\": \"object\",\r\n"
+                       "            \"additionalProperties\": {\r\n"
+                       "                \"anyOf\": [\r\n"
+                       "                    { \"$ref\": \"#\" },\r\n"
+                       "                    { \"$ref\": \"#/definitions/stringArray\" }\r\n"
+                       "                ]\r\n"
+                       "            }\r\n"
+                       "        },\r\n"
+                       "        \"propertyNames\": { \"$ref\": \"#\" },\r\n"
+                       "        \"const\": {},\r\n"
+                       "        \"enum\": {\r\n"
+                       "            \"type\": \"array\",\r\n"
+                       "            \"minItems\": 1,\r\n"
+                       "            \"uniqueItems\": true\r\n"
+                       "        },\r\n"
+                       "        \"type\": {\r\n"
+                       "            \"anyOf\": [\r\n"
+                       "                { \"$ref\": \"#/definitions/simpleTypes\" },\r\n"
+                       "                {\r\n"
+                       "                    \"type\": \"array\",\r\n"
+                       "                    \"items\": { \"$ref\": \"#/definitions/simpleTypes\" },\r\n"
+                       "                    \"minItems\": 1,\r\n"
+                       "                    \"uniqueItems\": true\r\n"
+                       "                }\r\n"
+                       "            ]\r\n"
+                       "        },\r\n"
+                       "        \"format\": { \"type\": \"string\" },\r\n"
+                       "        \"allOf\": { \"$ref\": \"#/definitions/schemaArray\" },\r\n"
+                       "        \"anyOf\": { \"$ref\": \"#/definitions/schemaArray\" },\r\n"
+                       "        \"oneOf\": { \"$ref\": \"#/definitions/schemaArray\" },\r\n"
+                       "        \"not\": { \"$ref\": \"#\" }\r\n"
+                       "    },\r\n"
+                       "    \"default\": {}\r\n"
+                       "}\r\n",
+               .output =
+                       "{\"$schema\":\"http://json-schema.org/draft-06/schema#\","
+                       "\"$id\":\"http://json-schema.org/draft-06/schema#\","
+                       "\"title\":\"Core schema meta-schema\",\"definitions\":{"
+                       "\"schemaArray\":{\"type\":\"array\",\"minItems\":1,"
+                       "\"items\":{\"$ref\":\"#\"}},\"nonNegativeInteger\":{"
+                       "\"type\":\"integer\",\"minimum\":0},"
+                       "\"nonNegativeIntegerDefault0\":{\"allOf\":["
+                       "{\"$ref\":\"#/definitions/nonNegativeInteger\"},"
+                       "{\"default\":0}]},\"simpleTypes\":{\"enum\":["
+                       "\"array\",\"boolean\",\"integer\",\"null\","
+                       "\"number\",\"object\",\"string\"]},\"stringArray\":{"
+                       "\"type\":\"array\",\"items\":{\"type\":\"string\"},"
+                       "\"uniqueItems\":true,\"default\":[]}},"
+                       "\"type\":[\"object\",\"boolean\"],"
+                       "\"properties\":{\"$id\":{\"type\":\"string\","
+                       "\"format\":\"uri-reference\"},\"$schema\":{"
+                       "\"type\":\"string\",\"format\":\"uri\"},"
+                       "\"$ref\":{\"type\":\"string\",\"format\":\"uri-reference\""
+                       "},\"title\":{\"type\":\"string\"},\"description\":{"
+                       "\"type\":\"string\"},\"default\":{},\"multipleOf\":{"
+                       "\"type\":\"number\",\"exclusiveMinimum\":0},"
+                       "\"maximum\":{\"type\":\"number\"},\"exclusiveMaximum\":{"
+                       "\"type\":\"number\"},\"minimum\":{\"type\":\"number\""
+                       "},\"exclusiveMinimum\":{\"type\":\"number\"},"
+                       "\"maxLength\":{\"$ref\":\"#/definitions/nonNegativeInteger\"},"
+                       "\"minLength\":{\"$ref\":\"#/definitions/nonNegativeIntegerDefault0\"},"
+                       "\"pattern\":{\"type\":\"string\",\"format\":\"regex\""
+                       "},\"additionalItems\":{\"$ref\":\"#\"},\"items\":{"
+                       "\"anyOf\":[{\"$ref\":\"#\"},{\"$ref\":\"#/definitions/schemaArray\"}"
+                       "],\"default\":{}},"
+                       "\"maxItems\":{\"$ref\":\"#/definitions/nonNegativeInteger\"},"
+                       "\"minItems\":{\"$ref\":\"#/definitions/nonNegativeIntegerDefault0\"},"
+                       "\"uniqueItems\":{\"type\":\"boolean\",\"default\":false},"
+                       "\"contains\":{\"$ref\":\"#\"},"
+                       "\"maxProperties\":{\"$ref\":\"#/definitions/nonNegativeInteger\"},"
+                       "\"minProperties\":{\"$ref\":\"#/definitions/nonNegativeIntegerDefault0\"},"
+                       "\"required\":{\"$ref\":\"#/definitions/stringArray\"},"
+                       "\"additionalProperties\":{\"$ref\":\"#\"},\"definitions\":{"
+                       "\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#\"},"
+                       "\"default\":{}},\"properties\":{\"type\":\"object\","
+                       "\"additionalProperties\":{\"$ref\":\"#\"},\"default\":{}"
+                       "},\"patternProperties\":{\"type\":\"object\","
+                       "\"additionalProperties\":{\"$ref\":\"#\"},"
+                       "\"default\":{}},\"dependencies\":{\"type\":\"object\","
+                       "\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#\"},"
+                       "{\"$ref\":\"#/definitions/stringArray\"}"
+                       "]}},\"propertyNames\":{\"$ref\":\"#\"},\"const\":{},"
+                       "\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true"
+                       "},\"type\":{\"anyOf\":[{\"$ref\":\"#/definitions/simpleTypes\"},"
+                       "{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/simpleTypes\"},"
+                       "\"minItems\":1,\"uniqueItems\":true}]},\"format\":{\"type\":\"string\"},"
+                       "\"allOf\":{\"$ref\":\"#/definitions/schemaArray\"},"
+                       "\"anyOf\":{\"$ref\":\"#/definitions/schemaArray\"},"
+                       "\"oneOf\":{\"$ref\":\"#/definitions/schemaArray\"},"
+                       "\"not\":{\"$ref\":\"#\"}},\"default\":{}}"
+       },
+       /* escape sequences */
+       {
+               .input = "\"\\u0020\"",
+               .output = "\" \"",
+       },{
+               .input = "\"\\u0020\\u0020\"",
+               .output = "\"  \"",
+       },{
+               .input = "\"\\\"\"",
+               .output = "\"\\\"\"",
+       },{
+               .input = "\"\\\\\"",
+               .output = "\"\\\\\"",
+       },{
+               .input = "\"\\/\"",
+               .output = "\"/\"",
+       },{
+               .input = "\"\\b\"",
+               .output = "\"\\b\"",
+       },{
+               .input = "\"\\f\"",
+               .output = "\"\\f\"",
+       },{
+               .input = "\"\\n\"",
+               .output = "\"\\n\"",
+       },{
+               .input = "\"\\r\"",
+               .output = "\"\\r\"",
+       },{
+               .input = "\"\\t\"",
+               .output = "\"\\t\"",
+       },{
+               .input = "\"\\u0020\\\"\\\\\\/\\b\\f\\n\\r\\t\"",
+               .output = "\" \\\"\\\\/\\b\\f\\n\\r\\t\"",
+       },{
+               .input = "\"\\u00a2\"",
+               .output = "\"\xC2\xA2\""
+       },{
+               .input = "\"\\u20AC\"",
+               .output = "\"\xE2\x82\xAC\""
+       },{
+               .input = "\"\\uD808\\uDC00\"",
+               .output = "\"\xF0\x92\x80\x80\""
+       },{
+               .input = "\"\\u00a2\\u20AC\\uD808\\uDC00\"",
+               .output = "\"\xC2\xA2\xE2\x82\xAC\xF0\x92\x80\x80\""
+       },{
+               .input = "\"\\uD81A\\uDCD8\"",
+               .output = "\"\xF0\x96\xA3\x98\""
+       },{
+               .input = "\"\\uD836\\uDD49\"",
+               .output = "\"\xF0\x9D\xA5\x89\"",
+       },{
+               .input = "\"\xF0\x92\x80\x80\"",
+               .output = "\"\xF0\x92\x80\x80\""
+       },{
+               .input = "\"\xF0\x96\xA3\x98\"",
+               .output = "\"\xF0\x96\xA3\x98\""
+       },{
+               .input = "\"\xF0\x9D\xA5\x89\"",
+               .output = "\"\xF0\x9D\xA5\x89\"",
+       },{
+               .input = "\"\\\\xFF\\\\xFF\\\\xFF\"",
+               .output = "\"\\\\xFF\\\\xFF\\\\xFF\"",
+       },{
+               .input = "\"\xe2\x80\xa8\xe2\x80\xa9\"",
+               .output = "\"\\u2028\\u2029\"",
+       }
+};
+
+static const unsigned tests_count = N_ELEMENTS(tests);
+
+/*
+ * Low-level I/O
+ */
+
+struct test_io_context;
+
+enum test_io_state {
+       TEST_STATE_NONE = 0,
+       TEST_STATE_OBJECT_OPEN,
+       TEST_STATE_ARRAY_OPEN,
+       TEST_STATE_OBJECT_MEMBER,
+       TEST_STATE_VALUE,
+       TEST_STATE_OBJECT_CLOSE,
+       TEST_STATE_ARRAY_CLOSE,
+};
+
+struct test_io_processor {
+       struct test_io_context *tctx;
+       const char *name;
+
+       string_t *membuf, *strbuf;
+       struct ostream *output;
+       struct istream *input;
+       struct io *io;
+
+       enum test_io_state state;
+
+       enum json_type type;
+       struct json_value value;
+       struct json_data data;
+
+       struct json_generator *generator;
+       struct json_parser *parser;
+
+       unsigned int pos;
+};
+
+struct test_io_context {
+       const struct json_io_test *test;
+       unsigned int scenario;
+
+       struct iostream_pump *pump_in, *pump_out;
+};
+
+static void
+test_copy_value(struct test_io_processor *tproc, enum json_type type,
+               const struct json_value *value)
+{
+       tproc->type = type;
+       tproc->value = *value;
+       switch (value->content_type) {
+       case JSON_CONTENT_TYPE_STRING:
+               str_truncate(tproc->strbuf, 0);
+               str_append(tproc->strbuf, value->content.str);
+               tproc->value.content.str = str_c(tproc->strbuf);
+               break;
+       case JSON_CONTENT_TYPE_DATA:
+               tproc->data = *value->content.data;
+               tproc->value.content.data = &tproc->data;
+               str_truncate(tproc->strbuf, 0);
+               str_append_data(tproc->strbuf, tproc->data.data,
+                               tproc->data.size);
+               tproc->data.data = str_data(tproc->strbuf);
+               break;
+       default:
+               break;
+       }
+}
+
+static void
+test_parse_list_open(void *context, void *parent_context ATTR_UNUSED,
+                    const char *name, bool object,
+                    void **list_context_r ATTR_UNUSED)
+{
+       struct test_io_processor *tproc = context;
+       int ret;
+
+       if (object) {
+               tproc->state = TEST_STATE_OBJECT_OPEN;
+       } else {
+               tproc->state = TEST_STATE_ARRAY_OPEN;
+       }
+
+       if (name != NULL) {
+               ret = json_generate_object_member(tproc->generator, name);
+               if (ret <= 0) {
+                       str_truncate(tproc->membuf, 0);
+                       str_append(tproc->membuf, name);
+                       json_parser_interrupt(tproc->parser);
+                       return;
+               }
+       }
+
+       tproc->state = TEST_STATE_NONE;
+       if (object)
+               json_generate_object_open(tproc->generator);
+       else
+               json_generate_array_open(tproc->generator);
+}
+
+static void
+test_parse_list_close(void *context, void *list_context ATTR_UNUSED,
+                     bool object)
+{
+       struct test_io_processor *tproc = context;
+       int ret;
+
+       if (object) {
+               tproc->state = TEST_STATE_OBJECT_CLOSE;
+               ret = json_generate_object_close(tproc->generator);
+       } else {
+               tproc->state = TEST_STATE_ARRAY_CLOSE;
+               ret = json_generate_array_close(tproc->generator);
+       }
+       if (ret <= 0) {
+               json_parser_interrupt(tproc->parser);
+               return;
+       }
+
+       tproc->state = TEST_STATE_NONE;
+}
+
+static void
+test_parse_value(void *context, void *parent_context ATTR_UNUSED,
+                const char *name, enum json_type type,
+                const struct json_value *value)
+{
+       struct test_io_processor *tproc = context;
+       int ret;
+
+       tproc->state = TEST_STATE_OBJECT_MEMBER;
+
+       if (name != NULL) {
+               ret = json_generate_object_member(tproc->generator, name);
+               if (ret <= 0) {
+                       str_truncate(tproc->membuf, 0);
+                       str_append(tproc->membuf, name);
+                       json_parser_interrupt(tproc->parser);
+                       test_copy_value(tproc, type, value);
+                       return;
+               }
+       }
+
+       tproc->state = TEST_STATE_VALUE;
+
+       ret = json_generate_value(tproc->generator, type, value);
+       if (ret <= 0) {
+               if (ret == 0)
+                       test_copy_value(tproc, type, value);
+               json_parser_interrupt(tproc->parser);
+               return;
+       }
+
+       tproc->state = TEST_STATE_NONE;
+}
+
+static int test_write(struct test_io_processor *tproc)
+{
+       int ret;
+
+       switch (tproc->state) {
+       case TEST_STATE_NONE:
+               break;
+       case TEST_STATE_OBJECT_OPEN:
+               ret = json_generate_object_member(tproc->generator,
+                                                 str_c(tproc->membuf));
+               if (ret <= 0)
+                       return ret;
+               tproc->state = TEST_STATE_VALUE;
+               json_generate_object_open(tproc->generator);
+               break;
+       case TEST_STATE_ARRAY_OPEN:
+               ret = json_generate_object_member(tproc->generator,
+                                                 str_c(tproc->membuf));
+               if (ret <= 0)
+                       return ret;
+               tproc->state = TEST_STATE_VALUE;
+               json_generate_array_open(tproc->generator);
+               break;
+       case TEST_STATE_OBJECT_MEMBER:
+               ret = json_generate_object_member(tproc->generator,
+                                                 str_c(tproc->membuf));
+               if (ret <= 0)
+                       return ret;
+               tproc->state = TEST_STATE_VALUE;
+               /* fall through */
+       case TEST_STATE_VALUE:
+               ret = json_generate_value(tproc->generator,
+                                         tproc->type, &tproc->value);
+               if (ret <= 0)
+                       return ret;
+               break;
+       case TEST_STATE_OBJECT_CLOSE:
+               ret = json_generate_object_close(tproc->generator);
+               if (ret <= 0)
+                       return ret;
+               break;
+       case TEST_STATE_ARRAY_CLOSE:
+               ret = json_generate_array_close(tproc->generator);
+               if (ret <= 0)
+                       return ret;
+               break;
+       }
+
+       tproc->state = TEST_STATE_NONE;
+       return 1;
+}
+
+struct json_parser_callbacks parser_callbacks = {
+       .parse_list_open = test_parse_list_open,
+       .parse_list_close = test_parse_list_close,
+
+       .parse_value = test_parse_value
+};
+
+static void
+test_io_processor_init(struct test_io_processor *tproc,
+                      const struct json_io_test *test,
+                      struct istream *input, struct ostream *output)
+{
+       i_zero(tproc);
+       tproc->membuf = str_new(default_pool, 256);;
+       tproc->strbuf = str_new(default_pool, 256);
+
+       tproc->output = output;
+       o_stream_set_no_error_handling(tproc->output, TRUE);
+       tproc->input = input;
+
+       tproc->parser = json_parser_init(
+               tproc->input, &test->limits, test->flags,
+               &parser_callbacks, tproc);
+       tproc->generator = json_generator_init(tproc->output, 0);
+}
+
+static void test_io_processor_deinit(struct test_io_processor *tproc)
+{
+       json_generator_deinit(&tproc->generator);
+       json_parser_deinit(&tproc->parser);
+
+       buffer_free(&tproc->strbuf);
+       buffer_free(&tproc->membuf);
+}
+
+static void test_json_io(void)
+{
+       static const unsigned int margins[] = { 0, 1, 2, 10, 50 };
+       string_t *outbuf;
+       unsigned int i, j;
+
+       outbuf = str_new(default_pool, 256);
+
+       for (i = 0; i < tests_count; i++) T_BEGIN {
+               const struct json_io_test *test;
+               struct test_io_processor tproc;
+               const char *text, *text_out;
+               unsigned int pos, margin, text_len;
+
+               test = &tests[i];
+               text = test->input;
+               text_out = test->output;
+               if (text_out == NULL)
+                       text_out = test->input;
+               text_len = strlen(text);
+
+               test_begin(t_strdup_printf("json io [%d]", i));
+
+               for (j = 0; j < N_ELEMENTS(margins); j++) {
+                       struct istream *input;
+                       struct ostream *output;
+                       const char *error = NULL;
+                       int pret = 0, wret = 0;
+
+                       margin = margins[j];
+
+                       buffer_set_used_size(outbuf, 0);
+
+                       input = test_istream_create_data(text, text_len);
+                       output = o_stream_create_buffer(outbuf);
+                       test_io_processor_init(&tproc, test, input, output);
+
+                       o_stream_set_max_buffer_size(output, 0);
+                       pret = 0; wret = 1;
+                       for (pos = 0;
+                               pos <= (text_len+margin) &&
+                                       (pret == 0 || wret == 0);
+                               pos++) {
+                               test_istream_set_size(input, pos);
+                               o_stream_set_max_buffer_size(output,
+                                       (pos > margin ? pos - margin : 0));
+                               if (wret > 0 && pret == 0) {
+                                       pret = json_parse_more(tproc.parser,
+                                                              &error);
+                                       if (pret < 0)
+                                               break;
+                               }
+                               wret = test_write(&tproc);
+                               if (wret == 0)
+                                       continue;
+                               if (wret < 0)
+                                       break;
+                       }
+
+                       if (pret == 0)
+                               pret = json_parse_more(tproc.parser, &error);
+
+                       o_stream_set_max_buffer_size(output, SIZE_MAX);
+                       wret = json_generator_flush(tproc.generator);
+
+                       test_out_reason_quiet(
+                               t_strdup_printf("parse success "
+                                               "(trickle, margin=%u)", margin),
+                               pret > 0, error);
+                       test_out_quiet(
+                               t_strdup_printf("write success "
+                                               "(trickle, margin=%u)", margin),
+                               wret > 0);
+                       test_out_quiet(
+                               t_strdup_printf("io match (trickle, margin=%u)",
+                                               margin),
+                               strcmp(text_out, str_c(outbuf)) == 0);
+                       if (debug) {
+                               i_debug("OUT: >%s<", text_out);
+                               i_debug("OUT: >%s<", str_c(outbuf));
+                       }
+
+                       test_io_processor_deinit(&tproc);
+                       i_stream_destroy(&input);
+                       o_stream_destroy(&output);
+               }
+
+               test_end();
+
+       } T_END;
+
+       buffer_free(&outbuf);
+}
+
+static void test_json_async_io_input_callback(struct test_io_processor *tproc)
+{
+       const char *error;
+       int ret;
+
+       ret = json_parse_more(tproc->parser, &error);
+       if (ret == 0) {
+               ret = test_write(tproc);
+               if (ret == 0) {
+                       o_stream_set_flush_pending(tproc->output, TRUE);
+                       io_remove(&tproc->io);
+                       return;
+               }
+               if (ret < 0) {
+                       test_assert(FALSE);
+                       io_loop_stop(current_ioloop);
+               }
+               return;
+       }
+
+       test_out_reason_quiet(
+               t_strdup_printf("%u: %s: parse success (async)",
+                               tproc->tctx->scenario, tproc->name),
+               ret > 0, error);
+       if (ret < 0) {
+               io_loop_stop(current_ioloop);
+       } else {
+               ret = test_write(tproc);
+               if (ret > 0)
+                       ret = json_generator_flush(tproc->generator);
+               if (ret == 0) {
+                       o_stream_set_flush_pending(tproc->output, TRUE);
+                       io_remove(&tproc->io);
+                       return;
+               }
+               test_out_quiet(t_strdup_printf("%u: %s: write success (async)",
+                                              tproc->tctx->scenario,
+                                              tproc->name), ret > 0);
+               if (ret < 0) {
+                       io_loop_stop(current_ioloop);
+                       return;
+               }
+
+               io_remove(&tproc->io);
+               o_stream_close(tproc->output);
+       }
+}
+
+static int test_json_async_io_flush_callback(struct test_io_processor *tproc)
+{
+       int ret;
+
+       ret = json_generator_flush(tproc->generator);
+       if (ret == 0)
+               return ret;
+       if (ret < 0) {
+               test_assert(FALSE);
+               io_loop_stop(current_ioloop);
+               return -1;
+       }
+
+       ret = test_write(tproc);
+       if (ret == 0)
+               return 0;
+       if (ret < 0) {
+               test_assert(FALSE);
+               io_loop_stop(current_ioloop);
+               return -1;
+       }
+
+       if (tproc->io == NULL) {
+               tproc->io = io_add_istream(
+                       tproc->input, test_json_async_io_input_callback, tproc);
+               i_stream_set_input_pending(tproc->input, TRUE);
+       }
+       return 1;
+}
+
+static void
+test_json_async_io_pump_in_callback(enum iostream_pump_status status,
+                                   struct test_io_context *tctx)
+{
+       if (status != IOSTREAM_PUMP_STATUS_INPUT_EOF) {
+               test_assert(FALSE);
+               io_loop_stop(current_ioloop);
+               return;
+       }
+
+       struct ostream *output = iostream_pump_get_output(tctx->pump_in);
+
+       o_stream_close(output);
+       iostream_pump_destroy(&tctx->pump_in);
+}
+
+static void
+test_json_async_io_pump_out_callback(enum iostream_pump_status status,
+                                    struct test_io_context *tctx)
+{
+       if (status != IOSTREAM_PUMP_STATUS_INPUT_EOF)
+               test_assert(FALSE);
+
+       io_loop_stop(current_ioloop);
+       iostream_pump_destroy(&tctx->pump_out);
+}
+
+static void
+test_json_async_io_run(const struct json_io_test *test, unsigned int scenario)
+{
+       struct test_io_context tctx;
+       string_t *outbuf;
+       struct test_io_processor tproc1, tproc2;
+       struct ioloop *ioloop;
+       int fd_pipe1[2], fd_pipe2[2], fd_pipe3[2];
+       const char *text, *text_out;
+       unsigned int text_len;
+       struct istream *input, *pipe1_input, *pipe2_input, *pipe3_input;
+       struct ostream *output, *pipe1_output, *pipe2_output, *pipe3_output;
+
+       i_zero(&tctx);
+       tctx.test = test;
+       tctx.scenario = scenario;
+
+       text = test->input;
+       text_out = test->output;
+       if (text_out == NULL)
+               text_out = test->input;
+       text_len = strlen(text);
+
+       outbuf = str_new(default_pool, 256);
+
+       if (pipe(fd_pipe1) < 0)
+               i_fatal("pipe() failed: %m");
+       if (pipe(fd_pipe2) < 0)
+               i_fatal("pipe() failed: %m");
+       if (pipe(fd_pipe3) < 0)
+               i_fatal("pipe() failed: %m");
+       fd_set_nonblock(fd_pipe1[0], TRUE);
+       fd_set_nonblock(fd_pipe1[1], TRUE);
+       fd_set_nonblock(fd_pipe2[0], TRUE);
+       fd_set_nonblock(fd_pipe2[1], TRUE);
+       fd_set_nonblock(fd_pipe3[0], TRUE);
+       fd_set_nonblock(fd_pipe3[1], TRUE);
+
+       ioloop = io_loop_create();
+
+       input = i_stream_create_from_data(text, text_len);
+       output = o_stream_create_buffer(outbuf);
+
+       switch (scenario) {
+       case 0: case 2:
+               pipe1_input = i_stream_create_fd_autoclose(&fd_pipe1[0], 16);
+               pipe2_input = i_stream_create_fd_autoclose(&fd_pipe2[0], 32);
+               pipe3_input = i_stream_create_fd_autoclose(&fd_pipe3[0], 64);
+               break;
+       case 1: case 3:
+               pipe1_input = i_stream_create_fd_autoclose(&fd_pipe1[0], 128);
+               pipe2_input = i_stream_create_fd_autoclose(&fd_pipe2[0], 64);
+               pipe3_input = i_stream_create_fd_autoclose(&fd_pipe3[0], 32);
+               break;
+       default:
+               i_unreached();
+       }
+
+       switch (scenario) {
+       case 0: case 1:
+               pipe1_output = o_stream_create_fd_autoclose(&fd_pipe1[1], 32);
+               pipe2_output = o_stream_create_fd_autoclose(&fd_pipe2[1], 64);
+               pipe3_output = o_stream_create_fd_autoclose(&fd_pipe3[1], 128);
+               break;
+       case 2: case 3:
+               pipe1_output = o_stream_create_fd_autoclose(&fd_pipe1[1], 64);
+               pipe2_output = o_stream_create_fd_autoclose(&fd_pipe2[1], 32);
+               pipe3_output = o_stream_create_fd_autoclose(&fd_pipe3[1], 16);
+               break;
+       default:
+               i_unreached();
+       }
+
+       tctx.pump_in = iostream_pump_create(input, pipe1_output);
+       tctx.pump_out = iostream_pump_create(pipe3_input, output);
+
+       iostream_pump_set_completion_callback(
+               tctx.pump_in, test_json_async_io_pump_in_callback, &tctx);
+       iostream_pump_set_completion_callback(
+               tctx.pump_out, test_json_async_io_pump_out_callback, &tctx);
+
+       /* Processor 1 */
+       test_io_processor_init(&tproc1, test, pipe1_input, pipe2_output);
+       tproc1.tctx = &tctx;
+       tproc1.name = "proc_a";
+       o_stream_uncork(tproc1.output);
+
+       o_stream_set_flush_callback(tproc1.output,
+                                   test_json_async_io_flush_callback, &tproc1);
+       tproc1.io = io_add_istream(tproc1.input,
+                                 test_json_async_io_input_callback, &tproc1);
+
+       /* Processor 2 */
+       test_io_processor_init(&tproc2, test, pipe2_input, pipe3_output);
+       tproc2.tctx = &tctx;
+       tproc2.name = "proc_b";
+       o_stream_uncork(tproc2.output);
+
+       o_stream_set_flush_callback(tproc2.output,
+                                   test_json_async_io_flush_callback, &tproc2);
+       tproc2.io = io_add_istream(tproc2.input,
+                                 test_json_async_io_input_callback, &tproc2);
+
+       struct timeout *to = timeout_add(5000, io_loop_stop, ioloop);
+
+       iostream_pump_start(tctx.pump_in);
+       iostream_pump_start(tctx.pump_out);
+
+       io_loop_run(ioloop);
+
+       timeout_remove(&to);
+
+       test_io_processor_deinit(&tproc1);
+       test_io_processor_deinit(&tproc2);
+
+       iostream_pump_destroy(&tctx.pump_in);
+       iostream_pump_destroy(&tctx.pump_out);
+
+       i_stream_destroy(&input);
+       i_stream_destroy(&pipe1_input);
+       i_stream_destroy(&pipe2_input);
+       i_stream_destroy(&pipe3_input);
+
+       o_stream_destroy(&output);
+       o_stream_destroy(&pipe1_output);
+       o_stream_destroy(&pipe2_output);
+       o_stream_destroy(&pipe3_output);
+
+       io_loop_destroy(&ioloop);
+
+       test_out_quiet(t_strdup_printf("%u: io match (async)", scenario),
+                      strcmp(text_out, str_c(outbuf)) == 0);
+
+       buffer_free(&outbuf);
+}
+
+static void test_json_io_async(void)
+{
+       unsigned int i, sc;
+
+       for (i = 0; i < tests_count; i++) T_BEGIN {
+               test_begin(t_strdup_printf("json io async [%d]", i));
+
+               for (sc = 0; sc < 4; sc++)
+                       test_json_async_io_run(&tests[i], sc);
+
+               test_end();
+       } T_END;
+}
+
+int main(int argc, char *argv[])
+{
+       int ret, c;
+
+       random_init();
+
+       static void (*test_functions[])(void) = {
+               test_json_io,
+               test_json_io_async,
+               NULL
+       };
+
+       while ((c = getopt(argc, argv, "D")) > 0) {
+               switch (c) {
+               case 'D':
+                       debug = TRUE;
+                       break;
+               default:
+                       i_fatal("Usage: %s [-D]", argv[0]);
+               }
+       }
+       argc -= optind;
+       argv += optind;
+
+       if (argc > 0)
+               i_fatal("Usage: %s [-D]", argv[0]);
+
+       ret = test_run(test_functions);
+
+       random_deinit();
+
+       return ret;
+}