From d4560252aeaff1cc942720ab8cc4aec6b7f268c7 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Wed, 8 Jul 2026 22:26:32 +0000 Subject: [PATCH 4/8] Add JSON5 single-quoted string support to the JSON lexer When json5 is set, accept '...'-delimited strings with the same escape handling as double-quoted strings, in normal and incremental modes. --- src/common/jsonapi.c | 38 +++++++++++++++---- .../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, 54 insertions(+), 8 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 e8634a749ea..f7728ffcf1f 100644 --- a/src/common/jsonapi.c +++ b/src/common/jsonapi.c @@ -1774,12 +1774,14 @@ json_lex(JsonLexContext *lex) JsonLexContext dummy_lex = {0}; JsonParseErrorType partial_result; - if (ptok->data[0] == '"') + if (ptok->data[0] == '"' || + (lex->json5 && ptok->data[0] == '\'')) { /* * It's a string. Accumulate characters until we reach an - * unescaped '"'. + * unescaped closing quote. */ + char pquote = ptok->data[0]; int escapes = 0; for (int i = ptok->len - 1; i > 0; i--) @@ -1797,7 +1799,7 @@ json_lex(JsonLexContext *lex) jsonapi_appendStringInfoCharMacro(ptok, c); added++; - if (c == '"' && escapes % 2 == 0) + if (c == pquote && escapes % 2 == 0) { tok_done = true; break; @@ -2182,6 +2184,18 @@ json_lex(JsonLexContext *lex) lex->token_terminator = s + 1; lex->token_type = JSON_TOKEN_COLON; break; + case '\'': + if (lex->json5) + { + result = json_lex_string(lex); + if (result != JSON_SUCCESS) + return result; + lex->token_type = JSON_TOKEN_STRING; + break; + } + lex->prev_token_terminator = lex->token_terminator; + lex->token_terminator = s + 1; + return JSON_INVALID_TOKEN; case '"': /* string */ result = json_lex_string(lex); @@ -2317,6 +2331,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; /* Convenience macros for error exits */ #define FAIL_OR_INCOMPLETE_AT_CHAR_START(code) \ @@ -2359,7 +2374,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 == '\\') { @@ -2467,6 +2482,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; @@ -2493,7 +2516,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 @@ -2519,13 +2543,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 75d2e7b2ae8..3302c756b50 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 @@ -32,7 +32,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.43.0