Commit b83afc6caf0 for php.net

commit b83afc6caf05e12a96745a102e43a92dd264a24a
Author: Gina Peter Banyard <girgias@php.net>
Date:   Mon Aug 24 14:38:18 2026 +0100

    Zend: refactor zend_parse_arg_impl() to return zend_expected_type (#23052)

    This effectively mimics part of what Fast ZPP does and allows us to re-use the fast ZPP error APIs

diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index e36ca8d1cfe..43e21cafd56 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -844,7 +844,7 @@ ZEND_API bool ZEND_FASTCALL zend_parse_arg_str_or_long_slow(zval *arg, zend_stri
 }
 /* }}} */

-static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec, char **error, uint32_t arg_num) /* {{{ */
+static zend_expected_type zend_parse_arg_impl(zval *arg, va_list *va, const char **spec, char **error, uint32_t arg_num) /* {{{ */
 {
 	const char *spec_walk = *spec;
 	char c = *spec_walk++;
@@ -878,7 +878,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				}

 				if (!zend_parse_arg_long(arg, p, is_null, check_null, arg_num)) {
-					return check_null ? "?int" : "int";
+					return check_null ? Z_EXPECTED_LONG_OR_NULL : Z_EXPECTED_LONG;
 				}
 			}
 			break;
@@ -893,7 +893,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				}

 				if (!zend_parse_arg_double(arg, p, is_null, check_null, arg_num)) {
-					return check_null ? "?float" : "float";
+					return check_null ? Z_EXPECTED_DOUBLE_OR_NULL : Z_EXPECTED_DOUBLE;
 				}
 			}
 			break;
@@ -903,7 +903,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				zval **p = va_arg(*va, zval **);

 				if (!zend_parse_arg_number(arg, p, check_null, arg_num)) {
-					return check_null ? "int|float|null" : "int|float";
+					return check_null ? Z_EXPECTED_NUMBER_OR_NULL : Z_EXPECTED_NUMBER;
 				}
 			}
 			break;
@@ -913,7 +913,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				char **p = va_arg(*va, char **);
 				size_t *pl = va_arg(*va, size_t *);
 				if (!zend_parse_arg_string(arg, p, pl, check_null, arg_num)) {
-					return check_null ? "?string" : "string";
+					return check_null ? Z_EXPECTED_STRING_OR_NULL : Z_EXPECTED_STRING;
 				}
 			}
 			break;
@@ -923,12 +923,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				char **p = va_arg(*va, char **);
 				size_t *pl = va_arg(*va, size_t *);
 				if (!zend_parse_arg_path(arg, p, pl, check_null, arg_num)) {
-					if (Z_TYPE_P(arg) == IS_STRING) {
-						zend_spprintf(error, 0, "must not contain any null bytes");
-						return "";
-					} else {
-						return check_null ? "?string" : "string";
-					}
+					return check_null ? Z_EXPECTED_PATH_OR_NULL : Z_EXPECTED_PATH;
 				}
 			}
 			break;
@@ -937,12 +932,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 			{
 				zend_string **str = va_arg(*va, zend_string **);
 				if (!zend_parse_arg_path_str(arg, str, check_null, arg_num)) {
-					if (Z_TYPE_P(arg) == IS_STRING) {
-						zend_spprintf(error, 0, "must not contain any null bytes");
-						return "";
-					} else {
-						return check_null ? "?string" : "string";
-					}
+					return check_null ? Z_EXPECTED_PATH_OR_NULL : Z_EXPECTED_PATH;
 				}
 			}
 			break;
@@ -951,7 +941,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 			{
 				zend_string **str = va_arg(*va, zend_string **);
 				if (!zend_parse_arg_str(arg, str, check_null, arg_num)) {
-					return check_null ? "?string" : "string";
+					return check_null ? Z_EXPECTED_STRING_OR_NULL : Z_EXPECTED_STRING;
 				}
 			}
 			break;
@@ -966,7 +956,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				}

 				if (!zend_parse_arg_bool(arg, p, is_null, check_null, arg_num)) {
-					return check_null ? "?bool" : "bool";
+					return check_null ? Z_EXPECTED_BOOL_OR_NULL : Z_EXPECTED_BOOL;
 				}
 			}
 			break;
