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