diff --git a/ext/ldap/JitLdapResult.php b/ext/ldap/JitLdapResult.php new file mode 100644 index 00000000000..55d9ebead37 --- /dev/null +++ b/ext/ldap/JitLdapResult.php @@ -0,0 +1,110 @@ + $args */ + public static function invokeCompare(Context $context, array $args): Value + { + $argc = \count($args); + if ($argc < 4 || $argc > 5) { + throw new \ArgumentCountError(\sprintf( + 'ldap_compare() expects between 4 and 5 arguments, %d given', + $argc + )); + } + + $handle = self::lowerConnectionHandle($context, $args[0], 'ldap_compare'); + $dn = JitStringBuiltinArg::lower($context, $args[1], 'ldap_compare', 1, 'dn'); + $attribute = JitStringBuiltinArg::lower($context, $args[2], 'ldap_compare', 2, 'attribute'); + $value = JitStringBuiltinArg::lower($context, $args[3], 'ldap_compare', 3, 'value'); + + $savedInsert = BasicBlockHelper::tryGetInsertBlock($context); + LdapRuntime::ensureLinked($context); + if (null !== $savedInsert) { + BasicBlockHelper::restoreInsertBlock($context, $savedInsert); + } + + return $context->builder->call( + $context->lookupFunction('__compiler_ldap_compare'), + $handle, + $dn, + $attribute, + $value + ); + } + + private static function lowerConnectionHandle(Context $context, JITVariable $arg, string $function): Value + { + if (JITVariable::TYPE_OBJECT === $arg->type) { + return JitGetObjectId::invoke($context, $arg, $function); + } + if (JITVariable::TYPE_VALUE === $arg->type) { + $loaded = JitValueBox::valuePtrFromVariable($context, $arg); + $obj = $context->builder->call( + $context->lookupFunction('__value__readObject'), + $loaded + ); + $voidp = $context->getTypeFromString('void')->pointerType(0); + $i64 = $context->getTypeFromString('int64'); + + return $context->builder->ptrToInt( + $context->builder->pointerCast($obj, $voidp), + $i64 + ); + } + + self::emitTypeErrorAndAbort($context, self::scalarTypeError($function, $arg->type)); + + return $context->getTypeFromString('int64')->constInt(0, false); + } + + private static function emitTypeErrorAndAbort(Context $context, string $message): void + { + TypeErrorRaise::registerDeclarations($context); + TypeErrorRaise::ensureLinked($context); + TypeErrorRaise::emitRaise($context, $message); + $context->builder->call($context->lookupFunction('abort')); + } + + private static function scalarTypeError(string $function, int $type): string + { + switch ($type) { + case JITVariable::TYPE_NATIVE_LONG: + return self::typeErrorMessage($function, 'int'); + case JITVariable::TYPE_NATIVE_DOUBLE: + return self::typeErrorMessage($function, 'float'); + case JITVariable::TYPE_NATIVE_BOOL: + return self::typeErrorMessage($function, 'bool'); + case JITVariable::TYPE_STRING: + return self::typeErrorMessage($function, 'string'); + case JITVariable::TYPE_NULL: + return self::typeErrorMessage($function, 'null'); + default: + return self::typeErrorMessage($function, 'mixed'); + } + } + + private static function typeErrorMessage(string $function, string $given): string + { + return \sprintf( + '%s(): Argument #1 ($ldap) must be of type LDAP\\Connection, %s given', + $function, + $given + ); + } +} diff --git a/ext/ldap/LdapResultJitHelper.php b/ext/ldap/LdapResultJitHelper.php new file mode 100644 index 00000000000..a53cb23f32c --- /dev/null +++ b/ext/ldap/LdapResultJitHelper.php @@ -0,0 +1,50 @@ +bool($result); + } else { + $out->int($result); + } + + return $out; + } + + private static function requireConnection(int $handle, string $function): ObjectEntry + { + if (VmLdapConnection::isClosedLookupKey($handle)) { + throw new \TypeError(\sprintf( + '%s(): supplied LDAP\\Connection is not a valid ldap link resource', + $function + )); + } + $conn = VmLdapConnection::connectionForLookupKey($handle); + if (null === $conn) { + throw new \TypeError(\sprintf( + '%s(): Argument #1 ($ldap) must be of type LDAP\\Connection, mixed given', + $function + )); + } + + return $conn; + } +} diff --git a/ext/ldap/ldap_result_builtins.php b/ext/ldap/ldap_result_builtins.php index 512dfb4e2f8..0a8ea5ce245 100644 --- a/ext/ldap/ldap_result_builtins.php +++ b/ext/ldap/ldap_result_builtins.php @@ -51,7 +51,7 @@ public function execute(Frame $frame): void public function call(Context $context, JITVariable ...$args): Value { - throw new \LogicException('ldap_compare() is not implemented for JIT in this compiler build (issue #22177)'); + return JitLdapResult::invokeCompare($context, $args); } } diff --git a/lib/JIT/Builtin/LdapRuntime.php b/lib/JIT/Builtin/LdapRuntime.php index 01a8dd96349..39140ee0798 100644 --- a/lib/JIT/Builtin/LdapRuntime.php +++ b/lib/JIT/Builtin/LdapRuntime.php @@ -9,8 +9,8 @@ /** * JIT/AOT link for ldap_escape / ldap_dn2ufn / ldap_explode_dn / ldap_connect / - * ldap_connect_wallet / ldap_bind / ldap_unbind / ldap_errno / ldap_error / ldap_err2str - * (#6352, #18173, #22212, #22276, #31984, #32000, #32001, #32002, #32106). + * ldap_connect_wallet / ldap_bind / ldap_unbind / ldap_errno / ldap_error / ldap_err2str / + * ldap_compare (#6352, #18173, #22212, #22276, #31984, #32000, #32001, #32002, #32106, #32121). * * Helper compile: {@see JitVmHelperLink::ensureBridge} (peer StringStrcoll #22256). * php-src: ext/ldap/ldap.c @@ -23,6 +23,8 @@ final class LdapRuntime private const LINK_HELPER_PATH = '/ext/ldap/LdapLinkJitHelper.php'; + private const RESULT_HELPER_PATH = '/ext/ldap/LdapResultJitHelper.php'; + private const LDAP_ESCAPE_HELPER = 'PHPCompiler\\ext\\ldap\\LdapEscapeJitHelper::ldapEscape'; private const LDAP_DN2UFN_HELPER = 'PHPCompiler\\ext\\ldap\\LdapDnJitHelper::dn2ufn'; @@ -45,6 +47,8 @@ final class LdapRuntime private const LDAP_ERR2STR_HELPER = 'PHPCompiler\\ext\\ldap\\LdapLinkJitHelper::err2strArgv'; + private const LDAP_COMPARE_HELPER = 'PHPCompiler\\ext\\ldap\\LdapResultJitHelper::compareArgv'; + /** @var list */ private const ESCAPE_HELPERS = [ self::LDAP_ESCAPE_HELPER, @@ -68,6 +72,11 @@ final class LdapRuntime self::LDAP_ERR2STR_HELPER, ]; + /** @var list */ + private const RESULT_HELPERS = [ + self::LDAP_COMPARE_HELPER, + ]; + public static function ensureLinked(Context $context): void { self::implement($context); @@ -211,6 +220,17 @@ private static function implement(Context $context): void self::LINK_HELPERS, '#32106' ); + JitVmHelperLink::ensureBridge( + $context, + '__compiler_ldap_compare', + 'ldap_compare_bridge_entry', + [$i64, $strPtr, $strPtr, $strPtr], + $valuePtr, + self::LDAP_COMPARE_HELPER, + self::RESULT_HELPER_PATH, + self::RESULT_HELPERS, + '#32121' + ); if (null !== $savedBlock) { $context->builder->positionAtEnd($savedBlock); diff --git a/test/repro/issue_ldap_compare_jit.php b/test/repro/issue_ldap_compare_jit.php new file mode 100644 index 00000000000..b849c4348ef --- /dev/null +++ b/test/repro/issue_ldap_compare_jit.php @@ -0,0 +1,38 @@ + true); +try { + $result = ldap_compare($link, 'cn=x', 'cn', 'x'); +} finally { + restore_error_handler(); +} + +if (\is_bool($result)) { + echo 'result=', $result ? 'true' : 'false', PHP_EOL; +} else { + echo 'result=', $result, PHP_EOL; +} +echo "ok\n"; diff --git a/test/unit/LdapCompareJitHelperTest.php b/test/unit/LdapCompareJitHelperTest.php new file mode 100644 index 00000000000..8c10dcb49b1 --- /dev/null +++ b/test/unit/LdapCompareJitHelperTest.php @@ -0,0 +1,76 @@ +assertStringContainsString('JitLdapResult::invokeCompare', $source); + $this->assertStringNotContainsString('ldap_compare() is not implemented for JIT', $source); + + $jit = (string) file_get_contents(__DIR__.'/../../ext/ldap/JitLdapResult.php'); + $this->assertStringContainsString('__compiler_ldap_compare', $jit); + + $runtime = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/LdapRuntime.php'); + $this->assertStringContainsString('LdapResultJitHelper::compareArgv', $runtime); + } + + public function testTypeErrorOnNonConnectionHandle(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('ldap_compare(): Argument #1 ($ldap) must be of type LDAP\\Connection, mixed given'); + LdapResultJitHelper::compareArgv(888_888, 'cn=x', 'cn', 'x'); + } + + public function testCompareReturnsBoolOrIntWithoutLiveDirectory(): void + { + if (!VmLdapNative::available()) { + self::markTestSkipped('libldap FFI absent — compare path not exercised'); + } + $ctx = self::ldapContext(); + $linkVar = VmLdapCore::connect('ldap://127.0.0.1', null, $ctx); + if (false === $linkVar) { + self::markTestSkipped('ldap_connect failed in container'); + } + $object = $linkVar->toObject(); + VmLdapConnection::enqueuePendingJitHandle($object->id); + VmLdapConnection::claimPendingJitHandle(42_003); + + set_error_handler(static fn (): bool => true); + try { + @VmLdapCore::bind($object, null, null); + $out = LdapResultJitHelper::compareArgv(42_003, 'cn=x', 'cn', 'x'); + } finally { + restore_error_handler(); + VmLdapConnection::close($object); + } + + $this->assertContains( + $out->type, + [Variable::TYPE_BOOLEAN, Variable::TYPE_INTEGER], + 'compare must return bool or int (-1)' + ); + } + + private static function ldapContext(): Context + { + $runtime = new Runtime(); + $runtime->load(new \PHPCompiler\ext\ldap\Module()); + + return $runtime->vmContext; + } +} diff --git a/test/unit/LdapRuntimeShrinkTest.php b/test/unit/LdapRuntimeShrinkTest.php index 9706158003b..5257b6db2b4 100644 --- a/test/unit/LdapRuntimeShrinkTest.php +++ b/test/unit/LdapRuntimeShrinkTest.php @@ -34,8 +34,9 @@ public function testLdapRuntimeUsesJitVmHelperLinkForAllBridges(): void $this->assertStringContainsString('__compiler_ldap_errno', $source); $this->assertStringContainsString('__compiler_ldap_error', $source); $this->assertStringContainsString('__compiler_ldap_err2str', $source); + $this->assertStringContainsString('__compiler_ldap_compare', $source); $this->assertStringContainsString('ldap_connect_bridge_entry', $source); - $this->assertSame(11, \preg_match_all('/JitVmHelperLink::ensureBridge\(/', $source)); + $this->assertSame(12, \preg_match_all('/JitVmHelperLink::ensureBridge\(/', $source)); $this->assertStringNotContainsString('NestedJitCompileScope::run', $source); $this->assertStringNotContainsString('parseAndCompile', $source); $this->assertStringNotContainsString('new JIT(', $source); @@ -43,6 +44,6 @@ public function testLdapRuntimeUsesJitVmHelperLinkForAllBridges(): void $this->assertStringNotContainsString('use PHPCompiler\\JIT\\NestedJitCompileScope;', $source); $this->assertStringNotContainsString('ensureEscapeHelperCompiled', $source); $this->assertStringNotContainsString('implementEscapeBridge', $source); - $this->assertLessThan(240, \substr_count($source, "\n") + 1); + $this->assertLessThan(250, \substr_count($source, "\n") + 1); } }