Resolve function imports before dispatch and policy checks (#108)

* fix(compiler): resolve function imports before dispatch and policy checks

* test: close PHP section in function import PHPT

* fix(compiler): resolve first-class function aliases
master
yavon007 4 weeks ago committed by GitHub
parent df6c0dfd99
commit ce4c712b0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      phpunit/code/function-import-policy.php
  2. 27
      phpunit/code/function-import-resolution.php
  3. 32
      phpunit/src/FunctionImportResolutionTest.php
  4. 28
      phpunit/src/NanoCapabilityPolicyTest.php
  5. 34
      tests/compiler/namespace/function-import-resolution.phpt

@ -0,0 +1,3 @@
<?php
use function exec as SAFE;
function importedPolicyCall(): void { safe('not-executed'); }

@ -0,0 +1,27 @@
<?php
namespace {
function route(): string { return 'global'; }
function size(string $value): int { return 99; }
}
namespace AliasLibrary {
function route(): string { return 'import'; }
function callback_target(): string { return 'import-callback'; }
}
namespace AliasConsumer {
use function AliasLibrary\route as ROUTE;
use function AliasLibrary\route as GET_CALLED_CLASS;
use function strlen as SIZE;
use function AliasLibrary\route as extract;
use function AliasLibrary\{route as GROUPED};
use function AliasLibrary\callback_target as CALLBACK_TARGET;
function exercise(): void {
echo route(), ':', RoUtE(), ':', get_called_class(), ':', sIzE('abc'), ':', grouped(), ':', extract(), ':', \route(), "\n";
$size = sIzE(...);
$callback = cAlLbAcK_tArGeT(...);
$route = rOuTe(...);
$dynamic = $size;
echo $size('abcd'), ':', $callback(), ':', $route(), ':', $dynamic('abc'), "\n";
}
}
namespace { function main(): void { \AliasConsumer\exercise(); } }

@ -0,0 +1,32 @@
<?php
use TypePhp\CompilerTest;
final class FunctionImportResolutionTest extends BaseTest
{
public function testImportedFunctionsTakePrecedenceRegardlessOfAliasCase(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/function-import-resolution.php';
$compiler->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('));
// The three first-class callables are materialized through
// Closure::fromCallable(); ordinary aliases above must stay direct.
self::assertSame(3, substr_count($match[1], 'php::call('));
self::assertStringNotContainsString('ZEND_STRL("sIzE")', $match[1]);
self::assertStringNotContainsString('ZEND_STRL("cAlLbAcK_tArGeT")', $match[1]);
$literalStrings = (new \ReflectionProperty($compiler, 'literalStrings'))->getValue($compiler);
self::assertArrayHasKey('strlen', $literalStrings);
self::assertArrayHasKey('AliasLibrary\\callback_target', $literalStrings);
self::assertArrayNotHasKey('sIzE', $literalStrings);
self::assertArrayNotHasKey('cAlLbAcK_tArGeT', $literalStrings);
}
}

@ -49,6 +49,34 @@ final class NanoCapabilityPolicyCompiler extends CompilerTest
final class NanoCapabilityPolicyTest extends BaseTest 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 public function testRejectsForbiddenDirectCallMissingFromBuildTimePhp(): void
{ {
global $translator; global $translator;

@ -0,0 +1,34 @@
--TEST--
Function imports are case-insensitive and take precedence over global functions
--FILE--
<?php
namespace {
function route(): string { return 'global'; }
function size(string $value): int { return 99; }
}
namespace AliasLibrary {
function route(): string { return 'import'; }
function callback_target(): string { return 'import-callback'; }
}
namespace AliasConsumer {
use function AliasLibrary\route as ROUTE;
use function AliasLibrary\route as GET_CALLED_CLASS;
use function strlen as SIZE;
use function AliasLibrary\route as extract;
use function AliasLibrary\{route as GROUPED};
use function AliasLibrary\callback_target as CALLBACK_TARGET;
function exercise(): void {
echo route(), ':', RoUtE(), ':', get_called_class(), ':', sIzE('abc'), ':', grouped(), ':', extract(), ':', \route(), "\n";
$size = sIzE(...);
$callback = cAlLbAcK_tArGeT(...);
$route = rOuTe(...);
$dynamic = $size;
echo $size('abcd'), ':', $callback(), ':', $route(), ':', $dynamic('abc'), "\n";
}
}
namespace { function main(): void { \AliasConsumer\exercise(); } }
?>
--EXPECT--
import:import:import:3:import:import:global
4:import-callback:import:3
Loading…
Cancel
Save