PHP で HTTP/2 の HPACK を最小実装。目的は HTTP/2 ヘッダーブロックの decode/encode を最小限に扱えるようにすることです。
HPACK の静的テーブル(Static Table)だけを扱うこと
Dynamic Table は実装しない
サイズ更新(Dynamic Table Size Update)も未対応
インデクシングに伴う追加・退避も未対応
Huffman 符号化は未対応(常に Huffman=0 として扱う)
decode で Huffman=1 が来たら例外(Unsupported)でよい
ヘッダーフィールド表現(Header Field Representation)の parse と generateを実装する 具体的には次を扱う(HPACK RFC の表現種別):
Indexed Header Field Representation(先頭ビット 1xxxxxxx)
Literal Header Field without Indexing(0000xxxx)
Literal Header Field never Indexed(0001xxxx) ※ with indexing(01xxxxxx)は decode だけ Unsupported でも良い(encode は不要)
スコープ外(やらない)
Dynamic table への追加・参照、eviction、サイズ計算
Huffman の decode/encode
HPACK 以外(HTTP/2 フレーム処理など)
src/Hpack/Decoder.php
src/Hpack/Encoder.php
src/Hpack/StaticTable.php
src/Hpack/Integer.php(HPACK integer representation: N-bit prefix)
src/Hpack/StringLiteral.php(Huffman=0 の文字列リテラル)
src/Hpack/Exceptions.php(Unsupported 等)
tests/(phpunit でも素の assert でも良い)
Composer 前提でも、単体スクリプト実行でもよいが、外部依存は極力なしで。
Hpack\Decoder::decode(string $headerBlock): array
戻り値は [['name' => string, 'value' => string], ...] の配列(順序保持)
Hpack\Encoder::encode(array $headers): string
入力は [['name'=>..., 'value'=>...], ...] または ['name' => 'value'] でも良い(どちらかに統一)
encode は最小でよい。基本方針:
(name,value) が静的テーブルに完全一致するなら Indexed を出す
(name) だけ一致するなら Literal (without indexing) with indexed name を出す
それも無ければ Literal (without indexing) with new name を出す
文字列は Huffman=0 固定
Integer 表現(HPACK Integer Representation)
decodeInteger(string $data, int $offset, int $prefixBits): array{int $value, int $nextOffset}
encodeInteger(int $value, int $prefixBits, int $prefixMaskBase): string
prefixBits: 5, 6, 7 のケースが出る
例: Indexed は prefix=7(先頭 1bit 以外が prefix)
不正入力(バッファ不足、継続バイト不足、過大シフトなど)は例外
String Literal(Huffmanなし固定)
先頭ビット: Huffman flag(MSB)。1なら Unsupported
長さは prefix=7 integer
その後ろに length バイトの生文字列(バイナリ安全に扱う)
decode で不足したら例外
Static Table
HPACK の static table を 1-indexed で定義
lookup:
getByIndex(int $i): array{name:string,value:string}
findIndexByPair(string $name, string $value): ?int
findIndexByName(string $name): ?int(最小 index を返すでよい)
Decoder::decode() は headerBlock を先頭から読み、表現に応じてヘッダーを配列に追加する。
Indexed:
index=0 は invalid(例外)
index が static table 範囲外なら Unsupported(dynamic 未実装のため)
Literal without indexing / never indexed:
name が indexed か new name かを判定して読む
value は string literal
Dynamic Table Size Update(001xxxxx)が来たら Unsupported(今回 scope 外)
可能なら Indexed を使う
それ以外は Literal without indexing を使う
never indexed は encode では基本使わない(オプションでフラグ指定があってもよい)
出力はバイナリ文字列(PHP の string)
少なくとも以下のテストを用意してください(ベタな assert で可):
Integer decode/encode: prefix=5/6/7 の境界(0, maxPrefix-1, maxPrefix, maxPrefix+1, 大きめ)
String literal decode/encode: 空文字、ASCII、バイナリ含む(\x00 等)、不足時例外
Indexed の decode: static table の典型例(:method: GET など)
Literal without indexing(indexed name)の decode/encode
Literal without indexing(new name)の decode/encode
Huffman=1 を含む string literal を decode したら Unsupported
Dynamic table update を見たら Unsupported
不正 index=0 は例外
バッファ不足系(途中で切れた header block)例外
コーディング指針
すべての関数は バイナリ安全(strlen, ord, substr を前提に注意)
例外メッセージはデバッグしやすく(offset, kind を含める)
strict_types=1
PHP 8.2+ を想定