diff --git a/NEWS b/NEWS index c548f32d052a..c2a0028e5521 100644 --- a/NEWS +++ b/NEWS @@ -333,6 +333,10 @@ PHP NEWS . pcntl_exec() now throws a ValueError if the $args array is not a list array. (Weilin Du) +- PCRE: + . Added the PREG_THROW_ON_ERROR flag to make the preg_*() functions throw a + \PregException on any PCRE error. (aldemeery) + - PDO_DBLIB: . Added dblib_handle_check_liveness handler. (freddy77) diff --git a/UPGRADING b/UPGRADING index 145f2239b56b..e2b1856f1cde 100644 --- a/UPGRADING +++ b/UPGRADING @@ -289,6 +289,23 @@ PHP 8.6 UPGRADE NOTES . Added TLS external PSK support for streams with new strem context options: psk_client_cb and psk_server_cb. This allows setting and receiving PSK. +- PCRE: + . Added the PREG_THROW_ON_ERROR flag. When passed to a preg_*() function that + accepts a $flags argument, a PCRE error additionally throws a \PregException + whose code and message are exactly what preg_last_error() and + preg_last_error_msg() report for that call. The flag does not otherwise + change the call: same warnings, same return value. It covers both execution + errors (such as an exhausted backtrack limit or malformed UTF-8 input under + the /u modifier) and compilation errors (such as a malformed pattern, which + still also emits its usual warning). preg_replace() and preg_filter() gained + a $flags parameter to accept it. A PCRE error raised by a nested preg_*() + call inside a preg_replace_callback()/preg_replace_callback_array() callback + is not attributed to the outer call, so a successful outer call does not + throw on its account. As on the non-flag path, by-reference outputs (the + $matches and $count arguments) may already have been written when the + \PregException is thrown, so a catch block should not assume they are left + untouched. + - Phar: . Overriding the getMTime() and getPathname() methods of SplFileInfo now influences the result of the phar buildFrom family of functions. @@ -373,6 +390,10 @@ PHP 8.6 UPGRADE NOTES . Output of openssl_x509_parse() contains criticalExtensions listing all critical certificate extensions. +- PCRE: + . preg_replace() and preg_filter() now accept an optional $flags argument + (for PREG_THROW_ON_ERROR). + - PDO_DBLIB: . When using persistent connections, there is now a liveness check in the constructor. @@ -443,6 +464,9 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/tls_session_resumption . Openssl\Psk +- PCRE: + . PregException + - Standard: . enum SortDirection RFC: https://wiki.php.net/rfc/sort_direction_enum @@ -502,6 +526,9 @@ PHP 8.6 UPGRADE NOTES . CURL_SEEKFUNC_FAIL. . CURL_SEEKFUNC_CANTSEEK. +- PCRE: + . PREG_THROW_ON_ERROR. + - Sockets: . TCP_USER_TIMEOUT (Linux only). . AF_UNSPEC. diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c index 6a62d9717e7b..fdcb9fc947aa 100644 --- a/ext/pcre/php_pcre.c +++ b/ext/pcre/php_pcre.c @@ -18,6 +18,7 @@ #include "ext/standard/info.h" #include "ext/standard/basic_functions.h" #include "zend_smart_str.h" +#include "zend_exceptions.h" #include "SAPI.h" #define PREG_PATTERN_ORDER 1 @@ -33,6 +34,8 @@ #define PREG_JIT (1<<3) +#define PREG_THROW_ON_ERROR (1<<16) + #define PCRE_CACHE_SIZE 4096 #ifdef HAVE_PCRE_JIT_SUPPORT @@ -43,6 +46,8 @@ char *php_pcre_version; +static zend_class_entry *php_pcre_exception_ce; + #include "php_pcre_arginfo.h" struct _pcre_cache_entry { @@ -165,6 +170,26 @@ static const char *php_pcre_get_error_msg(php_pcre_error_code error_code) /* {{{ } /* }}} */ +static bool php_pcre_throw_on_error(zend_long flags) +{ + if ((flags & PREG_THROW_ON_ERROR) + && PCRE_G(error_code) != PHP_PCRE_NO_ERROR + && !EG(exception)) { + zend_throw_exception( + php_pcre_exception_ce, php_pcre_get_error_msg(PCRE_G(error_code)), PCRE_G(error_code)); + return true; + } + + return false; +} + +static void php_pcre_clear_stale_error(zend_long flags) +{ + if (flags & PREG_THROW_ON_ERROR) { + PCRE_G(error_code) = PHP_PCRE_NO_ERROR; + } +} + static void php_free_pcre_cache(zval *data) /* {{{ */ { pcre_cache_entry *pce = (pcre_cache_entry *) Z_PTR_P(data); @@ -426,6 +451,8 @@ static PHP_MINIT_FUNCTION(pcre) register_php_pcre_symbols(module_number); + php_pcre_exception_ce = register_class_PregException(zend_ce_exception); + return SUCCESS; } /* }}} */ @@ -1102,8 +1129,11 @@ static void php_do_pcre_match(INTERNAL_FUNCTION_PARAMETERS, bool global) /* {{{ Z_PARAM_LONG(start_offset) ZEND_PARSE_PARAMETERS_END(); + php_pcre_clear_stale_error(flags); + /* Compile regex or get it from cache. */ if ((pce = pcre_get_compiled_regex_cache(regex)) == NULL) { + php_pcre_throw_on_error(flags); RETURN_FALSE; } @@ -1116,6 +1146,8 @@ static void php_do_pcre_match(INTERNAL_FUNCTION_PARAMETERS, bool global) /* {{{ php_pcre_match_impl(pce, subject, return_value, subpats, global, flags, start_offset); pce->refcount--; + + php_pcre_throw_on_error(flags); } /* }}} */ @@ -1945,9 +1977,13 @@ static zend_string *php_pcre_replace_func_impl(pcre_cache_entry *pce, zend_strin size_t new_len = result_len + offsets[0] - last_end_offset; /* part before the match */ /* Use custom function to get replacement string and its length. */ + php_pcre_error_code saved_error_code = PCRE_G(error_code); zend_string *eval_result = preg_do_repl_func( fci, fcc, ZSTR_VAL(subject_str), offsets, subpat_names, num_subpats, count, pcre2_get_mark(match_data), flags); + if (flags & PREG_THROW_ON_ERROR) { + PCRE_G(error_code) = saved_error_code; + } if (UNEXPECTED(eval_result == NULL)) { goto error; @@ -2341,23 +2377,29 @@ static void preg_replace_common(INTERNAL_FUNCTION_PARAMETERS, bool is_filter) HashTable *regex_ht, *replace_ht, *subject_ht; zend_long limit = -1; zval *zcount = NULL; + zend_long flags = 0; /* Get function parameters and do error-checking. */ - ZEND_PARSE_PARAMETERS_START(3, 5) + ZEND_PARSE_PARAMETERS_START(3, 6) Z_PARAM_ARRAY_HT_OR_STR(regex_ht, regex_str) Z_PARAM_ARRAY_HT_OR_STR(replace_ht, replace_str) Z_PARAM_ARRAY_HT_OR_STR(subject_ht, subject_str) Z_PARAM_OPTIONAL Z_PARAM_LONG(limit) Z_PARAM_ZVAL(zcount) + Z_PARAM_LONG(flags) ZEND_PARSE_PARAMETERS_END(); + php_pcre_clear_stale_error(flags); + _preg_replace_common( return_value, regex_ht, regex_str, replace_ht, replace_str, subject_ht, subject_str, limit, zcount, is_filter); + + php_pcre_throw_on_error(flags); } /* }}} */ @@ -2415,12 +2457,16 @@ PHP_FUNCTION(preg_replace_callback) Z_PARAM_LONG(flags) ZEND_PARSE_PARAMETERS_END(); + php_pcre_clear_stale_error(flags); + replace_count = php_preg_replace_func_impl(return_value, regex_str, regex_ht, &fci, &fcc, subject_str, subject_ht, limit, flags); if (zcount) { ZEND_TRY_ASSIGN_REF_LONG(zcount, replace_count); } + + php_pcre_throw_on_error(flags); } /* }}} */ @@ -2443,6 +2489,8 @@ PHP_FUNCTION(preg_replace_callback_array) Z_PARAM_LONG(flags) ZEND_PARSE_PARAMETERS_END(); + php_pcre_clear_stale_error(flags); + if (subject_ht) { GC_TRY_ADDREF(subject_ht); } else { @@ -2484,6 +2532,7 @@ PHP_FUNCTION(preg_replace_callback_array) break; case IS_NULL: RETVAL_NULL(); + php_pcre_throw_on_error(flags); goto error; default: ZEND_UNREACHABLE(); } @@ -2503,11 +2552,13 @@ PHP_FUNCTION(preg_replace_callback_array) if (GC_FLAGS(subject_ht) & IS_ARRAY_IMMUTABLE) { Z_TYPE_FLAGS_P(return_value) = 0; } - return; } else { - RETURN_STR(subject_str); + RETVAL_STR(subject_str); } + php_pcre_throw_on_error(flags); + return; + error: if (subject_ht) { zend_array_release(subject_ht); @@ -2542,14 +2593,19 @@ PHP_FUNCTION(preg_split) Z_PARAM_LONG(flags) ZEND_PARSE_PARAMETERS_END(); + php_pcre_clear_stale_error(flags); + /* Compile regex or get it from cache. */ if ((pce = pcre_get_compiled_regex_cache(regex)) == NULL) { + php_pcre_throw_on_error(flags); RETURN_FALSE; } pce->refcount++; php_pcre_split_impl(pce, subject, return_value, limit_val, flags); pce->refcount--; + + php_pcre_throw_on_error(flags); } /* }}} */ @@ -2904,14 +2960,19 @@ PHP_FUNCTION(preg_grep) Z_PARAM_LONG(flags) ZEND_PARSE_PARAMETERS_END(); + php_pcre_clear_stale_error(flags); + /* Compile regex or get it from cache. */ if ((pce = pcre_get_compiled_regex_cache(regex)) == NULL) { + php_pcre_throw_on_error(flags); RETURN_FALSE; } pce->refcount++; php_pcre_grep_impl(pce, input, return_value, flags); pce->refcount--; + + php_pcre_throw_on_error(flags); } /* }}} */ diff --git a/ext/pcre/php_pcre.stub.php b/ext/pcre/php_pcre.stub.php index 0cd045b7efae..20097768c032 100644 --- a/ext/pcre/php_pcre.stub.php +++ b/ext/pcre/php_pcre.stub.php @@ -42,6 +42,11 @@ * @cvalue PREG_GREP_INVERT */ const PREG_GREP_INVERT = UNKNOWN; +/** + * @var int + * @cvalue PREG_THROW_ON_ERROR + */ +const PREG_THROW_ON_ERROR = UNKNOWN; /** * @var int * @cvalue PHP_PCRE_NO_ERROR @@ -112,13 +117,13 @@ function preg_match_all(string $pattern, string $subject, &$matches = null, int * @return string|array|null * @frameless-function {"arity": 3} */ -function preg_replace(string|array $pattern, string|array $replacement, string|array $subject, int $limit = -1, &$count = null): string|array|null {} +function preg_replace(string|array $pattern, string|array $replacement, string|array $subject, int $limit = -1, &$count = null, int $flags = 0): string|array|null {} /** * @param int $count * @return string|array|null */ -function preg_filter(string|array $pattern, string|array $replacement, string|array $subject, int $limit = -1, &$count = null): string|array|null {} +function preg_filter(string|array $pattern, string|array $replacement, string|array $subject, int $limit = -1, &$count = null, int $flags = 0): string|array|null {} /** * @param int $count @@ -144,3 +149,8 @@ function preg_grep(string $pattern, array $array, int $flags = 0): array|false { function preg_last_error(): int {} function preg_last_error_msg(): string {} + +/** + * @strict-properties + */ +class PregException extends \Exception {} diff --git a/ext/pcre/php_pcre_arginfo.h b/ext/pcre/php_pcre_arginfo.h index 0d22c2414fb8..86e8ae105b06 100644 --- a/ext/pcre/php_pcre_arginfo.h +++ b/ext/pcre/php_pcre_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_pcre.stub.php instead. - * Stub hash: 63de1d37ab303e1d6af7c96eaeeba09d7f35d116 */ + * Stub hash: 16921e0a916c7d2635ccbf691ce173a4a17aa6ea */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_preg_match, 0, 2, MAY_BE_LONG|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, pattern, IS_STRING, 0) @@ -17,6 +17,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_preg_replace, 0, 3, MAY_BE_STRIN ZEND_ARG_TYPE_MASK(0, subject, MAY_BE_STRING|MAY_BE_ARRAY, NULL) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, limit, IS_LONG, 0, "-1") ZEND_ARG_INFO_WITH_DEFAULT_VALUE(1, count, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0") ZEND_END_ARG_INFO() #define arginfo_preg_filter arginfo_preg_replace @@ -112,6 +113,7 @@ static void register_php_pcre_symbols(int module_number) REGISTER_LONG_CONSTANT("PREG_SPLIT_DELIM_CAPTURE", PREG_SPLIT_DELIM_CAPTURE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("PREG_SPLIT_OFFSET_CAPTURE", PREG_SPLIT_OFFSET_CAPTURE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("PREG_GREP_INVERT", PREG_GREP_INVERT, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("PREG_THROW_ON_ERROR", PREG_THROW_ON_ERROR, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("PREG_NO_ERROR", PHP_PCRE_NO_ERROR, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("PREG_INTERNAL_ERROR", PHP_PCRE_INTERNAL_ERROR, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("PREG_BACKTRACK_LIMIT_ERROR", PHP_PCRE_BACKTRACK_LIMIT_ERROR, CONST_PERSISTENT); @@ -124,3 +126,13 @@ static void register_php_pcre_symbols(int module_number) REGISTER_LONG_CONSTANT("PCRE_VERSION_MINOR", PCRE2_MINOR, CONST_PERSISTENT); REGISTER_BOOL_CONSTANT("PCRE_JIT_SUPPORT", PHP_PCRE_JIT_SUPPORT, CONST_PERSISTENT); } + +static zend_class_entry *register_class_PregException(zend_class_entry *class_entry_Exception) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "PregException", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Exception, ZEND_ACC_NO_DYNAMIC_PROPERTIES); + + return class_entry; +} diff --git a/ext/pcre/tests/preg_throw_on_error.phpt b/ext/pcre/tests/preg_throw_on_error.phpt new file mode 100644 index 000000000000..184b201e2f1b --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error.phpt @@ -0,0 +1,38 @@ +--TEST-- +PREG_THROW_ON_ERROR: every preg_*() function throws PregException on a PCRE error +--FILE-- + fn() => preg_match('//u', $bad, $m, PREG_THROW_ON_ERROR), + 'preg_match_all' => fn() => preg_match_all('//u', $bad, $m, PREG_THROW_ON_ERROR), + 'preg_replace' => fn() => preg_replace('//u', 'x', $bad, -1, $c, PREG_THROW_ON_ERROR), + 'preg_filter' => fn() => preg_filter('//u', 'x', $bad, -1, $c, PREG_THROW_ON_ERROR), + 'preg_replace_callback' => fn() => preg_replace_callback('//u', fn($m) => 'x', $bad, -1, $c, PREG_THROW_ON_ERROR), + 'preg_replace_callback_array' => fn() => preg_replace_callback_array(['//u' => fn($m) => 'x'], $bad, -1, $c, PREG_THROW_ON_ERROR), + 'preg_split' => fn() => preg_split('//u', $bad, -1, PREG_THROW_ON_ERROR), + 'preg_grep' => fn() => preg_grep('//u', [$bad], PREG_THROW_ON_ERROR), +]; + +foreach ($cases as $name => $case) { + try { + $case(); + echo "$name: no exception thrown\n"; + } catch (PregException $e) { + printf("%s: %s: %s (code matches: %s)\n", $name, $e::class, $e->getMessage(), + $e->getCode() === PREG_BAD_UTF8_ERROR ? 'yes' : 'no'); + } +} + +?> +--EXPECT-- +preg_match: PregException: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_match_all: PregException: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_replace: PregException: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_filter: PregException: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_replace_callback: PregException: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_replace_callback_array: PregException: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_split: PregException: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_grep: PregException: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) diff --git a/ext/pcre/tests/preg_throw_on_error_array_pattern.phpt b/ext/pcre/tests/preg_throw_on_error_array_pattern.phpt new file mode 100644 index 000000000000..c1bbf2094984 --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_array_pattern.phpt @@ -0,0 +1,24 @@ +--TEST-- +PREG_THROW_ON_ERROR: preg_replace() with an array of patterns stops at the first failing pattern and throws the error preg_last_error() would report +--FILE-- + fn() => @preg_replace(['/a/', '/[/', '/c/'], 'x', 'abc', -1, $c, PREG_THROW_ON_ERROR), + 'exec' => fn() => preg_replace(['/a/', '//u'], 'x', "\xff", -1, $c, PREG_THROW_ON_ERROR), +]; + +foreach ($cases as $label => $case) { + try { + $case(); + echo "$label: no exception thrown\n"; + } catch (PregException $e) { + printf("%s: %s | matches: %s\n", $label, $e->getMessage(), + ($e->getCode() === preg_last_error() && $e->getMessage() === preg_last_error_msg()) ? 'yes' : 'no'); + } +} + +?> +--EXPECT-- +compile: Internal error | matches: yes +exec: Malformed UTF-8 characters, possibly incorrectly encoded | matches: yes diff --git a/ext/pcre/tests/preg_throw_on_error_array_subject.phpt b/ext/pcre/tests/preg_throw_on_error_array_subject.phpt new file mode 100644 index 000000000000..5323b2df52df --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_array_subject.phpt @@ -0,0 +1,45 @@ +--TEST-- +PREG_THROW_ON_ERROR: for an array of subjects the flag throws exactly when the unflagged call leaves an error in preg_last_error(), with the same code +--FILE-- + [fn() => preg_replace('//u', 'x', [$bad, 'ok'], -1, $c), fn() => preg_replace('//u', 'x', [$bad, 'ok'], -1, $c, PREG_THROW_ON_ERROR)], + 'replace bad-last' => [fn() => preg_replace('//u', 'x', ['ok', $bad], -1, $c), fn() => preg_replace('//u', 'x', ['ok', $bad], -1, $c, PREG_THROW_ON_ERROR)], + 'filter bad-last' => [fn() => preg_filter('//u', 'x', ['ok', $bad], -1, $c), fn() => preg_filter('//u', 'x', ['ok', $bad], -1, $c, PREG_THROW_ON_ERROR)], + 'callback bad-last' => [fn() => preg_replace_callback('//u', fn($m) => 'x', ['ok', $bad], -1, $c), fn() => preg_replace_callback('//u', fn($m) => 'x', ['ok', $bad], -1, $c, PREG_THROW_ON_ERROR)], + 'callback_array bad-last' => [fn() => preg_replace_callback_array(['//u' => fn($m) => 'x'], ['ok', $bad], -1, $c), fn() => preg_replace_callback_array(['//u' => fn($m) => 'x'], ['ok', $bad], -1, $c, PREG_THROW_ON_ERROR)], + 'grep bad-first' => [fn() => preg_grep('//u', [$bad, 'ok']), fn() => preg_grep('//u', [$bad, 'ok'], PREG_THROW_ON_ERROR)], + 'grep bad-last' => [fn() => preg_grep('//u', ['ok', $bad]), fn() => preg_grep('//u', ['ok', $bad], PREG_THROW_ON_ERROR)], +]; + +foreach ($cases as $label => [$noflag, $flag]) { + $noflag(); + $err = preg_last_error(); + $reports = $err !== PREG_NO_ERROR; + + $threw = false; + $code = PREG_NO_ERROR; + try { + $flag(); + } catch (PregException $e) { + $threw = true; + $code = $e->getCode(); + } + + $consistent = ($threw === $reports) && (!$threw || $code === $err); + printf("%s: unflagged reports error=%s, flag throws=%s, consistent=%s\n", + $label, $reports ? 'yes' : 'no', $threw ? 'yes' : 'no', $consistent ? 'yes' : 'no'); +} + +?> +--EXPECT-- +replace bad-first: unflagged reports error=no, flag throws=no, consistent=yes +replace bad-last: unflagged reports error=yes, flag throws=yes, consistent=yes +filter bad-last: unflagged reports error=yes, flag throws=yes, consistent=yes +callback bad-last: unflagged reports error=yes, flag throws=yes, consistent=yes +callback_array bad-last: unflagged reports error=yes, flag throws=yes, consistent=yes +grep bad-first: unflagged reports error=yes, flag throws=yes, consistent=yes +grep bad-last: unflagged reports error=yes, flag throws=yes, consistent=yes diff --git a/ext/pcre/tests/preg_throw_on_error_backtrack.phpt b/ext/pcre/tests/preg_throw_on_error_backtrack.phpt new file mode 100644 index 000000000000..b995fa7a5914 --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_backtrack.phpt @@ -0,0 +1,19 @@ +--TEST-- +PREG_THROW_ON_ERROR: exhausting the backtrack limit throws PregException +--INI-- +pcre.backtrack_limit=1000 +pcre.jit=0 +--FILE-- +getMessage(), "\n"; + var_dump($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR); +} + +?> +--EXPECT-- +Backtrack limit exhausted +bool(true) diff --git a/ext/pcre/tests/preg_throw_on_error_callback_nested.phpt b/ext/pcre/tests/preg_throw_on_error_callback_nested.phpt new file mode 100644 index 000000000000..5743430fb2b0 --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_callback_nested.phpt @@ -0,0 +1,42 @@ +--TEST-- +PREG_THROW_ON_ERROR: a nested preg_*() error inside a replacement callback does not make the outer (successful) call throw +--FILE-- + $polluter], 'xy', -1, $c, PREG_THROW_ON_ERROR); +printf("callback_array: %s (count=%d)\n", $r, $c); + +try { + preg_replace_callback('//u', fn($m) => 'x', "\xff", -1, $c, PREG_THROW_ON_ERROR); + echo "outer error: no throw\n"; +} catch (PregException $e) { + echo "outer error: threw ", $e->getMessage(), "\n"; +} + +try { + preg_replace_callback('/\w/', function ($m) { + throw new RuntimeException('from callback'); + }, 'a', -1, $c, PREG_THROW_ON_ERROR); + echo "user exception: no throw\n"; +} catch (RuntimeException $e) { + echo "user exception: ", $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +callback single: AB (count=2) +callback array: AB,CD (count=4) +callback_array: XY (count=2) +outer error: threw Malformed UTF-8 characters, possibly incorrectly encoded +user exception: from callback diff --git a/ext/pcre/tests/preg_throw_on_error_class.phpt b/ext/pcre/tests/preg_throw_on_error_class.phpt new file mode 100644 index 000000000000..d41fe5632708 --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_class.phpt @@ -0,0 +1,25 @@ +--TEST-- +PREG_THROW_ON_ERROR: PregException is a global, strict-properties Exception subclass +--FILE-- +getName()); +var_dump($r->getParentClass()->getName()); +var_dump($r->isFinal()); + +try { + $ex = new PregException('boom'); + $ex->dynamic = 1; + echo "dynamic property allowed\n"; +} catch (\Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +string(13) "PregException" +string(9) "Exception" +bool(false) +Cannot create dynamic property PregException::$dynamic diff --git a/ext/pcre/tests/preg_throw_on_error_compile.phpt b/ext/pcre/tests/preg_throw_on_error_compile.phpt new file mode 100644 index 000000000000..aa246bd77323 --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_compile.phpt @@ -0,0 +1,49 @@ +--TEST-- +PREG_THROW_ON_ERROR: a malformed pattern still warns exactly as without the flag, and additionally throws PregException carrying the generic error preg_last_error()/preg_last_error_msg() report +--FILE-- + fn() => preg_match($bad, 'x', $m, PREG_THROW_ON_ERROR), + 'preg_match_all' => fn() => preg_match_all($bad, 'x', $m, PREG_THROW_ON_ERROR), + 'preg_replace' => fn() => preg_replace($bad, 'r', 'x', -1, $c, PREG_THROW_ON_ERROR), + 'preg_filter' => fn() => preg_filter($bad, 'r', 'x', -1, $c, PREG_THROW_ON_ERROR), + 'preg_replace_callback' => fn() => preg_replace_callback($bad, fn($m) => 'r', 'x', -1, $c, PREG_THROW_ON_ERROR), + 'preg_replace_callback_array' => fn() => preg_replace_callback_array([$bad => fn($m) => 'r'], 'x', -1, $c, PREG_THROW_ON_ERROR), + 'preg_split' => fn() => preg_split($bad, 'x', -1, PREG_THROW_ON_ERROR), + 'preg_grep' => fn() => preg_grep($bad, ['x'], PREG_THROW_ON_ERROR), +]; + +foreach ($cases as $name => $case) { + $warned = false; + try { + $case(); + echo "$name: no exception thrown\n"; + } catch (PregException $e) { + printf("%s: %s | warned: %s | matches: %s\n", + $name, $e->getMessage(), + $warned ? 'yes' : 'no', + ($e->getCode() === preg_last_error() && $e->getMessage() === preg_last_error_msg()) ? 'yes' : 'no'); + } +} + +restore_error_handler(); + +?> +--EXPECT-- +preg_match: Internal error | warned: yes | matches: yes +preg_match_all: Internal error | warned: yes | matches: yes +preg_replace: Internal error | warned: yes | matches: yes +preg_filter: Internal error | warned: yes | matches: yes +preg_replace_callback: Internal error | warned: yes | matches: yes +preg_replace_callback_array: Internal error | warned: yes | matches: yes +preg_split: Internal error | warned: yes | matches: yes +preg_grep: Internal error | warned: yes | matches: yes diff --git a/ext/pcre/tests/preg_throw_on_error_compile_opt_in.phpt b/ext/pcre/tests/preg_throw_on_error_compile_opt_in.phpt new file mode 100644 index 000000000000..766f2c304b30 --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_compile_opt_in.phpt @@ -0,0 +1,13 @@ +--TEST-- +PREG_THROW_ON_ERROR: without the flag, a malformed pattern still warns and returns false +--FILE-- + +--EXPECTF-- +Warning: preg_match(): Compilation failed: %s in %s on line %d +bool(false) +bool(true) diff --git a/ext/pcre/tests/preg_throw_on_error_consistency.phpt b/ext/pcre/tests/preg_throw_on_error_consistency.phpt new file mode 100644 index 000000000000..47e5850ada8d --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_consistency.phpt @@ -0,0 +1,32 @@ +--TEST-- +PREG_THROW_ON_ERROR: the exception mirrors preg_last_error()/preg_last_error_msg() byte-for-byte for every error kind +--INI-- +pcre.backtrack_limit=1000 +pcre.jit=0 +--FILE-- + fn() => preg_match('//u', "\xff", $m, PREG_THROW_ON_ERROR), + 'backtrack limit (execution)' => fn() => preg_match('/(a+)+$/', str_repeat('a', 1000) . 'X', $m, PREG_THROW_ON_ERROR), + 'malformed pattern (compile)' => fn() => preg_match('/[/', 'x', $m, PREG_THROW_ON_ERROR), +]; + +foreach ($cases as $label => $case) { + try { + @$case(); + echo "$label: no exception thrown\n"; + } catch (PregException $e) { + printf("%s: %s | code===preg_last_error()=%s, msg===preg_last_error_msg()=%s\n", + $label, + $e->getMessage(), + $e->getCode() === preg_last_error() ? 'yes' : 'no', + $e->getMessage() === preg_last_error_msg() ? 'yes' : 'no'); + } +} + +?> +--EXPECT-- +bad utf-8 (execution): Malformed UTF-8 characters, possibly incorrectly encoded | code===preg_last_error()=yes, msg===preg_last_error_msg()=yes +backtrack limit (execution): Backtrack limit exhausted | code===preg_last_error()=yes, msg===preg_last_error_msg()=yes +malformed pattern (compile): Internal error | code===preg_last_error()=yes, msg===preg_last_error_msg()=yes diff --git a/ext/pcre/tests/preg_throw_on_error_named_arg.phpt b/ext/pcre/tests/preg_throw_on_error_named_arg.phpt new file mode 100644 index 000000000000..e01318a7e80f --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_named_arg.phpt @@ -0,0 +1,21 @@ +--TEST-- +PREG_THROW_ON_ERROR: the new preg_replace()/preg_filter() $flags parameter is reachable by name +--FILE-- +getMessage(), "\n"; +} + +try { + preg_filter('//u', 'x', "\xff", flags: PREG_THROW_ON_ERROR); +} catch (PregException $e) { + echo 'preg_filter: ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +preg_replace: Malformed UTF-8 characters, possibly incorrectly encoded +preg_filter: Malformed UTF-8 characters, possibly incorrectly encoded diff --git a/ext/pcre/tests/preg_throw_on_error_opt_in.phpt b/ext/pcre/tests/preg_throw_on_error_opt_in.phpt new file mode 100644 index 000000000000..4c6f39251980 --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_opt_in.phpt @@ -0,0 +1,33 @@ +--TEST-- +PREG_THROW_ON_ERROR: the flag is opt-in and does not change behavior on success +--FILE-- + +--EXPECT-- +bool(false) +bool(true) +Malformed UTF-8 characters, possibly incorrectly encoded +int(1) +123 +bool(true) +int(1) +array(2) { + [0]=> + string(3) "123" + [1]=> + int(3) +} diff --git a/ext/pcre/tests/preg_throw_on_error_stale_error.phpt b/ext/pcre/tests/preg_throw_on_error_stale_error.phpt new file mode 100644 index 000000000000..eb0ea2d0973a --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_stale_error.phpt @@ -0,0 +1,40 @@ +--TEST-- +PREG_THROW_ON_ERROR: a replace call that does no matching does not throw a stale error left by an earlier call +--FILE-- + fn() => preg_replace('/a/', 'x', [], -1, $c, PREG_THROW_ON_ERROR), + 'preg_replace (empty pattern)' => fn() => preg_replace([], 'x', 'hello', -1, $c, PREG_THROW_ON_ERROR), + 'preg_filter (empty subject)' => fn() => preg_filter('/a/', 'x', [], -1, $c, PREG_THROW_ON_ERROR), + 'preg_replace_callback (empty subject)' => fn() => preg_replace_callback('/a/', fn($m) => 'x', [], -1, $c, PREG_THROW_ON_ERROR), + 'preg_replace_callback_array (empty)' => fn() => preg_replace_callback_array(['/a/' => fn($m) => 'x'], [], -1, $c, PREG_THROW_ON_ERROR), +]; + +foreach ($cases as $label => $case) { + @preg_match('//u', "\xff"); + $seeded = preg_last_error() === PREG_BAD_UTF8_ERROR; + try { + $case(); + printf("%s: seeded=%s, no throw\n", $label, $seeded ? 'yes' : 'no'); + } catch (PregException $e) { + printf("%s: seeded=%s, THREW %s\n", $label, $seeded ? 'yes' : 'no', $e->getMessage()); + } +} + +@preg_match('//u', "\xff"); +try { + preg_replace('//u', 'x', "\xff", -1, $c, PREG_THROW_ON_ERROR); + echo "genuine error: no throw\n"; +} catch (PregException $e) { + echo "genuine error: threw ", $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +preg_replace (empty subject): seeded=yes, no throw +preg_replace (empty pattern): seeded=yes, no throw +preg_filter (empty subject): seeded=yes, no throw +preg_replace_callback (empty subject): seeded=yes, no throw +preg_replace_callback_array (empty): seeded=yes, no throw +genuine error: threw Malformed UTF-8 characters, possibly incorrectly encoded diff --git a/sapi/cli/tests/006.phpt b/sapi/cli/tests/006.phpt index 957543c1a28d..ef44a8af3adf 100644 --- a/sapi/cli/tests/006.phpt +++ b/sapi/cli/tests/006.phpt @@ -44,7 +44,7 @@ string(%d) "Extension [ extension #%d pcre version %s ] { } } - - Constants [19] { + - Constants [20] { Constant [ int PREG_PATTERN_ORDER ] { 1 } Constant [ int PREG_SET_ORDER ] { 2 } Constant [ int PREG_OFFSET_CAPTURE ] { 256 } @@ -53,6 +53,7 @@ string(%d) "Extension [ extension #%d pcre version %s ] { Constant [ int PREG_SPLIT_DELIM_CAPTURE ] { 2 } Constant [ int PREG_SPLIT_OFFSET_CAPTURE ] { 4 } Constant [ int PREG_GREP_INVERT ] { 1 } + Constant [ int PREG_THROW_ON_ERROR ] { 65536 } Constant [ int PREG_NO_ERROR ] { 0 } Constant [ int PREG_INTERNAL_ERROR ] { 1 } Constant [ int PREG_BACKTRACK_LIMIT_ERROR ] { 2 } @@ -91,23 +92,25 @@ string(%d) "Extension [ extension #%d pcre version %s ] { } Function [ function preg_replace ] { - - Parameters [5] { + - Parameters [6] { Parameter #0 [ array|string $pattern ] Parameter #1 [ array|string $replacement ] Parameter #2 [ array|string $subject ] Parameter #3 [ int $limit = -1 ] Parameter #4 [ &$count = null ] + Parameter #5 [ int $flags = 0 ] } - Return [ array|string|null ] } Function [ function preg_filter ] { - - Parameters [5] { + - Parameters [6] { Parameter #0 [ array|string $pattern ] Parameter #1 [ array|string $replacement ] Parameter #2 [ array|string $subject ] Parameter #3 [ int $limit = -1 ] Parameter #4 [ &$count = null ] + Parameter #5 [ int $flags = 0 ] } - Return [ array|string|null ] } @@ -174,6 +177,12 @@ string(%d) "Extension [ extension #%d pcre version %s ] { - Return [ string ] } } + + - Classes [1] { + Class [ class PregException extends Exception implements Throwable, Stringable ] { +%A + } + } } "