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