-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
internal/http3: qpack wire primitives
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
Showing
2 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |