/* 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, gboolean dot_to_underscore) { if (!str) { fputs("null", fp); 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" }; 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); } if (dot_to_underscore && str[i] == '.') fputc('_', fp); else 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, dumper->flags & JSON_DUMPER_DOT_TO_UNDERSCORE); 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, FALSE); 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; }