Replace JSON-GLib by custom JSON dumper library
authorPeter Wu <peter@lekensteyn.nl>
Tue, 20 Nov 2018 01:47:36 +0000 (02:47 +0100)
committerAnders Broman <a.broman58@gmail.com>
Tue, 20 Nov 2018 05:03:56 +0000 (05:03 +0000)
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 <peter@lekensteyn.nl>
Tested-by: Petri Dish Buildbot
Reviewed-by: Anders Broman <a.broman58@gmail.com>
debian/libwsutil0.symbols
epan/proto.c
epan/proto.h
test/baseline/elastic-mapping-ip-subset.json [new file with mode: 0644]
test/suite_clopts.py
tshark.c
wsutil/CMakeLists.txt
wsutil/json_dumper.c [new file with mode: 0644]
wsutil/json_dumper.h [new file with mode: 0644]

index e5b9e771bc15e100c021645fb61993642d3e051c..791decb8fe2995babfc7e6b5bf5c847c467c6632 100644 (file)
@@ -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
  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
  json_parse@Base 2.9.0
  json_validate@Base 2.9.0
  linear2alaw@Base 1.12.0~rc1
index c31f3d095eabddbdd4b6251fb814566f73a28af3..9b73ab0bc78f32fb65ae72bc09bda39578ce49e7 100644 (file)
@@ -20,6 +20,7 @@
 #include <wsutil/bits_count_ones.h>
 #include <wsutil/sign_ext.h>
 #include <wsutil/utf8_entities.h>
 #include <wsutil/bits_count_ones.h>
 #include <wsutil/sign_ext.h>
 #include <wsutil/utf8_entities.h>
+#include <wsutil/json_dumper.h>
 
 #include <ftypes/ftypes-int.h>
 
 
 #include <ftypes/ftypes-int.h>
 
 #include <wsutil/ws_printf.h> /* ws_debug_printf */
 #include <wsutil/crash_info.h>
 
 #include <wsutil/ws_printf.h> /* ws_debug_printf */
 #include <wsutil/crash_info.h>
 
-#ifdef HAVE_JSONGLIB
-#include <json-glib/json-glib.h>
-#endif
-
 /* Ptvcursor limits */
 #define SUBTREE_ONCE_ALLOCATION_NUMBER 8
 #define SUBTREE_MAX_LEVELS 256
 /* 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);
 }
 
        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_)
 }
 
 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;
 {
        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;
        guint i;
        gboolean open_object = TRUE;
        const char* prev_proto = NULL;
-       gchar* data;
        gchar* str;
        gchar** protos = NULL;
        gchar* proto;
        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
         */
 
         * 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)
 
        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)) {
                        }
 
                        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) {
                                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);
                                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);
                        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) {
                }
        }
 
        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);
 }
        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
 
 /* 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
index dbcbb2aeee38ff7deeb6677f1cb4a6d34d784eb7..afe8dae6e24eb02068a291aa65068edc36932011 100644 (file)
@@ -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);
 
 /** 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);
 /** 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. */
 
 /** 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 (file)
index 0000000..49333ff
--- /dev/null
@@ -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"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
index 722769b08d3df021dbde9c0d02aeaa17d1aeb45f..4a0d7b10ce8b8a97491a76b6346435fdec5bbfe4 100644 (file)
@@ -9,6 +9,8 @@
 #
 '''Command line option tests'''
 
 #
 '''Command line option tests'''
 
+import json
+import os.path
 import subprocess
 import subprocesstest
 import fixtures
 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')
 
         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
 
 @fixtures.mark_usefixtures('test_env')
 @fixtures.uses_fixtures
index 7d97cdaa2f50fcad5a0c3a147fbb836719db00ae..d2b1e59dd7e480c6462e07d5fd011f9e3ef89784 100644 (file)
--- a/tshark.c
+++ b/tshark.c
  */
 #define LONGOPT_COLOR (65536+1000)
 #define LONGOPT_NO_DUPLICATE_KEYS (65536+1001)
  */
 #define LONGOPT_COLOR (65536+1000)
 #define LONGOPT_NO_DUPLICATE_KEYS (65536+1001)
-#ifdef HAVE_JSONGLIB
 #define LONGOPT_ELASTIC_MAPPING_FILTER (65536+1002)
 #define LONGOPT_ELASTIC_MAPPING_FILTER (65536+1002)
-#endif
 
 #if 0
 #define tshark_debug(...) g_warning(__VA_ARGS__)
 
 #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");
   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 <protocols> If -G elastic-mapping is specified, put only the\n");
   fprintf(output, "                           specified protocols within the mapping file\n");
   fprintf(output, "  --elastic-mapping-filter <protocols> 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");
 
   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");
   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");
   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");
   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},
     {"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},
     {"elastic-mapping-filter", required_argument, NULL, LONGOPT_ELASTIC_MAPPING_FILTER},
-#endif
     {0, 0, 0, 0 }
   };
   gboolean             arg_error = FALSE;
     {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;
   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;
   const gchar*         elastic_mapping_filter = NULL;
-#endif
 
 /*
  * The leading + ensures that getopt_long() does not permute the argv[]
 
 /*
  * 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;
     case 'X':
       ex_opt_add(optarg);
       break;
-#ifdef HAVE_JSONGLIB
     case LONGOPT_ELASTIC_MAPPING_FILTER:
       elastic_mapping_filter = optarg;
       break;
     case LONGOPT_ELASTIC_MAPPING_FILTER:
       elastic_mapping_filter = optarg;
       break;
-#endif
     default:
       break;
     }
     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();
         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);
       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();
       else if (strcmp(argv[2], "fieldcount") == 0) {
         /* return value for the test suite */
         exit_status = proto_registrar_dump_fieldcount();
index dfc649932aa6d11b4ec934cf316a0b71d9b68b8f..9af601854f1a33bb7c0eb9017cb1c80b1c6a0a87 100644 (file)
@@ -43,6 +43,7 @@ set(WSUTIL_PUBLIC_HEADERS
        inet_ipv6.h
        interface.h
        jsmn.h
        inet_ipv6.h
        interface.h
        jsmn.h
+       json_dumper.h
        mpeg-audio.h
        netlink.h
        nstime.h
        mpeg-audio.h
        netlink.h
        nstime.h
@@ -99,6 +100,7 @@ set(WSUTIL_COMMON_FILES
        inet_addr.c
        interface.c
        jsmn.c
        inet_addr.c
        interface.c
        jsmn.c
+       json_dumper.c
        mpeg-audio.c
        nstime.c
        cpu_info.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 (file)
index 0000000..ebac957
--- /dev/null
@@ -0,0 +1,285 @@
+/* wsjson.h
+ * Routines for serializing data as JSON.
+ *
+ * Copyright 2018, Peter Wu <peter@lekensteyn.nl>
+ *
+ * Wireshark - Network traffic analyzer
+ * By Gerald Combs <gerald@wireshark.org>
+ * 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 (file)
index 0000000..98ce99a
--- /dev/null
@@ -0,0 +1,107 @@
+/* wsjson.h
+ * Routines for serializing data as JSON.
+ *
+ * Copyright 2018, Peter Wu <peter@lekensteyn.nl>
+ *
+ * Wireshark - Network traffic analyzer
+ * By Gerald Combs <gerald@wireshark.org>
+ * 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 <glib.h>
+#include <stdio.h>
+
+#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:
+ */