Repository navigation
Enable checked-exception analysis and validate malformed database data #299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a610348
38190d7
b6d93de
da47649
0a57395
ebc163b
be567b1
a272d7a
b0f69f1
e84048d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,16 @@ parameters: | |
| paths: | ||
| - src | ||
| - tests | ||
| exceptions: | ||
| # These classes signal programmer errors, so callers need not declare them. | ||
| uncheckedExceptionClasses: | ||
| - Error | ||
| - BadMethodCallException | ||
| check: | ||
| missingCheckedExceptionInThrows: true | ||
|
Comment on lines
+6
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,45p' phpstan.neon
sed -n '1,75p' composer.json
rg -n '"name": "phpstan/phpstan"|"version": "[12]\.' composer.lock
rg -n 'phpstan analyse|phpstan/phpstan|composer.lock' .github Makefile composer.jsonRepository: maxmind/MaxMind-DB-Reader-php Length of output: 2381 🌐 Web query:
💡 Result: 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- changed files and diff ---'
git diff --stat c3c430991bafbf6b42d7d5554109fab5637deda0 7025dcb4ead8549f66409d2a536737194e06a070
git diff --unified=30 c3c430991bafbf6b42d7d5554109fab5637deda0 7025dcb4ead8549f66409d2a536737194e06a070 -- phpstan.neon composer.json .github
printf '%s\n' '--- repository files relevant to dependency resolution and analysis ---'
git ls-files | rg '(^|/)(composer(\.json|\.lock)?|phpstan\.neon|Makefile|\.github/)' || true
printf '%s\n' '--- analysis references ---'
rg -n -S 'phpstan|composer install|composer update|composer require' --glob '!vendor/**' --glob '!node_modules/**' . || trueRepository: maxmind/MaxMind-DB-Reader-php Length of output: 4235 🌐 Web query:
💡 Result: 🏁 Script executed: git diff --stat c3c430991bafbf6b42d7d5554109fab5637deda0 7025dcb4ead8549f66409d2a536737194e06a070
git diff --unified=15 c3c430991bafbf6b42d7d5554109fab5637deda0 7025dcb4ead8549f66409d2a536737194e06a070 -- phpstan.neon composer.json .github
git ls-files | rg '(^|/)(composer(\.json|\.lock)?|phpstan\.neon|Makefile|\.github/)'
rg -n -S 'phpstan|composer install|composer update|composer require' --glob '!vendor/**' --glob '!node_modules/**' .Repository: maxmind/MaxMind-DB-Reader-php Length of output: 3609 🌐 Web query:
💡 Result: 🏁 Script executed: set -u
printf '%s\n' '--- repository diff ---'
git diff --unified=12 c3c430991bafbf6b42d7d5554109fab5637deda0 7025dcb4ead8549f66409d2a536737194e06a070 -- phpstan.neon composer.json
printf '%s\n' '--- tracked workflow and docs names ---'
git ls-files | grep -E '(^|/)(composer\.json|composer\.lock|phpstan\.neon|Makefile|\.github/|README|CONTRIBUTING)' || true
printf '%s\n' '--- PHPStan references ---'
grep -RInE 'phpstan|composer (install|update|require)' --exclude-dir=.git --exclude-dir=vendor . 2>/dev/null || true
printf '%s\n' '--- PHPStan 1.12 tagged config source ---'
python3 - <<'PY'
import urllib.request
urls = [
'https://raw.githubusercontent.com/phpstan/phpstan/1.12.x/conf/config.neon',
'https://raw.githubusercontent.com/phpstan/phpstan/1.12.0/conf/config.neon',
'https://raw.githubusercontent.com/phpstan/phpstan/1.12.x/src/Analyser/Exceptions/ExceptionsConfigurationExtension.php',
]
for url in urls:
print('URL', url)
try:
text = urllib.request.urlopen(url, timeout=10).read().decode()
for i, line in enumerate(text.splitlines(), 1):
if 'missingCheckedExceptionInThrows' in line or 'uncheckedExceptionClasses' in line or 'exceptions:' in line:
print(i, line)
except Exception as e:
print(type(e).__name__, e)
PYRepository: maxmind/MaxMind-DB-Reader-php Length of output: 4616 🏁 Script executed: set -u
printf '%s\n' '--- exception declarations ---'
rg -n -S 'class [A-Za-z0-9_]+Exception|interface [A-Za-z0-9_]+Exception|trait [A-Za-z0-9_]+Exception' src tests || true
printf '%s\n' '--- throw sites and throws annotations ---'
rg -n -S 'throw new|throw \$|@throws|throws ' src tests || true
printf '%s\n' '--- workflow context ---'
sed -n '20,48p' .github/workflows/lint.ymlRepository: maxmind/MaxMind-DB-Reader-php Length of output: 8979 Configure the checked exception set.
🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agent reply on behalf of @oschwald. No change needed. If |
||
| ignoreErrors: | ||
| # PHPUnit handles exceptions from test methods and their helpers. | ||
| - | ||
| identifier: missingType.checkedException | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This
🤖 Comment by Claude Opus 5.5.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in b6d93de, with the same change in the other three PRs. The test ignore now sets 🤖 Comment by Codex on behalf of Greg. |
||
| path: tests/* | ||
| reportUnmatched: false | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,11 +7,15 @@ | |
| use MaxMind\Db\Reader\Decoder; | ||
| use MaxMind\Db\Reader\InvalidDatabaseException; | ||
| use MaxMind\Db\Reader\Metadata; | ||
| use MaxMind\Db\Reader\UnsupportedPlatformException; | ||
| use MaxMind\Db\Reader\Util; | ||
|
|
||
| /** | ||
| * Instances of this class provide a reader for the MaxMind DB format. IP | ||
| * addresses can be looked up using the get method. | ||
| * | ||
| * The declared exceptions describe the pure PHP reader. The C extension may | ||
| * differ. | ||
| */ | ||
| class Reader | ||
| { | ||
|
|
@@ -71,10 +75,18 @@ class Reader | |
| * | ||
| * @param string $database the MaxMind DB file to use | ||
| * | ||
| * @throws \InvalidArgumentException for invalid database path or unknown arguments | ||
| * @throws \InvalidArgumentException if the database file does not exist or | ||
| * is not readable | ||
| * @throws InvalidDatabaseException | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The constructor gives the decoded metadata to If This bug is older than this PR, but the PR now documents this method. 🤖 Comment by Claude Opus 5.5.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 0a57395. The reader checks the decoded metadata container, and 🤖 Comment by Codex on behalf of Greg. |
||
| * if the database is invalid or there is an error reading | ||
| * from it | ||
| * if the database is invalid or there is an error reading | ||
| * from it | ||
| * @throws \UnexpectedValueException if the size of the database file | ||
| * cannot be determined | ||
| * @throws UnsupportedPlatformException if the metadata contains an integer | ||
| * that needs the gmp or bcmath extension | ||
| * and neither is installed, or a data | ||
| * offset that is too large for the | ||
| * platform | ||
| */ | ||
| public function __construct(string $database) | ||
| { | ||
|
|
@@ -110,6 +122,9 @@ public function __construct(string $database) | |
| $start = $this->findMetadataStart($database); | ||
| $metadataDecoder = new Decoder($this->fileHandle, $start); | ||
| [$metadataArray] = $metadataDecoder->decode($start); | ||
| if (!\is_array($metadataArray)) { | ||
| throw new InvalidDatabaseException('The database metadata must be a map.'); | ||
| } | ||
| $this->metadata = new Metadata($metadataArray); | ||
| $this->decoder = new Decoder( | ||
| $this->fileHandle, | ||
|
|
@@ -123,11 +138,18 @@ public function __construct(string $database) | |
| * | ||
| * @param string $ipAddress the IP address to look up | ||
| * | ||
| * @throws \BadMethodCallException if the database is closed or another lookup is in progress | ||
| * @throws \InvalidArgumentException if something other than a single IP address is passed to the method | ||
| * @throws \BadMethodCallException if the database is closed or another lookup is in progress | ||
| * @throws \InvalidArgumentException if the IP address is not valid, or if | ||
| * it is an IPv6 address and the database | ||
| * is IPv4-only | ||
| * @throws InvalidDatabaseException | ||
| * if the database is invalid or there is an error reading | ||
| * from it | ||
| * if the database is invalid or there is an error reading | ||
| * from it | ||
| * @throws UnsupportedPlatformException if the record contains an integer | ||
| * that needs the gmp or bcmath extension | ||
| * and neither is installed, or a data | ||
| * offset that is too large for the | ||
| * platform | ||
| * | ||
| * @return mixed the record for the IP address | ||
| */ | ||
|
|
@@ -148,11 +170,18 @@ public function get(string $ipAddress) | |
| * | ||
| * @param string $ipAddress the IP address to look up | ||
| * | ||
| * @throws \BadMethodCallException if the database is closed or another lookup is in progress | ||
| * @throws \InvalidArgumentException if something other than a single IP address is passed to the method | ||
| * @throws \BadMethodCallException if the database is closed or another lookup is in progress | ||
| * @throws \InvalidArgumentException if the IP address is not valid, or if | ||
| * it is an IPv6 address and the database | ||
| * is IPv4-only | ||
| * @throws InvalidDatabaseException | ||
| * if the database is invalid or there is an error reading | ||
| * from it | ||
| * if the database is invalid or there is an error reading | ||
| * from it | ||
| * @throws UnsupportedPlatformException if the record contains an integer | ||
| * that needs the gmp or bcmath extension | ||
| * and neither is installed, or a data | ||
| * offset that is too large for the | ||
| * platform | ||
| * | ||
| * @return array{0:mixed, 1:int} an array where the first element is the record and the | ||
| * second the network prefix length for the record | ||
|
|
@@ -193,6 +222,9 @@ public function getWithPrefixLen(string $ipAddress): array | |
| } | ||
|
|
||
| /** | ||
| * @throws \InvalidArgumentException | ||
| * @throws InvalidDatabaseException | ||
| * | ||
| * @return array{0:int, 1:int} | ||
| */ | ||
| private function findAddressInTree(string $ipAddress): array | ||
|
|
@@ -254,6 +286,9 @@ private function findAddressInTree(string $ipAddress): array | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @throws InvalidDatabaseException | ||
| */ | ||
| private function ipV4StartNode(): int | ||
| { | ||
| // If we have an IPv4 database, the start node is the first node | ||
|
|
@@ -270,6 +305,9 @@ private function ipV4StartNode(): int | |
| return $node; | ||
| } | ||
|
|
||
| /** | ||
| * @throws InvalidDatabaseException | ||
| */ | ||
| private function readNode(int $nodeNumber, int $index): int | ||
| { | ||
| $baseOffset = $nodeNumber * $this->metadata->nodeByteSize; | ||
|
|
@@ -325,6 +363,9 @@ private function readNode(int $nodeNumber, int $index): int | |
| } | ||
|
|
||
| /** | ||
| * @throws InvalidDatabaseException | ||
| * @throws UnsupportedPlatformException | ||
| * | ||
| * @return mixed | ||
| */ | ||
| private function resolveDataPointer(int $pointer) | ||
|
|
@@ -342,10 +383,12 @@ private function resolveDataPointer(int $pointer) | |
| return $data; | ||
| } | ||
|
|
||
| /* | ||
| /** | ||
| * This is an extremely naive but reasonably readable implementation. There | ||
| * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever | ||
| * an issue, but I suspect it won't be. | ||
| * | ||
| * @throws InvalidDatabaseException | ||
| */ | ||
| private function findMetadataStart(string $filename): int | ||
| { | ||
|
|
@@ -374,8 +417,10 @@ private function findMetadataStart(string $filename): int | |
| } | ||
|
|
||
| /** | ||
| * @throws \InvalidArgumentException if arguments are passed to the method | ||
| * @throws \BadMethodCallException if the database has been closed | ||
| * The C extension can also throw InvalidDatabaseException if it cannot | ||
| * decode the metadata. The pure PHP reader decodes it during construction. | ||
| * | ||
| * @throws \BadMethodCallException if the database has been closed | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
PHPStan reads this PHP source for 🤖 Comment by Claude Opus 5.5.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. b0f69f1 names the C-extension failure in the 🤖 Comment by Codex on behalf of Greg. |
||
| * | ||
| * @return Metadata object for the database | ||
| */ | ||
|
|
@@ -401,8 +446,7 @@ public function metadata(): Metadata | |
| /** | ||
| * Closes the MaxMind DB and returns resources to the system. | ||
| * | ||
| * @throws \Exception | ||
| * if an I/O error occurs | ||
| * @throws \BadMethodCallException if the database has already been closed | ||
| */ | ||
| public function close(): void | ||
| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Release order for the 4 STF-1850 PRs. This comment is the same on each of them:
Reader #299 and web-service-common #142 add
@throws \RuntimeExceptionto methods that GeoIP2 and minFraud call. GeoIP2 and minFraud do not commit a lockfile, and their version constraints accept the new releases. GeoIP2 requiresmaxmind-db/reader: ^1.13.0andmaxmind/web-service-common: ~0.11. minFraud gets web-service-common throughgeoip2/geoip2: ^v3.4.0. Thus, when an upstream PR is released, PHPStan fails in the downstream repo on its next CI run, unless the downstream change is already merged.Suggested order:
@throws \RuntimeExceptionwhere the upstream releases need it. The other comments in these reviews give the lines. An extra@throwstag is not an error now, becausetooWideThrowTypeis not enabled. In GeoIP2, move themetadata()andclose()ignores tophpstan.neonwithreportUnmatched: false, so that PHPStan passes before and after the reader release.🤖 Comment by Claude Opus 5.5.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. The downstream fixes are now pushed in maxmind/GeoIP2-php#348 and maxmind/minfraud-api-php#292. Both pass PHPStan and PHPUnit with the revised upstream source. Merge those PRs before releasing this PR and maxmind/web-service-common-php#142. The dependency minimums and obsolete compatibility ignores can be updated after release.
🤖 Comment by Codex on behalf of Greg.