Skip to content
Open
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
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ PHP NEWS
. Added the "filter.max_filter_count" stream context option for php://filter
URLs. Using more than 16 filters without configuring this option is now
deprecated. (Sjoerd Langkemper)
. Improved performance of array_intersect(). (mehmetcansahin)
. Fixed bug GH-23006 (phpcredits() full-page HTML title says phpinfo()).
(Weilin Du)
. The following functions now raise a ValueError when the $filename argument
Expand Down
9 changes: 9 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ PHP 8.6 UPGRADE NOTES
SplTempFileObject; the two previously returned different values.

- Standard:
. array_intersect() with at least two arrays now converts values to strings
while scanning its inputs instead of during sort comparisons. This can
change the number and order of conversion warnings and __toString() calls,
which conversion exception is reached, and the result for stateful
__toString() implementations. Argument types are validated before checking
for empty arrays or converting values, so an invalid later argument can
suppress conversion side effects from earlier arrays. Values are not
converted if any input array is empty.
. Form feed (\f) is now added in the default trimmed characters of trim(),
rtrim() and ltrim().
RFC: https://wiki.php.net/rfc/trim_form_feed
Expand Down Expand Up @@ -705,6 +713,7 @@ PHP 8.6 UPGRADE NOTES

- Standard:
. Improved performance of array_fill_keys().
. Improved performance of array_intersect().
. Improved performance of array_map() with multiple arrays passed.
. Improved performance of array_sum() and array_product() for
integer-only arrays.
Expand Down
7 changes: 4 additions & 3 deletions Zend/tests/bug74093.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ max_execution_time=1
hard_timeout=1
--FILE--
<?php
$a1 = range(1, 3000000);
$a2 = range(100000, 3999999);
array_intersect($a1, $a2);
$values = range(1, 6000000);
/* array_intersect() now uses a linear-time hash implementation. Use a large
* internal string sort to retain the hard-timeout workload. */
sort($values, SORT_STRING);
?>
--EXPECTF--
Fatal error: Maximum execution time of 1+1 seconds exceeded %s
10 changes: 10 additions & 0 deletions Zend/tests/named_params/internal_variadics.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ try {
echo $e->getMessage(), "\n";
}

var_dump(array_intersect(array: [1, 2]) === [1, 2]);

try {
array_intersect([1, 2], arrays: [2]);
} catch (ArgumentCountError $e) {
echo $e->getMessage(), "\n";
}

