From b706afaf671ae70f6451f1bd9f1e4ed6c9e9d18d Mon Sep 17 00:00:00 2001 From: Christopher Vagnetoft Date: Sun, 25 Feb 2024 15:49:14 +0100 Subject: [PATCH] Implemented header packing logic --- README.md | 6 +++--- src/Header/HeaderBag.php | 10 ++++++++- src/Header/HeaderPacker.php | 34 +++++++++++++++++++++++++++++-- tests/Header/HeaderPackerTest.php | 19 ++++++++++++++++- 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 55fe406..7b2730e 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ This is a project that is exploring the feasability and practicality of bringing Currently implemented: -* Huffman encoding (for compressed headers) -* Header parsing (including dictionary) -* Frame parsing (not all frame types) +* Huffman encoding and decoding (for compressed headers) +* Header parsing and packing (only static dictionary, no dynamic dictionary) +* Frame parsing core (not all frame types) ## Notes diff --git a/src/Header/HeaderBag.php b/src/Header/HeaderBag.php index 7d723dd..6cf9ced 100644 --- a/src/Header/HeaderBag.php +++ b/src/Header/HeaderBag.php @@ -2,15 +2,18 @@ namespace NoccyLabs\React\Http2\Header; +use ArrayIterator; +use IteratorAggregate; use NoccyLabs\React\Http2\Huffman\Codec; use NoccyLabs\React\Http2\Huffman\Dictionary; +use Traversable; /** * An ordered list of HTTP/2 headers * * */ -class HeaderBag +class HeaderBag implements IteratorAggregate { private array $headers = []; @@ -24,4 +27,9 @@ class HeaderBag { $this->headers[] = [ $name, $value ]; } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->headers); + } } \ No newline at end of file diff --git a/src/Header/HeaderPacker.php b/src/Header/HeaderPacker.php index ef44a8e..792bd22 100644 --- a/src/Header/HeaderPacker.php +++ b/src/Header/HeaderPacker.php @@ -79,6 +79,8 @@ class HeaderPacker ]; private const STATIC_TABLE_SIZE = 62; // considering unused index 0 + private array $dynamicTable = []; + public function __construct() { if (!self::$huffman) self::$huffman = new Codec(Dictionary::createRfc7541Dictionary()); @@ -88,6 +90,20 @@ class HeaderPacker { $packed = ''; + foreach ($headers as [$header,$value]) { + if ($index = $this->lookupIndexForHeader($header,$value)) { + // Indexed + $packed .= chr(0x80 | ($index & 0x7F)); + } elseif ($index = $this->lookupIndexForHeader($header,null)) { + // Indexed literal + $packed .= chr(0x40 | ($index & 0x3F)); + $value = self::$huffman->encode($value); + $packed .= chr(0x80 | strlen($value) & 0x7F); + $packed .= $value; + } else { + // Literal + } + } return $packed; } @@ -149,7 +165,21 @@ class HeaderPacker private function getIndexedHeader(int $index): array { - return self::$staticTable[$index]; + if ($index < self::STATIC_TABLE_SIZE) + return self::$staticTable[$index]; + // TODO bounds check + return $this->dynamicTable[$index - self::STATIC_TABLE_SIZE]; } -} \ No newline at end of file + private function lookupIndexForHeader(string $header, ?string $value): ?int + { + foreach (self::$staticTable as $index=>[$iName, $iValue]) { + if ($header === $iName && $value === $iValue) return $index; + } + foreach ($this->dynamicTable as $index=>[$iName, $iValue]) { + if ($header === $iName && $value === $iValue) return $index + self::STATIC_TABLE_SIZE; + } + return null; + } + +} diff --git a/tests/Header/HeaderPackerTest.php b/tests/Header/HeaderPackerTest.php index dc66449..5b8f8cd 100644 --- a/tests/Header/HeaderPackerTest.php +++ b/tests/Header/HeaderPackerTest.php @@ -26,5 +26,22 @@ class HeaderPackerTest extends \PHPUnit\Framework\TestCase } + public function testPackingExamplesFromRfc7541() + { + $packer = new HeaderPacker(); -} \ No newline at end of file + $expect = "\x82\x86\x84\x41\x8c\xf1\xe3\xc2\xe5\xf2\x3a\x6b\xa0\xab\x90\xf4\xff"; + $in = new HeaderBag([ + ':method' => 'GET', + ':scheme' => 'http', + ':path' => '/', + ':authority' => 'www.example.com' + ]); + $out = $packer->packHeaders($in); + + $this->assertEquals($expect, $out); + + } + + +}