Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\VarDumper\Cloner; Chris@0: Chris@0: /** Chris@0: * @author Nicolas Grekas Chris@0: */ Chris@0: class VarCloner extends AbstractCloner Chris@0: { Chris@0: private static $gid; Chris@0: private static $hashMask = 0; Chris@0: private static $hashOffset = 0; Chris@17: private static $arrayCache = []; Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: protected function doClone($var) Chris@0: { Chris@0: $len = 1; // Length of $queue Chris@12: $pos = 0; // Number of cloned items past the minimum depth Chris@0: $refsCounter = 0; // Hard references counter Chris@17: $queue = [[$var]]; // This breadth-first queue is the return value Chris@17: $indexedArrays = []; // Map of queue indexes that hold numerically indexed arrays Chris@17: $hardRefs = []; // Map of original zval hashes to stub objects Chris@17: $objRefs = []; // Map of original object handles to their stub object counterpart Chris@18: $objects = []; // Keep a ref to objects to ensure their handle cannot be reused while cloning Chris@17: $resRefs = []; // Map of original resource handles to their stub object counterpart Chris@17: $values = []; // Map of stub objects' hashes to original values Chris@0: $maxItems = $this->maxItems; Chris@0: $maxString = $this->maxString; Chris@12: $minDepth = $this->minDepth; Chris@12: $currentDepth = 0; // Current tree depth Chris@12: $currentDepthFinalIndex = 0; // Final $queue index for current tree depth Chris@12: $minimumDepthReached = 0 === $minDepth; // Becomes true when minimum tree depth has been reached Chris@17: $cookie = (object) []; // Unique object used to detect hard references Chris@0: $a = null; // Array cast for nested structures Chris@0: $stub = null; // Stub capturing the main properties of an original item value Chris@0: // or null if the original value is used directly Chris@0: Chris@0: if (!self::$hashMask) { Chris@0: self::$gid = uniqid(mt_rand(), true); // Unique string used to detect the special $GLOBALS variable Chris@0: self::initHashMask(); Chris@0: } Chris@0: $gid = self::$gid; Chris@0: $hashMask = self::$hashMask; Chris@0: $hashOffset = self::$hashOffset; Chris@0: $arrayStub = new Stub(); Chris@0: $arrayStub->type = Stub::TYPE_ARRAY; Chris@0: $fromObjCast = false; Chris@0: Chris@0: for ($i = 0; $i < $len; ++$i) { Chris@12: // Detect when we move on to the next tree depth Chris@12: if ($i > $currentDepthFinalIndex) { Chris@12: ++$currentDepth; Chris@12: $currentDepthFinalIndex = $len - 1; Chris@12: if ($currentDepth >= $minDepth) { Chris@12: $minimumDepthReached = true; Chris@12: } Chris@12: } Chris@12: Chris@0: $refs = $vals = $queue[$i]; Chris@0: if (\PHP_VERSION_ID < 70200 && empty($indexedArrays[$i])) { Chris@0: // see https://wiki.php.net/rfc/convert_numeric_keys_in_object_array_casts Chris@0: foreach ($vals as $k => $v) { Chris@0: if (\is_int($k)) { Chris@0: continue; Chris@0: } Chris@17: foreach ([$k => true] as $gk => $gv) { Chris@0: } Chris@0: if ($gk !== $k) { Chris@0: $fromObjCast = true; Chris@0: $refs = $vals = \array_values($queue[$i]); Chris@0: break; Chris@0: } Chris@0: } Chris@0: } Chris@0: foreach ($vals as $k => $v) { Chris@0: // $v is the original value or a stub object in case of hard references Chris@0: $refs[$k] = $cookie; Chris@0: if ($zvalIsRef = $vals[$k] === $cookie) { Chris@0: $vals[$k] = &$stub; // Break hard references to make $queue completely Chris@0: unset($stub); // independent from the original structure Chris@0: if ($v instanceof Stub && isset($hardRefs[\spl_object_hash($v)])) { Chris@0: $vals[$k] = $refs[$k] = $v; Chris@0: if ($v->value instanceof Stub && (Stub::TYPE_OBJECT === $v->value->type || Stub::TYPE_RESOURCE === $v->value->type)) { Chris@0: ++$v->value->refCount; Chris@0: } Chris@0: ++$v->refCount; Chris@0: continue; Chris@0: } Chris@0: $refs[$k] = $vals[$k] = new Stub(); Chris@0: $refs[$k]->value = $v; Chris@0: $h = \spl_object_hash($refs[$k]); Chris@0: $hardRefs[$h] = &$refs[$k]; Chris@0: $values[$h] = $v; Chris@0: $vals[$k]->handle = ++$refsCounter; Chris@0: } Chris@0: // Create $stub when the original value $v can not be used directly Chris@0: // If $v is a nested structure, put that structure in array $a Chris@0: switch (true) { Chris@12: case null === $v: Chris@12: case \is_bool($v): Chris@0: case \is_int($v): Chris@0: case \is_float($v): Chris@0: continue 2; Chris@0: Chris@0: case \is_string($v): Chris@12: if ('' === $v) { Chris@12: continue 2; Chris@12: } Chris@0: if (!\preg_match('//u', $v)) { Chris@0: $stub = new Stub(); Chris@0: $stub->type = Stub::TYPE_STRING; Chris@0: $stub->class = Stub::STRING_BINARY; Chris@0: if (0 <= $maxString && 0 < $cut = \strlen($v) - $maxString) { Chris@0: $stub->cut = $cut; Chris@0: $stub->value = \substr($v, 0, -$cut); Chris@0: } else { Chris@0: $stub->value = $v; Chris@0: } Chris@0: } elseif (0 <= $maxString && isset($v[1 + ($maxString >> 2)]) && 0 < $cut = \mb_strlen($v, 'UTF-8') - $maxString) { Chris@0: $stub = new Stub(); Chris@0: $stub->type = Stub::TYPE_STRING; Chris@0: $stub->class = Stub::STRING_UTF8; Chris@0: $stub->cut = $cut; Chris@0: $stub->value = \mb_substr($v, 0, $maxString, 'UTF-8'); Chris@0: } else { Chris@0: continue 2; Chris@0: } Chris@0: $a = null; Chris@0: break; Chris@0: Chris@0: case \is_array($v): Chris@12: if (!$v) { Chris@12: continue 2; Chris@12: } Chris@0: $stub = $arrayStub; Chris@0: $stub->class = Stub::ARRAY_INDEXED; Chris@0: Chris@0: $j = -1; Chris@0: foreach ($v as $gk => $gv) { Chris@0: if ($gk !== ++$j) { Chris@0: $stub->class = Stub::ARRAY_ASSOC; Chris@0: break; Chris@0: } Chris@0: } Chris@0: $a = $v; Chris@0: Chris@0: if (Stub::ARRAY_ASSOC === $stub->class) { Chris@0: // Copies of $GLOBALS have very strange behavior, Chris@0: // let's detect them with some black magic Chris@0: $a[$gid] = true; Chris@0: Chris@0: // Happens with copies of $GLOBALS Chris@0: if (isset($v[$gid])) { Chris@0: unset($v[$gid]); Chris@17: $a = []; Chris@0: foreach ($v as $gk => &$gv) { Chris@0: $a[$gk] = &$gv; Chris@0: } Chris@0: unset($gv); Chris@0: } else { Chris@0: $a = $v; Chris@0: } Chris@0: } elseif (\PHP_VERSION_ID < 70200) { Chris@0: $indexedArrays[$len] = true; Chris@0: } Chris@0: break; Chris@0: Chris@0: case \is_object($v): Chris@0: case $v instanceof \__PHP_Incomplete_Class: Chris@0: if (empty($objRefs[$h = $hashMask ^ \hexdec(\substr(\spl_object_hash($v), $hashOffset, \PHP_INT_SIZE))])) { Chris@0: $stub = new Stub(); Chris@0: $stub->type = Stub::TYPE_OBJECT; Chris@0: $stub->class = \get_class($v); Chris@0: $stub->value = $v; Chris@0: $stub->handle = $h; Chris@0: $a = $this->castObject($stub, 0 < $i); Chris@0: if ($v !== $stub->value) { Chris@0: if (Stub::TYPE_OBJECT !== $stub->type || null === $stub->value) { Chris@0: break; Chris@0: } Chris@0: $h = $hashMask ^ \hexdec(\substr(\spl_object_hash($stub->value), $hashOffset, \PHP_INT_SIZE)); Chris@0: $stub->handle = $h; Chris@0: } Chris@0: $stub->value = null; Chris@12: if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) { Chris@0: $stub->cut = \count($a); Chris@0: $a = null; Chris@0: } Chris@0: } Chris@0: if (empty($objRefs[$h])) { Chris@0: $objRefs[$h] = $stub; Chris@18: $objects[] = $v; Chris@0: } else { Chris@0: $stub = $objRefs[$h]; Chris@0: ++$stub->refCount; Chris@0: $a = null; Chris@0: } Chris@0: break; Chris@0: Chris@0: default: // resource Chris@0: if (empty($resRefs[$h = (int) $v])) { Chris@0: $stub = new Stub(); Chris@0: $stub->type = Stub::TYPE_RESOURCE; Chris@0: if ('Unknown' === $stub->class = @\get_resource_type($v)) { Chris@0: $stub->class = 'Closed'; Chris@0: } Chris@0: $stub->value = $v; Chris@0: $stub->handle = $h; Chris@0: $a = $this->castResource($stub, 0 < $i); Chris@0: $stub->value = null; Chris@12: if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) { Chris@0: $stub->cut = \count($a); Chris@0: $a = null; Chris@0: } Chris@0: } Chris@0: if (empty($resRefs[$h])) { Chris@0: $resRefs[$h] = $stub; Chris@0: } else { Chris@0: $stub = $resRefs[$h]; Chris@0: ++$stub->refCount; Chris@0: $a = null; Chris@0: } Chris@0: break; Chris@0: } Chris@0: Chris@0: if ($a) { Chris@12: if (!$minimumDepthReached || 0 > $maxItems) { Chris@0: $queue[$len] = $a; Chris@0: $stub->position = $len++; Chris@0: } elseif ($pos < $maxItems) { Chris@0: if ($maxItems < $pos += \count($a)) { Chris@0: $a = \array_slice($a, 0, $maxItems - $pos); Chris@0: if ($stub->cut >= 0) { Chris@0: $stub->cut += $pos - $maxItems; Chris@0: } Chris@0: } Chris@0: $queue[$len] = $a; Chris@0: $stub->position = $len++; Chris@0: } elseif ($stub->cut >= 0) { Chris@0: $stub->cut += \count($a); Chris@0: $stub->position = 0; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($arrayStub === $stub) { Chris@0: if ($arrayStub->cut) { Chris@17: $stub = [$arrayStub->cut, $arrayStub->class => $arrayStub->position]; Chris@0: $arrayStub->cut = 0; Chris@0: } elseif (isset(self::$arrayCache[$arrayStub->class][$arrayStub->position])) { Chris@0: $stub = self::$arrayCache[$arrayStub->class][$arrayStub->position]; Chris@0: } else { Chris@17: self::$arrayCache[$arrayStub->class][$arrayStub->position] = $stub = [$arrayStub->class => $arrayStub->position]; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($zvalIsRef) { Chris@0: $refs[$k]->value = $stub; Chris@0: } else { Chris@0: $vals[$k] = $stub; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($fromObjCast) { Chris@0: $fromObjCast = false; Chris@0: $refs = $vals; Chris@17: $vals = []; Chris@0: $j = -1; Chris@0: foreach ($queue[$i] as $k => $v) { Chris@17: foreach ([$k => true] as $gk => $gv) { Chris@0: } Chris@0: if ($gk !== $k) { Chris@0: $vals = (object) $vals; Chris@0: $vals->{$k} = $refs[++$j]; Chris@0: $vals = (array) $vals; Chris@0: } else { Chris@0: $vals[$k] = $refs[++$j]; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $queue[$i] = $vals; Chris@0: } Chris@0: Chris@0: foreach ($values as $h => $v) { Chris@0: $hardRefs[$h] = $v; Chris@0: } Chris@0: Chris@0: return $queue; Chris@0: } Chris@0: Chris@0: private static function initHashMask() Chris@0: { Chris@17: $obj = (object) []; Chris@0: self::$hashOffset = 16 - PHP_INT_SIZE; Chris@0: self::$hashMask = -1; Chris@0: Chris@17: if (\defined('HHVM_VERSION')) { Chris@0: self::$hashOffset += 16; Chris@0: } else { Chris@0: // check if we are nested in an output buffering handler to prevent a fatal error with ob_start() below Chris@17: $obFuncs = ['ob_clean', 'ob_end_clean', 'ob_flush', 'ob_end_flush', 'ob_get_contents', 'ob_get_flush']; Chris@0: foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { Chris@17: if (isset($frame['function'][0]) && !isset($frame['class']) && 'o' === $frame['function'][0] && \in_array($frame['function'], $obFuncs)) { Chris@0: $frame['line'] = 0; Chris@0: break; Chris@0: } Chris@0: } Chris@0: if (!empty($frame['line'])) { Chris@0: ob_start(); Chris@0: debug_zval_dump($obj); Chris@0: self::$hashMask = (int) substr(ob_get_clean(), 17); Chris@0: } Chris@0: } Chris@0: Chris@0: self::$hashMask ^= hexdec(substr(spl_object_hash($obj), self::$hashOffset, PHP_INT_SIZE)); Chris@0: } Chris@0: }