Skip to content

Commit e6b13a9

Browse files
[DeepClone] Match unserialize() permissiveness on scoped-mode prop names
Mirrors symfony/php-ext-deepclone#12 follow-up commit. In scoped mode, the pre-v0.4.0 hot-path validation rejected non-string keys, NUL-containing names, and mangled-shape keys with a ValueError. That was stricter than what unserialize() does on the same shapes in an O:… payload — unserialize() coerces integer keys, stores NUL-in-middle names as raw dynamic properties, and lets the engine's native Error surface for NUL-prefix names. - Removed the per-prop `!\is_string($name) || str_contains($name, "\0")` check from all eight hydrator closures (stdClass + 3 variants × 2 ref-modes). - Dropped the now-unused `DeepClone::throwInvalidPropName()` helper. - Updated and renamed the corresponding tests. Besides semantic alignment with unserialize(), this saves ~18 ns per property in the hot path — on a 5-prop stdClass hydration that's ~130 ns (~18% of the call), matching what the ext-side commit also avoids.
1 parent 771a2e3 commit e6b13a9

1 file changed

Lines changed: 10 additions & 43 deletions

File tree

DeepClone.php

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,10 @@ public static function deepclone_hydrate(object|string $object_or_class, array $
406406
$hasRefs = false;
407407
if ($flags & \DEEPCLONE_HYDRATE_PRESERVE_REFS) {
408408
foreach ($properties as $k => $_) {
409-
if (\ReflectionReference::fromArrayElement($properties, $k)) { $hasRefs = true; break; }
409+
if (\ReflectionReference::fromArrayElement($properties, $k)) {
410+
$hasRefs = true;
411+
break;
412+
}
410413
}
411414
}
412415
(self::$simpleHydrators[$class] ??= self::getSimpleHydrator($class))($properties, $object, $hasRefs);
@@ -444,7 +447,7 @@ public static function deepclone_hydrate(object|string $object_or_class, array $
444447
$scopeName = substr($name, 1, $sep - 1);
445448
$realName = substr($name, $sep + 1);
446449

447-
if (\str_contains($realName, "\0")) {
450+
if (str_contains($realName, "\0")) {
448451
throw new \ValueError('deepclone_hydrate(): Argument #2 ($vars) in MANGLED_VARS mode contains an invalid mangled key');
449452
}
450453

@@ -491,7 +494,10 @@ public static function deepclone_hydrate(object|string $object_or_class, array $
491494
$hasRefs = false;
492495
if ($flags & \DEEPCLONE_HYDRATE_PRESERVE_REFS) {
493496
foreach ($properties as $k => $_) {
494-
if (\ReflectionReference::fromArrayElement($properties, $k)) { $hasRefs = true; break; }
497+
if (\ReflectionReference::fromArrayElement($properties, $k)) {
498+
$hasRefs = true;
499+
break;
500+
}
495501
}
496502
}
497503

@@ -510,20 +516,6 @@ public static function deepclone_hydrate(object|string $object_or_class, array $
510516
return $object;
511517
}
512518

513-
/**
514-
* @internal
515-
*
516-
* Cold path invoked from the generated hydrator closures to report a
517-
* property name that is not a string or contains a NUL byte.
518-
*/
519-
public static function throwInvalidPropName(mixed $name, string $scope): never
520-
{
521-
if (!\is_string($name)) {
522-
throw new \ValueError(\sprintf('deepclone_hydrate(): Argument #2 ($vars) scope "%s" must have only string keys', $scope));
523-
}
524-
throw new \ValueError(\sprintf('deepclone_hydrate(): Argument #2 ($vars) scope "%s" contains an invalid property name; use bare property names in scoped mode, or pass DEEPCLONE_HYDRATE_MANGLED_VARS in $flags', $scope));
525-
}
526-
527519
/**
528520
* Returns a type name matching zend_zval_value_name() output for
529521
* error messages compatible with the C extension.
@@ -1357,17 +1349,11 @@ private static function getSimpleHydrator(string $class, int $flags = 0): \Closu
13571349
$baseHydrator = self::$simpleHydrators['stdClass'] ??= static function ($properties, $object, $hasRefs) {
13581350
if ($hasRefs) {
13591351
foreach ($properties as $name => &$value) {
1360-
if (!\is_string($name) || \str_contains($name, "\0")) {
1361-
DeepClone::throwInvalidPropName($name, 'stdClass');
1362-
}
13631352
$object->$name = $value;
13641353
$object->$name = &$value;
13651354
}
13661355
} else {
13671356
foreach ($properties as $name => $value) {
1368-
if (!\is_string($name) || \str_contains($name, "\0")) {
1369-
DeepClone::throwInvalidPropName($name, 'stdClass');
1370-
}
13711357
$object->$name = $value;
13721358
}
13731359
}
@@ -1447,8 +1433,7 @@ private static function getSimpleHydrator(string $class, int $flags = 0): \Closu
14471433
$notByRef[$propertyReflector->name] = static function ($object, $value) use ($propertyReflector) {
14481434
// Idempotent rehydrate: skip same-value writes that the engine would reject.
14491435
if ($propertyReflector->isInitialized($object)
1450-
&& $propertyReflector->getValue($object) === $value)
1451-
{
1436+
&& $propertyReflector->getValue($object) === $value) {
14521437
return;
14531438
}
14541439
$propertyReflector->setValue($object, $value);
@@ -1476,9 +1461,6 @@ private static function getSimpleHydrator(string $class, int $flags = 0): \Closu
14761461
return \Closure::bind(static function ($properties, $object, $hasRefs) use ($notByRef, $scope): void {
14771462
if ($hasRefs) {
14781463
foreach ($properties as $name => &$value) {
1479-
if (!\is_string($name) || \str_contains($name, "\0")) {
1480-
DeepClone::throwInvalidPropName($name, $scope);
1481-
}
14821464
if (!$noRef = $notByRef[$name] ?? false) {
14831465
$object->$name = $value;
14841466
$object->$name = &$value;
@@ -1490,9 +1472,6 @@ private static function getSimpleHydrator(string $class, int $flags = 0): \Closu
14901472
}
14911473
} else {
14921474
foreach ($properties as $name => $value) {
1493-
if (!\is_string($name) || \str_contains($name, "\0")) {
1494-
DeepClone::throwInvalidPropName($name, $scope);
1495-
}
14961475
if (!$noRef = $notByRef[$name] ?? false) {
14971476
$object->$name = $value;
14981477
} elseif (true !== $noRef) {
@@ -1508,9 +1487,6 @@ private static function getSimpleHydrator(string $class, int $flags = 0): \Closu
15081487
return \Closure::bind(static function ($properties, $object, $hasRefs) use ($notByRef, $unsetOnNull, $scope): void {
15091488
if ($hasRefs) {
15101489
foreach ($properties as $name => &$value) {
1511-
if (!\is_string($name) || \str_contains($name, "\0")) {
1512-
DeepClone::throwInvalidPropName($name, $scope);
1513-
}
15141490
if (null === $value && isset($unsetOnNull[$name])) {
15151491
unset($object->$name);
15161492
continue;
@@ -1526,9 +1502,6 @@ private static function getSimpleHydrator(string $class, int $flags = 0): \Closu
15261502
}
15271503
} else {
15281504
foreach ($properties as $name => $value) {
1529-
if (!\is_string($name) || \str_contains($name, "\0")) {
1530-
DeepClone::throwInvalidPropName($name, $scope);
1531-
}
15321505
if (null === $value && isset($unsetOnNull[$name])) {
15331506
unset($object->$name);
15341507
continue;
@@ -1548,9 +1521,6 @@ private static function getSimpleHydrator(string $class, int $flags = 0): \Closu
15481521
return \Closure::bind(static function ($properties, $object, $hasRefs) use ($notByRef, $unsetOnNull, $backedEnum, $scope): void {
15491522
if ($hasRefs) {
15501523
foreach ($properties as $name => &$value) {
1551-
if (!\is_string($name) || \str_contains($name, "\0")) {
1552-
DeepClone::throwInvalidPropName($name, $scope);
1553-
}
15541524
if (null === $value && isset($unsetOnNull[$name])) {
15551525
unset($object->$name);
15561526
continue;
@@ -1569,9 +1539,6 @@ private static function getSimpleHydrator(string $class, int $flags = 0): \Closu
15691539
}
15701540
} else {
15711541
foreach ($properties as $name => $value) {
1572-
if (!\is_string($name) || \str_contains($name, "\0")) {
1573-
DeepClone::throwInvalidPropName($name, $scope);
1574-
}
15751542
if (null === $value && isset($unsetOnNull[$name])) {
15761543
unset($object->$name);
15771544
continue;

0 commit comments

Comments
 (0)