blob: 3b9a995fa0c790ccf923da942dc42260fe1bd7d8 [file] [log] [blame] [edit]
// 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 && goexperiment.synctest
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")
}
})
}
}