From 6a1b913bc5790fddf460aec4ea4700605a7079c6 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Mon, 20 Jul 2026 09:13:51 +0000 Subject: [PATCH v2 2/8] Add JSON5 trailing comma support to the JSON parser When the json5 flag is set, allow one trailing comma before a closing ] or } in the recursive-descent parser. --- src/common/jsonapi.c | 4 ++ .../json5_trailing_commas.json5 | 7 +++ .../json5_trailing_commas.out | 32 ++++++++++++++ .../test_json_parser/t/005_test_json5.pl | 43 +++++++++++++++---- 4 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 src/test/modules/test_json_parser/json5_trailing_commas.json5 create mode 100644 src/test/modules/test_json_parser/json5_trailing_commas.out diff --git a/src/common/jsonapi.c b/src/common/jsonapi.c index 81410d62bbf..e74e2fbfd4c 100644 --- a/src/common/jsonapi.c +++ b/src/common/jsonapi.c @@ -1577,6 +1577,8 @@ parse_object(JsonLexContext *lex, const JsonSemAction *sem) result = json_lex(lex); if (result != JSON_SUCCESS) break; + if (unlikely(lex->json5) && lex_peek(lex) == JSON_TOKEN_OBJECT_END) + break; result = parse_object_field(lex, sem); } break; @@ -1689,6 +1691,8 @@ parse_array(JsonLexContext *lex, const JsonSemAction *sem) result = json_lex(lex); if (result != JSON_SUCCESS) break; + if (unlikely(lex->json5) && lex_peek(lex) == JSON_TOKEN_ARRAY_END) + break; result = parse_array_element(lex, sem); } } diff --git a/src/test/modules/test_json_parser/json5_trailing_commas.json5 b/src/test/modules/test_json_parser/json5_trailing_commas.json5 new file mode 100644 index 00000000000..b41af4393ee --- /dev/null +++ b/src/test/modules/test_json_parser/json5_trailing_commas.json5 @@ -0,0 +1,7 @@ +{ + "arr": [1, 2, 3,], + "nested_arr": [[1,], [2,],], + "obj": {"a": 1, "b": 2,}, + "nested_obj": {"x": {"y": 1,},}, + "last": true, +} diff --git a/src/test/modules/test_json_parser/json5_trailing_commas.out b/src/test/modules/test_json_parser/json5_trailing_commas.out new file mode 100644 index 00000000000..86f88bda47f --- /dev/null +++ b/src/test/modules/test_json_parser/json5_trailing_commas.out @@ -0,0 +1,32 @@ +{ +"arr": [ +1, +2, +3 +] +, +"nested_arr": [ +[ +1 +] +, +[ +2 +] + +] +, +"obj": { +"a": 1, +"b": 2 +} +, +"nested_obj": { +"x": { +"y": 1 +} + +} +, +"last": true +} diff --git a/src/test/modules/test_json_parser/t/005_test_json5.pl b/src/test/modules/test_json_parser/t/005_test_json5.pl index 815d4699306..18024aedeef 100644 --- a/src/test/modules/test_json_parser/t/005_test_json5.pl +++ b/src/test/modules/test_json_parser/t/005_test_json5.pl @@ -55,7 +55,32 @@ my @features = ( name => 'comments', file => 'json5_comments', error => qr/Token "\/" is invalid/, - },); + }, + { name => 'trailing commas', file => 'json5_trailing_commas' },); + +# Inputs that stay invalid even in json5 mode. +my @json5_invalid = ( + [ 'doubled trailing comma', '[1,,]' ], + [ 'doubled trailing comma in object', '{"a":1,,}' ], + [ 'leading comma', '[,1]' ], + [ 'leading comma in object', '{,"a":1}' ], + [ 'lone comma', '[,]' ], + [ 'lone comma in object', '{,}' ], + [ 'missing comma', '[true false]' ], + [ 'missing comma in object', '{"a":1 "b":2}' ],); + +# Write $content to a temp file and return the file name. The file is +# written in binary mode, so that CR and LF in $content reach the parser +# as they are. +sub inline_file +{ + my ($content) = @_; + my ($fh, $fname) = tempfile(DIR => $dir); + binmode($fh); + print $fh $content; + close($fh); + return $fname; +} # Run the parser executable @$exe with @args and return its output, with # the CRLF line ends that printf() produces on Windows normalized to LF. @@ -116,13 +141,7 @@ foreach my $exe (@exes) foreach my $c (@json5_inline_cases) { my ($label, $content, $expected) = @$c; - my ($fh, $fname) = tempfile(DIR => $dir); - - # binary mode, so that CR and LF in $content reach the parser as - # they are - binmode($fh); - print $fh $content; - close($fh); + my $fname = inline_file($content); if (defined $expected) { @@ -148,6 +167,14 @@ foreach my $exe (@exes) check_rejected($exe, $file, "non-json5 mode: $f->{name}", $f->{error}); } + + foreach my $inv (@json5_invalid) + { + my ($label, $content) = @$inv; + my $fname = inline_file($content); + + check_rejected($exe, $fname, "json5 mode: $label", undef, "--json5"); + } } done_testing(); -- 2.55.0