diff --git a/ext/json/json.c b/ext/json/json.c index 4fac95fab39fe..d5f538416a0c8 100644 --- a/ext/json/json.c +++ b/ext/json/json.c @@ -157,11 +157,12 @@ static const char *php_json_get_error_msg(php_json_error_code error_code) /* {{{ } /* }}} */ -PHP_JSON_API zend_result php_json_decode_ex(zval *return_value, const char *str, size_t str_len, zend_long options, zend_long depth) /* {{{ */ +/* {{{ */ +PHP_JSON_API zend_result php_json_decode_ex(zval *return_value, const char *str, size_t str_len, zend_long options, zend_long depth) { php_json_parser parser; - php_json_parser_init(&parser, return_value, str, str_len, (int)options, (int)depth); + php_json_parser_init(&parser, return_value, str, str_len, (int)options, (int)depth, NULL); if (php_json_yyparse(&parser)) { php_json_error_code error_code = php_json_parser_error_code(&parser); @@ -178,6 +179,31 @@ PHP_JSON_API zend_result php_json_decode_ex(zval *return_value, const char *str, } /* }}} */ +/* {{{ */ +PHP_JSON_API zend_result php_is_json_ex(zval *return_value, const char *str, size_t str_len, zend_long options, zend_long depth) +{ + php_json_parser parser; + + + static const php_json_parser_methods parser_methods = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }; + + php_json_parser_init(&parser, return_value, str, str_len, (int)options, (int)depth, &parser_methods); + + if (php_json_yyparse(&parser)) { + php_json_error_code error_code = php_json_parser_error_code(&parser); + if (!(options & PHP_JSON_THROW_ON_ERROR)) { + JSON_G(error_code) = error_code; + } else { + zend_throw_exception(php_json_exception_ce, php_json_get_error_msg(error_code), error_code); + } + RETVAL_FALSE; + return FAILURE; + } + + return SUCCESS; +} +/* }}} */ + /* {{{ Returns the JSON representation of a value */ PHP_FUNCTION(json_encode) { @@ -216,6 +242,66 @@ PHP_FUNCTION(json_encode) } /* }}} */ +/* {{{ Validates if an string contains a valid json */ +PHP_FUNCTION(is_json) +{ + char *str; + size_t str_len; + zend_long depth = PHP_JSON_PARSER_DEFAULT_DEPTH; + zend_long options = 0; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_STRING(str, str_len) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(depth) + Z_PARAM_LONG(options) + ZEND_PARSE_PARAMETERS_END(); + + + if (options != 0) { + zend_long tmp_options = options; + tmp_options ^= (PHP_JSON_INVALID_UTF8_IGNORE | PHP_JSON_THROW_ON_ERROR); + + if (tmp_options != 0) { + if (!((tmp_options == PHP_JSON_INVALID_UTF8_IGNORE) || (tmp_options == PHP_JSON_THROW_ON_ERROR))) { + zend_argument_value_error(3, "must be a valid flag (JSON_THROW_ON_ERROR, JSON_INVALID_UTF8_IGNORE)"); + RETURN_THROWS(); + } + } + } + + if (!str_len) { + if (!(options & PHP_JSON_THROW_ON_ERROR)) { + JSON_G(error_code) = PHP_JSON_ERROR_SYNTAX; + } else { + zend_throw_exception(php_json_exception_ce, php_json_get_error_msg(PHP_JSON_ERROR_SYNTAX), PHP_JSON_ERROR_SYNTAX); + } + + RETURN_FALSE; + } + + if (!(options & PHP_JSON_THROW_ON_ERROR)) { + JSON_G(error_code) = PHP_JSON_ERROR_NONE; + } + + if (depth <= 0) { + zend_argument_value_error(2, "must be greater than 0"); + RETURN_THROWS(); + } + + if (depth > INT_MAX) { + zend_argument_value_error(2, "must be less than %d", INT_MAX); + RETURN_THROWS(); + } + + if (php_is_json_ex(return_value, str, str_len, options, depth) == SUCCESS) { + RETURN_TRUE; + } + + RETURN_FALSE; +} +/* }}} */ + /* {{{ Decodes the JSON representation into a PHP value */ PHP_FUNCTION(json_decode) { diff --git a/ext/json/json.stub.php b/ext/json/json.stub.php index 7a8b479ab7657..fc7396493e8ff 100644 --- a/ext/json/json.stub.php +++ b/ext/json/json.stub.php @@ -156,6 +156,8 @@ function json_encode(mixed $value, int $flags = 0, int $depth = 512): string|fal function json_decode(string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed {} +function is_json(string $json, int $depth = 512, int $flags = 0): bool {} + function json_last_error(): int {} /** @refcount 1 */ diff --git a/ext/json/json_arginfo.h b/ext/json/json_arginfo.h index 8c88f24c403cd..3530bf12367c6 100644 --- a/ext/json/json_arginfo.h +++ b/ext/json/json_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 91b1a992a7020081c42e1db876e5cdce94b681dd */ + * Stub hash: 83310b1415a30a6593cef4e0b27f3f323fea5a8b */ 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) @@ -14,6 +14,12 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_json_decode, 0, 1, IS_MIXED, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_is_json, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, json, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, depth, IS_LONG, 0, "512") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_json_last_error, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() @@ -26,6 +32,7 @@ ZEND_END_ARG_INFO() ZEND_FUNCTION(json_encode); ZEND_FUNCTION(json_decode); +ZEND_FUNCTION(is_json); ZEND_FUNCTION(json_last_error); ZEND_FUNCTION(json_last_error_msg); @@ -33,6 +40,7 @@ ZEND_FUNCTION(json_last_error_msg); static const zend_function_entry ext_functions[] = { ZEND_FE(json_encode, arginfo_json_encode) ZEND_FE(json_decode, arginfo_json_decode) + ZEND_FE(is_json, arginfo_is_json) ZEND_FE(json_last_error, arginfo_json_last_error) ZEND_FE(json_last_error_msg, arginfo_json_last_error_msg) ZEND_FE_END diff --git a/ext/json/json_parser.y b/ext/json/json_parser.y index 90ff0a7c758fe..fa9efe66ef82d 100644 --- a/ext/json/json_parser.y +++ b/ext/json/json_parser.y @@ -115,10 +115,14 @@ object_end: members: %empty { - if ((parser->scanner.options & PHP_JSON_OBJECT_AS_ARRAY) && parser->methods.object_create == php_json_parser_object_create) { - ZVAL_EMPTY_ARRAY(&$$); + if (parser->methods.object_create) { + if ((parser->scanner.options & PHP_JSON_OBJECT_AS_ARRAY) && parser->methods.object_create == php_json_parser_object_create) { + ZVAL_EMPTY_ARRAY(&$$); + } else { + parser->methods.object_create(parser, &$$); + } } else { - parser->methods.object_create(parser, &$$); + ZVAL_EMPTY_ARRAY(&$$); } } | member @@ -127,14 +131,17 @@ members: member: key ':' value { - parser->methods.object_create(parser, &$$); - if (parser->methods.object_update(parser, &$$, Z_STR($1), &$3) == FAILURE) { + if (parser->methods.object_create) { + parser->methods.object_create(parser, &$$); + } + + if (parser->methods.object_update && parser->methods.object_update(parser, &$$, Z_STR($1), &$3) == FAILURE) { YYERROR; } } | member ',' key ':' value { - if (parser->methods.object_update(parser, &$1, Z_STR($3), &$5) == FAILURE) { + if (parser->methods.object_update && parser->methods.object_update(parser, &$1, Z_STR($3), &$5) == FAILURE) { YYERROR; } ZVAL_COPY_VALUE(&$$, &$1); @@ -171,10 +178,14 @@ array_end: elements: %empty { - if (parser->methods.array_create == php_json_parser_array_create) { - ZVAL_EMPTY_ARRAY(&$$); + if (parser->methods.array_create) { + if (parser->methods.array_create == php_json_parser_array_create) { + ZVAL_EMPTY_ARRAY(&$$); + } else { + parser->methods.array_create(parser, &$$); + } } else { - parser->methods.array_create(parser, &$$); + ZVAL_EMPTY_ARRAY(&$$); } } | element @@ -183,12 +194,19 @@ elements: element: value { - parser->methods.array_create(parser, &$$); - parser->methods.array_append(parser, &$$, &$1); + if (parser->methods.array_create) { + parser->methods.array_create(parser, &$$); + } + + if (parser->methods.array_append) { + parser->methods.array_append(parser, &$$, &$1); + } } | element ',' value { - parser->methods.array_append(parser, &$1, &$3); + if (parser->methods.array_append) { + parser->methods.array_append(parser, &$1, &$3); + } ZVAL_COPY_VALUE(&$$, &$1); } ; @@ -259,6 +277,18 @@ static int php_json_yylex(union YYSTYPE *value, php_json_parser *parser) { int token = php_json_scan(&parser->scanner); value->value = parser->scanner.value; + + if (!parser->methods.array_create + && !parser->methods.array_append + && !parser->methods.array_start + && !parser->methods.array_end + && !parser->methods.object_create + && !parser->methods.object_update + && !parser->methods.object_start + && !parser->methods.object_end) { + zval_ptr_dtor_str(&(parser->scanner.value)); + } + return token; } @@ -307,8 +337,13 @@ PHP_JSON_API void php_json_parser_init(php_json_parser *parser, const char *str, size_t str_len, int options, - int max_depth) + int max_depth, + const php_json_parser_methods *parser_methods) { + if (parser_methods == NULL) { + parser_methods = &default_parser_methods; + } + php_json_parser_init_ex( parser, return_value, @@ -316,7 +351,7 @@ PHP_JSON_API void php_json_parser_init(php_json_parser *parser, str_len, options, max_depth, - &default_parser_methods); + parser_methods); } PHP_JSON_API int php_json_parse(php_json_parser *parser) diff --git a/ext/json/php_json.h b/ext/json/php_json.h index d4d8ac421f886..d0c027b7f882c 100644 --- a/ext/json/php_json.h +++ b/ext/json/php_json.h @@ -72,11 +72,13 @@ typedef enum { #define PHP_JSON_PRESERVE_ZERO_FRACTION (1<<10) #define PHP_JSON_UNESCAPED_LINE_TERMINATORS (1<<11) -/* json_decode() and json_encode() common options */ +/* is_json(), json_decode() and json_encode() common options */ #define PHP_JSON_INVALID_UTF8_IGNORE (1<<20) -#define PHP_JSON_INVALID_UTF8_SUBSTITUTE (1<<21) #define PHP_JSON_THROW_ON_ERROR (1<<22) +/* json_decode() and json_encode() common options */ +#define PHP_JSON_INVALID_UTF8_SUBSTITUTE (1<<21) + /* Internal flags */ #define PHP_JSON_OUTPUT_ARRAY 0 #define PHP_JSON_OUTPUT_OBJECT 1 @@ -100,6 +102,7 @@ ZEND_TSRMLS_CACHE_EXTERN() PHP_JSON_API zend_result php_json_encode_ex(smart_str *buf, zval *val, int options, zend_long depth); PHP_JSON_API zend_result php_json_encode(smart_str *buf, zval *val, int options); PHP_JSON_API zend_result php_json_decode_ex(zval *return_value, const char *str, size_t str_len, zend_long options, zend_long depth); +PHP_JSON_API zend_result php_is_json_ex(zval *return_value, const char *str, size_t str_len, zend_long options, zend_long depth); static inline zend_result php_json_decode(zval *return_value, const char *str, size_t str_len, bool assoc, zend_long depth) { diff --git a/ext/json/php_json_parser.h b/ext/json/php_json_parser.h index c17cf708d35a5..3a86b77dcb8df 100644 --- a/ext/json/php_json_parser.h +++ b/ext/json/php_json_parser.h @@ -73,7 +73,8 @@ PHP_JSON_API void php_json_parser_init( const char *str, size_t str_len, int options, - int max_depth); + int max_depth, + const php_json_parser_methods *parser_methods); PHP_JSON_API php_json_error_code php_json_parser_error_code(const php_json_parser *parser); diff --git a/ext/json/tests/is_json_001.phpt b/ext/json/tests/is_json_001.phpt new file mode 100644 index 0000000000000..7de79e9f6e81f --- /dev/null +++ b/ext/json/tests/is_json_001.phpt @@ -0,0 +1,41 @@ +--TEST-- +is_json() - General usage +--FILE-- +"), + is_json(";"), + is_json("руссиш"), + is_json("blah"), + is_json('{ "": "": "" } }'), + is_json('{ "": { "": "" }'), + is_json('{ "test": {} "foo": "bar" }, "test2": {"foo" : "bar" }, "test2": {"foo" : "bar" } }'), + + is_json('{ "test": { "foo": "bar" } }'), + is_json('{ "test": { "foo": "" } }'), + is_json('{ "": { "foo": "" } }'), + is_json('{ "": { "": "" } }'), + is_json('{ "test": {"foo": "bar"}, "test2": {"foo" : "bar" }, "test2": {"foo" : "bar" } }'), + is_json('{ "test": {"foo": "bar"}, "test2": {"foo" : "bar" }, "test3": {"foo" : "bar" } }'), +); + +?> +--EXPECTF-- +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/json/tests/is_json_002.phpt b/ext/json/tests/is_json_002.phpt new file mode 100644 index 0000000000000..efa7ed299b9eb --- /dev/null +++ b/ext/json/tests/is_json_002.phpt @@ -0,0 +1,113 @@ +--TEST-- +is_json() - Error handling +--FILE-- + +--EXPECTF-- +*** Flag JSON_THROW_ON_ERROR Off + +Deprecated: is_json(): Passing null to parameter #1 ($json) of type string is deprecated in %s on line %d +bool(false) +int(4) +string(12) "Syntax error" +bool(false) +int(4) +string(12) "Syntax error" +bool(false) +int(4) +string(12) "Syntax error" +bool(false) +int(4) +string(12) "Syntax error" +bool(false) +int(1) +string(28) "Maximum stack depth exceeded" +bool(true) +int(0) +string(8) "No error" +Error: 0 is_json(): Argument #2 ($depth) must be greater than 0 +int(0) +string(8) "No error" +Error: 0 is_json(): Argument #3 ($flags) must be a valid flag (JSON_THROW_ON_ERROR, JSON_INVALID_UTF8_IGNORE) +int(0) +string(8) "No error" +Error: 0 is_json(): Argument #3 ($flags) must be a valid flag (JSON_THROW_ON_ERROR, JSON_INVALID_UTF8_IGNORE) +int(0) +string(8) "No error" +bool(false) +int(4) +string(12) "Syntax error" +bool(true) +int(0) +string(8) "No error" + +*** Flag JSON_THROW_ON_ERROR On + +Deprecated: is_json(): Passing null to parameter #1 ($json) of type string is deprecated in %s on line %d +JsonException: 4 Syntax error +int(0) +string(8) "No error" +JsonException: 4 Syntax error +int(0) +string(8) "No error" +JsonException: 4 Syntax error +int(0) +string(8) "No error" +JsonException: 4 Syntax error +int(0) +string(8) "No error" +JsonException: 1 Maximum stack depth exceeded +int(0) +string(8) "No error" +bool(true) +int(0) +string(8) "No error" +Error: 0 is_json(): Argument #2 ($depth) must be greater than 0 +int(0) +string(8) "No error" +Error: 0 is_json(): Argument #3 ($flags) must be a valid flag (JSON_THROW_ON_ERROR, JSON_INVALID_UTF8_IGNORE) +int(0) +string(8) "No error" +Error: 0 is_json(): Argument #3 ($flags) must be a valid flag (JSON_THROW_ON_ERROR, JSON_INVALID_UTF8_IGNORE) +int(0) +string(8) "No error" +JsonException: 4 Syntax error +int(0) +string(8) "No error" +bool(true) +int(0) +string(8) "No error" +bool(true) +int(0) +string(8) "No error" diff --git a/ext/json/tests/is_json_003.phpt b/ext/json/tests/is_json_003.phpt new file mode 100644 index 0000000000000..9f6e31ba16313 --- /dev/null +++ b/ext/json/tests/is_json_003.phpt @@ -0,0 +1,19 @@ +--TEST-- +is_json() - Error handling for max depth +--SKIPIF-- + +--FILE-- +getMessage() . PHP_EOL; + var_dump(json_last_error(), json_last_error_msg()); +} + +?> +--EXPECTF-- +is_json(): Argument #2 ($depth) must be less than %d +int(0) +string(8) "No error" diff --git a/ext/json/tests/is_json_004.phpt b/ext/json/tests/is_json_004.phpt new file mode 100644 index 0000000000000..843adcdab6f4b --- /dev/null +++ b/ext/json/tests/is_json_004.phpt @@ -0,0 +1,81 @@ +--TEST-- +is_json() - Invalid UTF-8's +--FILE-- + +--EXPECTF-- +Testing Invalid UTF-8 +bool(false) +int(5) +string(56) "Malformed UTF-8 characters, possibly incorrectly encoded" +bool(false) +int(5) +string(56) "Malformed UTF-8 characters, possibly incorrectly encoded" +bool(false) +int(5) +string(56) "Malformed UTF-8 characters, possibly incorrectly encoded" +bool(false) +int(5) +string(56) "Malformed UTF-8 characters, possibly incorrectly encoded" +JsonException: 5 Malformed UTF-8 characters, possibly incorrectly encoded +int(5) +string(56) "Malformed UTF-8 characters, possibly incorrectly encoded" +JsonException: 5 Malformed UTF-8 characters, possibly incorrectly encoded +int(5) +string(56) "Malformed UTF-8 characters, possibly incorrectly encoded" +JsonException: 5 Malformed UTF-8 characters, possibly incorrectly encoded +int(5) +string(56) "Malformed UTF-8 characters, possibly incorrectly encoded" +JsonException: 5 Malformed UTF-8 characters, possibly incorrectly encoded +int(5) +string(56) "Malformed UTF-8 characters, possibly incorrectly encoded" +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(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" diff --git a/ext/json/tests/is_json_005.phpt b/ext/json/tests/is_json_005.phpt new file mode 100644 index 0000000000000..932fd0cf519b0 --- /dev/null +++ b/ext/json/tests/is_json_005.phpt @@ -0,0 +1,63 @@ +--TEST-- +is_json() - Tests is_json() performance +--INI-- +memory_limit=128M +--FILE-- + {$is_json_memory_usage} MB < {$json_decode_memory_usage} MB = ". (($is_json_memory_usage < $json_decode_memory_usage) ? "TRUE" : "FALSE") . PHP_EOL; +echo "is_json() uses less memory peak than json_decode() => {$is_json_memory_peak_usage} MB < {$json_decode_memory_peak_usage} MB = ". (($is_json_memory_peak_usage < $json_decode_memory_peak_usage) ? "TRUE" : "FALSE") . PHP_EOL; +echo "is_json() is faster than json_decode() => {$is_json_time} Seconds < {$json_decode_time} Seconds = ". (((float)$is_json_time < (float)$json_decode_time) ? "TRUE" : "FALSE") . PHP_EOL; + +return; + +?> +--EXPECTF-- +is_json() uses less memory than json_decode() => %d MB < %d MB = TRUE +is_json() uses less memory peak than json_decode() => %d MB < %d MB = TRUE +is_json() is faster than json_decode() => %f Seconds < %f Seconds = TRUE diff --git a/ext/json/tests/is_json_requires.inc b/ext/json/tests/is_json_requires.inc new file mode 100644 index 0000000000000..5f379c44e8d7b --- /dev/null +++ b/ext/json/tests/is_json_requires.inc @@ -0,0 +1,17 @@ +getCode()} {$e->getMessage()}". PHP_EOL; + } catch (Exception $e) { + echo "Exception: {$e->getCode()} {$e->getMessage()}". PHP_EOL; + } catch (Error $e) { + echo "Error: {$e->getCode()} {$e->getMessage()}". PHP_EOL; + } + + var_dump(json_last_error(), json_last_error_msg()); +} + +?> diff --git a/sapi/fuzzer/fuzzer-json.c b/sapi/fuzzer/fuzzer-json.c index 4335598bc3caa..10511e53ce3dd 100644 --- a/sapi/fuzzer/fuzzer-json.c +++ b/sapi/fuzzer/fuzzer-json.c @@ -42,7 +42,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { for (int option = 0; option <=1; ++option) { zval result; php_json_parser parser; - php_json_parser_init(&parser, &result, data, Size, option, 10); + php_json_parser_init(&parser, &result, data, Size, option, 10, NULL); if (php_json_yyparse(&parser) == SUCCESS) { zval_ptr_dtor(&result); }