diff --git a/internal/http3/qpack.go b/internal/http3/qpack.go new file mode 100644 index 000000000..5dcdf6a6a --- /dev/null +++ b/internal/http3/qpack.go @@ -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 +} diff --git a/internal/http3/qpack_test.go b/internal/http3/qpack_test.go new file mode 100644 index 000000000..6e16511fc --- /dev/null +++ b/internal/http3/qpack_test.go @@ -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) + } +}