Skip to content
Draft
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
31 changes: 30 additions & 1 deletion src/Rules/Functions/PrintfHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

namespace PHPStan\Rules\Functions;

use ErrorException;
use Nette\Utils\Strings;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use Throwable;
use function array_filter;
use function array_keys;
use function array_slice;
use function count;
use function in_array;
use function max;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function sscanf;
use function strlen;
use const PREG_SET_ORDER;

Expand All @@ -37,7 +43,30 @@ public function getPrintfPlaceholders(string $format): ?array

public function getScanfPlaceholdersCount(string $format): ?int
{
return $this->getPlaceholdersCount('(?<specifier>[cdDeEfinosuxX%s]|\[[^\]]+\])', $format, true);
try {
// if we would *know* that simple downgrader can handle
// Throwable -> ErrorException then 7.4 required error
// handler could be injected this way into the try/catch
// & appending finally as an extension. *dreaming*
set_error_handler(
static function ($s, $m, ...$vv) {
$vv = array_slice($vv, 0, 2);
throw new ErrorException($m, 0, $s, ...$vv);
},
);
$nFormat = '%*n' . $format;
$result = sscanf('', $nFormat);
} catch (Throwable) {
return null;
} finally {
restore_error_handler();
}
// one day phpstan may report here that $result can never be null for sscanf('', $nFormat),
// which is actually correct: https://3v4l.org/rO7Ni
if ($result === null) {
return null;
}
return count($result);
}

/**
Expand Down
13 changes: 13 additions & 0 deletions tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ public function testFile(): void
'Call to sprintf contains 2 placeholders, 1 value given.',
29,
],
[
'Call to sscanf contains an invalid placeholder.',
38,
],
[
'Call to fscanf contains an invalid placeholder.',
39,
],
[
'Call to sprintf contains 2 placeholders, 1 value given.',
45,
Expand Down Expand Up @@ -147,4 +155,9 @@ public function testBug10260(): void
$this->analyse([__DIR__ . '/data/bug-10260.php'], []);
}

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

}
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-14567.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Bug14567;

// NUL byte terminates sscanf/fscanf format string parsing
// Placeholders after \0 should not be counted

// Only 1 placeholder active before NUL
sscanf('123 456', "%d\0%d", $a);

// Only 1 placeholder active before NUL (fscanf)
fscanf(STDIN, "%d\0%d", $a2);

// No placeholders after NUL
sscanf('123', "\0%d");

// Multiple placeholders, NUL in middle
sscanf('123 456 789', "%d %d\0%d", $b, $c);
4 changes: 2 additions & 2 deletions tests/PHPStan/Rules/Functions/data/printf.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
sscanf($str, "%20[^abcde]a%d", $string, $number); // ok
printf("%.E", 3.14159); // ok
sprintf("%.E", 3.14159); // ok
sscanf($str, '%.E', $number); // ok
fscanf($str, '%.E', $number); // ok
sscanf($str, '%.E', $number); // bad scan conversion character '.'
fscanf($str, '%.E', $number); // bad scan conversion character '.'
sscanf($str, '%[A-Z]%d', $char, $number); // ok
sprintf('%s %s %s', ...[1]); // do not detect unpacked arguments
sprintf('%s %s %s', ...[1, 2, 3]); // ok
Expand Down
Loading