diff options
-rw-r--r-- | wsutil/json_dumper.c | 495 | ||||
-rw-r--r-- | wsutil/json_dumper.h | 2 |
2 files changed, 351 insertions, 146 deletions
diff --git a/wsutil/json_dumper.c b/wsutil/json_dumper.c index 731594a6d6..7466026803 100644 --- a/wsutil/json_dumper.c +++ b/wsutil/json_dumper.c @@ -1,4 +1,4 @@ -/* wsjson.h +/* json_dumper.c * Routines for serializing data as JSON. * * Copyright 2018, Peter Wu <peter@lekensteyn.nl> @@ -20,8 +20,15 @@ /* * json_dumper.state[current_depth] describes a nested element: - * - type: none/object/array/value + * - 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, @@ -33,6 +40,15 @@ enum json_dumper_element_type { #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 { @@ -128,104 +144,111 @@ json_puts_string(const json_dumper *dumper, const char *str, gboolean dot_to_und jd_putc(dumper, '"'); } +static inline guint8 +json_dumper_get_prev_state(json_dumper *dumper) +{ + guint depth = dumper->current_depth; + return depth != 0 ? dumper->state[depth - 1] : 0; +} + +static inline guint8 +json_dumper_get_curr_state(json_dumper *dumper) +{ + guint 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, enum json_dumper_change change, - enum json_dumper_element_type type, const char *what) +json_dumper_bad(json_dumper *dumper, const char *what) { - unsigned states[3]; - int depth = dumper->current_depth; - /* Do not use add/subtract from depth to avoid signed overflow. */ - int adj = -1; - for (int i = 0; i < 3; i++, adj++) { - if (depth >= -adj && depth < JSON_DUMPER_MAX_DEPTH - adj) { - states[i] = dumper->state[depth + adj]; - } else { - states[i] = 0xbad; - } - } + 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); } - ws_error("Bad json_dumper state: %s; change=%d type=%d depth=%d prev/curr/next state=%02x %02x %02x", - what, change, type, dumper->current_depth, states[0], states[1], states[2]); + char unknown_curr_type_name[10+1]; + char unknown_prev_type_name[10+1]; + const char *curr_type_name, *prev_type_name; + guint8 curr_state = json_dumper_get_curr_state(dumper); + guint8 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) { + guint8 prev_state = json_dumper_get_prev_state(dumper); + guint8 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 gboolean +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 gboolean +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 state is valid for a new change. Any error will be - * sticky and prevent further dumps from succeeding. + * 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 gboolean -json_dumper_check_state(json_dumper *dumper, enum json_dumper_change change, enum json_dumper_element_type type) +json_dumper_check_previous_error(json_dumper *dumper) { if ((dumper->flags & JSON_DUMPER_FLAGS_ERROR)) { - json_dumper_bad(dumper, change, type, "previous corruption detected"); + json_dumper_bad(dumper, "previous corruption detected"); 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; - json_dumper_bad(dumper, change, type, "depth corruption"); - 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 if (prev_type == JSON_DUMPER_TYPE_BASE64) { - ok = FALSE; - } else { - ok = JSON_DUMPER_TYPE(dumper->state[depth]) == JSON_DUMPER_TYPE_NONE; - } - break; - case JSON_DUMPER_WRITE_BASE64: - ok = (prev_type == JSON_DUMPER_TYPE_BASE64) && - (type == JSON_DUMPER_TYPE_NONE || type == JSON_DUMPER_TYPE_BASE64); - break; - case JSON_DUMPER_FINISH: - ok = depth == 0; - break; - } - if (!ok) { - dumper->flags |= JSON_DUMPER_FLAGS_ERROR; - json_dumper_bad(dumper, change, type, "illegal transition"); - } - return ok; + return TRUE; } static void -print_newline_indent(const json_dumper *dumper, int depth) +print_newline_indent(const json_dumper *dumper, guint depth) { if ((dumper->flags & JSON_DUMPER_FLAGS_PRETTY_PRINT)) { jd_putc(dumper, '\n'); - for (int i = 0; i < depth; i++) { + for (guint i = 0; i < depth; i++) { jd_puts(dumper, " "); } } @@ -261,45 +284,164 @@ prepare_token(json_dumper *dumper) return; } - if (dumper->state[dumper->current_depth]) { + guint8 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 close an object/array, printing a closing character (and if - * necessary, it is preceded by newline and indentation). + * Common code to open an object/array/base64 value, printing + * an opening character. + * + * It also makes various correctness checks. */ -static void -finish_token(const json_dumper *dumper, char close_char) +static gboolean +json_dumper_begin_nested_element(json_dumper *dumper, enum json_dumper_element_type type) { - // 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); + if (!json_dumper_check_previous_error(dumper)) { + return FALSE; } - jd_putc(dumper, close_char); -} -void -json_dumper_begin_object(json_dumper *dumper) -{ - if (!json_dumper_check_state(dumper, JSON_DUMPER_BEGIN, JSON_DUMPER_TYPE_OBJECT)) { - return; + /* Make sure we won't overflow the dumper stack */ + if (json_dumper_stack_would_overflow(dumper)) { + return FALSE; } prepare_token(dumper); - jd_putc(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] = JSON_DUMPER_TYPE_OBJECT; + 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] = 0; + 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 gboolean +json_dumper_end_nested_element(json_dumper *dumper, enum json_dumper_element_type type) +{ + if (!json_dumper_check_previous_error(dumper)) { + return FALSE; + } + + guint8 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; + } + + 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: + { + gchar buf[4]; + gsize 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_state(dumper, JSON_DUMPER_SET_NAME, JSON_DUMPER_TYPE_NONE)) { + if (!json_dumper_check_previous_error(dumper)) { + return; + } + + guint8 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; } @@ -316,46 +458,114 @@ json_dumper_set_member_name(json_dumper *dumper, const char *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; + json_dumper_end_nested_element(dumper, JSON_DUMPER_TYPE_OBJECT); } 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); - jd_putc(dumper, '['); - - dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_ARRAY; - ++dumper->current_depth; - dumper->state[dumper->current_depth] = 0; + json_dumper_begin_nested_element(dumper, JSON_DUMPER_TYPE_ARRAY); } void json_dumper_end_array(json_dumper *dumper) { - if (!json_dumper_check_state(dumper, JSON_DUMPER_END, JSON_DUMPER_TYPE_ARRAY)) { - return; - } + json_dumper_end_nested_element(dumper, JSON_DUMPER_TYPE_ARRAY); +} - finish_token(dumper, ']'); +static gboolean +json_dumper_setting_value_ok(json_dumper *dumper) +{ + guint8 prev_state = json_dumper_get_prev_state(dumper); - --dumper->current_depth; + 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: + { + guint8 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_state(dumper, JSON_DUMPER_SET_VALUE, JSON_DUMPER_TYPE_VALUE)) { + if (!json_dumper_check_previous_error(dumper)) { + return; + } + if (!json_dumper_setting_value_ok(dumper)) { return; } @@ -368,7 +578,11 @@ json_dumper_value_string(json_dumper *dumper, const char *value) void json_dumper_value_double(json_dumper *dumper, double value) { - if (!json_dumper_check_state(dumper, JSON_DUMPER_SET_VALUE, JSON_DUMPER_TYPE_VALUE)) { + if (!json_dumper_check_previous_error(dumper)) { + return; + } + + if (!json_dumper_setting_value_ok(dumper)) { return; } @@ -386,7 +600,11 @@ json_dumper_value_double(json_dumper *dumper, double value) void json_dumper_value_va_list(json_dumper *dumper, const char *format, va_list ap) { - if (!json_dumper_check_state(dumper, JSON_DUMPER_SET_VALUE, JSON_DUMPER_TYPE_VALUE)) { + if (!json_dumper_check_previous_error(dumper)) { + return; + } + + if (!json_dumper_setting_value_ok(dumper)) { return; } @@ -409,38 +627,37 @@ json_dumper_value_anyf(json_dumper *dumper, const char *format, ...) gboolean json_dumper_finish(json_dumper *dumper) { - if (!json_dumper_check_state(dumper, JSON_DUMPER_FINISH, JSON_DUMPER_TYPE_NONE)) { + 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] = 0; + dumper->state[0] = JSON_DUMPER_TYPE_NONE; return TRUE; } void json_dumper_begin_base64(json_dumper *dumper) { - if (!json_dumper_check_state(dumper, JSON_DUMPER_BEGIN, JSON_DUMPER_TYPE_BASE64)) { - return; - } - - dumper->base64_state = 0; - dumper->base64_save = 0; - - prepare_token(dumper); - - jd_putc(dumper, '"'); - - dumper->state[dumper->current_depth] = JSON_DUMPER_TYPE_BASE64; - ++dumper->current_depth; - dumper->state[dumper->current_depth] = 0; + json_dumper_begin_nested_element(dumper, JSON_DUMPER_TYPE_BASE64); } void json_dumper_write_base64(json_dumper* dumper, const guchar *data, size_t len) { - if (!json_dumper_check_state(dumper, JSON_DUMPER_WRITE_BASE64, JSON_DUMPER_TYPE_BASE64)) { + if (!json_dumper_check_previous_error(dumper)) { + return; + } + + guint8 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; } @@ -461,17 +678,5 @@ json_dumper_write_base64(json_dumper* dumper, const guchar *data, size_t len) void json_dumper_end_base64(json_dumper *dumper) { - if (!json_dumper_check_state(dumper, JSON_DUMPER_END, JSON_DUMPER_TYPE_BASE64)) { - return; - } - - gchar buf[4]; - gsize wrote; - - wrote = g_base64_encode_close(FALSE, buf, &dumper->base64_state, &dumper->base64_save); - jd_puts_len(dumper, buf, wrote); - - jd_putc(dumper, '"'); - - --dumper->current_depth; + json_dumper_end_nested_element(dumper, JSON_DUMPER_TYPE_BASE64); } diff --git a/wsutil/json_dumper.h b/wsutil/json_dumper.h index 966c1afa76..ae47ada617 100644 --- a/wsutil/json_dumper.h +++ b/wsutil/json_dumper.h @@ -58,7 +58,7 @@ typedef struct json_dumper { #define JSON_DUMPER_FLAGS_NO_DEBUG (1 << 17) /* Disable fatal ws_error messsges on error(intended for speeding up fuzzing). */ int flags; /* for internal use, initialize with zeroes. */ - int current_depth; + guint current_depth; gint base64_state; gint base64_save; guint8 state[JSON_DUMPER_MAX_DEPTH]; |