Skip to content

Commit d11e94e

Browse files
committed
Add SplFixedArray::__set_state
Fixes bug 75268. The design is that all string keys must come before all integer keys. The string keys become properties and the integer keys become values in the array. Other serialization/unserialization methods should adopt this same strategy as there are bugs: https://3v4l.org/IMnI5 Migrate SplFixedArray to get_properties_for. Extract spl_fixedarray_import, as I expect to reuse this code for other cases as well, such as the serialization bug mentioned above.
1 parent 1079622 commit d11e94e

7 files changed

+340
-13
lines changed

ext/spl/spl_fixedarray.c

Lines changed: 183 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -195,23 +195,18 @@ static HashTable* spl_fixedarray_object_get_gc(zend_object *obj, zval **table, i
195195
return ht;
196196
}
197197

198-
static HashTable* spl_fixedarray_object_get_properties(zend_object *obj)
198+
static HashTable* spl_fixedarray_get_properties_for(zend_object *obj, zend_prop_purpose purpose)
199199
{
200200
spl_fixedarray_object *intern = spl_fixed_array_from_obj(obj);
201-
HashTable *ht = zend_std_get_properties(obj);
202201

203-
if (!spl_fixedarray_empty(&intern->array)) {
204-
zend_long j = zend_hash_num_elements(ht);
202+
/* Keep the values and properties separate*/
203+
HashTable *ht = zend_array_dup(zend_std_get_properties(obj));
205204

205+
if (!spl_fixedarray_empty(&intern->array)) {
206206
for (zend_long i = 0; i < intern->array.size; i++) {
207-
zend_hash_index_update(ht, i, &intern->array.elements[i]);
207+
zend_hash_index_add_new(ht, i, &intern->array.elements[i]);
208208
Z_TRY_ADDREF(intern->array.elements[i]);
209209
}
210-
if (j > intern->array.size) {
211-
for (zend_long i = intern->array.size; i < j; ++i) {
212-
zend_hash_index_del(ht, i);
213-
}
214-
}
215210
}
216211

217212
return ht;
@@ -628,7 +623,6 @@ PHP_METHOD(SplFixedArray, fromArray)
628623
} else if (num > 0 && !save_indexes) {
629624
zval *element;
630625
zend_long i = 0;
631-
632626
spl_fixedarray_init(&array, num);
633627

634628
ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(data), element) {
@@ -754,6 +748,182 @@ PHP_METHOD(SplFixedArray, getIterator)
754748
zend_create_internal_iterator_zval(return_value, ZEND_THIS);
755749
}
756750

751+
static zend_result spl_fixedarray_import_packed(zend_object *object, HashTable *ht)
752+
{
753+
ZEND_ASSERT(HT_FLAGS(ht) & HASH_FLAG_PACKED);
754+
755+
spl_fixedarray_object *intern = spl_fixed_array_from_obj(object);
756+
spl_fixedarray *fixed = &intern->array;
757+
758+
uint32_t n = zend_hash_num_elements(ht);
759+
zend_ulong num_idx = 0, i = 0;
760+
761+
/* This will allocate memory even if the value checks fail, but allows
762+
* a single iteration over the input array.
763+
* It's also manually initialized to do a single pass over the output.
764+
* Be sure to dtor and free the correct range if there's an error during
765+
* the construction process.
766+
*/
767+
fixed->size = n;
768+
fixed->elements = safe_emalloc(n, sizeof(zval), 0);
769+
zval *begin = fixed->elements, *end = fixed->elements + n;
770+
zval *val = NULL;
771+
772+
ZEND_HASH_FOREACH_NUM_KEY_VAL(ht, num_idx, val) {
773+
if (UNEXPECTED(num_idx != i)) {
774+
spl_fixedarray_dtor_range(fixed, 0, i);
775+
efree(fixed->elements);
776+
777+
/* Zero-initialize so the object release is valid. */
778+
spl_fixedarray_init(fixed, 0);
779+
780+
zend_argument_value_error(1,
781+
"did not have integer keys that start at 0 and increment sequentially");
782+
return FAILURE;
783+
}
784+
785+
i += 1;
786+
787+
ZEND_ASSERT(begin != end);
788+
ZVAL_COPY(begin++, val);
789+
} ZEND_HASH_FOREACH_END();
790+
791+
ZEND_ASSERT(begin == end);
792+
793+
return SUCCESS;
794+
}
795+
796+
static zend_result spl_fixedarray_import_not_packed(zend_object *object, HashTable *ht)
797+
{
798+
ZEND_ASSERT(!(HT_FLAGS(ht) & HASH_FLAG_PACKED));
799+
800+
spl_fixedarray_object *intern = spl_fixed_array_from_obj(object);
801+
spl_fixedarray *fixed = &intern->array;
802+
803+
/* The array can have string keys, but they must come prior to integers:
804+
*
805+
* SplFixedArray::__set_state(array(
806+
* 'property' => 'value',
807+
* 0 => 1,
808+
* 1 => 2,
809+
* 2 => 3,
810+
* ))
811+
*/
812+
spl_fixedarray_init(fixed, 0);
813+
814+
/* For performance we do a single pass over the input array and the
815+
* spl_fixedarray elems. This complicates the implementation, but part
816+
* of the reason for choosing SplFixedArray is for peformance, and
817+
* although that is primarily for memory we should still care about CPU.
818+
*
819+
* We need to track the number of string keys, which must come before
820+
* all the integer keys. This way the total size of the input minus the
821+
* number of string keys equals the number of integer keys, so we can
822+
* allocate the spl_fixedarray.elements and do this in a single pass.
823+
*/
824+
825+
// The total size of the input array
826+
const zend_long nht = zend_hash_num_elements(ht);
827+
zend_long nstrings = 0; // The number of string keys
828+
zend_long nints; // will be nht - nstrings
829+
830+
zend_string *str_idx = NULL; // current string key of input array
831+
zend_ulong num_idx = 0; // current int key (valid iff str_idx == NULL)
832+
zval *val = NULL; // current value of input array
833+
zend_long max_idx = -1; // the largest index found so far
834+
zval *begin = NULL; // pointer to beginning of fixedarray's storage
835+
zval *end = NULL; // points one element passed the last element
836+
const char *ex_msg = NULL; // message for the value exception
837+
838+
ZEND_HASH_FOREACH_KEY_VAL(ht, num_idx, str_idx, val) {
839+
if (str_idx != NULL) {
840+
if (UNEXPECTED(max_idx >= 0)) {
841+
ex_msg = "must have all its string keys come before all integer keys";
842+
goto value_error;
843+
}
844+
845+
++nstrings;
846+
object->handlers->write_property(object, str_idx, val, NULL);
847+
848+
} else if (UNEXPECTED((zend_long)num_idx != ++max_idx)) {
849+
ex_msg = "did not have integer keys that start at 0 and increment sequentially";
850+
goto value_error;
851+
852+
} else {
853+
if (UNEXPECTED(max_idx == 0)) {
854+
nints = nht - nstrings;
855+
fixed->size = nints;
856+
fixed->elements =
857+
safe_emalloc(nints, sizeof(zval), 0);
858+
begin = fixed->elements;
859+
end = fixed->elements + nints;
860+
}
861+
862+
ZEND_ASSERT(num_idx == max_idx);
863+
ZEND_ASSERT(begin != end);
864+
865+
ZVAL_COPY(begin++, val);
866+
}
867+
} ZEND_HASH_FOREACH_END();
868+
869+
ZEND_ASSERT(begin == end);
870+
return SUCCESS;
871+
872+
value_error:
873+
spl_fixedarray_dtor_range(fixed, 0, max_idx);
874+
if (fixed->elements) {
875+
efree(fixed->elements);
876+
}
877+
878+
/* Zero-initialize so the object release is valid. */
879+
spl_fixedarray_init(fixed, 0);
880+
881+
zend_argument_value_error(1, ex_msg);
882+
return FAILURE;
883+
}
884+
885+
/* Assumes the object has been created and the spl_fixedarray_object's
886+
* array member is uninitialized or zero-initialized.
887+
*/
888+
static zend_result spl_fixedarray_import(zend_object *object, HashTable *ht)
889+
{
890+
891+
/* if result != SUCCESS then these need to dtor their contents. */
892+
zend_result result = (EXPECTED((HT_FLAGS(ht) & HASH_FLAG_PACKED)))
893+
? spl_fixedarray_import_packed(object, ht)
894+
: spl_fixedarray_import_not_packed(object, ht);
895+
896+
if (UNEXPECTED(result != SUCCESS)) {
897+
zend_object_release(object);
898+
}
899+
900+
return result;
901+
}
902+
903+
PHP_METHOD(SplFixedArray, __set_state)
904+
{
905+
zval *state = NULL;
906+
907+
ZEND_PARSE_PARAMETERS_START(1, 1)
908+
Z_PARAM_ARRAY(state);
909+
ZEND_PARSE_PARAMETERS_END();
910+
911+
HashTable *ht = Z_ARRVAL_P(state);
912+
zend_class_entry *called_scope = zend_get_called_scope(execute_data);
913+
914+
zend_result result = object_init_ex(return_value, called_scope);
915+
if (UNEXPECTED(result != SUCCESS)) {
916+
ZVAL_NULL(return_value);
917+
RETURN_THROWS();
918+
}
919+
920+
zend_object *object = Z_OBJ_P(return_value);
921+
if (UNEXPECTED(spl_fixedarray_import(object, ht) != SUCCESS)) {
922+
ZVAL_NULL(return_value);
923+
RETURN_THROWS();
924+
}
925+
}
926+
757927
static void spl_fixedarray_it_dtor(zend_object_iterator *iter)
758928
{
759929
zval_ptr_dtor(&iter->data);
@@ -844,7 +1014,8 @@ PHP_MINIT_FUNCTION(spl_fixedarray)
8441014
spl_handler_SplFixedArray.unset_dimension = spl_fixedarray_object_unset_dimension;
8451015
spl_handler_SplFixedArray.has_dimension = spl_fixedarray_object_has_dimension;
8461016
spl_handler_SplFixedArray.count_elements = spl_fixedarray_object_count_elements;
847-
spl_handler_SplFixedArray.get_properties = spl_fixedarray_object_get_properties;
1017+
spl_handler_SplFixedArray.get_properties_for
1018+
= spl_fixedarray_get_properties_for;
8481019
spl_handler_SplFixedArray.get_gc = spl_fixedarray_object_get_gc;
8491020
spl_handler_SplFixedArray.dtor_obj = zend_objects_destroy_object;
8501021
spl_handler_SplFixedArray.free_obj = spl_fixedarray_object_free_storage;

ext/spl/spl_fixedarray.stub.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,7 @@ public function offsetSet($index, mixed $value) {}
4949
public function offsetUnset($index) {}
5050

5151
public function getIterator(): Iterator {}
52+
53+
/** @return SplFixedArray */
54+
public static function __set_state(array $state) {}
5255
}

ext/spl/spl_fixedarray_arginfo.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: c820bad6bcfcc7c60a221464008a882515b5c05e */
2+
* Stub hash: 7578bd2365447aba8ce9fcf8c11a768d2aece034 */
33

44
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_SplFixedArray___construct, 0, 0, 0)
55
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, size, IS_LONG, 0, "0")
@@ -39,6 +39,10 @@ ZEND_END_ARG_INFO()
3939
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_SplFixedArray_getIterator, 0, 0, Iterator, 0)
4040
ZEND_END_ARG_INFO()
4141

