Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/Rules/Functions/PrintfHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use function max;
use function sprintf;
use function strlen;
use function strstr;
use const PREG_SET_ORDER;

#[AutowiredService]
Expand All @@ -37,14 +38,21 @@ public function getPrintfPlaceholders(string $format): ?array

public function getScanfPlaceholdersCount(string $format): ?int
{
return $this->getPlaceholdersCount('(?<specifier>[cdDeEfinosuxX%s]|\[[^\]]+\])', $format, true);
return $this->getPlaceholdersCount('(?:[lLh]?(?<specifier>[cdDeEfginosuxX%s]|\[[^\]]+\]))', $format, true);
}

/**
* @phpstan-return array<int, non-empty-list<PrintfPlaceholder>>|null parameter index => placeholders
*/
private function parsePlaceholders(string $specifiersPattern, string $format, bool $isScanf): ?array
{
if ($isScanf) {
$beforeNul = strstr($format, "\0", true);
if ($beforeNul !== false) {
$format = $beforeNul;
}
}

$addSpecifier = '';
if ($this->phpVersion->supportsHhPrintfSpecifier()) {
$addSpecifier .= 'hH';
Expand Down
23 changes: 16 additions & 7 deletions src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use function count;
use function in_array;
use function preg_match_all;
use function strstr;

#[AutowiredService]
final class SscanfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
Expand Down Expand Up @@ -48,9 +49,15 @@ public function getTypeFromFunctionCall(
return null;
}

if (preg_match_all('/%(\d*)(\[[^\]]+\]|[cdeEfosux]{1})/', $formatType->getValue(), $matches) > 0) {
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
$formatValue = $formatType->getValue();
$beforeNul = strstr($formatValue, "\0", true);
if ($beforeNul !== false) {
$formatValue = $beforeNul;
}

$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();

if (preg_match_all('/%(\d*)[lLh]?(\[[^\]]+\]|[cDdeEfginosuxX])/', $formatValue, $matches) > 0) {
for ($i = 0; $i < count($matches[0]); $i++) {
$length = $matches[1][$i];
$specifier = $matches[2][$i];
Expand All @@ -70,22 +77,24 @@ public function getTypeFromFunctionCall(
}
}

if (in_array($specifier, ['d', 'o', 'u', 'x'], true)) {
if (in_array($specifier, ['d', 'D', 'i', 'n', 'o', 'x', 'X'], true)) {
$type = new IntegerType();
}

if (in_array($specifier, ['e', 'E', 'f'], true)) {
if ($specifier === 'u') {
$type = TypeCombinator::union(new IntegerType(), new StringType());
}

if (in_array($specifier, ['e', 'E', 'f', 'g'], true)) {
$type = new FloatType();
}

$type = TypeCombinator::addNull($type);
$arrayBuilder->setOffsetValueType(new ConstantIntegerType($i), $type);
}

return TypeCombinator::addNull($arrayBuilder->getArray());
}

return null;
return TypeCombinator::addNull($arrayBuilder->getArray());
}

}
56 changes: 56 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14567.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Bug14567;

use function PHPStan\Testing\assertType;

function sscanfNulTerminator(string $s) {
// NUL byte terminates sscanf format string - placeholders after \0 are ignored
assertType('array{int|null}|null', sscanf($s, "%d\0%d"));
assertType('array{int|null, string|null}|null', sscanf($s, "%d %s\0%d"));
assertType('array{}|null', sscanf($s, "\0%d%s"));
}

function fscanfNulTerminator($r) {
// Same for fscanf
assertType('array{int|null}|null', fscanf($r, "%d\0%d"));
assertType('array{int|null, string|null}|null', fscanf($r, "%d %s\0%d"));
assertType('array{}|null', fscanf($r, "\0%d%s"));
}

function sscanfEdgeCases(string $s) {
// Empty format string - no placeholders
assertType('array{}|null', sscanf($s, ""));

// %n - counts characters consumed, returns integer
assertType('array{int|null}|null', sscanf($s, "%n"));

// %% - literal percent, not a placeholder
assertType('array{}|null', sscanf($s, "%%"));

// %i - integer with base detection
assertType('array{int|null}|null', sscanf($s, "%i"));

// %X - uppercase hex, same as %x
assertType('array{int|null}|null', sscanf($s, "%X"));

// %D - uppercase alias for %d
assertType('array{int|null}|null', sscanf($s, "%D"));

// %g - general float
assertType('array{float|null}|null', sscanf($s, "%g"));

// %u - unsigned integer, can return string for values > PHP_INT_MAX
assertType('array{int|string|null}|null', sscanf($s, "%u"));

// mixed specifiers with %n
assertType('array{int|null, int|null}|null', sscanf($s, "%d%n"));

// Size modifiers (l, L, h) — consumed by ValidateFormat, no effect on PHP type
assertType('array{int|null}|null', sscanf($s, "%ld"));
assertType('array{float|null}|null', sscanf($s, "%lf"));
assertType('array{float|null}|null', sscanf($s, "%Lf"));
assertType('array{int|null}|null', sscanf($s, "%hd"));
assertType('array{int|string|null}|null', sscanf($s, "%lu"));
assertType('array{int|null, float|null, string|null}|null', sscanf($s, "%ld %lf %s"));
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/sscanf.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function sscanfFormatInference(string $s) {
assertType('array{float|null}|null', sscanf($s, '%f'));
assertType('array{int|null}|null', sscanf($s, '%o'));
assertType('array{string|null}|null', sscanf($s, '%s'));
assertType('array{int|null}|null', sscanf($s, '%u'));
assertType('array{int|string|null}|null', sscanf($s, '%u'));
assertType('array{int|null}|null', sscanf($s, '%x'));

$mandate = "January 01 2000";
Expand Down
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,9 @@ public function testBug10260(): void
$this->analyse([__DIR__ . '/data/bug-10260.php'], []);
}

public function testBug14567(): void
{
$this->analyse([__DIR__ . '/data/bug-14567.php'], []);
}

}
Loading
Loading