Skip to content

Commit

Permalink
internal/http3: qpack wire primitives
Browse files Browse the repository at this point in the history
Encode and decode QPACK prefixed integers and string literals.

For golang/go#70914

Change-Id: Id12d1853738fc6d0e03bbbef36b67c24298451e1
Reviewed-on: https://go-review.googlesource.com/c/net/+/642115
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Auto-Submit: Damien Neil <dneil@google.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
  • Loading branch information
neild authored and gopherbot committed Jan 13, 2025
1 parent f6b2e53 commit 7ad0ebf
Show file tree
Hide file tree
Showing 2 changed files with 326 additions and 0 deletions.
153 changes: 153 additions & 0 deletions internal/http3/qpack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// 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 (
"io"

"golang.org/x/net/http2/hpack"
)

// Prefixed-integer encoding from RFC 7541, section 5.1
//
// Prefixed integers consist of some number of bits of data,
// N bits of encoded integer, and 0 or more additional bytes of
// encoded integer.
//
// The RFCs represent this as, for example:
//
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 0 | 0 | 1 | Capacity (5+) |
// +---+---+---+-------------------+
//
// "Capacity" is an integer with a 5-bit prefix.
//
// In the following functions, a "prefixLen" parameter is the number
// of integer bits in the first byte (5 in the above example), and
// a "firstByte" parameter is a byte containing the first byte of
// the encoded value (0x001x_xxxx in the above example).
//
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.1
// https://www.rfc-editor.org/rfc/rfc7541#section-5.1

// readPrefixedInt reads an RFC 7541 prefixed integer from st.
func (st *stream) readPrefixedInt(prefixLen uint8) (firstByte byte, v int64, err error) {
firstByte, err = st.ReadByte()
if err != nil {
return 0, 0, errQPACKDecompressionFailed
}
v, err = st.readPrefixedIntWithByte(firstByte, prefixLen)
return firstByte, v, err
}

// readPrefixedInt reads an RFC 7541 prefixed integer from st.
// The first byte has already been read from the stream.
func (st *stream) readPrefixedIntWithByte(firstByte byte, prefixLen uint8) (v int64, err error) {
prefixMask := (byte(1) << prefixLen) - 1
v = int64(firstByte & prefixMask)
if v != int64(prefixMask) {
return v, nil
}
m := 0
for {
b, err := st.ReadByte()
if err != nil {
return 0, errQPACKDecompressionFailed
}
v += int64(b&127) << m
m += 7
if b&128 == 0 {
break
}
}
return v, err
}

// appendPrefixedInt appends an RFC 7541 prefixed integer to b.
//
// The firstByte parameter includes the non-integer bits of the first byte.
// The other bits must be zero.
func appendPrefixedInt(b []byte, firstByte byte, prefixLen uint8, i int64) []byte {
u := uint64(i)
prefixMask := (uint64(1) << prefixLen) - 1
if u < prefixMask {
return append(b, firstByte|byte(u))
}
b = append(b, firstByte|byte(prefixMask))
u -= prefixMask
for u >= 128 {
b = append(b, 0x80|byte(u&0x7f))
u >>= 7
}
return append(b, byte(u))
}

// String literal encoding from RFC 7541, section 5.2
//
// String literals consist of a single bit flag indicating
// whether the string is Huffman-encoded, a prefixed integer (see above),
// and the string.
//
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2
// https://www.rfc-editor.org/rfc/rfc7541#section-5.2

// readPrefixedString reads an RFC 7541 string from st.
func (st *stream) readPrefixedString(prefixLen uint8) (firstByte byte, s string, err error) {
firstByte, err = st.ReadByte()
if err != nil {
return 0, "", errQPACKDecompressionFailed
}
s, err = st.readPrefixedStringWithByte(firstByte, prefixLen)
return firstByte, s, err
}

// readPrefixedString reads an RFC 7541 string from st.
// The first byte has already been read from the stream.
func (st *stream) readPrefixedStringWithByte(firstByte byte, prefixLen uint8) (s string, err error) {
size, err := st.readPrefixedIntWithByte(firstByte, prefixLen)
if err != nil {
return "", errQPACKDecompressionFailed
}

hbit := byte(1) << prefixLen
isHuffman := firstByte&hbit != 0

// TODO: Avoid allocating here.
data := make([]byte, size)
if _, err := io.ReadFull(st, data); err != nil {
return "", errQPACKDecompressionFailed
}
if isHuffman {
// TODO: Move Huffman functions into a new package that hpack (HTTP/2)
// and this package can both import. Most of the hpack package isn't
// relevant to HTTP/3.
s, err := hpack.HuffmanDecodeToString(data)
if err != nil {
return "", errQPACKDecompressionFailed
}
return s, nil
}
return string(data), nil
}

// appendPrefixedString appends an RFC 7541 string to st.
//
// The firstByte parameter includes the non-integer bits of the first byte.
// The other bits must be zero.
func appendPrefixedString(b []byte, firstByte byte, prefixLen uint8, s string) []byte {
huffmanLen := hpack.HuffmanEncodeLength(s)
if huffmanLen < uint64(len(s)) {
hbit := byte(1) << prefixLen
b = appendPrefixedInt(b, firstByte|hbit, prefixLen, int64(huffmanLen))
b = hpack.AppendHuffmanString(b, s)
} else {
b = appendPrefixedInt(b, firstByte, prefixLen, int64(len(s)))
b = append(b, s...)
}
return b
}
173 changes: 173 additions & 0 deletions internal/http3/qpack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// 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"
"testing"
)

