diff --git a/ext/json/json.c b/ext/json/json.c index 04a62f52152f..0ac944a54708 100644 --- a/ext/json/json.c +++ b/ext/json/json.c @@ -351,8 +351,8 @@ PHP_FUNCTION(json_validate) ZEND_PARSE_PARAMETERS_END(); - if ((options != 0) && (options != PHP_JSON_INVALID_UTF8_IGNORE)) { - zend_argument_value_error(3, "must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)"); + if ((options & ~(PHP_JSON_INVALID_UTF8_IGNORE | PHP_JSON_ALLOW_COMMENTS | PHP_JSON_ALLOW_TRAILING_COMMAS)) != 0) { + zend_argument_value_error(3, "must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE, JSON_ALLOW_COMMENTS, JSON_ALLOW_TRAILING_COMMAS)"); RETURN_THROWS(); } diff --git a/ext/json/json.stub.php b/ext/json/json.stub.php index a805c3893dd1..737d341e9ca2 100644 --- a/ext/json/json.stub.php +++ b/ext/json/json.stub.php @@ -90,6 +90,17 @@ */ const JSON_THROW_ON_ERROR = UNKNOWN; +/** + * @var int + * @cvalue PHP_JSON_ALLOW_COMMENTS + */ +const JSON_ALLOW_COMMENTS = UNKNOWN; +/** + * @var int + * @cvalue PHP_JSON_ALLOW_TRAILING_COMMAS + */ +const JSON_ALLOW_TRAILING_COMMAS = UNKNOWN; + /** * @var int * @cvalue PHP_JSON_ERROR_NONE diff --git a/ext/json/json_arginfo.h b/ext/json/json_arginfo.h index 87ba9cce3afd..906491686cbd 100644 --- a/ext/json/json_arginfo.h +++ b/ext/json/json_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit json.stub.php instead. - * Stub hash: 0ceb50047401c4b9e878c09cc518eacc274f7fff */ + * Stub hash: 405dc5ea03cb0ed560c3717cad38203b2f55d870 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_json_encode, 0, 1, MAY_BE_STRING|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) @@ -68,6 +68,8 @@ static void register_json_symbols(int module_number) REGISTER_LONG_CONSTANT("JSON_INVALID_UTF8_IGNORE", PHP_JSON_INVALID_UTF8_IGNORE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("JSON_INVALID_UTF8_SUBSTITUTE", PHP_JSON_INVALID_UTF8_SUBSTITUTE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("JSON_THROW_ON_ERROR", PHP_JSON_THROW_ON_ERROR, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("JSON_ALLOW_COMMENTS", PHP_JSON_ALLOW_COMMENTS, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("JSON_ALLOW_TRAILING_COMMAS", PHP_JSON_ALLOW_TRAILING_COMMAS, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("JSON_ERROR_NONE", PHP_JSON_ERROR_NONE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("JSON_ERROR_DEPTH", PHP_JSON_ERROR_DEPTH, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("JSON_ERROR_STATE_MISMATCH", PHP_JSON_ERROR_STATE_MISMATCH, CONST_PERSISTENT); diff --git a/ext/json/json_parser.y b/ext/json/json_parser.y index 0fd3e2c4e364..742f7141042f 100644 --- a/ext/json/json_parser.y +++ b/ext/json/json_parser.y @@ -119,6 +119,17 @@ members: } } | member + | member ',' + { + if (!(parser->scanner.options & PHP_JSON_ALLOW_TRAILING_COMMAS)) { + if (!parser->scanner.errcode) { + parser->scanner.errcode = PHP_JSON_ERROR_SYNTAX; + } + zval_ptr_dtor_nogc(&$1); + YYERROR; + } + ZVAL_COPY_VALUE(&$$, &$1); + } ; member: @@ -175,6 +186,17 @@ elements: } } | element + | element ',' + { + if (!(parser->scanner.options & PHP_JSON_ALLOW_TRAILING_COMMAS)) { + if (!parser->scanner.errcode) { + parser->scanner.errcode = PHP_JSON_ERROR_SYNTAX; + } + zval_ptr_dtor_nogc(&$1); + YYERROR; + } + ZVAL_COPY_VALUE(&$$, &$1); + } ; element: diff --git a/ext/json/json_scanner.re b/ext/json/json_scanner.re index be62875a00e0..6821b4140d58 100644 --- a/ext/json/json_scanner.re +++ b/ext/json/json_scanner.re @@ -123,6 +123,8 @@ std: EXP = ( INT | FLOAT ) [eE] [+-]? DIGIT+ ; NL = "\r"? "\n" ; WS = [ \t\r]+ ; + CMT_SL = "//" [^\r\n\x00]* ; + CMT_ML = "/*" ( [^*\x00] | ( "*"+ [^*/\x00] ) )* "*"+ "/" ; EOI = "\000"; CTRL = [\x00-\x1F] ; UTF8T = [\x80-\xBF] ; @@ -203,6 +205,27 @@ std: goto std; } WS { goto std; } + CMT_SL { + if (!(s->options & PHP_JSON_ALLOW_COMMENTS)) { + s->errcode = PHP_JSON_ERROR_SYNTAX; + return PHP_JSON_T_ERROR; + } + goto std; + } + CMT_ML { + php_json_ctype *p; + if (!(s->options & PHP_JSON_ALLOW_COMMENTS)) { + s->errcode = PHP_JSON_ERROR_SYNTAX; + return PHP_JSON_T_ERROR; + } + for (p = s->token; p < s->cursor; p++) { + if (*p == '\n') { + s->line++; + s->line_start = p + 1; + } + } + goto std; + } EOI { if (s->limit < s->cursor) { return PHP_JSON_T_EOI; diff --git a/ext/json/php_json.h b/ext/json/php_json.h index f20b20964a71..0145a0dfaa6c 100644 --- a/ext/json/php_json.h +++ b/ext/json/php_json.h @@ -89,6 +89,10 @@ static inline void php_json_error_details_clear(php_json_error_details *out) { #define PHP_JSON_INVALID_UTF8_SUBSTITUTE (1<<21) #define PHP_JSON_THROW_ON_ERROR (1<<22) +/* json_validate() and json_decode() common options */ +#define PHP_JSON_ALLOW_COMMENTS (1<<23) +#define PHP_JSON_ALLOW_TRAILING_COMMAS (1<<24) + /* default depth */ #define PHP_JSON_PARSER_DEFAULT_DEPTH 512 diff --git a/ext/json/tests/json_decode_comments.phpt b/ext/json/tests/json_decode_comments.phpt new file mode 100644 index 000000000000..7c47ffd8687f --- /dev/null +++ b/ext/json/tests/json_decode_comments.phpt @@ -0,0 +1,84 @@ +--TEST-- +json_decode() with JSON_ALLOW_COMMENTS - valid comment usage +--FILE-- + +--EXPECT-- +int(1) +int(1) +int(1) +int(1) +int(1) +int(1) +int(1) +int(1) +array(2) { + [0]=> + int(1) + [1]=> + int(2) +} +array(2) { + ["a"]=> + int(1) + ["b"]=> + int(2) +} +string(16) "// not a comment" +string(19) "/* not a comment */" +array(0) { +} +array(0) { +} +array(0) { +} +array(0) { +} +array(0) { +} +array(2) { + [0]=> + int(1) + [1]=> + int(2) +} +int(1) +int(1) +bool(true) +bool(true) +bool(true) diff --git a/ext/json/tests/json_decode_comments_errors.phpt b/ext/json/tests/json_decode_comments_errors.phpt new file mode 100644 index 000000000000..7cb8cd224107 --- /dev/null +++ b/ext/json/tests/json_decode_comments_errors.phpt @@ -0,0 +1,69 @@ +--TEST-- +json_decode() with JSON_ALLOW_COMMENTS - malformed comments and other errors +--FILE-- +getCode(), " ", $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +NULL +4: Syntax error near location 1:1 +NULL +4: Syntax error near location 1:1 +NULL +4: Syntax error near location 2:2 +NULL +4: Syntax error near location 1:1 +NULL +4: Syntax error near location 1:1 +NULL +4: Syntax error near location 1:10 +NULL +4: Syntax error near location 1:1 +NULL +4: Syntax error near location 1:2 +NULL +4: Syntax error near location 1:4 +NULL +4: Syntax error near location 1:6 +NULL +4: Syntax error near location 2:1 +NULL +4: Syntax error near location 1:3 +NULL +4: Syntax error near location 1:7 +NULL +4: Syntax error near location 1:12 +JsonException: 4 Syntax error near location 1:1 diff --git a/ext/json/tests/json_decode_comments_trailing_flags_off.phpt b/ext/json/tests/json_decode_comments_trailing_flags_off.phpt new file mode 100644 index 000000000000..32b86762a1b5 --- /dev/null +++ b/ext/json/tests/json_decode_comments_trailing_flags_off.phpt @@ -0,0 +1,61 @@ +--TEST-- +JSONC syntax is rejected when JSON_ALLOW_COMMENTS / JSON_ALLOW_TRAILING_COMMAS are not set +--FILE-- + +--EXPECT-- +NULL +4: Syntax error near location 1:1 +NULL +4: Syntax error near location 1:1 +NULL +4: Syntax error near location 1:1 +NULL +4: Syntax error near location 1:8 +NULL +4: Syntax error near location 1:4 +NULL +4: Syntax error near location 1:8 +NULL +4: Syntax error near location 1:7 +NULL +4: Syntax error near location 1:4 +NULL +4: Syntax error near location 1:5 +NULL +3: Control character error, possibly incorrectly encoded near location 1:4 +NULL +5: Malformed UTF-8 characters, possibly incorrectly encoded near location 1:4 +NULL +10: Single unpaired UTF-16 surrogate in unicode escape near location 1:4 +NULL +3: Control character error, possibly incorrectly encoded near location 1:8 +bool(false) +bool(false) diff --git a/ext/json/tests/json_decode_jsonc_combined.phpt b/ext/json/tests/json_decode_jsonc_combined.phpt new file mode 100644 index 000000000000..5a704e21f6cb --- /dev/null +++ b/ext/json/tests/json_decode_jsonc_combined.phpt @@ -0,0 +1,59 @@ +--TEST-- +json_decode() with JSON_ALLOW_COMMENTS | JSON_ALLOW_TRAILING_COMMAS - full JSONC documents +--FILE-- + +--EXPECT-- +array(4) { + ["host"]=> + string(9) "localhost" + ["port"]=> + int(8080) + ["features"]=> + array(2) { + [0]=> + string(5) "alpha" + [1]=> + string(4) "beta" + } + ["debug"]=> + bool(true) +} +bool(true) +array(1) { + [0]=> + int(1) +} +array(1) { + ["a"]=> + int(1) +} +NULL +4: Syntax error near location 2:1 diff --git a/ext/json/tests/json_decode_trailing_commas.phpt b/ext/json/tests/json_decode_trailing_commas.phpt new file mode 100644 index 000000000000..6268c5b6930d --- /dev/null +++ b/ext/json/tests/json_decode_trailing_commas.phpt @@ -0,0 +1,123 @@ +--TEST-- +json_decode() with JSON_ALLOW_TRAILING_COMMAS +--FILE-- +getCode(), " ", $e->getMessage(), "\n"; +} +try { + json_decode("[1,,]", true, 512, JSON_ALLOW_TRAILING_COMMAS | JSON_THROW_ON_ERROR); +} catch (JsonException $e) { + echo "JsonException: ", $e->getCode(), " ", $e->getMessage(), "\n"; +} +// validate mirrors decode +var_dump(json_validate("[1,]", 512, JSON_ALLOW_TRAILING_COMMAS)); +var_dump(json_validate("{\"a\":1,}", 512, JSON_ALLOW_TRAILING_COMMAS)); +var_dump(json_validate("[1,,]", 512, JSON_ALLOW_TRAILING_COMMAS)); + +?> +--EXPECTF-- +array(1) { + [0]=> + int(1) +} +0: No error +array(1) { + [0]=> + int(1) +} +0: No error +array(1) { + [0]=> + int(1) +} +0: No error +array(1) { + [0]=> + string(1) "a" +} +0: No error +array(1) { + ["a"]=> + int(1) +} +0: No error +object(stdClass)#%d (1) { + ["a"]=> + int(1) +} +0: No error +array(1) { + [0]=> + array(1) { + [0]=> + int(1) + } +} +0: No error +array(1) { + ["a"]=> + array(1) { + [0]=> + int(1) + } +} +0: No error +NULL +4: Syntax error near location 1:2 +NULL +4: Syntax error near location 1:2 +NULL +4: Syntax error near location 1:2 +NULL +4: Syntax error near location 1:2 +NULL +4: Syntax error near location 1:4 +NULL +4: Syntax error near location 1:4 +NULL +4: Syntax error near location 1:8 +NULL +2: State mismatch (invalid or malformed JSON) near location 1:8 +NULL +2: State mismatch (invalid or malformed JSON) near location 1:4 +NULL +4: Syntax error near location 1:7 +array(1) { + [0]=> + int(1) +} +JsonException: 4 Syntax error near location 1:4 +bool(true) +bool(true) +bool(false) diff --git a/ext/json/tests/json_last_error_msg_error_location_011.phpt b/ext/json/tests/json_last_error_msg_error_location_011.phpt new file mode 100644 index 000000000000..91af2aa5038a --- /dev/null +++ b/ext/json/tests/json_last_error_msg_error_location_011.phpt @@ -0,0 +1,83 @@ +--TEST-- +json_last_error_msg() - Error location reporting with JSONC comments +--FILE-- + +--EXPECT-- +Error on line 4, column 20: +bool(false) +int(4) +string(31) "Syntax error near location 4:20" + +Error on line 3, column 4: +bool(false) +int(4) +string(30) "Syntax error near location 3:4" + +Error on line 1, column 12: +bool(false) +int(4) +string(31) "Syntax error near location 1:12" + +Error on line 1, column 22: +bool(false) +int(4) +string(31) "Syntax error near location 1:22" + +Error on line 3, column 3: +bool(false) +int(4) +string(30) "Syntax error near location 3:3" + +Error on line 1, column 13: +bool(false) +int(4) +string(31) "Syntax error near location 1:13" + +Error on line 3, column 8: +bool(false) +int(4) +string(30) "Syntax error near location 3:8" diff --git a/ext/json/tests/json_validate_002.phpt b/ext/json/tests/json_validate_002.phpt index 423564c4ad7c..5ab1adca7c2b 100644 --- a/ext/json/tests/json_validate_002.phpt +++ b/ext/json/tests/json_validate_002.phpt @@ -15,6 +15,12 @@ json_validate_trycatchdump("-", 512, JSON_BIGINT_AS_STRING); json_validate_trycatchdump("-", 512, JSON_BIGINT_AS_STRING | JSON_INVALID_UTF8_IGNORE); json_validate_trycatchdump("-", 512, JSON_INVALID_UTF8_IGNORE); json_validate_trycatchdump("{}", 512, JSON_INVALID_UTF8_IGNORE); +json_validate_trycatchdump("[1,] // ok", 512, JSON_ALLOW_COMMENTS | JSON_ALLOW_TRAILING_COMMAS); +json_validate_trycatchdump("// c\n{}", 512, JSON_ALLOW_COMMENTS); +json_validate_trycatchdump("[1,]", 512, JSON_ALLOW_TRAILING_COMMAS); +json_validate_trycatchdump("[1,]", 512, JSON_ALLOW_COMMENTS); +json_validate_trycatchdump("// c\n{}", 512, JSON_ALLOW_TRAILING_COMMAS); +json_validate_trycatchdump("{}", 512, JSON_ALLOW_COMMENTS | JSON_BIGINT_AS_STRING); ?> --EXPECTF-- @@ -36,10 +42,10 @@ string(8) "No error" Error: 0 json_validate(): Argument #2 ($depth) must be greater than 0 int(0) string(8) "No error" -Error: 0 json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE) +Error: 0 json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE, JSON_ALLOW_COMMENTS, JSON_ALLOW_TRAILING_COMMAS) int(0) string(8) "No error" -Error: 0 json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE) +Error: 0 json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE, JSON_ALLOW_COMMENTS, JSON_ALLOW_TRAILING_COMMAS) int(0) string(8) "No error" bool(false) @@ -48,3 +54,21 @@ string(30) "Syntax error near location 1:1" bool(true) int(0) string(8) "No error" +bool(true) +int(0) +string(8) "No error" +bool(true) +int(0) +string(8) "No error" +bool(true) +int(0) +string(8) "No error" +bool(false) +int(4) +string(30) "Syntax error near location 1:4" +bool(false) +int(4) +string(30) "Syntax error near location 1:1" +Error: 0 json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE, JSON_ALLOW_COMMENTS, JSON_ALLOW_TRAILING_COMMAS) +int(4) +string(30) "Syntax error near location 1:1"