diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 6cf86211..a43a3526 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -8,9 +8,9 @@ Regenerate: `php script/bootstrap-inventory.php` | Metric | Count | |--------|------:| -| PHP files on vm.php path | 238 | +| PHP files on vm.php path | 239 | | Source constructs flagged (blockers) | 10 | -| Source constructs flagged (warnings) | 626 | +| Source constructs flagged (warnings) | 628 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -58,7 +58,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `ext/standard/JitStripTags.php` | 0 | 1 | | `ext/standard/JitStrpos.php` | 0 | 1 | | `ext/standard/JitUrlencode.php` | 0 | 1 | -| `ext/standard/Module.php` | 0 | 109 | +| `ext/standard/Module.php` | 0 | 110 | | `ext/standard/VmDate.php` | 0 | 1 | | `ext/standard/VmExit.php` | 0 | 2 | | `ext/standard/VmFs.php` | 0 | 3 | @@ -159,6 +159,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `ext/standard/string_rtrim.php` | 0 | 1 | | `ext/standard/string_trim.php` | 0 | 1 | | `ext/standard/strip_tags.php` | 0 | 1 | +| `ext/standard/stripos.php` | 0 | 1 | | `ext/standard/strncmp.php` | 0 | 1 | | `ext/standard/strpos.php` | 0 | 1 | | `ext/standard/strrev.php` | 0 | 1 | @@ -449,64 +450,65 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new substr (line 69) - new strrev (line 70) - new strpos (line 71) -- new str_contains (line 72) -- new str_starts_with (line 73) -- new str_ends_with (line 74) -- new strncmp (line 75) -- new array_count (line 76) +- new stripos (line 72) +- new str_contains (line 73) +- new str_starts_with (line 74) +- new str_ends_with (line 75) +- new strncmp (line 76) - new array_count (line 77) -- new array_key_exists (line 78) -- new in_array (line 79) -- new array_push (line 80) -- new array_pop (line 81) -- new array_shift (line 82) -- new sort_ (line 83) -- new array_values (line 84) -- new array_keys (line 85) -- new array_merge (line 86) -- new array_slice (line 87) -- new explode (line 88) -- new implode (line 89) -- new str_replace (line 90) -- new nl2br (line 91) -- new array_reverse (line 92) -- new array_search (line 93) -- new array_sum (line 94) -- new array_flip (line 95) -- new array_unique (line 96) -- new array_fill (line 97) -- new array_combine (line 98) -- new range (line 99) -- new bin2hex (line 100) -- new hex2bin (line 101) -- new random_bytes (line 102) -- new str_pad (line 103) -- new str_split (line 104) -- new htmlspecialchars (line 105) -- new strip_tags (line 106) -- new header_ (line 107) -- new header_remove (line 108) -- new header_list (line 109) -- new getallheaders_ (line 110) -- new http_response_code (line 111) -- new urlencode (line 112) -- new rawurlencode (line 113) -- new urldecode (line 114) -- new rawurldecode (line 115) -- new parse_url (line 116) -- new dirname (line 117) -- new basename (line 118) -- new realpath (line 119) -- new file_get_contents (line 120) -- new getenv_ (line 121) -- new putenv_ (line 122) -- new extract_ (line 123) -- new compact_ (line 124) -- new scandir (line 125) -- new glob_ (line 126) -- new time (line 127) -- new date (line 128) -- new gmdate (line 129) +- new array_count (line 78) +- new array_key_exists (line 79) +- new in_array (line 80) +- new array_push (line 81) +- new array_pop (line 82) +- new array_shift (line 83) +- new sort_ (line 84) +- new array_values (line 85) +- new array_keys (line 86) +- new array_merge (line 87) +- new array_slice (line 88) +- new explode (line 89) +- new implode (line 90) +- new str_replace (line 91) +- new nl2br (line 92) +- new array_reverse (line 93) +- new array_search (line 94) +- new array_sum (line 95) +- new array_flip (line 96) +- new array_unique (line 97) +- new array_fill (line 98) +- new array_combine (line 99) +- new range (line 100) +- new bin2hex (line 101) +- new hex2bin (line 102) +- new random_bytes (line 103) +- new str_pad (line 104) +- new str_split (line 105) +- new htmlspecialchars (line 106) +- new strip_tags (line 107) +- new header_ (line 108) +- new header_remove (line 109) +- new header_list (line 110) +- new getallheaders_ (line 111) +- new http_response_code (line 112) +- new urlencode (line 113) +- new rawurlencode (line 114) +- new urldecode (line 115) +- new rawurldecode (line 116) +- new parse_url (line 117) +- new dirname (line 118) +- new basename (line 119) +- new realpath (line 120) +- new file_get_contents (line 121) +- new getenv_ (line 122) +- new putenv_ (line 123) +- new extract_ (line 124) +- new compact_ (line 125) +- new scandir (line 126) +- new glob_ (line 127) +- new time (line 128) +- new date (line 129) +- new gmdate (line 130) - 2 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `ext/standard/VmDate.php` @@ -544,7 +546,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: **Warnings** (review for bootstrap subset): - new Exception (line 182) - new Exception (line 190) -- 52 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +- 56 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler - 1 closure(s) ### `ext/standard/abs.php` @@ -1041,6 +1043,11 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: **Warnings** (review for bootstrap subset): - 2 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +### `ext/standard/stripos.php` + +**Warnings** (review for bootstrap subset): +- 2 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler + ### `ext/standard/strncmp.php` **Warnings** (review for bootstrap subset): diff --git a/docs/capabilities.md b/docs/capabilities.md index f589481e..36cce544 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -109,6 +109,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `str_starts_with` | yes | yes | yes | standard | AOT PHPT | | `strcmp` | yes | yes | yes | standard | | | `strip_tags` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | +| `stripos` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | | `strlen` | yes | yes | yes | types | JIT PHPT; AOT PHPT | | `strncmp` | yes | yes | yes | standard | | | `strpos` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | diff --git a/ext/standard/JitStrpos.php b/ext/standard/JitStrpos.php index cb3d7ec5..bd724b44 100644 --- a/ext/standard/JitStrpos.php +++ b/ext/standard/JitStrpos.php @@ -23,7 +23,8 @@ public static function find( Context $context, Value $haystack, Value $needle, - ?Value $offset = null + ?Value $offset = null, + bool $caseInsensitive = false ): Value { $map = $context->structFieldMap['__string__']; $hayLen = $context->builder->load( @@ -39,8 +40,9 @@ public static function find( $searchPtr = $context->builder->inBoundsGEP($hayPtr, $clamped); } + $searchFn = $caseInsensitive ? 'strcasestr' : 'strstr'; $found = $context->builder->call( - $context->lookupFunction('strstr'), + $context->lookupFunction($searchFn), $searchPtr, $needlePtr ); diff --git a/ext/standard/Module.php b/ext/standard/Module.php index 45af7674..fbd47170 100755 --- a/ext/standard/Module.php +++ b/ext/standard/Module.php @@ -69,6 +69,7 @@ public function getFunctions(): array new substr(), new strrev(), new strpos(), + new stripos(), new str_contains(), new str_starts_with(), new str_ends_with(), @@ -159,6 +160,14 @@ public function jitInit(JIT\Context $context): void $fn = $context->module->addFunction('strstr', $ft); $context->registerFunction('strstr', $fn); } + try { + $context->lookupFunction('strcasestr'); + } catch (\Throwable $e) { + $i8p = $context->getTypeFromString('int8*'); + $ft = $context->context->functionType($i8p, false, $i8p, $i8p); + $fn = $context->module->addFunction('strcasestr', $ft); + $context->registerFunction('strcasestr', $fn); + } try { $context->lookupFunction('strtol'); } catch (\Throwable $e) { diff --git a/ext/standard/VmString.php b/ext/standard/VmString.php index 3e39e530..0861d74f 100644 --- a/ext/standard/VmString.php +++ b/ext/standard/VmString.php @@ -799,6 +799,22 @@ public static function strpos(string $haystack, string $needle, int $offset = 0) return false === $pos ? false : $pos; } + /** + * @return int|false + */ + public static function stripos(string $haystack, string $needle, int $offset = 0) + { + if ('' === $needle) { + throw new \LogicException('stripos(): Argument #2 ($needle) cannot be empty'); + } + if ($offset < 0) { + $offset = 0; + } + $pos = self::findSubstringCaseInsensitive($haystack, $needle, $offset); + + return false === $pos ? false : $pos; + } + public static function startsWith(string $haystack, string $needle): bool { $nlen = self::byteLength($needle); @@ -850,6 +866,27 @@ private static function compareBytes(string $haystack, string $needle, int $leng return true; } + private static function compareBytesCaseInsensitive(string $haystack, string $needle, int $length, int $hayOffset = 0): bool + { + for ($i = 0; $i < $length; ++$i) { + if (self::asciiLowerByte($haystack[$hayOffset + $i]) !== self::asciiLowerByte($needle[$i])) { + return false; + } + } + + return true; + } + + private static function asciiLowerByte(string $byte): string + { + $ord = self::byteOrd($byte); + if ($ord >= 65 && $ord <= 90) { + return self::byteChr($ord + 32); + } + + return $byte; + } + /** * @return int|false */ @@ -873,6 +910,29 @@ private static function findSubstring(string $haystack, string $needle, int $off return false; } + /** + * @return int|false + */ + private static function findSubstringCaseInsensitive(string $haystack, string $needle, int $offset) + { + $hayLen = self::byteLength($haystack); + $needleLen = self::byteLength($needle); + if (0 === $needleLen) { + return false; + } + if ($offset >= $hayLen) { + return false; + } + $limit = $hayLen - $needleLen; + for ($i = $offset; $i <= $limit; ++$i) { + if (self::compareBytesCaseInsensitive($haystack, $needle, $needleLen, $i)) { + return $i; + } + } + + return false; + } + private static function asciiCaseTransform(string $string, bool $toLower): string { $out = ''; diff --git a/ext/standard/stripos.php b/ext/standard/stripos.php new file mode 100644 index 00000000..af65377a --- /dev/null +++ b/ext/standard/stripos.php @@ -0,0 +1,73 @@ +calledArgs); + if ($argc < 2 || $argc > 3) { + throw new \LogicException('stripos() requires two or three arguments'); + } + $haystack = $frame->calledArgs[0]->resolveIndirect(); + $needle = $frame->calledArgs[1]->resolveIndirect(); + if (null === $frame->returnVar) { + return; + } + if (Variable::TYPE_STRING !== $haystack->type || Variable::TYPE_STRING !== $needle->type) { + throw new \LogicException('stripos() only supports strings in this compiler build'); + } + $offset = 0; + if (3 === $argc) { + $offVar = $frame->calledArgs[2]->resolveIndirect(); + if (Variable::TYPE_INTEGER !== $offVar->type) { + throw new \LogicException('stripos() offset must be an integer in this compiler build'); + } + $offset = $offVar->toInt(); + } + $result = VmString::stripos($haystack->toString(), $needle->toString(), $offset); + if (false === $result) { + $frame->returnVar->bool(false); + } else { + $frame->returnVar->int($result); + } + } + + public Context $context; + + public function call(Context $context, JITVariable ...$args): Value + { + $this->context = $context; + $argc = count($args); + if ($argc < 2 || $argc > 3) { + throw new \LogicException('stripos() requires two or three arguments'); + } + if (JITVariable::TYPE_STRING !== $args[0]->type || JITVariable::TYPE_STRING !== $args[1]->type) { + throw new \LogicException('stripos() only supports strings in this compiler build'); + } + if (3 === $argc && JITVariable::TYPE_NATIVE_LONG !== $args[2]->type) { + throw new \LogicException('stripos() offset must be an integer in this compiler build'); + } + + $hay = $context->helper->loadValue($args[0]); + $needle = $context->helper->loadValue($args[1]); + $offset = 3 === $argc + ? $context->builder->truncOrBitCast($context->helper->loadValue($args[2]), $context->getTypeFromString('int64')) + : null; + + return JitStrpos::find($context, $hay, $needle, $offset, true); + } +} diff --git a/test/compliance/cases/stdlib/stripos.phpt b/test/compliance/cases/stdlib/stripos.phpt new file mode 100644 index 00000000..72951138 --- /dev/null +++ b/test/compliance/cases/stdlib/stripos.phpt @@ -0,0 +1,11 @@ +--TEST-- +stdlib stripos() +--FILE-- +