]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-json: Implement JSON value output stream
authorStephan Bosch <stephan.bosch@open-xchange.com>
Wed, 7 Aug 2019 19:24:50 +0000 (21:24 +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-ostream.c [new file with mode: 0644]
src/lib-json/json-ostream.h [new file with mode: 0644]
src/lib-json/test-json-io.c
src/lib-json/test-json-ostream.c [new file with mode: 0644]

index 24934ae3c7ba0ea0f0912cae82c1fff62bf8436b..f67e94f9bb816742eddb0b35ccabf31e7bd986b0 100644 (file)
@@ -9,7 +9,8 @@ libjson_la_SOURCES = \
        json-types.c \
        json-parser.new.c \
        json-generator.c \
-       json-istream.c
+       json-istream.c \
+       json-ostream.c
 libjson_la_LIBADD = -lm
 
 headers = \
@@ -17,13 +18,15 @@ headers = \
        json-types.h \
        json-parser.new.h \
        json-generator.h \
-       json-istream.h
+       json-istream.h \
+       json-ostream.h
 
 test_programs = \
        test-json-parser \
        test-json-generator \
        test-json-io \
-       test-json-istream
+       test-json-istream \
+       test-json-ostream
 
 noinst_PROGRAMS = $(test_programs)
 
@@ -67,6 +70,13 @@ test_json_istream_LDADD = \
 test_json_istream_DEPENDENCIES = \
        $(test_deps)
 
+test_json_ostream_SOURCE = \
+       test-json-ostream.c
+test_json_ostream_LDADD = \
+       $(test_libs)
+test_json_ostream_DEPENDENCIES = \
+       $(test_deps)
+
 pkginc_libdir=$(pkgincludedir)
 pkginc_lib_HEADERS = $(headers)
 
diff --git a/src/lib-json/json-ostream.c b/src/lib-json/json-ostream.c
new file mode 100644 (file)
index 0000000..89c8193
--- /dev/null
@@ -0,0 +1,1073 @@
+/* Copyright (c) 2017-2023 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "array.h"
+#include "ostream.h"
+
+#include "json-ostream.h"
+
+enum json_ostream_node_state {
+       JSON_OSTREAM_NODE_STATE_NONE = 0,
+       JSON_OSTREAM_NODE_STATE_VALUE,
+       JSON_OSTREAM_NODE_STATE_ARRAY_CLOSE,
+       JSON_OSTREAM_NODE_STATE_OBJECT_CLOSE,
+};
+
+struct json_ostream {
+       int refcount;
+
+       struct ostream *output;
+       struct json_generator *generator;
+
+       struct json_node node;
+       enum json_ostream_node_state node_state;
+       unsigned int write_node_level;
+
+       struct json_data node_data;
+       string_t *buffer;
+
+       char *error;
+
+       bool member_name_written:1;
+       bool value_opened:1;
+       bool value_persists:1;
+       bool string_opened:1;
+       bool last_errors_not_checked:1;
+       bool error_handling_disabled:1;
+       bool nfailed:1;
+       bool closed:1;
+};
+
+static int json_ostream_write_node_more(struct json_ostream *stream);
+
+struct json_ostream *
+json_ostream_create(struct ostream *output,
+                   enum json_generator_flags gen_flags)
+{
+       struct json_ostream *stream;
+
+       stream = i_new(struct json_ostream, 1);
+       stream->refcount = 1;
+
+       stream->output = output;
+       o_stream_ref(output);
+
+       stream->generator = json_generator_init(output, gen_flags);
+
+       return stream;
+}
+
+struct json_ostream *
+json_ostream_create_str(string_t *buf, enum json_generator_flags gen_flags)
+{
+       struct json_ostream *stream;
+
+       stream = i_new(struct json_ostream, 1);
+       stream->refcount = 1;
+
+       stream->generator = json_generator_init_str(buf, gen_flags);
+
+       return stream;
+}
+
+void json_ostream_ref(struct json_ostream *stream)
+{
+       i_assert(stream->refcount > 0);
+       stream->refcount++;
+}
+
+void json_ostream_unref(struct json_ostream **_stream)
+{
+       struct json_ostream *stream = *_stream;
+
+       if (stream == NULL)
+               return;
+       *_stream = NULL;
+
+       i_assert(stream->refcount > 0);
+       if (--stream->refcount != 0)
+               return;
+
+       if (stream->output != NULL && stream->last_errors_not_checked &&
+           !stream->error_handling_disabled) {
+               i_panic("JSON output stream %s is missing error handling",
+                       o_stream_get_name(stream->output));
+       }
+
+       json_generator_deinit(&stream->generator);
+       o_stream_unref(&stream->output);
+       str_free(&stream->buffer);
+
+       i_free(stream->error);
+       i_free(stream);
+}
+
+void json_ostream_destroy(struct json_ostream **_stream)
+{
+       struct json_ostream *stream = *_stream;
+
+       if (stream == NULL)
+               return;
+
+       json_ostream_close(stream);
+       json_ostream_unref(_stream);
+}
+
+unsigned int json_ostream_get_write_node_level(struct json_ostream *stream)
+{
+       return stream->write_node_level;
+}
+
+static void ATTR_FORMAT(2, 3)
+json_ostream_set_error(struct json_ostream *stream, const char *fmt, ...)
+{
+       va_list args;
+
+       va_start(args, fmt);
+       i_free(stream->error);
+       stream->error = i_strdup_vprintf(fmt, args);
+       va_end(args);
+}
+
+const char *json_ostream_get_error(struct json_ostream *stream)
+{
+       if (stream->error != NULL)
+               return stream->error;
+       if (stream->closed)
+               return "<closed>";
+       if (stream->output != NULL)
+               return o_stream_get_error(stream->output);
+       return "<no error>";
+}
+
+void json_ostream_close(struct json_ostream *stream)
+{
+       stream->closed = TRUE;
+}
+
+bool json_ostream_is_closed(struct json_ostream *stream)
+{
+       return stream->closed;
+}
+
+void json_ostream_cork(struct json_ostream *stream)
+{
+       if (stream->output != NULL)
+               o_stream_cork(stream->output);
+}
+
+void json_ostream_uncork(struct json_ostream *stream)
+{
+       if (stream->output != NULL)
+               o_stream_uncork(stream->output);
+}
+
+bool json_ostream_is_corked(struct json_ostream *stream)
+{
+       return (stream->output != NULL && o_stream_is_corked(stream->output));
+}
+
+int json_ostream_nfinish(struct json_ostream *stream)
+{
+       if (stream->closed)
+               return -1;
+       if (stream->error != NULL)
+               return -1;
+       json_ostream_nflush(stream);
+       if (stream->output == NULL)
+               return 0;
+       json_ostream_ignore_last_errors(stream);
+       if (stream->output->stream_errno == 0 && stream->nfailed) {
+               json_ostream_set_error(stream,
+                       "Output stream buffer was full (%zu bytes)",
+                       o_stream_get_max_buffer_size(stream->output));
+               return -1;
+       }
+       return (stream->output->stream_errno != 0 ? -1 : 0);
+}
+
+void json_ostream_nfinish_destroy(struct json_ostream **_stream)
+{
+       struct json_ostream *stream = *_stream;
+       int ret;
+
+       if (stream == NULL)
+               return;
+
+       ret = json_ostream_nfinish(stream);
+       i_assert(ret >= 0);
+       json_ostream_destroy(_stream);
+}
+
+void json_ostream_ignore_last_errors(struct json_ostream *stream)
+{
+       stream->last_errors_not_checked = FALSE;
+}
+
+void json_ostream_set_no_error_handling(struct json_ostream *stream, bool set)
+{
+       stream->error_handling_disabled = set;
+}
+
+/*
+ * Partial data
+ */
+
+static void json_ostream_persist_value(struct json_ostream *stream)
+{
+       if (!json_node_is_singular(&stream->node))
+               return;
+       if (stream->value_persists)
+               return;
+       stream->value_persists = TRUE;
+
+       switch (stream->node.value.content_type) {
+       case JSON_CONTENT_TYPE_STRING:
+               i_unreached();
+       case JSON_CONTENT_TYPE_DATA:
+               if (stream->buffer == NULL)
+                       stream->buffer = str_new(default_pool, 128);
+               stream->node_data = *stream->node.value.content.data;
+               str_truncate(stream->buffer, 0);
+               str_append_data(stream->buffer,
+                               stream->node_data.data, stream->node_data.size);
+               stream->node_data.data = str_data(stream->buffer);
+               stream->node.value.content.data = &stream->node_data;
+               break;
+       default:
+               break;
+       }
+}
+
+static int json_ostream_do_write_more(struct json_ostream *stream)
+{
+       struct json_data *jdata;
+       ssize_t sret;
+       int ret;
+
+       if (stream->closed)
+               return -1;
+
+       switch (stream->node.value.content_type) {
+       case JSON_CONTENT_TYPE_STRING:
+               i_zero(&stream->node_data);
+               stream->node_data.data = (const unsigned char *)
+                       stream->node.value.content.str;
+               stream->node_data.size = strlen(stream->node.value.content.str);
+               stream->node.value.content.data = &stream->node_data;
+               stream->node.value.content_type =
+                       JSON_CONTENT_TYPE_DATA;
+               /* fall through */
+       case JSON_CONTENT_TYPE_DATA:
+               jdata = stream->node.value.content.data;
+               switch (stream->node.type) {
+               /* string */
+               case JSON_TYPE_STRING:
+                       if (stream->string_opened)
+                               stream->value_opened = TRUE;
+                       if (!stream->value_opened) {
+                               json_generate_string_open(stream->generator);
+                               stream->value_opened = TRUE;
+                       }
+                       while (jdata->size > 0) {
+                               sret = json_generate_string_more(
+                                       stream->generator, jdata->data,
+                                       jdata->size, TRUE);
+                               if (sret <= 0)
+                                       return (int)sret;
+                               i_assert((size_t)sret <= jdata->size);
+                               jdata->data += sret;
+                               jdata->size -= (size_t)sret;
+                       }
+                       if (!stream->string_opened) {
+                               ret = json_generate_string_write_close(
+                                       stream->generator);
+                               if (ret <= 0)
+                                       return ret;
+                       }
+                       stream->value_opened = FALSE;
+                       return 1;
+               /* number, JSON-text */
+               case JSON_TYPE_NUMBER:
+               case JSON_TYPE_TEXT:
+                       i_assert(!stream->string_opened);
+                       if (!stream->value_opened) {
+                               json_generate_text_open(stream->generator);
+                               stream->value_opened = TRUE;
+                       }
+                       while (jdata->size > 0) {
+                               sret = json_generate_text_more(
+                                       stream->generator, jdata->data,
+                                       jdata->size);
+                               if (sret <= 0)
+                                       return (int)sret;
+                               i_assert((size_t)sret <= jdata->size);
+                               jdata->data += sret;
+                               jdata->size -= (size_t)sret;
+                       }
+                       ret = json_generate_text_close(stream->generator);
+                       if (ret <= 0)
+                               return ret;
+                       stream->value_opened = FALSE;
+                       return 1;
+               default:
+                       i_unreached();
+               }
+               break;
+       default:
+               break;
+       }
+
+       return json_generate_value(stream->generator, stream->node.type,
+                                  &stream->node.value);
+}
+
+static int json_ostream_write_more(struct json_ostream *stream)
+{
+       int ret;
+
+       ret = json_ostream_do_write_more(stream);
+       if (ret <= 0) {
+               i_assert(stream->output != NULL);
+               return ret;
+       }
+       i_zero(&stream->node);
+       stream->value_persists = FALSE;
+       stream->value_opened = FALSE;
+       return 1;
+}
+
+int json_ostream_flush(struct json_ostream *stream)
+{
+       int ret;
+
+       if (stream->closed)
+               return -1;
+       if (stream->node_state != JSON_OSTREAM_NODE_STATE_NONE) {
+               ret = json_ostream_write_node_more(stream);
+               if (ret <= 0)
+                       return ret;
+       }
+       if (json_node_is_none(&stream->node))
+               return json_generator_flush(stream->generator);
+       ret = json_ostream_write_more(stream);
+       if (ret <= 0)
+               return ret;
+       return 1;
+}
+
+void json_ostream_nflush(struct json_ostream *stream)
+{
+       if (unlikely(stream->closed))
+               return;
+       if (unlikely(stream->nfailed)) {
+               i_assert(stream->output != NULL);
+               return;
+       }
+       if (stream->output != NULL) {
+               if (unlikely(stream->output->closed ||
+                            stream->output->stream_errno != 0))
+                       return;
+       }
+       if (json_ostream_flush(stream) <= 0) {
+               i_assert(stream->output != NULL);
+               stream->nfailed = TRUE;
+       }
+       stream->last_errors_not_checked = TRUE;
+}
+
+static int
+json_ostream_write_init(struct json_ostream *stream, const char *name,
+                       enum json_type type)
+{
+       int ret;
+
+       i_assert(name == NULL || !stream->string_opened);
+       i_assert(!stream->string_opened || type == JSON_TYPE_STRING);
+
+       ret = json_ostream_flush(stream);
+       if (ret <= 0)
+               return ret;
+
+       if (stream->string_opened)
+               return 1;
+
+       if (name != NULL) {
+               i_assert(!stream->member_name_written);
+               ret = json_generate_object_member(stream->generator, name);
+               if (ret <= 0)
+                       return ret;
+       }
+       stream->member_name_written = FALSE;
+       return 1;
+}
+
+/*
+ * Values
+ */
+
+static inline bool json_ostream_nwrite_pre(struct json_ostream *stream)
+{
+       if (unlikely(stream->closed))
+               return FALSE;
+       if (unlikely(stream->nfailed)) {
+               i_assert(stream->output != NULL);
+               return FALSE;
+       }
+       if (stream->output != NULL) {
+               if (unlikely(stream->output->closed ||
+                            stream->output->stream_errno != 0))
+                       return FALSE;
+       }
+       return TRUE;
+}
+
+static inline void
+json_ostream_nwrite_post(struct json_ostream *stream, int ret)
+{
+       if (ret <= 0) {
+               i_assert(stream->output != NULL);
+               stream->nfailed = TRUE;
+       }
+       stream->last_errors_not_checked = TRUE;
+}
+
+/* object member */
+
+int json_ostream_write_object_member(struct json_ostream *stream,
+                                    const char *name)
+{
+       int ret;
+
+       ret = json_ostream_flush(stream);
+       if (ret <= 0)
+               return ret;
+
+       i_assert(!stream->member_name_written);
+       ret = json_generate_object_member(stream->generator, name);
+       if (ret <= 0)
+               return ret;
+       stream->member_name_written = TRUE;
+       return 1;
+}
+
+void json_ostream_nwrite_object_member(struct json_ostream *stream,
+                                      const char *name)
+{
+       int ret;
+
+       if (!json_ostream_nwrite_pre(stream))
+               return;
+       ret = json_ostream_write_object_member(stream, name);
+       json_ostream_nwrite_post(stream, ret);
+}
+
+/* value */
+
+static int
+json_ostream_do_write_value(struct json_ostream *stream,
+                           const char *name, enum json_type type,
+                           const struct json_value *value, bool persist)
+{
+       int ret;
+
+       ret = json_ostream_write_init(stream, name, type);
+       if (ret <= 0)
+               return ret;
+
+       i_zero(&stream->node);
+       stream->node.type = type;
+       stream->node.value = *value;
+
+       ret = json_ostream_write_more(stream);
+       if (ret < 0)
+               return ret;
+       if (ret == 0 && persist)
+               json_ostream_persist_value(stream);
+       return 1;
+}
+
+int json_ostream_write_value(struct json_ostream *stream,
+                            const char *name, enum json_type type,
+                            const struct json_value *value)
+{
+       return json_ostream_do_write_value(stream, name, type, value, TRUE);
+}
+
+void json_ostream_nwrite_value(struct json_ostream *stream,
+                              const char *name, enum json_type type,
+                              const struct json_value *value)
+{
+       int ret;
+
+       if (!json_ostream_nwrite_pre(stream))
+               return;
+       ret = json_ostream_do_write_value(stream, name, type, value, FALSE);
+       json_ostream_nwrite_post(stream, ret);
+}
+
+/* number */
+
+int json_ostream_write_number(struct json_ostream *stream,
+                             const char *name, intmax_t number)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_INTEGER;
+       jvalue.content.intnum = number;
+
+       return json_ostream_write_value(stream, name,
+                                       JSON_TYPE_NUMBER, &jvalue);
+}
+
+void json_ostream_nwrite_number(struct json_ostream *stream,
+                               const char *name, intmax_t number)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_INTEGER;
+       jvalue.content.intnum = number;
+
+       json_ostream_nwrite_value(stream, name,
+                                 JSON_TYPE_NUMBER, &jvalue);
+}
+
+int json_ostream_write_number_raw(struct json_ostream *stream,
+                                 const char *name, const char *number)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_STRING;
+       jvalue.content.str = number;
+
+       return json_ostream_write_value(stream, name,
+                                       JSON_TYPE_NUMBER, &jvalue);
+}
+
+void json_ostream_nwrite_number_raw(struct json_ostream *stream,
+                                   const char *name, const char *number)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_STRING;
+       jvalue.content.str = number;
+
+       json_ostream_nwrite_value(stream, name, JSON_TYPE_NUMBER, &jvalue);
+}
+
+/* string */
+
+int json_ostream_write_string_data(struct json_ostream *stream,
+                                  const char *name,
+                                  const void *data, size_t size)
+{
+       struct json_value jvalue;
+       struct json_data jdata;
+
+       i_zero(&jdata);
+       jdata.data = data;
+       jdata.size = size;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_DATA;
+       jvalue.content.data = &jdata;
+
+       return json_ostream_write_value(stream, name, JSON_TYPE_STRING,
+                                       &jvalue);
+}
+
+void json_ostream_nwrite_string_data(struct json_ostream *stream,
+                                    const char *name,
+                                    const void *data, size_t size)
+{
+       struct json_value jvalue;
+       struct json_data jdata;
+
+       i_zero(&jdata);
+       jdata.data = data;
+       jdata.size = size;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_DATA;
+       jvalue.content.data = &jdata;
+
+       json_ostream_nwrite_value(stream, name, JSON_TYPE_STRING, &jvalue);
+}
+
+int json_ostream_write_string(struct json_ostream *stream,
+                             const char *name, const char *str)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_STRING;
+       jvalue.content.str = str;
+
+       return json_ostream_write_value(stream, name, JSON_TYPE_STRING,
+                                        &jvalue);
+}
+
+void json_ostream_nwrite_string(struct json_ostream *stream,
+                               const char *name, const char *str)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_STRING;
+       jvalue.content.str = str;
+
+       json_ostream_nwrite_value(stream, name, JSON_TYPE_STRING, &jvalue);
+}
+
+void json_ostream_nwritef_string(struct json_ostream *stream,
+                                const char *name, const char *format, ...)
+{
+       va_list args;
+
+       va_start(args, format);
+       json_ostream_nwrite_string(stream, name,
+                                  t_strdup_vprintf(format, args));
+       va_end(args);
+}
+
+int json_ostream_open_string(struct json_ostream *stream, const char *name)
+{
+       int ret;
+
+       ret = json_ostream_write_init(stream, name, JSON_TYPE_STRING);
+       if (ret <= 0)
+               return ret;
+
+       i_zero(&stream->node);
+       json_generate_string_open(stream->generator);
+       stream->string_opened = TRUE;
+       return 1;
+}
+
+void json_ostream_nopen_string(struct json_ostream *stream, const char *name)
+{
+       int ret;
+
+       if (!json_ostream_nwrite_pre(stream))
+               return;
+       ret = json_ostream_open_string(stream, name);
+       json_ostream_nwrite_post(stream, ret);
+}
+
+int json_ostream_close_string(struct json_ostream *stream)
+{
+       int ret;
+
+       i_assert(stream->string_opened);
+
+       ret = json_ostream_flush(stream);
+       if (ret <= 0)
+               return ret;
+       i_zero(&stream->node);
+       ret = json_generate_string_write_close(stream->generator);
+       if (ret <= 0)
+               return ret;
+       stream->string_opened = FALSE;
+       return 1;
+}
+
+void json_ostream_nclose_string(struct json_ostream *stream)
+{
+       int ret;
+
+       if (!json_ostream_nwrite_pre(stream))
+               return;
+       ret = json_ostream_close_string(stream);
+       json_ostream_nwrite_post(stream, ret);
+}
+
+/* null */
+
+int json_ostream_write_null(struct json_ostream *stream, const char *name)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       return json_ostream_write_value(stream, name, JSON_TYPE_NULL, &jvalue);
+}
+
+void json_ostream_nwrite_null(struct json_ostream *stream, const char *name)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       json_ostream_nwrite_value(stream, name, JSON_TYPE_NULL, &jvalue);
+}
+
+
+/* false, true */
+
+int json_ostream_write_false(struct json_ostream *stream, const char *name)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       return json_ostream_write_value(stream, name, JSON_TYPE_FALSE,
+                                       &jvalue);
+}
+
+void json_ostream_nwrite_false(struct json_ostream *stream, const char *name)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       json_ostream_nwrite_value(stream, name, JSON_TYPE_FALSE, &jvalue);
+}
+
+int json_ostream_write_true(struct json_ostream *stream, const char *name)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       return json_ostream_write_value(stream, name, JSON_TYPE_TRUE, &jvalue);
+}
+
+void json_ostream_nwrite_true(struct json_ostream *stream, const char *name)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       json_ostream_nwrite_value(stream, name, JSON_TYPE_TRUE, &jvalue);
+}
+
+int json_ostream_write_bool(struct json_ostream *stream, const char *name,
+                           bool value)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       return json_ostream_write_value(
+               stream, name, (value ? JSON_TYPE_TRUE : JSON_TYPE_FALSE),
+               &jvalue);
+}
+
+void json_ostream_nwrite_bool(struct json_ostream *stream, const char *name,
+                             bool value)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       json_ostream_nwrite_value(
+               stream, name, (value ? JSON_TYPE_TRUE : JSON_TYPE_FALSE),
+               &jvalue);
+}
+
+/* object */
+
+int json_ostream_descend_object(struct json_ostream *stream, const char *name)
+{
+       int ret;
+
+       ret = json_ostream_write_init(stream, name, JSON_TYPE_OBJECT);
+       if (ret <= 0)
+               return ret;
+
+       i_zero(&stream->node);
+       json_generate_object_open(stream->generator);
+       stream->write_node_level++;
+       return 1;
+}
+
+void json_ostream_ndescend_object(struct json_ostream *stream,
+                                 const char *name)
+{
+       int ret;
+
+       if (!json_ostream_nwrite_pre(stream))
+               return;
+       ret = json_ostream_descend_object(stream, name);
+       json_ostream_nwrite_post(stream, ret);
+}
+
+int json_ostream_ascend_object(struct json_ostream *stream)
+{
+       int ret;
+
+       ret = json_ostream_flush(stream);
+       if (ret <= 0)
+               return ret;
+       ret = json_generate_object_close(stream->generator);
+       if (ret <= 0)
+               return ret;
+       i_assert(stream->write_node_level > 0);
+       stream->write_node_level--;
+       return 1;
+}
+
+void json_ostream_nascend_object(struct json_ostream *stream)
+{
+       int ret;
+
+       if (!json_ostream_nwrite_pre(stream))
+               return;
+       ret = json_ostream_ascend_object(stream);
+       json_ostream_nwrite_post(stream, ret);
+}
+
+/* array */
+
+int json_ostream_descend_array(struct json_ostream *stream, const char *name)
+{
+       int ret;
+
+       ret = json_ostream_write_init(stream, name, JSON_TYPE_ARRAY);
+       if (ret <= 0)
+               return ret;
+
+       i_zero(&stream->node);
+       json_generate_array_open(stream->generator);
+       stream->write_node_level++;
+       return 1;
+}
+
+void json_ostream_ndescend_array(struct json_ostream *stream,
+                                const char *name)
+{
+       int ret;
+
+       if (!json_ostream_nwrite_pre(stream))
+               return;
+       ret = json_ostream_descend_array(stream, name);
+       json_ostream_nwrite_post(stream, ret);
+}
+
+int json_ostream_ascend_array(struct json_ostream *stream)
+{
+       int ret;
+
+       ret = json_ostream_flush(stream);
+       if (ret <= 0)
+               return ret;
+       ret = json_generate_array_close(stream->generator);
+       if (ret <= 0)
+               return ret;
+       i_assert(stream->write_node_level > 0);
+       stream->write_node_level--;
+       return 1;
+}
+
+void json_ostream_nascend_array(struct json_ostream *stream)
+{
+       int ret;
+
+       if (!json_ostream_nwrite_pre(stream))
+               return;
+       ret = json_ostream_ascend_array(stream);
+       json_ostream_nwrite_post(stream, ret);
+}
+
+/* JSON-text */
+
+/* string */
+
+int json_ostream_write_text_data(struct json_ostream *stream,
+                                const char *name,
+                                const void *data, size_t size)
+{
+       struct json_value jvalue;
+       struct json_data jdata;
+
+       i_zero(&jdata);
+       jdata.data = data;
+       jdata.size = size;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_DATA;
+       jvalue.content.data = &jdata;
+
+       return json_ostream_write_value(stream, name, JSON_TYPE_TEXT, &jvalue);
+}
+
+void json_ostream_nwrite_text_data(struct json_ostream *stream,
+                                  const char *name,
+                                  const void *data, size_t size)
+{
+       struct json_value jvalue;
+       struct json_data jdata;
+
+       i_zero(&jdata);
+       jdata.data = data;
+       jdata.size = size;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_DATA;
+       jvalue.content.data = &jdata;
+
+       json_ostream_nwrite_value(stream, name, JSON_TYPE_TEXT, &jvalue);
+}
+
+int json_ostream_write_text(struct json_ostream *stream,
+                           const char *name, const char *str)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_STRING;
+       jvalue.content.str = str;
+
+       return json_ostream_write_value(stream, name, JSON_TYPE_TEXT, &jvalue);
+}
+
+void json_ostream_nwrite_text(struct json_ostream *stream,
+                             const char *name, const char *str)
+{
+       struct json_value jvalue;
+
+       i_zero(&jvalue);
+       jvalue.content_type = JSON_CONTENT_TYPE_STRING;
+       jvalue.content.str = str;
+
+       json_ostream_nwrite_value(stream, name, JSON_TYPE_TEXT, &jvalue);
+}
+
+/*
+ * Nodes
+ */
+
+static int json_ostream_write_node_more(struct json_ostream *stream)
+{
+       int ret;
+
+       switch (stream->node_state) {
+       case JSON_OSTREAM_NODE_STATE_NONE:
+               break;
+       case JSON_OSTREAM_NODE_STATE_VALUE:
+               /* Continue value */
+               ret = json_ostream_write_more(stream);
+               if (ret <= 0)
+                       return ret;
+               stream->node_state = JSON_OSTREAM_NODE_STATE_NONE;
+               i_zero(&stream->node);
+               break;
+       case JSON_OSTREAM_NODE_STATE_ARRAY_CLOSE:
+               /* Continue array close */
+               ret = json_generate_array_close(stream->generator);
+               if (ret <= 0)
+                       return ret;
+               stream->node_state = JSON_OSTREAM_NODE_STATE_NONE;
+               i_zero(&stream->node);
+               break;
+       case JSON_OSTREAM_NODE_STATE_OBJECT_CLOSE:
+               /* Continue object close */
+               ret = json_generate_object_close(stream->generator);
+               if (ret <= 0)
+                       return ret;
+               stream->node_state = JSON_OSTREAM_NODE_STATE_NONE;
+               i_zero(&stream->node);
+               break;
+       default:
+               i_unreached();
+       }
+       return 1;
+}
+
+static int
+json_ostream_do_write_node(struct json_ostream *stream,
+                          const struct json_node *node, bool flush,
+                          bool persist)
+{
+       int ret;
+
+       if (flush) {
+               ret = json_ostream_flush(stream);
+               if (ret <= 0)
+                       return ret;
+
+               i_assert(stream->node_state == JSON_OSTREAM_NODE_STATE_NONE);
+       } else if (stream->node_state != JSON_OSTREAM_NODE_STATE_NONE) {
+               return 0;
+       }
+
+       i_assert(!json_node_is_none(node));
+
+       if (!json_node_is_end(node) && node->name != NULL) {
+               i_assert(!stream->member_name_written);
+               ret = json_generate_object_member(stream->generator,
+                                                 node->name);
+               if (ret <= 0)
+                       return ret;
+       }
+       stream->member_name_written = FALSE;
+
+       switch (node->type) {
+       case JSON_TYPE_ARRAY:
+               if (!json_node_is_array_end(node)) {
+                       /* Open array */
+                       json_generate_array_open(stream->generator);
+                       return 1;
+               }
+               /* Close array */
+               stream->node = *node;
+               stream->node_state = JSON_OSTREAM_NODE_STATE_ARRAY_CLOSE;
+               break;
+       case JSON_TYPE_OBJECT:
+               if (!json_node_is_object_end(node)) {
+                       /* Open object */
+                       json_generate_object_open(stream->generator);
+                       return 1;
+               }
+               /* Close object */
+               stream->node = *node;
+               stream->node_state = JSON_OSTREAM_NODE_STATE_OBJECT_CLOSE;
+               break;
+       default:
+               /* Write normal value */
+               stream->node = *node;
+               stream->node_state = JSON_OSTREAM_NODE_STATE_VALUE;
+               break;
+       }
+
+       ret = json_ostream_write_node_more(stream);
+       if (ret < 0)
+               return -1;
+       if (ret == 0 && persist)
+               json_ostream_persist_value(stream);
+
+       return 1;
+}
+
+int json_ostream_write_node(struct json_ostream *stream,
+                           const struct json_node *node, bool copy)
+{
+       return json_ostream_do_write_node(stream, node, TRUE, copy);
+}
+
+void json_ostream_nwrite_node(struct json_ostream *stream,
+                             const struct json_node *node)
+{
+       switch (node->type) {
+       case JSON_TYPE_ARRAY:
+               if (!json_node_is_array_end(node)) {
+                       /* Open array */
+                       json_ostream_ndescend_array(stream, node->name);
+                       return;
+               }
+               /* Close array */
+               json_ostream_nascend_array(stream);
+               return;
+       case JSON_TYPE_OBJECT:
+               if (!json_node_is_object_end(node)) {
+                       /* Open object */
+                       json_ostream_ndescend_object(stream, node->name);
+                       return;
+               }
+               /* Close object */
+               json_ostream_nascend_object(stream);
+               return;
+       default:
+               break;
+       }
+
+       json_ostream_nwrite_value(stream, node->name,
+                                 node->type, &node->value);
+}
diff --git a/src/lib-json/json-ostream.h b/src/lib-json/json-ostream.h
new file mode 100644 (file)
index 0000000..de917af
--- /dev/null
@@ -0,0 +1,256 @@
+#ifndef JSON_OSTREAM_H
+#define JSON_OSTREAM_H
+
+#include "lib.h"
+
+#include "json-types.h"
+#include "json-generator.h"
+
+struct json_ostream;
+
+/*
+ * JSON ostream
+ */
+
+struct json_ostream *
+json_ostream_create(struct ostream *output,
+                   enum json_generator_flags gen_flags);
+struct json_ostream *
+json_ostream_create_str(string_t *buf,
+                       enum json_generator_flags gen_flags);
+
+void json_ostream_ref(struct json_ostream *stream);
+void json_ostream_unref(struct json_ostream **_stream);
+void json_ostream_destroy(struct json_ostream **_stream);
+
+void json_ostream_close(struct json_ostream *stream);
+bool json_ostream_is_closed(struct json_ostream *stream) ATTR_PURE;
+
+/*
+ * Position
+ */
+
+unsigned int json_ostream_get_write_node_level(struct json_ostream *stream);
+
+/*
+ * Cork
+ */
+
+void json_ostream_cork(struct json_ostream *stream);
+void json_ostream_uncork(struct json_ostream *stream);
+bool json_ostream_is_corked(struct json_ostream *stream);
+
+/*
+ * Flush
+ */
+
+/* Try to flush the output stream. Returns 1 if all sent, 0 if not,
+   -1 if error. */
+int json_ostream_flush(struct json_ostream *stream);
+void json_ostream_nflush(struct json_ostream *stream);
+
+/*
+ * Error handling
+ */
+
+/* Returns error string for the previous error. */
+const char *json_ostream_get_error(struct json_ostream *stream);
+
+/* Marks the stream's error handling as completed. Flushes the stream and
+   returns -1 if any of the nwrite*(), ndescend*(), etc. calls didn't write
+   all data. */
+int json_ostream_nfinish(struct json_ostream *stream);
+/* Same as json_ostream_nfinish() but expects guaranteed success and implicitly
+   destroys the stream. This will assert fail if the internal
+   json_ostream_nfinish() call fails, so this is mostly only suitable for
+   buffer output. */
+void json_ostream_nfinish_destroy(struct json_ostream **_stream);
+/* Marks the stream's error handling as completed to avoid i_panic() on
+   destroy. */
+void json_ostream_ignore_last_errors(struct json_ostream *stream);
+/* If error handling is disabled, the i_panic() on destroy is never called.
+   This function can be called immediately after the stream is created. */
+void json_ostream_set_no_error_handling(struct json_ostream *stream, bool set);
+
+/*
+ * Write functions
+ */
+
+/* The 'name' argument is the name of the object member if the value is
+   written in the context of an object. If not, it MUST be NULL, or the
+   underlying JSON generator will trigger an assertion panic. The object member
+   name can also be written earlier using json_ostream_write_object_member(),
+   in which case the name argument of the subsequent value write function must
+   also be NULL.
+
+   Just like an ostream, the 'n' functions send their data with delayed error
+   handling. json_ostream_nfinish() or json_ostream_ignore_last_errors()
+   must be called after these functions before the stream is destroyed. If
+   any of the data can't be sent due to stream's buffer getting full, all
+   further 'n' function calls are ignored and json_ostream_nfinish() will
+   fail.
+ */
+
+/* value */
+
+/* object member */
+
+int json_ostream_write_object_member(struct json_ostream *stream,
+                                    const char *name);
+void json_ostream_nwrite_object_member(struct json_ostream *stream,
+                                       const char *name);
+
+/* Try to write the value to the output stream. Returns 1 if buffered, 0
+   if not, -1 if error. */
+int json_ostream_write_value(struct json_ostream *stream,
+                            const char *name, enum json_type type,
+                            const struct json_value *value);
+void json_ostream_nwrite_value(struct json_ostream *stream,
+                               const char *name, enum json_type type,
+                               const struct json_value *value);
+
+/* node */
+
+/* Try to write the JSON node to the output stream. Returns 1 if buffered,
+   0 if not, -1 if error. Value is copied to stream upon partial write if
+   copy is TRUE, otherwise caller is responsible for keeping it allocated until
+   the potentially buffered node is flushed by the stream. */
+int json_ostream_write_node(struct json_ostream *stream,
+                            const struct json_node *node, bool copy);
+void json_ostream_nwrite_node(struct json_ostream *stream,
+                              const struct json_node *node);
+
+/* number */
+
+/* Try to write the number to the output stream. Returns 1 if buffered,
+   0 if not, -1 if error. */
+int json_ostream_write_number(struct json_ostream *stream,
+                              const char *name, intmax_t number);
+void json_ostream_nwrite_number(struct json_ostream *stream,
+                                const char *name, intmax_t number);
+/* Try to write the number (the string) to the output stream. Returns 1
+   if buffered, 0 if not, -1 if error. */
+int json_ostream_write_number_raw(struct json_ostream *stream,
+                                  const char *name, const char *number);
+void json_ostream_nwrite_number_raw(struct json_ostream *stream,
+                                    const char *name, const char *number);
+
+/* string */
+
+/* Try to write the data to the output stream as a string. Returns 1 if
+   buffered, 0 if not, -1 if error. */
+int json_ostream_write_string_data(struct json_ostream *stream,
+                                  const char *name,
+                                  const void *data, size_t size);
+void json_ostream_nwrite_string_data(struct json_ostream *stream,
+                                    const char *name,
+                                    const void *data, size_t size);
+/* Try to write the buffer to the output stream as a string. Returns 1 if
+   buffered, 0 if not, -1 if error. */
+static inline int
+json_ostream_write_string_buffer(struct json_ostream *stream,
+                                 const char *name, const buffer_t *buf)
+{
+        return json_ostream_write_string_data(stream, name,
+                                               buf->data, buf->used);
+}
+static inline void
+json_ostream_nwrite_string_buffer(struct json_ostream *stream,
+                                  const char *name, const buffer_t *buf)
+{
+        json_ostream_nwrite_string_data(stream, name, buf->data, buf->used);
+}
+/* Try to write the string to the output stream. Returns 1 if buffered,
+   0 if not, -1 if error. */
+int json_ostream_write_string(struct json_ostream *stream,
+                             const char *name, const char *str);
+void json_ostream_nwrite_string(struct json_ostream *stream,
+                               const char *name, const char *str);
+void json_ostream_nwritef_string(struct json_ostream *stream,
+                                const char *name,
+                                const char *format, ...) ATTR_FORMAT(3, 4);
+
+/* Open a string on the stream, which means that all subsequent string
+   write functions are concatenated into a single JSON string value. Note that
+   the individual string values need to be valid and complete UTF-8. Any invalid
+   or incomplete UTF-8 code point will yield replacement characters in the
+   output, so code points cannot span sequential string values and must always
+   be fully contained within a single write. */
+int json_ostream_open_string(struct json_ostream *stream, const char *name);
+void json_ostream_nopen_string(struct json_ostream *stream, const char *name);
+/* Close the earlier opened string value on the stream. All subsequent string
+   write functions will create separate string values once more. */
+int json_ostream_close_string(struct json_ostream *stream);
+void json_ostream_nclose_string(struct json_ostream *stream);
+
+/* null */
+
+/* Try to write the `null' literal to the output stream. Returns 1 if
+   buffered, 0 if not, -1 if error. */
+int json_ostream_write_null(struct json_ostream *stream, const char *name);
+void json_ostream_nwrite_null(struct json_ostream *stream, const char *name);
+
+/* false, true */
+
+/* Try to write the `false' literal to the output stream. Returns 1 if
+   buffered, 0 if not, -1 if error. */
+int json_ostream_write_false(struct json_ostream *stream, const char *name);
+void json_ostream_nwrite_false(struct json_ostream *stream, const char *name);
+/* Try to write the `true' literal to the output stream. Returns 1 if
+   buffered, 0 if not, -1 if error. */
+int json_ostream_write_true(struct json_ostream *stream, const char *name);
+void json_ostream_nwrite_true(struct json_ostream *stream, const char *name);
+/* Try to write the boolean value to the output stream. Returns 1 if
+   buffered, 0 if not, -1 if error. */
+int json_ostream_write_bool(struct json_ostream *stream,
+                           const char *name, bool value);
+void json_ostream_nwrite_bool(struct json_ostream *stream,
+                             const char *name, bool value);
+
+/* object */
+
+/* Try to descend into a JSON object by writing '{' to the output stream.
+   Returns 1 if buffered, 0 if not, -1 if error. */
+int json_ostream_descend_object(struct json_ostream *stream,
+                                const char *name);
+void json_ostream_ndescend_object(struct json_ostream *stream,
+                                 const char *name);
+
+/* Try to ascend from a JSON object by writing '}' to the output stream.
+   Returns 1 if buffered, 0 if not, -1 if error. */
+int json_ostream_ascend_object(struct json_ostream *stream);
+void json_ostream_nascend_object(struct json_ostream *stream);
+
+/* array */
+
+/* Try to descend into a JSON array by writing '[' to the output stream.
+   Returns 1 if buffered, 0 if not, -1 if error. */
+int json_ostream_descend_array(struct json_ostream *stream,
+                               const char *name);
+void json_ostream_ndescend_array(struct json_ostream *stream,
+                                const char *name);
+
+/* Try to ascend from a JSON arrayh by writing ']' to the output stream.
+   Returns 1 if buffered, 0 if not, -1 if error. */
+int json_ostream_ascend_array(struct json_ostream *stream);
+void json_ostream_nascend_array(struct json_ostream *stream);
+
+/* JSON-text */
+
+/* Try to write the data to the output stream directly (JSON-text, not as
+   a string). Returns 1 if buffered, 0 if not, -1 if error. */
+int json_ostream_write_text_data(struct json_ostream *stream,
+                                const char *name,
+                                const void *data, size_t size);
+void json_ostream_nwrite_text_data(struct json_ostream *stream,
+                                  const char *name,
+                                  const void *data, size_t size);
+
+/* Try to write the string to the output stream directly (JSON-text, not as
+   a string). Returns 1 if buffered, 0 if not, -1 if error. */
+int json_ostream_write_text(struct json_ostream *stream,
+                           const char *name, const char *str);
+void json_ostream_nwrite_text(struct json_ostream *stream,
+                             const char *name, const char *str);
+
+#endif
index 31f8e6f98f28f1e0dbd2471d8f88eb4610e18078..0813e4caea34bc63a8f3b142d83340a3f65234f5 100644 (file)
@@ -11,6 +11,8 @@
 
 #include "json-parser.new.h"
 #include "json-generator.h"
