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
7 changes: 4 additions & 3 deletions lib/VM/HashTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -581,10 +581,11 @@ private function rehash(): void {
if ($this->isWithoutHoles()) {
do {
$bucket = $this->buckets->read($bucketIndex);
$index = $bucket->hash;
$bucket->value->next = $index;
$this->indexes->write($index, $bucketIndex);
$hash = $bucket->hash;
$bucket->value->next = $this->indexes->read($hash);
$this->indexes->write($hash, $bucketIndex);
} while (++$bucketIndex < $this->numUsed);

return;
}
//todo
Expand Down
37 changes: 28 additions & 9 deletions lib/Web/DevServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,32 @@ public static function handleConnection($conn, string $docroot, callable $handle
*/
public static function readRequest($conn): ?array
{
$lines = '';
$buf = '';
while (!feof($conn)) {
$chunk = fgets($conn);
if (false === $chunk) {
$chunk = fread($conn, 8192);
if (false === $chunk || '' === $chunk) {
break;
}
$lines .= $chunk;
if ("\r\n" === $chunk) {
$buf .= $chunk;
if (str_contains($buf, "\r\n\r\n") || str_contains($buf, "\n\n")) {
break;
}
}

if (!preg_match('#^(\S+)\s+(\S+)\s+(HTTP/\S+)#', $lines, $m)) {
$headerEnd = strpos($buf, "\r\n\r\n");
$sepLen = 4;
if (false === $headerEnd) {
$headerEnd = strpos($buf, "\n\n");
$sepLen = 2;
}
if (false === $headerEnd) {
return null;
}

$headerBlock = substr($buf, 0, $headerEnd);
$body = substr($buf, $headerEnd + $sepLen);

if (!preg_match('#^(\S+)\s+(\S+)\s+(HTTP/\S+)#', $headerBlock, $m)) {
return null;
}

Expand All @@ -219,19 +232,25 @@ public static function readRequest($conn): ?array
}

$headers = [];
foreach (explode("\r\n", $lines) as $line) {
foreach (preg_split("/\r\n|\n/", $headerBlock) as $line) {
if ('' === $line || false === strpos($line, ':')) {
continue;
}
[$name, $value] = explode(':', $line, 2);
$headers[strtolower(trim($name))] = trim($value);
}

$body = '';
if (isset($headers['content-length'])) {
$len = (int) $headers['content-length'];
while (strlen($body) < $len && !feof($conn)) {
$body .= fread($conn, $len - strlen($body));
$chunk = fread($conn, $len - strlen($body));
if (false === $chunk || '' === $chunk) {
break;
}
$body .= $chunk;
}
if (strlen($body) > $len) {
$body = substr($body, 0, $len);
}
}

Expand Down
1 change: 1 addition & 0 deletions lib/Web/Superglobals.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public static function populateFromEnvironment(
);
self::populateServer($context, $queryString, $postBody);
self::populateRequest($context);
self::$activeContext = null;
}

/**
Expand Down
42 changes: 42 additions & 0 deletions test/real/ServeAotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,48 @@ public function testServeAotPopulatesContentLengthOnPost(): void
@rmdir($binaryDir);
}

public function testServeAotPostFormUrlencoded(): void
{
$docroot = $this->makeDocroot([
'form.php' => <<<'PHP'
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
echo 'name=', $_POST['name'];
PHP,
]);
$binaryDir = sys_get_temp_dir().'/phpc_serve_aot_post_'.bin2hex(random_bytes(4));
$this->assertTrue(mkdir($binaryDir));
$binary = $binaryDir.'/app';
$this->compileExample($docroot.'/form.php', $binary);
$response = $this->httpPostAot($docroot, $binary, '/form.php', 'name=Alice');
$this->assertStringContainsString('HTTP/1.1 200', $response);
$this->assertStringContainsString('name=Alice', $response);
@unlink($binary);
@rmdir($binaryDir);
}

public function testServeAotHttpResponseCode404SetsStatusLine(): void
{
$docroot = $this->makeDocroot([
'notfound.php' => <<<'PHP'
<?php
declare(strict_types=1);
http_response_code(404);
echo 'missing';
PHP,
]);
$binaryDir = sys_get_temp_dir().'/phpc_serve_aot_404_'.bin2hex(random_bytes(4));
$this->assertTrue(mkdir($binaryDir));
$binary = $binaryDir.'/app';
$this->compileExample($docroot.'/notfound.php', $binary);
$response = $this->httpGetAot($docroot, $binary, '/notfound.php');
$this->assertStringContainsString('HTTP/1.1 404', $response);
$this->assertStringContainsString('missing', $response);
@unlink($binary);
@rmdir($binaryDir);
}

public function testServeAotHttpResponseCode405SetsStatusLine(): void
{
$docroot = $this->makeDocroot([
Expand Down
14 changes: 14 additions & 0 deletions test/real/ServeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,20 @@ public function testPopulatesContentLengthOnPost(): void
$this->assertStringContainsString('12', $response);
}

public function testPostFormUrlencoded(): void
{
$docroot = $this->makeDocroot([
'form.php' => <<<'PHP'
<?php
header('Content-Type: text/plain; charset=UTF-8');
echo 'name=', $_POST['name'];
PHP,
]);
$response = $this->httpPost($docroot, '/form.php', 'name=Alice');
$this->assertStringContainsString('HTTP/1.1 200', $response);
$this->assertStringContainsString('name=Alice', $this->responseBody($response));
}

/**
* @param array<string, string> $extraEnv
* @param list<string> $extraRequestHeaders
Expand Down
27 changes: 27 additions & 0 deletions test/unit/VM/HashTableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,33 @@ public function testFindOnUninitializedReturnsNull(): void
$this->assertNull($ht->findIndex(0));
}

/** Regression: rehash must chain buckets (issue #248 POST / $_SERVER population). */
public function testAddManyStringKeysIncludingContentLength(): void
{
$ht = new HashTable();
$keys = [
'REQUEST_METHOD' => 'POST',
'QUERY_STRING' => '',
'SCRIPT_NAME' => '/index.php',
'PHP_SELF' => '/index.php',
'REQUEST_URI' => '/index.php',
'GATEWAY_INTERFACE' => 'CGI/1.1',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'SERVER_SOFTWARE' => 'PHP-Compiler-VM',
'CONTENT_LENGTH' => '0',
];
foreach ($keys as $name => $value) {
$var = new Variable(Variable::TYPE_STRING);
$var->string($value);
$this->assertNotNull($ht->add($name, $var), 'add failed for '.$name);
}
foreach ($keys as $name => $value) {
$found = $ht->find($name);
$this->assertNotNull($found, 'find failed for '.$name);
$this->assertSame($value, $found->resolveIndirect()->toString());
}
}

private function int(int $value): Variable
{
$var = new Variable();
Expand Down
27 changes: 27 additions & 0 deletions test/unit/Web/DevServerHeadersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,31 @@ public function testParsePeerAddressRejectsInvalid(): void
$this->assertNull(DevServer::parsePeerAddress('no-port'));
$this->assertNull(DevServer::parsePeerAddress('[::1]'));
}

public function testReadRequestPostBodyWithoutTrailingNewline(): void
{
$pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
if (false === $pair) {
$this->markTestSkipped('stream_socket_pair unavailable');
}
[$server, $client] = $pair;
$raw = "POST /form.php HTTP/1.1\r\n"
."Host: 127.0.0.1\r\n"
."Content-Type: application/x-www-form-urlencoded\r\n"
."Content-Length: 10\r\n"
."Connection: close\r\n\r\n"
.'name=Alice';
fwrite($client, $raw);
stream_socket_shutdown($client, STREAM_SHUT_WR);
fclose($client);

$parsed = DevServer::readRequest($server);
fclose($server);

$this->assertNotNull($parsed);
$this->assertSame('POST', $parsed[0]);
$this->assertSame('/form.php', $parsed[1]);
$this->assertSame('name=Alice', $parsed[4]);
$this->assertSame('10', $parsed[3]['content-length'] ?? null);
}
}
Loading