func TestPrefixedInt(t *testing.T) {
st1, st2 := newStreamPair(t)
for _, test := range []struct {
value int64
prefixLen uint8
encoded []byte
}{
// https://www.rfc-editor.org/rfc/rfc7541#appendix-C.1.1
{
value: 10,
prefixLen: 5,
encoded: []byte{
0b_0000_1010,
},
},
// https://www.rfc-editor.org/rfc/rfc7541#appendix-C.1.2
{
value: 1337,
prefixLen: 5,
encoded: []byte{
0b0001_1111,
0b1001_1010,
0b0000_1010,
},
},
// https://www.rfc-editor.org/rfc/rfc7541#appendix-C.1.3
{
value: 42,
prefixLen: 8,
encoded: []byte{
0b0010_1010,
},
},
} {
highBitMask := ^((byte(1) << test.prefixLen) - 1)
for _, highBits := range []byte{
0, highBitMask, 0b1010_1010 & highBitMask,
} {
gotEnc := appendPrefixedInt(nil, highBits, test.prefixLen, test.value)
wantEnc := append([]byte{}, test.encoded...)
wantEnc[0] |= highBits
if !bytes.Equal(gotEnc, wantEnc) {
t.Errorf("appendPrefixedInt(nil, 0b%08b, %v, %v) = {%x}, want {%x}",
highBits, test.prefixLen, test.value, gotEnc, wantEnc)
}

st1.Write(gotEnc)
if err := st1.Flush(); err != nil {
t.Fatal(err)
}
gotFirstByte, v, err := st2.readPrefixedInt(test.prefixLen)
if err != nil || gotFirstByte&highBitMask != highBits || v != test.value {
t.Errorf("st.readPrefixedInt(%v) = 0b%08b, %v, %v; want 0b%08b, %v, nil", test.prefixLen, gotFirstByte, v, err, highBits, test.value)
}
}
}
}

func TestPrefixedString(t *testing.T) {
st1, st2 := newStreamPair(t)
for _, test := range []struct {
value string
prefixLen uint8
encoded []byte
}{
// https://www.rfc-editor.org/rfc/rfc7541#appendix-C.6.1
{
value: "302",
prefixLen: 7,
encoded: []byte{
0x82, // H bit + length 2
0x64, 0x02,
},
},
{
value: "private",
prefixLen: 5,
encoded: []byte{
0x25, // H bit + length 5
0xae, 0xc3, 0x77, 0x1a, 0x4b,
},
},
{
value: "Mon, 21 Oct 2013 20:13:21 GMT",
prefixLen: 7,
encoded: []byte{
0x96, // H bit + length 22
0xd0, 0x7a, 0xbe, 0x94, 0x10, 0x54, 0xd4, 0x44,
0xa8, 0x20, 0x05, 0x95, 0x04, 0x0b, 0x81, 0x66,
0xe0, 0x82, 0xa6, 0x2d, 0x1b, 0xff,
},
},
{
value: "https://www.example.com",
prefixLen: 7,
encoded: []byte{
0x91, // H bit + length 17
0x9d, 0x29, 0xad, 0x17, 0x18, 0x63, 0xc7, 0x8f,
0x0b, 0x97, 0xc8, 0xe9, 0xae, 0x82, 0xae, 0x43,
0xd3,
},
},
// Not Huffman encoded (encoded size == unencoded size).
{
value: "a",
prefixLen: 7,
encoded: []byte{
0x01, // length 1
0x61,
},
},
// Empty string.
{
value: "",
prefixLen: 7,
encoded: []byte{
0x00, // length 0
},
},
} {
highBitMask := ^((byte(1) << (test.prefixLen + 1)) - 1)
for _, highBits := range []byte{
0, highBitMask, 0b1010_1010 & highBitMask,
} {
gotEnc := appendPrefixedString(nil, highBits, test.prefixLen, test.value)
wantEnc := append([]byte{}, test.encoded...)
wantEnc[0] |= highBits
if !bytes.Equal(gotEnc, wantEnc) {
t.Errorf("appendPrefixedString(nil, 0b%08b, %v, %v) = {%x}, want {%x}",
highBits, test.prefixLen, test.value, gotEnc, wantEnc)
}

st1.Write(gotEnc)
if err := st1.Flush(); err != nil {
t.Fatal(err)
}
gotFirstByte, v, err := st2.readPrefixedString(test.prefixLen)
if err != nil || gotFirstByte&highBitMask != highBits || v != test.value {
t.Errorf("st.readPrefixedInt(%v) = 0b%08b, %q, %v; want 0b%08b, %q, nil", test.prefixLen, gotFirstByte, v, err, highBits, test.value)
}
}
}
}

func TestHuffmanDecodingFailure(t *testing.T) {
st1, st2 := newStreamPair(t)
st1.Write([]byte{
0x82, // H bit + length 4
0b_1111_1111,
0b_1111_1111,
0b_1111_1111,
0b_1111_1111,
})
if err := st1.Flush(); err != nil {
t.Fatal(err)
}
if b, v, err := st2.readPrefixedString(7); err == nil {
t.Fatalf("readPrefixedString(7) = %x, %v, nil; want error", b, v)
}
}

0 comments on commit 7ad0ebf

Please sign in to comment.