diff --git a/phpunit/code/function-import-policy.php b/phpunit/code/function-import-policy.php new file mode 100644 index 00000000..7fd09b24 --- /dev/null +++ b/phpunit/code/function-import-policy.php @@ -0,0 +1,3 @@ +addFiles([$source]); + $compiler->prepareFile($source); + $code = file_get_contents($compiler->convertFile($source)); + preg_match('/void php_aliasconsumer__exercise\(\) \{(.*?)\n\}/s', $code, $match); + self::assertCount(2, $match); + self::assertSame(5, substr_count($match[1], 'php_aliaslibrary__route(')); + self::assertSame(1, substr_count($match[1], 'php_route(')); + self::assertStringNotContainsString('php::call(', $match[1]); + } +} diff --git a/phpunit/src/NanoCapabilityPolicyTest.php b/phpunit/src/NanoCapabilityPolicyTest.php index 0091f129..2c76fb1a 100644 --- a/phpunit/src/NanoCapabilityPolicyTest.php +++ b/phpunit/src/NanoCapabilityPolicyTest.php @@ -49,6 +49,34 @@ public function forgetBuildTimeFunction(string $name): void final class NanoCapabilityPolicyTest extends BaseTest { + public function testFunctionImportCannotBypassNanoPolicy(): void + { + $this->assertImportedFunctionRejected(false); + } + + public function testFunctionImportCannotBypassWasiPolicy(): void + { + $this->assertImportedFunctionRejected(true); + } + + private function assertImportedFunctionRejected(bool $wasi): void + { + global $translator; + $compiler = new NanoCapabilityPolicyCompiler(TYPEPHP_ROOT_PATH); + if ($wasi) { + $compiler->enableWasiForTest(); + } else { + $compiler->enableNanoForTest(); + } + $translator = $compiler; + $source = __DIR__ . '/../code/function-import-policy.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $this->expectException(TestError::class); + $this->expectExceptionMessage('Function `exec` is not supported'); + $compiler->convertFile($source); + } + public function testRejectsForbiddenDirectCallMissingFromBuildTimePhp(): void { global $translator; diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 38b708ef..07fbada0 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3647,14 +3647,17 @@ protected function findNativeFunction(string $funcName): string|false $possibleFunctionNames = [ $this->escapeName($this->getNamespacedClassName($funcName)), ]; + } elseif (isset($this->useFunctions[strtolower($funcName)])) { + // An explicit function import selects one target; it must not + // fall back to a same-named compiled global function. + $possibleFunctionNames = [ + $this->escapeNamespace($this->useFunctions[strtolower($funcName)]), + ]; } else { $possibleFunctionNames = [$this->escapeName($funcName)]; if ($this->namespace) { $possibleFunctionNames[] = $this->escapeNamespace($this->namespace) . self::NAMESPACE_SEPARATOR . $this->escapeName($funcName); } - if (isset($this->useFunctions[$funcName])) { - $possibleFunctionNames[] = $this->escapeNamespace($this->useFunctions[$funcName]); - } } foreach ($possibleFunctionNames as $nativeFunc) { diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index a1de091c..60c9cfa9 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -114,7 +114,7 @@ protected function parseFuncCall(Expr\FuncCall $expr): string $name = ''; } elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') { $name = $this->parseIdentifier($expr->name); - $globalName = ltrim($name, '\\'); + $globalName = strtolower(ltrim($this->getNamespacedFuncName($name), '\\')); $this->compilationStatistics->record( CompilationStatistics::FUNCTIONS, strtolower($globalName), diff --git a/src/Resolver/DeclarationSymbolTrait.php b/src/Resolver/DeclarationSymbolTrait.php index c2987cf0..967a579b 100644 --- a/src/Resolver/DeclarationSymbolTrait.php +++ b/src/Resolver/DeclarationSymbolTrait.php @@ -78,7 +78,7 @@ protected function parseUse(Node\Stmt\Use_ $v2): void $type = $use->type !== Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $v2->type; $alias = $this->registerUseImportAlias($use, $type, $id); if ($type === Node\Stmt\Use_::TYPE_FUNCTION) { - $this->useFunctions[$alias] = $id; + $this->useFunctions[strtolower($alias)] = $id; } elseif ($type === Node\Stmt\Use_::TYPE_CONSTANT) { // $id is already the fully qualified constant name. Splitting // and re-joining it on `\` corrupted single-segment imports: diff --git a/src/Resolver/NameResolutionTrait.php b/src/Resolver/NameResolutionTrait.php index 699307e7..c7b7220e 100644 --- a/src/Resolver/NameResolutionTrait.php +++ b/src/Resolver/NameResolutionTrait.php @@ -146,8 +146,9 @@ public function getNamespacedFuncName(string $funcName): string if ($funcName[0] == '\\') { return ltrim($funcName, '\\'); } - if (isset($this->useFunctions[$funcName])) { - return $this->useFunctions[$funcName]; + $alias = strtolower($funcName); + if (isset($this->useFunctions[$alias])) { + return $this->useFunctions[$alias]; } return $funcName; } diff --git a/tests/compiler/namespace/function-import-resolution.phpt b/tests/compiler/namespace/function-import-resolution.phpt new file mode 100644 index 00000000..1b5f3bc9 --- /dev/null +++ b/tests/compiler/namespace/function-import-resolution.phpt @@ -0,0 +1,20 @@ +--TEST-- +Function imports are case-insensitive and take precedence over global functions +--FILE-- + +--EXPECT-- +import:import:import:3:import:import:global