From 6ee99c6b047d9ae35dc3457f730a2faad2691eea Mon Sep 17 00:00:00 2001 From: Osama Aldemeery Date: Sun, 12 Jul 2026 01:13:05 +0300 Subject: [PATCH] Add a PREG_THROW_ON_ERROR flag that makes the preg functions throw on error. --- NEWS | 4 + UPGRADING | 24 +++ ext/fileinfo/libmagic/funcs.c | 2 +- ext/fileinfo/libmagic/softmagic.c | 2 +- ext/pcre/php_pcre.c | 155 ++++++++++++++---- ext/pcre/php_pcre.h | 4 +- ext/pcre/php_pcre.stub.php | 14 +- ext/pcre/php_pcre_arginfo.h | 14 +- ext/pcre/tests/preg_throw_on_error.phpt | 38 +++++ .../preg_throw_on_error_array_pattern.phpt | 25 +++ .../preg_throw_on_error_array_subject.phpt | 36 ++++ .../tests/preg_throw_on_error_backtrack.phpt | 19 +++ .../preg_throw_on_error_callback_nested.phpt | 42 +++++ ext/pcre/tests/preg_throw_on_error_class.phpt | 25 +++ .../tests/preg_throw_on_error_compile.phpt | 64 ++++++++ .../preg_throw_on_error_compile_opt_in.phpt | 13 ++ .../preg_throw_on_error_consistency.phpt | 32 ++++ .../tests/preg_throw_on_error_named_arg.phpt | 21 +++ .../tests/preg_throw_on_error_opt_in.phpt | 33 ++++ .../preg_throw_on_error_stale_error.phpt | 40 +++++ sapi/cli/tests/006.phpt | 15 +- win32/sendmail.c | 4 +- 22 files changed, 579 insertions(+), 47 deletions(-) create mode 100644 ext/pcre/tests/preg_throw_on_error.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_array_pattern.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_array_subject.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_backtrack.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_callback_nested.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_class.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_compile.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_compile_opt_in.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_consistency.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_named_arg.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_opt_in.phpt create mode 100644 ext/pcre/tests/preg_throw_on_error_stale_error.phpt 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..f333dcdbe80a 100644 --- a/UPGRADING +++ b/UPGRADING @@ -289,6 +289,20 @@ 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, any PCRE error reported by preg_last_error() + throws a \PregException instead of emitting a warning or returning + false/null. The exception's code and message are exactly what + preg_last_error() and preg_last_error_msg() report for the same call: the + flag changes how an error is delivered, not the error itself. This covers + both compilation errors (such as a malformed pattern) and execution errors + (such as an exhausted backtrack limit or malformed UTF-8 input under the /u + modifier). preg_replace() and preg_filter() gained a $flags parameter to + accept it. 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 +387,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 +461,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 +523,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/fileinfo/libmagic/funcs.c b/ext/fileinfo/libmagic/funcs.c index cebf41309a66..532ecad6a3a2 100644 --- a/ext/fileinfo/libmagic/funcs.c +++ b/ext/fileinfo/libmagic/funcs.c @@ -663,7 +663,7 @@ file_replace(struct magic_set *ms, const char *pat, const char *rep) opts |= PCRE2_MULTILINE; pattern = convert_libmagic_pattern(pat, strlen(pat), opts); - pce = pcre_get_compiled_regex_cache_ex(pattern, 0); + pce = pcre_get_compiled_regex_cache_ex(pattern, 0, 0); zend_string_release_ex(pattern, 0); if (pce == NULL) { rep_cnt = -1; diff --git a/ext/fileinfo/libmagic/softmagic.c b/ext/fileinfo/libmagic/softmagic.c index 9ad85cce3b10..607a0fe4a368 100644 --- a/ext/fileinfo/libmagic/softmagic.c +++ b/ext/fileinfo/libmagic/softmagic.c @@ -497,7 +497,7 @@ check_fmt(struct magic_set *ms, const char *fmt) return 0; pattern = ZSTR_INIT_LITERAL("~%[-0-9\\.]*s~", 0); - if ((pce = pcre_get_compiled_regex_cache_ex(pattern, 0)) == NULL) { + if ((pce = pcre_get_compiled_regex_cache_ex(pattern, 0, 0)) == NULL) { rv = -1; } else { pcre2_code *re = php_pcre_pce_re(pce); diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c index 6a62d9717e7b..14a93084095c 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,53 @@ 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; +} + +ZEND_ATTRIBUTE_FORMAT(printf, 3, 4) +static void php_pcre_throw_or_warn(zend_long flags, int pcre_errcode, const char *format, ...) +{ + va_list args; + char *message; + + if (flags & PREG_THROW_ON_ERROR) { + pcre_handle_exec_error(pcre_errcode); + php_pcre_throw_on_error(flags); + return; + } + + va_start(args, format); + vspprintf(&message, 0, format, args); + va_end(args); + + php_error_docref(NULL, E_WARNING, "%s", message); + efree(message); + + pcre_handle_exec_error(pcre_errcode); +} + +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 bool php_pcre_flag_error_pending(zend_long flags) +{ + return (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 +478,8 @@ static PHP_MINIT_FUNCTION(pcre) register_php_pcre_symbols(module_number); + php_pcre_exception_ce = register_class_PregException(zend_ce_exception); + return SUCCESS; } /* }}} */ @@ -571,7 +625,7 @@ static zend_always_inline size_t calculate_unit_length(pcre_cache_entry *pce, co /* }}} */ /* {{{ pcre_get_compiled_regex_cache */ -PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bool locale_aware) +PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bool locale_aware, zend_long flags) { pcre2_code *re = NULL; #if 10 == PCRE2_MAJOR && 37 == PCRE2_MINOR && !defined(HAVE_BUNDLED_PCRE) @@ -625,8 +679,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo if (key != regex) { zend_string_release_ex(key, 0); } - php_error_docref(NULL, E_WARNING, "Empty regular expression"); - pcre_handle_exec_error(PCRE2_ERROR_INTERNAL); + php_pcre_throw_or_warn(flags, PCRE2_ERROR_INTERNAL, "Empty regular expression"); return NULL; } @@ -637,8 +690,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo if (key != regex) { zend_string_release_ex(key, 0); } - php_error_docref(NULL, E_WARNING, "Delimiter must not be alphanumeric, backslash, or NUL byte"); - pcre_handle_exec_error(PCRE2_ERROR_INTERNAL); + php_pcre_throw_or_warn(flags, PCRE2_ERROR_INTERNAL, "Delimiter must not be alphanumeric, backslash, or NUL byte"); return NULL; } @@ -681,11 +733,10 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo zend_string_release_ex(key, 0); } if (start_delimiter == end_delimiter) { - php_error_docref(NULL,E_WARNING, "No ending delimiter '%c' found", delimiter); + php_pcre_throw_or_warn(flags, PCRE2_ERROR_INTERNAL, "No ending delimiter '%c' found", delimiter); } else { - php_error_docref(NULL,E_WARNING, "No ending matching delimiter '%c' found", delimiter); + php_pcre_throw_or_warn(flags, PCRE2_ERROR_INTERNAL, "No ending matching delimiter '%c' found", delimiter); } - pcre_handle_exec_error(PCRE2_ERROR_INTERNAL); return NULL; } @@ -734,11 +785,10 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo case 'e': /* legacy eval */ default: if (pp[-1]) { - php_error_docref(NULL, E_WARNING, "Unknown modifier '%c'", pp[-1]); + php_pcre_throw_or_warn(flags, PCRE2_ERROR_INTERNAL, "Unknown modifier '%c'", pp[-1]); } else { - php_error_docref(NULL, E_WARNING, "NUL byte is not a valid modifier"); + php_pcre_throw_or_warn(flags, PCRE2_ERROR_INTERNAL, "NUL byte is not a valid modifier"); } - pcre_handle_exec_error(PCRE2_ERROR_INTERNAL); efree(pattern); if (key != regex) { zend_string_release_ex(key, 0); @@ -756,8 +806,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo * set ptr to NULL first so the destructor (pefree) is safe. */ ZVAL_PTR(zv, NULL); zend_hash_str_del(&char_tables, ZSTR_VAL(BG(ctype_string)), ZSTR_LEN(BG(ctype_string))); - php_error_docref(NULL,E_WARNING, "Failed to generate locale character tables"); - pcre_handle_exec_error(PCRE2_ERROR_NOMEMORY); + php_pcre_throw_or_warn(flags, PCRE2_ERROR_NOMEMORY, "Failed to generate locale character tables"); zend_string_release_ex(key, 0); efree(pattern); return NULL; @@ -779,8 +828,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo zend_string_release_ex(key, 0); } pcre2_get_error_message(errnumber, error, sizeof(error)); - php_error_docref(NULL,E_WARNING, "Compilation failed: %s at offset %zu", error, erroffset); - pcre_handle_exec_error(PCRE2_ERROR_INTERNAL); + php_pcre_throw_or_warn(flags, PCRE2_ERROR_INTERNAL, "Compilation failed: %s at offset %zu", error, erroffset); efree(pattern); return NULL; } @@ -832,8 +880,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo zend_string_release_ex(key, 0); } pcre2_code_free(new_entry.re); - php_error_docref(NULL, E_WARNING, "Internal pcre_pattern_info() error %d", rc); - pcre_handle_exec_error(PCRE2_ERROR_INTERNAL); + php_pcre_throw_or_warn(flags, PCRE2_ERROR_INTERNAL, "Internal pcre_pattern_info() error %d", rc); return NULL; } @@ -866,7 +913,7 @@ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bo /* {{{ pcre_get_compiled_regex_cache */ PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache(zend_string *regex) { - return pcre_get_compiled_regex_cache_ex(regex, true); + return pcre_get_compiled_regex_cache_ex(regex, true, 0); } /* }}} */ @@ -1102,8 +1149,10 @@ 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) { + if ((pce = pcre_get_compiled_regex_cache_ex(regex, true, flags)) == NULL) { RETURN_FALSE; } @@ -1116,6 +1165,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); } /* }}} */ @@ -1579,7 +1630,7 @@ PHPAPI zend_string *php_pcre_replace(zend_string *regex, zend_string *subject_str, const char *subject, size_t subject_len, zend_string *replace_str, - size_t limit, size_t *replace_count) + size_t limit, size_t *replace_count, zend_long flags) { pcre_cache_entry *pce; /* Compiled regular expression */ zend_string *result; /* Function result */ @@ -1590,7 +1641,7 @@ PHPAPI zend_string *php_pcre_replace(zend_string *regex, } /* Compile regex or get it from cache. */ - if ((pce = pcre_get_compiled_regex_cache(regex)) == NULL) { + if ((pce = pcre_get_compiled_regex_cache_ex(regex, true, flags)) == NULL) { return NULL; } pce->refcount++; @@ -1945,9 +1996,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; @@ -2056,7 +2111,7 @@ static zend_always_inline zend_string *php_pcre_replace_func(zend_string *regex, zend_string *result; /* Function result */ /* Compile regex or get it from cache. */ - if ((pce = pcre_get_compiled_regex_cache(regex)) == NULL) { + if ((pce = pcre_get_compiled_regex_cache_ex(regex, true, flags)) == NULL) { return NULL; } pce->refcount++; @@ -2069,7 +2124,7 @@ static zend_always_inline zend_string *php_pcre_replace_func(zend_string *regex, /* {{{ php_pcre_replace_array */ static zend_string *php_pcre_replace_array(HashTable *regex, zend_string *replace_str, HashTable *replace_ht, - zend_string *subject_str, size_t limit, size_t *replace_count) + zend_string *subject_str, size_t limit, size_t *replace_count, zend_long flags) { zval *regex_entry; zend_string *result; @@ -2105,7 +2160,7 @@ static zend_string *php_pcre_replace_array(HashTable *regex, /* Do the actual replacement and put the result back into subject_str for further replacements. */ result = php_pcre_replace(regex_str, subject_str, ZSTR_VAL(subject_str), - ZSTR_LEN(subject_str), replace_entry_str, limit, replace_count); + ZSTR_LEN(subject_str), replace_entry_str, limit, replace_count, flags); zend_tmp_string_release(tmp_replace_entry_str); zend_tmp_string_release(tmp_regex_str); zend_string_release_ex(subject_str, 0); @@ -2127,7 +2182,7 @@ static zend_string *php_pcre_replace_array(HashTable *regex, /* Do the actual replacement and put the result back into subject_str for further replacements. */ result = php_pcre_replace(regex_str, subject_str, ZSTR_VAL(subject_str), - ZSTR_LEN(subject_str), replace_str, limit, replace_count); + ZSTR_LEN(subject_str), replace_str, limit, replace_count, flags); zend_tmp_string_release(tmp_regex_str); zend_string_release_ex(subject_str, 0); subject_str = result; @@ -2146,18 +2201,18 @@ static zend_string *php_pcre_replace_array(HashTable *regex, static zend_always_inline zend_string *php_replace_in_subject( zend_string *regex_str, HashTable *regex_ht, zend_string *replace_str, HashTable *replace_ht, - zend_string *subject, size_t limit, size_t *replace_count) + zend_string *subject, size_t limit, size_t *replace_count, zend_long flags) { zend_string *result; if (regex_str) { ZEND_ASSERT(replace_str != NULL); result = php_pcre_replace(regex_str, subject, ZSTR_VAL(subject), ZSTR_LEN(subject), - replace_str, limit, replace_count); + replace_str, limit, replace_count, flags); } else { ZEND_ASSERT(regex_ht != NULL); result = php_pcre_replace_array(regex_ht, replace_str, replace_ht, subject, - limit, replace_count); + limit, replace_count, flags); } return result; } @@ -2253,6 +2308,10 @@ static size_t php_preg_replace_func_impl(zval *return_value, } } zend_tmp_string_release(tmp_subject_entry_str); + + if (php_pcre_flag_error_pending(flags)) { + break; + } } ZEND_HASH_FOREACH_END(); } @@ -2266,6 +2325,7 @@ static void _preg_replace_common( HashTable *subject_ht, zend_string *subject_str, zend_long limit, zval *zcount, + zend_long flags, bool is_filter ) { size_t replace_count = 0; @@ -2281,7 +2341,7 @@ static void _preg_replace_common( if (subject_str) { old_replace_count = replace_count; result = php_replace_in_subject(regex_str, regex_ht, replace_str, replace_ht, - subject_str, limit, &replace_count); + subject_str, limit, &replace_count, flags); if (result != NULL) { if (!is_filter || replace_count > old_replace_count) { RETVAL_STR(result); @@ -2310,7 +2370,7 @@ static void _preg_replace_common( zend_string *tmp_subject_entry_str; zend_string *subject_entry_str = zval_get_tmp_string(subject_entry, &tmp_subject_entry_str); result = php_replace_in_subject(regex_str, regex_ht, replace_str, replace_ht, - subject_entry_str, limit, &replace_count); + subject_entry_str, limit, &replace_count, flags); if (result != NULL) { if (!is_filter || replace_count > old_replace_count) { @@ -2326,6 +2386,10 @@ static void _preg_replace_common( } } zend_tmp_string_release(tmp_subject_entry_str); + + if (php_pcre_flag_error_pending(flags)) { + break; + } } ZEND_HASH_FOREACH_END(); } @@ -2341,23 +2405,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); + limit, zcount, flags, is_filter); + + php_pcre_throw_on_error(flags); } /* }}} */ @@ -2383,7 +2453,7 @@ ZEND_FRAMELESS_FUNCTION(preg_replace, 3) regex_ht, regex_str, replace_ht, replace_str, subject_ht, subject_str, - /* limit */ -1, /* zcount */ NULL, /* is_filter */ false); + /* limit */ -1, /* zcount */ NULL, /* flags */ 0, /* is_filter */ false); flf_clean:; Z_FLF_PARAM_FREE_STR(1, regex_tmp); @@ -2415,12 +2485,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 +2517,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 +2560,7 @@ PHP_FUNCTION(preg_replace_callback_array) break; case IS_NULL: RETVAL_NULL(); + php_pcre_throw_on_error(flags); goto error; default: ZEND_UNREACHABLE(); } @@ -2491,6 +2568,10 @@ PHP_FUNCTION(preg_replace_callback_array) if (EG(exception)) { goto error; } + + if (php_pcre_throw_on_error(flags)) { + goto error; + } } ZEND_HASH_FOREACH_END(); if (zcount) { @@ -2543,13 +2624,15 @@ PHP_FUNCTION(preg_split) ZEND_PARSE_PARAMETERS_END(); /* Compile regex or get it from cache. */ - if ((pce = pcre_get_compiled_regex_cache(regex)) == NULL) { + if ((pce = pcre_get_compiled_regex_cache_ex(regex, true, flags)) == NULL) { RETURN_FALSE; } pce->refcount++; php_pcre_split_impl(pce, subject, return_value, limit_val, flags); pce->refcount--; + + php_pcre_throw_on_error(flags); } /* }}} */ @@ -2905,13 +2988,15 @@ PHP_FUNCTION(preg_grep) ZEND_PARSE_PARAMETERS_END(); /* Compile regex or get it from cache. */ - if ((pce = pcre_get_compiled_regex_cache(regex)) == NULL) { + if ((pce = pcre_get_compiled_regex_cache_ex(regex, true, flags)) == NULL) { 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.h b/ext/pcre/php_pcre.h index ebaa686a31c3..b27e9783f742 100644 --- a/ext/pcre/php_pcre.h +++ b/ext/pcre/php_pcre.h @@ -23,7 +23,7 @@ #include -PHPAPI zend_string *php_pcre_replace(zend_string *regex, zend_string *subject_str, const char *subject, size_t subject_len, zend_string *replace_str, size_t limit, size_t *replace_count); +PHPAPI zend_string *php_pcre_replace(zend_string *regex, zend_string *subject_str, const char *subject, size_t subject_len, zend_string *replace_str, size_t limit, size_t *replace_count, zend_long flags); PHPAPI pcre2_code* pcre_get_compiled_regex(zend_string *regex, uint32_t *capture_count); extern zend_module_entry pcre_module_entry; @@ -45,7 +45,7 @@ typedef enum { } php_pcre_error_code; PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache(zend_string *regex); -PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bool locale_aware); +PHPAPI pcre_cache_entry* pcre_get_compiled_regex_cache_ex(zend_string *regex, bool locale_aware, zend_long flags); PHPAPI void php_pcre_match_impl(pcre_cache_entry *pce, zend_string *subject_str, zval *return_value, zval *subpats, bool global, zend_long flags, zend_off_t start_offset); 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..5e35a0f6ff4d --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_array_pattern.phpt @@ -0,0 +1,25 @@ +--TEST-- +PREG_THROW_ON_ERROR: preg_replace() with an array of patterns throws on the first failing pattern +--FILE-- +getMessage(), + ($e->getCode() === preg_last_error() && $e->getMessage() === preg_last_error_msg()) ? 'yes' : 'no'); +} + +try { + preg_replace(['/a/', '//u'], 'x', "\xff", -1, $count, PREG_THROW_ON_ERROR); + echo "exec: no exception thrown\n"; +} catch (PregException $e) { + printf("exec: %s | matches: %s\n", $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..cc054b5710eb --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_array_subject.phpt @@ -0,0 +1,36 @@ +--TEST-- +PREG_THROW_ON_ERROR: an array of subjects (or grep inputs) throws on any failing entry, regardless of position +--FILE-- + fn() => preg_replace('//u', 'x', [$bad, 'ok'], -1, $c, PREG_THROW_ON_ERROR), + 'preg_replace (bad last)' => fn() => preg_replace('//u', 'x', ['ok', $bad], -1, $c, PREG_THROW_ON_ERROR), + 'preg_filter' => fn() => preg_filter('//u', 'x', [$bad, 'ok'], -1, $c, PREG_THROW_ON_ERROR), + 'preg_replace_callback' => fn() => preg_replace_callback('//u', fn($m) => 'x', [$bad, 'ok'], -1, $c, PREG_THROW_ON_ERROR), + 'preg_replace_callback_array' => fn() => preg_replace_callback_array(['//u' => fn($m) => 'x'], [$bad, 'ok'], -1, $c, PREG_THROW_ON_ERROR), + 'preg_grep (bad first)' => fn() => preg_grep('//u', [$bad, 'ok'], PREG_THROW_ON_ERROR), + 'preg_grep (bad last)' => fn() => preg_grep('//u', ['ok', $bad], PREG_THROW_ON_ERROR), +]; + +foreach ($cases as $name => $case) { + try { + $case(); + echo "$name: no exception thrown\n"; + } catch (PregException $e) { + printf("%s: %s (code matches: %s)\n", $name, $e->getMessage(), + $e->getCode() === PREG_BAD_UTF8_ERROR ? 'yes' : 'no'); + } +} + +?> +--EXPECT-- +preg_replace (bad first): Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_replace (bad last): Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_filter: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_replace_callback: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_replace_callback_array: Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_grep (bad first): Malformed UTF-8 characters, possibly incorrectly encoded (code matches: yes) +preg_grep (bad last): Malformed UTF-8 characters, possibly incorrectly encoded (code matches: 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..9a5f7fbd82c6 --- /dev/null +++ b/ext/pcre/tests/preg_throw_on_error_compile.phpt @@ -0,0 +1,64 @@ +--TEST-- +PREG_THROW_ON_ERROR: a malformed pattern throws PregException carrying the same error as preg_last_error()/preg_last_error_msg(), with no warning +--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) { + try { + $case(); + echo "$name: no exception thrown\n"; + } catch (PregException $e) { + printf("%s: %s | matches preg_last_error()/msg(): %s\n", + $name, $e->getMessage(), + ($e->getCode() === preg_last_error() && $e->getMessage() === preg_last_error_msg()) ? 'yes' : 'no'); + } +} + +echo "\n"; + +$sites = [ + 'empty pattern' => '', + 'bad delimiter' => '1a1', + 'no ending delimiter' => '/x', + 'unknown modifier' => '/x/Z', + 'unterminated class' => '/[/', +]; +foreach ($sites as $label => $bad) { + try { + preg_match($bad, 'x', $m, PREG_THROW_ON_ERROR); + 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-- +preg_match: Internal error | matches preg_last_error()/msg(): yes +preg_match_all: Internal error | matches preg_last_error()/msg(): yes +preg_replace: Internal error | matches preg_last_error()/msg(): yes +preg_filter: Internal error | matches preg_last_error()/msg(): yes +preg_replace_callback: Internal error | matches preg_last_error()/msg(): yes +preg_replace_callback_array: Internal error | matches preg_last_error()/msg(): yes +preg_split: Internal error | matches preg_last_error()/msg(): yes +preg_grep: Internal error | matches preg_last_error()/msg(): yes + +empty pattern: Internal error | matches: yes +bad delimiter: Internal error | matches: yes +no ending delimiter: Internal error | matches: yes +unknown modifier: Internal error | matches: yes +unterminated class: Internal error | 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..03dd402d6a73 --- /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 + } + } } " diff --git a/win32/sendmail.c b/win32/sendmail.c index 0160838054d8..3f050c77d1ad 100644 --- a/win32/sendmail.c +++ b/win32/sendmail.c @@ -148,7 +148,7 @@ static zend_string *php_win32_mail_trim_header(const char *header) NULL, header, strlen(header), replace, -1, - NULL); + NULL, 0); zend_string_release_ex(replace, false); zend_string_release_ex(regex, false); @@ -164,7 +164,7 @@ static zend_string *php_win32_mail_trim_header(const char *header) result, ZSTR_VAL(result), ZSTR_LEN(result), replace, -1, - NULL); + NULL, 0); zend_string_release_ex(replace, false); zend_string_release_ex(regex, false); zend_string_release_ex(result, false);