@@ -976,7 +966,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				zval **p = va_arg(*va, zval **);

 				if (!zend_parse_arg_resource(arg, p, check_null)) {
-					return check_null ? "resource or null" : "resource";
+					return check_null ? Z_EXPECTED_RESOURCE_OR_NULL : Z_EXPECTED_RESOURCE;
 				}
 			}
 			break;
@@ -987,7 +977,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				zval **p = va_arg(*va, zval **);

 				if (!zend_parse_arg_array(arg, p, check_null, c == 'A')) {
-					return check_null ? "?array" : "array";
+					return check_null ? Z_EXPECTED_ARRAY_OR_NULL : Z_EXPECTED_ARRAY;
 				}
 			}
 			break;
@@ -998,7 +988,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				HashTable **p = va_arg(*va, HashTable **);

 				if (!zend_parse_arg_array_ht(arg, p, check_null, c == 'H', separate)) {
-					return check_null ? "?array" : "array";
+					return check_null ? Z_EXPECTED_ARRAY_OR_NULL : Z_EXPECTED_ARRAY;
 				}
 			}
 			break;
@@ -1008,7 +998,7 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				zval **p = va_arg(*va, zval **);

 				if (!zend_parse_arg_object(arg, p, NULL, check_null)) {
-					return check_null ? "?object" : "object";
+					return check_null ? Z_EXPECTED_OBJECT_OR_NULL : Z_EXPECTED_OBJECT;
 				}
 			}
 			break;
@@ -1020,15 +1010,9 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec

 				if (!zend_parse_arg_object(arg, p, ce, check_null)) {
 					if (ce) {
-						if (check_null) {
-							zend_spprintf(error, 0, "must be of type ?%s, %s given", ZSTR_VAL(ce->name), zend_zval_value_name(arg));
-							return "";
-						} else {
-							return ZSTR_VAL(ce->name);
-						}
-					} else {
-						return check_null ? "?object" : "object";
+						*error = ZSTR_VAL(ce->name);
 					}
+					return check_null ? Z_EXPECTED_OBJECT_OR_NULL : Z_EXPECTED_OBJECT;
 				}
 			}
 			break;
@@ -1046,14 +1030,13 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 				/* Only accept string and Stringable(?) as int/foat/bool are not valid class names */
 				if (UNEXPECTED(Z_TYPE_P(arg) != IS_STRING)) {
 					if (Z_TYPE_P(arg) != IS_OBJECT || !zend_parse_arg_str_slow(arg, arg_num)) {
-						*pce = NULL;
 						/* __toString may throw */
 						if (!EG(exception)) {
 							zend_spprintf(error, 0, "must be a valid class name%s, %s given",
 								check_null ? " or null" : "", zend_zval_value_name(arg));
 						}
 						*pce = NULL;
-						return "";
+						return check_null ? Z_EXPECTED_CLASS_NAME_OR_NULL : Z_EXPECTED_CLASS_NAME;
 					}
 					/* Object was converted to string */
 					ZEND_ASSERT(Z_TYPE_P(arg) == IS_STRING);
@@ -1066,13 +1049,11 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 						zend_spprintf(error, 0, "must be a class name derived from %s%s, \"%s\" given",
 							ZSTR_VAL(ce_base->name), check_null ? " or null" : "", Z_STRVAL_P(arg));
 						*pce = NULL;
-						return "";
+						return check_null ? Z_EXPECTED_CLASS_NAME_OR_NULL : Z_EXPECTED_CLASS_NAME;
 					}
 				}
 				if (!*pce) {
-					zend_spprintf(error, 0, "must be a valid class name%s, \"%s\" given",
-						check_null ? " or null" : "", Z_STRVAL_P(arg));
-					return "";
+					return check_null ? Z_EXPECTED_CLASS_NAME_OR_NULL : Z_EXPECTED_CLASS_NAME;
 				}
 				break;

