diff --git a/ext/standard/JitIncludePath.php b/ext/standard/JitIncludePath.php new file mode 100644 index 00000000000..fb040537bc9 --- /dev/null +++ b/ext/standard/JitIncludePath.php @@ -0,0 +1,40 @@ +builder->call($context->lookupFunction('__compiler_get_include_path'), $ptr); + + return $ptr; + } + + public static function set(Context $context, Value $newPath): Value + { + IncludePathRuntime::ensureLinked($context); + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + $context->builder->call($context->lookupFunction('__compiler_set_include_path'), $newPath, $ptr); + + return $ptr; + } + + public static function restore(Context $context): void + { + IncludePathRuntime::ensureLinked($context); + $context->builder->call($context->lookupFunction('__compiler_restore_include_path')); + } +} diff --git a/ext/standard/JitResolveIncludePath.php b/ext/standard/JitResolveIncludePath.php new file mode 100644 index 00000000000..ea5d55abaaf --- /dev/null +++ b/ext/standard/JitResolveIncludePath.php @@ -0,0 +1,46 @@ +builder->call( + $context->lookupFunction('__compiler_stream_resolve_include_path'), + $filename + ); + $strPtrTy = $context->getTypeFromString('__string__*'); + $isFalse = $context->builder->icmp(Builder::INT_EQ, $resolved, $strPtrTy->constNull()); + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + $failBlock = BasicBlockHelper::append($context, 'resolve_inc_fail'); + $okBlock = BasicBlockHelper::append($context, 'resolve_inc_ok'); + $doneBlock = BasicBlockHelper::append($context, 'resolve_inc_done'); + $context->builder->branchIf($isFalse, $failBlock, $okBlock); + + $context->builder->positionAtEnd($failBlock); + $i1 = $context->getTypeFromString('int1'); + JitValueBox::writeBool($context, $slot, $i1->constInt(0, false)); + $context->builder->branch($doneBlock); + + $context->builder->positionAtEnd($okBlock); + $context->builder->call($context->lookupFunction('__value__writeString'), $ptr, $resolved); + $context->builder->branch($doneBlock); + + $context->builder->positionAtEnd($doneBlock); + + return $ptr; + } +} diff --git a/ext/standard/Module.php b/ext/standard/Module.php index f266b781858..1a604b09064 100755 --- a/ext/standard/Module.php +++ b/ext/standard/Module.php @@ -495,6 +495,10 @@ public function getFunctions(): array new tempnam(), new tmpfile(), new getcwd_(), + new get_include_path(), + new set_include_path(), + new restore_include_path(), + new stream_resolve_include_path(), new gethostname(), new gethostbynamel(), new gethostbyname(), diff --git a/ext/standard/VmFs.php b/ext/standard/VmFs.php index 1c0a8795a8d..95f3bb360c8 100644 --- a/ext/standard/VmFs.php +++ b/ext/standard/VmFs.php @@ -5,6 +5,7 @@ namespace PHPCompiler\ext\standard; use PHPCompiler\VM\HashTable; +use PHPCompiler\VM\ScriptStack; use PHPCompiler\VM\Variable; /** Host filesystem helpers for stdlib builtins (VM). */ @@ -453,6 +454,38 @@ public static function fileGetContents(string $path) { return $data; } + /** + * stream_resolve_include_path() — search include_path for filename (ext/standard/streams.c; #6051). + * + * @return string|false absolute path when found + */ + public static function resolveIncludePath(string $filename): string|false + { + if ('' === $filename || str_contains($filename, "\0")) { + return false; + } + if ($filename[0] === '/' || (\strlen($filename) > 1 && $filename[1] === ':')) { + $normalized = ScriptStack::normalize($filename); + + return '' !== $normalized && \is_file($normalized) ? $normalized : false; + } + $includePath = VmIncludePath::get(); + if ('' === $includePath) { + return false; + } + foreach (\explode(\PATH_SEPARATOR, $includePath) as $dir) { + if ('' === $dir) { + continue; + } + $candidate = ScriptStack::normalize(\rtrim($dir, '/\\').'/'.$filename); + if ('' !== $candidate && \is_file($candidate)) { + return $candidate; + } + } + + return false; + } + /** * @return list|false */ diff --git a/ext/standard/VmIncludePath.php b/ext/standard/VmIncludePath.php new file mode 100644 index 00000000000..3b2a7277376 --- /dev/null +++ b/ext/standard/VmIncludePath.php @@ -0,0 +1,47 @@ + */ + private static array $stack = ['.']; + + public static function get(): string + { + return self::$stack[\count(self::$stack) - 1]; + } + + /** @return string previous include_path */ + public static function set(string $newPath): string + { + $old = self::get(); + self::$stack[\count(self::$stack) - 1] = $newPath; + + return $old; + } + + public static function restore(): void + { + if (\count(self::$stack) > 1) { + \array_pop(self::$stack); + } + } + + /** Push a new include_path while preserving the previous value on the stack. */ + public static function push(string $newPath): string + { + $old = self::get(); + self::$stack[] = $newPath; + + return $old; + } +} diff --git a/ext/standard/VmMetaTags.php b/ext/standard/VmMetaTags.php index 61ecc7669e2..050ff9e8dd3 100644 --- a/ext/standard/VmMetaTags.php +++ b/ext/standard/VmMetaTags.php @@ -16,7 +16,7 @@ final class VmMetaTags */ public static function getMetaTags(string $filename, bool $useIncludePath = false) { if ($useIncludePath) { - $resolved = stream_resolve_include_path($filename); + $resolved = VmFs::resolveIncludePath($filename); if (false !== $resolved) { $filename = $resolved; } diff --git a/ext/standard/get_include_path.php b/ext/standard/get_include_path.php new file mode 100644 index 00000000000..52b16f913e8 --- /dev/null +++ b/ext/standard/get_include_path.php @@ -0,0 +1,44 @@ +calledArgs) > 0) { + throw new \ArgumentCountError( + 'get_include_path() expects exactly 0 arguments, '.\count($frame->calledArgs).' given' + ); + } + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->string(VmIncludePath::get()); + } + + public function call(Context $context, JITVariable ...$args): Value + { + if (\count($args) > 0) { + throw new \ArgumentCountError( + 'get_include_path() expects exactly 0 arguments, '.\count($args).' given' + ); + } + + return JitIncludePath::get($context); + } +} diff --git a/ext/standard/restore_include_path.php b/ext/standard/restore_include_path.php new file mode 100644 index 00000000000..2ef24ca359c --- /dev/null +++ b/ext/standard/restore_include_path.php @@ -0,0 +1,42 @@ +calledArgs) > 0) { + throw new \ArgumentCountError( + 'restore_include_path() expects exactly 0 arguments, '.\count($frame->calledArgs).' given' + ); + } + VmIncludePath::restore(); + } + + public function call(Context $context, JITVariable ...$args): Value + { + if (\count($args) > 0) { + throw new \ArgumentCountError( + 'restore_include_path() expects exactly 0 arguments, '.\count($args).' given' + ); + } + JitIncludePath::restore($context); + + return $context->getTypeFromString('int32')->constInt(0, false); + } +} diff --git a/ext/standard/set_include_path.php b/ext/standard/set_include_path.php new file mode 100644 index 00000000000..f9eb0a9721a --- /dev/null +++ b/ext/standard/set_include_path.php @@ -0,0 +1,59 @@ +calledArgs)) { + throw new \ArgumentCountError( + 'set_include_path() expects exactly 1 argument, '.\count($frame->calledArgs).' given' + ); + } + if (null === $frame->returnVar) { + return; + } + $newPath = VmString::coerceStringBuiltinArg( + $frame->calledArgs[0], + 'set_include_path', + 0, + 'new_include_path' + ); + $old = VmIncludePath::push($newPath); + $frame->returnVar->string($old); + } + + public function call(Context $context, JITVariable ...$args): Value + { + if (1 !== \count($args)) { + throw new \ArgumentCountError( + 'set_include_path() expects exactly 1 argument, '.\count($args).' given' + ); + } + $newPath = JitStringBuiltinArg::lower( + $context, + $args[0], + 'set_include_path', + 0, + 'new_include_path' + ); + + return JitIncludePath::set($context, $newPath); + } +} diff --git a/ext/standard/stream_resolve_include_path.php b/ext/standard/stream_resolve_include_path.php new file mode 100644 index 00000000000..c7e06d4540e --- /dev/null +++ b/ext/standard/stream_resolve_include_path.php @@ -0,0 +1,64 @@ +calledArgs)) { + throw new \ArgumentCountError( + 'stream_resolve_include_path() expects exactly 1 argument, '.\count($frame->calledArgs).' given' + ); + } + if (null === $frame->returnVar) { + return; + } + $filename = VmString::coerceStringBuiltinArg( + $frame->calledArgs[0], + 'stream_resolve_include_path', + 0, + 'filename' + ); + $resolved = VmFs::resolveIncludePath($filename); + if (false === $resolved) { + $frame->returnVar->bool(false); + } else { + $frame->returnVar->string($resolved); + } + } + + public function call(Context $context, JITVariable ...$args): Value + { + if (1 !== \count($args)) { + throw new \ArgumentCountError( + 'stream_resolve_include_path() expects exactly 1 argument, '.\count($args).' given' + ); + } + $filename = JitStringBuiltinArg::lower( + $context, + $args[0], + 'stream_resolve_include_path', + 0, + 'filename' + ); + + return JitResolveIncludePath::invoke($context, $filename); + } +} diff --git a/lib/JIT/Builtin/IncludePathRuntime.php b/lib/JIT/Builtin/IncludePathRuntime.php new file mode 100644 index 00000000000..3ea6004fe29 --- /dev/null +++ b/lib/JIT/Builtin/IncludePathRuntime.php @@ -0,0 +1,427 @@ +getTypeFromString('int8*'); + $i32 = $context->getTypeFromString('int32'); + $i64 = $context->getTypeFromString('int64'); + $sizeT = $context->getTypeFromString('size_t'); + foreach ( + [ + 'access' => [$i32, false, [$i8p, $i32]], + 'realpath' => [$i8p, false, [$i8p, $i8p]], + 'strlen' => [$sizeT, false, [$i8p]], + 'strchr' => [$i8p, false, [$i8p, $i32]], + 'memcpy' => [$i8p, false, [$i8p, $i8p, $sizeT]], + ] as $name => [$ret, $vararg, $params] + ) { + self::ensureExternalFunction( + $context, + $name, + $context->context->functionType($ret, $vararg, ...$params) + ); + } + } + + private static function ensureExternalFunction(Context $context, string $name, $signature): void + { + if (null === $context->module->getNamedFunction($name)) { + $context->module->addFunction($name, $signature); + } + $context->registerFunction($name, $context->module->getNamedFunction($name)); + } + + private static function implementGlobals(Context $context): void + { + if (null !== $context->module->getNamedGlobal('phpc_include_path_depth')) { + return; + } + $i32 = $context->getTypeFromString('int32'); + $i8 = $context->getTypeFromString('int8'); + $depth = $context->module->addGlobal($i32, 'phpc_include_path_depth'); + $depth->setInitializer($i32->constInt(1, false)); + + $pathTy = $i8->arrayType(self::MAX_PATH); + $context->module->addGlobal($pathTy, 'phpc_include_path_current'); + + $stackTy = $i8->arrayType(self::MAX_PATH * self::MAX_STACK); + $context->module->addGlobal($stackTy, 'phpc_include_path_stack'); + + self::implementInitDefaultPath($context); + } + + /** Zend default include_path "." when unset (php.ini / PG(include_path)). */ + private static function implementInitDefaultPath(Context $context): void + { + $probe = $context->module->getNamedFunction('__compiler_include_path_init'); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction('__compiler_include_path_init', $probe); + + return; + } + $fn = $context->lookupFunction('__compiler_include_path_init'); + $entry = $fn->appendBasicBlock('entry'); + $context->builder->positionAtEnd($entry); + $i8 = $context->getTypeFromString('int8'); + $i8p = $context->getTypeFromString('int8*'); + $current = $context->module->getNamedGlobal('phpc_include_path_current'); + $i64 = $context->getTypeFromString('int64'); + $zero = $i64->constInt(0, false); + $ptr = $context->builder->pointerCast( + $context->builder->gep($current, $zero, $zero), + $i8p + ); + $first = $context->builder->load($ptr); + $needsInit = $context->builder->icmp(Builder::INT_EQ, $first, $i8->constInt(0, false)); + $initBlock = $fn->appendBasicBlock('init'); + $doneBlock = $fn->appendBasicBlock('done'); + $context->builder->branchIf($needsInit, $initBlock, $doneBlock); + $context->builder->positionAtEnd($initBlock); + $context->builder->store($i8->constInt(ord('.'), false), $ptr); + $context->builder->branch($doneBlock); + $context->builder->positionAtEnd($doneBlock); + $context->builder->returnVoid(); + $context->builder->clearInsertionPosition(); + } + + private static function implementGet(Context $context): void + { + $probe = $context->module->getNamedFunction('__compiler_get_include_path'); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction('__compiler_get_include_path', $probe); + + return; + } + $fn = $context->lookupFunction('__compiler_get_include_path'); + $entry = $fn->appendBasicBlock('entry'); + $context->builder->positionAtEnd($entry); + $context->builder->call($context->lookupFunction('__compiler_include_path_init')); + $out = $fn->getParam(0); + $i8 = $context->getTypeFromString('int8'); + $i8p = $context->getTypeFromString('int8*'); + $i64 = $context->getTypeFromString('int64'); + $current = $context->module->getNamedGlobal('phpc_include_path_current'); + $zero = $i64->constInt(0, false); + $currentPtr = $context->builder->pointerCast( + $context->builder->gep($current, $zero, $zero), + $i8p + ); + $len = $context->builder->call($context->lookupFunction('strlen'), $currentPtr); + $lenI64 = $context->builder->zExt($len, $i64); + $str = $context->builder->call($context->lookupFunction('__string__init'), $lenI64, $currentPtr); + $context->builder->call($context->lookupFunction('__value__writeString'), $out, $str); + $context->builder->returnVoid(); + $context->builder->clearInsertionPosition(); + } + + private static function implementSet(Context $context): void + { + $probe = $context->module->getNamedFunction('__compiler_set_include_path'); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction('__compiler_set_include_path', $probe); + + return; + } + $fn = $context->lookupFunction('__compiler_set_include_path'); + $entry = $fn->appendBasicBlock('entry'); + $context->builder->positionAtEnd($entry); + $newPath = $fn->getParam(0); + $out = $fn->getParam(1); + $strMap = $context->structFieldMap['__string__']; + $i8 = $context->getTypeFromString('int8'); + $i8p = $context->getTypeFromString('int8*'); + $i32 = $context->getTypeFromString('int32'); + $i64 = $context->getTypeFromString('int64'); + $sizeT = $context->getTypeFromString('size_t'); + + $depthGlobal = $context->module->getNamedGlobal('phpc_include_path_depth'); + $depth = $context->builder->load($depthGlobal); + $current = $context->module->getNamedGlobal('phpc_include_path_current'); + $zero = $i64->constInt(0, false); + $currentPtr = $context->builder->pointerCast( + $context->builder->gep($current, $zero, $zero), + $i8p + ); + $oldLen = $context->builder->call($context->lookupFunction('strlen'), $currentPtr); + $oldLenI64 = $context->builder->zExt($oldLen, $i64); + $oldStr = $context->builder->call($context->lookupFunction('__string__init'), $oldLenI64, $currentPtr); + $context->builder->call($context->lookupFunction('__value__writeString'), $out, $oldStr); + + $stack = $context->module->getNamedGlobal('phpc_include_path_stack'); + $slotIndex = $context->builder->mul( + $context->builder->zExt($depth, $i64), + $i64->constInt(self::MAX_PATH, false) + ); + $slotPtr = $context->builder->pointerCast( + $context->builder->gep($stack, $zero, $slotIndex), + $i8p + ); + $context->builder->call( + $context->lookupFunction('memcpy'), + $slotPtr, + $currentPtr, + $sizeT->constInt(self::MAX_PATH, false) + ); + $context->builder->store( + $context->builder->add($depth, $i32->constInt(1, false)), + $depthGlobal + ); + + $newLen = $context->builder->load($context->builder->structGep($newPath, $strMap['length'])); + $newBytes = $context->builder->structGep($newPath, $strMap['value']); + $maxCopy = $i64->constInt(self::MAX_PATH - 1, false); + $copyLen = $context->builder->select( + $context->builder->icmp(Builder::INT_ULT, $newLen, $maxCopy), + $newLen, + $maxCopy + ); + $context->builder->call( + $context->lookupFunction('memcpy'), + $currentPtr, + $newBytes, + $context->builder->truncOrBitCast($copyLen, $sizeT) + ); + $context->builder->store($i8->constInt(0, false), $context->builder->inBoundsGEP($currentPtr, $copyLen)); + $context->builder->returnVoid(); + $context->builder->clearInsertionPosition(); + } + + private static function implementRestore(Context $context): void + { + $probe = $context->module->getNamedFunction('__compiler_restore_include_path'); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction('__compiler_restore_include_path', $probe); + + return; + } + $fn = $context->lookupFunction('__compiler_restore_include_path'); + $entry = $fn->appendBasicBlock('entry'); + $context->builder->positionAtEnd($entry); + $i8p = $context->getTypeFromString('int8*'); + $i32 = $context->getTypeFromString('int32'); + $i64 = $context->getTypeFromString('int64'); + $sizeT = $context->getTypeFromString('size_t'); + + $depthGlobal = $context->module->getNamedGlobal('phpc_include_path_depth'); + $depth = $context->builder->load($depthGlobal); + $canPop = $context->builder->icmp(Builder::INT_SGT, $depth, $i32->constInt(1, false)); + $done = $fn->appendBasicBlock('done'); + $pop = $fn->appendBasicBlock('pop'); + $context->builder->branchIf($canPop, $pop, $done); + + $context->builder->positionAtEnd($pop); + $newDepth = $context->builder->sub($depth, $i32->constInt(1, false)); + $context->builder->store($newDepth, $depthGlobal); + $stack = $context->module->getNamedGlobal('phpc_include_path_stack'); + $zero = $i64->constInt(0, false); + $slotIndex = $context->builder->mul( + $context->builder->zExt($newDepth, $i64), + $i64->constInt(self::MAX_PATH, false) + ); + $slotPtr = $context->builder->pointerCast( + $context->builder->gep($stack, $zero, $slotIndex), + $i8p + ); + $current = $context->module->getNamedGlobal('phpc_include_path_current'); + $currentPtr = $context->builder->pointerCast( + $context->builder->gep($current, $zero, $zero), + $i8p + ); + $context->builder->call( + $context->lookupFunction('memcpy'), + $currentPtr, + $slotPtr, + $sizeT->constInt(self::MAX_PATH, false) + ); + $context->builder->branch($done); + + $context->builder->positionAtEnd($done); + $context->builder->returnVoid(); + $context->builder->clearInsertionPosition(); + } + + private static function implementResolve(Context $context): void + { + $probe = $context->module->getNamedFunction('__compiler_stream_resolve_include_path'); + if (null !== $probe && $probe->countBasicBlocks() > 0) { + $context->registerFunction('__compiler_stream_resolve_include_path', $probe); + + return; + } + $fn = $context->lookupFunction('__compiler_stream_resolve_include_path'); + $entry = $fn->appendBasicBlock('entry'); + $context->builder->positionAtEnd($entry); + $context->builder->call($context->lookupFunction('__compiler_include_path_init')); + + $filename = $fn->getParam(0); + $strMap = $context->structFieldMap['__string__']; + $strPtrTy = $context->getTypeFromString('__string__*'); + $i8 = $context->getTypeFromString('int8'); + $i8p = $context->getTypeFromString('int8*'); + $i32 = $context->getTypeFromString('int32'); + $i64 = $context->getTypeFromString('int64'); + $sizeT = $context->getTypeFromString('size_t'); + $nullStr = $strPtrTy->constNull(); + $zeroI32 = $i32->constInt(0, false); + + $nameLen = $context->builder->load($context->builder->structGep($filename, $strMap['length'])); + $nameBytes = $context->builder->structGep($filename, $strMap['value']); + $empty = $context->builder->icmp(Builder::INT_EQ, $nameLen, $i64->constInt(0, false)); + $failBlock = BasicBlockHelper::append($context, 'resolve_fail'); + $absBlock = BasicBlockHelper::append($context, 'resolve_abs'); + $context->builder->branchIf($empty, $failBlock, $absBlock); + + $pathBuf = $context->builder->alloca($i8, $i64->constInt(self::MAX_PATH, false), 'resolve_path'); + $pathCStr = $context->builder->pointerCast($pathBuf, $i8p); + $realBuf = $context->builder->alloca($i8, $i64->constInt(self::MAX_PATH, false), 'resolve_real'); + $realCStr = $context->builder->pointerCast($realBuf, $i8p); + + $context->builder->positionAtEnd($absBlock); + $first = $context->builder->load($nameBytes); + $isAbs = $context->builder->icmp(Builder::INT_EQ, $first, $i8->constInt(ord('/'), false)); + $absTry = BasicBlockHelper::append($context, 'resolve_abs_try'); + $searchBlock = BasicBlockHelper::append($context, 'resolve_search'); + $context->builder->branchIf($isAbs, $absTry, $searchBlock); + + $context->builder->positionAtEnd($absTry); + $context->builder->call( + $context->lookupFunction('memcpy'), + $pathCStr, + $nameBytes, + $context->builder->truncOrBitCast($nameLen, $sizeT) + ); + $context->builder->store($i8->constInt(0, false), $context->builder->inBoundsGEP($pathCStr, $nameLen)); + $resolved = $context->builder->call($context->lookupFunction('realpath'), $pathCStr, $realCStr); + $resolvedNull = $context->builder->icmp(Builder::INT_EQ, $resolved, $i8p->constNull()); + $absOk = BasicBlockHelper::append($context, 'resolve_abs_ok'); + $context->builder->branchIf($resolvedNull, $failBlock, $absOk); + $context->builder->positionAtEnd($absOk); + $context->builder->returnValue(self::stringFromCstr($context, $resolved)); + + $context->builder->positionAtEnd($searchBlock); + $current = $context->module->getNamedGlobal('phpc_include_path_current'); + $zero = $i64->constInt(0, false); + $includePtr = $context->builder->pointerCast( + $context->builder->gep($current, $zero, $zero), + $i8p + ); + $idxSlot = $context->builder->alloca($i64, 1, 'resolve_idx'); + $context->builder->store($zero, $idxSlot); + $incLen = $context->builder->call($context->lookupFunction('strlen'), $includePtr); + $incLenI64 = $context->builder->zExt($incLen, $i64); + $loopHead = BasicBlockHelper::append($context, 'resolve_loop'); + $context->builder->branch($loopHead); + + $context->builder->positionAtEnd($loopHead); + $idx = $context->builder->load($idxSlot); + $pastEnd = $context->builder->icmp(Builder::INT_SGE, $idx, $incLenI64); + $dirBlock = BasicBlockHelper::append($context, 'resolve_dir'); + $context->builder->branchIf($pastEnd, $failBlock, $dirBlock); + + $context->builder->positionAtEnd($dirBlock); + $dirStart = $context->builder->gep($includePtr, $idx); + $sepPtr = $context->builder->call( + $context->lookupFunction('strchr'), + $dirStart, + $i32->constInt(ord(':'), false) + ); + $sepNull = $context->builder->icmp(Builder::INT_EQ, $sepPtr, $i8p->constNull()); + $dirEnd = $context->builder->select( + $sepNull, + $context->builder->gep($includePtr, $incLenI64), + $sepPtr + ); + $dirLen = $context->builder->sub( + $context->builder->ptrToInt($dirEnd, $i64), + $context->builder->ptrToInt($dirStart, $i64) + ); + $context->builder->call( + $context->lookupFunction('memcpy'), + $pathCStr, + $dirStart, + $context->builder->truncOrBitCast($dirLen, $sizeT) + ); + $context->builder->store($i8->constInt(ord('/'), false), $context->builder->gep($pathCStr, $dirLen)); + $afterSlash = $context->builder->add($dirLen, $i64->constInt(1, false)); + $context->builder->call( + $context->lookupFunction('memcpy'), + $context->builder->gep($pathCStr, $afterSlash), + $nameBytes, + $context->builder->truncOrBitCast($nameLen, $sizeT) + ); + $totalLen = $context->builder->add($afterSlash, $nameLen); + $context->builder->store($i8->constInt(0, false), $context->builder->gep($pathCStr, $totalLen)); + + $exists = $context->builder->call( + $context->lookupFunction('access'), + $pathCStr, + $i32->constInt(self::ACCESS_F_OK, false) + ); + $found = $context->builder->icmp(Builder::INT_EQ, $exists, $zeroI32); + $tryReal = BasicBlockHelper::append($context, 'resolve_try_real'); + $nextDir = BasicBlockHelper::append($context, 'resolve_next'); + $context->builder->branchIf($found, $tryReal, $nextDir); + + $context->builder->positionAtEnd($tryReal); + $resolved = $context->builder->call($context->lookupFunction('realpath'), $pathCStr, $realCStr); + $resolvedNull = $context->builder->icmp(Builder::INT_EQ, $resolved, $i8p->constNull()); + $foundOk = BasicBlockHelper::append($context, 'resolve_found'); + $context->builder->branchIf($resolvedNull, $nextDir, $foundOk); + $context->builder->positionAtEnd($foundOk); + $context->builder->returnValue(self::stringFromCstr($context, $resolved)); + + $context->builder->positionAtEnd($nextDir); + $nextIdx = $context->builder->select( + $sepNull, + $context->builder->add($incLenI64, $i64->constInt(1, false)), + $context->builder->add( + $idx, + $context->builder->add($dirLen, $i64->constInt(1, false)) + ) + ); + $context->builder->store($nextIdx, $idxSlot); + $context->builder->branch($loopHead); + + $context->builder->positionAtEnd($failBlock); + $context->builder->returnValue($nullStr); + $context->builder->clearInsertionPosition(); + } + + private static function stringFromCstr(Context $context, $cstr) + { + $i64 = $context->getTypeFromString('int64'); + $len = $context->builder->call($context->lookupFunction('strlen'), $cstr); + + return $context->builder->call( + $context->lookupFunction('__string__init'), + $context->builder->zExt($len, $i64), + $cstr + ); + } +} diff --git a/lib/JIT/Builtin/Type.php b/lib/JIT/Builtin/Type.php index 0d16dfed12a..b2ed67fedb2 100755 --- a/lib/JIT/Builtin/Type.php +++ b/lib/JIT/Builtin/Type.php @@ -229,6 +229,50 @@ public function register(): void { $fntypeFileGetContents ); $this->context->registerFunction('__compiler_file_get_contents', $fnFileGetContents); + $fntypeIncludePathGet = $this->context->context->functionType( + $this->context->getTypeFromString('void'), + false, + $this->context->getTypeFromString('__value__*') + ); + $fnIncludePathGet = $this->context->module->addFunction( + '__compiler_get_include_path', + $fntypeIncludePathGet + ); + $this->context->registerFunction('__compiler_get_include_path', $fnIncludePathGet); + $fntypeIncludePathSet = $this->context->context->functionType( + $this->context->getTypeFromString('void'), + false, + $this->context->getTypeFromString('__string__*'), + $this->context->getTypeFromString('__value__*') + ); + $fnIncludePathSet = $this->context->module->addFunction( + '__compiler_set_include_path', + $fntypeIncludePathSet + ); + $this->context->registerFunction('__compiler_set_include_path', $fnIncludePathSet); + $fntypeIncludePathRestore = $this->context->context->functionType( + $this->context->getTypeFromString('void'), + false + ); + $fnIncludePathRestore = $this->context->module->addFunction( + '__compiler_restore_include_path', + $fntypeIncludePathRestore + ); + $this->context->registerFunction('__compiler_restore_include_path', $fnIncludePathRestore); + $fnStreamResolveIncludePath = $this->context->module->addFunction( + '__compiler_stream_resolve_include_path', + $fntypeFileGetContents + ); + $this->context->registerFunction('__compiler_stream_resolve_include_path', $fnStreamResolveIncludePath); + $fntypeIncludePathInit = $this->context->context->functionType( + $this->context->getTypeFromString('void'), + false + ); + $fnIncludePathInit = $this->context->module->addFunction( + '__compiler_include_path_init', + $fntypeIncludePathInit + ); + $this->context->registerFunction('__compiler_include_path_init', $fnIncludePathInit); $fntypeMimeContentType = $this->context->context->functionType( $this->context->getTypeFromString('__string__*'), false, @@ -1044,6 +1088,7 @@ public function initialize(): void { WeakRefRegistryRuntime::ensureLinked($this->context); MemoryRuntime::ensureLinked($this->context); IniRuntime::ensureLinked($this->context); + IncludePathRuntime::ensureLinked($this->context); StringEnvLocal::ensureLinked($this->context); ErrorHandlerOutput::registerExternals($this->context); StringTriggerError::ensureLinked($this->context); diff --git a/lib/JIT/SelfHostBuiltinPolicy.php b/lib/JIT/SelfHostBuiltinPolicy.php index e1696dfc0af..cd11b892ca6 100644 --- a/lib/JIT/SelfHostBuiltinPolicy.php +++ b/lib/JIT/SelfHostBuiltinPolicy.php @@ -155,6 +155,8 @@ final class SelfHostBuiltinPolicy 'is_uploaded_file' => 'filesystem', 'move_uploaded_file' => 'filesystem', 'touch' => 'filesystem', 'getenv' => 'filesystem', 'putenv' => 'filesystem', 'sys_get_temp_dir' => 'filesystem', 'tempnam' => 'filesystem', 'tmpfile' => 'filesystem', 'getcwd' => 'filesystem', 'chdir' => 'filesystem', 'gethostname' => 'filesystem', + 'get_include_path' => 'filesystem', 'set_include_path' => 'filesystem', + 'restore_include_path' => 'filesystem', 'stream_resolve_include_path' => 'filesystem', 'get_current_user' => 'filesystem', 'gethostbynamel' => 'filesystem', 'gethostbyname' => 'filesystem', diff --git a/lib/VM.php b/lib/VM.php index fb63d000ea2..cf5b4116a5d 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -5984,18 +5984,16 @@ private function resolveIncludeFilename(string $file, Frame $frame): ?string } } - // 3) include_path search (best-effort using host get_include_path when available) - if (\function_exists('get_include_path')) { - $includePath = (string) @get_include_path(); - if ('' !== $includePath) { - foreach (explode(\PATH_SEPARATOR, $includePath) as $dir) { - if ('' === $dir) { - continue; - } - $cand = VM\ScriptStack::normalize(rtrim($dir, '/').'/'.$file); - if ('' !== $cand && is_file($cand)) { - return $cand; - } + // 3) include_path search (VmIncludePath stack; issues #3223, #6051) + $includePath = \PHPCompiler\ext\standard\VmIncludePath::get(); + if ('' !== $includePath) { + foreach (explode(\PATH_SEPARATOR, $includePath) as $dir) { + if ('' === $dir) { + continue; + } + $cand = VM\ScriptStack::normalize(rtrim($dir, '/').'/'.$file); + if ('' !== $cand && is_file($cand)) { + return $cand; } } } diff --git a/test/compliance/cases/stdlib/stream_resolve_include_path.phpt b/test/compliance/cases/stdlib/stream_resolve_include_path.phpt new file mode 100644 index 00000000000..1ee063ea804 --- /dev/null +++ b/test/compliance/cases/stdlib/stream_resolve_include_path.phpt @@ -0,0 +1,24 @@ +--TEST-- +stream_resolve_include_path() resolves via include_path (issue #6051) +--FILE-- +vmContext; + $this->assertTrue(VmReflection::functionExists($ctx, 'stream_resolve_include_path')); + $this->assertTrue(VmReflection::functionExists($ctx, 'get_include_path')); + $this->assertTrue(VmReflection::functionExists($ctx, 'set_include_path')); + } + + public function testEnumCaseOperandTypeError(): void + { + $runtime = new Runtime(); + $fn = new stream_resolve_include_path(); + $enum = new ClassEntry('E'); + $enum->isEnum = true; + $enum->backedType = 'string'; + $backing = new VMVariable(); + $backing->string('x'); + $case = EnumCaseSupport::createCase($enum, 'A', $backing); + $frame = $fn->getFrame($runtime->vmContext); + $frame->calledArgs[] = $case; + $frame->returnVar = new VMVariable(); + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('stream_resolve_include_path(): Argument #1 ($filename) must be of type string, E given'); + $fn->execute($frame); + } + + public function testResolvesFromIncludePath(): void + { + $dir = \sys_get_temp_dir().'/phpc_stream_resolve_'.\getmypid(); + @\mkdir($dir); + $file = $dir.'/target.inc'; + \file_put_contents($file, 'x'); + $runtime = new Runtime(); + $set = new set_include_path(); + $resolve = new stream_resolve_include_path(); + $setFrame = $set->getFrame($runtime->vmContext); + $pathArg = new VMVariable(); + $pathArg->string($dir); + $setFrame->calledArgs[] = $pathArg; + $setFrame->returnVar = new VMVariable(); + $set->execute($setFrame); + $frame = $resolve->getFrame($runtime->vmContext); + $nameArg = new VMVariable(); + $nameArg->string('target.inc'); + $frame->calledArgs[] = $nameArg; + $frame->returnVar = new VMVariable(); + $resolve->execute($frame); + $result = $frame->returnVar->resolveIndirect(); + $this->assertSame(VMVariable::TYPE_STRING, $result->type); + $this->assertTrue(\is_file($result->toString())); + @\unlink($file); + @\rmdir($dir); + } +}