Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.94% covered (warning)
87.94%
248 / 282
84.44% covered (warning)
84.44%
38 / 45
CRAP
0.00% covered (danger)
0.00%
0 / 1
Taintedness
87.94% covered (warning)
87.94%
248 / 282
84.44% covered (warning)
84.44%
38 / 45
158.17
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
 newSafe
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newUnknown
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newTainted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 asCollapsed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 asKnownKeysMadeUnknown
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getAllKeysTaint
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 add
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 with
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 remove
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 without
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
7
 keepOnly
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 withOnly
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 intersectForSink
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
7
 removeKnownKeysFrom
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 mergeWith
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 asMergedWith
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setOffsetTaintedness
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addKeysTaintedness
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 asMergedForAssignment
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 arrayPlus
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 asArrayPlusWith
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getTaintednessForOffsetOrWhole
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 asMaybeMovedAtOffset
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 asValueFirstLevel
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 withoutKey
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 withoutKeys
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 asKeyForForeach
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 arrayReplace
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 arrayMerge
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 isSafe
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
7
 asExecToYesTaint
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 asYesToExecTaint
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 asMovedAtRelevantOffsetsForBackprop
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
6.05
 flagsAsExecToYesTaint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 flagsAsYesToExecTaint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 asPreservedTaintedness
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 decomposeForLinks
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 toString
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 toShortString
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 __clone
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 __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;
6
7/**
8 * Value object used to store taintedness. This should always be used to manipulate taintedness values,
9 * instead of directly using taint constants directly (except for comparisons etc.).
10 *
11 * Note that this class should be used as copy-on-write (like phan's UnionType), so in-place
12 * manipulation should never be done on phan objects.
13 */
14class Taintedness {
15    /** @var int Combination of the class constants */
16    private $flags;
17
18    /** @var self[] Taintedness for each possible array element */
19    private $dimTaint = [];
20
21    /** @var int Taintedness of the array keys */
22    private $keysTaint = SecurityCheckPlugin::NO_TAINT;
23
24    /**
25     * @var self|null Taintedness for array elements that we couldn't attribute to any key
26     */
27    private $unknownDimsTaint;
28
29    /**
30     * @param int $val One of the class constants
31     */
32    public function __construct( int $val ) {
33        $this->flags = $val;
34    }
35
36    // Common creation shortcuts
37
38    /**
39     * @return self
40     */
41    public static function newSafe(): self {
42        return new self( SecurityCheckPlugin::NO_TAINT );
43    }
44
45    /**
46     * @return self
47     */
48    public static function newUnknown(): self {
49        return new self( SecurityCheckPlugin::UNKNOWN_TAINT );
50    }
51
52    /**
53     * @return self
54     */
55    public static function newTainted(): self {
56        return new self( SecurityCheckPlugin::YES_TAINT );
57    }
58
59    /**
60     * @param Taintedness[] $values
61     * @return self
62     */
63    public static function newFromArray( array $values ): self {
64        $ret = self::newSafe();
65        foreach ( $values as $key => $value ) {
66            assert( $value instanceof self );
67            $ret->setOffsetTaintedness( $key, $value );
68        }
69        return $ret;
70    }
71
72    /**
73     * Get a numeric representation of the taint stored in this object. This includes own taint,
74     * array keys and whatnot.
75     * @note This should almost NEVER be used outside of this class! Use accessors as much as possible!
76     *
77     * @return int
78     */
79    public function get(): int {
80        $ret = $this->flags | $this->getAllKeysTaint() | $this->keysTaint;
81        return $this->unknownDimsTaint ? ( $ret | $this->unknownDimsTaint->get() ) : $ret;
82    }
83
84    /**
85     * Get a flattened version of this object, with any taint from keys etc. collapsed into flags
86     * @return $this
87     */
88    public function asCollapsed(): self {
89        return new self( $this->get() );
90    }
91
92    /**
93     * Returns a copy of this object where the taintedness of every known key has been reassigned
94     * to unknown keys.
95     * @return self
96     */
97    public function asKnownKeysMadeUnknown(): self {
98        $ret = new self( $this->flags );
99        $ret->keysTaint = $this->keysTaint;
100        $ret->unknownDimsTaint = $this->unknownDimsTaint;
101        if ( $this->dimTaint ) {
102            $ret->unknownDimsTaint ??= self::newSafe();
103            foreach ( $this->dimTaint as $keyTaint ) {
104                $ret->unknownDimsTaint->mergeWith( $keyTaint );
105            }
106        }
107        return $ret;
108    }
109
110    /**
111     * Recursively extract the taintedness from each key.
112     *
113     * @return int
114     */
115    private function getAllKeysTaint(): int {
116        $ret = SecurityCheckPlugin::NO_TAINT;
117        foreach ( $this->dimTaint as $val ) {
118            $ret |= $val->get();
119        }
120        return $ret;
121    }
122
123    // Value manipulation
124
125    /**
126     * Add the given taint to this object's flags, *without* creating a clone
127     * @see Taintedness::with() if you need a clone
128     * @see Taintedness::mergeWith() if you want to preserve the whole shape
129     *
130     * @param int $taint
131     */
132    public function add( int $taint ): void {
133        // TODO: Should this clear UNKNOWN_TAINT if its present only in one of the args?
134        $this->flags |= $taint;
135    }
136
137    /**
138     * Returns a copy of this object, with the bits in $other added to flags.
139     * @see Taintedness::add() for the in-place version
140     * @see Taintedness::asMergedWith() if you want to preserve the whole shape
141     *
142     * @param int $other
143     * @return $this
144     */
145    public function with( int $other ): self {
146        $ret = clone $this;
147        $ret->add( $other );
148        return $ret;
149    }
150
151    /**
152     * Recursively remove the given taint from this object, *without* creating a clone
153     * @see Taintedness::without() if you need a clone
154     *
155     * @param int $other
156     */
157    public function remove( int $other ): void {
158        $this->keepOnly( ~$other );
159    }
160
161    /**
162     * Returns a copy of this object, with the bits in $other removed recursively.
163     * @see Taintedness::remove() for the in-place version
164     *
165     * @param int $other
166     * @return $this
167     */
168    public function without( int $other ): self {
169        $ret = clone $this;
170        $ret->remove( $other );
171        return $ret;
172    }
173
174    /**
175     * Check whether this object has the given flag, recursively.
176     * @note If $taint has more than one flag, this will check for at least one, not all.
177     *
178     * @param int $taint
179     * @return bool
180     */
181    public function has( int $taint ): bool {
182        // Avoid using get() for performance
183        if ( ( $this->flags & $taint ) !== SecurityCheckPlugin::NO_TAINT ) {
184            return true;
185        }
186        if ( ( $this->keysTaint & $taint ) !== SecurityCheckPlugin::NO_TAINT ) {
187            return true;
188        }
189        if ( $this->unknownDimsTaint && $this->unknownDimsTaint->has( $taint ) ) {
190            return true;
191        }
192        foreach ( $this->dimTaint as $val ) {
193            if ( $val->has( $taint ) ) {
194                return true;
195            }
196        }
197        return false;
198    }
199
200    /**
201     * Keep only the taint in $taint, recursively, preserving the shape and without creating a copy.
202     * @see Taintedness::withOnly if you need a clone
203     *
204     * @param int $taint
205     */
206    public function keepOnly( int $taint ): void {
207        $this->flags &= $taint;
208        if ( $this->unknownDimsTaint ) {
209            $this->unknownDimsTaint->keepOnly( $taint );
210        }
211        $this->keysTaint &= $taint;
212        foreach ( $this->dimTaint as $val ) {
213            $val->keepOnly( $taint );
214        }
215    }
216
217    /**
218     * Returns a copy of this object, with only the taint in $taint kept (recursively, preserving the shape)
219     * @see Taintedness::keepOnly() for the in-place version
220     *
221     * @param int $other
222     * @return $this
223     */
224    public function withOnly( int $other ): self {
225        $ret = clone $this;
226        $ret->keepOnly( $other );
227        return $ret;
228    }
229
230    /**
231     * Intersect the taintedness of a value against that of a sink, to later determine whether the
232     * expression is safe. In case of function calls, $sink is the param taint and $value is the arg taint.
233     *
234     * @note The order of the arguments is important! This method preserves the shape of $sink, not $value.
235     *
236     * @note The order of the arguments is important! This method preserves the shape of $sink, not $value.
237     *
238     * @param Taintedness $sink
239     * @param Taintedness $value
240     * @return self
241     */
242    public static function intersectForSink( self $sink, self $value ): self {
243        $intersect = new self( SecurityCheckPlugin::NO_TAINT );
244        // If the sink has non-zero flags, intersect it with the whole other side. This particularly preserves
245        // the shape of $sink, discarding anything from $value if the sink has a NO_TAINT in that position.
246        if ( $sink->flags & SecurityCheckPlugin::SQL_NUMKEY_EXEC_TAINT ) {
247            // Special case: NUMKEY is only for the outer array
248            $rightFlags = $value->flags | $value->keysTaint;
249            if ( $value->keysTaint & SecurityCheckPlugin::SQL_TAINT ) {
250                // FIXME HACK: If keys are tainted, add numkey. This assumes that numkey is really only used for
251                // Database methods, where keys are never escaped.
252                $rightFlags |= SecurityCheckPlugin::SQL_NUMKEY_TAINT;
253            }
254            $rightFlags |= ( $value->getAllKeysTaint() & ~SecurityCheckPlugin::SQL_NUMKEY_TAINT );
255            if ( $value->unknownDimsTaint ) {
256                $rightFlags |= $value->unknownDimsTaint->get() & ~SecurityCheckPlugin::SQL_NUMKEY_TAINT;
257            }
258            $intersect->flags = $sink->flags & ( ( $rightFlags & SecurityCheckPlugin::ALL_TAINT ) << 1 );
259        } elseif ( $sink->flags ) {
260            $intersect->flags = $sink->flags & ( ( $value->get() & SecurityCheckPlugin::ALL_TAINT ) << 1 );
261        }
262        if ( $sink->unknownDimsTaint ) {
263            $intersect->unknownDimsTaint = self::intersectForSink(
264                $sink->unknownDimsTaint,
265                $value->asValueFirstLevel()
266            );
267        }
268        $valueKeysAsExec = ( ( $value->keysTaint | $value->flags ) & SecurityCheckPlugin::ALL_TAINT ) << 1;
269        $intersect->keysTaint = $sink->keysTaint & $valueKeysAsExec;
270        foreach ( $sink->dimTaint as $key => $dTaint ) {
271            $intersect->dimTaint[$key] = self::intersectForSink(
272                $dTaint,
273                $value->getTaintednessForOffsetOrWhole( $key )
274            );
275        }
276        return $intersect;
277    }
278
279    /**
280     * Removes offset data from $this for all known offsets of $other, in place.
281     *
282     * @param Taintedness $other
283     * @return void
284     */
285    public function removeKnownKeysFrom( self $other ): void {
286        foreach ( $other->dimTaint as $key => $_ ) {
287            unset( $this->dimTaint[$key] );
288        }
289        if (
290            ( $this->flags & SecurityCheckPlugin::SQL_NUMKEY_TAINT ) &&
291            !$this->has( SecurityCheckPlugin::SQL_TAINT )
292        ) {
293            // Note that this adjustment is not guaranteed to happen immediately after the removal of the last
294            // integer key. For instance, in [ 0 => unsafe, 'foo' => unsafe ], if only the element 0 is removed,
295            // this branch will not run because 'foo' still contributes sql taint.
296            $this->flags &= ~SecurityCheckPlugin::SQL_NUMKEY_TAINT;
297        }
298    }
299
300    /**
301     * Merge this object with $other, recursively and without creating a copy.
302     * @see Taintedness::asMergedWith() if you need a copy
303     *
304     * @param Taintedness $other
305     */
306    public function mergeWith( self $other ): void {
307        $this->flags |= $other->flags;
308        if ( $other->unknownDimsTaint && !$this->unknownDimsTaint ) {
309            $this->unknownDimsTaint = $other->unknownDimsTaint;
310        } elseif ( $other->unknownDimsTaint ) {
311            $this->unknownDimsTaint->mergeWith( $other->unknownDimsTaint );
312        }
313        $this->keysTaint |= $other->keysTaint;
314        foreach ( $other->dimTaint as $key => $val ) {
315            if ( !isset( $this->dimTaint[$key] ) ) {
316                $this->dimTaint[$key] = clone $val;
317            } else {
318                $this->dimTaint[$key]->mergeWith( $val );
319            }
320        }
321    }
322
323    /**
324     * Merge this object with $other, recursively, creating a copy.
325     * @see Taintedness::mergeWith() for in-place merge
326     *
327     * @param Taintedness $other
328     * @return $this
329     */
330    public function asMergedWith( self $other ): self {
331        $ret = clone $this;
332        $ret->mergeWith( $other );
333        return $ret;
334    }
335
336    // Offsets taintedness
337
338    /**
339     * Set the taintedness for $offset to $value, in place
340     *
341     * @param Node|mixed $offset Node or a scalar value, already resolved
342     * @param Taintedness $value
343     */
344    public function setOffsetTaintedness( $offset, self $value ): void {
345        if ( is_scalar( $offset ) ) {
346            $this->dimTaint[$offset] = $value;
347        } else {
348            $this->unknownDimsTaint ??= self::newSafe();
349            $this->unknownDimsTaint->mergeWith( $value );
350        }
351    }
352
353    /**
354     * Adds the bits in $value to the taintedness of the keys
355     * @param int $value
356     */
357    public function addKeysTaintedness( int $value ): void {
358        $this->keysTaint |= $value;
359    }
360
361    /**
362     * @param self $other
363     * @param int $depth
364     * @return self
365     */
366    public function asMergedForAssignment( self $other, int $depth ): self {
367        if ( $depth === 0 ) {
368            return $other;
369        }
370        $ret = clone $this;
371        $ret->flags |= $other->flags;
372        $ret->keysTaint |= $other->keysTaint;
373        if ( !$ret->unknownDimsTaint ) {
374            $ret->unknownDimsTaint = $other->unknownDimsTaint;
375        } elseif ( $other->unknownDimsTaint ) {
376            $ret->unknownDimsTaint->mergeWith( $other->unknownDimsTaint );
377        }
378        foreach ( $other->dimTaint as $k => $v ) {
379            $ret->dimTaint[$k] = isset( $ret->dimTaint[$k] )
380                ? $ret->dimTaint[$k]->asMergedForAssignment( $v, $depth - 1 )
381                : $v;
382        }
383        return $ret;
384    }
385
386    /**
387     * Apply an array addition with $other
388     *
389     * @param Taintedness $other
390     */
391    public function arrayPlus( self $other ): void {
392        $this->flags |= $other->flags;
393        if ( $other->unknownDimsTaint && !$this->unknownDimsTaint ) {
394            $this->unknownDimsTaint = $other->unknownDimsTaint;
395        } elseif ( $other->unknownDimsTaint ) {
396            $this->unknownDimsTaint->mergeWith( $other->unknownDimsTaint );
397        }
398        $this->keysTaint |= $other->keysTaint;
399        // This is not recursive because array addition isn't
400        $this->dimTaint += $other->dimTaint;
401    }
402
403    /**
404     * Apply the effect of array addition and return a clone of $this
405     *
406     * @param Taintedness $other
407     * @return $this
408     */
409    public function asArrayPlusWith( self $other ): self {
410        $ret = clone $this;
411        $ret->arrayPlus( $other );
412        return $ret;
413    }
414
415    /**
416     * Get the taintedness for the given offset, if set. If $offset could not be resolved, this
417     * will return the whole object, with taint from unknown keys added. If the offset is not known,
418     * it will return a new Taintedness object without the original shape, and with taint from
419     * unknown keys added.
420     *
421     * @param Node|string|int|bool|float|null $offset
422     * @return self Always a copy
423     */
424    public function getTaintednessForOffsetOrWhole( $offset ): self {
425        if ( !is_scalar( $offset ) ) {
426            return $this->asValueFirstLevel();
427        }
428        if ( isset( $this->dimTaint[$offset] ) ) {
429            if ( $this->unknownDimsTaint ) {
430                $ret = $this->dimTaint[$offset]->asMergedWith( $this->unknownDimsTaint );
431            } else {
432                $ret = $this->dimTaint[$offset];
433            }
434        } elseif ( $this->unknownDimsTaint ) {
435            $ret = $this->unknownDimsTaint;
436        } else {
437            return new self( $this->flags );
438        }
439        $ret->flags |= $this->flags;
440        return $ret;
441    }
442
443    /**
444     * Create a new object with $this at the given $offset (if scalar) or as unknown object.
445     *
446     * @param Node|string|int|bool|float|null $offset
447     * @param int|null $offsetTaint If available, will be used as key taint
448     * @return self Always a copy
449     */
450    public function asMaybeMovedAtOffset( $offset, int $offsetTaint = null ): self {
451        $ret = self::newSafe();
452        if ( $offsetTaint !== null ) {
453            $ret->keysTaint = $offsetTaint;
454        }
455        if ( $offset instanceof Node || $offset === null ) {
456            $ret->unknownDimsTaint = clone $this;
457        } else {
458            $ret->dimTaint[$offset] = clone $this;
459        }
460        return $ret;
461    }
462
463    /**
464     * Get a representation of this taint at the first depth level. For instance, this can be used in a foreach
465     * assignment for the value. Own taint and unknown keys taint are preserved, and then we merge in recursively
466     * all the current keys.
467     *
468     * @return $this
469     */
470    public function asValueFirstLevel(): self {
471        $ret = new self( $this->flags & ~SecurityCheckPlugin::SQL_NUMKEY_TAINT );
472        if ( $this->unknownDimsTaint ) {
473            $ret->mergeWith( $this->unknownDimsTaint );
474        }
475        foreach ( $this->dimTaint as $val ) {
476            $ret->mergeWith( $val );
477        }
478        return $ret;
479    }
480
481    /**
482     * Creates a copy of this object without the given key
483     * @param string|int|bool|float $key
484     * @return $this
485     */
486    public function withoutKey( $key ): self {
487        $ret = clone $this;
488        unset( $ret->dimTaint[$key] );
489        if (
490            ( $ret->flags & SecurityCheckPlugin::SQL_NUMKEY_TAINT ) &&
491            !$ret->has( SecurityCheckPlugin::SQL_TAINT )
492        ) {
493            // Note that this adjustment is not guaranteed to happen immediately after the removal of the last
494            // integer key. For instance, in [ 0 => unsafe, 'foo' => unsafe ], if the element 0 is removed,
495            // this branch will not run because 'foo' still contributes sql taint.
496            $ret->flags &= ~SecurityCheckPlugin::SQL_NUMKEY_TAINT;
497        }
498        return $ret;
499    }
500
501    /**
502     * Creates a copy of this object without known offsets, and without keysTaint
503     * @return $this
504     */
505    public function withoutKeys(): self {
506        $ret = clone $this;
507        $ret->keysTaint = SecurityCheckPlugin::NO_TAINT;
508        if ( !$ret->dimTaint ) {
509            return $ret;
510        }
511        $ret->unknownDimsTaint ??= self::newSafe();
512        foreach ( $ret->dimTaint as $dim => $taint ) {
513            $ret->unknownDimsTaint->mergeWith( $taint );
514            unset( $ret->dimTaint[$dim] );
515        }
516        return $ret;
517    }
518
519    /**
520     * Get a representation of this taint to be used in a foreach assignment for the key
521     *
522     * @return $this
523     */
524    public function asKeyForForeach(): self {
525        return new self( ( $this->keysTaint | $this->flags ) & ~SecurityCheckPlugin::SQL_NUMKEY_TAINT );
526    }
527
528    /**
529     * Applies an array_replace operations to $this, in place.
530     *
531     * @param Taintedness $other
532     * @return void
533     */
534    public function arrayReplace( self $other ): void {
535        $this->flags |= $other->flags;
536        $this->dimTaint = array_replace( $this->dimTaint, $other->dimTaint );
537        if ( $other->unknownDimsTaint ) {
538            if ( $this->unknownDimsTaint ) {
539                $this->unknownDimsTaint->mergeWith( $other->unknownDimsTaint );
540            } else {
541                $this->unknownDimsTaint = $other->unknownDimsTaint;
542            }
543        }
544    }
545
546    /**
547     * Applies an array_merge operations to $this, in place.
548     *
549     * @param Taintedness $other
550     * @return void
551     */
552    public function arrayMerge( self $other ): void {
553        // First merge the known elements
554        $this->dimTaint = array_merge( $this->dimTaint, $other->dimTaint );
555        // Then merge general flags, key flags, and any unknown keys
556        $this->flags |= $other->flags;
557        $this->keysTaint |= $other->keysTaint;
558        $this->unknownDimsTaint ??= new self( SecurityCheckPlugin::NO_TAINT );
559        if ( $other->unknownDimsTaint ) {
560            $this->unknownDimsTaint->mergeWith( $other->unknownDimsTaint );
561        }
562        // Finally, move taintedness from int keys to unknown
563        foreach ( $this->dimTaint as $k => $val ) {
564            if ( is_int( $k ) ) {
565                $this->unknownDimsTaint->mergeWith( $val );
566                unset( $this->dimTaint[$k] );
567            }
568        }
569    }
570
571    // Conversion/checks shortcuts
572
573    /**
574     * Check whether this object has no taintedness.
575     *
576     * @return bool
577     */
578    public function isSafe(): bool {
579        // Don't use get() for performance
580        if ( $this->flags !== SecurityCheckPlugin::NO_TAINT ) {
581            return false;
582        }
583        if ( $this->keysTaint !== SecurityCheckPlugin::NO_TAINT ) {
584            return false;
585        }
586        if ( $this->unknownDimsTaint && !$this->unknownDimsTaint->isSafe() ) {
587            return false;
588        }
589        foreach ( $this->dimTaint as $val ) {
590            if ( !$val->isSafe() ) {
591                return false;
592            }
593        }
594        return true;
595    }
596
597    /**
598     * Convert exec to yes taint recursively. Special flags like UNKNOWN or INAPPLICABLE are discarded.
599     * Any YES flags are also discarded. Note that this returns a copy of the
600     * original object. The shape is preserved.
601     *
602     * @warning This function is nilpotent: f^2(x) = 0
603     *
604     * @return self
605     */
606    public function asExecToYesTaint(): self {
607        $ret = new self( ( $this->flags & SecurityCheckPlugin::ALL_EXEC_TAINT ) >> 1 );
608        if ( $this->unknownDimsTaint ) {
609            $ret->unknownDimsTaint = $this->unknownDimsTaint->asExecToYesTaint();
610        }
611        $ret->keysTaint = ( $this->keysTaint & SecurityCheckPlugin::ALL_EXEC_TAINT ) >> 1;
612        foreach ( $this->dimTaint as $k => $val ) {
613            $ret->dimTaint[$k] = $val->asExecToYesTaint();
614        }
615        return $ret;
616    }
617
618    /**
619     * Convert the yes taint bits to corresponding exec taint bits recursively.
620     * Any UNKNOWN_TAINT or INAPPLICABLE_TAINT is discarded. Note that this returns a copy of the
621     * original object. The shape is preserved.
622     *
623     * @warning This function is nilpotent: f^2(x) = 0
624     *
625     * @return self
626     * @suppress PhanUnreferencedPublicMethod For consistency
627     */
628    public function asYesToExecTaint(): self {
629        $ret = new self( ( $this->flags & SecurityCheckPlugin::ALL_TAINT ) << 1 );
630        if ( $this->unknownDimsTaint ) {
631            $ret->unknownDimsTaint = $this->unknownDimsTaint->asYesToExecTaint();
632        }
633        $ret->keysTaint = ( $this->keysTaint & SecurityCheckPlugin::ALL_TAINT ) << 1;
634        foreach ( $this->dimTaint as $k => $val ) {
635            $ret->dimTaint[$k] = $val->asYesToExecTaint();
636        }
637        return $ret;
638    }
639
640    /**
641     * @param ParamLinksOffsets $offsets
642     * @return self
643     */
644    public function asMovedAtRelevantOffsetsForBackprop( ParamLinksOffsets $offsets ): self {
645        $offsetsFlags = $offsets->getFlags();
646        $ret = $offsetsFlags ?
647            $this->withOnly( ( $offsetsFlags & SecurityCheckPlugin::ALL_TAINT ) << 1 )
648            : new self( SecurityCheckPlugin::NO_TAINT );
649        foreach ( $offsets->getDims() as $k => $val ) {
650            $newVal = $this->asMovedAtRelevantOffsetsForBackprop( $val );
651            if ( isset( $ret->dimTaint[$k] ) ) {
652                $ret->dimTaint[$k]->mergeWith( $newVal );
653            } else {
654                $ret->dimTaint[$k] = $newVal;
655            }
656        }
657        $unknownOffs = $offsets->getUnknown();
658        if ( $unknownOffs ) {
659            $newVal = $this->asMovedAtRelevantOffsetsForBackprop( $unknownOffs );
660            if ( $ret->unknownDimsTaint ) {
661                $ret->unknownDimsTaint->mergeWith( $newVal );
662            } else {
663                $ret->unknownDimsTaint = $newVal;
664            }
665        }
666        $ret->keysTaint |= ( $this->flags | $this->keysTaint ) &
667            ( ( $offsets->getKeysFlags() & SecurityCheckPlugin::ALL_TAINT ) << 1 );
668        return $ret;
669    }
670
671    /**
672     * Utility method to convert some flags from EXEC to YES. Note that this is not used internally
673     * to avoid the unnecessary overhead of a function call in hot code.
674     *
675     * @param int $flags
676     * @return int
677     */
678    public static function flagsAsExecToYesTaint( int $flags ): int {
679        return ( $flags & SecurityCheckPlugin::ALL_EXEC_TAINT ) >> 1;
680    }
681
682    /**
683     * Utility method to convert some flags from YES to EXEC. Note that this is not used internally
684     * to avoid the unnecessary overhead of a function call in hot code.
685     *
686     * @param int $flags
687     * @return int
688     */
689    public static function flagsAsYesToExecTaint( int $flags ): int {
690        return ( $flags & SecurityCheckPlugin::ALL_TAINT ) << 1;
691    }
692
693    /**
694     * @todo This method shouldn't be necessary (ideally)
695     * @return PreservedTaintedness
696     */
697    public function asPreservedTaintedness(): PreservedTaintedness {
698        $ret = new PreservedTaintedness( new ParamLinksOffsets( $this->flags ) );
699        foreach ( $this->dimTaint as $k => $val ) {
700            $ret->setOffsetTaintedness( $k, $val->asPreservedTaintedness() );
701        }
702        if ( $this->unknownDimsTaint ) {
703            $ret->setOffsetTaintedness( null, $this->unknownDimsTaint->asPreservedTaintedness() );
704        }
705        return $ret;
706    }
707
708    /**
709     * Given some method links, returns a list of pairs of LinksSet and Taintedness objects, where the taintedness
710     * in each pair should be backpropagated to the links in the LinksSet.
711     *
712     * @param MethodLinks $links
713     * @return array<array<LinksSet|Taintedness>>
714     * @phan-return array<array{0:LinksSet,1:Taintedness}>
715     */
716    public function decomposeForLinks( MethodLinks $links ): array {
717        $pairs = [];
718
719        if ( $this->flags !== SecurityCheckPlugin::NO_TAINT ) {
720            $pairs[] = [ $links->getLinksCollapsing(), new self( $this->flags ) ];
721        }
722
723        if ( $this->keysTaint !== SecurityCheckPlugin::NO_TAINT ) {
724            $pairs[] = [ $links->asKeyForForeach()->getLinksCollapsing(), $this->asKeyForForeach() ];
725        }
726
727        foreach ( $this->dimTaint as $k => $dimTaint ) {
728            $pairs = array_merge(
729                $pairs,
730                $dimTaint->decomposeForLinks( $links->getForDim( $k ) )
731            );
732        }
733
734        if ( $this->unknownDimsTaint ) {
735            $pairs = array_merge(
736                $pairs,
737                $this->unknownDimsTaint->decomposeForLinks( $links->getForDim( null ) )
738            );
739        }
740        return $pairs;
741    }
742
743    /**
744     * Get a stringified representation of this taintedness, useful for debugging etc.
745     *
746     * @param string $indent
747     * @return string
748     * @suppress PhanUnreferencedPublicMethod
749     */
750    public function toString( $indent = '' ): string {
751        $flags = SecurityCheckPlugin::taintToString( $this->flags );
752        $keys = SecurityCheckPlugin::taintToString( $this->keysTaint );
753        $ret = <<<EOT
754{
755$indent    Own taint: $flags
756$indent    Keys: $keys
757$indent    Elements: {
758EOT;
759
760        $kIndent = "$indent    ";
761        $first = "\n";
762        $last = '';
763        foreach ( $this->dimTaint as $key => $taint ) {
764            $ret .= "$first$kIndent    $key => " . $taint->toString( "$kIndent    " ) . "\n";
765            $first = '';
766            $last = $kIndent;
767        }
768        if ( $this->unknownDimsTaint ) {
769            $ret .= "$first$kIndent    UNKNOWN => " . $this->unknownDimsTaint->toString( "$kIndent    " ) . "\n";
770            $last = $kIndent;
771        }
772        $ret .= "$last}\n$indent}";
773        return $ret;
774    }
775
776    /**
777     * Get a stringified representation of this taintedness suitable for the debug annotation
778     *
779     * @return string
780     */
781    public function toShortString(): string {
782        $flags = SecurityCheckPlugin::taintToString( $this->flags );
783        $ret = "{Own: $flags";
784        if ( $this->keysTaint ) {
785            $ret .= '; Keys: ' . SecurityCheckPlugin::taintToString( $this->keysTaint );
786        }
787        $keyParts = [];
788        if ( $this->dimTaint ) {
789            foreach ( $this->dimTaint as $key => $taint ) {
790                $keyParts[] = "$key => " . $taint->toShortString();
791            }
792        }
793        if ( $this->unknownDimsTaint ) {
794            $keyParts[] = 'UNKNOWN => ' . $this->unknownDimsTaint->toShortString();
795        }
796        if ( $keyParts ) {
797            $ret .= '; Elements: {' . implode( '; ', $keyParts ) . '}';
798        }
799        $ret .= '}';
800        return $ret;
801    }
802
803    /**
804     * Make sure to clone member variables, too.
805     */
806    public function __clone() {
807        if ( $this->unknownDimsTaint ) {
808            $this->unknownDimsTaint = clone $this->unknownDimsTaint;
809        }
810        foreach ( $this->dimTaint as $k => $v ) {
811            $this->dimTaint[$k] = clone $v;
812        }
813    }
814
815    /**
816     * @return string
817     */
818    public function __toString(): string {
819        return $this->toShortString();
820    }
821}