@@ -1084,19 +1065,11 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 			{
 				zend_fcall_info *fci = va_arg(*va, zend_fcall_info *);
 				zend_fcall_info_cache *fcc = va_arg(*va, zend_fcall_info_cache *);
-				char *is_callable_error = NULL;
-				if (EXPECTED(zend_parse_arg_func(arg, fci, fcc, check_null, &is_callable_error, c == 'f'))) {
-					ZEND_ASSERT(!is_callable_error);
+				if (EXPECTED(zend_parse_arg_func(arg, fci, fcc, check_null, error, c == 'f'))) {
+					ZEND_ASSERT(!*error);
 					break;
 				}
-
-				if (is_callable_error) {
-					zend_spprintf(error, 0, "must be a valid callback%s, %s", check_null ? " or null" : "", is_callable_error);
-					efree(is_callable_error);
-					return "";
-				} else {
-					return check_null ? "a valid callback or null" : "a valid callback";
-				}
+				return check_null ? Z_EXPECTED_FUNC_OR_NULL : Z_EXPECTED_FUNC;
 			}

 		case 'z':
@@ -1112,37 +1085,68 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec
 			ZEND_ASSERT(0 && "ZPP modifier no longer supported");
 			ZEND_FALLTHROUGH;
 		default:
-			return "unknown";
+			ZEND_ASSERT(false && "Unknown ZPP modifier");
 	}

 	*spec = spec_walk;

-	return NULL;
+	return Z_EXPECTED_LAST;
 }
 /* }}} */

 static zend_result zend_parse_arg(uint32_t arg_num, zval *arg, va_list *va, const char **spec, int flags) /* {{{ */
 {
-	const char *expected_type = NULL;
 	char *error = NULL;

-	expected_type = zend_parse_arg_impl(arg, va, spec, &error, arg_num);
-	if (expected_type) {
+	zend_expected_type expected_type = zend_parse_arg_impl(arg, va, spec, &error, arg_num);
+	if (expected_type != Z_EXPECTED_LAST) {
 		if (EG(exception)) {
 			return FAILURE;
 		}
-		if (!(flags & ZEND_PARSE_PARAMS_QUIET) && (*expected_type || error)) {
+
+		if (!(flags & ZEND_PARSE_PARAMS_QUIET)) {
+			/* More complex error, can only happen for:
+			 * Objects of a specific class
+			 * Z_EXPECTED_OBJECT
+			 * Z_EXPECTED_OBJECT_OR_NULL
+			 * Class names
+			 * Z_EXPECTED_CLASS_NAME
+			 * Z_EXPECTED_CLASS_NAME_OR_NULL
+			 * Functions
+			 * Z_EXPECTED_FUNC
+			 * Z_EXPECTED_FUNC_OR_NULL
+			 */
 			if (error) {
-				if (strcmp(error, "must not contain any null bytes") == 0) {
-					zend_argument_value_error(arg_num, "%s", error);
-				} else {
-					zend_argument_type_error(arg_num, "%s", error);
+				switch (expected_type) {
+					case Z_EXPECTED_OBJECT:
+						/* DO NOT FREE error: it's a pointer to ZSTR_VAL(ce->name) */
+						zend_wrong_parameter_class_error(arg_num, error, arg);
+						break;
+					case Z_EXPECTED_OBJECT_OR_NULL:
+						/* DO NOT FREE error: it's a pointer to ZSTR_VAL(ce->name) */
+						zend_wrong_parameter_class_or_null_error(arg_num, error, arg);
+						break;
+					case Z_EXPECTED_FUNC:
+						/* error is freed by zend_wrong_callback_error() */
+						zend_wrong_callback_error(arg_num, error);
+						break;
+					case Z_EXPECTED_FUNC_OR_NULL:
+						/* error is freed by zend_wrong_callback_or_null_error() */
+						zend_wrong_callback_or_null_error(arg_num, error);
+						break;
+					case Z_EXPECTED_CLASS_NAME:
+					case Z_EXPECTED_CLASS_NAME_OR_NULL:
+						zend_argument_type_error(arg_num, "%s", error);
+						efree(error);
+						break;
+					default:
+						ZEND_UNREACHABLE();
 				}
-				efree(error);
-			} else {
-				zend_argument_type_error(arg_num, "must be of type %s, %s given", expected_type, zend_zval_value_name(arg));
 			}
-		} else if (error) {
+			zend_wrong_parameter_type_error(arg_num, expected_type, arg);
+		} else if (error
+			/* DO NOT FREE error when it's a pointer to ZSTR_VAL(ce->name) */
+			&& expected_type != Z_EXPECTED_OBJECT && expected_type != Z_EXPECTED_OBJECT_OR_NULL) {
 			efree(error);
 		}