From 350d1242b6585576719e681276987f325d15628d Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Mon, 20 Jul 2026 09:44:15 +0000 Subject: [PATCH v2 4/8] Add JSON5 single-quoted string support to the JSON lexer When json5 is set, accept single-quote-delimited strings with the same escape handling as double-quoted strings. --- src/common/jsonapi.c | 31 ++++++++++++++++--- .../test_json_parser/json5_strings.json5 | 8 +++++ .../test_json_parser/json5_strings.out | 13 ++++++++ .../test_json_parser/t/005_test_json5.pl | 3 +- 4 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 src/test/modules/test_json_parser/json5_strings.json5 create mode 100644 src/test/modules/test_json_parser/json5_strings.out diff --git a/src/common/jsonapi.c b/src/common/jsonapi.c index 96d739a79e7..f6d804a23b2 100644 --- a/src/common/jsonapi.c +++ b/src/common/jsonapi.c @@ -2293,6 +2293,19 @@ json_lex(JsonLexContext *lex) lex->token_terminator = s + 1; lex->token_type = JSON_TOKEN_COLON; break; + case '\'': + if (!lex->json5) + { + lex->prev_token_terminator = lex->token_terminator; + lex->token_terminator = s + 1; + return JSON_INVALID_TOKEN; + } + + /* + * json5 single-quoted string. Share the call below, so that + * json_lex_string() still gets inlined. + */ + pg_fallthrough; case '"': /* string */ result = json_lex_string(lex); @@ -2417,6 +2430,7 @@ json_lex_string(JsonLexContext *lex) const char *s; const char *const end = lex->input + lex->input_length; int hi_surrogate = -1; + char quote_char = *lex->token_start; JsonParseErrorType result; /* Convenience macros for error exits */ @@ -2460,7 +2474,7 @@ json_lex_string(JsonLexContext *lex) /* Premature end of the string. */ if (s >= end) FAIL_OR_INCOMPLETE_AT_CHAR_START(JSON_INVALID_TOKEN); - else if (*s == '"') + else if (*s == quote_char) break; else if (*s == '\\') { @@ -2539,6 +2553,14 @@ json_lex_string(JsonLexContext *lex) case '/': jsonapi_appendStringInfoChar(lex->strval, *s); break; + case '\'': + if (!lex->json5) + { + lex->token_start = s; + FAIL_AT_CHAR_END(JSON_ESCAPING_INVALID); + } + jsonapi_appendStringInfoChar(lex->strval, *s); + break; case 'b': jsonapi_appendStringInfoChar(lex->strval, '\b'); break; @@ -2565,7 +2587,8 @@ json_lex_string(JsonLexContext *lex) FAIL_AT_CHAR_END(JSON_ESCAPING_INVALID); } } - else if (strchr("\"\\/bfnrt", *s) == NULL) + else if (strchr("\"\\/bfnrt", *s) == NULL && + !(lex->json5 && *s == '\'')) { /* * Simpler processing if we're not bothered about de-escaping @@ -2591,13 +2614,13 @@ json_lex_string(JsonLexContext *lex) */ while (p < end - sizeof(Vector8) && !pg_lfind8('\\', (const uint8 *) p, sizeof(Vector8)) && - !pg_lfind8('"', (const uint8 *) p, sizeof(Vector8)) && + !pg_lfind8(quote_char, (const uint8 *) p, sizeof(Vector8)) && !pg_lfind8_le(31, (const uint8 *) p, sizeof(Vector8))) p += sizeof(Vector8); for (; p < end; p++) { - if (*p == '\\' || *p == '"') + if (*p == '\\' || *p == quote_char) break; else if ((unsigned char) *p <= 31) { diff --git a/src/test/modules/test_json_parser/json5_strings.json5 b/src/test/modules/test_json_parser/json5_strings.json5 new file mode 100644 index 00000000000..7345d8a452f --- /dev/null +++ b/src/test/modules/test_json_parser/json5_strings.json5 @@ -0,0 +1,8 @@ +{ + a: 'single quoted', + b: 'embedded "double" quotes', + c: 'escaped \' apostrophe', + d: "double with 'single' inside", + e: 'line1\nline2', + arr: [ 'x', "y", 'z' ] +} diff --git a/src/test/modules/test_json_parser/json5_strings.out b/src/test/modules/test_json_parser/json5_strings.out new file mode 100644 index 00000000000..3efe7a744db --- /dev/null +++ b/src/test/modules/test_json_parser/json5_strings.out @@ -0,0 +1,13 @@ +{ +"a": "single quoted", +"b": "embedded \"double\" quotes", +"c": "escaped ' apostrophe", +"d": "double with 'single' inside", +"e": "line1\nline2", +"arr": [ +"x", +"y", +"z" +] + +} 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 aa077d751e7..9447aee0801 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 @@ -75,7 +75,8 @@ my @features = ( error => qr/Token "\/" is invalid/, }, { name => 'trailing commas', file => 'json5_trailing_commas' }, - { name => 'unquoted keys', file => 'json5_keys' },); + { name => 'unquoted keys', file => 'json5_keys' }, + { name => 'single-quoted strings', file => 'json5_strings' },); # Inputs that stay invalid even in json5 mode. my @json5_invalid = ( -- 2.55.0