From 656cc19fc7de25c3ac7f9d847c37745ccc272247 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Tue, 20 Nov 2018 02:47:36 +0100 Subject: [PATCH] Replace JSON-GLib by custom JSON dumper library The (optional) JSON-GLib library adds dependencies on GObject, GIO. For statically linked oss-fuzz builds it also adds libffi and more. To avoid these dependencies, replace JSON-GLib by some custom code. This allows `tshark -G elastic-mapping` to be enabled by default without extra deps. API design goals of the new JSON dumper library: - Small interface without a lot of abstraction. - Avoid memory allocations if possible (currently none, but maybe json_puts_string will be replaced to improve UTF-8 support). - Do not implement parsing, this is currently handled by jsmn. Methods to open/close array/objects and to set members are inspired by the JsonGlib interface. The interfaces to write values is inspired by the sharkd code (json_puts_string is also borrowed from that). The only observed differences in the tshark output: - JSON-GLib ignores duplicates, json_dumper does not and may produce duplicates and currently print two "ip.opt.sec_prot_auth_unassigned". - JSON-GLib adds a space before a colon (unimportant formatting detail). - (Not observed, but UTF-8 strings will be wrong like bug 14948.) A test was added to catch changes in the tshark output. I also fuzzed json_dumper with libFuzzer + UBSAN/ASAN and fixed an off-by-one error. Change-Id: I0c85b18777b04d1e0f613a3d59935ec59be87ff4 Link: https://www.wireshark.org/lists/wireshark-dev/201811/msg00052.html Reviewed-on: https://code.wireshark.org/review/30732 Petri-Dish: Peter Wu Tested-by: Petri Dish Buildbot Reviewed-by: Anders Broman --- debian/libwsutil0.symbols | 8 + epan/proto.c | 140 ++++----- epan/proto.h | 2 - test/baseline/elastic-mapping-ip-subset.json | 45 +++ test/suite_clopts.py | 17 ++ tshark.c | 14 - wsutil/CMakeLists.txt | 2 + wsutil/json_dumper.c | 285 +++++++++++++++++++ wsutil/json_dumper.h | 107 +++++++ 9 files changed, 525 insertions(+), 95 deletions(-) create mode 100644 test/baseline/elastic-mapping-ip-subset.json create mode 100644 wsutil/json_dumper.c create mode 100644 wsutil/json_dumper.h diff --git a/debian/libwsutil0.symbols b/debian/libwsutil0.symbols index e5b9e771bc..791decb8fe 100644 --- a/debian/libwsutil0.symbols +++ b/debian/libwsutil0.symbols @@ -101,6 +101,14 @@ libwsutil.so.0 libwsutil0 #MINVER# isprint_string@Base 1.10.0 isprint_utf8_string@Base 2.6.1 json_decode_string_inplace@Base 2.9.0 + json_dumper_begin_array@Base 2.9.0 + json_dumper_begin_object@Base 2.9.0 + json_dumper_end_array@Base 2.9.0 + json_dumper_end_object@Base 2.9.0 + json_dumper_finish@Base 2.9.0 + json_dumper_set_member_name@Base 2.9.0 + json_dumper_value_anyf@Base 2.9.0 + json_dumper_value_string@Base 2.9.0 json_parse@Base 2.9.0 json_validate@Base 2.9.0 linear2alaw@Base 1.12.0~rc1 diff --git a/epan/proto.c b/epan/proto.c index c31f3d095e..9b73ab0bc7 100644 --- a/epan/proto.c +++ b/epan/proto.c @@ -20,6 +20,7 @@ #include #include #include +#include #include @@ -47,10 +48,6 @@ #include /* ws_debug_printf */ #include -#ifdef HAVE_JSONGLIB -#include -#endif - /* Ptvcursor limits */ #define SUBTREE_ONCE_ALLOCATION_NUMBER 8 #define SUBTREE_MAX_LEVELS 256 @@ -10263,21 +10260,17 @@ proto_registrar_dump_fieldcount(void) return (gpa_hfinfo.allocated_len > PROTO_PRE_ALLOC_HF_FIELDS_MEM); } -#ifdef HAVE_JSONGLIB - -static JsonBuilder* -elastic_add_base_mapping(JsonBuilder* builder) +static void +elastic_add_base_mapping(json_dumper *dumper) { - json_builder_set_member_name(builder, "template"); - json_builder_add_string_value(builder, "packets-*"); + json_dumper_set_member_name(dumper, "template"); + json_dumper_value_string(dumper, "packets-*"); - json_builder_set_member_name(builder, "settings"); - json_builder_begin_object(builder); - json_builder_set_member_name(builder, "index.mapping.total_fields.limit"); - json_builder_add_int_value(builder, 1000000); - json_builder_end_object(builder); - - return builder; + json_dumper_set_member_name(dumper, "settings"); + json_dumper_begin_object(dumper); + json_dumper_set_member_name(dumper, "index.mapping.total_fields.limit"); + json_dumper_value_anyf(dumper, "%d", 1000000); + json_dumper_end_object(dumper); } gchar* ws_type_to_elastic(guint type _U_) @@ -10358,14 +10351,9 @@ proto_registrar_dump_elastic(const gchar* filter) { header_field_info *hfinfo; header_field_info *parent_hfinfo; - JsonGenerator* generator; - JsonBuilder* builder; - JsonNode* root; - gsize length; guint i; gboolean open_object = TRUE; const char* prev_proto = NULL; - gchar* data; gchar* str; gchar** protos = NULL; gchar* proto; @@ -10382,30 +10370,33 @@ proto_registrar_dump_elastic(const gchar* filter) * n.label -> where n is the indentation level and label the name of the object */ - builder = json_builder_new(); - json_builder_begin_object(builder); // 1.root - builder = elastic_add_base_mapping(builder); - - json_builder_set_member_name(builder, "mappings"); - json_builder_begin_object(builder); // 2.mappings - json_builder_set_member_name(builder, "pcap_file"); - - json_builder_begin_object(builder); // 3.pcap_file - json_builder_set_member_name(builder, "dynamic"); - json_builder_add_boolean_value(builder, FALSE); - - json_builder_set_member_name(builder, "properties"); - json_builder_begin_object(builder); // 4.properties - json_builder_set_member_name(builder, "timestamp"); - json_builder_begin_object(builder); // 5.timestamp - json_builder_set_member_name(builder, "type"); - json_builder_add_string_value(builder, "date"); - json_builder_end_object(builder); // 5.timestamp - - json_builder_set_member_name(builder, "layers"); - json_builder_begin_object(builder); // 5.layers - json_builder_set_member_name(builder, "properties"); - json_builder_begin_object(builder); // 6.properties + json_dumper dumper = { + .output_file = stdout, + .flags = JSON_DUMPER_FLAGS_PRETTY_PRINT, + }; + json_dumper_begin_object(&dumper); // 1.root + elastic_add_base_mapping(&dumper); + + json_dumper_set_member_name(&dumper, "mappings"); + json_dumper_begin_object(&dumper); // 2.mappings + json_dumper_set_member_name(&dumper, "pcap_file"); + + json_dumper_begin_object(&dumper); // 3.pcap_file + json_dumper_set_member_name(&dumper, "dynamic"); + json_dumper_value_anyf(&dumper, "false"); + + json_dumper_set_member_name(&dumper, "properties"); + json_dumper_begin_object(&dumper); // 4.properties + json_dumper_set_member_name(&dumper, "timestamp"); + json_dumper_begin_object(&dumper); // 5.timestamp + json_dumper_set_member_name(&dumper, "type"); + json_dumper_value_string(&dumper, "date"); + json_dumper_end_object(&dumper); // 5.timestamp + + json_dumper_set_member_name(&dumper, "layers"); + json_dumper_begin_object(&dumper); // 5.layers + json_dumper_set_member_name(&dumper, "properties"); + json_dumper_begin_object(&dumper); // 6.properties for (i = 0; i < gpa_hfinfo.len; i++) { if (gpa_hfinfo.hfi[i] == NULL) @@ -10444,55 +10435,46 @@ proto_registrar_dump_elastic(const gchar* filter) } if (prev_proto && g_strcmp0(parent_hfinfo->abbrev, prev_proto)) { - json_builder_end_object(builder); // 8.properties - json_builder_end_object(builder); // 7.parent_hfinfo->abbrev + json_dumper_end_object(&dumper); // 8.properties + json_dumper_end_object(&dumper); // 7.parent_hfinfo->abbrev open_object = TRUE; } prev_proto = parent_hfinfo->abbrev; if (open_object) { - json_builder_set_member_name(builder, parent_hfinfo->abbrev); - json_builder_begin_object(builder); // 7.parent_hfinfo->abbrev - json_builder_set_member_name(builder, "properties"); - json_builder_begin_object(builder); // 8.properties + json_dumper_set_member_name(&dumper, parent_hfinfo->abbrev); + json_dumper_begin_object(&dumper); // 7.parent_hfinfo->abbrev + json_dumper_set_member_name(&dumper, "properties"); + json_dumper_begin_object(&dumper); // 8.properties open_object = FALSE; } str = g_strdup(hfinfo->abbrev); - json_builder_set_member_name(builder, dot_to_underscore(str)); + json_dumper_set_member_name(&dumper, dot_to_underscore(str)); g_free(str); - json_builder_begin_object(builder); // 9.hfinfo->abbrev - json_builder_set_member_name(builder, "type"); - json_builder_add_string_value(builder, ws_type_to_elastic(hfinfo->type)); - json_builder_end_object(builder); // 9.hfinfo->abbrev + json_dumper_begin_object(&dumper); // 9.hfinfo->abbrev + json_dumper_set_member_name(&dumper, "type"); + json_dumper_value_string(&dumper, ws_type_to_elastic(hfinfo->type)); + json_dumper_end_object(&dumper); // 9.hfinfo->abbrev } } if (prev_proto) { - json_builder_end_object(builder); // 8.properties - json_builder_end_object(builder); // 7.parent_hfinfo->abbrev - } - - json_builder_end_object(builder); // 6.properties - json_builder_end_object(builder); // 5.layers - json_builder_end_object(builder); // 4.properties - json_builder_end_object(builder); // 3.pcap_file - json_builder_end_object(builder); // 2.mappings - DISSECTOR_ASSERT(json_builder_end_object(builder)); // 1.root - - generator = json_generator_new(); - json_generator_set_pretty(generator, TRUE); - root = json_builder_get_root(builder); - json_generator_set_root(generator, root); - json_node_free(root); - g_object_unref(builder); - data = json_generator_to_data(generator, &length); - g_object_unref(generator); - ws_debug_printf("%s\n", data); - g_free(data); + json_dumper_end_object(&dumper); // 8.properties + json_dumper_end_object(&dumper); // 7.parent_hfinfo->abbrev + } + + json_dumper_end_object(&dumper); // 6.properties + json_dumper_end_object(&dumper); // 5.layers + json_dumper_end_object(&dumper); // 4.properties + json_dumper_end_object(&dumper); // 3.pcap_file + json_dumper_end_object(&dumper); // 2.mappings + json_dumper_end_object(&dumper); // 1.root + gboolean ret = json_dumper_finish(&dumper); + DISSECTOR_ASSERT(ret); + g_strfreev(protos); } -#endif /* Dumps the contents of the registration database to stdout. An independent * program can take this output and format it into nice tables or HTML or diff --git a/epan/proto.h b/epan/proto.h index dbcbb2aeee..afe8dae6e2 100644 --- a/epan/proto.h +++ b/epan/proto.h @@ -2479,10 +2479,8 @@ WS_DLL_PUBLIC void proto_registrar_dump_protocols(void); /** Dumps a glossary of the field value strings or true/false strings to STDOUT */ WS_DLL_PUBLIC void proto_registrar_dump_values(void); -#ifdef HAVE_JSONGLIB /** Dumps a mapping file for loading tshark output into ElasticSearch */ WS_DLL_PUBLIC void proto_registrar_dump_elastic(const gchar* filter); -#endif /** Dumps the number of protocol and field registrations to STDOUT. @return FALSE if we pre-allocated enough fields, TRUE otherwise. */ diff --git a/test/baseline/elastic-mapping-ip-subset.json b/test/baseline/elastic-mapping-ip-subset.json new file mode 100644 index 0000000000..49333ffc01 --- /dev/null +++ b/test/baseline/elastic-mapping-ip-subset.json @@ -0,0 +1,45 @@ +{ + "template" : "packets-*", + "settings" : { + "index.mapping.total_fields.limit" : 1000000 + }, + "mappings" : { + "pcap_file" : { + "dynamic" : false, + "properties" : { + "timestamp" : { + "type" : "date" + }, + "layers" : { + "properties" : { + "ip" : { + "properties" : { + "ip_version" : { + "type" : "short" + }, + "ip_tos_delay" : { + "type" : "boolean" + }, + "ip_len" : { + "type" : "integer" + }, + "ip_dst" : { + "type" : "ip" + }, + "ip_dst_host" : { + "type" : "string" + }, + "ip_geoip_lat" : { + "type" : "float" + }, + "ip_opt_padding" : { + "type" : "byte" + } + } + } + } + } + } + } + } +} diff --git a/test/suite_clopts.py b/test/suite_clopts.py index 722769b08d..4a0d7b10ce 100644 --- a/test/suite_clopts.py +++ b/test/suite_clopts.py @@ -9,6 +9,8 @@ # '''Command line option tests''' +import json +import os.path import subprocess import subprocesstest import fixtures @@ -181,6 +183,21 @@ class case_tshark_dump_glossaries(subprocesstest.SubprocessTestCase): self.runProcess((cmd_tshark, '-G', 'plugins'), env=base_env) self.assertGreaterEqual(self.countOutput('dissector'), 10, 'Fewer than 10 dissector plugins found') + def test_tshark_elastic_mapping(self, cmd_tshark, dirs, base_env): + def get_ip_props(obj): + return obj['mappings']['pcap_file']['properties']['layers']['properties']['ip']['properties'] + baseline_file = os.path.join(dirs.baseline_dir, 'elastic-mapping-ip-subset.json') + with open(baseline_file) as f: + expected_obj = json.load(f) + keys_to_check = get_ip_props(expected_obj).keys() + proc = self.assertRun((cmd_tshark, '-G', 'elastic-mapping', '--elastic-mapping-filter', 'ip')) + actual_obj = json.loads(proc.stdout_str) + ip_props = get_ip_props(actual_obj) + for key in list(ip_props.keys()): + if key not in keys_to_check: + del ip_props[key] + self.assertEqual(actual_obj, expected_obj) + @fixtures.mark_usefixtures('test_env') @fixtures.uses_fixtures diff --git a/tshark.c b/tshark.c index 7d97cdaa2f..d2b1e59dd7 100644 --- a/tshark.c +++ b/tshark.c @@ -140,9 +140,7 @@ */ #define LONGOPT_COLOR (65536+1000) #define LONGOPT_NO_DUPLICATE_KEYS (65536+1001) -#ifdef HAVE_JSONGLIB #define LONGOPT_ELASTIC_MAPPING_FILTER (65536+1002) -#endif #if 0 #define tshark_debug(...) g_warning(__VA_ARGS__) @@ -439,10 +437,8 @@ print_usage(FILE *output) fprintf(output, " --no-duplicate-keys If -T json is specified, merge duplicate keys in an object\n"); fprintf(output, " into a single key with as value a json array containing all\n"); fprintf(output, " values\n"); -#ifdef HAVE_JSONGLIB fprintf(output, " --elastic-mapping-filter If -G elastic-mapping is specified, put only the\n"); fprintf(output, " specified protocols within the mapping file\n"); -#endif fprintf(output, "\n"); fprintf(output, "Miscellaneous:\n"); @@ -479,9 +475,7 @@ glossary_option_help(void) fprintf(output, " -G column-formats dump column format codes and exit\n"); fprintf(output, " -G decodes dump \"layer type\"/\"decode as\" associations and exit\n"); fprintf(output, " -G dissector-tables dump dissector table names, types, and properties\n"); -#ifdef HAVE_JSONGLIB fprintf(output, " -G elastic-mapping dump ElasticSearch mapping file\n"); -#endif fprintf(output, " -G fieldcount dump count of header fields and exit\n"); fprintf(output, " -G fields dump fields glossary and exit\n"); fprintf(output, " -G ftypes dump field type basic and descriptive names\n"); @@ -683,9 +677,7 @@ real_main(int argc, char *argv[]) {"export-objects", required_argument, NULL, LONGOPT_EXPORT_OBJECTS}, {"color", no_argument, NULL, LONGOPT_COLOR}, {"no-duplicate-keys", no_argument, NULL, LONGOPT_NO_DUPLICATE_KEYS}, -#ifdef HAVE_JSONGLIB {"elastic-mapping-filter", required_argument, NULL, LONGOPT_ELASTIC_MAPPING_FILTER}, -#endif {0, 0, 0, 0 } }; gboolean arg_error = FALSE; @@ -729,9 +721,7 @@ real_main(int argc, char *argv[]) gchar *volatile pdu_export_arg = NULL; char *volatile exp_pdu_filename = NULL; exp_pdu_t exp_pdu_tap_data; -#ifdef HAVE_JSONGLIB const gchar* elastic_mapping_filter = NULL; -#endif /* * The leading + ensures that getopt_long() does not permute the argv[] @@ -866,11 +856,9 @@ real_main(int argc, char *argv[]) case 'X': ex_opt_add(optarg); break; -#ifdef HAVE_JSONGLIB case LONGOPT_ELASTIC_MAPPING_FILTER: elastic_mapping_filter = optarg; break; -#endif default: break; } @@ -973,10 +961,8 @@ real_main(int argc, char *argv[]) write_prefs(NULL); else if (strcmp(argv[2], "dissector-tables") == 0) dissector_dump_dissector_tables(); -#ifdef HAVE_JSONGLIB else if (strcmp(argv[2], "elastic-mapping") == 0) proto_registrar_dump_elastic(elastic_mapping_filter); -#endif else if (strcmp(argv[2], "fieldcount") == 0) { /* return value for the test suite */ exit_status = proto_registrar_dump_fieldcount(); diff --git a/wsutil/CMakeLists.txt b/wsutil/CMakeLists.txt index dfc649932a..9af601854f 100644 --- a/wsutil/CMakeLists.txt +++ b/wsutil/CMakeLists.txt @@ -43,6 +43,7 @@ set(WSUTIL_PUBLIC_HEADERS inet_ipv6.h interface.h jsmn.h + json_dumper.h mpeg-audio.h netlink.h nstime.h @@ -99,6 +100,7 @@ set(WSUTIL_COMMON_FILES inet_addr.c interface.c jsmn.c + json_dumper.c mpeg-audio.c nstime.c cpu_info.c diff --git a/wsutil/json_dumper.c b/wsutil/json_dumper.c new file mode 100644 index 0000000000..ebac957617 --- /dev/null +++ b/wsutil/json_dumper.c @@ -0,0 +1,285 @@ +/* wsjson.h + * Routines for serializing data as JSON. + * + * Copyright 2018, Peter Wu + * + * Wireshark - Network traffic analyzer + * By Gerald Combs + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "json_dumper.h" + +/* + * json_dumper.state[current_depth] describes a nested element: + * - type: none/object/array/value + * - has_name: Whether the object member name was set. + */ +enum json_dumper_element_type { + JSON_DUMPER_TYPE_NONE = 0, + JSON_DUMPER_TYPE_VALUE = 1, + JSON_DUMPER_TYPE_OBJECT = 2, + JSON_DUMPER_TYPE_ARRAY = 3, +}; +#define JSON_DUMPER_TYPE(state) ((enum json_dumper_element_type)((state) & 3)) +#define JSON_DUMPER_HAS_NAME (1 << 2) + +#define JSON_DUMPER_FLAGS_ERROR (1 << 16) /* Output flag: an error occurred. */ + +enum json_dumper_change { + JSON_DUMPER_BEGIN, + JSON_DUMPER_END, + JSON_DUMPER_SET_NAME, + JSON_DUMPER_SET_VALUE, + JSON_DUMPER_FINISH, +}; + +static void +json_puts_string(FILE *fp, const char *str) +{ + static const char json_cntrl[0x20][6] = { + "u0000", "u0001", "u0002", "u0003", "u0004", "u0005", "u0006", "u0007", "b", "t", "n", "u000b", "f", "r", "u000e", "u000f", + "u0010", "u0011", "u0012", "u0013", "u0014", "u0015", "u0016", "u0017", "u0018", "u0019", "u001a", "u001b", "u001c", "u001d", "u001e", "u001f" + }; + + fputc('"', fp); + for (int i = 0; str[i]; i++) { + if ((guint)str[i] < 0x20) { + fputc('\\', fp); + fputs(json_cntrl[(guint)str[i]], fp); + } else { + if (str[i] == '\\' || str[i] == '"') { + fputc('\\', fp); + } + fputc(str[i], fp); + } + } + fputc('"', fp); +} + +/** + * Checks that the dumper state is valid for a new change. Any error will be + * sticky and prevent further dumps from succeeding. + */ +static gboolean +json_dumper_check_state(json_dumper *dumper, enum json_dumper_change change, enum json_dumper_element_type type) +{ + if ((dumper->flags & JSON_DUMPER_FLAGS_ERROR)) { + return FALSE; + } + + int depth = dumper->current_depth; + if (depth < 0 || depth >= JSON_DUMPER_MAX_DEPTH) { + /* Corrupted state, no point in continuing. */ + dumper->flags |= JSON_DUMPER_FLAGS_ERROR; + return FALSE; + } + + guint8 prev_state = depth > 0 ? dumper->state[depth - 1] : 0; + enum json_dumper_element_type prev_type = JSON_DUMPER_TYPE(prev_state); + + gboolean ok = FALSE; + switch (change) { + case JSON_DUMPER_BEGIN: + ok = depth + 1 < JSON_DUMPER_MAX_DEPTH; + break; + case JSON_DUMPER_END: + ok = prev_type == type && !(prev_state & JSON_DUMPER_HAS_NAME); + break; + case JSON_DUMPER_SET_NAME: + /* An object name can only be set once before a value is set. */ + ok = prev_type == JSON_DUMPER_TYPE_OBJECT && !(prev_state & JSON_DUMPER_HAS_NAME); + break; + case JSON_DUMPER_SET_VALUE: + if (prev_type == JSON_DUMPER_TYPE_OBJECT) { + ok = (prev_state & JSON_DUMPER_HAS_NAME); + } else if (prev_type == JSON_DUMPER_TYPE_ARRAY) { + ok = TRUE; + } else { + ok = JSON_DUMPER_TYPE(dumper->state[depth]) == JSON_DUMPER_TYPE_NONE; + } + break; + case JSON_DUMPER_FINISH: + ok = depth == 0; + break; + } + if (!ok) { + dumper->flags |= JSON_DUMPER_FLAGS_ERROR; + } + return ok; +} + +static void +print_newline_indent(const json_dumper *dumper, int depth) +{ + if ((dumper->flags & JSON_DUMPER_FLAGS_PRETTY_PRINT)) { + fputc('\n', dumper->output_file); + for (int i = 0; i < depth; i++) { + fputs(" ", dumper->output_file); + } + } +} + +/** + * Prints commas, newlines and indentation (if necessary). Used for array + * values, object names and normal values (strings, etc.). + */ +static void +prepare_token(json_dumper *dumper) +{ + if (dumper->current_depth == 0) { + // not part of an array or object. + return; + } + guint8 prev_state = dumper->state[dumper->current_depth - 1]; + + // While processing the object value, reset the key state as it is consumed. + dumper->state[dumper->current_depth - 1] &= ~JSON_DUMPER_HAS_NAME; + + switch (JSON_DUMPER_TYPE(prev_state)) { + case JSON_DUMPER_TYPE_OBJECT: + if ((prev_state & JSON_DUMPER_HAS_NAME)) { + // Object key already set, value follows. No indentation needed. + return; + } + break; + case JSON_DUMPER_TYPE_ARRAY: + break; + default: + // Initial values do not need indentation. + return; + } + + if (dumper->state[dumper->current_depth]) { + fputc(',', dumper->output_file); + } + print_newline_indent(dumper, dumper->current_depth); +} + +/** + * Common code to close an object/array, printing a closing character (and if + * necessary, it is preceded by newline and indentation). + */ +static void +finish_token(const json_dumper *dumper, char close_char) +{ + // if the object/array was non-empty, add a newline and indentation. + if (dumper->state[dumper->current_depth]) { + print_newline_indent(dumper, dumper->current_depth - 1); + } + fputc(close_char, dumper->output_file); +} + +void +json_dumper_begin_object(json_dumper *dumper) +{ + if (!json_dumper_check_state(dumper, JSON_DUMPER_BEGIN, JSON_DUMPER_TYPE_OBJECT)) { + return; + } + + prepare_token(dumper); + fputc('{', dumper->output_file); + + dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_OBJECT; + ++dumper->current_depth; + dumper->state[dumper->current_depth] = 0; +} + +void +json_dumper_set_member_name(json_dumper *dumper, const char *name) +{ + if (!json_dumper_check_state(dumper, JSON_DUMPER_SET_NAME, JSON_DUMPER_TYPE_NONE)) { + return; + } + + prepare_token(dumper); + json_puts_string(dumper->output_file, name); + fputc(':', dumper->output_file); + if ((dumper->flags & JSON_DUMPER_FLAGS_PRETTY_PRINT)) { + fputc(' ', dumper->output_file); + } + + dumper->state[dumper->current_depth - 1] |= JSON_DUMPER_HAS_NAME; +} + +void +json_dumper_end_object(json_dumper *dumper) +{ + if (!json_dumper_check_state(dumper, JSON_DUMPER_END, JSON_DUMPER_TYPE_OBJECT)) { + return; + } + + finish_token(dumper, '}'); + + --dumper->current_depth; +} + +void +json_dumper_begin_array(json_dumper *dumper) +{ + if (!json_dumper_check_state(dumper, JSON_DUMPER_BEGIN, JSON_DUMPER_TYPE_ARRAY)) { + return; + } + + prepare_token(dumper); + fputc('[', dumper->output_file); + + dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_ARRAY; + ++dumper->current_depth; + dumper->state[dumper->current_depth] = 0; +} + +void +json_dumper_end_array(json_dumper *dumper) +{ + if (!json_dumper_check_state(dumper, JSON_DUMPER_END, JSON_DUMPER_TYPE_ARRAY)) { + return; + } + + finish_token(dumper, ']'); + + --dumper->current_depth; +} + +void +json_dumper_value_string(json_dumper *dumper, const char *value) +{ + if (!json_dumper_check_state(dumper, JSON_DUMPER_SET_VALUE, JSON_DUMPER_TYPE_VALUE)) { + return; + } + + prepare_token(dumper); + json_puts_string(dumper->output_file, value); + + dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_VALUE; +} + +void +json_dumper_value_anyf(json_dumper *dumper, const char *format, ...) +{ + va_list ap; + if (!json_dumper_check_state(dumper, JSON_DUMPER_SET_VALUE, JSON_DUMPER_TYPE_VALUE)) { + return; + } + + prepare_token(dumper); + va_start(ap, format); + vfprintf(dumper->output_file, format, ap); + va_end(ap); + + dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_VALUE; +} + +gboolean +json_dumper_finish(json_dumper *dumper) +{ + if (!json_dumper_check_state(dumper, JSON_DUMPER_FINISH, JSON_DUMPER_TYPE_NONE)) { + return FALSE; + } + + fputc('\n', dumper->output_file); + dumper->state[0] = 0; + return TRUE; +} diff --git a/wsutil/json_dumper.h b/wsutil/json_dumper.h new file mode 100644 index 0000000000..98ce99a3e7 --- /dev/null +++ b/wsutil/json_dumper.h @@ -0,0 +1,107 @@ +/* wsjson.h + * Routines for serializing data as JSON. + * + * Copyright 2018, Peter Wu + * + * Wireshark - Network traffic analyzer + * By Gerald Combs + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef __JSON_DUMPER_H__ +#define __JSON_DUMPER_H__ + +#include "ws_symbol_export.h" +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Example: + * + * json_dumper dumper = { + * .output_file = stdout, + * .flags = JSON_DUMPER_FLAGS_PRETTY_PRINT, + * }; + * json_dumper_begin_object(&dumper); + * json_dumper_set_member_name(&dumper, "key"); + * json_dumper_value_string(&dumper, "value"); + * json_dumper_set_member_name(&dumper, "array"); + * json_dumper_begin_array(&dumper); + * json_dumper_value_anyf(&dumper, "true"); + * json_dumper_value_anyf(&dumper, "1.0"); + * json_dumper_begin_object(&dumper); + * json_dumper_end_object(&dumper); + * json_dumper_begin_array(&dumper); + * json_dumper_end_array(&dumper); + * json_dumper_end_array(&dumper); + * json_dumper_end_object(&dumper); + * json_dumper_finish(&dumper); + */ + +/** Maximum object/array nesting depth. */ +#define JSON_DUMPER_MAX_DEPTH 15 +typedef struct json_dumper { + FILE *output_file; /**< Output file, must be set. */ +#define JSON_DUMPER_FLAGS_PRETTY_PRINT (1 << 0) /* Enable pretty printing. */ + int flags; + /* for internal use, initialize with zeroes. */ + int current_depth; + guint8 state[JSON_DUMPER_MAX_DEPTH]; +} json_dumper; + +WS_DLL_PUBLIC void +json_dumper_begin_object(json_dumper *dumper); + +WS_DLL_PUBLIC void +json_dumper_set_member_name(json_dumper *dumper, const char *name); + +WS_DLL_PUBLIC void +json_dumper_end_object(json_dumper *dumper); + +WS_DLL_PUBLIC void +json_dumper_begin_array(json_dumper *dumper); + +WS_DLL_PUBLIC void +json_dumper_end_array(json_dumper *dumper); + +WS_DLL_PUBLIC void +json_dumper_value_string(json_dumper *dumper, const char *value); + +/** + * Dump number, "true", "false" or "null" values. + */ +WS_DLL_PUBLIC void +json_dumper_value_anyf(json_dumper *dumper, const char *format, ...) +G_GNUC_PRINTF(2, 3); + +/** + * Finishes dumping data. Returns TRUE if everything is okay and FALSE if + * something went wrong (open/close mismatch, missing values, etc.). + */ +WS_DLL_PUBLIC gboolean +json_dumper_finish(json_dumper *dumper); + +#ifdef __cplusplus +} +#endif + +#endif /* __JSON_DUMPER_H__ */ + +/* + * Editor modelines - https://www.wireshark.org/tools/modelines.html + * + * Local variables: + * c-basic-offset: 4 + * tab-width: 8 + * indent-tabs-mode: nil + * End: + * + * vi: set shiftwidth=4 tabstop=8 expandtab: + * :indentSize=4:tabSize=8:noTabs=true: + */ -- 2.34.1