+#include "json-istream.h"
+#include "json-ostream.h"
 
 #include <stdio.h>
 #include <sys/stat.h>
@@ -1249,6 +1251,553 @@ static void test_json_io_async(void)
        } T_END;
 }
 
+/*
+ * Stream I/O
+ */
+
+struct test_sio_context;
+
+struct test_sio_processor {
+       struct test_sio_context *tctx;
+       const char *name;
+
+       struct istream *input;
+       struct ostream *output;
+       struct io *io;
+
+       struct json_node jnode;
+
+       struct json_istream *jinput;
+       struct json_ostream *joutput;
+
+       bool input_finished:1;
+};
+
+struct test_sio_context {
+       const struct json_io_test *test;
+       unsigned int scenario;
+
+       struct iostream_pump *pump_in, *pump_out;
+};
+
+static void test_json_stream_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;
+               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 stream io [%d]", i));
+
+               for (j = 0; j < N_ELEMENTS(margins); j++) {
+                       struct istream *input;
+                       struct ostream *output;
+                       struct json_istream *jinput;
+                       struct json_ostream *joutput;
+                       struct json_node jnode;
+                       int pret = 0, wret = 0;
+                       const char *error = NULL;
+
+                       margin = margins[j];
+
+                       buffer_set_used_size(outbuf, 0);
+
+                       output = o_stream_create_buffer(outbuf);
+                       o_stream_set_no_error_handling(output, TRUE);
+                       input = test_istream_create_data(text, text_len);
+
+                       jinput = json_istream_create(input, 0, &test->limits, test->flags);
+                       joutput = json_ostream_create(output, 0);
+
+                       o_stream_set_max_buffer_size(output, 0);
+                       pret = 0; wret = 1;
+                       i_zero(&jnode);
+                       for (pos = 0;
+                               pos <= (text_len+margin+1) && (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_istream_walk(jinput, &jnode);
+                                       if (pret < 0)
+                                               break;
+                                       test_assert(!json_istream_failed(jinput));
+                               }
+                               if (json_node_is_none(&jnode))
+                                       wret = 1;
+                               else
+                                       wret = json_ostream_write_node(joutput, &jnode, TRUE);
+                               if (wret == 0)
+                                       continue;
+                               if (wret < 0)
+                                       break;
+                               i_zero(&jnode);
+                               pret = 0;
+                       }
+
+                       o_stream_set_max_buffer_size(output, SIZE_MAX);
+                       wret = json_ostream_flush(joutput);
+                       json_ostream_destroy(&joutput);
+
+                       pret = json_istream_finish(&jinput, &error);
+
+                       test_out_reason_quiet(
+                               t_strdup_printf("read 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));
+                       }
+
+                       o_stream_unref(&output);
+                       i_stream_unref(&input);
+               }
+
+               test_end();
+
+       } T_END;
+
+       buffer_free(&outbuf);
+}
+
+static void test_json_stream_io_input_callback(struct test_sio_processor *tproc)
+{
+       int ret;
+
+       if (!json_node_is_none(&tproc->jnode)) {
+               io_remove(&tproc->io);
+               return;
+       }
+
+       ret = json_istream_walk(tproc->jinput, &tproc->jnode);
+       test_assert(!json_istream_failed(tproc->jinput) || ret < 0);
+       if (ret < 0) {
+               const char *error = NULL;
+
+               ret = json_istream_finish(&tproc->jinput, &error);
+               i_assert(ret != 0);
+
+               test_out_reason_quiet(
+                       t_strdup_printf("%u: %s: read success (async)",
+                                       tproc->tctx->scenario, tproc->name),
+                       ret > 0, error);
+
+               if (ret < 0)
+                       io_loop_stop(current_ioloop);
+               else {
+                       tproc->input_finished = TRUE;
+                       io_remove(&tproc->io);
+                       o_stream_set_flush_pending(tproc->output, TRUE);
+               }
+               return;
+       }
+       if (ret == 0)
+               return;
+
+       o_stream_set_flush_pending(tproc->output, TRUE);
+}
+
+static int test_json_stream_io_flush_callback(struct test_sio_processor *tproc)
+{
+       int ret;
+
+       if (json_node_is_none(&tproc->jnode))
+               ret = 1;
+       else {
+               ret = json_ostream_write_node(tproc->joutput, &tproc->jnode,
+                                              TRUE);
+       }
+       if (ret < 0) {
+               test_assert(FALSE);
+               io_loop_stop(current_ioloop);
+               return -1;
+       }
+       if (ret == 0) {
+               io_remove(&tproc->io);
+               return 0;
+       }
+       i_zero(&tproc->jnode);
+
+       if (tproc->input_finished) {
+               ret = json_ostream_flush(tproc->joutput);
+               if (ret == 0)
+                       return 0;
+               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);
+               else {
+                       io_remove(&tproc->io);
+                       o_stream_close(tproc->output);
+               }
+               return ret;
+       }
+
+       if (tproc->io == NULL) {
+               tproc->io = io_add_istream(
+                       tproc->input, test_json_stream_io_input_callback, tproc);
+               i_stream_set_input_pending(tproc->input, TRUE);
+       }
+       return 1;
+}
+
+static void
+test_json_stream_io_pump_in_callback(enum iostream_pump_status status,
+                                    struct test_sio_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_stream_io_pump_out_callback(enum iostream_pump_status status,
+                                     struct test_sio_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_sio_processor_init(struct test_sio_processor *tproc,
+                       const struct json_io_test *test,
+                       struct istream *input, struct ostream *output)
+{
+       i_zero(tproc);
+
+       tproc->output = output;
+       o_stream_set_no_error_handling(tproc->output, TRUE);
+       o_stream_set_flush_callback(tproc->output,
+                                   test_json_stream_io_flush_callback, tproc);
+       o_stream_uncork(tproc->output);
+
+       tproc->input = input;
+       tproc->io = io_add_istream(tproc->input,
+                                  test_json_stream_io_input_callback, tproc);
+
+       tproc->jinput = json_istream_create(input, 0, &test->limits,
+                                            test->flags);
+       tproc->joutput = json_ostream_create(output, 0);
+}
+
+static void test_sio_processor_deinit(struct test_sio_processor *tproc)
+{
+       json_ostream_destroy(&tproc->joutput);
+       json_istream_destroy(&tproc->jinput);
+}
+
+static void
+test_json_stream_io_async_run(const struct json_io_test *test,
+                             unsigned int scenario)
+{
+       struct test_sio_context tctx;
+       string_t *outbuf;
+       struct test_sio_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_stream_io_pump_in_callback, &tctx);
+       iostream_pump_set_completion_callback(
+               tctx.pump_out, test_json_stream_io_pump_out_callback, &tctx);
+
+       /* Processor 1 */
+       test_sio_processor_init(&tproc1, test, pipe1_input, pipe2_output);
+       tproc1.tctx = &tctx;
+       tproc1.name = "proc_a";
+
+       /* Processor 2 */
+       test_sio_processor_init(&tproc2, test, pipe2_input, pipe3_output);
+       tproc2.tctx = &tctx;
+       tproc2.name = "proc_b";
+
+       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_sio_processor_deinit(&tproc1);
+       test_sio_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_stream_io_async(void)
+{
+       unsigned int i, sc;
+
+       for (i = 0; i < tests_count; i++) T_BEGIN {
+               test_begin(t_strdup_printf("json stream io async [%d]", i));
+
+               for (sc = 0; sc < 4; sc++)
+                       test_json_stream_io_async_run(&tests[i], sc);
+               test_end();
+       } T_END;
+}
+
+/*
+ * File I/O
+ */
+
+static void
+test_json_file_io_run(struct istream *input, struct ostream *output)
+{
+       struct json_istream *jinput;
+       struct json_ostream *joutput;
+       struct json_node jnode;
+       struct json_limits json_limits;
+       int rret, wret;
+
+       i_zero(&json_limits);
+       json_limits.max_name_size = SIZE_MAX;
+       json_limits.max_string_size = SIZE_MAX;
+       json_limits.max_nesting = UINT_MAX;
+       json_limits.max_list_items = UINT_MAX;
+
+       jinput = json_istream_create(input, 0, &json_limits, 0);
+       joutput = json_ostream_create(output, 0);
+
+       rret = 0; wret = 1;
+       i_zero(&jnode);
+       for (;;) {
+               if (wret > 0 && rret == 0) {
+                       rret = json_istream_walk(jinput, &jnode);
+                       if (rret < 0)
+                               break;
+                       test_assert(!json_istream_failed(jinput));
+               }
+               if (json_node_is_none(&jnode))
+                       wret = 1;
+               else
+                       wret = json_ostream_write_node(joutput, &jnode, TRUE);
+               if (wret == 0)
+                       continue;
+               if (wret < 0)
+                       break;
+               i_zero(&jnode);
+               rret = 0;
+       }
+       wret = json_ostream_flush(joutput);
+
+       test_out_reason("read success",
+               !json_istream_failed(jinput),
+               (!json_istream_failed(jinput) ?
+                NULL : json_istream_get_error(jinput)));
+       test_out("write success", wret > 0);
+
+       json_ostream_destroy(&joutput);
+       json_istream_destroy(&jinput);
+}
+
+static void
+test_json_file_compare(struct istream *input1, struct istream *input2)
+{
+       const unsigned char *data1, *data2;
+       size_t size1, size2, pleft;
+       off_t ret;
+
+       i_stream_seek(input1, 0);
+       i_stream_seek(input2, 0);
+
+       /* read payload */
+       while ((ret = i_stream_read_more(input1, &data1, &size1)) > 0) {
+               /* compare with file on disk */
+               pleft = size1;
+               while ((ret = i_stream_read_more(input2,
+                                                &data2, &size2)) > 0 &&
+                      pleft > 0) {
+                       size2 = (size2 > pleft ? pleft : size2);
+                       if (memcmp(data1, data2, size2) != 0) {
+                               i_fatal("processed data does not match "
+                                       "(%"PRIuUOFF_T":%"PRIuUOFF_T")",
+                                       input1->v_offset, input2->v_offset);
+                       }
+                       i_stream_skip(input2, size2);
+                       pleft -= size2;
+                       data1 += size2;
+               }
+               if (ret < 0 && input2->stream_errno != 0) {
+                       i_fatal("failed to read result stream: %s",
+                               i_stream_get_error(input2));
+               }
+               i_stream_skip(input1, size1);
+       }
+
+       i_assert(ret != 0);
+
+       (void)i_stream_read(input2);
+       if (input2->stream_errno != 0) {
+               i_fatal("failed to read input stream: %s",
+                       i_stream_get_error(input1));
+       } if (i_stream_have_bytes_left(input2)) {
+               if (i_stream_read_more(input2, &data2, &size2) <= 0)
+                       size2 = 0;
+               i_fatal("result stream ended prematurely "
+                       "(at least %zu bytes left)", size2);
+       }
+}
+
+static void test_json_file_io(const char *file)
+{
+       struct istream *input, *oinput;
+       struct ostream *output;
+       int fd;
+
+       test_begin(t_strdup_printf("json file io [%s]", file));
+
+       fd = open(file, O_RDONLY);
+       if (fd < 0)
+               i_fatal("Failed to open: %m");
+
+       input = i_stream_create_fd_autoclose(&fd, 1024);
+       output = iostream_temp_create("/tmp/.test-json-io-", 0);
+       o_stream_set_no_error_handling(output, TRUE);
+       test_json_file_io_run(input, output);
+       i_stream_unref(&input);
+
+       if (test_has_failed()) {
+               o_stream_unref(&output);
+               return;
+       }
+
+       input = iostream_temp_finish(&output, IO_BLOCK_SIZE);
+       output = iostream_temp_create("/tmp/.test-json-io-", 0);
+       test_json_file_io_run(input, output);
+
+       oinput = iostream_temp_finish(&output, IO_BLOCK_SIZE);
+       test_json_file_compare(input, oinput);
+
+       i_stream_unref(&input);
+       i_stream_unref(&oinput);
+
+       test_end();
+}
+
 int main(int argc, char *argv[])
 {
        int ret, c;
@@ -1258,6 +1807,8 @@ int main(int argc, char *argv[])
        static void (*test_functions[])(void) = {
                test_json_io,
                test_json_io_async,
+               test_json_stream_io,
+               test_json_stream_io_async,
                NULL
        };
 
@@ -1267,14 +1818,18 @@ int main(int argc, char *argv[])
                        debug = TRUE;
                        break;
                default:
-                       i_fatal("Usage: %s [-D]", argv[0]);
+                       i_fatal("Usage: %s [-D] [<json-file>]", argv[0]);
                }
        }
        argc -= optind;
        argv += optind;
 
-       if (argc > 0)
-               i_fatal("Usage: %s [-D]", argv[0]);
+       if (argc > 1 )
+               i_fatal("Usage: %s [-D] [<json-file>]", argv[0]);
+       if (argc == 1) {
+               test_json_file_io(argv[0]);
+               return 0;
+       }
 
        ret = test_run(test_functions);
 
diff --git a/src/lib-json/test-json-ostream.c b/src/lib-json/test-json-ostream.c
new file mode 100644 (file)
index 0000000..0fed584
--- /dev/null
@@ -0,0 +1,999 @@
+/* Copyright (c) 2017-2023 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "ostream.h"
+#include "test-common.h"
+
+#include "json-ostream.h"
+
+#include <unistd.h>
+
+static bool debug = FALSE;
+
+static void test_json_ostream_write(void)
+{
+       string_t *buffer;
+       struct ostream *output;
+       struct json_ostream *joutput;
+       unsigned int state, pos;
+       int ret;
+
+       buffer = str_new(default_pool, 256);
+       output = o_stream_create_buffer(buffer);
+       o_stream_set_no_error_handling(output, TRUE);
+
+       /* number */
+       test_begin("json ostream write - number");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_write_number(joutput, NULL, 23423);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("23423", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* false */
+       test_begin("json ostream write - false");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_write_false(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("false", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* false */
+       test_begin("json ostream write - null");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_write_null(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("null", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* true */
+       test_begin("json ostream write - true");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_write_true(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("true", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string */
+       test_begin("json ostream write - string");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_write_string(joutput, NULL, "frop!");
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("\"frop!\"", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* <JSON-text> */
+       test_begin("json ostream write - <JSON-text>");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_write_text(joutput, NULL, "\"frop!\"");
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("\"frop!\"", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ ] */
+       test_begin("json ostream write - array [ ]");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_descend_array(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_array(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("[]", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ string ] */
+       test_begin("json ostream write - array [ string ]");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_descend_array(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_write_string(joutput, NULL, "frop");
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_array(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("[\"frop\"]", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ <JSON-text> ] */
+       test_begin("json ostream write - array [ <JSON-text> ]");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_descend_array(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_write_text(joutput, NULL, "[1,\"frop\",2]");
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_array(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("[[1,\"frop\",2]]", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { } */
+       test_begin("json ostream write - object { }");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_descend_object(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_object(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("{}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": "friep" } */
+       test_begin("json ostream write - object { \"frop\": \"friep\" }");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_descend_object(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_write_string(joutput, "frop", "friep");
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_object(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("{\"frop\":\"friep\"}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": 1234 } */
+       test_begin("json ostream write - object { \"frop\": 1234 }");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_descend_object(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_write_object_member(joutput, "frop");
+       i_assert(ret > 0);
+       ret = json_ostream_write_number(joutput, NULL, 1234);
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_object(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("{\"frop\":1234}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "a": [{"d": 1}], "b": [{"e": 2}], "c": [{"f": 3}] } */
+       test_begin("json ostream write - "
+                  "object { \"a\": [{\"d\": 1}], \"b\": [{\"e\": 2}], "
+                           "\"c\": [{\"f\": 3}] }");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_descend_object(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_descend_array(joutput, "a");
+       i_assert(ret > 0);
+       ret = json_ostream_descend_object(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_write_number(joutput, "d", 1);
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_object(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_array(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_descend_array(joutput, "b");
+       i_assert(ret > 0);
+       ret = json_ostream_descend_object(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_write_number(joutput, "e", 2);
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_object(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_array(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_descend_array(joutput, "c");
+       i_assert(ret > 0);
+       ret = json_ostream_descend_object(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_write_number(joutput, "f", 3);
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_object(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_array(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_object(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp(
+               "{\"a\":[{\"d\":1}],\"b\":[{\"e\":2}],\"c\":[{\"f\":3}]}",
+               str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": <JSON-text> } */
+       test_begin("json ostream write - object { \"frop\": <JSON-text> }");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_descend_object(joutput, NULL);
+       i_assert(ret > 0);
+       ret = json_ostream_write_text(joutput, "frop",
+                                     "[false,\"friep\",true]");
+       i_assert(ret > 0);
+       ret = json_ostream_ascend_object(joutput);
+       i_assert(ret > 0);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("{\"frop\":[false,\"friep\",true]}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* trickle[1] */
+       test_begin("json ostream write - object, trickle[1]");
+       o_stream_set_max_buffer_size(output, 0);
+       joutput = json_ostream_create(output, 0);
+       state = 0;
+       for (pos = 0; pos < 400 && state <= 17; pos++) {
+               o_stream_set_max_buffer_size(output, pos);
+               switch (state) {
+               case 0:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 1:
+                       ret = json_ostream_descend_array(joutput, "aaaaaa");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 2:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 3:
+                       ret = json_ostream_write_number(joutput, "dddddd", 1);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 4:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 5:
+                       ret = json_ostream_ascend_array(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 6:
+                       ret = json_ostream_descend_array(joutput, "bbbbbb");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 7:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 8:
+                       ret = json_ostream_write_number(joutput, "eeeeee", 2);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 9:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 10:
+                       ret = json_ostream_ascend_array(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 11:
+                       ret = json_ostream_descend_array(joutput, "cccccc");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 12:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 13:
+                       ret = json_ostream_write_object_member(
+                               joutput, "ffffff");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 14:
+                       ret = json_ostream_write_number(joutput, NULL, 3);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 15:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 16:
+                       ret = json_ostream_ascend_array(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 17:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               }
+       }
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("{\"aaaaaa\":[{\"dddddd\":1}],"
+                          "\"bbbbbb\":[{\"eeeeee\":2}],"
+                          "\"cccccc\":[{\"ffffff\":3}]}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* trickle[2] */
+       test_begin("json ostream write - object, trickle[2]");
+       o_stream_set_max_buffer_size(output, 0);
+       joutput = json_ostream_create(output, 0);
+       state = 0;
+       for (pos = 0; pos < 65535 && state <= 25; pos++) {
+               o_stream_set_max_buffer_size(output, pos);
+               switch (state) {
+               case 0:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 1:
+                       ret = json_ostream_descend_array(joutput, "aaaaaa");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 2:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 3:
+                       ret = json_ostream_write_number(joutput, "dddddd", 1);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 4:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 5:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 6:
+                       ret = json_ostream_write_number(joutput, "gggggg", 4);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 7:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 8:
+                       ret = json_ostream_ascend_array(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 9:
+                       ret = json_ostream_descend_array(joutput, "bbbbbb");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 10:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 11:
+                       ret = json_ostream_write_number(joutput, "eeeeee", 2);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 12:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 13:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 14:
+                       ret = json_ostream_write_number(joutput, "hhhhhh", 5);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 15:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 16:
+                       ret = json_ostream_ascend_array(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 17:
+                       ret = json_ostream_descend_array(joutput, "cccccc");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 18:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 19:
+                       ret = json_ostream_write_number(joutput, "ffffff", 3);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 20:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 21:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 22:
+                       ret = json_ostream_write_number(joutput, "iiiiii", 6);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 23:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 24:
+                       ret = json_ostream_ascend_array(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 25:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               }
+       }
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("{\"aaaaaa\":[{\"dddddd\":1},{\"gggggg\":4}],"
+                          "\"bbbbbb\":[{\"eeeeee\":2},{\"hhhhhh\":5}],"
+                          "\"cccccc\":[{\"ffffff\":3},{\"iiiiii\":6}]}",
+                          str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* trickle[3] */
+       test_begin("json ostream write - object, trickle[3]");
+       o_stream_set_max_buffer_size(output, 0);
+       joutput = json_ostream_create(output, 0);
+       state = 0;
+       for (pos = 0; pos < 400 && state <= 16; pos++) {
+               o_stream_set_max_buffer_size(output, pos);
+               switch (state) {
+               case 0:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 1:
+                       ret = json_ostream_descend_array(joutput, "aaaaaa");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 2:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 3:
+                       ret = json_ostream_write_text(joutput, "dddddd",
+                                                     "1234567");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 4:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 5:
+                       ret = json_ostream_ascend_array(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 6:
+                       ret = json_ostream_descend_array(joutput, "bbbbbb");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 7:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 8:
+                       ret = json_ostream_write_text(joutput, "eeeeee",
+                                                     "[1,2,3,4,5,6,7]");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 9:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 10:
+                       ret = json_ostream_ascend_array(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 11:
+                       ret = json_ostream_descend_array(joutput, "cccccc");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 12:
+                       ret = json_ostream_descend_object(joutput, NULL);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 13:
+                       ret = json_ostream_write_text(joutput, "ffffff",
+                                                     "\"1234567\"");
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 14:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 15:
+                       ret = json_ostream_ascend_array(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               case 16:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               }
+       }
+       json_ostream_unref(&joutput);
+       test_assert_strcmp("{\"aaaaaa\":[{\"dddddd\":1234567}],"
+                          "\"bbbbbb\":[{\"eeeeee\":[1,2,3,4,5,6,7]}],"
+                          "\"cccccc\":[{\"ffffff\":\"1234567\"}]}",
+                          str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       o_stream_destroy(&output);
+       str_free(&buffer);
+}
+
+static void test_json_ostream_nwrite(void)
+{
+       string_t *buffer;
+       struct ostream *output;
+       struct json_ostream *joutput;
+
+       buffer = str_new(default_pool, 256);
+       output = o_stream_create_buffer(buffer);
+       o_stream_set_no_error_handling(output, TRUE);
+
+       /* number */
+       test_begin("json ostream nwrite - number");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_nwrite_number(joutput, NULL, 23423);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("23423", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* false */
+       test_begin("json ostream nwrite - false");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_nwrite_false(joutput, NULL);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("false", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* false */
+       test_begin("json ostream nwrite - null");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_nwrite_null(joutput, NULL);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("null", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* true */
+       test_begin("json ostream nwrite - true");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_nwrite_true(joutput, NULL);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("true", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string */
+       test_begin("json ostream nwrite - string");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_nwrite_string(joutput, NULL, "frop!");
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("\"frop!\"", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* formatted string */
+       test_begin("json ostream nwrite - formatted string");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_nwritef_string(joutput, NULL, "%u", 12345);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("\"12345\"", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* <JSON-text> */
+       test_begin("json ostream nwrite - <JSON-text>");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_nwrite_text(joutput, NULL, "\"frop!\"");
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("\"frop!\"", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ ] */
+       test_begin("json ostream nwrite - array [ ]");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_array(joutput, NULL);
+       json_ostream_nascend_array(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("[]", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ string ] */
+       test_begin("json ostream nwrite - array [ string ]");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_array(joutput, NULL);
+       json_ostream_nwrite_string(joutput, NULL, "frop");
+       json_ostream_nascend_array(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("[\"frop\"]", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ <JSON-text> ] */
+       test_begin("json ostream nwrite - array [ <JSON-text> ]");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_array(joutput, NULL);
+       json_ostream_nwrite_text(joutput, NULL, "[1,\"frop\",2]");
+       json_ostream_nascend_array(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("[[1,\"frop\",2]]", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { } */
+       test_begin("json ostream nwrite - object { }");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_object(joutput, NULL);
+       json_ostream_nascend_object(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("{}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { "frop": "friep" } */
+       test_begin("json ostream nwrite - object { \"frop\": \"friep\" }");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_object(joutput, NULL);
+       json_ostream_nwrite_string(joutput, "frop", "friep");
+       json_ostream_nascend_object(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("{\"frop\":\"friep\"}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* complex example */
+       test_begin("json ostream nwrite - complex example");
+       joutput = json_ostream_create_str(buffer, 0);
+       json_ostream_ndescend_object(joutput, NULL);
+       json_ostream_nwrite_string(joutput, "user", "testuser3");
+       json_ostream_nwrite_string(joutput, "event", "messageNew");
+       json_ostream_nwrite_string(joutput, "folder", "INBOX");
+       json_ostream_nwrite_number(joutput, "imap-uidvalidity", 1588805816);
+       json_ostream_nwrite_number(joutput, "imap-uid", 1);
+       json_ostream_nwrite_string(
+               joutput, "from",
+               "Source =?utf8?q?p=C3=A4_=3Dutf8=3Fq=3Fencoding=3F=3D?= "
+               "<from@example.com>");
+       json_ostream_nwrite_string(
+               joutput, "subject",
+               "Stuff =?utf8?q?p=C3=A4_=3Dutf8=3Fq=3Fencoding=3F=3D?=");
+       json_ostream_nwrite_string(joutput, "snippet",
+                                   "P\xc3\xa4iv\xc3\xa4\xc3\xa4.");
+       json_ostream_nwrite_number(joutput,  "unseen", 1);
+       json_ostream_nascend_object(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp(
+               "{\"user\":\"testuser3\","
+                "\"event\":\"messageNew\","
+                "\"folder\":\"INBOX\","
+                "\"imap-uidvalidity\":1588805816,"
+                "\"imap-uid\":1,"
+                "\"from\":\"Source =?utf8?q?p=C3=A4_=3Dutf8=3Fq=3Fencoding=3F=3D?= <from@example.com>\","
+                "\"subject\":\"Stuff =?utf8?q?p=C3=A4_=3Dutf8=3Fq=3Fencoding=3F=3D?=\","
+                "\"snippet\":\"P\xc3\xa4iv\xc3\xa4\xc3\xa4.\","
+                "\"unseen\":1}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* concatenated string */
+       test_begin("json ostream nwrite - concatenated string");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_nopen_string(joutput, NULL);
+       json_ostream_nwrite_string(joutput, NULL, "friep!");
+       json_ostream_nwrite_string(joutput, NULL, "frop!");
+       json_ostream_nwrite_string(joutput, NULL, "frml!");
+       json_ostream_nclose_string(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("\"friep!frop!frml!\"", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* array [ concatenated string ] */
+       test_begin("json ostream nwrite - array [ concatenated string ]");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_array(joutput, NULL);
+       json_ostream_nopen_string(joutput, NULL);
+       json_ostream_nwrite_string(joutput, NULL, "friep!");
+       json_ostream_nwrite_string(joutput, NULL, "frop!");
+       json_ostream_nwrite_string(joutput, NULL, "frml!");
+       json_ostream_nclose_string(joutput);
+       json_ostream_nascend_array(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("[\"friep!frop!frml!\"]", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* object { concatenated string } */
+       test_begin("json ostream nwrite - object { concatenated string }");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_object(joutput, NULL);
+       json_ostream_nopen_string(joutput, "foo");
+       json_ostream_nwrite_string(joutput, NULL, "friep!");
+       json_ostream_nwrite_string(joutput, NULL, "frop!");
+       json_ostream_nwrite_string(joutput, NULL, "frml!");
+       json_ostream_nclose_string(joutput);
+       json_ostream_nascend_object(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("{\"foo\":\"friep!frop!frml!\"}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* array [ complex concatenated string ] */
+       test_begin("json ostream nwrite - "
+                  "array [ complex concatenated string ]");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_array(joutput, NULL);
+       json_ostream_nopen_string(joutput, NULL);
+       json_ostream_nwrite_string(joutput, NULL, "friep!");
+       json_ostream_nwrite_string(joutput, NULL, "frop!");
+       json_ostream_nwrite_string(joutput, NULL, "frml!");
+       json_ostream_nclose_string(joutput);
+       json_ostream_nwrite_string(joutput, NULL, "friep!");
+       json_ostream_nwrite_string(joutput, NULL, "frop!");
+       json_ostream_nwrite_string(joutput, NULL, "frml!");
+       json_ostream_nopen_string(joutput, NULL);
+       json_ostream_nwrite_string(joutput, NULL, "frml!");
+       json_ostream_nwrite_string(joutput, NULL, "frop!");
+       json_ostream_nwrite_string(joutput, NULL, "friep!");
+       json_ostream_nclose_string(joutput);
+       json_ostream_nascend_array(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("[\"friep!frop!frml!\","
+                           "\"friep!\",\"frop!\",\"frml!\","
+                           "\"frml!frop!friep!\"]", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* object [ complex concatenated string ] */
+       test_begin("json ostream nwrite - "
+                  "object [ complex concatenated string ]");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_object(joutput, NULL);
+       json_ostream_nopen_string(joutput, "a");
+       json_ostream_nwrite_string(joutput, NULL, "friep!");
+       json_ostream_nwrite_string(joutput, NULL, "frop!");
+       json_ostream_nwrite_string(joutput, NULL, "frml!");
+       json_ostream_nclose_string(joutput);
+       json_ostream_nwrite_string(joutput, "b", "friep!");
+       json_ostream_nwrite_string(joutput, "c", "frop!");
+       json_ostream_nwrite_string(joutput, "d", "frml!");
+       json_ostream_nopen_string(joutput, "e");
+       json_ostream_nwrite_string(joutput, NULL, "frml!");
+       json_ostream_nwrite_string(joutput, NULL, "frop!");
+       json_ostream_nwrite_string(joutput, NULL, "friep!");
+       json_ostream_nclose_string(joutput);
+       json_ostream_nascend_object(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("{\"a\":\"friep!frop!frml!\","
+                           "\"b\":\"friep!\",\"c\":\"frop!\",\"d\":\"frml!\","
+                           "\"e\":\"frml!frop!friep!\"}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       o_stream_destroy(&output);
+       str_free(&buffer);
+}
+
+int main(int argc, char *argv[])
+{
+       int c;
+
+       static void (*test_functions[])(void) = {
+               test_json_ostream_write,
+               test_json_ostream_nwrite,
+               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);
+}