Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ext/standard/JitStripTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use PHPLLVM\Value;

/**
* LLVM JIT/AOT helpers for strip_tags() (mirrors VmString::stripTags).
* JIT/AOT helpers for strip_tags() — runtime via StripTagsJitHelper (#9196); array allowed-tags markup here.
*/
final class JitStripTags
{
Expand Down
166 changes: 166 additions & 0 deletions ext/standard/StripTagsJitHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\ext\standard;

/**
* Lowered into JIT/AOT modules for strip_tags() runtime (#9196, php-in-PHP).
*
* Logic mirrors {@see VmString::stripTags()} (php-src ext/standard/string.c — php_strip_tags_ex).
* Self-contained so parseAndCompile() does not depend on compiling all of VmString.php.
*/
final class StripTagsJitHelper
{
public static function stripTags(string $input, string $allowedMarkup): string
{
$allowed = '' === $allowedMarkup ? [] : self::parseAllowedTags($allowedMarkup);
$out = '';
$len = \strlen($input);
$i = 0;
while ($i < $len) {
$ch = $input[$i];
if ('<' !== $ch) {
$out .= $ch;
++$i;
continue;
}
if ($i + 3 < $len && '<!--' === \substr($input, $i, 4)) {
$end = self::findSubstring($input, '-->', $i + 4);
if (false !== $end) {
$i = $end + 3;
continue;
}
}
if ($i + 1 < $len && '<?' === \substr($input, $i, 2)) {
$end = self::findSubstring($input, '?>', $i + 2);
if (false !== $end) {
$i = $end + 2;
continue;
}
}
$gt = self::findSubstring($input, '>', $i + 1);
if (false === $gt) {
$out .= $ch;
++$i;
continue;
}
$tagContent = \substr($input, $i + 1, $gt - $i - 1);
$tagName = self::extractTagName($tagContent);
if (null !== $tagName && [] !== $allowed && self::isTagAllowed($tagName, $allowed)) {
$out .= \substr($input, $i, $gt - $i + 1);
}
$i = $gt + 1;
}

return $out;
}

/**
* @return list<string>
*/
private static function parseAllowedTags(string $allowedTags): array
{
$tags = [];
$len = \strlen($allowedTags);
$i = 0;
while ($i < $len) {
if ('<' !== $allowedTags[$i]) {
++$i;
continue;
}
$gt = self::findSubstring($allowedTags, '>', $i + 1);
if (false === $gt) {
break;
}
$name = self::extractTagName(\substr($allowedTags, $i + 1, $gt - $i - 1));
if (null !== $name && '' !== $name) {
$tags[] = $name;
}
$i = $gt + 1;
}

return $tags;
}

private static function extractTagName(string $tagContent): ?string
{
$len = \strlen($tagContent);
$i = 0;
while ($i < $len && self::isTagWhitespace($tagContent[$i])) {
++$i;
}
if ($i < $len && '/' === $tagContent[$i]) {
++$i;
}
if ($i >= $len) {
return null;
}
$start = $i;
while ($i < $len) {
$ch = $tagContent[$i];
if (self::isTagWhitespace($ch) || '>' === $ch || '/' === $ch) {
break;
}
if (!self::isTagChar($ch)) {
return null;
}
++$i;
}
if ($start === $i) {
return null;
}

return self::asciiLower(\substr($tagContent, $start, $i - $start));
}

/**
* @param list<string> $allowed
*/
private static function isTagAllowed(string $tagName, array $allowed): bool
{
$tagName = self::asciiLower($tagName);
foreach ($allowed as $name) {
if ($tagName === $name) {
return true;
}
}

return false;
}

private static function isTagWhitespace(string $ch): bool
{
return ' ' === $ch || "\t" === $ch || "\n" === $ch || "\r" === $ch || "\0" === $ch || "\x0B" === $ch;
}

private static function isTagChar(string $ch): bool
{
return ($ch >= 'a' && $ch <= 'z')
|| ($ch >= 'A' && $ch <= 'Z')
|| ($ch >= '0' && $ch <= '9');
}

private static function asciiLower(string $text): string
{
$out = '';
$len = \strlen($text);
for ($i = 0; $i < $len; ++$i) {
$ch = $text[$i];
if ($ch >= 'A' && $ch <= 'Z') {
$out .= \chr(\ord($ch) + 32);
} else {
$out .= $ch;
}
}

return $out;
}

private static function findSubstring(string $haystack, string $needle, int $offset = 0): int|false
{
$pos = \strpos($haystack, $needle, $offset);

return false === $pos ? false : $pos;
}
}
109 changes: 106 additions & 3 deletions lib/JIT/Builtin/StringStripTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,123 @@

namespace PHPCompiler\JIT\Builtin;

use PHPCompiler\JIT;
use PHPCompiler\JIT\Builtin;
use PHPCompiler\JIT\Context;
use PHPLLVM\Value\Function_ as LlvmFunction;

/**
* JIT/AOT link for __compiler_strip_tags LLVM runtime.
* JIT/AOT link for __compiler_strip_tags via StripTagsJitHelper PHP (#9196).
*
* JIT/normal modules use compiled PHP SSOT; AOT standalone keeps {@see StringStripTagsStandaloneLlvm}
* until native link can host compiled VmString helpers reliably.
*/
final class StringStripTags
{
private const HELPER_PATH = '/ext/standard/StripTagsJitHelper.php';

private const STRIP_TAGS_HELPER = 'PHPCompiler\\ext\\standard\\StripTagsJitHelper::stripTags';

/** @var list<string> */
private const COMPILED_HELPERS = [
self::STRIP_TAGS_HELPER,
];

public static function ensureLinked(Context $context): void
{
StringStripTagsJit::implement($context);
self::implement($context);
}

public static function ensureStandaloneBodies(Context $context): void
{
StringStripTagsJit::implement($context);
self::implement($context);
}

public static function implement(Context $context): void
{
if (Builtin::LOAD_TYPE_STANDALONE === $context->loadType) {
StringStripTagsStandaloneLlvm::implement($context);

return;
}

self::implementBridge($context, '__compiler_strip_tags', self::STRIP_TAGS_HELPER, 2);
}

private static function implementBridge(
Context $context,
string $abiName,
string $helperLogical,
int $paramCount
): void {
$probe = $context->module->getNamedFunction($abiName);
if (null !== $probe && $probe->countBasicBlocks() > 0) {
$context->registerFunction($abiName, $probe);

return;
}

$strPtr = $context->getTypeFromString('__string__*');
$params = \array_fill(0, $paramCount, $strPtr);
$ft = $context->context->functionType($strPtr, false, ...$params);
$fn = null !== $probe
? $probe
: $context->module->addFunction($abiName, $ft);

self::ensureJitHelperCompiled($context);

$entry = $fn->appendBasicBlock('strip_tags_bridge_entry');
$context->builder->positionAtEnd($entry);
$callArgs = [];
for ($i = 0; $i < $paramCount; ++$i) {
$callArgs[] = $fn->getParam($i);
}
$result = $context->builder->call(
self::helperFunction($context, $helperLogical),
...$callArgs
);
$context->builder->returnValue($result);
$context->registerFunction($abiName, $fn);
$context->builder->clearInsertionPosition();
}

private static function helperFunction(Context $context, string $logical): LlvmFunction
{
self::ensureJitHelperCompiled($context);
$lc = \strtolower($logical);
$fn = $context->functions[$lc] ?? null;
if (null === $fn) {
throw new \LogicException($logical.' missing after StripTagsJitHelper compile (#9196)');
}

return $fn;
}

private static function ensureJitHelperCompiled(Context $context): void
{
$missing = false;
foreach (self::COMPILED_HELPERS as $logical) {
if (!isset($context->functions[\strtolower($logical)])) {
$missing = true;
break;
}
}
if (!$missing) {
return;
}

$runtime = $context->runtime;
$path = \dirname(__DIR__, 3).self::HELPER_PATH;
$block = $runtime->parseAndCompile((string) \file_get_contents($path), 'StripTagsJitHelper.php');
if (null === $block) {
throw new \LogicException('StripTagsJitHelper.php parseAndCompile failed (#9196)');
}
$jit = new JIT($context);
$jit->compile($block);
foreach (self::COMPILED_HELPERS as $logical) {
if (!isset($context->functions[\strtolower($logical)])) {
throw new \LogicException($logical.' was not compiled for JIT (#9196)');
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
use PHPLLVM\Value\Function_ as LlvmFunction;

/**
* LLVM body for __compiler_strip_tags and st_* helper parity from superglobals_refresh.c.
* LLVM body for __compiler_strip_tags — AOT standalone only (#9196).
*
* JIT uses {@see StripTagsJitHelper} PHP; keep this until compiled PHP static storage is
* reliable in native standalone link (same pattern as {@see LastErrorRuntimeLlvm}).
*/
final class StringStripTagsJit
final class StringStripTagsStandaloneLlvm
{
private const ALLOWED_TAG_CAP = 32;

Expand Down
3 changes: 2 additions & 1 deletion test/selfhost/compiler_lib_spine_smoke/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -2075,8 +2075,9 @@
require_once __DIR__.'/../../../lib/JIT/Builtin/StringStrGetcsvJit.php';
require_once __DIR__.'/../../../lib/JIT/Builtin/StringStreamCsv.php';
require_once __DIR__.'/../../../lib/JIT/Builtin/StringStrftime.php';
require_once __DIR__.'/../../../ext/standard/StripTagsJitHelper.php';
require_once __DIR__.'/../../../lib/JIT/Builtin/StringStripTags.php';
require_once __DIR__.'/../../../lib/JIT/Builtin/StringStripTagsJit.php';
require_once __DIR__.'/../../../lib/JIT/Builtin/StringStripTagsStandaloneLlvm.php';
require_once __DIR__.'/../../../lib/JIT/Builtin/StringStripcslashesRuntime.php';
require_once __DIR__.'/../../../lib/JIT/Builtin/StringStripslashes.php';
require_once __DIR__.'/../../../lib/JIT/Builtin/StringStrnatcasecmp.php';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use PHPUnit\Framework\TestCase;

/**
* AOT standalone must define strip_tags runtime in LLVM, not superglobals_refresh.c.
* Issue #9196: standalone AOT keeps LLVM strip_tags; JIT path uses StripTagsJitHelper.
*
* @group aot-lint
*/
Expand Down
28 changes: 28 additions & 0 deletions test/unit/StripTagsRuntimeShrinkTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\Test\Unit;

use PHPUnit\Framework\TestCase;

/** StringStripTags JIT path uses StripTagsJitHelper PHP, not StringStripTagsJit (#9196). */
final class StripTagsRuntimeShrinkTest extends TestCase
{
public function testStringStripTagsUsesStripTagsJitHelperForJitPath(): void
{
$source = (string) file_get_contents(__DIR__.'/../../lib/JIT/Builtin/StringStripTags.php');
$this->assertStringContainsString('StripTagsJitHelper', $source);
$this->assertStringNotContainsString('StringStripTagsJit', $source);
$this->assertStringContainsString('StringStripTagsStandaloneLlvm', $source);
$this->assertFileDoesNotExist(__DIR__.'/../../lib/JIT/Builtin/StringStripTagsJit.php');
}

public function testStripTagsJitHelperIsSelfContained(): void
{
$source = (string) file_get_contents(__DIR__.'/../../ext/standard/StripTagsJitHelper.php');
$this->assertStringContainsString('extractTagName', $source);
$this->assertStringNotContainsString('return VmString::', $source);
$this->assertStringNotContainsString('ctype_alpha', $source);
}
}