Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.55% covered (success)
91.55%
195 / 213
84.62% covered (warning)
84.62%
22 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
MethodLinks
91.55% covered (success)
91.55%
195 / 213
84.62% covered (warning)
84.62%
22 / 26
122.98
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getForDim
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 asValueFirstLevel
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 asKeyForForeach
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setAtDim
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addKeysLinks
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 asCollapsed
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 mergeWith
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
9
 asMergedWith
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 withAddedOffset
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 asMaybeMovedAtOffset
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 asMergedForAssignment
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 normalize
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
12
 __clone
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getLinksCollapsing
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getMethodAndParamTuples
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 isEmpty
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
 hasDataForFuncAndParam
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
10
 initializeParamForFunc
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 filterPreservedFlags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllPreservedFlags
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
 asPreservedTaintednessForFuncParam
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
9
 asFilteredForFuncAndParam
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
6.09
 toString
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php declare( strict_types=1 );
2
3namespace SecurityCheckPlugin;
4
5use ast\Node;
6use 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 */
12class 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}