]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-json: json-ostream - Add support for writing JSON tree values
authorStephan Bosch <stephan.bosch@open-xchange.com>
Wed, 7 Aug 2019 19:49:25 +0000 (21:49 +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
src/lib-json/json-ostream.h
src/lib-json/test-json-ostream.c
src/lib-json/test-json-tree-io.c [new file with mode: 0644]

index bcc815ae23a43dffb2d4b3957c429c470453ce17..f694b4a70699b27ca5cc33bcbe178108f011e7ac 100644 (file)
@@ -29,7 +29,8 @@ test_programs = \
        test-json-io \
        test-json-istream \
        test-json-ostream \
-       test-json-tree
+       test-json-tree \
+       test-json-tree-io
 
 noinst_PROGRAMS = $(test_programs)
 
@@ -87,6 +88,13 @@ test_json_tree_LDADD = \
 test_json_tree_DEPENDENCIES = \
        $(test_deps)
 
+test_json_tree_io_SOURCE = \
+       test-json-tree-io.c
+test_json_tree_io_LDADD = \
+       $(test_libs)
+test_json_tree_io_DEPENDENCIES = \
+       $(test_deps)
+
 pkginc_libdir=$(pkgincludedir)
 pkginc_lib_HEADERS = $(headers)
 
index c9a7844a33d9310920ac4e43b8948c4f6dc77831..9f7176fb9865e606f448efc8bd4282d53e348815 100644 (file)
@@ -25,6 +25,10 @@ struct json_ostream {
        enum json_ostream_node_state node_state;
        unsigned int write_node_level;
 
+       struct json_tree_walker *tree_walker;
+       struct json_tree *tree;
+       struct json_node tree_node;
+
        struct json_data node_data;
        string_t *buffer;
 
@@ -40,7 +44,12 @@ struct json_ostream {
        bool closed:1;
 };
 
+static int json_ostream_write_tree_more(struct json_ostream *stream);
 static int json_ostream_write_node_more(struct json_ostream *stream);
+static int
+json_ostream_do_write_node(struct json_ostream *stream,
+                          const struct json_node *node, bool flush,
+                          bool persist);
 
 struct json_ostream *
 json_ostream_create(struct ostream *output,
@@ -100,6 +109,9 @@ void json_ostream_unref(struct json_ostream **_stream)
        o_stream_unref(&stream->output);
        str_free(&stream->buffer);
 
+       json_tree_walker_free(&stream->tree_walker);
+       json_tree_unref(&stream->tree);
+
        i_free(stream->error);
        i_free(stream);
 }
@@ -350,6 +362,16 @@ int json_ostream_flush(struct json_ostream *stream)
                if (ret <= 0)
                        return ret;
        }
+       if (stream->tree_walker != NULL) {
+               ret = json_ostream_write_tree_more(stream);
+               if (ret <= 0)
+                       return ret;
+               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);
@@ -985,6 +1007,88 @@ void json_ostream_nwrite_text_stream(struct json_ostream *stream,
                stream->nfailed = TRUE;
 }
 
+static int json_ostream_write_tree_more(struct json_ostream *stream)
+{
+       int ret;
+
+       i_assert(stream->tree_walker != NULL);
+
+       for (;;) {
+               /* Walk the tree to the next node */
+               if (json_node_is_none(&stream->tree_node) &&
+                   !json_tree_walk(stream->tree_walker,
+                                   &stream->tree_node)) {
+                       json_tree_walker_free(&stream->tree_walker);
+                       json_tree_unref(&stream->tree);
+                       i_zero(&stream->tree_node);
+                       return 1;
+               }
+
+               ret = json_ostream_do_write_node(stream, &stream->tree_node,
+                                                FALSE, FALSE);
+               if (ret < 0) {
+                       json_tree_walker_free(&stream->tree_walker);
+                       json_tree_unref(&stream->tree);
+                       i_zero(&stream->tree_node);
+                       return -1;
+               }
+               if (ret == 0)
+                       return ret;
+               i_zero(&stream->tree_node);
+       }
+       i_unreached();
+}
+
+static int
+json_ostream_write_tree_init(struct json_ostream *stream, const char *name,
+                            const struct json_tree *jtree)
+{
+       int ret;
+
+       i_assert(jtree != NULL);
+
+       ret = json_ostream_write_init(stream, name, JSON_TYPE_TEXT);
+       if (ret <= 0)
+               return ret;
+
+       i_assert(stream->tree_walker == NULL);
+       stream->tree_walker = json_tree_walker_create(jtree);
+       i_zero(&stream->tree_node);
+
+       return 1;
+}
+
+int json_ostream_write_tree(struct json_ostream *stream, const char *name,
+                           struct json_tree *jtree)
+{
+       int ret;
+
+       ret = json_ostream_write_tree_init(stream, name, jtree);
+       if (ret <= 0)
+               return ret;
+
+       ret = json_ostream_write_tree_more(stream);
+       if (stream->tree_walker != NULL) {
+               stream->tree = jtree;
+               json_tree_ref(jtree);
+       }
+       return 1;
+}
+
+void json_ostream_nwrite_tree(struct json_ostream *stream, const char *name,
+                             const struct json_tree *jtree)
+{
+       int ret;
+
+       if (!json_ostream_nwrite_pre(stream))
+               return;
+       ret = json_ostream_write_tree_init(stream, name, jtree);
+       if (ret > 0)
+               ret = json_ostream_write_tree_more(stream);
+       i_assert(ret <= 0 || stream->tree_walker == NULL);
+       json_ostream_nwrite_post(stream, ret);
+}
+
 /*
  * Nodes
  */
index fc865afe696c11a6626b37b34333171a0aa9cc3e..3d0356f63ada1f8aaba05e1d024bfd8f624c880c 100644 (file)
@@ -4,6 +4,7 @@
 #include "lib.h"
 
 #include "json-types.h"
+#include "json-tree.new.h"
 #include "json-generator.h"
 
 struct json_ostream;
@@ -266,6 +267,18 @@ int json_ostream_write_text_stream(struct json_ostream *stream,
 void json_ostream_nwrite_text_stream(struct json_ostream *stream,
                                      const char *name, struct istream *input);
 
+/* Write a JSON-text tree object to the stream. Returns 1 on success,
+   0 if the stream buffer is full, or -1 upon error. Success does not mean
+   that the tree is already (fully) sent. It just means that the stream was
+   able to accept/buffer the tree object immediately. While the tree is still
+   being sent, the stream holds a reference to it. Sending an incompletely sent
+   tree continues once one of the write(), descend(), acsend(), or flush()
+   functions is called. */
+int json_ostream_write_tree(struct json_ostream *stream, const char *name,
+                            struct json_tree *jtree);
+void json_ostream_nwrite_tree(struct json_ostream *stream, const char *name,
+                              const struct json_tree *jtree);
+
 /*
  * String output stream
  */
index 8df070396aa8b55e155ff6c8e7e38ab34cd5b6c8..14e95f3553c987dd27be01fea6920407d4a7162b 100644 (file)
@@ -1243,6 +1243,1178 @@ static void test_json_ostream_nwrite(void)
        str_free(&buffer);
 }
 
+static void test_json_ostream_write_tree(void)
+{
+       string_t *buffer;
+       struct istream *input;
+       struct ostream *output;
+       struct json_ostream *joutput;
+       struct json_tree *jtree, *jtree2;
+       struct json_tree_node *jtnode;
+       const char *data;
+       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 tree - number");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_number_int(
+               json_tree_get_root(jtree), NULL, 23423);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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 tree - false");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_false(
+               json_tree_get_root(jtree), NULL);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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;
+
+       /* null */
+       test_begin("json ostream write tree - null");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_null(
+               json_tree_get_root(jtree), NULL);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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 tree - true");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_true(
+               json_tree_get_root(jtree), NULL);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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 tree - string");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_string(
+               json_tree_get_root(jtree), NULL, "frop");
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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;
+
+       /* string stream */
+       test_begin("json ostream write tree - string stream");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       data = "AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ";
+       input = i_stream_create_from_data(data, strlen(data));
+       (void)json_tree_node_add_string_stream(
+               json_tree_get_root(jtree), NULL, input);
+       i_stream_unref(&input);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp(
+               "\"AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ\"",
+               str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* array */
+       test_begin("json ostream write tree - array");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_array(
+               json_tree_get_root(jtree), NULL);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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 tree - array [ string ]");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_array(
+               json_tree_get_root(jtree), NULL);
+       (void)json_tree_node_add_string(jtnode, NULL, "frop");
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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;
+
+       /* [ string stream ] */
+       test_begin("json ostream write tree - array [ string stream ]");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_array(
+               json_tree_get_root(jtree), NULL);
+       data = "AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ";
+       input = i_stream_create_from_data(data, strlen(data));
+       (void)json_tree_node_add_string_stream(jtnode, NULL, input);
+       i_stream_unref(&input);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp(
+               "[\"AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ\"]",
+               str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* object */
+       test_begin("json ostream write tree - object");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_object(
+               json_tree_get_root(jtree), NULL);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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;
+
+       /* { member: string } */
+       test_begin("json ostream write tree - object { member: string }");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_object(
+               json_tree_get_root(jtree), NULL);
+       (void)json_tree_node_add_string(jtnode, "frop", "friep");
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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;
+
+       /* { member: string stream } */
+       test_begin("json ostream write tree - object { member: string stream }");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_object(
+               json_tree_get_root(jtree), NULL);
+       data = "AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ";
+       input = i_stream_create_from_data(data, strlen(data));
+       (void)json_tree_node_add_string_stream(jtnode, "frop", input);
+       i_stream_unref(&input);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       ret = json_ostream_flush(joutput);
+       i_assert(ret > 0);
+       json_ostream_unref(&joutput);
+       test_assert_strcmp(
+               "{\"frop\":"
+               "\"AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ\"}",
+               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 tree - object "
+                  "{ \"a\": [{\"d\": 1}], \"b\": [{\"e\": 2}], "
+                    "\"c\": [{\"f\": 3}] }");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_object(
+               json_tree_get_root(jtree), NULL);
+       jtnode = json_tree_node_add_array(jtnode, "a");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "d", 1);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_add_array(jtnode, "b");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "e", 2);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_add_array(jtnode, "c");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "f", 3);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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;
+
+       /* { "a": [{"d": 1}], "b": [{"e": 2}], "c": [{"f": 3}] } */
+       test_begin("json ostream write tree - descended");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_descend_object(joutput, NULL);
+       i_assert(ret > 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "d", 1);
+       ret = json_ostream_write_tree(joutput, "a", jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "e", 2);
+       ret = json_ostream_write_tree(joutput, "b", jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "f", 3);
+       ret = json_ostream_write_tree(joutput, "c", jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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;
+
+       /* descended, strings */
+       test_begin("json ostream write tree - descended, strings");
+       joutput = json_ostream_create(output, 0);
+       ret = json_ostream_descend_object(joutput, NULL);
+       i_assert(ret > 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_string(jtnode, "d", "gggggggggg");
+       ret = json_ostream_write_tree(joutput, "a", jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_string(jtnode, "e", "hhhhhhhhhh");
+       ret = json_ostream_write_tree(joutput, "b", jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_string(jtnode, "f", "iiiiiiiiii");
+       ret = json_ostream_write_tree(joutput, "c", jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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\":\"gggggggggg\"}],"
+                          "\"b\":[{\"e\":\"hhhhhhhhhh\"}],"
+                          "\"c\":[{\"f\":\"iiiiiiiiii\"}]}", 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 tree - nested trees");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       jtree2 = json_tree_create();
+       jtnode = json_tree_get_root(jtree2);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "d", 1);
+       jtnode = json_tree_get_root(jtree);
+       json_tree_node_add_subtree(jtnode, "a", jtree2);
+       json_tree_unref(&jtree2);
+       jtree2 = json_tree_create();
+       jtnode = json_tree_get_root(jtree2);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "e", 2);
+       jtnode = json_tree_get_root(jtree);
+       json_tree_node_add_subtree(jtnode, "b", jtree2);
+       json_tree_unref(&jtree2);
+       jtree2 = json_tree_create();
+       jtnode = json_tree_get_root(jtree2);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "f", 3);
+       jtnode = json_tree_get_root(jtree);
+       json_tree_node_add_subtree(jtnode, "c", jtree2);
+       json_tree_unref(&jtree2);
+       ret = json_ostream_write_tree(joutput, NULL, jtree);
+       i_assert(ret > 0);
+       json_tree_unref(&jtree);
+       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;
+
+       /* trickle [1] */
+       test_begin("json ostream write tree - object, trickle[1]");
+       o_stream_set_max_buffer_size(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_object(
+               json_tree_get_root(jtree), NULL);
+       jtnode = json_tree_node_add_array(jtnode, "aaaaaa");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "dddddd", 1);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_add_array(jtnode, "bbbbbb");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "eeeeee", 2);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_add_array(jtnode, "cccccc");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "ffffff", 3);
+       joutput = json_ostream_create(output, 0);
+       state = 0;
+       for (pos = 0; pos < 400 && state <= 1; pos++) {
+               o_stream_set_max_buffer_size(output, pos);
+               switch (state) {
+               case 0:
+                       ret = json_ostream_write_tree(joutput, NULL, jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 1:
+                       ret = json_ostream_flush(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       state++;
+                       continue;
+               }
+       }
+       json_ostream_unref(&joutput);
+       test_assert(state == 2);
+       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 tree - object, trickle[2]");
+       o_stream_set_max_buffer_size(output, 0);
+       joutput = json_ostream_create(output, 0);
+       state = 0;
+       for (pos = 0; pos < 400 && state <= 7; 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:
+                       jtree = json_tree_create();
+                       jtnode = json_tree_get_root(jtree);
+                       jtnode = json_tree_node_add_array(jtnode, NULL);
+                       jtnode = json_tree_node_add_object(jtnode, NULL);
+                       (void)json_tree_node_add_number_int(
+                               jtnode, "dddddd", 1);
+                       state++;
+                       /* fall through */
+               case 2:
+                       ret = json_ostream_write_tree(joutput, "aaaaaa", jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 3:
+                       jtree = json_tree_create();
+                       jtnode = json_tree_get_root(jtree);
+                       jtnode = json_tree_node_add_array(jtnode, NULL);
+                       jtnode = json_tree_node_add_object(jtnode, NULL);
+                       (void)json_tree_node_add_number_int(
+                               jtnode, "eeeeee", 2);
+                       state++;
+                       /* fall through */
+               case 4:
+                       ret = json_ostream_write_tree(joutput, "bbbbbb", jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 5:
+                       jtree = json_tree_create();
+                       jtnode = json_tree_get_root(jtree);
+                       jtnode = json_tree_node_add_array(jtnode, NULL);
+                       jtnode = json_tree_node_add_object(jtnode, NULL);
+                       (void)json_tree_node_add_number_int(
+                               jtnode, "ffffff", 3);
+                       state++;
+                       /* fall through */
+               case 6:
+                       ret = json_ostream_write_tree(joutput, "cccccc", jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 7:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       state++;
+                       continue;
+               }
+       }
+       json_ostream_unref(&joutput);
+       test_assert(state == 8);
+       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[3] */
+       test_begin("json ostream write tree - object, trickle[3]");
+       o_stream_set_max_buffer_size(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       jtree2 = json_tree_create();
+       jtnode = json_tree_get_root(jtree2);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "dddddd", 1);
+       jtnode = json_tree_get_root(jtree);
+       json_tree_node_add_subtree(jtnode, "aaaaaa", jtree2);
+       json_tree_unref(&jtree2);
+       jtree2 = json_tree_create();
+       jtnode = json_tree_get_root(jtree2);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "eeeeee", 2);
+       jtnode = json_tree_get_root(jtree);
+       json_tree_node_add_subtree(jtnode, "bbbbbb", jtree2);
+       json_tree_unref(&jtree2);
+       jtree2 = json_tree_create();
+       jtnode = json_tree_get_root(jtree2);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "ffffff", 3);
+       jtnode = json_tree_get_root(jtree);
+       json_tree_node_add_subtree(jtnode, "cccccc", jtree2);
+       json_tree_unref(&jtree2);
+       joutput = json_ostream_create(output, 0);
+       state = 0;
+       for (pos = 0; pos < 400 && state <= 1; pos++) {
+               o_stream_set_max_buffer_size(output, pos);
+               switch (state) {
+               case 0:
+                       ret = json_ostream_write_tree(joutput, NULL, jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 1:
+                       ret = json_ostream_flush(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       state++;
+                       continue;
+               }
+       }
+       json_ostream_unref(&joutput);
+       test_assert(state == 2);
+       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 [4] */
+       test_begin("json ostream write tree - object, trickle[4]");
+       o_stream_set_max_buffer_size(output, 0);
+       joutput = json_ostream_create(output, 0);
+       state = 0;
+       for (pos = 0; pos < 400 && state <= 7; 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:
+                       jtree = json_tree_create();
+                       jtnode = json_tree_get_root(jtree);
+                       jtnode = json_tree_node_add_array(jtnode, NULL);
+                       jtnode = json_tree_node_add_object(jtnode, NULL);
+                       (void)json_tree_node_add_string(
+                               jtnode, "dddddd", "gggggggggg");
+                       state++;
+                       /* fall through */
+               case 2:
+                       ret = json_ostream_write_tree(joutput, "aaaaaa", jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 3:
+                       jtree = json_tree_create();
+                       jtnode = json_tree_get_root(jtree);
+                       jtnode = json_tree_node_add_array(jtnode, NULL);
+                       jtnode = json_tree_node_add_object(jtnode, NULL);
+                       (void)json_tree_node_add_string(
+                               jtnode, "eeeeee", "hhhhhhhhhh");
+                       state++;
+                       /* fall through */
+               case 4:
+                       ret = json_ostream_write_tree(joutput, "bbbbbb", jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 5:
+                       jtree = json_tree_create();
+                       jtnode = json_tree_get_root(jtree);
+                       jtnode = json_tree_node_add_array(jtnode, NULL);
+                       jtnode = json_tree_node_add_object(jtnode, NULL);
+                       (void)json_tree_node_add_string(
+                               jtnode, "ffffff", "iiiiiiiiii");
+                       state++;
+                       /* fall through */
+               case 6:
+                       ret = json_ostream_write_tree(joutput, "cccccc", jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 7:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       state++;
+                       continue;
+               }
+       }
+       json_ostream_unref(&joutput);
+       test_assert(state == 8);
+       test_assert_strcmp("{\"aaaaaa\":[{\"dddddd\":\"gggggggggg\"}],"
+                          "\"bbbbbb\":[{\"eeeeee\":\"hhhhhhhhhh\"}],"
+                          "\"cccccc\":[{\"ffffff\":\"iiiiiiiiii\"}]}",
+                          str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* trickle [5] */
+       test_begin("json ostream write tree - object, trickle[5]");
+       o_stream_set_max_buffer_size(output, 0);
+       joutput = json_ostream_create(output, 0);
+       state = 0;
+       for (pos = 0; pos < 400 && state <= 7; 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:
+                       jtree = json_tree_create();
+                       jtnode = json_tree_get_root(jtree);
+                       jtnode = json_tree_node_add_array(jtnode, NULL);
+                       jtnode = json_tree_node_add_object(jtnode, NULL);
+                       (void)json_tree_node_add_text(
+                               jtnode, "dddddd", "1234567");
+                       state++;
+                       /* fall through */
+               case 2:
+                       ret = json_ostream_write_tree(joutput, "aaaaaa", jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 3:
+                       jtree = json_tree_create();
+                       jtnode = json_tree_get_root(jtree);
+                       jtnode = json_tree_node_add_array(jtnode, NULL);
+                       jtnode = json_tree_node_add_object(jtnode, NULL);
+                       (void)json_tree_node_add_text(
+                               jtnode, "eeeeee", "[1,2,3,4,5,6,7]");
+                       state++;
+                       /* fall through */
+               case 4:
+                       ret = json_ostream_write_tree(joutput, "bbbbbb", jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 5:
+                       jtree = json_tree_create();
+                       jtnode = json_tree_get_root(jtree);
+                       jtnode = json_tree_node_add_array(jtnode, NULL);
+                       jtnode = json_tree_node_add_object(jtnode, NULL);
+                       (void)json_tree_node_add_text(
+                               jtnode, "ffffff", "\"1234567\"");
+                       state++;
+                       /* fall through */
+               case 6:
+                       ret = json_ostream_write_tree(joutput, "cccccc", jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 7:
+                       ret = json_ostream_ascend_object(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0)
+                               break;
+                       state++;
+                       continue;
+               }
+       }
+       json_ostream_unref(&joutput);
+       test_assert(state == 8);
+       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;
+
+       /* trickle [6] */
+       test_begin("json ostream write tree - object, trickle[6]");
+       o_stream_set_max_buffer_size(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_object(
+               json_tree_get_root(jtree), NULL);
+       jtnode = json_tree_node_add_array(jtnode, "aaaaaa");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       data = "AAAAA";
+       input = i_stream_create_from_data(data, strlen(data));
+       (void)json_tree_node_add_string_stream(jtnode, "dddddd", input);
+       i_stream_unref(&input);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_add_array(jtnode, "bbbbbb");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       data = "BBBBB";
+       input = i_stream_create_from_data(data, strlen(data));
+       (void)json_tree_node_add_string_stream(jtnode, "eeeeee", input);
+       i_stream_unref(&input);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_add_array(jtnode, "cccccc");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       data = "CCCCC";
+       input = i_stream_create_from_data(data, strlen(data));
+       (void)json_tree_node_add_string_stream(jtnode, "ffffff", input);
+       i_stream_unref(&input);
+       joutput = json_ostream_create(output, 0);
+       state = 0;
+       for (pos = 0; pos < 400 && state <= 1; pos++) {
+               o_stream_set_max_buffer_size(output, pos);
+               switch (state) {
+               case 0:
+                       ret = json_ostream_write_tree(joutput, NULL, jtree);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       json_tree_unref(&jtree);
+                       state++;
+                       continue;
+               case 1:
+                       ret = json_ostream_flush(joutput);
+                       i_assert(ret >= 0);
+                       if (ret == 0) break;
+                       state++;
+                       continue;
+               }
+       }
+       json_ostream_unref(&joutput);
+       test_assert(state == 2);
+       test_assert_strcmp("{\"aaaaaa\":[{\"dddddd\":\"AAAAA\"}],"
+                          "\"bbbbbb\":[{\"eeeeee\":\"BBBBB\"}],"
+                          "\"cccccc\":[{\"ffffff\":\"CCCCC\"}]}",
+                          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_tree(void)
+{
+       string_t *buffer;
+       struct istream *input;
+       struct ostream *output;
+       struct json_ostream *joutput;
+       struct json_tree *jtree, *jtree2;
+       struct json_tree_node *jtnode;
+       const char *data;
+
+       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 tree - number");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_number_int(
+               json_tree_get_root(jtree), NULL, 23423);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       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 tree - false");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_false(
+               json_tree_get_root(jtree), NULL);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("false", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* null */
+       test_begin("json ostream nwrite tree - null");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_null(
+               json_tree_get_root(jtree), NULL);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       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 tree - true");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_true(
+               json_tree_get_root(jtree), NULL);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       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 tree - string");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_string(
+               json_tree_get_root(jtree), NULL, "frop");
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("\"frop\"", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* string stream */
+       test_begin("json ostream nwrite tree - string stream");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       data = "AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ";
+       input = i_stream_create_from_data(data, strlen(data));
+       (void)json_tree_node_add_string_stream(
+               json_tree_get_root(jtree), NULL, input);
+       i_stream_unref(&input);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp(
+               "\"AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ\"",
+               str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* array */
+       test_begin("json ostream nwrite tree - array");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_array(
+               json_tree_get_root(jtree), NULL);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       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 tree - array [ string ]");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_array(
+               json_tree_get_root(jtree), NULL);
+       (void)json_tree_node_add_string(jtnode, NULL, "frop");
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("[\"frop\"]", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* [ string stream ] */
+       test_begin("json ostream nwrite tree - array [ string stream ]");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_array(
+               json_tree_get_root(jtree), NULL);
+       data = "AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ";
+       input = i_stream_create_from_data(data, strlen(data));
+       (void)json_tree_node_add_string_stream(jtnode, NULL, input);
+       i_stream_unref(&input);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp(
+               "[\"AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ\"]",
+               str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* object */
+       test_begin("json ostream nwrite tree - object");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       (void)json_tree_node_add_object(
+               json_tree_get_root(jtree), NULL);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("{}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { member: string } */
+       test_begin("json ostream nwrite tree - object { member: string }");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_object(
+               json_tree_get_root(jtree), NULL);
+       (void)json_tree_node_add_string(jtnode, "frop", "friep");
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("{\"frop\":\"friep\"}", str_c(buffer));
+       test_end();
+       str_truncate(buffer, 0);
+       output->offset = 0;
+
+       /* { member: string stream } */
+       test_begin("json ostream nwrite tree - "
+                  "object { member: string stream }");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_object(
+               json_tree_get_root(jtree), NULL);
+       data = "AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ";
+       input = i_stream_create_from_data(data, strlen(data));
+       (void)json_tree_node_add_string_stream(jtnode, "frop", input);
+       i_stream_unref(&input);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp(
+               "{\"frop\":"
+               "\"AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ\"}",
+               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 nwrite tree - object "
+                  "{ \"a\": [{\"d\": 1}], \"b\": [{\"e\": 2}], "
+                    "\"c\": [{\"f\": 3}] }");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_node_add_object(
+               json_tree_get_root(jtree), NULL);
+       jtnode = json_tree_node_add_array(jtnode, "a");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "d", 1);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_add_array(jtnode, "b");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "e", 2);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_get_parent(jtnode);
+       jtnode = json_tree_node_add_array(jtnode, "c");
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "f", 3);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nfinish_destroy(&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;
+
+       /* { "a": [{"d": 1}], "b": [{"e": 2}], "c": [{"f": 3}] } */
+       test_begin("json ostream nwrite tree - descended");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_object(joutput, NULL);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "d", 1);
+       json_ostream_nwrite_tree(joutput, "a", jtree);
+       json_tree_unref(&jtree);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "e", 2);
+       json_ostream_nwrite_tree(joutput, "b", jtree);
+       json_tree_unref(&jtree);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "f", 3);
+       json_ostream_nwrite_tree(joutput, "c", jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nascend_object(joutput);
+       json_ostream_nfinish_destroy(&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;
+
+       /* descended, strings */
+       test_begin("json ostream nwrite tree - descended, strings");
+       joutput = json_ostream_create(output, 0);
+       json_ostream_ndescend_object(joutput, NULL);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_string(jtnode, "d", "gggggggggg");
+       json_ostream_nwrite_tree(joutput, "a", jtree);
+       json_tree_unref(&jtree);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_string(jtnode, "e", "hhhhhhhhhh");
+       json_ostream_nwrite_tree(joutput, "b", jtree);
+       json_tree_unref(&jtree);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_string(jtnode, "f", "iiiiiiiiii");
+       json_ostream_nwrite_tree(joutput, "c", jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nascend_object(joutput);
+       json_ostream_nfinish_destroy(&joutput);
+       test_assert_strcmp("{\"a\":[{\"d\":\"gggggggggg\"}],"
+                          "\"b\":[{\"e\":\"hhhhhhhhhh\"}],"
+                          "\"c\":[{\"f\":\"iiiiiiiiii\"}]}", 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 nwrite tree - nested trees");
+       joutput = json_ostream_create(output, 0);
+       jtree = json_tree_create();
+       jtnode = json_tree_get_root(jtree);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       jtree2 = json_tree_create();
+       jtnode = json_tree_get_root(jtree2);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "d", 1);
+       jtnode = json_tree_get_root(jtree);
+       json_tree_node_add_subtree(jtnode, "a", jtree2);
+       json_tree_unref(&jtree2);
+       jtree2 = json_tree_create();
+       jtnode = json_tree_get_root(jtree2);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "e", 2);
+       jtnode = json_tree_get_root(jtree);
+       json_tree_node_add_subtree(jtnode, "b", jtree2);
+       json_tree_unref(&jtree2);
+       jtree2 = json_tree_create();
+       jtnode = json_tree_get_root(jtree2);
+       jtnode = json_tree_node_add_array(jtnode, NULL);
+       jtnode = json_tree_node_add_object(jtnode, NULL);
+       (void)json_tree_node_add_number_int(jtnode, "f", 3);
+       jtnode = json_tree_get_root(jtree);
+       json_tree_node_add_subtree(jtnode, "c", jtree2);
+       json_tree_unref(&jtree2);
+       json_ostream_nwrite_tree(joutput, NULL, jtree);
+       json_tree_unref(&jtree);
+       json_ostream_nfinish_destroy(&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;
+
+       o_stream_destroy(&output);
+       str_free(&buffer);
+}
+
 int main(int argc, char *argv[])
 {
        int c;
@@ -1250,6 +2422,8 @@ int main(int argc, char *argv[])
        static void (*test_functions[])(void) = {
                test_json_ostream_write,
                test_json_ostream_nwrite,
+               test_json_ostream_write_tree,
+               test_json_ostream_nwrite_tree,
                NULL
        };
 
diff --git a/src/lib-json/test-json-tree-io.c b/src/lib-json/test-json-tree-io.c
new file mode 100644 (file)
index 0000000..2bb8a5c
--- /dev/null
@@ -0,0 +1,446 @@
+/* Copyright (c) 2017-2023 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "test-common.h"
+
+#include "json-istream.h"
+#include "json-ostream.h"
+
+#include <unistd.h>
+
+static bool debug = FALSE;
+
+struct json_io_test {
+       const char *input;
+       const char *output;
+       struct json_limits limits;
+       enum json_parser_flags flags;
+};
+
+static const struct json_io_test
+tests[] = {
+       {
+               .input = "{\"kty\":\"EC\","
+                         "\"crv\":\"P-256\","
+                         "\"x\":\"Kp0Y4-Wpt-D9t_2XenFIj0LmvaZByLG69yOisek4aMI\","
+                         "\"y\":\"wjEPB5BhH5SRPw1cCN5grWrLCphrW19fCFR8p7c9O5o\","
+                         "\"use\":\"sig\","
+                         "\"kid\":\"123\","
+                         "\"d\":\"Po2z9rs86J2Qb_xWprr4idsWNPlgKf3G8-mftnE2ync\""
+                        "}",
+       },
+       {
+               .input =
+                       "{\r\n"
+                       "    \"$schema\": \"http://json-schema.org/draft-06/schema#\",\r\n"
+                       "    \"$id\": \"http://json-schema.org/draft-06/schema#\",\r\n"
+                       "    \"title\": \"Core schema meta-schema\",\r\n"
+                       "    \"definitions\": {\r\n"
+                       "        \"schemaArray\": {\r\n"
+                       "            \"type\": \"array\",\r\n"
+                       "            \"minItems\": 1,\r\n"
+                       "            \"items\": { \"$ref\": \"#\" }\r\n"
+                       "        },\r\n"
+                       "        \"nonNegativeInteger\": {\r\n"
+                       "            \"type\": \"integer\",\r\n"
+                       "            \"minimum\": 0\r\n"
+                       "        },\r\n"
+                       "        \"nonNegativeIntegerDefault0\": {\r\n"
+                       "            \"allOf\": [\r\n"
+                       "                { \"$ref\": \"#/definitions/nonNegativeInteger\" },\r\n"
+                       "                { \"default\": 0 }\r\n"
+                       "            ]\r\n"
+                       "        },\r\n"
+                       "        \"simpleTypes\": {\r\n"
+                       "            \"enum\": [\r\n"
+                       "                \"array\",\r\n"
+                       "                \"boolean\",\r\n"
+                       "                \"integer\",\r\n"
+                       "                \"null\",\r\n"
+                       "                \"number\",\r\n"
+                       "                \"object\",\r\n"
+                       "                \"string\"\r\n"
+                       "            ]\r\n"
+                       "        },\r\n"
+                       "        \"stringArray\": {\r\n"
+                       "            \"type\": \"array\",\r\n"
+                       "            \"items\": { \"type\": \"string\" },\r\n"
+                       "            \"uniqueItems\": true,\r\n"
+                       "            \"default\": []\r\n"
+                       "        }\r\n"
+                       "    },\r\n"
+                       "    \"type\": [\"object\", \"boolean\"],\r\n"
+                       "    \"properties\": {\r\n"
+                       "        \"$id\": {\r\n"
+                       "            \"type\": \"string\",\r\n"
+                       "            \"format\": \"uri-reference\"\r\n"
+                       "        },\r\n"
+                       "        \"$schema\": {\r\n"
+                       "            \"type\": \"string\",\r\n"
+                       "            \"format\": \"uri\"\r\n"
+                       "        },\r\n"
+                       "        \"$ref\": {\r\n"
+                       "            \"type\": \"string\",\r\n"
+                       "            \"format\": \"uri-reference\"\r\n"
+                       "        },\r\n"
+                       "        \"title\": {\r\n"
+                       "            \"type\": \"string\"\r\n"
+                       "        },\r\n"
+                       "        \"description\": {\r\n"
+                       "            \"type\": \"string\"\r\n"
+                       "        },\r\n"
+                       "        \"default\": {},\r\n"
+                       "        \"multipleOf\": {\r\n"
+                       "            \"type\": \"number\",\r\n"
+                       "            \"exclusiveMinimum\": 0\r\n"
+                       "        },\r\n"
+                       "        \"maximum\": {\r\n"
+                       "            \"type\": \"number\"\r\n"
+                       "        },\r\n"
+                       "        \"exclusiveMaximum\": {\r\n"
+                       "            \"type\": \"number\"\r\n"
+                       "        },\r\n"
+                       "        \"minimum\": {\r\n"
+                       "            \"type\": \"number\"\r\n"
+                       "        },\r\n"
+                       "        \"exclusiveMinimum\": {\r\n"
+                       "            \"type\": \"number\"\r\n"
+                       "        },\r\n"
+                       "        \"maxLength\": { \"$ref\": \"#/definitions/nonNegativeInteger\" },\r\n"
+                       "        \"minLength\": { \"$ref\": \"#/definitions/nonNegativeIntegerDefault0\" },\r\n"
+                       "        \"pattern\": {\r\n"
+                       "            \"type\": \"string\",\r\n"
+                       "            \"format\": \"regex\"\r\n"
+                       "        },\r\n"
+                       "        \"additionalItems\": { \"$ref\": \"#\" },\r\n"
+                       "        \"items\": {\r\n"
+                       "            \"anyOf\": [\r\n"
+                       "                { \"$ref\": \"#\" },\r\n"
+                       "                { \"$ref\": \"#/definitions/schemaArray\" }\r\n"
+                       "            ],\r\n"
+                       "            \"default\": {}\r\n"
+                       "        },\r\n"
+                       "        \"maxItems\": { \"$ref\": \"#/definitions/nonNegativeInteger\" },\r\n"
+                       "        \"minItems\": { \"$ref\": \"#/definitions/nonNegativeIntegerDefault0\" },\r\n"
+                       "        \"uniqueItems\": {\r\n"
+                       "            \"type\": \"boolean\",\r\n"
+                       "            \"default\": false\r\n"
+                       "        },\r\n"
+                       "        \"contains\": { \"$ref\": \"#\" },\r\n"
+                       "        \"maxProperties\": { \"$ref\": \"#/definitions/nonNegativeInteger\" },\r\n"
+                       "        \"minProperties\": { \"$ref\": \"#/definitions/nonNegativeIntegerDefault0\" },\r\n"
+                       "        \"required\": { \"$ref\": \"#/definitions/stringArray\" },\r\n"
+                       "        \"additionalProperties\": { \"$ref\": \"#\" },\r\n"
+                       "        \"definitions\": {\r\n"
+                       "            \"type\": \"object\",\r\n"
+                       "            \"additionalProperties\": { \"$ref\": \"#\" },\r\n"
+                       "            \"default\": {}\r\n"
+                       "        },\r\n"
+                       "        \"properties\": {\r\n"
+                       "            \"type\": \"object\",\r\n"
+                       "            \"additionalProperties\": { \"$ref\": \"#\" },\r\n"
+                       "            \"default\": {}\r\n"
+                       "        },\r\n"
+                       "        \"patternProperties\": {\r\n"
+                       "            \"type\": \"object\",\r\n"
+                       "            \"additionalProperties\": { \"$ref\": \"#\" },\r\n"
+                       "            \"default\": {}\r\n"
+                       "        },\r\n"
+                       "        \"dependencies\": {\r\n"
+                       "            \"type\": \"object\",\r\n"
+                       "            \"additionalProperties\": {\r\n"
+                       "                \"anyOf\": [\r\n"
+                       "                    { \"$ref\": \"#\" },\r\n"
+                       "                    { \"$ref\": \"#/definitions/stringArray\" }\r\n"
+                       "                ]\r\n"
+                       "            }\r\n"
+                       "        },\r\n"
+                       "        \"propertyNames\": { \"$ref\": \"#\" },\r\n"
+                       "        \"const\": {},\r\n"
+                       "        \"enum\": {\r\n"
+                       "            \"type\": \"array\",\r\n"
+                       "            \"minItems\": 1,\r\n"
+                       "            \"uniqueItems\": true\r\n"
+                       "        },\r\n"
+                       "        \"type\": {\r\n"
+                       "            \"anyOf\": [\r\n"
+                       "                { \"$ref\": \"#/definitions/simpleTypes\" },\r\n"
+                       "                {\r\n"
+                       "                    \"type\": \"array\",\r\n"
+                       "                    \"items\": { \"$ref\": \"#/definitions/simpleTypes\" },\r\n"
+                       "                    \"minItems\": 1,\r\n"
+                       "                    \"uniqueItems\": true\r\n"
+                       "                }\r\n"
+                       "            ]\r\n"
+                       "        },\r\n"
+                       "        \"format\": { \"type\": \"string\" },\r\n"
+                       "        \"allOf\": { \"$ref\": \"#/definitions/schemaArray\" },\r\n"
+                       "        \"anyOf\": { \"$ref\": \"#/definitions/schemaArray\" },\r\n"
+                       "        \"oneOf\": { \"$ref\": \"#/definitions/schemaArray\" },\r\n"
+                       "        \"not\": { \"$ref\": \"#\" }\r\n"
+                       "    },\r\n"
+                       "    \"default\": {}\r\n"
+                       "}\r\n",
+               .output =
+                       "{\"$schema\":\"http://json-schema.org/draft-06/schema#\","
+                       "\"$id\":\"http://json-schema.org/draft-06/schema#\","
+                       "\"title\":\"Core schema meta-schema\",\"definitions\":{"
+                       "\"schemaArray\":{\"type\":\"array\",\"minItems\":1,"
+                       "\"items\":{\"$ref\":\"#\"}},\"nonNegativeInteger\":{"
+                       "\"type\":\"integer\",\"minimum\":0},"
+                       "\"nonNegativeIntegerDefault0\":{\"allOf\":["
+                       "{\"$ref\":\"#/definitions/nonNegativeInteger\"},"
+                       "{\"default\":0}]},\"simpleTypes\":{\"enum\":["
+                       "\"array\",\"boolean\",\"integer\",\"null\","
+                       "\"number\",\"object\",\"string\"]},\"stringArray\":{"
+                       "\"type\":\"array\",\"items\":{\"type\":\"string\"},"
+                       "\"uniqueItems\":true,\"default\":[]}},"
+                       "\"type\":[\"object\",\"boolean\"],"
+                       "\"properties\":{\"$id\":{\"type\":\"string\","
+                       "\"format\":\"uri-reference\"},\"$schema\":{"
+                       "\"type\":\"string\",\"format\":\"uri\"},"
+                       "\"$ref\":{\"type\":\"string\",\"format\":\"uri-reference\""
+                       "},\"title\":{\"type\":\"string\"},\"description\":{"
+                       "\"type\":\"string\"},\"default\":{},\"multipleOf\":{"
+                       "\"type\":\"number\",\"exclusiveMinimum\":0},"
+                       "\"maximum\":{\"type\":\"number\"},\"exclusiveMaximum\":{"
+                       "\"type\":\"number\"},\"minimum\":{\"type\":\"number\""
+                       "},\"exclusiveMinimum\":{\"type\":\"number\"},"
+                       "\"maxLength\":{\"$ref\":\"#/definitions/nonNegativeInteger\"},"
+                       "\"minLength\":{\"$ref\":\"#/definitions/nonNegativeIntegerDefault0\"},"
+                       "\"pattern\":{\"type\":\"string\",\"format\":\"regex\""
+                       "},\"additionalItems\":{\"$ref\":\"#\"},\"items\":{"
+                       "\"anyOf\":[{\"$ref\":\"#\"},{\"$ref\":\"#/definitions/schemaArray\"}"
+                       "],\"default\":{}},"
+                       "\"maxItems\":{\"$ref\":\"#/definitions/nonNegativeInteger\"},"
+                       "\"minItems\":{\"$ref\":\"#/definitions/nonNegativeIntegerDefault0\"},"
+                       "\"uniqueItems\":{\"type\":\"boolean\",\"default\":false},"
+                       "\"contains\":{\"$ref\":\"#\"},"
+                       "\"maxProperties\":{\"$ref\":\"#/definitions/nonNegativeInteger\"},"
+                       "\"minProperties\":{\"$ref\":\"#/definitions/nonNegativeIntegerDefault0\"},"
+                       "\"required\":{\"$ref\":\"#/definitions/stringArray\"},"
+                       "\"additionalProperties\":{\"$ref\":\"#\"},\"definitions\":{"
+                       "\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#\"},"
+                       "\"default\":{}},\"properties\":{\"type\":\"object\","
+                       "\"additionalProperties\":{\"$ref\":\"#\"},\"default\":{}"
+                       "},\"patternProperties\":{\"type\":\"object\","
+                       "\"additionalProperties\":{\"$ref\":\"#\"},"
+                       "\"default\":{}},\"dependencies\":{\"type\":\"object\","
+                       "\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#\"},"
+                       "{\"$ref\":\"#/definitions/stringArray\"}"
+                       "]}},\"propertyNames\":{\"$ref\":\"#\"},\"const\":{},"
+                       "\"enum\":{\"type\":\"array\",\"minItems\":1,\"uniqueItems\":true"
+                       "},\"type\":{\"anyOf\":[{\"$ref\":\"#/definitions/simpleTypes\"},"
+                       "{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/simpleTypes\"},"
+                       "\"minItems\":1,\"uniqueItems\":true}]},\"format\":{\"type\":\"string\"},"
+                       "\"allOf\":{\"$ref\":\"#/definitions/schemaArray\"},"
+                       "\"anyOf\":{\"$ref\":\"#/definitions/schemaArray\"},"
+                       "\"oneOf\":{\"$ref\":\"#/definitions/schemaArray\"},"
+                       "\"not\":{\"$ref\":\"#\"}},\"default\":{}}"
+       }
+};
+
+static const unsigned tests_count = N_ELEMENTS(tests);
+
+static void test_json_tree_stream_io(void)
+{
+       string_t *outbuf;
+       unsigned int i;
+
+       outbuf = str_new(default_pool, 1024);
+
+       for (i = 0; i < tests_count; i++) T_BEGIN {
+               const struct json_io_test *test;
+               const char *text, *text_out;
+               unsigned int pos, text_len;
+               struct istream *input;
+               struct ostream *output;
+               struct json_istream *jinput;
+               struct json_ostream *joutput;
+               struct json_tree *jtree;
+               const char *error = NULL;
+               int ret = 0;
+
+               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 tree stream io [%d]", i));
+
+               buffer_set_used_size(outbuf, 0);
+
+               input = test_istream_create_data(text, text_len);
+               output = o_stream_create_buffer(outbuf);
+               o_stream_set_no_error_handling(output, TRUE);
+
+               jinput = json_istream_create(input, 0, NULL, 0);
+               joutput = json_ostream_create(output, 0);
+
+               o_stream_set_max_buffer_size(output, 0);
+               ret = 0;
+               for (pos = 0; pos <= text_len && ret == 0; pos++) {
+                       test_istream_set_size(input, pos);
+                       ret = json_istream_read_tree(jinput, &jtree);
+               }
+               test_assert(ret > 0);
+
+               test_istream_set_size(input, text_len);
+               ret = json_istream_finish(&jinput, &error);
+               test_out_reason_quiet("input stream ok (trickle)",
+                                     ret > 0, error);
+
+               ret = 0;
+               for (pos = 0;   pos <= 65535 && ret == 0; pos++) {
+                       o_stream_set_max_buffer_size(output, pos);
+                       if (jtree != NULL) {
+                               ret = json_ostream_write_tree(joutput, NULL, jtree);
+                               if (ret > 0)
+                                       json_tree_unref(&jtree);
+                       }
+                       if (jtree == NULL)
+                               ret = json_ostream_flush(joutput);
+               }
+               json_ostream_unref(&joutput);
+               test_out_quiet("output stream ok (trickle)", ret > 0);
+
+               test_out_quiet("io match (trickle)",
+                              strcmp(text_out, str_c(outbuf)) == 0);
+
+               if (debug) {
+                       i_debug("OUT: >%s<", text_out);
+                       i_debug("OUT: >%s<", str_c(outbuf));
+               }
+
+               json_tree_unref(&jtree);
+               i_stream_unref(&input);
+               o_stream_unref(&output);
+
+               buffer_set_used_size(outbuf, 0);
+
+               input = test_istream_create_data(text, text_len);
+               output = o_stream_create_buffer(outbuf);
+               o_stream_set_no_error_handling(output, TRUE);
+
+               jinput = json_istream_create(input, 0, NULL, 0);
+               joutput = json_ostream_create(output, 0);
+
+               ret = json_istream_read_tree(jinput, &jtree);
+               test_assert(ret > 0);
+
+               ret = json_istream_finish(&jinput, &error);
+               test_out_reason_quiet("input stream ok (buffer)",
+                                     ret > 0, error);
+
+               if (jtree != NULL) {
+                       ret = json_ostream_write_tree(joutput, NULL, jtree);
+                       if (ret > 0) {
+                               json_tree_unref(&jtree);
+                               ret = json_ostream_flush(joutput);
+                       }
+               }
+               json_ostream_unref(&joutput);
+               test_out_quiet("output stream ok (buffer)", ret > 0);
+
+               test_out_quiet("io match (buffer)",
+                              strcmp(text_out, str_c(outbuf)) == 0);
+
+               if (debug) {
+                       i_debug("OUT: >%s<", text_out);
+                       i_debug("OUT: >%s<", str_c(outbuf));
+               }
+
+               json_tree_unref(&jtree);
+               i_stream_unref(&input);
+               o_stream_unref(&output);
+
+               test_end();
+
+       } T_END;
+
+       buffer_free(&outbuf);
+}
+
+static void test_json_tree_file(const char *file)
+{
+       struct istream *input;
+       struct ostream *output;
+       struct json_istream *jinput;
+       struct json_ostream *joutput;
+       struct json_tree *jtree;
+       int ret = 0;
+
+       input = i_stream_create_file(file, 1024);
+       output = o_stream_create_fd(1, 1024);
+       o_stream_set_no_error_handling(output, TRUE);
+
+       jinput = json_istream_create(input, 0, NULL,
+               JSON_PARSER_FLAG_NUMBERS_AS_STRING);
+       joutput = json_ostream_create(output, 0);
+
+       ret = 0;
+       while (ret == 0)
+               ret = json_istream_read_tree(jinput, &jtree);
+
+       if (ret < 0) {
+               i_fatal("Failed to read JSON: %s",
+                       json_istream_get_error(jinput));
+       }
+
+       ret = 0;
+       while (ret == 0) {
+               if (jtree != NULL) {
+                       ret = json_ostream_write_tree(joutput, NULL, jtree);
+                       if (ret > 0)
+                               json_tree_unref(&jtree);
+               }
+               if (jtree == NULL)
+                       ret = json_ostream_flush(joutput);
+       }
+
+       if (ret < 0) {
+               i_fatal("Failed to write JSON: %s",
+                       o_stream_get_error(output));
+       }
+
+       json_istream_unref(&jinput);
+       json_ostream_unref(&joutput);
+
+       o_stream_nsend_str(output, "\n");
+       i_stream_unref(&input);
+       o_stream_unref(&output);
+}
+
+int main(int argc, char *argv[])
+{
+       int c;
+
+       static void (*test_functions[])(void) = {
+               test_json_tree_stream_io,
+               NULL
+       };
+
+       while ((c = getopt(argc, argv, "D")) > 0) {
+               switch (c) {
+               case 'D':
+                       debug = TRUE;
+                       break;
+               default:
+                       i_fatal("Usage: %s [-D]", argv[0]);
+               }
+       }
+       argc -= optind;
+       argv += optind;
+
+       if (argc > 0) {
+               test_json_tree_file(argv[0]);
+               return 0;
+       }
+
+       return test_run(test_functions);
+}