From 113d1d1bbfbb8a1af117df946c5e0cf77569d717 Mon Sep 17 00:00:00 2001 From: PurHur Date: Wed, 3 Jun 2026 22:59:57 +0000 Subject: [PATCH] Fix version_compare() canonicalize for dotted segments (#4796) Exclude '.' from digit/non-digit transition in php_canonicalize_version so '8.2.31' vs '8.2' compares like Zend. Matches php-src versioning.c isdig/isndig macros in VmInfo.php and phpc_info.c. Co-authored-by: Cursor --- ext/standard/VmInfo.php | 16 ++++++++++++++-- lib/AOT/runtime/phpc_info.c | 4 ++-- test/compliance/VersionCompareVMTest.php | 4 ++++ .../cases/stdlib/version_compare_partial.phpt | 11 +++++++++++ test/tmp/repro-version-compare-partial.php | 4 ++++ 5 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 test/compliance/cases/stdlib/version_compare_partial.phpt create mode 100644 test/tmp/repro-version-compare-partial.php diff --git a/ext/standard/VmInfo.php b/ext/standard/VmInfo.php index 389136cf0b2..af86c61c334 100644 --- a/ext/standard/VmInfo.php +++ b/ext/standard/VmInfo.php @@ -201,8 +201,8 @@ private static function canonicalizeVersion(string $version): string $buf .= '.'; } } elseif ( - (!ctype_digit($lp) && ctype_digit($ch)) - || (ctype_digit($lp) && !ctype_digit($ch)) + (self::isVersionNonDigitChar($lp) && self::isVersionDigitChar($ch)) + || (self::isVersionDigitChar($lp) && self::isVersionNonDigitChar($ch)) ) { if ('.' !== $lq) { $buf .= '.'; @@ -224,6 +224,18 @@ private static function canonicalizeVersion(string $version): string return $buf; } + /** php-src versioning.c isdig(): digit excluding '.' */ + private static function isVersionDigitChar(string $ch): bool + { + return ctype_digit($ch); + } + + /** php-src versioning.c isndig(): non-digit excluding '.' */ + private static function isVersionNonDigitChar(string $ch): bool + { + return !ctype_digit($ch) && '.' !== $ch; + } + private static function compareSpecialVersionForms(string $form1, string $form2): int { /** @var array */ diff --git a/lib/AOT/runtime/phpc_info.c b/lib/AOT/runtime/phpc_info.c index cdd933447ca..cf93f3792f1 100644 --- a/lib/AOT/runtime/phpc_info.c +++ b/lib/AOT/runtime/phpc_info.c @@ -192,8 +192,8 @@ static char *phpc_canonicalize_version(const char *version) *q++ = '.'; } } else if ( - (!isdigit((unsigned char) lp) && isdigit((unsigned char) *p)) - || (isdigit((unsigned char) lp) && !isdigit((unsigned char) *p)) + (!isdigit((unsigned char) lp) && '.' != lp && isdigit((unsigned char) *p)) + || (isdigit((unsigned char) lp) && !isdigit((unsigned char) *p) && '.' != *p) ) { if ('.' != lq) { *q++ = '.'; diff --git a/test/compliance/VersionCompareVMTest.php b/test/compliance/VersionCompareVMTest.php index 3f8f22225cd..55fcabfacac 100644 --- a/test/compliance/VersionCompareVMTest.php +++ b/test/compliance/VersionCompareVMTest.php @@ -17,6 +17,10 @@ public static function providePHPTests(): \Generator __DIR__.'/cases/stdlib/version_compare.phpt', 'version_compare.phpt' ); + yield 'version_compare_partial.phpt' => self::parsePHPT( + __DIR__.'/cases/stdlib/version_compare_partial.phpt', + 'version_compare_partial.phpt' + ); } public function setUp(): void diff --git a/test/compliance/cases/stdlib/version_compare_partial.phpt b/test/compliance/cases/stdlib/version_compare_partial.phpt new file mode 100644 index 00000000000..65bfd568695 --- /dev/null +++ b/test/compliance/cases/stdlib/version_compare_partial.phpt @@ -0,0 +1,11 @@ +--TEST-- +stdlib version_compare shorter operand parity (#4796) +--FILE-- +=') ? "ge\n" : "no\n"; +echo version_compare('8.2', '8.2.31', '<=') ? "le\n" : "no\n"; +echo version_compare('8.2.31', '8.2.0', '>=') ? "ge0\n" : "no\n"; +--EXPECT-- +ge +le +ge0 diff --git a/test/tmp/repro-version-compare-partial.php b/test/tmp/repro-version-compare-partial.php new file mode 100644 index 00000000000..c3715eaa580 --- /dev/null +++ b/test/tmp/repro-version-compare-partial.php @@ -0,0 +1,4 @@ +=')); +echo PHP_EOL;