Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.55% |
195 / 213 |
|
84.62% |
22 / 26 |
CRAP | |
0.00% |
0 / 1 |
MethodLinks | |
91.55% |
195 / 213 |
|
84.62% |
22 / 26 |
122.98 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newEmpty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getForDim | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
asValueFirstLevel | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
asKeyForForeach | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setAtDim | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
addKeysLinks | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
asCollapsed | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
mergeWith | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
9 | |||
asMergedWith | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
withAddedOffset | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
asMaybeMovedAtOffset | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
asMergedForAssignment | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
8 | |||
normalize | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
12 | |||
__clone | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
getLinksCollapsing | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
getMethodAndParamTuples | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
7 | |||
isEmpty | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
8 | |||
hasDataForFuncAndParam | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
10 | |||
initializeParamForFunc | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
filterPreservedFlags | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllPreservedFlags | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
5.20 | |||
asPreservedTaintednessForFuncParam | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
9 | |||
asFilteredForFuncAndParam | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
6.09 | |||
toString | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 | |||
__toString | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php declare( strict_types=1 ); |
2 | |
3 | namespace SecurityCheckPlugin; |
4 | |
5 | use ast\Node; |
6 | use Phan\Language\Element\FunctionInterface; |
7 | |
8 | /** |
9 | * Value object that represents method links. |
10 | * @todo We might store links inside Taintedness, but the memory usage might skyrocket |
11 | */ |
12 | class MethodLinks { |
13 | /** @var LinksSet */ |
14 | private $links; |
15 | |
16 | /** @var self[] */ |
17 | private $dimLinks = []; |
18 | |
19 | /** @var self|null */ |
20 | private $unknownDimLinks; |
21 | |
22 | /** @var LinksSet|null */ |
23 | private $keysLinks; |
24 | |
25 | /** |
26 | * @param LinksSet|null $links |
27 | */ |
28 | public function __construct( LinksSet $links = null ) { |
29 | $this->links = $links ?? new LinksSet(); |
30 | } |
31 | |
32 | /** |
33 | * @return self |
34 | */ |
35 | public static function newEmpty(): self { |
36 | return new self( new LinksSet ); |
37 | } |
38 | |
39 | /** |
40 | * @note This returns a clone |
41 | * @param mixed $dim |
42 | * @return self |
43 | */ |
44 | public function getForDim( $dim ): self { |
45 | if ( !is_scalar( $dim ) ) { |
46 | return $this->asValueFirstLevel()->withAddedOffset( $dim ); |
47 | } |
48 | if ( isset( $this->dimLinks[$dim] ) ) { |
49 | $ret = clone $this->dimLinks[$dim]; |
50 | $ret->mergeWith( $this->unknownDimLinks ?? self::newEmpty() ); |
51 | $ret->links->mergeWith( $this->links ); |
52 | return $ret->withAddedOffset( $dim ); |
53 | } |
54 | $ret = $this->unknownDimLinks ? clone $this->unknownDimLinks : self::newEmpty(); |
55 | $ret->links->mergeWith( $this->links ); |
56 | return $ret->withAddedOffset( $dim ); |
57 | } |
58 | |
59 | /** |
60 | * @return self |
61 | */ |
62 | public function asValueFirstLevel(): self { |
63 | $ret = new self( clone $this->links ); |
64 | $ret->mergeWith( $this->unknownDimLinks ?? self::newEmpty() ); |
65 | foreach ( $this->dimLinks as $links ) { |
66 | $ret->mergeWith( $links ); |
67 | } |
68 | return $ret; |
69 | } |
70 | |
71 | /** |
72 | * @return self |
73 | */ |
74 | public function asKeyForForeach(): self { |
75 | if ( $this->keysLinks ) { |
76 | $links = $this->keysLinks->asMergedWith( $this->links ); |
77 | } else { |
78 | $links = $this->links; |
79 | } |
80 | return new self( $links->asAllMovedToKeys() ); |
81 | } |
82 | |
83 | /** |
84 | * @param mixed $dim |
85 | * @param MethodLinks $links |
86 | */ |
87 | public function setAtDim( $dim, self $links ): void { |
88 | if ( is_scalar( $dim ) ) { |
89 | $this->dimLinks[$dim] = $links; |
90 | } else { |
91 | $this->unknownDimLinks ??= self::newEmpty(); |
92 | $this->unknownDimLinks->mergeWith( $links ); |
93 | } |
94 | } |
95 | |
96 | /** |
97 | * @param LinksSet $links |
98 | */ |
99 | public function addKeysLinks( LinksSet $links ): void { |
100 | if ( !$this->keysLinks ) { |
101 | $this->keysLinks = $links; |
102 | } else { |
103 | $this->keysLinks->mergeWith( $links ); |
104 | } |
105 | } |
106 | |
107 | /** |
108 | * @return self |
109 | */ |
110 | public function asCollapsed(): self { |
111 | $ret = new self( $this->links ); |
112 | foreach ( $this->dimLinks as $links ) { |
113 | $ret->mergeWith( $links->asCollapsed() ); |
114 | } |
115 | if ( $this->unknownDimLinks ) { |
116 | $ret->mergeWith( $this->unknownDimLinks->asCollapsed() ); |
117 | } |
118 | return $ret; |
119 | } |
120 | |
121 | /** |
122 | * Merge this object with $other, recursively and without creating a copy. |
123 | * |
124 | * @param self $other |
125 | */ |
126 | public function mergeWith( self $other ): void { |
127 | $this->links->mergeWith( $other->links ); |
128 | foreach ( $other->dimLinks as $key => $links ) { |
129 | if ( isset( $this->dimLinks[$key] ) ) { |
130 | $this->dimLinks[$key]->mergeWith( $links ); |
131 | } else { |
132 | $this->dimLinks[$key] = $links; |
133 | } |
134 | } |
135 | if ( $other->unknownDimLinks && !$this->unknownDimLinks ) { |
136 | $this->unknownDimLinks = $other->unknownDimLinks; |
137 | } elseif ( $other->unknownDimLinks ) { |
138 | $this->unknownDimLinks->mergeWith( $other->unknownDimLinks ); |
139 | } |
140 | if ( $other->keysLinks && !$this->keysLinks ) { |
141 | $this->keysLinks = $other->keysLinks; |
142 | } elseif ( $other->keysLinks ) { |
143 | $this->keysLinks->mergeWith( $other->keysLinks ); |
144 | } |
145 | } |
146 | |
147 | /** |
148 | * Merge this object with $other, recursively, creating a copy. |
149 | * |
150 | * @param self $other |
151 | * @return self |
152 | */ |
153 | public function asMergedWith( self $other ): self { |
154 | $ret = clone $this; |
155 | $ret->mergeWith( $other ); |
156 | return $ret; |
157 | } |
158 | |
159 | /** |
160 | * @param Node|mixed $offset |
161 | * @return self |
162 | */ |
163 | public function withAddedOffset( $offset ): self { |
164 | $ret = clone $this; |
165 | foreach ( $ret->links as $func ) { |
166 | $ret->links[$func]->pushOffsetToAll( $offset ); |
167 | } |
168 | return $ret; |
169 | } |
170 | |
171 | /** |
172 | * Create a new object with $this at the given $offset (if scalar) or as unknown object. |
173 | * |
174 | * @param Node|string|int|bool|float|null $offset |
175 | * @param LinksSet|null $keyLinks |
176 | * @return self Always a copy |
177 | */ |
178 | public function asMaybeMovedAtOffset( $offset, LinksSet $keyLinks = null ): self { |
179 | $ret = new self; |
180 | if ( $offset instanceof Node || $offset === null ) { |
181 | $ret->unknownDimLinks = clone $this; |
182 | } else { |
183 | $ret->dimLinks[$offset] = clone $this; |
184 | } |
185 | $ret->keysLinks = $keyLinks; |
186 | return $ret; |
187 | } |
188 | |
189 | /** |
190 | * @param self $other |
191 | * @param int $depth |
192 | * @return self |
193 | */ |
194 | public function asMergedForAssignment( self $other, int $depth ): self { |
195 | if ( $depth === 0 ) { |
196 | return $other; |
197 | } |
198 | $ret = clone $this; |
199 | $ret->links->mergeWith( $other->links ); |
200 | if ( !$ret->keysLinks ) { |
201 | $ret->keysLinks = $other->keysLinks; |
202 | } elseif ( $other->keysLinks ) { |
203 | $ret->keysLinks->mergeWith( $other->keysLinks ); |
204 | } |
205 | if ( !$ret->unknownDimLinks ) { |
206 | $ret->unknownDimLinks = $other->unknownDimLinks; |
207 | } elseif ( $other->unknownDimLinks ) { |
208 | $ret->unknownDimLinks->mergeWith( $other->unknownDimLinks ); |
209 | } |
210 | foreach ( $other->dimLinks as $k => $v ) { |
211 | $ret->dimLinks[$k] = isset( $ret->dimLinks[$k] ) |
212 | ? $ret->dimLinks[$k]->asMergedForAssignment( $v, $depth - 1 ) |
213 | : $v; |
214 | } |
215 | $ret->normalize(); |
216 | return $ret; |
217 | } |
218 | |
219 | /** |
220 | * Remove offset links which are already present in the "main" links. This is done for performance |
221 | * (see test backpropoffsets-blowup). |
222 | * |
223 | * @todo Improve (e.g. recurse) |
224 | * @todo Might happen sometime earlier |
225 | */ |
226 | private function normalize(): void { |
227 | if ( !count( $this->links ) ) { |
228 | return; |
229 | } |
230 | foreach ( $this->dimLinks as $k => $links ) { |
231 | foreach ( $links->links as $func ) { |
232 | if ( $this->links->contains( $func ) ) { |
233 | $dimParams = array_keys( $links->links[$func]->getParams() ); |
234 | $thisParams = array_keys( $this->links[$func]->getParams() ); |
235 | $keepParams = array_diff( $dimParams, $thisParams ); |
236 | if ( !$keepParams ) { |
237 | unset( $links->links[$func] ); |
238 | } else { |
239 | $links->links[$func]->keepOnlyParams( $keepParams ); |
240 | } |
241 | } |
242 | } |
243 | if ( $links->isEmpty() ) { |
244 | unset( $this->dimLinks[$k] ); |
245 | } |
246 | } |
247 | if ( $this->unknownDimLinks ) { |
248 | foreach ( $this->unknownDimLinks->links as $func ) { |
249 | if ( $this->links->contains( $func ) ) { |
250 | $dimParams = array_keys( $this->unknownDimLinks->links[$func]->getParams() ); |
251 | $thisParams = array_keys( $this->links[$func]->getParams() ); |
252 | $keepParams = array_diff( $dimParams, $thisParams ); |
253 | if ( !$keepParams ) { |
254 | unset( $this->unknownDimLinks->links[$func] ); |
255 | } else { |
256 | $this->unknownDimLinks->links[$func]->keepOnlyParams( $keepParams ); |
257 | } |
258 | } |
259 | } |
260 | if ( $this->unknownDimLinks->isEmpty() ) { |
261 | $this->unknownDimLinks = null; |
262 | } |
263 | } |
264 | } |
265 | |
266 | /** |
267 | * Make sure to clone member variables, too. |
268 | */ |
269 | public function __clone() { |
270 | $this->links = clone $this->links; |
271 | foreach ( $this->dimLinks as $k => $links ) { |
272 | $this->dimLinks[$k] = clone $links; |
273 | } |
274 | if ( $this->unknownDimLinks ) { |
275 | $this->unknownDimLinks = clone $this->unknownDimLinks; |
276 | } |
277 | if ( $this->keysLinks ) { |
278 | $this->keysLinks = clone $this->keysLinks; |
279 | } |
280 | } |
281 | |
282 | /** |
283 | * Returns all the links stored in this object as a single LinkSet object, destroying the shape. This should only |
284 | * be used when the shape is not relevant. |
285 | * |
286 | * @return LinksSet |
287 | */ |
288 | public function getLinksCollapsing(): LinksSet { |
289 | $ret = clone $this->links; |
290 | foreach ( $this->dimLinks as $link ) { |
291 | $ret->mergeWith( $link->getLinksCollapsing() ); |
292 | } |
293 | if ( $this->unknownDimLinks ) { |
294 | $ret->mergeWith( $this->unknownDimLinks->getLinksCollapsing() ); |
295 | } |
296 | if ( $this->keysLinks ) { |
297 | $ret->mergeWith( $this->keysLinks ); |
298 | } |
299 | return $ret; |
300 | } |
301 | |
302 | /** |
303 | * @return array[] |
304 | * @phan-return array<array{0:FunctionInterface,1:int}> |
305 | */ |
306 | public function getMethodAndParamTuples(): array { |
307 | $ret = []; |
308 | foreach ( $this->links as $func ) { |
309 | $info = $this->links[$func]; |
310 | foreach ( $info->getParams() as $i => $_ ) { |
311 | $ret[] = [ $func, $i ]; |
312 | } |
313 | } |
314 | foreach ( $this->dimLinks as $link ) { |
315 | $ret = array_merge( $ret, $link->getMethodAndParamTuples() ); |
316 | } |
317 | if ( $this->unknownDimLinks ) { |
318 | $ret = array_merge( $ret, $this->unknownDimLinks->getMethodAndParamTuples() ); |
319 | } |
320 | foreach ( $this->keysLinks ?? [] as $func ) { |
321 | $info = $this->keysLinks[$func]; |
322 | foreach ( $info->getParams() as $i => $_ ) { |
323 | $ret[] = [ $func, $i ]; |
324 | } |
325 | } |
326 | return array_unique( $ret, SORT_REGULAR ); |
327 | } |
328 | |
329 | /** |
330 | * @return bool |
331 | */ |
332 | public function isEmpty(): bool { |
333 | if ( count( $this->links ) ) { |
334 | return false; |
335 | } |
336 | foreach ( $this->dimLinks as $links ) { |
337 | if ( !$links->isEmpty() ) { |
338 | return false; |
339 | } |
340 | } |
341 | if ( $this->unknownDimLinks && !$this->unknownDimLinks->isEmpty() ) { |
342 | return false; |
343 | } |
344 | if ( $this->keysLinks && count( $this->keysLinks ) ) { |
345 | return false; |
346 | } |
347 | return true; |
348 | } |
349 | |
350 | /** |
351 | * @param FunctionInterface $func |
352 | * @param int $i |
353 | * @return bool |
354 | */ |
355 | public function hasDataForFuncAndParam( FunctionInterface $func, int $i ): bool { |
356 | if ( $this->links->contains( $func ) && $this->links[$func]->hasParam( $i ) ) { |
357 | return true; |
358 | } |
359 | foreach ( $this->dimLinks as $dimLinks ) { |
360 | if ( $dimLinks->hasDataForFuncAndParam( $func, $i ) ) { |
361 | return true; |
362 | } |
363 | } |
364 | if ( $this->unknownDimLinks && $this->unknownDimLinks->hasDataForFuncAndParam( $func, $i ) ) { |
365 | return true; |
366 | } |
367 | if ( $this->keysLinks && $this->keysLinks->contains( $func ) && $this->keysLinks[$func]->hasParam( $i ) ) { |
368 | return true; |
369 | } |
370 | return false; |
371 | } |
372 | |
373 | /** |
374 | * @param FunctionInterface $func |
375 | * @param int $i |
376 | */ |
377 | public function initializeParamForFunc( FunctionInterface $func, int $i ): void { |
378 | if ( $this->links->contains( $func ) ) { |
379 | $this->links[$func]->addParam( $i ); |
380 | } else { |
381 | $this->links[$func] = SingleMethodLinks::newWithParam( $i ); |
382 | } |
383 | } |
384 | |
385 | /** |
386 | * Given some taint flags, return their intersection with the flags that can be preserved by this object |
387 | * @param int $taint |
388 | * @return int |
389 | */ |
390 | public function filterPreservedFlags( int $taint ): int { |
391 | return $taint & $this->getAllPreservedFlags(); |
392 | } |
393 | |
394 | /** |
395 | * @return int |
396 | */ |
397 | private function getAllPreservedFlags(): int { |
398 | $ret = SecurityCheckPlugin::NO_TAINT; |
399 | foreach ( $this->links as $func ) { |
400 | $ret |= $this->links[$func]->getAllPreservedFlags(); |
401 | } |
402 | foreach ( $this->dimLinks as $dimLinks ) { |
403 | $ret |= $dimLinks->getAllPreservedFlags(); |
404 | } |
405 | if ( $this->unknownDimLinks ) { |
406 | $ret |= $this->unknownDimLinks->getAllPreservedFlags(); |
407 | } |
408 | foreach ( $this->keysLinks ?? [] as $func ) { |
409 | $ret |= $this->keysLinks[$func]->getAllPreservedFlags(); |
410 | } |
411 | return $ret; |
412 | } |
413 | |
414 | /** |
415 | * @param FunctionInterface $func |
416 | * @param int $param |
417 | * @return PreservedTaintedness |
418 | */ |
419 | public function asPreservedTaintednessForFuncParam( FunctionInterface $func, int $param ): PreservedTaintedness { |
420 | $ret = null; |
421 | if ( $this->links->contains( $func ) ) { |
422 | $ownInfo = $this->links[$func]; |
423 | if ( $ownInfo->hasParam( $param ) ) { |
424 | $ret = new PreservedTaintedness( $ownInfo->getParamOffsets( $param ) ); |
425 | } |
426 | } |
427 | if ( !$ret ) { |
428 | $ret = new PreservedTaintedness( ParamLinksOffsets::newEmpty() ); |
429 | } |
430 | foreach ( $this->dimLinks as $dim => $dimLinks ) { |
431 | $ret->setOffsetTaintedness( $dim, $dimLinks->asPreservedTaintednessForFuncParam( $func, $param ) ); |
432 | } |
433 | if ( $this->unknownDimLinks ) { |
434 | $ret->setOffsetTaintedness( |
435 | null, |
436 | $this->unknownDimLinks->asPreservedTaintednessForFuncParam( $func, $param ) |
437 | ); |
438 | } |
439 | if ( $this->keysLinks && $this->keysLinks->contains( $func ) ) { |
440 | $keyInfo = $this->keysLinks[$func]; |
441 | if ( $keyInfo->hasParam( $param ) ) { |
442 | $ret->setKeysOffsets( $keyInfo->getParamOffsets( $param ) ); |
443 | } |
444 | } |
445 | return $ret; |
446 | } |
447 | |
448 | /** |
449 | * @param FunctionInterface $func |
450 | * @param int $param |
451 | * @return self |
452 | */ |
453 | public function asFilteredForFuncAndParam( FunctionInterface $func, int $param ): self { |
454 | $retLinks = new LinksSet(); |
455 | if ( $this->links->contains( $func ) ) { |
456 | $retLinks->attach( $func, $this->links[$func] ); |
457 | } |
458 | $ret = new self( $retLinks ); |
459 | foreach ( $this->dimLinks as $dim => $dimLinks ) { |
460 | $ret->setAtDim( $dim, $dimLinks->asFilteredForFuncAndParam( $func, $param ) ); |
461 | } |
462 | if ( $this->unknownDimLinks ) { |
463 | $ret->setAtDim( |
464 | null, |
465 | $this->unknownDimLinks->asFilteredForFuncAndParam( $func, $param ) |
466 | ); |
467 | } |
468 | if ( $this->keysLinks && $this->keysLinks->contains( $func ) ) { |
469 | $ret->keysLinks = new LinksSet(); |
470 | $ret->keysLinks->attach( $func, $this->keysLinks[$func] ); |
471 | } |
472 | return $ret; |
473 | } |
474 | |
475 | /** |
476 | * @param string $indent |
477 | * @return string |
478 | */ |
479 | public function toString( string $indent = '' ): string { |
480 | $elementsIndent = $indent . "\t"; |
481 | $ret = "{\n$elementsIndent" . 'OWN: ' . $this->links->__toString() . ','; |
482 | if ( $this->keysLinks ) { |
483 | $ret .= "\n{$elementsIndent}KEYS: " . $this->keysLinks->__toString() . ','; |
484 | } |
485 | if ( $this->dimLinks || $this->unknownDimLinks ) { |
486 | $ret .= "\n{$elementsIndent}CHILDREN: {"; |
487 | $childrenIndent = $elementsIndent . "\t"; |
488 | foreach ( $this->dimLinks as $key => $links ) { |
489 | $ret .= "\n$childrenIndent$key: " . $links->toString( $childrenIndent ) . ','; |
490 | } |
491 | if ( $this->unknownDimLinks ) { |
492 | $ret .= "\n$childrenIndent(UNKNOWN): " . $this->unknownDimLinks->toString( $childrenIndent ); |
493 | } |
494 | $ret .= "\n$elementsIndent}"; |
495 | } |
496 | return $ret . "\n$indent}"; |
497 | } |
498 | |
499 | /** |
500 | * @return string |
501 | */ |
502 | public function __toString(): string { |
503 | return $this->toString(); |
504 | } |
505 | } |