// Copyright 2025 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build go1.24 package http3 import ( "reflect" "strings" "testing" ) func TestQPACKDecode(t *testing.T) { type header struct { itype indexType name, value string } // Many test cases here taken from Google QUICHE, // quiche/quic/core/qpack/qpack_encoder_test.cc. for _, test := range []struct { name string enc []byte want []header }{{ name: "empty", enc: unhex("0000"), want: []header{}, }, { name: "literal entry empty value", enc: unhex("000023666f6f00"), want: []header{ {mayIndex, "foo", ""}, }, }, { name: "simple literal entry", enc: unhex("000023666f6f03626172"), want: []header{ {mayIndex, "foo", "bar"}, }, }, { name: "multiple literal entries", enc: unhex("0000" + // prefix // foo: bar "23666f6f03626172" + // 7 octet long header name, the smallest number // that does not fit on a 3-bit prefix. "2700666f6f62616172" + // 127 octet long header value, the smallest number // that does not fit on a 7-bit prefix. "7f00616161616161616161616161616161616161616161616161616161616161616161" + "6161616161616161616161616161616161616161616161616161616161616161616161" + "6161616161616161616161616161616161616161616161616161616161616161616161" + "616161616161616161616161616161616161616161616161", ), want: []header{ {mayIndex, "foo", "bar"}, {mayIndex, "foobaar", strings.Repeat("a", 127)}, }, }, { name: "line feed in value", enc: unhex("000023666f6f0462610a72"), want: []header{ {mayIndex, "foo", "ba\nr"}, }, }, { name: "huffman simple", enc: unhex("00002f0125a849e95ba97d7f8925a849e95bb8e8b4bf"), want: []header{ {mayIndex, "custom-key", "custom-value"}, }, }, { name: "alternating huffman nonhuffman", enc: unhex("0000" + // Prefix. "2f0125a849e95ba97d7f" + // Huffman-encoded name. "8925a849e95bb8e8b4bf" + // Huffman-encoded value. "2703637573746f6d2d6b6579" + // Non-Huffman encoded name. "0c637573746f6d2d76616c7565" + // Non-Huffman encoded value. "2f0125a849e95ba97d7f" + // Huffman-encoded name. "0c637573746f6d2d76616c7565" + // Non-Huffman encoded value. "2703637573746f6d2d6b6579" + // Non-Huffman encoded name. "8925a849e95bb8e8b4bf", // Huffman-encoded value. ), want: []header{ {mayIndex, "custom-key", "custom-value"}, {mayIndex, "custom-key", "custom-value"}, {mayIndex, "custom-key", "custom-value"}, {mayIndex, "custom-key", "custom-value"}, }, }, { name: "static table", enc: unhex("0000d1d45f00055452414345dfcc5f108621e9aec2a11f5c8294e75f1000"), want: []header{ {mayIndex, ":method", "GET"}, {mayIndex, ":method", "POST"}, {mayIndex, ":method", "TRACE"}, {mayIndex, "accept-encoding", "gzip, deflate, br"}, {mayIndex, "location", ""}, {mayIndex, "accept-encoding", "compress"}, {mayIndex, "location", "foo"}, {mayIndex, "accept-encoding", ""}, }, }} { runSynctestSubtest(t, test.name, func(t testing.TB) { st1, st2 := newStreamPair(t) st1.Write(test.enc) st1.Flush() st2.lim = int64(len(test.enc)) var dec qpackDecoder got := []header{} err := dec.decode(st2, func(itype indexType, name, value string) error { got = append(got, header{itype, name, value}) return nil }) if err != nil { t.Fatalf("decode: %v", err) } if !reflect.DeepEqual(got, test.want) { t.Errorf("encoded: %x", test.enc) t.Errorf("got headers:") for _, h := range got { t.Errorf(" %v: %q", h.name, h.value) } t.Errorf("want headers:") for _, h := range test.want { t.Errorf(" %v: %q", h.name, h.value) } } }) } } func TestQPACKDecodeErrors(t *testing.T) { // Many test cases here taken from Google QUICHE, // quiche/quic/core/qpack/qpack_encoder_test.cc. for _, test := range []struct { name string enc []byte }{{ name: "literal entry empty name", enc: unhex("00002003666f6f"), }, { name: "literal entry empty name and value", enc: unhex("00002000"), }, { name: "name length too large for varint", enc: unhex("000027ffffffffffffffffffff"), }, { name: "string literal too long", enc: unhex("000027ffff7f"), }, { name: "value length too large for varint", enc: unhex("000023666f6f7fffffffffffffffffffff"), }, { name: "value length too long", enc: unhex("000023666f6f7fffff7f"), }, { name: "incomplete header block", enc: unhex("00002366"), }, { name: "huffman name does not have eos prefix", enc: unhex("00002f0125a849e95ba97d7e8925a849e95bb8e8b4bf"), }, { name: "huffman value does not have eos prefix", enc: unhex("00002f0125a849e95ba97d7f8925a849e95bb8e8b4be"), }, { name: "huffman name eos prefix too long", enc: unhex("00002f0225a849e95ba97d7fff8925a849e95bb8e8b4bf"), }, { name: "huffman value eos prefix too long", enc: unhex("00002f0125a849e95ba97d7f8a25a849e95bb8e8b4bfff"), }, { name: "too high static table index", enc: unhex("0000ff23ff24"), }} { runSynctestSubtest(t, test.name, func(t testing.TB) { st1, st2 := newStreamPair(t) st1.Write(test.enc) st1.Flush() st2.lim = int64(len(test.enc)) var dec qpackDecoder err := dec.decode(st2, func(itype indexType, name, value string) error { return nil }) if err == nil { t.Errorf("encoded: %x", test.enc) t.Fatalf("decode succeeded; want error") } }) } }