From e4ba6dbc3f1e76890b22773807ea37fe8fa2b1bc Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 22:34:10 +0200 Subject: Adding upstream version 4.2.2. Signed-off-by: Daniel Baumann --- wsutil/json_dumper.c | 689 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 689 insertions(+) create mode 100644 wsutil/json_dumper.c (limited to 'wsutil/json_dumper.c') diff --git a/wsutil/json_dumper.c b/wsutil/json_dumper.c new file mode 100644 index 00000000..5cbc9494 --- /dev/null +++ b/wsutil/json_dumper.c @@ -0,0 +1,689 @@ +/* json_dumper.c + * Routines for serializing data as JSON. + * + * Copyright 2018, Peter Wu + * Copyright (C) 2016 Jakub Zawadzki + * + * Wireshark - Network traffic analyzer + * By Gerald Combs + * Copyright 1998 Gerald Combs + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include + +#include "json_dumper.h" +#define WS_LOG_DOMAIN LOG_DOMAIN_WSUTIL + +#include + +#include + +/* + * json_dumper.state[current_depth] describes a nested element: + * - type: none/object/array/non-base64 value/base64 value + * - has_name: Whether the object member name was set. + * + * (A base64 value isn't really a nested element, but that's a + * convenient way of handling them, with a begin call that opens + * the string with a double-quote, one or more calls to convert + * raw bytes to base64 and add them to the value, and an end call + * that finishes the base64 encoding, adds any remaining raw bytes + * in base64 encoding, and closes the string with a double-quote.) + */ +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, + JSON_DUMPER_TYPE_BASE64 = 4, +}; +#define JSON_DUMPER_TYPE(state) ((enum json_dumper_element_type)((state) & 7)) +#define JSON_DUMPER_HAS_NAME (1 << 3) + +static const char *json_dumper_element_type_names[] = { + [JSON_DUMPER_TYPE_NONE] = "none", + [JSON_DUMPER_TYPE_VALUE] = "value", + [JSON_DUMPER_TYPE_OBJECT] = "object", + [JSON_DUMPER_TYPE_ARRAY] = "array", + [JSON_DUMPER_TYPE_BASE64] = "base64" +}; +#define NUM_JSON_DUMPER_ELEMENT_TYPE_NAMES (sizeof json_dumper_element_type_names / sizeof json_dumper_element_type_names[0]) + +#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_WRITE_BASE64, + JSON_DUMPER_FINISH, +}; + +/* JSON Dumper putc */ +static void +jd_putc(const json_dumper *dumper, char c) +{ + if (dumper->output_file) { + fputc(c, dumper->output_file); + } + + if (dumper->output_string) { + g_string_append_c(dumper->output_string, c); + } +} + +/* JSON Dumper puts */ +static void +jd_puts(const json_dumper *dumper, const char *s) +{ + if (dumper->output_file) { + fputs(s, dumper->output_file); + } + + if (dumper->output_string) { + g_string_append(dumper->output_string, s); + } +} + +static void +jd_puts_len(const json_dumper *dumper, const char *s, size_t len) +{ + if (dumper->output_file) { + fwrite(s, 1, len, dumper->output_file); + } + + if (dumper->output_string) { + g_string_append_len(dumper->output_string, s, len); + } +} + +static void +jd_vprintf(const json_dumper *dumper, const char *format, va_list args) +{ + if (dumper->output_file) { + vfprintf(dumper->output_file, format, args); + } + + if (dumper->output_string) { + g_string_append_vprintf(dumper->output_string, format, args); + } +} + +static void +json_puts_string(const json_dumper *dumper, const char *str, bool dot_to_underscore) +{ + if (!str) { + jd_puts(dumper, "null"); + return; + } + + 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" + }; + + jd_putc(dumper, '"'); + for (int i = 0; str[i]; i++) { + if ((unsigned)str[i] < 0x20) { + jd_putc(dumper, '\\'); + jd_puts(dumper, json_cntrl[(unsigned)str[i]]); + } else if (i > 0 && str[i - 1] == '<' && str[i] == '/') { + // Convert to <\/script> to avoid breaking web pages. + jd_puts(dumper, "\\/"); + } else { + if (str[i] == '\\' || str[i] == '"') { + jd_putc(dumper, '\\'); + } + if (dot_to_underscore && str[i] == '.') + jd_putc(dumper, '_'); + else + jd_putc(dumper, str[i]); + } + } + jd_putc(dumper, '"'); +} + +static inline uint8_t +json_dumper_get_prev_state(json_dumper *dumper) +{ + unsigned depth = dumper->current_depth; + return depth != 0 ? dumper->state[depth - 1] : 0; +} + +static inline uint8_t +json_dumper_get_curr_state(json_dumper *dumper) +{ + unsigned depth = dumper->current_depth; + return dumper->state[depth]; +} + +/** + * Called when a programming error is encountered where the JSON manipulation + * state got corrupted. This could happen when pairing the wrong begin/end + * calls, when writing multiple values for the same object, etc. + */ +static void +json_dumper_bad(json_dumper *dumper, const char *what) +{ + dumper->flags |= JSON_DUMPER_FLAGS_ERROR; + if ((dumper->flags & JSON_DUMPER_FLAGS_NO_DEBUG)) { + /* Console output can be slow, disable log calls to speed up fuzzing. */ + /* + * XXX - should this call abort()? If that flag isn't set, + * ws_error() wou;d call it; is there any point in continuing + * to do anything if we get here when fuzzing? + */ + return; + } + + if (dumper->output_file) { + fflush(dumper->output_file); + } + char unknown_curr_type_name[10+1]; + char unknown_prev_type_name[10+1]; + const char *curr_type_name, *prev_type_name; + uint8_t curr_state = json_dumper_get_curr_state(dumper); + uint8_t curr_type = JSON_DUMPER_TYPE(curr_state); + if (curr_type < NUM_JSON_DUMPER_ELEMENT_TYPE_NAMES) { + curr_type_name = json_dumper_element_type_names[curr_type]; + } else { + snprintf(unknown_curr_type_name, sizeof unknown_curr_type_name, "%u", curr_type); + curr_type_name = unknown_curr_type_name; + } + if (dumper->current_depth != 0) { + uint8_t prev_state = json_dumper_get_prev_state(dumper); + uint8_t prev_type = JSON_DUMPER_TYPE(prev_state); + if (prev_type < NUM_JSON_DUMPER_ELEMENT_TYPE_NAMES) { + prev_type_name = json_dumper_element_type_names[prev_type]; + } else { + snprintf(unknown_prev_type_name, sizeof unknown_prev_type_name, "%u", prev_type); + prev_type_name = unknown_prev_type_name; + } + } else { + prev_type_name = "(none)"; + } + ws_error("json_dumper error: %s: current stack depth %u, current type %s, previous_type %s", + what, dumper->current_depth, curr_type_name, prev_type_name); + /* NOTREACHED */ +} + +static inline bool +json_dumper_stack_would_overflow(json_dumper *dumper) +{ + if (dumper->current_depth + 1 >= JSON_DUMPER_MAX_DEPTH) { + json_dumper_bad(dumper, "JSON dumper stack overflow"); + return true; + } + return false; +} + +static inline bool +json_dumper_stack_would_underflow(json_dumper *dumper) +{ + if (dumper->current_depth == 0) { + json_dumper_bad(dumper, "JSON dumper stack underflow"); + return true; + } + return false; +} + +/** + * Checks that the dumper has not already had an error. Fail, and + * return false, to tell our caller not to do any more work, if it + * has. + */ +static bool +json_dumper_check_previous_error(json_dumper *dumper) +{ + if ((dumper->flags & JSON_DUMPER_FLAGS_ERROR)) { + json_dumper_bad(dumper, "previous corruption detected"); + return false; + } + return true; +} + +static void +print_newline_indent(const json_dumper *dumper, unsigned depth) +{ + if ((dumper->flags & JSON_DUMPER_FLAGS_PRETTY_PRINT)) { + jd_putc(dumper, '\n'); + for (unsigned i = 0; i < depth; i++) { + jd_puts(dumper, " "); + } + } +} + +/** + * 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; + } + uint8_t 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; + } + + uint8_t curr_state = json_dumper_get_curr_state(dumper); + if (curr_state != JSON_DUMPER_TYPE_NONE) { + jd_putc(dumper, ','); + } + print_newline_indent(dumper, dumper->current_depth); +} + +/** + * Common code to open an object/array/base64 value, printing + * an opening character. + * + * It also makes various correctness checks. + */ +static bool +json_dumper_begin_nested_element(json_dumper *dumper, enum json_dumper_element_type type) +{ + if (!json_dumper_check_previous_error(dumper)) { + return false; + } + + /* Make sure we won't overflow the dumper stack */ + if (json_dumper_stack_would_overflow(dumper)) { + return false; + } + + prepare_token(dumper); + switch (type) { + case JSON_DUMPER_TYPE_OBJECT: + jd_putc(dumper, '{'); + break; + case JSON_DUMPER_TYPE_ARRAY: + jd_putc(dumper, '['); + break; + case JSON_DUMPER_TYPE_BASE64: + dumper->base64_state = 0; + dumper->base64_save = 0; + + jd_putc(dumper, '"'); + break; + default: + json_dumper_bad(dumper, "beginning unknown nested element type"); + return false; + } + + dumper->state[dumper->current_depth] = type; + /* + * Guaranteed not to overflow, as json_dumper_stack_would_overflow() + * returned false. + */ + ++dumper->current_depth; + dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_NONE; + return true; +} + +/** + * Common code to close an object/array/base64 value, printing a + * closing character (and if necessary, it is preceded by newline + * and indentation). + * + * It also makes various correctness checks. + */ +static bool +json_dumper_end_nested_element(json_dumper *dumper, enum json_dumper_element_type type) +{ + if (!json_dumper_check_previous_error(dumper)) { + return false; + } + + uint8_t prev_state = json_dumper_get_prev_state(dumper); + + switch (type) { + case JSON_DUMPER_TYPE_OBJECT: + if (JSON_DUMPER_TYPE(prev_state) != JSON_DUMPER_TYPE_OBJECT) { + json_dumper_bad(dumper, "ending non-object nested item type as object"); + return false; + } + break; + case JSON_DUMPER_TYPE_ARRAY: + if (JSON_DUMPER_TYPE(prev_state) != JSON_DUMPER_TYPE_ARRAY) { + json_dumper_bad(dumper, "ending non-array nested item type as array"); + return false; + } + break; + case JSON_DUMPER_TYPE_BASE64: + if (JSON_DUMPER_TYPE(prev_state) != JSON_DUMPER_TYPE_BASE64) { + json_dumper_bad(dumper, "ending non-base64 nested item type as base64"); + return false; + } + break; + default: + json_dumper_bad(dumper, "ending unknown nested element type"); + return false; + } + + if (prev_state & JSON_DUMPER_HAS_NAME) { + json_dumper_bad(dumper, "finishing object with last item having name but no value"); + return false; + } + + /* Make sure we won't underflow the dumper stack */ + if (json_dumper_stack_would_underflow(dumper)) { + return false; + } + + // 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); + } + + switch (type) { + case JSON_DUMPER_TYPE_OBJECT: + jd_putc(dumper, '}'); + break; + case JSON_DUMPER_TYPE_ARRAY: + jd_putc(dumper, ']'); + break; + case JSON_DUMPER_TYPE_BASE64: + { + char buf[4]; + size_t wrote; + + wrote = g_base64_encode_close(false, buf, &dumper->base64_state, &dumper->base64_save); + jd_puts_len(dumper, buf, wrote); + + jd_putc(dumper, '"'); + break; + } + default: + json_dumper_bad(dumper, "endning unknown nested element type"); + return false; + } + + /* + * Guaranteed not to underflow, as json_dumper_stack_would_underflow() + * returned false. + */ + --dumper->current_depth; + return true; +} + +void +json_dumper_begin_object(json_dumper *dumper) +{ + json_dumper_begin_nested_element(dumper, JSON_DUMPER_TYPE_OBJECT); +} + +void +json_dumper_set_member_name(json_dumper *dumper, const char *name) +{ + if (!json_dumper_check_previous_error(dumper)) { + return; + } + + uint8_t prev_state = json_dumper_get_prev_state(dumper); + + /* Only object members, not array members, have names. */ + if (JSON_DUMPER_TYPE(prev_state) != JSON_DUMPER_TYPE_OBJECT) { + json_dumper_bad(dumper, "setting name on non-object nested item type"); + return; + } + /* An object member name can only be set once before its value is set. */ + if (prev_state & JSON_DUMPER_HAS_NAME) { + json_dumper_bad(dumper, "setting name twice on an object member"); + return; + } + + prepare_token(dumper); + json_puts_string(dumper, name, dumper->flags & JSON_DUMPER_DOT_TO_UNDERSCORE); + jd_putc(dumper, ':'); + if ((dumper->flags & JSON_DUMPER_FLAGS_PRETTY_PRINT)) { + jd_putc(dumper, ' '); + } + + dumper->state[dumper->current_depth - 1] |= JSON_DUMPER_HAS_NAME; +} + +void +json_dumper_end_object(json_dumper *dumper) +{ + json_dumper_end_nested_element(dumper, JSON_DUMPER_TYPE_OBJECT); +} + +void +json_dumper_begin_array(json_dumper *dumper) +{ + json_dumper_begin_nested_element(dumper, JSON_DUMPER_TYPE_ARRAY); +} + +void +json_dumper_end_array(json_dumper *dumper) +{ + json_dumper_end_nested_element(dumper, JSON_DUMPER_TYPE_ARRAY); +} + +static bool +json_dumper_setting_value_ok(json_dumper *dumper) +{ + uint8_t prev_state = json_dumper_get_prev_state(dumper); + + switch (JSON_DUMPER_TYPE(prev_state)) { + case JSON_DUMPER_TYPE_OBJECT: + /* + * This value is part of an object. As such, it must + * have a name. + */ + if (!(prev_state & JSON_DUMPER_HAS_NAME)) { + json_dumper_bad(dumper, "setting value of object member without a name"); + return false; + } + break; + case JSON_DUMPER_TYPE_ARRAY: + /* + * This value is part of an array. As such, it's not + * required to have a name (and shouldn't have a name; + * that's already been checked in json_dumper_set_member_name()). + */ + break; + case JSON_DUMPER_TYPE_BASE64: + /* + * We're in the middle of constructing a base64-encoded + * value. Only json_dumper_write_base64() can be used + * for that; we can't add individual values to it. + */ + json_dumper_bad(dumper, "attempt to set value of base64 item to something not base64-encoded"); + return false; + case JSON_DUMPER_TYPE_NONE: + case JSON_DUMPER_TYPE_VALUE: + { + uint8_t curr_state = json_dumper_get_curr_state(dumper); + switch (JSON_DUMPER_TYPE(curr_state)) { + case JSON_DUMPER_TYPE_NONE: + /* + * We haven't put a value yet, so we can put one now. + */ + break; + case JSON_DUMPER_TYPE_VALUE: + /* + * This value isn't part of an object or array, + * and we've already put one value. + */ + json_dumper_bad(dumper, "value not in object or array immediately follows another value"); + return false; + case JSON_DUMPER_TYPE_OBJECT: + case JSON_DUMPER_TYPE_ARRAY: + case JSON_DUMPER_TYPE_BASE64: + /* + * This should never be the case, no matter what + * our callers do: + * + * JSON_DUMPER_TYPE_OBJECT can be the previous + * type, meaning we're in the process of adding + * elements to an object, but it should never be + * the current type; + * + * JSON_DUMPER_TYPE_ARRAY can be the previous + * type, meaning we're in the process of adding + * elements to an array, but it should never be + * the current type; + * + * JSON_DUMPER_TYPE_BASE64 should only be the + * current type if we're in the middle of + * building a base64 value, in which case the + * previous type should also be JSON_DUMPER_TYPE_BASE64, + * but that's not the previous type. + */ + json_dumper_bad(dumper, "internal error setting value - should not happen"); + return false; + default: + json_dumper_bad(dumper, "internal error setting value, bad current state - should not happen"); + return false; + } + break; + } + default: + json_dumper_bad(dumper, "internal error setting value, bad previous state - should not happen"); + return false; + } + return true; +} + +void +json_dumper_value_string(json_dumper *dumper, const char *value) +{ + if (!json_dumper_check_previous_error(dumper)) { + return; + } + if (!json_dumper_setting_value_ok(dumper)) { + return; + } + + prepare_token(dumper); + json_puts_string(dumper, value, false); + + dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_VALUE; +} + +void +json_dumper_value_double(json_dumper *dumper, double value) +{ + if (!json_dumper_check_previous_error(dumper)) { + return; + } + + if (!json_dumper_setting_value_ok(dumper)) { + return; + } + + prepare_token(dumper); + char buffer[G_ASCII_DTOSTR_BUF_SIZE] = { 0 }; + if (isfinite(value) && g_ascii_dtostr(buffer, G_ASCII_DTOSTR_BUF_SIZE, value) && buffer[0]) { + jd_puts(dumper, buffer); + } else { + jd_puts(dumper, "null"); + } + + dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_VALUE; +} + +void +json_dumper_value_va_list(json_dumper *dumper, const char *format, va_list ap) +{ + if (!json_dumper_check_previous_error(dumper)) { + return; + } + + if (!json_dumper_setting_value_ok(dumper)) { + return; + } + + prepare_token(dumper); + jd_vprintf(dumper, format, ap); + + dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_VALUE; +} + +void +json_dumper_value_anyf(json_dumper *dumper, const char *format, ...) +{ + va_list ap; + + va_start(ap, format); + json_dumper_value_va_list(dumper, format, ap); + va_end(ap); +} + +bool +json_dumper_finish(json_dumper *dumper) +{ + if (!json_dumper_check_previous_error(dumper)) { + return false; + } + + if (dumper->current_depth != 0) { + json_dumper_bad(dumper, "JSON dumper stack not empty at finish"); + return false; + } + + jd_putc(dumper, '\n'); + dumper->state[0] = JSON_DUMPER_TYPE_NONE; + return true; +} + +void +json_dumper_begin_base64(json_dumper *dumper) +{ + json_dumper_begin_nested_element(dumper, JSON_DUMPER_TYPE_BASE64); +} + +void +json_dumper_write_base64(json_dumper* dumper, const unsigned char *data, size_t len) +{ + if (!json_dumper_check_previous_error(dumper)) { + return; + } + + uint8_t prev_state = json_dumper_get_prev_state(dumper); + + if (JSON_DUMPER_TYPE(prev_state) != JSON_DUMPER_TYPE_BASE64) { + json_dumper_bad(dumper, "writing base64 data to a non-base64 value"); + return; + } + + #define CHUNK_SIZE 1024 + char buf[(CHUNK_SIZE / 3 + 1) * 4 + 4]; + + while (len > 0) { + size_t chunk_size = len < CHUNK_SIZE ? len : CHUNK_SIZE; + size_t output_size = g_base64_encode_step(data, chunk_size, false, buf, &dumper->base64_state, &dumper->base64_save); + jd_puts_len(dumper, buf, output_size); + data += chunk_size; + len -= chunk_size; + } + + dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_BASE64; +} + +void +json_dumper_end_base64(json_dumper *dumper) +{ + json_dumper_end_nested_element(dumper, JSON_DUMPER_TYPE_BASE64); +} -- cgit v1.2.3