Skip to content

Commit 2e8699f

Browse files
juan-moralesjcm
and
jcm
authored
RFC - json_validate() (#9399)
* Add `json_validate(string $json, int $depth = 512, int $flags = 0): bool` from https://wiki.php.net/rfc/json_validate * In json_validate, use a different set of C no-op functions for creating/updating arrays/objects when validating while reusing the unmodified parser/scanner code * Forbid unsupported flags in json_validate() * Remove test of passing NULL as parameter (normal behavior of https://wiki.php.net/rfc/deprecate_null_to_scalar_internal_arg for internal functions) Co-authored-by: jcm <juan.carlos.morales@tradebyte.com>
1 parent d498908 commit 2e8699f

12 files changed

+362
-2
lines changed

ext/json/json.c

+60
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,24 @@ PHP_JSON_API zend_result php_json_decode_ex(zval *return_value, const char *str,
178178
}
179179
/* }}} */
180180

181+
/* {{{ */
182+
PHP_JSON_API bool php_json_validate_ex(const char *str, size_t str_len, zend_long options, zend_long depth)
183+
{
184+
php_json_parser parser;
185+
zval tmp;
186+
const php_json_parser_methods* parser_validate_methods = php_json_get_validate_methods();
187+
php_json_parser_init_ex(&parser, &tmp, str, str_len, (int)options, (int)depth, parser_validate_methods);
188+
189+
if (php_json_yyparse(&parser)) {
190+
php_json_error_code error_code = php_json_parser_error_code(&parser);
191+
JSON_G(error_code) = error_code;
192+
return false;
193+
}
194+
195+
return true;
196+
}
197+
/* }}} */
198+
181199
/* {{{ Returns the JSON representation of a value */
182200
PHP_FUNCTION(json_encode)
183201
{
@@ -270,6 +288,48 @@ PHP_FUNCTION(json_decode)
270288
}
271289
/* }}} */
272290

291+
/* {{{ Validates if a string contains a valid json */
292+
PHP_FUNCTION(json_validate)
293+
{
294+
char *str;
295+
size_t str_len;
296+
zend_long depth = PHP_JSON_PARSER_DEFAULT_DEPTH;
297+
zend_long options = 0;
298+
299+
ZEND_PARSE_PARAMETERS_START(1, 3)
300+
Z_PARAM_STRING(str, str_len)
301+
Z_PARAM_OPTIONAL
302+
Z_PARAM_LONG(depth)
303+
Z_PARAM_LONG(options)
304+
ZEND_PARSE_PARAMETERS_END();
305+
306+
307+
if ((options != 0) && (options != PHP_JSON_INVALID_UTF8_IGNORE)) {
308+
zend_argument_value_error(3, "must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)");
309+
RETURN_THROWS();
310+
}
311+
312+
if (!str_len) {
313+
JSON_G(error_code) = PHP_JSON_ERROR_SYNTAX;
314+
RETURN_FALSE;
315+
}
316+
317+
JSON_G(error_code) = PHP_JSON_ERROR_NONE;
318+
319+
if (depth <= 0) {
320+
zend_argument_value_error(2, "must be greater than 0");
321+
RETURN_THROWS();
322+
}
323+
324+
if (depth > INT_MAX) {
325+
zend_argument_value_error(2, "must be less than %d", INT_MAX);
326+
RETURN_THROWS();
327+
}
328+
329+
RETURN_BOOL(php_json_validate_ex(str, str_len, options, depth));
330+
}
331+
/* }}} */
332+
273333
/* {{{ Returns the error code of the last json_encode() or json_decode() call. */
274334
PHP_FUNCTION(json_last_error)
275335
{

ext/json/json.stub.php

+2
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ function json_encode(mixed $value, int $flags = 0, int $depth = 512): string|fal
156156

157157
function json_decode(string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed {}
158158

159+
function json_validate(string $json, int $depth = 512, int $flags = 0): bool {}
160+
159161
function json_last_error(): int {}
160162

161163
/** @refcount 1 */

ext/json/json_arginfo.h

+9-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/json/json_parser.y

+47
Original file line numberDiff line numberDiff line change
@@ -255,10 +255,40 @@ static int php_json_parser_object_update(php_json_parser *parser, zval *object,
255255
return SUCCESS;
256256
}
257257

258+
static int php_json_parser_array_create_validate(php_json_parser *parser, zval *array)
259+
{
260+
ZVAL_NULL(array);
261+
return SUCCESS;
262+
}
263+
264+
static int php_json_parser_array_append_validate(php_json_parser *parser, zval *array, zval *zvalue)
265+
{
266+
return SUCCESS;
267+
}
268+
269+
static int php_json_parser_object_create_validate(php_json_parser *parser, zval *object)
270+
{
271+
ZVAL_NULL(object);
272+
return SUCCESS;
273+
}
274+
275+
static int php_json_parser_object_update_validate(php_json_parser *parser, zval *object, zend_string *key, zval *zvalue)
276+
{
277+
return SUCCESS;
278+
}
279+
258280
static int php_json_yylex(union YYSTYPE *value, php_json_parser *parser)
259281
{
260282
int token = php_json_scan(&parser->scanner);
261283
value->value = parser->scanner.value;
284+
285+
if (parser->methods.array_create == php_json_parser_array_create_validate
286+
&& parser->methods.array_append == php_json_parser_array_append_validate
287+
&& parser->methods.object_create == php_json_parser_object_create_validate
288+
&& parser->methods.object_update == php_json_parser_object_update_validate) {
289+
zval_ptr_dtor_str(&(parser->scanner.value));
290+
}
291+
262292
return token;
263293
}
264294

@@ -286,6 +316,18 @@ static const php_json_parser_methods default_parser_methods =
286316
NULL,
287317
};
288318

319+
static const php_json_parser_methods validate_parser_methods =
320+
{
321+
php_json_parser_array_create_validate,
322+
php_json_parser_array_append_validate,
323+
NULL,
324+
NULL,
325+
php_json_parser_object_create_validate,
326+
php_json_parser_object_update_validate,
327+
NULL,
328+
NULL,
329+
};
330+
289331
PHP_JSON_API void php_json_parser_init_ex(php_json_parser *parser,
290332
zval *return_value,
291333
const char *str,
@@ -323,3 +365,8 @@ PHP_JSON_API int php_json_parse(php_json_parser *parser)
323365
{
324366
return php_json_yyparse(parser);
325367
}
368+
369+
const php_json_parser_methods* php_json_get_validate_methods()
370+
{
371+
return &validate_parser_methods;
372+
}

ext/json/php_json.h

+4-1
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ typedef enum {
7272
#define PHP_JSON_PRESERVE_ZERO_FRACTION (1<<10)
7373
#define PHP_JSON_UNESCAPED_LINE_TERMINATORS (1<<11)
7474

75-
/* json_decode() and json_encode() common options */
75+
/* json_validate(), json_decode() and json_encode() common options */
7676
#define PHP_JSON_INVALID_UTF8_IGNORE (1<<20)
77+
78+
/* json_decode() and json_encode() common options */
7779
#define PHP_JSON_INVALID_UTF8_SUBSTITUTE (1<<21)
7880
#define PHP_JSON_THROW_ON_ERROR (1<<22)
7981

@@ -100,6 +102,7 @@ ZEND_TSRMLS_CACHE_EXTERN()
100102
PHP_JSON_API zend_result php_json_encode_ex(smart_str *buf, zval *val, int options, zend_long depth);
101103
PHP_JSON_API zend_result php_json_encode(smart_str *buf, zval *val, int options);
102104
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);
105+
PHP_JSON_API bool php_json_validate_ex(const char *str, size_t str_len, zend_long options, zend_long depth);
103106

104107
static inline zend_result php_json_decode(zval *return_value, const char *str, size_t str_len, bool assoc, zend_long depth)
105108
{

ext/json/php_json_parser.h

+2
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,6 @@ PHP_JSON_API int php_json_parse(php_json_parser *parser);
8181

8282
int php_json_yyparse(php_json_parser *parser);
8383

84+
const php_json_parser_methods* php_json_get_validate_methods(void);
85+
8486
#endif /* PHP_JSON_PARSER_H */

ext/json/tests/json_validate_001.phpt

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
json_validate() - General usage
3+
--FILE--
4+
<?php
5+
6+
var_dump(
7+
json_validate(""),
8+
json_validate("."),
9+
json_validate("<?>"),
10+
json_validate(";"),
11+
json_validate("руссиш"),
12+
json_validate("blah"),
13+
json_validate('{ "": "": "" } }'),
14+
json_validate('{ "": { "": "" }'),
15+
json_validate('{ "test": {} "foo": "bar" }, "test2": {"foo" : "bar" }, "test2": {"foo" : "bar" } }'),
16+
17+
json_validate('{ "test": { "foo": "bar" } }'),
18+
json_validate('{ "test": { "foo": "" } }'),
19+
json_validate('{ "": { "foo": "" } }'),
20+
json_validate('{ "": { "": "" } }'),
21+
json_validate('{ "test": {"foo": "bar"}, "test2": {"foo" : "bar" }, "test2": {"foo" : "bar" } }'),
22+
json_validate('{ "test": {"foo": "bar"}, "test2": {"foo" : "bar" }, "test3": {"foo" : "bar" } }'),
23+
);
24+
25+
?>
26+
--EXPECT--
27+
bool(false)
28+
bool(false)
29+
bool(false)
30+
bool(false)
31+
bool(false)
32+
bool(false)
33+
bool(false)
34+
bool(false)
35+
bool(false)
36+
bool(true)
37+
bool(true)
38+
bool(true)
39+
bool(true)
40+
bool(true)
41+
bool(true)

ext/json/tests/json_validate_002.phpt

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
--TEST--
2+
json_validate() - Error handling
3+
--FILE--
4+
<?php
5+
6+
require_once("json_validate_requires.inc");
7+
8+
json_validate_trycatchdump("");
9+
json_validate_trycatchdump("-");
10+
json_validate_trycatchdump("", -1);
11+
json_validate_trycatchdump('{"key1":"value1", "key2":"value2"}', 1);
12+
json_validate_trycatchdump('{"key1":"value1", "key2":"value2"}', 2);
13+
json_validate_trycatchdump("-", 0);
14+
json_validate_trycatchdump("-", 512, JSON_BIGINT_AS_STRING);
15+
json_validate_trycatchdump("-", 512, JSON_BIGINT_AS_STRING | JSON_INVALID_UTF8_IGNORE);
16+
json_validate_trycatchdump("-", 512, JSON_INVALID_UTF8_IGNORE);
17+
json_validate_trycatchdump("{}", 512, JSON_INVALID_UTF8_IGNORE);
18+
19+
?>
20+
--EXPECTF--
21+
bool(false)
22+
int(4)
23+
string(12) "Syntax error"
24+
bool(false)
25+
int(4)
26+
string(12) "Syntax error"
27+
bool(false)
28+
int(4)
29+
string(12) "Syntax error"
30+
bool(false)
31+
int(1)
32+
string(28) "Maximum stack depth exceeded"
33+
bool(true)
34+
int(0)
35+
string(8) "No error"
36+
Error: 0 json_validate(): Argument #2 ($depth) must be greater than 0
37+
int(0)
38+
string(8) "No error"
39+
Error: 0 json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)
40+
int(0)
41+
string(8) "No error"
42+
Error: 0 json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)
43+
int(0)
44+
string(8) "No error"
45+
bool(false)
46+
int(4)
47+
string(12) "Syntax error"
48+
bool(true)
49+
int(0)
50+
string(8) "No error"

ext/json/tests/json_validate_003.phpt

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--TEST--
2+
json_validate() - Error handling for max depth
3+
--SKIPIF--
4+
<?php if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platform only"); ?>
5+
--FILE--
6+
<?php
7+
8+
try {
9+
var_dump(json_validate("-", PHP_INT_MAX));
10+
} catch (ValueError $error) {
11+
echo $error->getMessage() . PHP_EOL;
12+
var_dump(json_last_error(), json_last_error_msg());
13+
}
14+
15+
?>
16+
--EXPECTF--
17+
json_validate(): Argument #2 ($depth) must be less than %d
18+
int(0)
19+
string(8) "No error"

ext/json/tests/json_validate_004.phpt

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
json_validate() - Invalid UTF-8's
3+
--FILE--
4+
<?php
5+
6+
require_once("json_validate_requires.inc");
7+
8+
echo "Testing Invalid UTF-8" . PHP_EOL;
9+
10+
11+
json_validate_trycatchdump("\"a\xb0b\"");
12+
json_validate_trycatchdump("\"a\xd0\xf2b\"");
13+
json_validate_trycatchdump("\"\x61\xf0\x80\x80\x41\"");
14+
json_validate_trycatchdump("[\"\xc1\xc1\",\"a\"]");
15+
16+
json_validate_trycatchdump("\"a\xb0b\"", 512, JSON_INVALID_UTF8_IGNORE);
17+
json_validate_trycatchdump("\"a\xd0\xf2b\"", 512, JSON_INVALID_UTF8_IGNORE);
18+
json_validate_trycatchdump("\"\x61\xf0\x80\x80\x41\"", 512, JSON_INVALID_UTF8_IGNORE);
19+
json_validate_trycatchdump("[\"\xc1\xc1\",\"a\"]", 512, JSON_INVALID_UTF8_IGNORE);
20+
21+
?>
22+
--EXPECT--
23+
Testing Invalid UTF-8
24+
bool(false)
25+
int(5)
26+
string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"
27+
bool(false)
28+
int(5)
29+
string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"
30+
bool(false)
31+
int(5)
32+
string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"
33+
bool(false)
34+
int(5)
35+
string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"
36+
bool(true)
37+
int(0)
38+
string(8) "No error"
39+
bool(true)
40+
int(0)
41+
string(8) "No error"
42+
bool(true)
43+
int(0)
44+
string(8) "No error"
45+
bool(true)
46+
int(0)
47+
string(8) "No error"

0 commit comments

Comments
 (0)