bug #28414 [VarExporter] fix exporting final serializable classes extending internal ones (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[VarExporter] fix exporting final serializable classes extending internal ones

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

Another edge case, discovered while reading https://github.com/doctrine/instantiator/issues/39

Commits
-------

a5bf9b0445 [VarExporter] fix exporting final serializable classes extending internal ones
This commit is contained in:
Nicolas Grekas 2018-09-09 23:08:05 +02:00
commit 3b931fe6c9
6 changed files with 81 additions and 25 deletions

View File

@ -76,7 +76,7 @@ class Exporter
$sleep = null;
$arrayValue = (array) $value;
if (!isset(Registry::$prototypes[$class])) {
if (!isset(Registry::$reflectors[$class])) {
// Might throw Exception("Serialization of '...' is not allowed")
Registry::getClassReflector($class);
serialize(Registry::$prototypes[$class]);
@ -87,14 +87,16 @@ class Exporter
$reflector = Registry::$reflectors[$class];
$proto = Registry::$prototypes[$class];
if ($value instanceof \ArrayIterator || $value instanceof \ArrayObject) {
if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
// ArrayIterator and ArrayObject need special care because their "flags"
// option changes the behavior of the (array) casting operator.
$proto = Registry::$cloneable[$class] ? clone Registry::$prototypes[$class] : $reflector->newInstanceWithoutConstructor();
$properties = self::getArrayObjectProperties($value, $arrayValue, $proto);
} elseif ($value instanceof \SplObjectStorage) {
// By implementing Serializable, SplObjectStorage breaks internal references,
// let's deal with it on our own.
// populates Registry::$prototypes[$class] with a new instance
Registry::getClassReflector($class, Registry::$instantiableWithoutConstructor[$class], Registry::$cloneable[$class]);
} elseif ($value instanceof \SplObjectStorage && Registry::$cloneable[$class] && null !== $proto) {
// By implementing Serializable, SplObjectStorage breaks
// internal references; let's deal with it on our own.
foreach (clone $value as $v) {
$properties[] = $v;
$properties[] = $value[$v];
@ -284,12 +286,15 @@ class Exporter
continue;
}
if (!Registry::$instantiableWithoutConstructor[$class]) {
if (is_subclass_of($class, 'Throwable')) {
$eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0";
$serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}';
if (is_subclass_of($class, 'Serializable')) {
$serializables[$k] = 'C:'.\strlen($class).':"'.$class.'":0:{}';
} else {
$serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":0:{}';
}
if (is_subclass_of($class, 'Throwable')) {
$eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0";
$serializables[$k] = substr_replace($serializables[$k], '1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}', -4);
}
continue;
}
$code .= $subIndent.(1 !== $k - $j ? $k.' => ' : '');

View File

@ -37,7 +37,9 @@ class Registry
try {
foreach ($serializables as $k => $v) {
$objects[$k] = unserialize($v);
if (false === $objects[$k] = unserialize($v)) {
throw new \Exception(error_get_last()['message'] ?? 'unserialize(): unknown error');
}
}
} finally {
ini_set('unserialize_callback_func', $unserializeCallback);
@ -67,18 +69,16 @@ class Registry
if (self::$instantiableWithoutConstructor[$class] = $instantiableWithoutConstructor || !$reflector->isFinal()) {
$proto = $reflector->newInstanceWithoutConstructor();
} else {
$r = $reflector;
do {
if ($r->isInternal()) {
if (false === $proto = @unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')) {
throw new \Exception(sprintf("Serialization of '%s' is not allowed", $class));
}
break;
}
} while ($r = $r->getParentClass());
if (self::$instantiableWithoutConstructor[$class] = !$r) {
try {
$proto = $reflector->newInstanceWithoutConstructor();
self::$instantiableWithoutConstructor[$class] = true;
} catch (\ReflectionException $e) {
$proto = $reflector->implementsInterface('Serializable') ? 'C:' : 'O:';
if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) {
$proto = null;
} elseif (false === $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}')) {
throw new \Exception(sprintf("Serialization of '%s' is not allowed", $class));
}
}
}

View File

@ -0,0 +1,11 @@
<?php
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
$o = \Symfony\Component\VarExporter\Internal\Registry::unserialize([], [
'C:54:"Symfony\\Component\\VarExporter\\Tests\\FinalArrayIterator":49:{a:2:{i:0;i:123;i:1;s:21:"x:i:0;a:0:{};m:a:0:{}";}}',
]),
null,
[],
$o[0],
[]
);

View File

@ -0,0 +1,11 @@
<?php
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
$o = [
(\Symfony\Component\VarExporter\Internal\Registry::$factories[\Symfony\Component\VarExporter\Tests\FinalStdClass::class] ?? \Symfony\Component\VarExporter\Internal\Registry::f(\Symfony\Component\VarExporter\Tests\FinalStdClass::class))(),
],
null,
[],
$o[0],
[]
);

View File

@ -68,9 +68,9 @@ class VarExporterTest extends TestCase
}
/**
* @dataProvider provideMarshall
* @dataProvider provideExport
*/
public function testMarshall(string $testName, $value, bool $staticValueExpected = false)
public function testExport(string $testName, $value, bool $staticValueExpected = false)
{
$dumpedValue = $this->getDump($value);
$isStaticValue = true;
@ -98,7 +98,7 @@ class VarExporterTest extends TestCase
}
}
public function provideMarshall()
public function provideExport()
{
yield array('multiline-string', array("\0\0\r\nA" => "B\rC\n\n"), true);
@ -175,6 +175,10 @@ class VarExporterTest extends TestCase
$rl->setValue($value, 123);
yield array('final-error', $value);
yield array('final-array-iterator', new FinalArrayIterator());
yield array('final-stdclass', new FinalStdClass());
}
}
@ -278,3 +282,28 @@ final class FinalError extends \Error
}
}
}
final class FinalArrayIterator extends \ArrayIterator
{
public function serialize()
{
return serialize(array(123, parent::serialize()));
}
public function unserialize($data)
{
if ('' === $data) {
throw new \InvalidArgumentException('Serialized data is empty.');
}
list(, $data) = unserialize($data);
parent::unserialize($data);
}
}
final class FinalStdClass extends \stdClass
{
public function __clone()
{
throw new \BadMethodCallException('Should not be called.');
}
}

View File

@ -2,7 +2,7 @@
"name": "symfony/var-exporter",
"type": "library",
"description": "A blend of var_export() + serialize() to turn any serializable data structure to plain PHP code",
"keywords": ["export", "serialize"],
"keywords": ["export", "serialize", "instantiate", "hydrate", "construct", "clone"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [