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
1 change: 1 addition & 0 deletions lib/JIT.php
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,7 @@ private function assignOperand(Operand $result, Variable $value): void {
$valueRef,
$this->context->helper->loadValue($value)
);
$result->valueBoxHashtable = true;

return;
case Variable::TYPE_VALUE:
Expand Down
49 changes: 34 additions & 15 deletions lib/JIT/Variable.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ final class Variable {
/** String literal value when this variable represents a constant string operand. */
public ?string $compileTimeString = null;

/** {@see __value__} slot holds a nested {@see __hashtable__} (e.g. $_FILES['field']). */
public bool $valueBoxHashtable = false;

private static int $lvalueCounter = 0;
public int $nextFreeElement = 0;

Expand Down Expand Up @@ -419,42 +422,39 @@ public function dimFetch(self $dim, ?Type $expectedType = null, bool $forWrite =

return HashTableHelper::writableStringKeyValueBox($this->context, $ht, $key);
}
if (null !== $expectedType && Type::TYPE_ARRAY === $expectedType->type) {
if ('_FILES' === $this->superglobalName && !$forWrite) {
$childHt = $this->context->builder->call(
$this->context->lookupFunction('__hashtable__readStringKeyHashtable'),
$ht,
$key
);

return new Variable(
$this->context,
self::TYPE_HASHTABLE,
self::KIND_VALUE,
$childHt
);
}
if (null !== $expectedType && Type::TYPE_STRING === $expectedType->type) {
$valPtr = $this->context->builder->call(
$this->context->lookupFunction('__hashtable__readStringKeyValue'),
if (null !== $expectedType && Type::TYPE_ARRAY === $expectedType->type) {
$childHt = $this->context->builder->call(
$this->context->lookupFunction('__hashtable__readStringKeyHashtable'),
$ht,
$key
);
$str = $this->context->builder->call(
$this->context->lookupFunction('__value__readString'),
$valPtr
);
$owned = $this->context->builder->call(
$this->context->lookupFunction('__string__separate'),
$str
);

return new Variable(
$this->context,
self::TYPE_STRING,
self::TYPE_HASHTABLE,
self::KIND_VALUE,
$owned
$childHt
);
}
if (null !== $expectedType && Type::TYPE_STRING === $expectedType->type) {
$this->context->refcount->addref($ht);
$boxed = HashTableHelper::readStringKeyToValueBox($this->context, $ht, $key);

return $boxed;
}

$this->context->refcount->addref($ht);
$boxed = HashTableHelper::readStringKeyToValueBox($this->context, $ht, $key);
Expand Down Expand Up @@ -491,6 +491,25 @@ public function dimFetch(self $dim, ?Type $expectedType = null, bool $forWrite =
);
}
return HashTableHelper::readIndexedToValueBox($this->context, $ht, $index);
case self::TYPE_VALUE:
if (!$this->valueBoxHashtable || self::TYPE_STRING !== $dim->type) {
throw new \LogicException(
'Array dim fetch on __value__ requires a nested hashtable in this compiler build'
);
}
$valPtr = JitValueBox::pointer($this->context, $this->value);
$childHt = $this->context->builder->call(
$this->context->lookupFunction('__value__readHashtable'),
$valPtr
);
$htVar = new Variable(
$this->context,
self::TYPE_HASHTABLE,
self::KIND_VALUE,
$childHt
);

return $htVar->dimFetch($dim, $expectedType, $forWrite);
default:
if (!($this->type & self::IS_NATIVE_ARRAY)) {
throw new \LogicException("Unsupported dim fetch on " . self::getStringType($this->type));
Expand Down
60 changes: 60 additions & 0 deletions test/aot/RuntimeSuperglobalRefreshTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,66 @@ public function testMultipartPostBody(): void
@unlink($bodyFile);
}

/**
* multipart upload populates nested $_FILES for AOT JIT dim fetch (issue #87).
*/
public function testNestedFilesFieldAot(): void
{
$source = <<<'PHP'
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
echo $_FILES['doc']['name'];
PHP;

$outfile = tempnam(sys_get_temp_dir(), 'phpc_files_nested_');
$this->assertNotFalse($outfile);
unlink($outfile);

$repoRoot = dirname(__DIR__, 2);
$env = $this->llvmProcessEnv($repoRoot);
$descriptorSpec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];

$compile = proc_open(
array_merge(
self::llvmEnvPrefix(),
self::phpCommand(),
[$this->compileBin, '-o', $outfile]
),
$descriptorSpec,
$pipes,
$repoRoot,
$env
);
fwrite($pipes[0], $source);
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 : ''));

$runEnv = $env;
$runEnv['REQUEST_METHOD'] = 'POST';
$runEnv['REQUEST_BODY'] = "--phpcFileB\r\n"
."Content-Disposition: form-data; name=\"doc\"; filename=\"f.txt\"\r\n"
."Content-Type: text/plain\r\n\r\n"
."bytes\r\n"
."--phpcFileB--\r\n";
$runEnv['CONTENT_TYPE'] = 'multipart/form-data; boundary=phpcFileB';
$runEnv['SCRIPT_NAME'] = '/example.php';
$runEnv['REQUEST_URI'] = '/example.php';
$out = $this->runBinary($outfile, $runEnv);
$body = $this->cgiBody($out);
$this->assertStringContainsString('f.txt', $body);

@unlink($outfile);
}

public function testHttpsSchemeFromCgiEnvironment(): void
{
$source = <<<'PHP'
Expand Down
19 changes: 19 additions & 0 deletions test/compliance/cases/stdlib/files_nested.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
--TEST--
stdlib nested $_FILES access (issue #87)
--ENV--
REQUEST_METHOD=POST
CONTENT_TYPE=multipart/form-data; boundary=phpcJitFileB
--POST--
--phpcJitFileB
Content-Disposition: form-data; name="doc"; filename="photo.txt"
Content-Type: text/plain

filedata
--phpcJitFileB--
--FILE--
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
echo $_FILES['doc']['name'], "\n";
--EXPECT--
photo.txt
22 changes: 22 additions & 0 deletions test/fixtures/aot/cases/web_files_nested.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
AOT: nested $_FILES field access after multipart upload (issue #87)
--ENV--
REQUEST_METHOD=POST
CONTENT_TYPE=multipart/form-data; boundary=phpcAotFileB
--POST--
--phpcAotFileB
Content-Disposition: form-data; name="doc"; filename="f.txt"
Content-Type: text/plain

bytes
--phpcAotFileB--
--FILE--
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
echo $_FILES['doc']['name'];
--EXPECTF--
Content-Type: text/plain; charset=UTF-8
f.txt
--EXPECT_EXIT--
0
18 changes: 18 additions & 0 deletions test/real/cases/web_files_nested.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
Web: nested $_FILES keys from multipart POST (issue #87)
--ENV--
REQUEST_METHOD=POST
CONTENT_TYPE=multipart/form-data; boundary=phpcWebFileB
--POST--
--phpcWebFileB
Content-Disposition: form-data; name="doc"; filename="upload.txt"
Content-Type: text/plain

hello
--phpcWebFileB--
--FILE--
<?php
declare(strict_types=1);
echo $_FILES['doc']['name'], "\n";
--EXPECT--
upload.txt