try {
$array = [1, 2];
array_push($array, ...['values' => 3]);
Expand All @@ -25,4 +33,6 @@ try {
--EXPECT--
Internal function array_merge() does not accept named variadic arguments
Internal function array_diff_key() does not accept named variadic arguments
bool(true)
Internal function array_intersect() does not accept named variadic arguments
Internal function array_push() does not accept named variadic arguments
199 changes: 199 additions & 0 deletions ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -5369,9 +5369,208 @@ PHP_FUNCTION(array_intersect_ukey)
}
/* }}} */

static zend_always_inline bool php_array_intersect_get_key(
zval *value, zend_ulong *num_key, zend_string **str_key, zend_string **tmp_key)
{
ZVAL_DEREF(value);
*tmp_key = NULL;

if (Z_TYPE_P(value) == IS_LONG) {
*num_key = (zend_ulong) Z_LVAL_P(value);
*str_key = NULL;
return true;
}

if (Z_TYPE_P(value) == IS_STRING) {
*str_key = Z_STR_P(value);
return true;
}

*str_key = zval_try_get_tmp_string(value, tmp_key);
return *str_key != NULL;
}

static zend_always_inline void php_array_intersect_empty_result(zval *first, zval *return_value)
{
HashTable *result;
bool in_place = zend_may_modify_arg_in_place(first);

if (in_place) {
result = Z_ARRVAL_P(first);
ZVAL_ARR(return_value, result);
} else {
result = zend_array_dup(Z_ARRVAL_P(first));
ZVAL_ARR(return_value, result);
}

ZEND_HASH_FOREACH_KEY(result, zend_ulong num_key, zend_string *key) {
if (key) {
zend_hash_del(result, key);
} else {
zend_hash_index_del(result, num_key);
}
} ZEND_HASH_FOREACH_END();

if (in_place) {
Z_ADDREF_P(return_value);
}
}

/* {{{ Hash-based implementation of array_intersect(). Values are compared
* using their string representation. On the long|string domain, this is
* exactly key equality under symtable normalization: a long and a string
* compare equal iff the string is the canonical decimal representation of the
* long, which is precisely when ZEND_HANDLE_NUMERIC converts it to that long
* key. Other values are converted to string before the same normalization. */
static zend_never_inline void php_array_intersect_hash(zval *args, uint32_t argc, zval *return_value)
{
for (uint32_t i = 0; i < argc; i++) {
if (Z_TYPE(args[i]) != IS_ARRAY) {
zend_argument_type_error(i + 1, "must be of type array, %s given", zend_zval_value_name(&args[i]));
return;
}
}

/* An empty argument makes the intersection empty, so no values need to be
* converted to string. */
for (uint32_t i = 0; i < argc; i++) {
if (zend_hash_num_elements(Z_ARRVAL(args[i])) == 0) {
php_array_intersect_empty_result(&args[0], return_value);
return;
}
}

/* Map each value of args[1] to the number of consecutive arguments,
* starting from args[1], the value has been seen in. */
zval one;
ZVAL_LONG(&one, 1);
HashTable set;
zend_hash_init(&set, zend_hash_num_elements(Z_ARRVAL(args[1])), NULL, NULL, 0);
zend_bitset delete_bitset = NULL;
ALLOCA_FLAG(use_heap);
bool in_place = false;

ZEND_HASH_FOREACH_VAL(Z_ARRVAL(args[1]), zval *value) {
zend_ulong value_num_key = 0;
zend_string *value_str_key, *tmp_key;
if (!php_array_intersect_get_key(value, &value_num_key, &value_str_key, &tmp_key)) {
goto cleanup;
}
if (value_str_key) {
zend_symtable_update(&set, value_str_key, &one);
} else {
zend_hash_index_update(&set, value_num_key, &one);
}
zend_tmp_string_release(tmp_key);
} ZEND_HASH_FOREACH_END();

for (uint32_t i = 2; i < argc; i++) {
ZEND_HASH_FOREACH_VAL(Z_ARRVAL(args[i]), zval *value) {
zend_ulong value_num_key = 0;
zend_string *value_str_key, *tmp_key;
if (!php_array_intersect_get_key(value, &value_num_key, &value_str_key, &tmp_key)) {
goto cleanup;
}
zval *count;
if (value_str_key) {
count = zend_symtable_find(&set, value_str_key);
} else {
count = zend_hash_index_find(&set, value_num_key);
}
zend_tmp_string_release(tmp_key);
if (count && Z_LVAL_P(count) == (zend_long) i - 1) {
ZVAL_LONG(count, i);
}
} ZEND_HASH_FOREACH_END();
}

/* Match the generic path by filtering the first argument in place if
* possible and duplicating it otherwise. In particular, duplication keeps
* bucket holes whose positions are observable through array_rand(). */
HashTable *result;
in_place = zend_may_modify_arg_in_place(&args[0]);
if (in_place) {
result = Z_ARRVAL(args[0]);
ZVAL_ARR(return_value, result);
} else {
result = zend_array_dup(Z_ARRVAL(args[0]));
ZVAL_ARR(return_value, result);
}

/* Determine all entries to remove before deleting any. Deleting an entry may
* invoke a user destructor that changes subsequent string conversions. */
uint32_t delete_bitset_len = zend_bitset_len(zend_hash_num_elements(result));
delete_bitset = ZEND_BITSET_ALLOCA(delete_bitset_len, use_heap);
zend_bitset_clear(delete_bitset, delete_bitset_len);

uint32_t result_pos = 0;
ZEND_HASH_FOREACH_VAL(result, zval *entry) {
zend_ulong value_num_key = 0;
zend_string *value_str_key, *tmp_key;
if (!php_array_intersect_get_key(entry, &value_num_key, &value_str_key, &tmp_key)) {
goto cleanup;
}
zval *count;
if (value_str_key) {
count = zend_symtable_find(&set, value_str_key);
} else {
count = zend_hash_index_find(&set, value_num_key);
}
zend_tmp_string_release(tmp_key);
if (!count || Z_LVAL_P(count) != (zend_long) argc - 1) {
zend_bitset_incl(delete_bitset, result_pos);
}
result_pos++;
} ZEND_HASH_FOREACH_END();

/* A conversion may retain the first argument through reentrant user code,
* so it may no longer be safe to modify the original array in place. */
if (in_place && !zend_may_modify_arg_in_place(&args[0])) {
result = zend_array_dup(Z_ARRVAL(args[0]));
ZVAL_ARR(return_value, result);
in_place = false;
}

result_pos = 0;
ZEND_HASH_FOREACH_KEY(result, zend_ulong num_key, zend_string *key) {
if (zend_bitset_in(delete_bitset, result_pos)) {
if (key) {
zend_hash_del(result, key);
} else {
zend_hash_index_del(result, num_key);
}
}
result_pos++;
} ZEND_HASH_FOREACH_END();

cleanup:
if (delete_bitset) {
free_alloca(delete_bitset, use_heap);
}
zend_hash_destroy(&set);
if (in_place) {
Z_ADDREF_P(return_value);
}
}
/* }}} */

/* {{{ Returns the entries of arr1 that have values which are present in all the other arguments */
PHP_FUNCTION(array_intersect)
{
zval *args;
uint32_t argc;

if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) {
RETURN_THROWS();
}

if (argc >= 2) {
php_array_intersect_hash(args, argc, return_value);
return;
}

/* Preserve the generic path and its conversion side effects for calls with
* a single array. */
php_array_intersect(INTERNAL_FUNCTION_PARAM_PASSTHRU, INTERSECT_NORMAL, INTERSECT_COMP_DATA_INTERNAL, INTERSECT_COMP_KEY_INTERNAL);
}
/* }}} */
Expand Down
55 changes: 55 additions & 0 deletions ext/standard/tests/array/array_intersect_empty.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--TEST--
array_intersect() does not convert values when an argument is empty
--FILE--
<?php
class ThrowingStringableValue {
public function __toString(): string {
throw new RuntimeException('conversion failed');
}
}

set_error_handler(static function (int $code, string $message): never {
throw new ErrorException($message, $code);
});

$cases = [
static fn() => array_intersect([], [[1]]),
static fn() => array_intersect([[1]], []),
static fn() => array_intersect([new ThrowingStringableValue()], ['value'], []),
static fn() => array_intersect([], [new stdClass()]),
];

foreach ($cases as $case) {
try {
var_dump($case());
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
}

restore_error_handler();

$result = array_intersect([9 => 'value'], []);
$result[] = 'appended';
var_dump(array_keys($result));

try {
array_intersect([], [], new stdClass());
} catch (TypeError $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
array(0) {
}
array(0) {
}
array(0) {
}
array(0) {
}
array(1) {
[0]=>
int(10)
}
array_intersect(): Argument #3 must be of type array, stdClass given
Loading
Loading