Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
27 changes: 27 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
67 changes: 64 additions & 3 deletions ext/pcre/php_pcre.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
/* }}} */
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}
/* }}} */

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
/* }}} */

Expand Down Expand Up @@ -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);
}
/* }}} */

Expand All @@ -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 {
Expand Down Expand Up @@ -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();
}
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
/* }}} */

Expand Down Expand Up @@ -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);
}
/* }}} */

Expand Down
14 changes: 12 additions & 2 deletions ext/pcre/php_pcre.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -112,13 +117,13 @@ function preg_match_all(string $pattern, string $subject, &$matches = null, int
* @return string|array<int|string, string>|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<int|string, string>|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
Expand All @@ -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 {}
14 changes: 13 additions & 1 deletion ext/pcre/php_pcre_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions ext/pcre/tests/preg_throw_on_error.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
--TEST--
PREG_THROW_ON_ERROR: every preg_*() function throws PregException on a PCRE error
--FILE--
<?php

$bad = "\xff";

$cases = [
'preg_match' => 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)
Loading
Loading