42+
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_SplFixedArray___set_state, 0, 0, 1)
43+
ZEND_ARG_TYPE_INFO(0, state, IS_ARRAY, 0)
44+
ZEND_END_ARG_INFO()
45+
4246

4347
ZEND_METHOD(SplFixedArray, __construct);
4448
ZEND_METHOD(SplFixedArray, __wakeup);
@@ -52,6 +56,7 @@ ZEND_METHOD(SplFixedArray, offsetGet);
5256
ZEND_METHOD(SplFixedArray, offsetSet);
5357
ZEND_METHOD(SplFixedArray, offsetUnset);
5458
ZEND_METHOD(SplFixedArray, getIterator);
59+
ZEND_METHOD(SplFixedArray, __set_state);
5560

5661

5762
static const zend_function_entry class_SplFixedArray_methods[] = {
@@ -67,5 +72,6 @@ static const zend_function_entry class_SplFixedArray_methods[] = {
6772
ZEND_ME(SplFixedArray, offsetSet, arginfo_class_SplFixedArray_offsetSet, ZEND_ACC_PUBLIC)
6873
ZEND_ME(SplFixedArray, offsetUnset, arginfo_class_SplFixedArray_offsetUnset, ZEND_ACC_PUBLIC)
6974
ZEND_ME(SplFixedArray, getIterator, arginfo_class_SplFixedArray_getIterator, ZEND_ACC_PUBLIC)
75+
ZEND_ME(SplFixedArray, __set_state, arginfo_class_SplFixedArray___set_state, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
7076
ZEND_FE_END
7177
};

ext/spl/tests/ArrayObject_overloaded_object_incompatible.phpt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
--TEST--
22
Objects with overloaded get_properties are incompatible with ArrayObject
3+
--XFAIL--
4+
SplFixedArray has migrated to get_properties_for; find new test case
35
--FILE--
46
<?php
57

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
--TEST--
2+
SplFixedArray: __set_state
3+
--FILE--
4+
<?php
5+
6+
$fixed = SplFixedArray::__set_state([1, 2, 3]);
7+
var_export($fixed);
8+
echo "\n---\n";
9+
10+
// dynamic properties
11+
$fixed->undefined = 'undef';
12+
var_export($fixed);
13+
echo "\n---\n";
14+
15+
// inheritance
16+
class Vec
17+
extends SplFixedArray
18+
{
19+
public $type;
20+
}
21+
$vec = new Vec(3);
22+
$vec[0] = 1;
23+
$vec[1] = 2;
24+
$vec[2] = 3;
25+
$vec->type = 'int';
26+
var_export($vec);
27+
echo "\n---\n";
28+
29+
$vec->undefined = 'undef';
30+
var_export($vec);
31+
32+
?>
33+
--EXPECT--
34+
SplFixedArray::__set_state(array(
35+
0 => 1,
36+
1 => 2,
37+
2 => 3,
38+
))
39+
---
40+
SplFixedArray::__set_state(array(
41+
'undefined' => 'undef',
42+
0 => 1,
43+
1 => 2,
44+
2 => 3,
45+
))
46+
---
47+
Vec::__set_state(array(
48+
'type' => 'int',
49+
0 => 1,
50+
1 => 2,
51+
2 => 3,
52+
))
53+
---
54+
Vec::__set_state(array(
55+
'type' => 'int',
56+
'undefined' => 'undef',
57+
0 => 1,
58+
1 => 2,
59+
2 => 3,
60+
))
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
--TEST--
2+
SplFixedArray: __set_state
3+
--FILE--
4+
<?php
5+
6+
try {
7+
$fixed = SplFixedArray::__set_state([1 => 2]);
8+
} catch (ValueError $error) {
9+
var_dump($error->getMessage());
10+
}
11+
12+
try {
13+
$fixed = SplFixedArray::__set_state([0 => 1, 1 => 2, 3 => 4]);
14+
} catch (ValueError $error) {
15+
var_dump($error->getMessage());
16+
}
17+
18+
try {
19+
$fixed = SplFixedArray::__set_state([-1 => 2]);
20+
} catch (ValueError $error) {
21+
var_dump($error->getMessage());
22+
}
23+
24+
echo "-----\n";
25+
26+
try {
27+
$fixed = SplFixedArray::__set_state([0 => 1, 'type' => 'undefined']);
28+
} catch (ValueError $error) {
29+
var_dump($error->getMessage());
30+
}
31+
--EXPECTF--
32+
string(%d) "SplFixedArray::__set_state(): Argument #1 ($state) did not have integer keys that start at 0 and increment sequentially"
33+
string(%d) "SplFixedArray::__set_state(): Argument #1 ($state) did not have integer keys that start at 0 and increment sequentially"
34+
string(%d) "SplFixedArray::__set_state(): Argument #1 ($state) did not have integer keys that start at 0 and increment sequentially"
35+
-----
36+
string(%d) "SplFixedArray::__set_state(): Argument #1 ($state) must have all its string keys come before all integer keys"

0 commit comments

Comments
 (0)