Skip to content

Commit 2fdb743

Browse files
committed
Improve return type of base64_decode()
- Detect when the result cannot be false - Correctly treat non-boolean $strict parameter
1 parent a29b29d commit 2fdb743

File tree

3 files changed

+51
-23
lines changed

3 files changed

+51
-23
lines changed

src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\DependencyInjection\AutowiredService;
88
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\TrinaryLogic;
910
use PHPStan\Type\BenevolentUnionType;
1011
use PHPStan\Type\Constant\ConstantBooleanType;
12+
use PHPStan\Type\Constant\ConstantStringType;
1113
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12-
use PHPStan\Type\MixedType;
1314
use PHPStan\Type\StringType;
1415
use PHPStan\Type\Type;
1516
use PHPStan\Type\UnionType;
17+
use function base64_decode;
1618

1719
#[AutowiredService]
1820
final class Base64DecodeDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
@@ -29,32 +31,41 @@ public function getTypeFromFunctionCall(
2931
Scope $scope,
3032
): Type
3133
{
32-
if (!isset($functionCall->getArgs()[1])) {
34+
if (!isset($functionCall->getArgs()[0])) {
3335
return new StringType();
3436
}
3537

36-
$argType = $scope->getType($functionCall->getArgs()[1]->value);
37-
38-
if ($argType instanceof MixedType) {
39-
return new BenevolentUnionType([new StringType(), new ConstantBooleanType(false)]);
38+
$stringArgNode = $functionCall->getArgs()[0]->value;
39+
$constantStrings = $scope->getType($stringArgNode)->getConstantStrings();
40+
if ($constantStrings !== []) {
41+
$isValidBase64 = TrinaryLogic::lazyExtremeIdentity(
42+
$constantStrings,
43+
static function (ConstantStringType $constantString): TrinaryLogic {
44+
$isValid = base64_decode($constantString->getValue(), true) !== false;
45+
return TrinaryLogic::createFromBoolean($isValid);
46+
},
47+
);
48+
} else {
49+
$isValidBase64 = TrinaryLogic::createMaybe();
4050
}
4151

42-
$isTrueType = $argType->isTrue();
43-
$isFalseType = $argType->isFalse();
44-
$compareTypes = $isTrueType->compareTo($isFalseType);
45-
if ($compareTypes === $isTrueType) {
46-
return new UnionType([new StringType(), new ConstantBooleanType(false)]);
52+
if (isset($functionCall->getArgs()[1])) {
53+
$strictArgNode = $functionCall->getArgs()[1]->value;
54+
$isStrict = $scope->getType($strictArgNode)->toBoolean()->toTrinaryLogic();
55+
} else {
56+
$isStrict = TrinaryLogic::createNo();
4757
}
48-
if ($compareTypes === $isFalseType) {
58+
59+
if ($isStrict->no() || $isValidBase64->yes()) {
4960
return new StringType();
5061
}
51-
52-
// second argument could be interpreted as true
53-
if (!$isTrueType->no()) {
54-
return new UnionType([new StringType(), new ConstantBooleanType(false)]);
62+
if ($isStrict->yes() && $isValidBase64->no()) {
63+
return new ConstantBooleanType(false);
5564
}
56-
57-
return new StringType();
65+
if ($isStrict->maybe() && $isValidBase64->maybe()) {
66+
return new BenevolentUnionType([new StringType(), new ConstantBooleanType(false)]);
67+
}
68+
return new UnionType([new StringType(), new ConstantBooleanType(false)]);
5869
}
5970

6071
}

tests/PHPStan/Analyser/data/functions.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,12 @@
7878
$fileObject = new \SplFileObject(__FILE__);
7979
$fileObjectStat = $fileObject->fstat();
8080

81-
$base64DecodeWithoutStrict = base64_decode('');
82-
$base64DecodeWithStrictDisabled = base64_decode('', false);
83-
$base64DecodeWithStrictEnabled = base64_decode('', true);
84-
$base64DecodeDefault = base64_decode('', null);
85-
$base64DecodeBenevolent = base64_decode('', $undefined);
81+
$string = (string)time();
82+
$base64DecodeWithoutStrict = base64_decode($string);
83+
$base64DecodeWithStrictDisabled = base64_decode($string, false);
84+
$base64DecodeWithStrictEnabled = base64_decode($string, true);
85+
$base64DecodeDefault = base64_decode($string, null);
86+
$base64DecodeBenevolent = base64_decode($string, $undefined);
8687

8788

8889
//str_word_count

tests/PHPStan/Analyser/nsrt/base64_decode.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,27 @@ public function nonStrictMode(string $string): void
1111
{
1212
assertType('string', base64_decode($string));
1313
assertType('string', base64_decode($string, false));
14+
assertType('string', base64_decode($string, 0));
15+
assertType('string', base64_decode('UEhQU3Rhbg==', false));
16+
assertType('string', base64_decode('!!!', false));
1417
}
1518

1619
public function strictMode(string $string): void
1720
{
1821
assertType('string|false', base64_decode($string, true));
22+
assertType('string|false', base64_decode($string, 1));
23+
assertType('string', base64_decode(mt_rand() ? 'UEhQU3Rhbg==' : 'cm9ja3Mh', true));
24+
assertType('string|false', base64_decode(mt_rand() ? 'UEhQU3Rhbg==' : '!!!', true));
25+
assertType('false', base64_decode(mt_rand() ? '!' : '!!!', true));
26+
}
27+
28+
public function mixedMode(string $string): void
29+
{
30+
assertType('(string|false)', base64_decode($string, unknown()));
31+
assertType('(string|false)', base64_decode($string, mt_rand(0, 1) === 1));
32+
assertType('(string|false)', base64_decode($string, mt_rand(0, 1)));
33+
assertType('string', base64_decode('UEhQU3Rhbg==', mt_rand(0, 1) === 1));
34+
assertType('string|false', base64_decode('!!!', mt_rand(0, 1) === 1));
1935
}
2036

2137
}

0 commit comments

Comments
 (0)