From 205cd4b4d5e894dbf14666a7013778f902fcc540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 24 Jun 2026 10:59:13 +0200 Subject: [PATCH] json: Store errors in a structured fashion This will make it both easier to consume the error information and reduce the chance for mistakes when settings errors, because not every field will need to be set manually. The functions that have been removed as part of this change were added in master only, thus removing those is not an API change between the latest stable (PHP 8.5) and this PR. Fixes php/php-src#22420. --- ext/json/json.c | 82 +++++++++++++++++-------------------- ext/json/json_parser.y | 11 ++--- ext/json/php_json.h | 16 ++++++-- ext/json/php_json_parser.h | 4 +- ext/json/tests/gh22420.phpt | 16 ++++++++ 5 files changed, 72 insertions(+), 57 deletions(-) create mode 100644 ext/json/tests/gh22420.phpt diff --git a/ext/json/json.c b/ext/json/json.c index 3ba53e959c5d..ac033c057ac4 100644 --- a/ext/json/json.c +++ b/ext/json/json.c @@ -60,18 +60,14 @@ static PHP_GINIT_FUNCTION(json) ZEND_TSRMLS_CACHE_UPDATE(); #endif json_globals->encoder_depth = 0; - json_globals->error_code = 0; - json_globals->error_line = 0; - json_globals->error_column = 0; + php_json_error_details_clear(&json_globals->error_details); json_globals->encode_max_depth = PHP_JSON_PARSER_DEFAULT_DEPTH; } /* }}} */ static PHP_RINIT_FUNCTION(json) { - JSON_G(error_code) = 0; - JSON_G(error_line) = 0; - JSON_G(error_column) = 0; + php_json_error_details_clear(&JSON_G(error_details)); return SUCCESS; } @@ -134,7 +130,11 @@ PHP_JSON_API zend_result php_json_encode_ex(smart_str *buf, zval *val, int optio encoder.max_depth = depth; return_code = php_json_encode_zval(buf, val, options, &encoder); - JSON_G(error_code) = encoder.error_code; + JSON_G(error_details) = (php_json_error_details){ + .code = encoder.error_code, + .line = 0, + .column = 0, + }; return return_code; } @@ -179,12 +179,12 @@ static const char *php_json_get_error_msg(php_json_error_code error_code) /* {{{ } /* }}} */ -static zend_string *php_json_get_error_msg_with_location(php_json_error_code error_code, size_t line, size_t column) /* {{{ */ +static zend_string *php_json_get_error_msg_with_location(const php_json_error_details *details) /* {{{ */ { - const char *base_msg = php_json_get_error_msg(error_code); + const char *base_msg = php_json_get_error_msg(details->code); - if (line > 0 && column > 0) { - return zend_strpprintf(0, "%s near location %zu:%zu", base_msg, line, column); + if (details->line > 0 && details->column > 0) { + return zend_strpprintf(0, "%s near location %zu:%zu", base_msg, details->line, details->column); } return zend_string_init(base_msg, strlen(base_msg), 0); @@ -198,17 +198,14 @@ PHP_JSON_API zend_result php_json_decode_ex(zval *return_value, const char *str, php_json_parser_init(&parser, return_value, str, str_len, (int)options, (int)depth); if (php_json_yyparse(&parser)) { - php_json_error_code error_code = php_json_parser_error_code(&parser); - size_t error_line = php_json_parser_error_line(&parser); - size_t error_column = php_json_parser_error_column(&parser); + php_json_error_details details; + php_json_parser_error_details(&parser, &details); if (!(options & PHP_JSON_THROW_ON_ERROR)) { - JSON_G(error_code) = error_code; - JSON_G(error_line) = error_line; - JSON_G(error_column) = error_column; + JSON_G(error_details) = details; } else { - zend_string *error_msg = php_json_get_error_msg_with_location(error_code, error_line, error_column); - zend_throw_exception(php_json_exception_ce, ZSTR_VAL(error_msg), error_code); + zend_string *error_msg = php_json_get_error_msg_with_location(&details); + zend_throw_exception(php_json_exception_ce, ZSTR_VAL(error_msg), details.code); zend_string_release(error_msg); } RETVAL_NULL(); @@ -228,13 +225,8 @@ PHP_JSON_API bool php_json_validate_ex(const char *str, size_t str_len, zend_lon php_json_parser_init_ex(&parser, &tmp, str, str_len, (int)options, (int)depth, parser_validate_methods); if (php_json_yyparse(&parser)) { - php_json_error_code error_code = php_json_parser_error_code(&parser); - size_t error_line = php_json_parser_error_line(&parser); - size_t error_column = php_json_parser_error_column(&parser); + php_json_parser_error_details(&parser, &JSON_G(error_details)); - JSON_G(error_code) = error_code; - JSON_G(error_line) = error_line; - JSON_G(error_column) = error_column; return false; } @@ -263,7 +255,12 @@ PHP_FUNCTION(json_encode) php_json_encode_zval(&buf, parameter, (int)options, &encoder); if (!(options & PHP_JSON_THROW_ON_ERROR) || (options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR)) { - JSON_G(error_code) = encoder.error_code; + JSON_G(error_details) = (php_json_error_details){ + .code = encoder.error_code, + .line = 0, + .column = 0, + }; + if (encoder.error_code != PHP_JSON_ERROR_NONE && !(options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR)) { smart_str_free(&buf); RETURN_FALSE; @@ -299,16 +296,16 @@ PHP_FUNCTION(json_decode) ZEND_PARSE_PARAMETERS_END(); if (!(options & PHP_JSON_THROW_ON_ERROR)) { - JSON_G(error_code) = PHP_JSON_ERROR_NONE; - JSON_G(error_line) = 0; - JSON_G(error_column) = 0; + php_json_error_details_clear(&JSON_G(error_details)); } if (!str_len) { if (!(options & PHP_JSON_THROW_ON_ERROR)) { - JSON_G(error_code) = PHP_JSON_ERROR_SYNTAX; - JSON_G(error_line) = 0; - JSON_G(error_column) = 0; + JSON_G(error_details) = (php_json_error_details){ + .code = PHP_JSON_ERROR_SYNTAX, + .line = 0, + .column = 0, + }; } else { zend_throw_exception(php_json_exception_ce, php_json_get_error_msg(PHP_JSON_ERROR_SYNTAX), PHP_JSON_ERROR_SYNTAX); } @@ -360,15 +357,16 @@ PHP_FUNCTION(json_validate) } if (!str_len) { - JSON_G(error_code) = PHP_JSON_ERROR_SYNTAX; - JSON_G(error_line) = 0; - JSON_G(error_column) = 0; + JSON_G(error_details) = (php_json_error_details){ + .code = PHP_JSON_ERROR_SYNTAX, + .line = 0, + .column = 0, + }; + RETURN_FALSE; } - JSON_G(error_code) = PHP_JSON_ERROR_NONE; - JSON_G(error_line) = 0; - JSON_G(error_column) = 0; + php_json_error_details_clear(&JSON_G(error_details)); if (depth <= 0) { zend_argument_value_error(2, "must be greater than 0"); @@ -389,7 +387,7 @@ PHP_FUNCTION(json_last_error) { ZEND_PARSE_PARAMETERS_NONE(); - RETURN_LONG(JSON_G(error_code)); + RETURN_LONG(JSON_G(error_details).code); } /* }}} */ @@ -398,10 +396,6 @@ PHP_FUNCTION(json_last_error_msg) { ZEND_PARSE_PARAMETERS_NONE(); - RETVAL_STR(php_json_get_error_msg_with_location( - JSON_G(error_code), - JSON_G(error_line), - JSON_G(error_column) - )); + RETURN_STR(php_json_get_error_msg_with_location(&JSON_G(error_details))); } /* }}} */ diff --git a/ext/json/json_parser.y b/ext/json/json_parser.y index cf296ed9db62..0d3b90b29e1e 100644 --- a/ext/json/json_parser.y +++ b/ext/json/json_parser.y @@ -311,14 +311,11 @@ PHP_JSON_API php_json_error_code php_json_parser_error_code(const php_json_parse return parser->scanner.errcode; } -PHP_JSON_API size_t php_json_parser_error_line(const php_json_parser *parser) +PHP_JSON_API void php_json_parser_error_details(const php_json_parser *parser, php_json_error_details *out) { - return parser->scanner.errloc.first_line; -} - -PHP_JSON_API size_t php_json_parser_error_column(const php_json_parser *parser) -{ - return parser->scanner.errloc.first_column; + out->code = parser->scanner.errcode; + out->line = parser->scanner.errloc.first_line; + out->column = parser->scanner.errloc.first_column; } static const php_json_parser_methods default_parser_methods = diff --git a/ext/json/php_json.h b/ext/json/php_json.h index e2728835ed42..f34684e149d8 100644 --- a/ext/json/php_json.h +++ b/ext/json/php_json.h @@ -52,6 +52,18 @@ typedef enum { PHP_JSON_ERROR_NON_BACKED_ENUM, } php_json_error_code; +typedef struct php_json_error_details { + php_json_error_code code; + size_t line; + size_t column; +} php_json_error_details; + +static inline void php_json_error_details_clear(php_json_error_details *out) { + out->code = PHP_JSON_ERROR_NONE; + out->line = 0; + out->column = 0; +} + /* json_decode() options */ #define PHP_JSON_OBJECT_AS_ARRAY (1<<0) #define PHP_JSON_BIGINT_AS_STRING (1<<1) @@ -83,9 +95,7 @@ typedef enum { ZEND_BEGIN_MODULE_GLOBALS(json) int encoder_depth; int encode_max_depth; - php_json_error_code error_code; - size_t error_line; - size_t error_column; + php_json_error_details error_details; ZEND_END_MODULE_GLOBALS(json) PHP_JSON_API ZEND_EXTERN_MODULE_GLOBALS(json) diff --git a/ext/json/php_json_parser.h b/ext/json/php_json_parser.h index 4a7d64307c43..888a0d317fe0 100644 --- a/ext/json/php_json_parser.h +++ b/ext/json/php_json_parser.h @@ -83,9 +83,7 @@ PHP_JSON_API void php_json_parser_init( PHP_JSON_API php_json_error_code php_json_parser_error_code(const php_json_parser *parser); -PHP_JSON_API size_t php_json_parser_error_line(const php_json_parser *parser); - -PHP_JSON_API size_t php_json_parser_error_column(const php_json_parser *parser); +PHP_JSON_API void php_json_parser_error_details(const php_json_parser *parser, php_json_error_details *out); PHP_JSON_API int php_json_parse(php_json_parser *parser); diff --git a/ext/json/tests/gh22420.phpt b/ext/json/tests/gh22420.phpt new file mode 100644 index 000000000000..eecdc6d97a8d --- /dev/null +++ b/ext/json/tests/gh22420.phpt @@ -0,0 +1,16 @@ +--TEST-- +GH-22420: json_encode() errors don't clear the error position +--FILE-- + +--EXPECTF-- +NULL +string(30) "Syntax error near location 1:2" +bool(false) +string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"