From b5a87ad9553ec5425de37da5cf45a3ae6a3d8dc1 Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 18 May 2026 19:02:14 +0000 Subject: [PATCH] AOT: parse nested query strings in CGI superglobal refresh (#200). Bracket notation (user[name]=, tags[]=) is parsed in the AOT runtime C refresh path with URL decoding, matching VM parse_str behavior. LLVM hashtable helpers support nested child tables; SuperglobalInit copies nested arrays at compile time. Docker CI installs libbsd0 so bundled LLVM loads in php:8.2-cli-bookworm. Co-authored-by: Cursor --- docs/capabilities.md | 4 +- lib/AOT/runtime/superglobals_refresh.c | 183 ++++++++++++++++- lib/JIT/Builtin/Type/HashTable.php | 171 ++++++++++++++++ lib/JIT/SuperglobalInit.php | 37 +++- lib/JIT/Variable.php | 23 ++- phpunit.xml.dist | 2 + script/ci-local.sh | 2 +- script/docker-capability-matrix.sh | 24 +++ script/docker-ci.sh | 2 +- test/aot/NestedSuperglobalsAotTest.php | 191 ++++++++++++++++++ .../fixtures/aot/cases/nested_get_params.phpt | 11 + 11 files changed, 639 insertions(+), 11 deletions(-) create mode 100755 script/docker-capability-matrix.sh create mode 100644 test/aot/NestedSuperglobalsAotTest.php create mode 100644 test/fixtures/aot/cases/nested_get_params.phpt diff --git a/docs/capabilities.md b/docs/capabilities.md index 704acb203..5d718cb85 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -44,7 +44,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `header` | yes | yes | yes | standard | AOT PHPT | | `hexdec` | yes | yes | yes | standard | | | `htmlspecialchars` | yes | yes | yes | standard | AOT PHPT | -| `implode` | yes | yes | yes | standard | doc: VM only; JIT PHPT; AOT PHPT | +| `implode` | yes | yes | yes | standard | doc: VM only; AOT PHPT | | `in_array` | yes | yes | yes | standard | doc: VM only; JIT PHPT | | `intdiv` | yes | yes | yes | standard | | | `intval` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | @@ -86,7 +86,7 @@ Auto-generated by `script/capability-matrix.php`. Do not edit by hand. | `scandir` | yes | yes | yes | standard | | | `sin` | yes | yes | yes | standard | | | `sizeof` | yes | yes | yes | standard | | -| `sort` | yes | yes | yes | standard | JIT PHPT; AOT PHPT | +| `sort` | yes | yes | yes | standard | | | `sqrt` | yes | yes | yes | standard | | | `str_contains` | yes | yes | yes | standard | | | `str_ends_with` | yes | yes | yes | standard | AOT PHPT | diff --git a/lib/AOT/runtime/superglobals_refresh.c b/lib/AOT/runtime/superglobals_refresh.c index aaa513229..654d4cb66 100644 --- a/lib/AOT/runtime/superglobals_refresh.c +++ b/lib/AOT/runtime/superglobals_refresh.c @@ -12,6 +12,10 @@ typedef struct __string__ __string__; extern __hashtable__ *__hashtable__alloc(void); extern void __hashtable__setStringKeyString(__hashtable__ *ht, __string__ *key, __string__ *val); +extern void __hashtable__setStringKeyHashtable(__hashtable__ *ht, __string__ *key, __hashtable__ *child); +extern void __hashtable__setStringAt(__hashtable__ *ht, size_t index, __string__ *val); +extern size_t __hashtable__getNumElements(__hashtable__ *ht); +extern __hashtable__ *__hashtable__readStringKeyHashtable(__hashtable__ *ht, __string__ *key); extern __string__ *__string__init(long long size, const char *value); extern __hashtable__ *sg_GET; @@ -38,6 +42,161 @@ static void set_string_key(__hashtable__ *ht, const char *key, const char *value __hashtable__setStringKeyString(ht, k, v); } +#define SG_MAX_KEY_PARTS 16 + +typedef struct { + char *parts[SG_MAX_KEY_PARTS]; + size_t count; + int append_list; +} sg_parsed_key; + +static int sg_is_hex(char c) +{ + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +static int sg_hex_value(char c) +{ + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + + return c - 'A' + 10; +} + +static void sg_url_decode_inplace(char *s) +{ + char *w = s; + + for (char *r = s; '\0' != *r; r++) { + if ('+' == *r) { + *w++ = ' '; + } else if ('%' == *r && sg_is_hex(r[1]) && sg_is_hex(r[2])) { + *w++ = (char) (sg_hex_value(r[1]) * 16 + sg_hex_value(r[2])); + r += 2; + } else { + *w++ = *r; + } + } + *w = '\0'; +} + +static void sg_free_parsed_key(sg_parsed_key *pk) +{ + size_t i; + + for (i = 0; i < pk->count; i++) { + free(pk->parts[i]); + pk->parts[i] = NULL; + } + pk->count = 0; + pk->append_list = 0; +} + +static int sg_parse_key_brackets(const char *raw, sg_parsed_key *out) +{ + const char *p = raw; + size_t base_len; + + out->count = 0; + out->append_list = 0; + if ('\0' == raw[0]) { + return -1; + } + + base_len = strcspn(p, "["); + if (base_len > 0) { + out->parts[out->count] = strndup(p, base_len); + if (NULL == out->parts[out->count]) { + return -1; + } + out->count++; + p += base_len; + } + + while ('[' == *p) { + p++; + if (']' == *p) { + out->append_list = 1; + p++; + break; + } + { + const char *close = strchr(p, ']'); + size_t len; + + if (NULL == close) { + return -1; + } + len = (size_t) (close - p); + out->parts[out->count] = malloc(len + 1); + if (NULL == out->parts[out->count]) { + return -1; + } + memcpy(out->parts[out->count], p, len); + out->parts[out->count][len] = '\0'; + out->count++; + p = close + 1; + } + if ('[' == *p && ']' == p[1]) { + out->append_list = 1; + p += 2; + } + } + + if ('\0' != *p || 0 == out->count) { + return -1; + } + + return 0; +} + +static __hashtable__ *sg_ensure_child(__hashtable__ *ht, const char *key) +{ + __string__ *k = cstr_to_string(key); + __hashtable__ *child = __hashtable__readStringKeyHashtable(ht, k); + + if (NULL != child) { + return child; + } + child = __hashtable__alloc(); + __hashtable__setStringKeyHashtable(ht, k, child); + + return child; +} + +static void sg_set_nested_value(__hashtable__ *root, sg_parsed_key *pk, const char *value) +{ + __hashtable__ *ht = root; + size_t last; + const char *leaf; + + if (0 == pk->count) { + return; + } + last = pk->count; + { + size_t i; + + for (i = 0; i + 1 < last; i++) { + ht = sg_ensure_child(ht, pk->parts[i]); + } + } + leaf = pk->parts[last - 1]; + if (pk->append_list) { + __hashtable__ *list_ht = sg_ensure_child(ht, leaf); + size_t idx = __hashtable__getNumElements(list_ht); + + __hashtable__setStringAt(list_ht, idx, cstr_to_string(value)); + + return; + } + set_string_key(ht, leaf, value); +} + static void parse_form_encoded(__hashtable__ *ht, const char *body) { char *copy; @@ -56,12 +215,30 @@ static void parse_form_encoded(__hashtable__ *ht, const char *body) pair = strtok_r(copy, "&", &saveptr); while (NULL != pair) { char *eq = strchr(pair, '='); + char *raw_key; + char *raw_val; + sg_parsed_key pk; + if (NULL != eq) { *eq = '\0'; - set_string_key(ht, pair, eq + 1); - } else if ('\0' != pair[0]) { - set_string_key(ht, pair, ""); + raw_key = pair; + raw_val = eq + 1; + } else { + raw_key = pair; + raw_val = (char *) ""; + } + if ('\0' == raw_key[0]) { + pair = strtok_r(NULL, "&", &saveptr); + continue; + } + sg_url_decode_inplace(raw_key); + sg_url_decode_inplace(raw_val); + if (0 == sg_parse_key_brackets(raw_key, &pk)) { + sg_set_nested_value(ht, &pk, raw_val); + } else { + set_string_key(ht, raw_key, raw_val); } + sg_free_parsed_key(&pk); pair = strtok_r(NULL, "&", &saveptr); } diff --git a/lib/JIT/Builtin/Type/HashTable.php b/lib/JIT/Builtin/Type/HashTable.php index 2a61ad821..063bec7e8 100755 --- a/lib/JIT/Builtin/Type/HashTable.php +++ b/lib/JIT/Builtin/Type/HashTable.php @@ -69,9 +69,13 @@ public function register(): void $this->registerFn('__hashtable__getNumElements', 'size_t', ['__hashtable__*']); $this->registerFn('__hashtable__offsetIsSet', 'int1', ['__hashtable__*', 'size_t']); $this->registerFn('__hashtable__setStringKeyString', 'void', ['__hashtable__*', '__string__*', '__string__*']); + $this->registerFn('__hashtable__setStringKeyHashtable', 'void', ['__hashtable__*', '__string__*', '__hashtable__*']); $this->registerFn('__hashtable__setStringKeyLong', 'void', ['__hashtable__*', '__string__*', 'int64']); $this->registerFn('__hashtable__offsetIsSetStringKey', 'int1', ['__hashtable__*', '__string__*']); $this->registerFn('__hashtable__readStringKeyValue', '__value__*', ['__hashtable__*', '__string__*']); + $this->registerFn('__hashtable__readStringKeyHashtable', '__hashtable__*', ['__hashtable__*', '__string__*']); + $this->registerFn('__value__readHashtable', '__hashtable__*', ['__value__*']); + $this->registerFn('__value__writeHashtable', 'void', ['__value__*', '__hashtable__*']); $this->registerFn('__hashtable__sortPacked', 'void', ['__hashtable__*']); $this->pointer = $this->context->getTypeFromString('__hashtable__*'); @@ -105,8 +109,12 @@ public function implement(): void $this->implementGetNumElements(); $this->implementOffsetIsSet(); $this->implementSetStringKeyString(); + $this->implementSetStringKeyHashtable(); $this->implementOffsetIsSetStringKey(); $this->implementReadStringKeyValue(); + $this->implementReadStringKeyHashtable(); + $this->implementValueReadHashtable(); + $this->implementValueWriteHashtable(); $this->implementSortPacked(); } @@ -550,6 +558,85 @@ private function implementSetStringKeyString(): void $this->context->builder->returnVoid(); } + private function implementSetStringKeyHashtable(): void + { + $fn = $this->context->lookupFunction('__hashtable__setStringKeyHashtable'); + $block = $fn->appendBasicBlock('main'); + $this->context->builder->positionAtEnd($block); + $ht = $fn->getParam(0); + $key = $fn->getParam(1); + $child = $fn->getParam(2); + + $htMap = $this->context->structFieldMap['__hashtable__']; + $nodeMap = $this->context->structFieldMap['__strkey_node__']; + $headSlot = $this->context->builder->structGep($ht, $htMap['strKeys']); + $head = $this->context->builder->load($headSlot); + + $done = $fn->appendBasicBlock('strkey_ht_done'); + $prepend = $fn->appendBasicBlock('strkey_ht_prepend'); + $loopHead = $fn->appendBasicBlock('strkey_ht_head'); + $loopBody = $fn->appendBasicBlock('strkey_ht_body'); + $this->context->builder->branch($loopHead); + + $this->context->builder->positionAtEnd($loopHead); + $node = $this->context->builder->phi($head->typeOf()); + $node->addIncoming($head, $block); + $isNull = $this->context->builder->icmp(Builder::INT_EQ, $node, $node->typeOf()->constNull()); + $this->context->builder->branchIf($isNull, $prepend, $loopBody); + + $this->context->builder->positionAtEnd($loopBody); + $nodeKey = $this->context->builder->load($this->context->builder->structGep($node, $nodeMap['key'])); + $cmp = $this->context->builder->call( + $this->context->lookupFunction('strcmp'), + $this->stringDataPtr($key), + $this->stringDataPtr($nodeKey) + ); + $isMatch = $this->context->builder->icmp(Builder::INT_EQ, $cmp, $cmp->typeOf()->constInt(0, false)); + $update = $fn->appendBasicBlock('strkey_ht_update'); + $next = $fn->appendBasicBlock('strkey_ht_next'); + $this->context->builder->branchIf($isMatch, $update, $next); + + $this->context->builder->positionAtEnd($update); + $valField = $this->context->builder->structGep($node, $nodeMap['value']); + $this->context->builder->call( + $this->context->lookupFunction('__value__writeHashtable'), + $valField, + $child + ); + $this->context->builder->branch($done); + + $this->context->builder->positionAtEnd($next); + $nextNode = $this->context->builder->load($this->context->builder->structGep($node, $nodeMap['next'])); + $this->context->builder->branch($loopHead); + $node->addIncoming($nextNode, $next); + + $this->context->builder->positionAtEnd($prepend); + $nodeType = $this->context->getTypeFromString('__strkey_node__'); + $newNode = $this->context->memory->malloc($nodeType); + $typeinfo = $this->context->getTypeFromString('int32')->constInt( + Refcount::TYPE_INFO_TYPE_STRING | Refcount::TYPE_INFO_REFCOUNTED, + false + ); + $ref = $this->context->builder->pointerCast( + $newNode, + $this->context->getTypeFromString('__ref__virtual*') + ); + $this->context->builder->call($this->context->lookupFunction('__ref__init'), $typeinfo, $ref); + $storedKey = $this->context->builder->call($this->context->lookupFunction('__string__separate'), $key); + $this->context->builder->store($storedKey, $this->context->builder->structGep($newNode, $nodeMap['key'])); + $this->context->builder->call( + $this->context->lookupFunction('__value__writeHashtable'), + $this->context->builder->structGep($newNode, $nodeMap['value']), + $child + ); + $this->context->builder->store($head, $this->context->builder->structGep($newNode, $nodeMap['next'])); + $this->context->builder->store($newNode, $headSlot); + $this->context->builder->branch($done); + + $this->context->builder->positionAtEnd($done); + $this->context->builder->returnVoid(); + } + private function implementSetStringKeyLong(): void { $fn = $this->context->lookupFunction('__hashtable__setStringKeyLong'); @@ -669,6 +756,90 @@ private function implementReadStringKeyValue(): void $this->context->builder->returnValue($valPtr); } + private function implementReadStringKeyHashtable(): void + { + $fn = $this->context->lookupFunction('__hashtable__readStringKeyHashtable'); + $block = $fn->appendBasicBlock('main'); + $this->context->builder->positionAtEnd($block); + $ht = $fn->getParam(0); + $key = $fn->getParam(1); + $htPtr = $this->context->getTypeFromString('__hashtable__*'); + $valPtr = $this->lookupStringKeyValue($fn, $block, $ht, $key); + $afterLookup = $fn->appendBasicBlock('strkey_read_ht_after_lookup'); + $this->context->builder->branch($afterLookup); + $this->context->builder->positionAtEnd($afterLookup); + $isNull = $this->context->builder->icmp(Builder::INT_EQ, $valPtr, $valPtr->typeOf()->constNull()); + $empty = $fn->appendBasicBlock('strkey_read_ht_empty'); + $read = $fn->appendBasicBlock('strkey_read_ht_read'); + $merge = $fn->appendBasicBlock('strkey_read_ht_merge'); + $this->context->builder->branchIf($isNull, $empty, $read); + $this->context->builder->positionAtEnd($read); + $child = $this->context->builder->call( + $this->context->lookupFunction('__value__readHashtable'), + $valPtr + ); + $this->context->builder->branch($merge); + $this->context->builder->positionAtEnd($empty); + $this->context->builder->branch($merge); + $this->context->builder->positionAtEnd($merge); + $result = $this->context->builder->phi($htPtr); + $result->addIncoming($child, $read); + $result->addIncoming($htPtr->constNull(), $empty); + $this->context->builder->returnValue($result); + } + + private function implementValueReadHashtable(): void + { + $fn = $this->context->lookupFunction('__value__readHashtable'); + $block = $fn->appendBasicBlock('main'); + $this->context->builder->positionAtEnd($block); + $value = $fn->getParam(0); + $map = $this->context->structFieldMap['__value__']; + $htPtr = $this->context->getTypeFromString('__hashtable__*'); + $typeByte = $this->context->builder->load($this->context->builder->structGep($value, $map['type'])); + $expected = $this->context->getTypeFromString('int8')->constInt(Variable::TYPE_HASHTABLE, false); + $isHt = $this->context->builder->icmp(Builder::INT_EQ, $typeByte, $expected); + $ok = $fn->appendBasicBlock('read_ht_ok'); + $empty = $fn->appendBasicBlock('read_ht_empty'); + $merge = $fn->appendBasicBlock('read_ht_merge'); + $this->context->builder->branchIf($isHt, $ok, $empty); + $this->context->builder->positionAtEnd($ok); + $ptrField = $this->context->builder->structGep($value, $map['value']); + $htSlot = $this->context->builder->pointerCast($ptrField, $htPtr->pointerType(0)); + $stored = $this->context->builder->load($htSlot); + $this->context->builder->branch($merge); + $this->context->builder->positionAtEnd($empty); + $this->context->builder->branch($merge); + $this->context->builder->positionAtEnd($merge); + $result = $this->context->builder->phi($htPtr); + $result->addIncoming($stored, $ok); + $result->addIncoming($htPtr->constNull(), $empty); + $this->context->builder->returnValue($result); + } + + private function implementValueWriteHashtable(): void + { + $fn = $this->context->lookupFunction('__value__writeHashtable'); + $block = $fn->appendBasicBlock('main'); + $this->context->builder->positionAtEnd($block); + $value = $fn->getParam(0); + $hashtable = $fn->getParam(1); + $map = $this->context->structFieldMap['__value__']; + $htPtr = $this->context->getTypeFromString('__hashtable__*'); + $this->context->builder->call( + $this->context->lookupFunction('__value__valueDelref'), + $value + ); + $this->context->builder->store( + $this->context->getTypeFromString('int8')->constInt(Variable::TYPE_HASHTABLE, false), + $this->context->builder->structGep($value, $map['type']) + ); + $ptrField = $this->context->builder->structGep($value, $map['value']); + $htSlot = $this->context->builder->pointerCast($ptrField, $htPtr->pointerType(0)); + $this->context->builder->store($hashtable, $htSlot); + $this->context->builder->returnVoid(); + } + private function lookupStringKeyValue( PHPLLVM\Value\Function_ $fn, PHPLLVM\BasicBlock $block, diff --git a/lib/JIT/SuperglobalInit.php b/lib/JIT/SuperglobalInit.php index 39a3f3bac..e529f1f65 100644 --- a/lib/JIT/SuperglobalInit.php +++ b/lib/JIT/SuperglobalInit.php @@ -54,19 +54,52 @@ private static function populateFromVm(Context $context, \PHPLLVM\Value $ht, str if (!$table instanceof HashTable) { return; } + self::populateHashTableFromVm($context, $ht, $table); + } + + private static function populateHashTableFromVm( + Context $context, + \PHPLLVM\Value $ht, + HashTable $table + ): void { $setString = $context->lookupFunction('__hashtable__setStringKeyString'); + $setHashtable = $context->lookupFunction('__hashtable__setStringKeyHashtable'); foreach ($table->iterateKeyed(true) as [$keyVar, $valueVar]) { + $resolved = $valueVar->resolveIndirect(); + if (VMVariable::TYPE_INTEGER === $keyVar->type) { + if (VMVariable::TYPE_STRING !== $resolved->type) { + continue; + } + $str = $context->builder->load( + $context->constantStringFromString($resolved->toString()) + ); + $context->builder->call( + $context->lookupFunction('__hashtable__setStringAt'), + $ht, + $context->getTypeFromString('size_t')->constInt($keyVar->toInt(), false), + $str + ); + + continue; + } if (VMVariable::TYPE_STRING !== $keyVar->type) { continue; } $key = $context->builder->load( $context->constantStringFromString($keyVar->toString()) ); - if (VMVariable::TYPE_STRING === $valueVar->type) { + if (VMVariable::TYPE_STRING === $resolved->type) { $str = $context->builder->load( - $context->constantStringFromString($valueVar->toString()) + $context->constantStringFromString($resolved->toString()) ); $context->builder->call($setString, $ht, $key, $str); + } elseif (VMVariable::TYPE_ARRAY === $resolved->type) { + $nested = $resolved->toArray(); + if ($nested instanceof HashTable) { + $child = $context->builder->call($context->lookupFunction('__hashtable__alloc')); + self::populateHashTableFromVm($context, $child, $nested); + $context->builder->call($setHashtable, $ht, $key, $child); + } } } } diff --git a/lib/JIT/Variable.php b/lib/JIT/Variable.php index 2de266455..95047be1e 100755 --- a/lib/JIT/Variable.php +++ b/lib/JIT/Variable.php @@ -373,7 +373,11 @@ public function dimFetch(self $dim, ?Type $expectedType = null): Variable { $ptr, ); case self::TYPE_HASHTABLE: - if (null !== $this->superglobalName && self::TYPE_STRING === $dim->type) { + if ( + null !== $this->superglobalName + && self::TYPE_STRING === $dim->type + && (null === $expectedType || Type::TYPE_ARRAY !== $expectedType->type) + ) { $key = $dim->compileTimeString; if (null !== $key) { $baked = SuperglobalInit::compileTimeReadString( @@ -393,10 +397,25 @@ public function dimFetch(self $dim, ?Type $expectedType = null): Variable { } $ht = $this->context->helper->loadValue($this); if (self::TYPE_STRING === $dim->type) { + $key = $this->context->helper->loadValue($dim); + if (null !== $expectedType && Type::TYPE_ARRAY === $expectedType->type) { + $childHt = $this->context->builder->call( + $this->context->lookupFunction('__hashtable__readStringKeyHashtable'), + $ht, + $key + ); + + return new Variable( + $this->context, + self::TYPE_HASHTABLE, + self::KIND_VALUE, + $childHt + ); + } $valPtr = $this->context->builder->call( $this->context->lookupFunction('__hashtable__readStringKeyValue'), $ht, - $this->context->helper->loadValue($dim) + $key ); $str = $this->context->builder->call( $this->context->lookupFunction('__value__readString'), diff --git a/phpunit.xml.dist b/phpunit.xml.dist index aaae7f4f7..5ade03643 100755 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -39,6 +39,8 @@ ./test/aot/ExampleWebAotTest.php ./test/aot/StdlibWebBuiltinsTest.php ./test/aot/RuntimeSuperglobalRefreshTest.php + ./test/aot/NestedSuperglobalsAotTest.php + ./test/aot/WebAppNumberFormatTest.php diff --git a/script/ci-local.sh b/script/ci-local.sh index 7a6a4f04a..7d75fda77 100755 --- a/script/ci-local.sh +++ b/script/ci-local.sh @@ -14,7 +14,7 @@ else python3 -c "import urllib.request; urllib.request.urlretrieve('https://getcomposer.org/download/latest-stable/composer.phar','/tmp/composer.phar')" COMPOSER=("$PHP_BIN" -d "extension=$EXT_DIR/phar.so" -d "extension=$EXT_DIR/mbstring.so" /tmp/composer.phar) fi -"${COMPOSER[@]}" install --no-interaction --ignore-platform-reqs --no-plugins 2>/dev/null || true +"${COMPOSER[@]}" install --no-interaction --ignore-platform-reqs 2>/dev/null || true chmod +x script/install-llvm9.sh script/apply-patches.sh 2>/dev/null || true if [[ -x script/install-llvm9.sh ]]; then diff --git a/script/docker-capability-matrix.sh b/script/docker-capability-matrix.sh new file mode 100755 index 000000000..dd4b10a33 --- /dev/null +++ b/script/docker-capability-matrix.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Regenerate docs/capabilities.md inside php:8.2-cli-bookworm and copy back via docker cp. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +IMAGE="${PHP_COMPILER_DOCKER_IMAGE:-php:8.2-cli-bookworm}" +CID=$(tar -C "$ROOT" -cf - --exclude='.git' --exclude='.phpunit.result.cache' . | docker run -i -d "$IMAGE" bash -c 'mkdir -p /compiler && tar xf - -C /compiler && exec sleep 600') +cleanup() { docker rm -f "$CID" >/dev/null 2>&1 || true; } +trap cleanup EXIT +docker exec "$CID" bash -lc ' +set -e +cd /compiler +apt-get update -qq +DEBIAN_FRONTEND=noninteractive apt-get install -y -qq pkg-config libffi-dev unzip curl > /dev/null +docker-php-ext-install -j"$(nproc)" ffi > /dev/null 2>&1 +if ! command -v composer >/dev/null 2>&1; then + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer +fi +composer install --no-interaction --ignore-platform-reqs > /dev/null 2>&1 +chmod +x script/*.sh +script/apply-patches.sh > /dev/null 2>&1 || true +php script/capability-matrix.php +' +docker cp "$CID:/compiler/docs/capabilities.md" "$ROOT/docs/capabilities.md" +echo "Updated $ROOT/docs/capabilities.md" diff --git a/script/docker-ci.sh b/script/docker-ci.sh index ecb7a76c0..6c607e336 100755 --- a/script/docker-ci.sh +++ b/script/docker-ci.sh @@ -12,7 +12,7 @@ mkdir -p /compiler && tar xf - -C /compiler cd /compiler apt-get update -qq DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ - build-essential dpkg-dev pkg-config libffi-dev unzip git curl > /dev/null + build-essential dpkg-dev pkg-config libffi-dev libbsd0 unzip git curl > /dev/null docker-php-ext-install -j"$(nproc)" ffi > /dev/null 2>&1 if ! command -v composer >/dev/null 2>&1; then curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer diff --git a/test/aot/NestedSuperglobalsAotTest.php b/test/aot/NestedSuperglobalsAotTest.php new file mode 100644 index 000000000..9edaa8ad9 --- /dev/null +++ b/test/aot/NestedSuperglobalsAotTest.php @@ -0,0 +1,191 @@ +compileBin = realpath(__DIR__ . '/../../bin/compile.php'); + if (!self::isLlvmReady()) { + $this->markTestSkipped( + 'LLVM 9 toolchain not available. Run script/install-llvm9.sh from the repository root.' + ); + } + } + + public function testNestedQueryStringStillServesFlatGetKey(): void + { + $source = <<<'PHP' +assertNotFalse($outfile); + unlink($outfile); + + $repoRoot = dirname(__DIR__, 2); + $env = self::llvmEnvironment($repoRoot); + $env['QUERY_STRING'] = 'user[name]=Ada&flat=ok'; + + $result = $this->compileAndRun($source, [], $outfile, $env); + @unlink($outfile); + + $this->assertSame("ok\n", $result); + } + + /** + * @param list $extraArgv + * @param array $runEnv + */ + private function compileAndRun(string $code, array $extraArgv, string $outfile, array $runEnv): string + { + $repoRoot = dirname(__DIR__, 2); + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $compileArgv = array_merge( + self::llvmEnvPrefix(), + self::phpCommand(), + array_merge([$this->compileBin], $extraArgv, ['-o', $outfile]) + ); + $compile = proc_open($compileArgv, $descriptorSpec, $pipes, $repoRoot, $runEnv); + fwrite($pipes[0], $code); + fclose($pipes[0]); + $compileErr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($compile); + + $this->assertFileExists($outfile, trim($compileErr !== false ? $compileErr : '')); + $this->assertTrue(is_executable($outfile)); + + $run = proc_open([$outfile], $descriptorSpec, $runPipes, $repoRoot, $runEnv); + $output = stream_get_contents($runPipes[1]); + fclose($runPipes[0]); + fclose($runPipes[1]); + fclose($runPipes[2]); + $exitCode = proc_close($run); + $this->assertSame(0, $exitCode, 'AOT binary should exit with status 0'); + + return $output !== false ? $output : ''; + } + + /** + * @return array + */ + private static function llvmEnvironment(string $repoRoot): array + { + $env = []; + foreach (array_merge($_ENV, $_SERVER) as $key => $value) { + if (is_string($value)) { + $env[$key] = $value; + } + } + $llvmDir = $repoRoot.'/.llvm'; + if (is_file($llvmDir.'/libLLVM-9.so.1')) { + $prefix = realpath($llvmDir) ?: $llvmDir; + $env['PHP_COMPILER_LLVM_PATH'] = $prefix; + $ld = $env['LD_LIBRARY_PATH'] ?? ''; + $env['LD_LIBRARY_PATH'] = '' === $ld ? $prefix : $prefix.':'.$ld; + $path = $env['PATH'] ?? ''; + $env['PATH'] = '' === $path ? $prefix : $prefix.':'.$path; + } + + return $env; + } + + private static function isLlvmReady(): bool + { + if (null !== self::$llvmReady) { + return self::$llvmReady; + } + $llvmDir = dirname(__DIR__, 2).'/.llvm'; + if (!is_file($llvmDir.'/libLLVM-9.so.1')) { + self::$llvmReady = false; + + return false; + } + if ('' === getenv('PHP_COMPILER_LLVM_PATH')) { + putenv('PHP_COMPILER_LLVM_PATH='.$llvmDir); + } + try { + \PHPLLVM\Chooser::choose(); + self::$llvmReady = true; + } catch (\Throwable $e) { + self::$llvmReady = false; + } + + return self::$llvmReady; + } + + /** + * @return list + */ + private static function phpCommand(): array + { + $phpEnv = getenv('PHP_COMPILER_PHP'); + if (false !== $phpEnv && '' !== $phpEnv) { + $cmd = preg_split('/\s+/', $phpEnv); + } else { + $cmd = [PHP_BINARY]; + } + $extDir = getenv('PHP_COMPILER_EXT_DIR') ?: '/usr/lib/php/20220829'; + if (is_dir($extDir)) { + foreach (['tokenizer', 'mbstring', 'dom', 'xml', 'xmlwriter', 'ffi', 'posix', 'phar'] as $ext) { + $so = $extDir.'/'.$ext.'.so'; + if (is_file($so)) { + $cmd[] = '-d'; + $cmd[] = 'extension='.$so; + } + } + } + $cmd[] = '-d'; + $cmd[] = 'display_errors=0'; + + return $cmd; + } + + /** + * @return list + */ + private static function llvmEnvPrefix(): array + { + $llvmDir = dirname(__DIR__, 2).'/.llvm'; + if (!is_file($llvmDir.'/libLLVM-9.so.1')) { + return []; + } + $prefix = realpath($llvmDir) ?: $llvmDir; + $ld = getenv('LD_LIBRARY_PATH'); + $ldVal = false === $ld || '' === $ld ? $prefix : $prefix.':'.$ld; + $path = getenv('PATH'); + $pathVal = false === $path || '' === $path ? $prefix : $prefix.':'.$path; + + return [ + 'env', + 'LD_LIBRARY_PATH='.$ldVal, + 'PATH='.$pathVal, + 'PHP_COMPILER_LLVM_PATH='.$prefix, + ]; + } +} diff --git a/test/fixtures/aot/cases/nested_get_params.phpt b/test/fixtures/aot/cases/nested_get_params.phpt new file mode 100644 index 000000000..9e0862d8e --- /dev/null +++ b/test/fixtures/aot/cases/nested_get_params.phpt @@ -0,0 +1,11 @@ +--TEST-- +AOT: nested QUERY_STRING does not break flat $_GET keys (C runtime refresh) +--ENV-- +QUERY_STRING=user[name]=Ada&flat=ok +--FILE-- +