diff options
author | Peter Wu <peter@lekensteyn.nl> | 2018-11-20 02:47:36 +0100 |
---|---|---|
committer | Anders Broman <a.broman58@gmail.com> | 2018-11-20 05:03:56 +0000 |
commit | 656cc19fc7de25c3ac7f9d847c37745ccc272247 (patch) | |
tree | 79de668a68c25fd5bfe8ac2ee36d5b18824fa653 /wsutil/json_dumper.c | |
parent | fb9c6905eff10f6f49a433ead8bd78e232fa068d (diff) |
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 <peter@lekensteyn.nl>
Tested-by: Petri Dish Buildbot
Reviewed-by: Anders Broman <a.broman58@gmail.com>
Diffstat (limited to 'wsutil/json_dumper.c')
-rw-r--r-- | wsutil/json_dumper.c | 285 |
1 files changed, 285 insertions, 0 deletions
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 <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; +} |