diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 614352fd..d921251b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,7 +55,7 @@ jobs: - name: Build and run tests run: | - c89 -ansi -Wall -Wpedantic -Werror -pedantic -pedantic-errors -D_ANSI_SOURCE -DJSON_TRACK_SOURCE -I. json.c tests/test.c -lm -o json-test + c89 -ansi -Wall -Wpedantic -Werror -pedantic -pedantic-errors -D_ANSI_SOURCE -DJSON_TRACK_SOURCE -DJSON_LINTING=1 -I. json.c tests/test.c -lm -o json-test cd tests ../json-test @@ -103,7 +103,7 @@ jobs: run: cl /permissive- /Zc:preprocessor /Zc:throwingNew /volatile:iso /utf-8 /std:c++latest /Zc:__cplusplus /Wall /DJSON_TRACK_SOURCE /LD /Tp json.c - name: Build test executable - run: cl /permissive- /Zc:preprocessor /Zc:throwingNew /volatile:iso /utf-8 /std:c++latest /Zc:__cplusplus /Wall /DJSON_TRACK_SOURCE /I . /MT /Tp json.c /Tp tests\test.c /Fetests\json-test.exe + run: cl /permissive- /Zc:preprocessor /Zc:throwingNew /volatile:iso /utf-8 /std:c++latest /Zc:__cplusplus /Wall /DJSON_TRACK_SOURCE /DJSON_LINTING=1 /I . /MT /Tp json.c /Tp tests\test.c /Fetests\json-test.exe - name: Run tests if: matrix.arch != 'amd64_arm' && matrix.arch != 'amd64_arm64' diff --git a/README.md b/README.md index 5ac0a3a9..56054c65 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,16 @@ Example usage: * `"-Djson_int_t=long long"` * `-Djson_int_t=__int128` +### `JSON_LINTING` +Loads code branches used for strict ECMA-404 JSON parsing. + +Setting JSON_LINTING to 1 additionally requires the use of the runtime option (json_enable_comments) to perform linting. +Setting JSON_LINTING to 2 performs linting without requiring the runtime option. + +Example usage: +* `-DJSON_LINTING=1` +* `-DJSON_LINTING=2` + Runtime Options --------------- @@ -69,6 +79,10 @@ settings |= json_enable_comments; ``` Enables C-style `// line` and `/* block */` comments. ```c +settings |= json_enable_linting; +``` +Enables strict ECMA-404 JSON parsing +```c size_t value_extra ``` The amount of space (if any) to allocate at the end of each `json_value`, in diff --git a/configure.ac b/configure.ac index d5d4488c..68c4645a 100644 --- a/configure.ac +++ b/configure.ac @@ -29,6 +29,16 @@ if test "x$enable_value_pos" = xyes; then CFLAGS="$CFLAGS -DJSON_TRACK_SOURCE" fi +AC_ARG_ENABLE([enable-linting], + [AS_HELP_STRING([--enable-linting], + [ Enables strict ECMA-404 JSON parsing. @<:@default=disabled@:>@])], + [enable_linting="$enableval"], + [enable_linting=no] +) +if test "x$enable_linting" = xyes; then + CFLAGS="$CFLAGS -DJSON_LINTING=1" +fi + AC_SUBST(VERSION_MAJOR, $VERSION_MAJOR) AC_CONFIG_FILES([ diff --git a/examples/test_json.c b/examples/test_json.c index 85fdf783..007b59c5 100644 --- a/examples/test_json.c +++ b/examples/test_json.c @@ -168,6 +168,14 @@ int main(int argc, char** argv) value = json_parse(json,file_size); + /* + * Use json_parse_ex to enable runtime options + */ + /* json_settings settings = { 0 }; */ + /* settings.settings |= json_enable_comments; */ + /* settings.settings |= json_enable_linting; */ + /* value = json_parse_ex(&settings, json, file_size, 0); */ + if (value == NULL) { fprintf(stderr, "Unable to parse data\n"); free(file_contents); diff --git a/json.c b/json.c index deef49ff..e86dcffa 100644 --- a/json.c +++ b/json.c @@ -69,6 +69,18 @@ typedef unsigned int json_uchar; const struct _json_value json_value_none; +#if defined JSON_LINTING && JSON_LINTING == 1 +int compile_option_linting = 0; +#elif defined JSON_LINTING && JSON_LINTING == 2 +int compile_option_linting = 1; +#endif + +/* + * Called when parsing closing brackets of objects and arrays + * Decrements json_char ptr, ignoring whitespace, allowing digits and { } [ ] " true false null + */ +static int trailing_garbage (const json_char * ptr); + static unsigned char hex_value (json_char c) { if (isdigit((unsigned char)c)) @@ -550,9 +562,23 @@ json_value * json_parse_ex (json_settings * settings, case ']': if (top && top->type == json_array) + { + #if defined JSON_LINTING && (JSON_LINTING == 1 || JSON_LINTING == 2) + if (state.settings.settings & json_enable_linting || compile_option_linting == 1) + { + if (trailing_garbage(state.ptr)) + { + sprintf (error, "Trailing garbage before %d:%d", + state.cur_line, state.cur_col); + goto e_failed; + } + } + #endif flags = (flags & ~ (flag_need_comma | flag_seek_value)) | flag_next; + } else - { sprintf (error, "%u:%u: Unexpected `]`", line_and_col); + { + sprintf (error, "%u:%u: Unexpected `]`", line_and_col); goto e_failed; } @@ -742,6 +768,18 @@ json_value * json_parse_ex (json_settings * settings, case '}': + #if defined JSON_LINTING && (JSON_LINTING == 1 || JSON_LINTING == 2) + if (state.settings.settings & json_enable_linting || compile_option_linting == 1) + { + if (trailing_garbage(state.ptr)) + { + sprintf (error, "Trailing garbage before %d:%d", + state.cur_line, state.cur_col); + goto e_failed; + } + } + #endif + flags = (flags & ~ flag_need_comma) | flag_next; break; @@ -1055,3 +1093,56 @@ void json_value_free (json_value * value) settings.mem_free = default_free; json_value_free_ex (&settings, value); } + +int trailing_garbage (const json_char * ptr) +{ + json_char * marker = (char *)ptr; + do + { + marker--; + } + while (isspace(*marker)); + + switch (*marker) + { + case '}': + case '{': + case ']': + case '[': + case '"': + return 0; + + case 'e': + /* Allow true */ + if (strncmp(marker-3, "true", 4) == 0) + { + return 0; + } + + /* Allow false */ + if (strncmp(marker-4, "false", 5) == 0) + { + return 0; + } + return 1; + + case 'l': + /* Allow null */ + if (strncmp(marker-3, "null", 4) == 0) + { + return 0; + } + return 1; + + default: + /* Allow digits */ + if (isdigit(*marker)) + { + return 0; + } + else + { + return 1; + } + } +} diff --git a/json.h b/json.h index 1f643161..81486c20 100644 --- a/json.h +++ b/json.h @@ -80,6 +80,7 @@ typedef struct } json_settings; #define json_enable_comments 0x01 +#define json_enable_linting 0x02 typedef enum { diff --git a/tests/invalid-0011.json b/tests/invalid-0011.json new file mode 100644 index 00000000..56597578 --- /dev/null +++ b/tests/invalid-0011.json @@ -0,0 +1,3 @@ +{ + "a": "b", +} diff --git a/tests/invalid-0012.json b/tests/invalid-0012.json new file mode 100644 index 00000000..994d2fdf --- /dev/null +++ b/tests/invalid-0012.json @@ -0,0 +1,3 @@ +[ + "foo", +] diff --git a/tests/test.c b/tests/test.c index 9d2c4fba..42b12633 100644 --- a/tests/test.c +++ b/tests/test.c @@ -136,7 +136,8 @@ static int json_verify(const char * filename_format, unsigned highest_file_num, if(extensions == 1) { - settings.settings = json_enable_comments; + settings.settings |= json_enable_comments; + settings.settings |= json_enable_linting; } for(test_num = 0; ; ++test_num) @@ -227,7 +228,7 @@ int main(void) JSON_COMPARE_STRING(1, "\\ud841\\udf31", "𠜱"); /* TODO: this should actually succeed after PR #58 is merged */ if(0 != json_verify( "valid-%04u.json", 13, 0, 0)){ exit_code = EXIT_FAILURE; } - if(0 != json_verify( "invalid-%04u.json", 10, 0, 1)){ exit_code = EXIT_FAILURE; } + if(0 != json_verify( "invalid-%04u.json", 12, 0, 1)){ exit_code = EXIT_FAILURE; } if(0 != json_verify( "ext-valid-%04u.json", 3, 1, 0)){ exit_code = EXIT_FAILURE; } if(0 != json_verify("ext-invalid-%04u.json", 2, 1, 1)){ exit_code = EXIT_FAILURE; }