Skip to content

Commit 21984c2

Browse files
committed
Improve curl_init() return type analysis
1 parent 03626d9 commit 21984c2

File tree

2 files changed

+92
-2
lines changed

2 files changed

+92
-2
lines changed

src/Type/Php/CurlInitReturnTypeExtension.php

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,32 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\Php\PhpVersion;
78
use PHPStan\Reflection\FunctionReflection;
89
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\ShouldNotHappenException;
911
use PHPStan\Type\Constant\ConstantBooleanType;
1012
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
13+
use PHPStan\Type\NeverType;
1114
use PHPStan\Type\Type;
1215
use PHPStan\Type\TypeCombinator;
1316
use function count;
17+
use function is_string;
18+
use function parse_url;
19+
use function str_contains;
20+
use function strcasecmp;
21+
use function strlen;
1422

1523
class CurlInitReturnTypeExtension implements DynamicFunctionReturnTypeExtension
1624
{
1725

26+
/** @see https://github.com/curl/curl/blob/curl-8_9_1/lib/urldata.h#L135 */
27+
private const CURL_MAX_INPUT_LENGTH = 8000000;
28+
29+
public function __construct(private PhpVersion $phpVersion)
30+
{
31+
}
32+
1833
public function isFunctionSupported(FunctionReflection $functionReflection): bool
1934
{
2035
return $functionReflection->getName() === 'curl_init';
@@ -26,10 +41,49 @@ public function getTypeFromFunctionCall(
2641
Scope $scope,
2742
): Type
2843
{
29-
$argsCount = count($functionCall->getArgs());
44+
$args = $functionCall->getArgs();
45+
$argsCount = count($args);
3046
$returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
47+
$notFalseReturnType = TypeCombinator::remove($returnType, new ConstantBooleanType(false));
3148
if ($argsCount === 0) {
32-
return TypeCombinator::remove($returnType, new ConstantBooleanType(false));
49+
return $notFalseReturnType;
50+
}
51+
52+
$urlArgType = $scope->getType($args[0]->value);
53+
if ($urlArgType->isNull()->yes()) {
54+
return $notFalseReturnType;
55+
}
56+
if ($urlArgType->isString()->yes()) {
57+
$urlArgValue = $urlArgType->getConstantScalarValues()[0] ?? null;
58+
if ($urlArgValue !== null) {
59+
if (!is_string($urlArgValue)) {
60+
throw new ShouldNotHappenException();
61+
}
62+
if (str_contains($urlArgValue, "\0")) {
63+
if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) {
64+
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L112-L115
65+
return new ConstantBooleanType(false);
66+
}
67+
// https://github.com/php/php-src/blob/php-8.0.0/ext/curl/interface.c#L104-L107
68+
return new NeverType();
69+
}
70+
if ($this->phpVersion->getVersionId() < 80000) {
71+
// Before PHP 8.0 an unparsable URL or a file:// scheme would fail if open_basedir is used
72+
// Since we can't detect open_basedir properly, we'll always consider a failure possible if these
73+
// conditions are given
74+
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L139-L158
75+
$parsedUrlArg = parse_url($urlArgValue);
76+
if ($parsedUrlArg === false || (isset($parsedUrlArg['scheme']) && strcasecmp($parsedUrlArg['scheme'], 'file') === 0)) {
77+
return $returnType;
78+
}
79+
}
80+
if (strlen($urlArgValue) > self::CURL_MAX_INPUT_LENGTH) {
81+
// Since libcurl 7.65.0 this would always fail, but no current PHP version requires it at the moment
82+
// https://github.com/curl/curl/commit/5fc28510a4664f46459d9a40187d81cc08571e60
83+
return $returnType;
84+
}
85+
return $notFalseReturnType;
86+
}
3387
}
3488

3589
return $returnType;

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3087,6 +3087,42 @@ public function dataBinaryOperations(): array
30873087
PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle|false',
30883088
'curl_init($string)',
30893089
],
3090+
[
3091+
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
3092+
'curl_init(null)',
3093+
],
3094+
[
3095+
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
3096+
'curl_init(\'https://phpstan.org\')',
3097+
],
3098+
[
3099+
PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*',
3100+
"curl_init('\0')",
3101+
],
3102+
[
3103+
PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*',
3104+
"curl_init('https://phpstan.org\0')",
3105+
],
3106+
[
3107+
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
3108+
'curl_init(\'\')',
3109+
],
3110+
[
3111+
PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle',
3112+
'curl_init(\':\')',
3113+
],
3114+
[
3115+
PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle',
3116+
'curl_init(\'file://host/text.txt\')',
3117+
],
3118+
[
3119+
PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle',
3120+
'curl_init(\'FIle://host/text.txt\')',
3121+
],
3122+
[
3123+
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
3124+
'curl_init(\'host/text.txt\')',
3125+
],
30903126
[
30913127
'string',
30923128
'sprintf($string, $string, 1)',

0 commit comments

Comments
 (0)