From dcdd065f9b7d4250613861a4b2f10ed70d349baf Mon Sep 17 00:00:00 2001 From: Cloudwalk Date: Sun, 28 Mar 2021 00:45:22 -0400 Subject: [PATCH] json: Crude (but working) initial implementation of json parser (lexer only for now) --- host.c | 2 + json.c | 379 +++++++++++++++++++++++++++++++++++++++++++++++++++ makefile.inc | 1 + 3 files changed, 382 insertions(+) create mode 100644 json.c diff --git a/host.c b/host.c index 1dc4b0d5..d3e5fb6f 100644 --- a/host.c +++ b/host.c @@ -189,6 +189,7 @@ Host_InitLocal */ void Host_SaveConfig_f(cmd_state_t *cmd); void Host_LoadConfig_f(cmd_state_t *cmd); +void Json_Test_f(cmd_state_t *cmd); extern cvar_t sv_writepicture_quality; extern cvar_t r_texture_jpeg_fastpicmip; static void Host_InitLocal (void) @@ -198,6 +199,7 @@ static void Host_InitLocal (void) Cmd_AddCommand(CF_SHARED, "saveconfig", Host_SaveConfig_f, "save settings to config.cfg (or a specified filename) immediately (also automatic when quitting)"); Cmd_AddCommand(CF_SHARED, "loadconfig", Host_LoadConfig_f, "reset everything and reload configs"); Cmd_AddCommand(CF_SHARED, "sendcvar", SendCvar_f, "sends the value of a cvar to the server as a sentcvar command, for use by QuakeC"); + Cmd_AddCommand(CF_SHARED, "json_test", Json_Test_f, "test the json parser"); Cvar_RegisterVariable (&cl_maxphysicsframesperserverframe); Cvar_RegisterVariable (&host_framerate); Cvar_RegisterCallback (&host_framerate, Host_Framerate_c); diff --git a/json.c b/json.c new file mode 100644 index 00000000..c12dea4f --- /dev/null +++ b/json.c @@ -0,0 +1,379 @@ +/* +Copyright (C) 2021 David Knapp (Cloudwalk) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "quakedef.h" +#include + +// taken from json's wikipedia article +const char json_test_string[] = +{ + "{\n" + "\t\"firstName\": \"John\",\n" + "\t\"lastName\": \"Smith\",\n" + "\t\"isAlive\": true,\n" + "\t\"age\": 27,\n" + "\t\"address\": {\n" + "\t\t\"streetAddress\": \"21 2nd Street\",\n" + "\t\t\"city\": \"New York\",\n" + "\t\t\"state\": \"NY\",\n" + "\t\t\"postalCode\": \"10021-3100\"\n" + "\t},\n" + "\t\"phoneNumbers\": [\n" + "\t\t{\n" + "\t\t\t\"type\": \"home\",\n" + "\t\t\t\"number\": \"212 555-1234\"\n" + "\t\t},\n" + "\t\t{\n" + "\t\t\t\"type\": \"office\",\n" + "\t\t\t\"number\": \"646 555-4567\"\n" + "\t\t}\n" + "\t],\n" + "\t\"children\": [],\n" + "\t\"spouse\": null\n" + "}\n\000" +}; + +static jmp_buf json_error; + +typedef enum qjson_err_e +{ + JSON_ERR_SUCCESS = 0, + JSON_ERR_INVAL = 1, + JSON_ERR_EOF = 2, + JSON_ERR_EMPTY = 3 +} qjson_err_t; + +typedef enum qjson_type_e +{ + JSON_TYPE_UNDEFINED = 0, + JSON_TYPE_OBJECT = 1, + JSON_TYPE_ARRAY = 2, + JSON_TYPE_STRING = 3, + JSON_TYPE_PRIMITIVE = 4 +} qjson_type_t; + +typedef struct qjson_token_s +{ + qjson_type_t type; + struct qjson_token_s *next; // if an array, next will be a NULL terminated array + char *string; // ASCII only for now +} qjson_token_t; + +struct qjson_state_s +{ + qjson_token_t *head, *cur; + const char *buf; + const char *pos; + int line, col; +}; + +static void Json_Parse_Object(struct qjson_state_s *state); +static void Json_Parse_Array(struct qjson_state_s *state); + +// Tell the user that their json is broken, why it's broken, and where it's broken, so hopefully they fix it. +static void Json_Parse_Error(struct qjson_state_s *state, qjson_err_t error) +{ + if(!error) + return; + else + { + switch (error) + { + case JSON_ERR_INVAL: + Con_Printf(CON_ERROR "Json Error: Unexpected token '%c', line %i, column %i\n", *state->pos, state->line, state->col); + break; + case JSON_ERR_EOF: + Con_Printf(CON_ERROR "Json Error: Unexpected end-of-file\n"); + break; + default: + return; + } + } + longjmp(json_error, 1); +} + +// Skips newlines, and handles different line endings. +static qbool Json_Parse_Newline(struct qjson_state_s *state) +{ + if(*state->pos == '\n') + goto newline; + if(*state->pos == '\r') + { + if(*state->pos + 1 == '\n') + state->pos++; + goto newline; + } + return false; +newline: + state->col = 1; + state->line++; + state->pos++; + return true; +} + +// Skips the current line. Only useful for comments. +static void Json_Parse_SkipLine(struct qjson_state_s *state) +{ + while(!Json_Parse_Newline(state)) + state->pos++; +} + +// Checks for C/C++-style comments and ignores them. This is not standard json. +static qbool Json_Parse_Comment(struct qjson_state_s *state) +{ + if(*state->pos == '/') + { + if(*state->pos++ == '/') + Json_Parse_SkipLine(state); + else if(*state->pos == '*') + { + while(*state->pos++ != '*' && *state->pos + 1 != '/') + continue; + } + else + Json_Parse_Error(state, JSON_ERR_INVAL); + return true; + } + return false; +} + +// Advance forward in the stream as many times as 'count', cleanly. +static void Json_Parse_Next(struct qjson_state_s *state, size_t count) +{ + state->col = state->col + count; + state->pos = state->pos + count; + + if(!*state->pos) + Json_Parse_Error(state, JSON_ERR_EOF); +} + +// Skip all whitespace, as we normally know it. +static void Json_Parse_Whitespace(struct qjson_state_s *state) +{ + while(*state->pos == ' ' || *state->pos == '\t') + Json_Parse_Next(state, 1); +} + +// Skip all whitespace, as json defines it. +static void Json_Parse_Skip(struct qjson_state_s *state) +{ + /* + * Repeat this until we run out of whitespace, newlines, and comments. + * state->pos should be left on non-whitespace when this returns. + */ + do { + Json_Parse_Whitespace(state); + } while (Json_Parse_Comment(state) || Json_Parse_Newline(state)); +} + +// Skip to the next token that isn't whitespace. Hopefully a valid one. +static char Json_Parse_NextToken(struct qjson_state_s *state) +{ + /* + * This assumes state->pos is already on whitespace. Most of the time this + * doesn't happen automatically, but advancing the pointer here would break + * comment and newline handling when it does happen automatically. + */ + Json_Parse_Skip(state); + return *state->pos; +} + +// TODO: handle escape sequences +static void Json_Parse_String(struct qjson_state_s *state) +{ + do { + Json_Parse_Next(state, 1); + if(*state->pos == '\\') + Json_Parse_Next(state, 1); + } while(*state->pos != '"'); + + Json_Parse_Next(state, 1); +} + +// Handles numbers. Json numbers can be either an integer or a double. +static qbool Json_Parse_Number(struct qjson_state_s *state) +{ + int i, numsize; + const char *in = state->pos; + //char out[128]; + qbool is_float = false; + qbool is_exp = false; + + for(i = 0, numsize = 0; isdigit(in[i]); i++, numsize++) + { + //out[numsize] = in[numsize]; + + if(in[i] == '.') + { + if(is_float || is_exp) + Json_Parse_Error(state, JSON_ERR_INVAL); + is_float = true; + i++; + continue; + } + + if(in[i] == 'e' || in[i] == 'E') + { + if(is_exp) + Json_Parse_Error(state, JSON_ERR_INVAL); + if(in[i+1] == '+' || in[i+1] == '-') + i++; + is_exp = true; + i++; + continue; + } + } + // TODO: use strtod() + Json_Parse_Next(state, i); + return true; +} + +// Parse a keyword. +static qbool Json_Parse_Keyword(struct qjson_state_s *state, const char *keyword) +{ + size_t keyword_size = strlen(keyword); + if(!strncmp(keyword, state->pos, keyword_size)) + { + Json_Parse_Next(state, keyword_size); + return true; + } + return false; +} + +// Parse a value. +static void Json_Parse_Value(struct qjson_state_s *state) +{ + Json_Parse_Next(state, 1); + + switch(Json_Parse_NextToken(state)) + { + case '"': // string + Json_Parse_String(state); + break; + case '{': // object + Json_Parse_Object(state); + break; + case '[': // array + Json_Parse_Array(state); + break; + case '-': + Json_Parse_Number(state); + break; + default: + if(Json_Parse_Keyword(state, "true")) + break; + if(Json_Parse_Keyword(state, "false")) + break; + if(Json_Parse_Keyword(state, "null")) + break; + if(isdigit(*state->pos)) + Json_Parse_Number(state); + } +} + +// Parse an object. +static void Json_Parse_Object(struct qjson_state_s *state) +{ + /* + * Json objects are basically a data map; key-value pairs. + * They end in a comma or a closing curly brace. + */ + do { + Json_Parse_Next(state, 1); + + // Parse the key + if(Json_Parse_NextToken(state) == '"') + Json_Parse_String(state); + else + goto fail; + + // And its value + if(Json_Parse_NextToken(state) == ':') + Json_Parse_Value(state); + else + goto fail; + } while (Json_Parse_NextToken(state) == ','); + + if(Json_Parse_NextToken(state) == '}') + return; +fail: + Json_Parse_Error(state, JSON_ERR_INVAL); +} + +// Parse an array. +static void Json_Parse_Array(struct qjson_state_s *state) +{ + /* + * Json arrays are basically lists. They can contain + * any value, comma-separated, and end with a closing square bracket. + */ + do { + Json_Parse_Value(state); + } while (Json_Parse_NextToken(state) == ','); + + if(Json_Parse_NextToken(state) == ']') + return; + else + Json_Parse_Error(state, JSON_ERR_INVAL); +} + +// Main function for the parser. +qjson_token_t *Json_Parse(const char *data) +{ + struct qjson_state_s state = + { + .head = NULL, + .buf = data, + .pos = &data[0], + .line = 1, + .col = 1 + }; + + if(data == NULL) + { + Con_Printf(CON_ERROR "Json_Parse: Empty json file\n"); + return NULL; + } + + if(setjmp(json_error)) + { + // actually not sure about this + return NULL; + } + + if(Json_Parse_NextToken(&state) == '{') + Json_Parse_Object(&state); + else + { + Con_Printf(CON_ERROR "Json_Parse: Not a json file\n"); + return NULL; + } + + // Success! + // TODO: Actually parse. + Con_Printf("Hmm, yes. This json is made of json\n"); + + return state.head; +} + +void Json_Test_f(cmd_state_t *cmd) +{ + Json_Parse(json_test_string); +} diff --git a/makefile.inc b/makefile.inc index 2fdc966a..eefcf8ee 100644 --- a/makefile.inc +++ b/makefile.inc @@ -124,6 +124,7 @@ OBJ_COMMON= \ image.o \ image_png.o \ jpeg.o \ + json.o \ keys.o \ lhnet.o \ libcurl.o \ -- 2.39.2