| // 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 ( |
| "bytes" |
| "strings" |
| "testing" |
| ) |
| |
| func TestQPACKEncode(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 |
| headers []header |
| want []byte |
| }{{ |
| name: "empty", |
| headers: []header{}, |
| want: unhex("0000"), |
| }, { |
| name: "empty name", |
| headers: []header{ |
| {mayIndex, "", "foo"}, |
| }, |
| want: unhex("0000208294e7"), |
| }, { |
| name: "empty value", |
| headers: []header{ |
| {mayIndex, "foo", ""}, |
| }, |
| want: unhex("00002a94e700"), |
| }, { |
| name: "empty name and value", |
| headers: []header{ |
| {mayIndex, "", ""}, |
| }, |
| want: unhex("00002000"), |
| }, { |
| name: "simple", |
| headers: []header{ |
| {mayIndex, "foo", "bar"}, |
| }, |
| want: unhex("00002a94e703626172"), |
| }, { |
| name: "multiple", |
| headers: []header{ |
| {mayIndex, "foo", "bar"}, |
| {mayIndex, "ZZZZZZZ", strings.Repeat("Z", 127)}, |
| }, |
| want: unhex("0000" + // prefix |
| // foo: bar |
| "2a94e703626172" + |
| // 7 octet long header name, the smallest number |
| // that does not fit on a 3-bit prefix. |
| "27005a5a5a5a5a5a5a" + |
| // 127 octet long header value, the smallest |
| // number that does not fit on a 7-bit prefix. |
| "7f005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + |
| "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + |
| "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + |
| "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"), |
| }, { |
| name: "static table 1", |
| headers: []header{ |
| {mayIndex, ":method", "GET"}, |
| {mayIndex, "accept-encoding", "gzip, deflate, br"}, |
| {mayIndex, "location", ""}, |
| }, |
| want: unhex("0000d1dfcc"), |
| }, { |
| name: "static table 2", |
| headers: []header{ |
| {mayIndex, ":method", "POST"}, |
| {mayIndex, "accept-encoding", "compress"}, |
| {mayIndex, "location", "foo"}, |
| }, |
| want: unhex("0000d45f108621e9aec2a11f5c8294e7"), |
| }, { |
| name: "static table 3", |
| headers: []header{ |
| {mayIndex, ":method", "TRACE"}, |
| {mayIndex, "accept-encoding", ""}, |
| }, |
| want: unhex("00005f000554524143455f1000"), |
| }, { |
| name: "never indexed literal field line with name reference", |
| headers: []header{ |
| {neverIndex, ":method", ""}, |
| }, |
| want: unhex("00007f0000"), |
| }, { |
| name: "never indexed literal field line with literal name", |
| headers: []header{ |
| {neverIndex, "a", "b"}, |
| }, |
| want: unhex("000031610162"), |
| }} { |
| t.Run(test.name, func(t *testing.T) { |
| var enc qpackEncoder |
| enc.init() |
| |
| got := enc.encode(func(f func(itype indexType, name, value string)) { |
| for _, h := range test.headers { |
| f(h.itype, h.name, h.value) |
| } |
| }) |
| if !bytes.Equal(got, test.want) { |
| for _, h := range test.headers { |
| t.Logf("header %v: %q", h.name, h.value) |
| } |
| t.Errorf("got: %x", got) |
| t.Errorf("want: %x", test.want) |
| } |
| }) |
| } |
| } |