Matthias Andreas Benkard | 7b2a3a1 | 2021-08-16 10:57:25 +0200 | [diff] [blame^] | 1 | <?php |
| 2 | |
| 3 | /* |
| 4 | * This file is part of the Symfony package. |
| 5 | * |
| 6 | * (c) Fabien Potencier <fabien@symfony.com> |
| 7 | * |
| 8 | * For the full copyright and license information, please view the LICENSE |
| 9 | * file that was distributed with this source code. |
| 10 | */ |
| 11 | |
| 12 | namespace Symfony\Component\VarDumper\Cloner; |
| 13 | |
| 14 | /** |
| 15 | * @author Nicolas Grekas <p@tchwork.com> |
| 16 | */ |
| 17 | class VarCloner extends AbstractCloner |
| 18 | { |
| 19 | private static $gid; |
| 20 | private static $arrayCache = []; |
| 21 | |
| 22 | /** |
| 23 | * {@inheritdoc} |
| 24 | */ |
| 25 | protected function doClone($var) |
| 26 | { |
| 27 | $len = 1; // Length of $queue |
| 28 | $pos = 0; // Number of cloned items past the minimum depth |
| 29 | $refsCounter = 0; // Hard references counter |
| 30 | $queue = [[$var]]; // This breadth-first queue is the return value |
| 31 | $hardRefs = []; // Map of original zval ids to stub objects |
| 32 | $objRefs = []; // Map of original object handles to their stub object counterpart |
| 33 | $objects = []; // Keep a ref to objects to ensure their handle cannot be reused while cloning |
| 34 | $resRefs = []; // Map of original resource handles to their stub object counterpart |
| 35 | $values = []; // Map of stub objects' ids to original values |
| 36 | $maxItems = $this->maxItems; |
| 37 | $maxString = $this->maxString; |
| 38 | $minDepth = $this->minDepth; |
| 39 | $currentDepth = 0; // Current tree depth |
| 40 | $currentDepthFinalIndex = 0; // Final $queue index for current tree depth |
| 41 | $minimumDepthReached = 0 === $minDepth; // Becomes true when minimum tree depth has been reached |
| 42 | $cookie = (object) []; // Unique object used to detect hard references |
| 43 | $a = null; // Array cast for nested structures |
| 44 | $stub = null; // Stub capturing the main properties of an original item value |
| 45 | // or null if the original value is used directly |
| 46 | |
| 47 | if (!$gid = self::$gid) { |
| 48 | $gid = self::$gid = md5(random_bytes(6)); // Unique string used to detect the special $GLOBALS variable |
| 49 | } |
| 50 | $arrayStub = new Stub(); |
| 51 | $arrayStub->type = Stub::TYPE_ARRAY; |
| 52 | $fromObjCast = false; |
| 53 | |
| 54 | for ($i = 0; $i < $len; ++$i) { |
| 55 | // Detect when we move on to the next tree depth |
| 56 | if ($i > $currentDepthFinalIndex) { |
| 57 | ++$currentDepth; |
| 58 | $currentDepthFinalIndex = $len - 1; |
| 59 | if ($currentDepth >= $minDepth) { |
| 60 | $minimumDepthReached = true; |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | $refs = $vals = $queue[$i]; |
| 65 | foreach ($vals as $k => $v) { |
| 66 | // $v is the original value or a stub object in case of hard references |
| 67 | |
| 68 | if (\PHP_VERSION_ID >= 70400) { |
| 69 | $zvalIsRef = null !== \ReflectionReference::fromArrayElement($vals, $k); |
| 70 | } else { |
| 71 | $refs[$k] = $cookie; |
| 72 | $zvalIsRef = $vals[$k] === $cookie; |
| 73 | } |
| 74 | |
| 75 | if ($zvalIsRef) { |
| 76 | $vals[$k] = &$stub; // Break hard references to make $queue completely |
| 77 | unset($stub); // independent from the original structure |
| 78 | if ($v instanceof Stub && isset($hardRefs[spl_object_id($v)])) { |
| 79 | $vals[$k] = $refs[$k] = $v; |
| 80 | if ($v->value instanceof Stub && (Stub::TYPE_OBJECT === $v->value->type || Stub::TYPE_RESOURCE === $v->value->type)) { |
| 81 | ++$v->value->refCount; |
| 82 | } |
| 83 | ++$v->refCount; |
| 84 | continue; |
| 85 | } |
| 86 | $refs[$k] = $vals[$k] = new Stub(); |
| 87 | $refs[$k]->value = $v; |
| 88 | $h = spl_object_id($refs[$k]); |
| 89 | $hardRefs[$h] = &$refs[$k]; |
| 90 | $values[$h] = $v; |
| 91 | $vals[$k]->handle = ++$refsCounter; |
| 92 | } |
| 93 | // Create $stub when the original value $v can not be used directly |
| 94 | // If $v is a nested structure, put that structure in array $a |
| 95 | switch (true) { |
| 96 | case null === $v: |
| 97 | case \is_bool($v): |
| 98 | case \is_int($v): |
| 99 | case \is_float($v): |
| 100 | continue 2; |
| 101 | case \is_string($v): |
| 102 | if ('' === $v) { |
| 103 | continue 2; |
| 104 | } |
| 105 | if (!preg_match('//u', $v)) { |
| 106 | $stub = new Stub(); |
| 107 | $stub->type = Stub::TYPE_STRING; |
| 108 | $stub->class = Stub::STRING_BINARY; |
| 109 | if (0 <= $maxString && 0 < $cut = \strlen($v) - $maxString) { |
| 110 | $stub->cut = $cut; |
| 111 | $stub->value = substr($v, 0, -$cut); |
| 112 | } else { |
| 113 | $stub->value = $v; |
| 114 | } |
| 115 | } elseif (0 <= $maxString && isset($v[1 + ($maxString >> 2)]) && 0 < $cut = mb_strlen($v, 'UTF-8') - $maxString) { |
| 116 | $stub = new Stub(); |
| 117 | $stub->type = Stub::TYPE_STRING; |
| 118 | $stub->class = Stub::STRING_UTF8; |
| 119 | $stub->cut = $cut; |
| 120 | $stub->value = mb_substr($v, 0, $maxString, 'UTF-8'); |
| 121 | } else { |
| 122 | continue 2; |
| 123 | } |
| 124 | $a = null; |
| 125 | break; |
| 126 | |
| 127 | case \is_array($v): |
| 128 | if (!$v) { |
| 129 | continue 2; |
| 130 | } |
| 131 | $stub = $arrayStub; |
| 132 | $stub->class = Stub::ARRAY_INDEXED; |
| 133 | |
| 134 | $j = -1; |
| 135 | foreach ($v as $gk => $gv) { |
| 136 | if ($gk !== ++$j) { |
| 137 | $stub->class = Stub::ARRAY_ASSOC; |
| 138 | break; |
| 139 | } |
| 140 | } |
| 141 | $a = $v; |
| 142 | |
| 143 | if (Stub::ARRAY_ASSOC === $stub->class) { |
| 144 | // Copies of $GLOBALS have very strange behavior, |
| 145 | // let's detect them with some black magic |
| 146 | if (\PHP_VERSION_ID < 80100 && ($a[$gid] = true) && isset($v[$gid])) { |
| 147 | unset($v[$gid]); |
| 148 | $a = []; |
| 149 | foreach ($v as $gk => &$gv) { |
| 150 | if ($v === $gv) { |
| 151 | unset($v); |
| 152 | $v = new Stub(); |
| 153 | $v->value = [$v->cut = \count($gv), Stub::TYPE_ARRAY => 0]; |
| 154 | $v->handle = -1; |
| 155 | $gv = &$hardRefs[spl_object_id($v)]; |
| 156 | $gv = $v; |
| 157 | } |
| 158 | |
| 159 | $a[$gk] = &$gv; |
| 160 | } |
| 161 | unset($gv); |
| 162 | } else { |
| 163 | $a = $v; |
| 164 | } |
| 165 | } |
| 166 | break; |
| 167 | |
| 168 | case \is_object($v): |
| 169 | if (empty($objRefs[$h = spl_object_id($v)])) { |
| 170 | $stub = new Stub(); |
| 171 | $stub->type = Stub::TYPE_OBJECT; |
| 172 | $stub->class = \get_class($v); |
| 173 | $stub->value = $v; |
| 174 | $stub->handle = $h; |
| 175 | $a = $this->castObject($stub, 0 < $i); |
| 176 | if ($v !== $stub->value) { |
| 177 | if (Stub::TYPE_OBJECT !== $stub->type || null === $stub->value) { |
| 178 | break; |
| 179 | } |
| 180 | $stub->handle = $h = spl_object_id($stub->value); |
| 181 | } |
| 182 | $stub->value = null; |
| 183 | if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) { |
| 184 | $stub->cut = \count($a); |
| 185 | $a = null; |
| 186 | } |
| 187 | } |
| 188 | if (empty($objRefs[$h])) { |
| 189 | $objRefs[$h] = $stub; |
| 190 | $objects[] = $v; |
| 191 | } else { |
| 192 | $stub = $objRefs[$h]; |
| 193 | ++$stub->refCount; |
| 194 | $a = null; |
| 195 | } |
| 196 | break; |
| 197 | |
| 198 | default: // resource |
| 199 | if (empty($resRefs[$h = (int) $v])) { |
| 200 | $stub = new Stub(); |
| 201 | $stub->type = Stub::TYPE_RESOURCE; |
| 202 | if ('Unknown' === $stub->class = @get_resource_type($v)) { |
| 203 | $stub->class = 'Closed'; |
| 204 | } |
| 205 | $stub->value = $v; |
| 206 | $stub->handle = $h; |
| 207 | $a = $this->castResource($stub, 0 < $i); |
| 208 | $stub->value = null; |
| 209 | if (0 <= $maxItems && $maxItems <= $pos && $minimumDepthReached) { |
| 210 | $stub->cut = \count($a); |
| 211 | $a = null; |
| 212 | } |
| 213 | } |
| 214 | if (empty($resRefs[$h])) { |
| 215 | $resRefs[$h] = $stub; |
| 216 | } else { |
| 217 | $stub = $resRefs[$h]; |
| 218 | ++$stub->refCount; |
| 219 | $a = null; |
| 220 | } |
| 221 | break; |
| 222 | } |
| 223 | |
| 224 | if ($a) { |
| 225 | if (!$minimumDepthReached || 0 > $maxItems) { |
| 226 | $queue[$len] = $a; |
| 227 | $stub->position = $len++; |
| 228 | } elseif ($pos < $maxItems) { |
| 229 | if ($maxItems < $pos += \count($a)) { |
| 230 | $a = \array_slice($a, 0, $maxItems - $pos, true); |
| 231 | if ($stub->cut >= 0) { |
| 232 | $stub->cut += $pos - $maxItems; |
| 233 | } |
| 234 | } |
| 235 | $queue[$len] = $a; |
| 236 | $stub->position = $len++; |
| 237 | } elseif ($stub->cut >= 0) { |
| 238 | $stub->cut += \count($a); |
| 239 | $stub->position = 0; |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | if ($arrayStub === $stub) { |
| 244 | if ($arrayStub->cut) { |
| 245 | $stub = [$arrayStub->cut, $arrayStub->class => $arrayStub->position]; |
| 246 | $arrayStub->cut = 0; |
| 247 | } elseif (isset(self::$arrayCache[$arrayStub->class][$arrayStub->position])) { |
| 248 | $stub = self::$arrayCache[$arrayStub->class][$arrayStub->position]; |
| 249 | } else { |
| 250 | self::$arrayCache[$arrayStub->class][$arrayStub->position] = $stub = [$arrayStub->class => $arrayStub->position]; |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | if ($zvalIsRef) { |
| 255 | $refs[$k]->value = $stub; |
| 256 | } else { |
| 257 | $vals[$k] = $stub; |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | if ($fromObjCast) { |
| 262 | $fromObjCast = false; |
| 263 | $refs = $vals; |
| 264 | $vals = []; |
| 265 | $j = -1; |
| 266 | foreach ($queue[$i] as $k => $v) { |
| 267 | foreach ([$k => true] as $gk => $gv) { |
| 268 | } |
| 269 | if ($gk !== $k) { |
| 270 | $vals = (object) $vals; |
| 271 | $vals->{$k} = $refs[++$j]; |
| 272 | $vals = (array) $vals; |
| 273 | } else { |
| 274 | $vals[$k] = $refs[++$j]; |
| 275 | } |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | $queue[$i] = $vals; |
| 280 | } |
| 281 | |
| 282 | foreach ($values as $h => $v) { |
| 283 | $hardRefs[$h] = $v; |
| 284 | } |
| 285 | |
| 286 | return $queue; |
| 287 | } |
| 288 | } |