├── .codecov.yml ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── golangci-lint.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .golangci.yml ├── LICENSE.md ├── README.md ├── decoder.go ├── decoder_test.go ├── encoder.go ├── encoder_test.go ├── example ├── fb-req-hq.out.0.0.0 └── main.go ├── fuzzing └── fuzz.go ├── go.mod ├── go.sum ├── header_field.go ├── header_field_test.go ├── integrationtests ├── interop │ └── interop_test.go └── self │ └── integration_test.go ├── static_table.go ├── static_table_test.go └── varint.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | round: nearest 3 | status: 4 | project: 5 | default: 6 | threshold: 1 7 | patch: false 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [marten-seemann] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-go@v5 16 | with: 17 | go-version: '1.23.x' 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@v6 20 | with: 21 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 22 | version: v1.60.3 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [ push, pull_request ] 2 | 3 | jobs: 4 | unit: 5 | strategy: 6 | matrix: 7 | go: [ "1.22.x", "1.23.x" ] 8 | runs-on: ubuntu-latest 9 | name: Unit tests (Go ${{ matrix.go }}) 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-go@v5 13 | with: 14 | go-version: ${{ matrix.go }} 15 | - run: go version 16 | - name: Run tests 17 | run: go test -v -cover -race -shuffle=on . 18 | - name: Run tests (32 bit) 19 | env: 20 | GOARCH: 386 21 | run: go test -v -cover -coverprofile=coverage.txt -shuffle=on . 22 | - name: Upload coverage to Codecov 23 | uses: codecov/codecov-action@v4 24 | with: 25 | files: coverage.txt 26 | env_vars: GO=${{ matrix.go }} 27 | token: ${{ secrets.CODECOV_TOKEN }} 28 | integration: 29 | strategy: 30 | matrix: 31 | go: [ "1.22.x", "1.23.x" ] 32 | runs-on: ubuntu-latest 33 | name: Integration tests (Go ${{ matrix.go }}) 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | submodules: 'recursive' 38 | - uses: actions/setup-go@v5 39 | with: 40 | go-version: ${{ matrix.go }} 41 | - run: go version 42 | - name: Run interop tests 43 | run: go test -v ./integrationtests/interop/ 44 | - name: Run integration tests 45 | run: | 46 | for i in {1..25}; do 47 | go test -v -race -shuffle=on ./integrationtests/self 48 | done 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | fuzzing/*.zip 2 | fuzzing/coverprofile 3 | fuzzing/crashers 4 | fuzzing/sonarprofile 5 | fuzzing/suppressions 6 | fuzzing/corpus/ 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "integrationtests/interop/qifs"] 2 | path = integrationtests/interop/qifs 3 | url = https://github.com/qpackers/qifs.git 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | linters-settings: 3 | linters: 4 | disable-all: true 5 | enable: 6 | - asciicheck 7 | - copyloopvar 8 | - exhaustive 9 | - goconst 10 | - gofmt # redundant, since gofmt *should* be a no-op after gofumpt 11 | - gofumpt 12 | - goimports 13 | - gosimple 14 | - govet 15 | - ineffassign 16 | - misspell 17 | - prealloc 18 | - staticcheck 19 | - stylecheck 20 | - unconvert 21 | - unparam 22 | - unused 23 | 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Marten Seemann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QPACK 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/quic-go/qpack)](https://pkg.go.dev/github.com/quic-go/qpack) 4 | [![Code Coverage](https://img.shields.io/codecov/c/github/quic-go/qpack/master.svg?style=flat-square)](https://codecov.io/gh/quic-go/qpack) 5 | [![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/quic-go.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:quic-go) 6 | 7 | This is a minimal QPACK ([RFC 9204](https://datatracker.ietf.org/doc/html/rfc9204)) implementation in Go. It is minimal in the sense that it doesn't use the dynamic table at all, but just the static table and (Huffman encoded) string literals. Wherever possible, it reuses code from the [HPACK implementation in the Go standard library](https://github.com/golang/net/tree/master/http2/hpack). 8 | 9 | It is interoperable with other QPACK implementations (both encoders and decoders), however it won't achieve a high compression efficiency. If you're interested in dynamic table support, please comment on [the issue](https://github.com/quic-go/qpack/issues/33). 10 | 11 | ## Running the Interop Tests 12 | 13 | Install the [QPACK interop files](https://github.com/qpackers/qifs/) by running 14 | ```bash 15 | git submodule update --init --recursive 16 | ``` 17 | 18 | Then run the tests: 19 | ```bash 20 | go test -v ./integrationtests/interop/ 21 | ``` 22 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package qpack 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | 9 | "golang.org/x/net/http2/hpack" 10 | ) 11 | 12 | // A decodingError is something the spec defines as a decoding error. 13 | type decodingError struct { 14 | err error 15 | } 16 | 17 | func (de decodingError) Error() string { 18 | return fmt.Sprintf("decoding error: %v", de.err) 19 | } 20 | 21 | // An invalidIndexError is returned when an encoder references a table 22 | // entry before the static table or after the end of the dynamic table. 23 | type invalidIndexError int 24 | 25 | func (e invalidIndexError) Error() string { 26 | return fmt.Sprintf("invalid indexed representation index %d", int(e)) 27 | } 28 | 29 | var errNoDynamicTable = decodingError{errors.New("no dynamic table")} 30 | 31 | // errNeedMore is an internal sentinel error value that means the 32 | // buffer is truncated and we need to read more data before we can 33 | // continue parsing. 34 | var errNeedMore = errors.New("need more data") 35 | 36 | // A Decoder is the decoding context for incremental processing of 37 | // header blocks. 38 | type Decoder struct { 39 | mutex sync.Mutex 40 | 41 | emitFunc func(f HeaderField) 42 | 43 | readRequiredInsertCount bool 44 | readDeltaBase bool 45 | 46 | // buf is the unparsed buffer. It's only written to 47 | // saveBuf if it was truncated in the middle of a header 48 | // block. Because it's usually not owned, we can only 49 | // process it under Write. 50 | buf []byte // not owned; only valid during Write 51 | 52 | // saveBuf is previous data passed to Write which we weren't able 53 | // to fully parse before. Unlike buf, we own this data. 54 | saveBuf bytes.Buffer 55 | } 56 | 57 | // NewDecoder returns a new decoder 58 | // The emitFunc will be called for each valid field parsed, 59 | // in the same goroutine as calls to Write, before Write returns. 60 | func NewDecoder(emitFunc func(f HeaderField)) *Decoder { 61 | return &Decoder{emitFunc: emitFunc} 62 | } 63 | 64 | func (d *Decoder) Write(p []byte) (int, error) { 65 | if len(p) == 0 { 66 | return 0, nil 67 | } 68 | 69 | d.mutex.Lock() 70 | n, err := d.writeLocked(p) 71 | d.mutex.Unlock() 72 | return n, err 73 | } 74 | 75 | func (d *Decoder) writeLocked(p []byte) (int, error) { 76 | // Only copy the data if we have to. Optimistically assume 77 | // that p will contain a complete header block. 78 | if d.saveBuf.Len() == 0 { 79 | d.buf = p 80 | } else { 81 | d.saveBuf.Write(p) 82 | d.buf = d.saveBuf.Bytes() 83 | d.saveBuf.Reset() 84 | } 85 | 86 | if err := d.decode(); err != nil { 87 | if err != errNeedMore { 88 | return 0, err 89 | } 90 | // TODO: limit the size of the buffer 91 | d.saveBuf.Write(d.buf) 92 | } 93 | return len(p), nil 94 | } 95 | 96 | // DecodeFull decodes an entire block. 97 | func (d *Decoder) DecodeFull(p []byte) ([]HeaderField, error) { 98 | if len(p) == 0 { 99 | return []HeaderField{}, nil 100 | } 101 | 102 | d.mutex.Lock() 103 | defer d.mutex.Unlock() 104 | 105 | saveFunc := d.emitFunc 106 | defer func() { d.emitFunc = saveFunc }() 107 | 108 | var hf []HeaderField 109 | d.emitFunc = func(f HeaderField) { hf = append(hf, f) } 110 | if _, err := d.writeLocked(p); err != nil { 111 | return nil, err 112 | } 113 | if err := d.Close(); err != nil { 114 | return nil, err 115 | } 116 | return hf, nil 117 | } 118 | 119 | // Close declares that the decoding is complete and resets the Decoder 120 | // to be reused again for a new header block. If there is any remaining 121 | // data in the decoder's buffer, Close returns an error. 122 | func (d *Decoder) Close() error { 123 | if d.saveBuf.Len() > 0 { 124 | d.saveBuf.Reset() 125 | return decodingError{errors.New("truncated headers")} 126 | } 127 | d.readRequiredInsertCount = false 128 | d.readDeltaBase = false 129 | return nil 130 | } 131 | 132 | func (d *Decoder) decode() error { 133 | if !d.readRequiredInsertCount { 134 | requiredInsertCount, rest, err := readVarInt(8, d.buf) 135 | if err != nil { 136 | return err 137 | } 138 | d.readRequiredInsertCount = true 139 | if requiredInsertCount != 0 { 140 | return decodingError{errors.New("expected Required Insert Count to be zero")} 141 | } 142 | d.buf = rest 143 | } 144 | if !d.readDeltaBase { 145 | base, rest, err := readVarInt(7, d.buf) 146 | if err != nil { 147 | return err 148 | } 149 | d.readDeltaBase = true 150 | if base != 0 { 151 | return decodingError{errors.New("expected Base to be zero")} 152 | } 153 | d.buf = rest 154 | } 155 | if len(d.buf) == 0 { 156 | return errNeedMore 157 | } 158 | 159 | for len(d.buf) > 0 { 160 | b := d.buf[0] 161 | var err error 162 | switch { 163 | case b&0x80 > 0: // 1xxxxxxx 164 | err = d.parseIndexedHeaderField() 165 | case b&0xc0 == 0x40: // 01xxxxxx 166 | err = d.parseLiteralHeaderField() 167 | case b&0xe0 == 0x20: // 001xxxxx 168 | err = d.parseLiteralHeaderFieldWithoutNameReference() 169 | default: 170 | err = fmt.Errorf("unexpected type byte: %#x", b) 171 | } 172 | if err != nil { 173 | return err 174 | } 175 | } 176 | return nil 177 | } 178 | 179 | func (d *Decoder) parseIndexedHeaderField() error { 180 | buf := d.buf 181 | if buf[0]&0x40 == 0 { 182 | return errNoDynamicTable 183 | } 184 | index, buf, err := readVarInt(6, buf) 185 | if err != nil { 186 | return err 187 | } 188 | hf, ok := d.at(index) 189 | if !ok { 190 | return decodingError{invalidIndexError(index)} 191 | } 192 | d.emitFunc(hf) 193 | d.buf = buf 194 | return nil 195 | } 196 | 197 | func (d *Decoder) parseLiteralHeaderField() error { 198 | buf := d.buf 199 | if buf[0]&0x10 == 0 { 200 | return errNoDynamicTable 201 | } 202 | // We don't need to check the value of the N-bit here. 203 | // It's only relevant when re-encoding header fields, 204 | // and determines whether the header field can be added to the dynamic table. 205 | // Since we don't support the dynamic table, we can ignore it. 206 | index, buf, err := readVarInt(4, buf) 207 | if err != nil { 208 | return err 209 | } 210 | hf, ok := d.at(index) 211 | if !ok { 212 | return decodingError{invalidIndexError(index)} 213 | } 214 | if len(buf) == 0 { 215 | return errNeedMore 216 | } 217 | usesHuffman := buf[0]&0x80 > 0 218 | val, buf, err := d.readString(buf, 7, usesHuffman) 219 | if err != nil { 220 | return err 221 | } 222 | hf.Value = val 223 | d.emitFunc(hf) 224 | d.buf = buf 225 | return nil 226 | } 227 | 228 | func (d *Decoder) parseLiteralHeaderFieldWithoutNameReference() error { 229 | buf := d.buf 230 | usesHuffmanForName := buf[0]&0x8 > 0 231 | name, buf, err := d.readString(buf, 3, usesHuffmanForName) 232 | if err != nil { 233 | return err 234 | } 235 | if len(buf) == 0 { 236 | return errNeedMore 237 | } 238 | usesHuffmanForVal := buf[0]&0x80 > 0 239 | val, buf, err := d.readString(buf, 7, usesHuffmanForVal) 240 | if err != nil { 241 | return err 242 | } 243 | d.emitFunc(HeaderField{Name: name, Value: val}) 244 | d.buf = buf 245 | return nil 246 | } 247 | 248 | func (d *Decoder) readString(buf []byte, n uint8, usesHuffman bool) (string, []byte, error) { 249 | l, buf, err := readVarInt(n, buf) 250 | if err != nil { 251 | return "", nil, err 252 | } 253 | if uint64(len(buf)) < l { 254 | return "", nil, errNeedMore 255 | } 256 | var val string 257 | if usesHuffman { 258 | var err error 259 | val, err = hpack.HuffmanDecodeToString(buf[:l]) 260 | if err != nil { 261 | return "", nil, err 262 | } 263 | } else { 264 | val = string(buf[:l]) 265 | } 266 | buf = buf[l:] 267 | return val, buf, nil 268 | } 269 | 270 | func (d *Decoder) at(i uint64) (hf HeaderField, ok bool) { 271 | if i >= uint64(len(staticTableEntries)) { 272 | return 273 | } 274 | return staticTableEntries[i], true 275 | } 276 | -------------------------------------------------------------------------------- /decoder_test.go: -------------------------------------------------------------------------------- 1 | package qpack 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "golang.org/x/net/http2/hpack" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type recordingDecoder struct { 13 | *Decoder 14 | headerFields []HeaderField 15 | } 16 | 17 | func newRecordingDecoder() *recordingDecoder { 18 | decoder := &recordingDecoder{} 19 | decoder.Decoder = NewDecoder(func(hf HeaderField) { decoder.headerFields = append(decoder.headerFields, hf) }) 20 | return decoder 21 | } 22 | 23 | func (decoder *recordingDecoder) Fields() []HeaderField { return decoder.headerFields } 24 | 25 | func insertPrefix(data []byte) []byte { 26 | prefix := appendVarInt(nil, 8, 0) 27 | prefix = appendVarInt(prefix, 7, 0) 28 | return append(prefix, data...) 29 | } 30 | 31 | func TestDecoderRejectsInvalidInputs(t *testing.T) { 32 | tests := []struct { 33 | name string 34 | input []byte 35 | expected string 36 | }{ 37 | { 38 | name: "non-zero required insert count", // we don't support dynamic table updates 39 | input: append(appendVarInt(nil, 8, 1), appendVarInt(nil, 7, 0)...), 40 | expected: "decoding error: expected Required Insert Count to be zero", 41 | }, 42 | { 43 | name: "non-zero delta base", // we don't support dynamic table updates 44 | input: append(appendVarInt(nil, 8, 0), appendVarInt(nil, 7, 1)...), 45 | expected: "decoding error: expected Base to be zero", 46 | }, 47 | { 48 | name: "unknown type byte", 49 | input: insertPrefix([]byte{0x10}), 50 | expected: "unexpected type byte: 0x10", 51 | }, 52 | } 53 | 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | _, err := NewDecoder(nil).Write(tt.input) 57 | require.EqualError(t, err, tt.expected) 58 | }) 59 | } 60 | } 61 | 62 | func doPartialWrites(t *testing.T, decoder *recordingDecoder, data []byte) { 63 | t.Helper() 64 | for i := 0; i < len(data)-1; i++ { 65 | n, err := decoder.Write([]byte{data[i]}) 66 | require.NoError(t, err) 67 | require.Equal(t, 1, n) 68 | require.Empty(t, decoder.Fields()) 69 | } 70 | n, err := decoder.Write([]byte{data[len(data)-1]}) 71 | require.NoError(t, err) 72 | require.Equal(t, 1, n) 73 | require.Len(t, decoder.Fields(), 1) 74 | } 75 | 76 | func TestDecoderIndexedHeaderFields(t *testing.T) { 77 | decoder := newRecordingDecoder() 78 | data := appendVarInt(nil, 6, 20) 79 | data[0] ^= 0x80 | 0x40 80 | doPartialWrites(t, decoder, insertPrefix(data)) 81 | require.Len(t, decoder.Fields(), 1) 82 | require.Equal(t, staticTableEntries[20], decoder.Fields()[0]) 83 | } 84 | 85 | func TestDecoderInvalidIndexedHeaderFields(t *testing.T) { 86 | tests := []struct { 87 | name string 88 | input []byte 89 | expected string 90 | }{ 91 | { 92 | name: "errors when a non-existent static table entry is referenced", 93 | input: func() []byte { 94 | data := appendVarInt(nil, 6, 10000) 95 | data[0] ^= 0x80 | 0x40 96 | return insertPrefix(data) 97 | }(), 98 | expected: "decoding error: invalid indexed representation index 10000", 99 | }, 100 | { 101 | name: "rejects an indexed header field that references the dynamic table", 102 | input: func() []byte { 103 | data := appendVarInt(nil, 6, 20) 104 | data[0] ^= 0x80 // don't set the static flag (0x40) 105 | return insertPrefix(data) 106 | }(), 107 | expected: errNoDynamicTable.Error(), 108 | }, 109 | } 110 | 111 | for _, tt := range tests { 112 | t.Run(tt.name, func(t *testing.T) { 113 | decoder := newRecordingDecoder() 114 | _, err := decoder.Write(tt.input) 115 | require.EqualError(t, err, tt.expected) 116 | require.Empty(t, decoder.Fields()) 117 | }) 118 | } 119 | } 120 | 121 | func TestDecoderLiteralHeaderFieldWithNameReference(t *testing.T) { 122 | t.Run("without the N-bit", func(t *testing.T) { 123 | testDecoderLiteralHeaderFieldWithNameReference(t, false) 124 | }) 125 | t.Run("with the N-bit", func(t *testing.T) { 126 | testDecoderLiteralHeaderFieldWithNameReference(t, true) 127 | }) 128 | } 129 | 130 | func testDecoderLiteralHeaderFieldWithNameReference(t *testing.T, n bool) { 131 | decoder := newRecordingDecoder() 132 | data := appendVarInt(nil, 4, 49) 133 | data[0] ^= 0x40 | 0x10 134 | if n { 135 | data[0] |= 0x20 136 | } 137 | data = appendVarInt(data, 7, 6) 138 | data = append(data, []byte("foobar")...) 139 | doPartialWrites(t, decoder, insertPrefix(data)) 140 | require.Len(t, decoder.Fields(), 1) 141 | require.Equal(t, "content-type", decoder.Fields()[0].Name) 142 | require.Equal(t, "foobar", decoder.Fields()[0].Value) 143 | } 144 | 145 | func TestDecoderLiteralHeaderFieldWithNameReferenceAndHuffmanEncoding(t *testing.T) { 146 | decoder := newRecordingDecoder() 147 | data := appendVarInt(nil, 4, 49) 148 | data[0] ^= 0x40 | 0x10 149 | data2 := appendVarInt(nil, 7, hpack.HuffmanEncodeLength("foobar")) 150 | data2[0] ^= 0x80 151 | data = hpack.AppendHuffmanString(append(data, data2...), "foobar") 152 | doPartialWrites(t, decoder, insertPrefix(data)) 153 | require.Len(t, decoder.Fields(), 1) 154 | require.Equal(t, "content-type", decoder.Fields()[0].Name) 155 | require.Equal(t, "foobar", decoder.Fields()[0].Value) 156 | } 157 | 158 | func TestDecoderLiteralHeaderFieldWithNameReferenceToTheDynamicTable(t *testing.T) { 159 | decoder := newRecordingDecoder() 160 | data := appendVarInt(nil, 4, 49) 161 | data[0] ^= 0x40 // don't set the static flag (0x10) 162 | data = appendVarInt(data, 7, 6) 163 | data = append(data, []byte("foobar")...) 164 | _, err := decoder.Write(insertPrefix(data)) 165 | require.ErrorIs(t, err, errNoDynamicTable) 166 | } 167 | 168 | func TestDecoderLiteralHeaderFieldWithoutNameReference(t *testing.T) { 169 | decoder := newRecordingDecoder() 170 | data := appendVarInt(nil, 3, 3) 171 | data[0] ^= 0x20 172 | data = append(data, []byte("foo")...) 173 | data2 := appendVarInt(nil, 7, 3) 174 | data2 = append(data2, []byte("bar")...) 175 | data = append(data, data2...) 176 | doPartialWrites(t, decoder, insertPrefix(data)) 177 | 178 | require.Len(t, decoder.Fields(), 1) 179 | require.Equal(t, "foo", decoder.Fields()[0].Name) 180 | require.Equal(t, "bar", decoder.Fields()[0].Value) 181 | } 182 | 183 | func TestDecodeFull(t *testing.T) { 184 | // decode nothing 185 | data, err := NewDecoder(nil).DecodeFull([]byte{}) 186 | require.NoError(t, err) 187 | require.Empty(t, data) 188 | 189 | // decode a few entries 190 | buf := &bytes.Buffer{} 191 | enc := NewEncoder(buf) 192 | require.NoError(t, enc.WriteField(HeaderField{Name: "foo", Value: "bar"})) 193 | require.NoError(t, enc.WriteField(HeaderField{Name: "lorem", Value: "ipsum"})) 194 | data, err = NewDecoder(nil).DecodeFull(buf.Bytes()) 195 | require.NoError(t, err) 196 | require.Equal(t, []HeaderField{ 197 | {Name: "foo", Value: "bar"}, 198 | {Name: "lorem", Value: "ipsum"}, 199 | }, data) 200 | } 201 | 202 | func TestDecodeFullIncompleteData(t *testing.T) { 203 | buf := &bytes.Buffer{} 204 | enc := NewEncoder(buf) 205 | require.NoError(t, enc.WriteField(HeaderField{Name: "foo", Value: "bar"})) 206 | _, err := NewDecoder(nil).DecodeFull(buf.Bytes()[:buf.Len()-2]) 207 | require.EqualError(t, err, "decoding error: truncated headers") 208 | } 209 | 210 | func TestDecodeFullRestoresEmitFunc(t *testing.T) { 211 | var emitFuncCalled bool 212 | emitFunc := func(HeaderField) { 213 | emitFuncCalled = true 214 | } 215 | decoder := NewDecoder(emitFunc) 216 | buf := &bytes.Buffer{} 217 | enc := NewEncoder(buf) 218 | require.NoError(t, enc.WriteField(HeaderField{Name: "foo", Value: "bar"})) 219 | _, err := decoder.DecodeFull(buf.Bytes()) 220 | require.NoError(t, err) 221 | require.False(t, emitFuncCalled) 222 | _, err = decoder.Write(buf.Bytes()) 223 | require.NoError(t, err) 224 | require.True(t, emitFuncCalled) 225 | } 226 | -------------------------------------------------------------------------------- /encoder.go: -------------------------------------------------------------------------------- 1 | package qpack 2 | 3 | import ( 4 | "io" 5 | 6 | "golang.org/x/net/http2/hpack" 7 | ) 8 | 9 | // An Encoder performs QPACK encoding. 10 | type Encoder struct { 11 | wrotePrefix bool 12 | 13 | w io.Writer 14 | buf []byte 15 | } 16 | 17 | // NewEncoder returns a new Encoder which performs QPACK encoding. An 18 | // encoded data is written to w. 19 | func NewEncoder(w io.Writer) *Encoder { 20 | return &Encoder{w: w} 21 | } 22 | 23 | // WriteField encodes f into a single Write to e's underlying Writer. 24 | // This function may also produce bytes for the Header Block Prefix 25 | // if necessary. If produced, it is done before encoding f. 26 | func (e *Encoder) WriteField(f HeaderField) error { 27 | // write the Header Block Prefix 28 | if !e.wrotePrefix { 29 | e.buf = appendVarInt(e.buf, 8, 0) 30 | e.buf = appendVarInt(e.buf, 7, 0) 31 | e.wrotePrefix = true 32 | } 33 | 34 | idxAndVals, nameFound := encoderMap[f.Name] 35 | if nameFound { 36 | if idxAndVals.values == nil { 37 | if len(f.Value) == 0 { 38 | e.writeIndexedField(idxAndVals.idx) 39 | } else { 40 | e.writeLiteralFieldWithNameReference(&f, idxAndVals.idx) 41 | } 42 | } else { 43 | valIdx, valueFound := idxAndVals.values[f.Value] 44 | if valueFound { 45 | e.writeIndexedField(valIdx) 46 | } else { 47 | e.writeLiteralFieldWithNameReference(&f, idxAndVals.idx) 48 | } 49 | } 50 | } else { 51 | e.writeLiteralFieldWithoutNameReference(f) 52 | } 53 | 54 | _, err := e.w.Write(e.buf) 55 | e.buf = e.buf[:0] 56 | return err 57 | } 58 | 59 | // Close declares that the encoding is complete and resets the Encoder 60 | // to be reused again for a new header block. 61 | func (e *Encoder) Close() error { 62 | e.wrotePrefix = false 63 | return nil 64 | } 65 | 66 | func (e *Encoder) writeLiteralFieldWithoutNameReference(f HeaderField) { 67 | offset := len(e.buf) 68 | e.buf = appendVarInt(e.buf, 3, hpack.HuffmanEncodeLength(f.Name)) 69 | e.buf[offset] ^= 0x20 ^ 0x8 70 | e.buf = hpack.AppendHuffmanString(e.buf, f.Name) 71 | offset = len(e.buf) 72 | e.buf = appendVarInt(e.buf, 7, hpack.HuffmanEncodeLength(f.Value)) 73 | e.buf[offset] ^= 0x80 74 | e.buf = hpack.AppendHuffmanString(e.buf, f.Value) 75 | } 76 | 77 | // Encodes a header field whose name is present in one of the tables. 78 | func (e *Encoder) writeLiteralFieldWithNameReference(f *HeaderField, id uint8) { 79 | offset := len(e.buf) 80 | e.buf = appendVarInt(e.buf, 4, uint64(id)) 81 | // Set the 01NTxxxx pattern, forcing N to 0 and T to 1 82 | e.buf[offset] ^= 0x50 83 | offset = len(e.buf) 84 | e.buf = appendVarInt(e.buf, 7, hpack.HuffmanEncodeLength(f.Value)) 85 | e.buf[offset] ^= 0x80 86 | e.buf = hpack.AppendHuffmanString(e.buf, f.Value) 87 | } 88 | 89 | // Encodes an indexed field, meaning it's entirely defined in one of the tables. 90 | func (e *Encoder) writeIndexedField(id uint8) { 91 | offset := len(e.buf) 92 | e.buf = appendVarInt(e.buf, 6, uint64(id)) 93 | // Set the 1Txxxxxx pattern, forcing T to 1 94 | e.buf[offset] ^= 0xc0 95 | } 96 | -------------------------------------------------------------------------------- /encoder_test.go: -------------------------------------------------------------------------------- 1 | package qpack 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "golang.org/x/net/http2/hpack" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | // errWriter wraps bytes.Buffer and optionally fails on every write 14 | // useful for testing misbehaving writers 15 | type errWriter struct { 16 | bytes.Buffer 17 | fail bool 18 | } 19 | 20 | func (ew *errWriter) Write(b []byte) (int, error) { 21 | if ew.fail { 22 | return 0, io.ErrClosedPipe 23 | } 24 | return ew.Buffer.Write(b) 25 | } 26 | 27 | func readPrefix(t *testing.T, data []byte) (rest []byte, requiredInsertCount uint64, deltaBase uint64) { 28 | var err error 29 | requiredInsertCount, rest, err = readVarInt(8, data) 30 | require.NoError(t, err) 31 | deltaBase, rest, err = readVarInt(7, rest) 32 | require.NoError(t, err) 33 | return 34 | } 35 | 36 | func checkHeaderField(t *testing.T, data []byte, hf HeaderField) []byte { 37 | require.Equal(t, uint8(0x20), data[0]&(0x80^0x40^0x20)) // 001xxxxx 38 | require.NotZero(t, data[0]&0x8) // Huffman encoding 39 | nameLen, data, err := readVarInt(3, data) 40 | require.NoError(t, err) 41 | l := hpack.HuffmanEncodeLength(hf.Name) 42 | require.Equal(t, l, nameLen) 43 | decodedName, err := hpack.HuffmanDecodeToString(data[:l]) 44 | require.NoError(t, err) 45 | require.Equal(t, hf.Name, decodedName) 46 | valueLen, data, err := readVarInt(7, data[l:]) 47 | require.NoError(t, err) 48 | l = hpack.HuffmanEncodeLength(hf.Value) 49 | require.Equal(t, l, valueLen) 50 | decodedValue, err := hpack.HuffmanDecodeToString(data[:l]) 51 | require.NoError(t, err) 52 | require.Equal(t, hf.Value, decodedValue) 53 | return data[l:] 54 | } 55 | 56 | // Reads one indexed field line representation from data and verifies it matches hf. 57 | // Returns the leftover bytes from data. 58 | func checkIndexedHeaderField(t *testing.T, data []byte, hf HeaderField) []byte { 59 | require.Equal(t, uint8(1), data[0]>>7) // 1Txxxxxx 60 | index, data, err := readVarInt(6, data) 61 | require.NoError(t, err) 62 | require.Equal(t, hf, staticTableEntries[index]) 63 | return data 64 | } 65 | 66 | func checkHeaderFieldWithNameRef(t *testing.T, data []byte, hf HeaderField) []byte { 67 | // read name reference 68 | require.Equal(t, uint8(1), data[0]>>6) // 01NTxxxx 69 | index, data, err := readVarInt(4, data) 70 | require.NoError(t, err) 71 | require.Equal(t, hf.Name, staticTableEntries[index].Name) 72 | // read literal value 73 | valueLen, data, err := readVarInt(7, data) 74 | require.NoError(t, err) 75 | l := hpack.HuffmanEncodeLength(hf.Value) 76 | require.Equal(t, l, valueLen) 77 | decodedValue, err := hpack.HuffmanDecodeToString(data[:l]) 78 | require.NoError(t, err) 79 | require.Equal(t, hf.Value, decodedValue) 80 | return data[l:] 81 | } 82 | 83 | func TestEncoderEncodesSingleField(t *testing.T) { 84 | output := &errWriter{} 85 | encoder := NewEncoder(output) 86 | 87 | hf := HeaderField{Name: "foobar", Value: "lorem ipsum"} 88 | require.NoError(t, encoder.WriteField(hf)) 89 | 90 | data, requiredInsertCount, deltaBase := readPrefix(t, output.Bytes()) 91 | require.Zero(t, requiredInsertCount) 92 | require.Zero(t, deltaBase) 93 | 94 | data = checkHeaderField(t, data, hf) 95 | require.Empty(t, data) 96 | } 97 | 98 | func TestEncoderFailsToEncodeWhenWriterErrs(t *testing.T) { 99 | output := &errWriter{fail: true} 100 | encoder := NewEncoder(output) 101 | 102 | hf := HeaderField{Name: "foobar", Value: "lorem ipsum"} 103 | err := encoder.WriteField(hf) 104 | require.EqualError(t, err, "io: read/write on closed pipe") 105 | } 106 | 107 | func TestEncoderEncodesMultipleFields(t *testing.T) { 108 | output := &errWriter{} 109 | encoder := NewEncoder(output) 110 | 111 | hf1 := HeaderField{Name: "foobar", Value: "lorem ipsum"} 112 | hf2 := HeaderField{Name: "raboof", Value: "dolor sit amet"} 113 | require.NoError(t, encoder.WriteField(hf1)) 114 | require.NoError(t, encoder.WriteField(hf2)) 115 | 116 | data, requiredInsertCount, deltaBase := readPrefix(t, output.Bytes()) 117 | require.Zero(t, requiredInsertCount) 118 | require.Zero(t, deltaBase) 119 | 120 | data = checkHeaderField(t, data, hf1) 121 | data = checkHeaderField(t, data, hf2) 122 | require.Empty(t, data) 123 | } 124 | 125 | func TestEncoderEncodesAllFieldsOfStaticTable(t *testing.T) { 126 | output := &errWriter{} 127 | encoder := NewEncoder(output) 128 | 129 | for _, hf := range staticTableEntries { 130 | require.NoError(t, encoder.WriteField(hf)) 131 | } 132 | 133 | data, requiredInsertCount, deltaBase := readPrefix(t, output.Bytes()) 134 | require.Zero(t, requiredInsertCount) 135 | require.Zero(t, deltaBase) 136 | 137 | for _, hf := range staticTableEntries { 138 | data = checkIndexedHeaderField(t, data, hf) 139 | } 140 | require.Empty(t, data) 141 | } 142 | 143 | func TestEncodeFieldsWithNameReferenceInStaticTable(t *testing.T) { 144 | output := &errWriter{} 145 | encoder := NewEncoder(output) 146 | 147 | hf1 := HeaderField{Name: ":status", Value: "666"} 148 | hf2 := HeaderField{Name: "server", Value: "lorem ipsum"} 149 | hf3 := HeaderField{Name: ":method", Value: ""} 150 | require.NoError(t, encoder.WriteField(hf1)) 151 | require.NoError(t, encoder.WriteField(hf2)) 152 | require.NoError(t, encoder.WriteField(hf3)) 153 | 154 | data, requiredInsertCount, deltaBase := readPrefix(t, output.Bytes()) 155 | require.Zero(t, requiredInsertCount) 156 | require.Zero(t, deltaBase) 157 | 158 | data = checkHeaderFieldWithNameRef(t, data, hf1) 159 | data = checkHeaderFieldWithNameRef(t, data, hf2) 160 | data = checkHeaderFieldWithNameRef(t, data, hf3) 161 | require.Empty(t, data) 162 | } 163 | 164 | func TestEncodeMultipleRequests(t *testing.T) { 165 | output := &errWriter{} 166 | encoder := NewEncoder(output) 167 | 168 | hf1 := HeaderField{Name: "foobar", Value: "lorem ipsum"} 169 | require.NoError(t, encoder.WriteField(hf1)) 170 | data, requiredInsertCount, deltaBase := readPrefix(t, output.Bytes()) 171 | require.Zero(t, requiredInsertCount) 172 | require.Zero(t, deltaBase) 173 | require.Empty(t, checkHeaderField(t, data, hf1)) 174 | 175 | output.Reset() 176 | require.NoError(t, encoder.Close()) 177 | hf2 := HeaderField{Name: "raboof", Value: "dolor sit amet"} 178 | require.NoError(t, encoder.WriteField(hf2)) 179 | data, requiredInsertCount, deltaBase = readPrefix(t, output.Bytes()) 180 | require.Zero(t, requiredInsertCount) 181 | require.Zero(t, deltaBase) 182 | require.Empty(t, checkHeaderField(t, data, hf2)) 183 | } 184 | -------------------------------------------------------------------------------- /example/fb-req-hq.out.0.0.0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quic-go/qpack/696c8e28d3f57014b5d86790095e7894f9168bc4/example/fb-req-hq.out.0.0.0 -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/quic-go/qpack" 11 | ) 12 | 13 | func main() { 14 | file, err := os.Open("example/fb-req-hq.out.0.0.0") 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | dec := qpack.NewDecoder(emitFunc) 20 | for { 21 | in, err := decodeInput(file) 22 | if err != nil { 23 | panic(err) 24 | } 25 | fmt.Printf("\nRequest on stream %d:\n", in.streamID) 26 | dec.Write(in.data) 27 | } 28 | } 29 | 30 | func emitFunc(hf qpack.HeaderField) { 31 | fmt.Printf("%#v\n", hf) 32 | } 33 | 34 | type input struct { 35 | streamID uint64 36 | data []byte 37 | } 38 | 39 | func decodeInput(r io.Reader) (*input, error) { 40 | prefix := make([]byte, 12) 41 | if _, err := io.ReadFull(r, prefix); err != nil { 42 | return nil, errors.New("insufficient data for prefix") 43 | } 44 | streamID := binary.BigEndian.Uint64(prefix[:8]) 45 | length := binary.BigEndian.Uint32(prefix[8:12]) 46 | if length > (1 << 15) { 47 | return nil, errors.New("input too long") 48 | } 49 | data := make([]byte, int(length)) 50 | if _, err := io.ReadFull(r, data); err != nil { 51 | return nil, errors.New("incomplete data") 52 | } 53 | return &input{ 54 | streamID: streamID, 55 | data: data, 56 | }, nil 57 | } 58 | -------------------------------------------------------------------------------- /fuzzing/fuzz.go: -------------------------------------------------------------------------------- 1 | package qpack 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/quic-go/qpack" 9 | ) 10 | 11 | func Fuzz(data []byte) int { 12 | if len(data) < 1 { 13 | return 0 14 | } 15 | 16 | chunkLen := int(data[0]) + 1 17 | data = data[1:] 18 | 19 | fields, err := qpack.NewDecoder(nil).DecodeFull(data) 20 | if err != nil { 21 | return 0 22 | } 23 | if len(fields) == 0 { 24 | return 0 25 | } 26 | 27 | var writtenFields []qpack.HeaderField 28 | decoder := qpack.NewDecoder(func(hf qpack.HeaderField) { 29 | writtenFields = append(writtenFields, hf) 30 | }) 31 | for len(data) > 0 { 32 | var chunk []byte 33 | if chunkLen <= len(data) { 34 | chunk = data[:chunkLen] 35 | data = data[chunkLen:] 36 | } else { 37 | chunk = data 38 | data = nil 39 | } 40 | n, err := decoder.Write(chunk) 41 | if err != nil { 42 | return 0 43 | } 44 | if n != len(chunk) { 45 | panic("len error") 46 | } 47 | } 48 | if !reflect.DeepEqual(fields, writtenFields) { 49 | fmt.Printf("%#v vs %#v", fields, writtenFields) 50 | panic("Write() and DecodeFull() produced different results") 51 | } 52 | 53 | buf := &bytes.Buffer{} 54 | encoder := qpack.NewEncoder(buf) 55 | for _, hf := range fields { 56 | if err := encoder.WriteField(hf); err != nil { 57 | panic(err) 58 | } 59 | } 60 | if err := encoder.Close(); err != nil { 61 | panic(err) 62 | } 63 | 64 | encodedFields, err := qpack.NewDecoder(nil).DecodeFull(buf.Bytes()) 65 | if err != nil { 66 | fmt.Printf("Fields: %#v\n", fields) 67 | panic(err) 68 | } 69 | if !reflect.DeepEqual(fields, encodedFields) { 70 | fmt.Printf("%#v vs %#v", fields, encodedFields) 71 | panic("unequal") 72 | } 73 | return 0 74 | } 75 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quic-go/qpack 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/stretchr/testify v1.9.0 7 | golang.org/x/net v0.28.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 8 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /header_field.go: -------------------------------------------------------------------------------- 1 | package qpack 2 | 3 | // A HeaderField is a name-value pair. Both the name and value are 4 | // treated as opaque sequences of octets. 5 | type HeaderField struct { 6 | Name string 7 | Value string 8 | } 9 | 10 | // IsPseudo reports whether the header field is an HTTP3 pseudo header. 11 | // That is, it reports whether it starts with a colon. 12 | // It is not otherwise guaranteed to be a valid pseudo header field, 13 | // though. 14 | func (hf HeaderField) IsPseudo() bool { 15 | return len(hf.Name) != 0 && hf.Name[0] == ':' 16 | } 17 | -------------------------------------------------------------------------------- /header_field_test.go: -------------------------------------------------------------------------------- 1 | package qpack 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestHeaderFieldIsPseudo(t *testing.T) { 10 | t.Run("Pseudo headers", func(t *testing.T) { 11 | require.True(t, (HeaderField{Name: ":status"}).IsPseudo()) 12 | require.True(t, (HeaderField{Name: ":authority"}).IsPseudo()) 13 | require.True(t, (HeaderField{Name: ":foobar"}).IsPseudo()) 14 | }) 15 | 16 | t.Run("Non-pseudo headers", func(t *testing.T) { 17 | require.False(t, (HeaderField{Name: "status"}).IsPseudo()) 18 | require.False(t, (HeaderField{Name: "foobar"}).IsPseudo()) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /integrationtests/interop/interop_test.go: -------------------------------------------------------------------------------- 1 | package interop 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "runtime" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/quic-go/qpack" 17 | 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | type request struct { 22 | headers []qpack.HeaderField 23 | } 24 | 25 | type qif struct { 26 | requests []request 27 | } 28 | 29 | var qifs map[string]qif 30 | 31 | func init() { 32 | qifs = make(map[string]qif) 33 | readQIFs() 34 | } 35 | 36 | func readQIFs() { 37 | qifDir := currentDir() + "/qifs/qifs" 38 | if err := filepath.Walk(qifDir, func(path string, info os.FileInfo, err error) error { 39 | if err != nil { 40 | return err 41 | } 42 | if info.IsDir() { 43 | return nil 44 | } 45 | _, filename := filepath.Split(path) 46 | name := filename[:len(filename)-len(filepath.Ext(filename))] 47 | file, err := os.Open(path) 48 | if err != nil { 49 | return err 50 | } 51 | defer file.Close() 52 | requests := parseRequests(file) 53 | qifs[name] = qif{requests: requests} 54 | return nil 55 | }); err != nil { 56 | log.Fatal(err) 57 | } 58 | } 59 | 60 | func parseRequests(r io.Reader) []request { 61 | lr := bufio.NewReader(r) 62 | var reqs []request 63 | for { 64 | headers, done := parseRequest(lr) 65 | if done { 66 | break 67 | } 68 | reqs = append(reqs, request{headers}) 69 | } 70 | return reqs 71 | } 72 | 73 | func parseRequest(lr *bufio.Reader) (headers []qpack.HeaderField, done bool) { 74 | for { 75 | line, isPrefix, err := lr.ReadLine() 76 | if err == io.EOF { 77 | return headers, true 78 | } 79 | if err != nil { 80 | return nil, true 81 | } 82 | if isPrefix { 83 | return nil, true 84 | } 85 | if len(line) == 0 { 86 | break 87 | } 88 | split := strings.Split(string(line), "\t") 89 | if len(split) != 1 && len(split) != 2 { 90 | return nil, true 91 | } 92 | name := split[0] 93 | var val string 94 | if len(split) == 2 { 95 | val = split[1] 96 | } 97 | headers = append(headers, qpack.HeaderField{Name: name, Value: val}) 98 | } 99 | return headers, false 100 | } 101 | 102 | func currentDir() string { 103 | _, filename, _, ok := runtime.Caller(0) 104 | if !ok { 105 | panic("Failed to get current frame") 106 | } 107 | return path.Dir(filename) 108 | } 109 | 110 | func findFiles() []string { 111 | var files []string 112 | encodedDir := currentDir() + "/qifs/encoded/qpack-06/" 113 | filepath.Walk(encodedDir, func(path string, info os.FileInfo, err error) error { 114 | if info.IsDir() { 115 | return nil 116 | } 117 | _, file := filepath.Split(path) 118 | split := strings.Split(file, ".") 119 | tableSize := split[len(split)-3] 120 | if tableSize == "0" { 121 | files = append(files, path) 122 | } 123 | return nil 124 | }) 125 | return files 126 | } 127 | 128 | func parseInput(r io.Reader) (uint64, []byte) { 129 | prefix := make([]byte, 12) 130 | if _, err := io.ReadFull(r, prefix); err != nil { 131 | return 0, nil 132 | } 133 | streamID := binary.BigEndian.Uint64(prefix[:8]) 134 | length := binary.BigEndian.Uint32(prefix[8:12]) 135 | if length > 1<<15 { 136 | return 0, nil 137 | } 138 | data := make([]byte, int(length)) 139 | if _, err := io.ReadFull(r, data); err != nil { 140 | return 0, nil 141 | } 142 | return streamID, data 143 | } 144 | 145 | func TestInteropDecodingEncodedFiles(t *testing.T) { 146 | filenames := findFiles() 147 | for _, path := range filenames { 148 | fpath, filename := filepath.Split(path) 149 | prettyPath := path[len(filepath.Dir(filepath.Dir(filepath.Dir(fpath))))+1:] 150 | 151 | t.Run(fmt.Sprintf("Decoding_%s", prettyPath), func(t *testing.T) { 152 | qif, ok := qifs[strings.Split(filename, ".")[0]] 153 | require.True(t, ok) 154 | 155 | file, err := os.Open(path) 156 | require.NoError(t, err) 157 | defer file.Close() 158 | 159 | var numRequests, numHeaderFields int 160 | require.NotEmpty(t, qif.requests) 161 | 162 | var headers []qpack.HeaderField 163 | decoder := qpack.NewDecoder(func(hf qpack.HeaderField) { headers = append(headers, hf) }) 164 | 165 | for _, req := range qif.requests { 166 | _, data := parseInput(file) 167 | require.NotNil(t, data) 168 | 169 | _, err = decoder.Write(data) 170 | require.NoError(t, err) 171 | 172 | require.Equal(t, req.headers, headers) 173 | 174 | numRequests++ 175 | numHeaderFields += len(headers) 176 | headers = nil 177 | decoder.Close() 178 | } 179 | 180 | t.Logf("Decoded %d requests containing %d header fields.", len(qif.requests), numHeaderFields) 181 | }) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /integrationtests/self/integration_test.go: -------------------------------------------------------------------------------- 1 | package self 2 | 3 | import ( 4 | "bytes" 5 | "math/rand/v2" 6 | "testing" 7 | _ "unsafe" // for go:linkname 8 | 9 | "github.com/quic-go/qpack" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var staticTable []qpack.HeaderField 14 | 15 | //go:linkname getStaticTable github.com/quic-go/qpack.getStaticTable 16 | func getStaticTable() []qpack.HeaderField 17 | 18 | func init() { 19 | staticTable = getStaticTable() 20 | } 21 | 22 | func randomString(l int) string { 23 | const charset = "abcdefghijklmnopqrstuvwxyz" + 24 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 25 | s := make([]byte, l) 26 | for i := range s { 27 | s[i] = charset[rand.IntN(len(charset))] 28 | } 29 | return string(s) 30 | } 31 | 32 | func getEncoder() (*qpack.Encoder, *bytes.Buffer) { 33 | output := &bytes.Buffer{} 34 | return qpack.NewEncoder(output), output 35 | } 36 | 37 | func TestEncodingAndDecodingSingleHeaderField(t *testing.T) { 38 | hf := qpack.HeaderField{ 39 | Name: randomString(15), 40 | Value: randomString(15), 41 | } 42 | encoder, output := getEncoder() 43 | require.NoError(t, encoder.WriteField(hf)) 44 | headerFields, err := qpack.NewDecoder(nil).DecodeFull(output.Bytes()) 45 | require.NoError(t, err) 46 | require.Equal(t, []qpack.HeaderField{hf}, headerFields) 47 | } 48 | 49 | func TestEncodingAndDecodingMultipleHeaderFields(t *testing.T) { 50 | hfs := []qpack.HeaderField{ 51 | {Name: "foo", Value: "bar"}, 52 | {Name: "lorem", Value: "ipsum"}, 53 | {Name: randomString(15), Value: randomString(20)}, 54 | } 55 | encoder, output := getEncoder() 56 | for _, hf := range hfs { 57 | require.NoError(t, encoder.WriteField(hf)) 58 | } 59 | headerFields, err := qpack.NewDecoder(nil).DecodeFull(output.Bytes()) 60 | require.NoError(t, err) 61 | require.Equal(t, hfs, headerFields) 62 | } 63 | 64 | func TestEncodingAndDecodingMultipleRequests(t *testing.T) { 65 | hfs1 := []qpack.HeaderField{{Name: "foo", Value: "bar"}} 66 | hfs2 := []qpack.HeaderField{ 67 | {Name: "lorem", Value: "ipsum"}, 68 | {Name: randomString(15), Value: randomString(20)}, 69 | } 70 | encoder, output := getEncoder() 71 | for _, hf := range hfs1 { 72 | require.NoError(t, encoder.WriteField(hf)) 73 | } 74 | req1 := append([]byte{}, output.Bytes()...) 75 | output.Reset() 76 | for _, hf := range hfs2 { 77 | require.NoError(t, encoder.WriteField(hf)) 78 | } 79 | req2 := append([]byte{}, output.Bytes()...) 80 | 81 | var headerFields []qpack.HeaderField 82 | decoder := qpack.NewDecoder(func(hf qpack.HeaderField) { headerFields = append(headerFields, hf) }) 83 | _, err := decoder.Write(req1) 84 | require.NoError(t, err) 85 | require.Equal(t, hfs1, headerFields) 86 | headerFields = nil 87 | _, err = decoder.Write(req2) 88 | require.NoError(t, err) 89 | require.Equal(t, hfs2, headerFields) 90 | } 91 | 92 | // replace one character by a random character at a random position 93 | func replaceRandomCharacter(s string) string { 94 | pos := rand.IntN(len(s)) 95 | new := s[:pos] 96 | for { 97 | if c := randomString(1); c != string(s[pos]) { 98 | new += c 99 | break 100 | } 101 | } 102 | new += s[pos+1:] 103 | return new 104 | } 105 | 106 | func check(t *testing.T, encoded []byte, hf qpack.HeaderField) { 107 | t.Helper() 108 | headerFields, err := qpack.NewDecoder(nil).DecodeFull(encoded) 109 | require.NoError(t, err) 110 | require.Len(t, headerFields, 1) 111 | require.Equal(t, hf, headerFields[0]) 112 | } 113 | 114 | func TestUsingStaticTableForFieldNamesWithoutValues(t *testing.T) { 115 | var hf qpack.HeaderField 116 | for { 117 | if entry := staticTable[rand.IntN(len(staticTable))]; len(entry.Value) == 0 { 118 | hf = qpack.HeaderField{Name: entry.Name} 119 | break 120 | } 121 | } 122 | encoder, output := getEncoder() 123 | require.NoError(t, encoder.WriteField(hf)) 124 | encodedLen := output.Len() 125 | check(t, output.Bytes(), hf) 126 | encoder, output = getEncoder() 127 | oldName := hf.Name 128 | hf.Name = replaceRandomCharacter(hf.Name) 129 | require.NoError(t, encoder.WriteField(hf)) 130 | t.Logf("Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes\n", oldName, encodedLen, hf.Name, output.Len()) 131 | require.Greater(t, output.Len(), encodedLen) 132 | } 133 | 134 | func TestUsingStaticTableForFieldNamesWithCustomValues(t *testing.T) { 135 | var hf qpack.HeaderField 136 | for { 137 | if entry := staticTable[rand.IntN(len(staticTable))]; len(entry.Value) == 0 { 138 | hf = qpack.HeaderField{ 139 | Name: entry.Name, 140 | Value: randomString(5), 141 | } 142 | break 143 | } 144 | } 145 | encoder, output := getEncoder() 146 | require.NoError(t, encoder.WriteField(hf)) 147 | encodedLen := output.Len() 148 | check(t, output.Bytes(), hf) 149 | encoder, output = getEncoder() 150 | oldName := hf.Name 151 | hf.Name = replaceRandomCharacter(hf.Name) 152 | require.NoError(t, encoder.WriteField(hf)) 153 | t.Logf("Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes", oldName, encodedLen, hf.Name, output.Len()) 154 | require.Greater(t, output.Len(), encodedLen) 155 | } 156 | 157 | func TestStaticTableForFieldNamesWithValues(t *testing.T) { 158 | var hf qpack.HeaderField 159 | for { 160 | // Only use values with at least 2 characters. 161 | // This makes sure that Huffman encoding doesn't compress them as much as encoding it using the static table would. 162 | if entry := staticTable[rand.IntN(len(staticTable))]; len(entry.Value) > 1 { 163 | hf = qpack.HeaderField{ 164 | Name: entry.Name, 165 | Value: randomString(20), 166 | } 167 | break 168 | } 169 | } 170 | encoder, output := getEncoder() 171 | require.NoError(t, encoder.WriteField(hf)) 172 | encodedLen := output.Len() 173 | check(t, output.Bytes(), hf) 174 | encoder, output = getEncoder() 175 | oldName := hf.Name 176 | hf.Name = replaceRandomCharacter(hf.Name) 177 | require.NoError(t, encoder.WriteField(hf)) 178 | t.Logf("Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes", oldName, encodedLen, hf.Name, output.Len()) 179 | require.Greater(t, output.Len(), encodedLen) 180 | } 181 | 182 | func TestStaticTableForFieldValues(t *testing.T) { 183 | var hf qpack.HeaderField 184 | for { 185 | // Only use values with at least 2 characters. 186 | // This makes sure that Huffman encoding doesn't compress them as much as encoding it using the static table would. 187 | if entry := staticTable[rand.IntN(len(staticTable))]; len(entry.Value) > 1 { 188 | hf = qpack.HeaderField{ 189 | Name: entry.Name, 190 | Value: entry.Value, 191 | } 192 | break 193 | } 194 | } 195 | encoder, output := getEncoder() 196 | require.NoError(t, encoder.WriteField(hf)) 197 | encodedLen := output.Len() 198 | check(t, output.Bytes(), hf) 199 | encoder, output = getEncoder() 200 | oldValue := hf.Value 201 | hf.Value = replaceRandomCharacter(hf.Value) 202 | require.NoError(t, encoder.WriteField(hf)) 203 | t.Logf( 204 | "Encoding field value:\n\t%s: %s -> %d bytes\n\t%s: %s -> %d bytes", 205 | hf.Name, oldValue, encodedLen, 206 | hf.Name, hf.Value, output.Len(), 207 | ) 208 | require.Greater(t, output.Len(), encodedLen) 209 | } 210 | -------------------------------------------------------------------------------- /static_table.go: -------------------------------------------------------------------------------- 1 | package qpack 2 | 3 | var staticTableEntries = [...]HeaderField{ 4 | {Name: ":authority"}, 5 | {Name: ":path", Value: "/"}, 6 | {Name: "age", Value: "0"}, 7 | {Name: "content-disposition"}, 8 | {Name: "content-length", Value: "0"}, 9 | {Name: "cookie"}, 10 | {Name: "date"}, 11 | {Name: "etag"}, 12 | {Name: "if-modified-since"}, 13 | {Name: "if-none-match"}, 14 | {Name: "last-modified"}, 15 | {Name: "link"}, 16 | {Name: "location"}, 17 | {Name: "referer"}, 18 | {Name: "set-cookie"}, 19 | {Name: ":method", Value: "CONNECT"}, 20 | {Name: ":method", Value: "DELETE"}, 21 | {Name: ":method", Value: "GET"}, 22 | {Name: ":method", Value: "HEAD"}, 23 | {Name: ":method", Value: "OPTIONS"}, 24 | {Name: ":method", Value: "POST"}, 25 | {Name: ":method", Value: "PUT"}, 26 | {Name: ":scheme", Value: "http"}, 27 | {Name: ":scheme", Value: "https"}, 28 | {Name: ":status", Value: "103"}, 29 | {Name: ":status", Value: "200"}, 30 | {Name: ":status", Value: "304"}, 31 | {Name: ":status", Value: "404"}, 32 | {Name: ":status", Value: "503"}, 33 | {Name: "accept", Value: "*/*"}, 34 | {Name: "accept", Value: "application/dns-message"}, 35 | {Name: "accept-encoding", Value: "gzip, deflate, br"}, 36 | {Name: "accept-ranges", Value: "bytes"}, 37 | {Name: "access-control-allow-headers", Value: "cache-control"}, 38 | {Name: "access-control-allow-headers", Value: "content-type"}, 39 | {Name: "access-control-allow-origin", Value: "*"}, 40 | {Name: "cache-control", Value: "max-age=0"}, 41 | {Name: "cache-control", Value: "max-age=2592000"}, 42 | {Name: "cache-control", Value: "max-age=604800"}, 43 | {Name: "cache-control", Value: "no-cache"}, 44 | {Name: "cache-control", Value: "no-store"}, 45 | {Name: "cache-control", Value: "public, max-age=31536000"}, 46 | {Name: "content-encoding", Value: "br"}, 47 | {Name: "content-encoding", Value: "gzip"}, 48 | {Name: "content-type", Value: "application/dns-message"}, 49 | {Name: "content-type", Value: "application/javascript"}, 50 | {Name: "content-type", Value: "application/json"}, 51 | {Name: "content-type", Value: "application/x-www-form-urlencoded"}, 52 | {Name: "content-type", Value: "image/gif"}, 53 | {Name: "content-type", Value: "image/jpeg"}, 54 | {Name: "content-type", Value: "image/png"}, 55 | {Name: "content-type", Value: "text/css"}, 56 | {Name: "content-type", Value: "text/html; charset=utf-8"}, 57 | {Name: "content-type", Value: "text/plain"}, 58 | {Name: "content-type", Value: "text/plain;charset=utf-8"}, 59 | {Name: "range", Value: "bytes=0-"}, 60 | {Name: "strict-transport-security", Value: "max-age=31536000"}, 61 | {Name: "strict-transport-security", Value: "max-age=31536000; includesubdomains"}, 62 | {Name: "strict-transport-security", Value: "max-age=31536000; includesubdomains; preload"}, 63 | {Name: "vary", Value: "accept-encoding"}, 64 | {Name: "vary", Value: "origin"}, 65 | {Name: "x-content-type-options", Value: "nosniff"}, 66 | {Name: "x-xss-protection", Value: "1; mode=block"}, 67 | {Name: ":status", Value: "100"}, 68 | {Name: ":status", Value: "204"}, 69 | {Name: ":status", Value: "206"}, 70 | {Name: ":status", Value: "302"}, 71 | {Name: ":status", Value: "400"}, 72 | {Name: ":status", Value: "403"}, 73 | {Name: ":status", Value: "421"}, 74 | {Name: ":status", Value: "425"}, 75 | {Name: ":status", Value: "500"}, 76 | {Name: "accept-language"}, 77 | {Name: "access-control-allow-credentials", Value: "FALSE"}, 78 | {Name: "access-control-allow-credentials", Value: "TRUE"}, 79 | {Name: "access-control-allow-headers", Value: "*"}, 80 | {Name: "access-control-allow-methods", Value: "get"}, 81 | {Name: "access-control-allow-methods", Value: "get, post, options"}, 82 | {Name: "access-control-allow-methods", Value: "options"}, 83 | {Name: "access-control-expose-headers", Value: "content-length"}, 84 | {Name: "access-control-request-headers", Value: "content-type"}, 85 | {Name: "access-control-request-method", Value: "get"}, 86 | {Name: "access-control-request-method", Value: "post"}, 87 | {Name: "alt-svc", Value: "clear"}, 88 | {Name: "authorization"}, 89 | {Name: "content-security-policy", Value: "script-src 'none'; object-src 'none'; base-uri 'none'"}, 90 | {Name: "early-data", Value: "1"}, 91 | {Name: "expect-ct"}, 92 | {Name: "forwarded"}, 93 | {Name: "if-range"}, 94 | {Name: "origin"}, 95 | {Name: "purpose", Value: "prefetch"}, 96 | {Name: "server"}, 97 | {Name: "timing-allow-origin", Value: "*"}, 98 | {Name: "upgrade-insecure-requests", Value: "1"}, 99 | {Name: "user-agent"}, 100 | {Name: "x-forwarded-for"}, 101 | {Name: "x-frame-options", Value: "deny"}, 102 | {Name: "x-frame-options", Value: "sameorigin"}, 103 | } 104 | 105 | // Only needed for tests. 106 | // use go:linkname to retrieve the static table. 107 | // 108 | //nolint:deadcode,unused 109 | func getStaticTable() []HeaderField { 110 | return staticTableEntries[:] 111 | } 112 | 113 | type indexAndValues struct { 114 | idx uint8 115 | values map[string]uint8 116 | } 117 | 118 | // A map of the header names from the static table to their index in the table. 119 | // This is used by the encoder to quickly find if a header is in the static table 120 | // and what value should be used to encode it. 121 | // There's a second level of mapping for the headers that have some predefined 122 | // values in the static table. 123 | var encoderMap = map[string]indexAndValues{ 124 | ":authority": {0, nil}, 125 | ":path": {1, map[string]uint8{"/": 1}}, 126 | "age": {2, map[string]uint8{"0": 2}}, 127 | "content-disposition": {3, nil}, 128 | "content-length": {4, map[string]uint8{"0": 4}}, 129 | "cookie": {5, nil}, 130 | "date": {6, nil}, 131 | "etag": {7, nil}, 132 | "if-modified-since": {8, nil}, 133 | "if-none-match": {9, nil}, 134 | "last-modified": {10, nil}, 135 | "link": {11, nil}, 136 | "location": {12, nil}, 137 | "referer": {13, nil}, 138 | "set-cookie": {14, nil}, 139 | ":method": {15, map[string]uint8{ 140 | "CONNECT": 15, 141 | "DELETE": 16, 142 | "GET": 17, 143 | "HEAD": 18, 144 | "OPTIONS": 19, 145 | "POST": 20, 146 | "PUT": 21, 147 | }}, 148 | ":scheme": {22, map[string]uint8{ 149 | "http": 22, 150 | "https": 23, 151 | }}, 152 | ":status": {24, map[string]uint8{ 153 | "103": 24, 154 | "200": 25, 155 | "304": 26, 156 | "404": 27, 157 | "503": 28, 158 | "100": 63, 159 | "204": 64, 160 | "206": 65, 161 | "302": 66, 162 | "400": 67, 163 | "403": 68, 164 | "421": 69, 165 | "425": 70, 166 | "500": 71, 167 | }}, 168 | "accept": {29, map[string]uint8{ 169 | "*/*": 29, 170 | "application/dns-message": 30, 171 | }}, 172 | "accept-encoding": {31, map[string]uint8{"gzip, deflate, br": 31}}, 173 | "accept-ranges": {32, map[string]uint8{"bytes": 32}}, 174 | "access-control-allow-headers": {33, map[string]uint8{ 175 | "cache-control": 33, 176 | "content-type": 34, 177 | "*": 75, 178 | }}, 179 | "access-control-allow-origin": {35, map[string]uint8{"*": 35}}, 180 | "cache-control": {36, map[string]uint8{ 181 | "max-age=0": 36, 182 | "max-age=2592000": 37, 183 | "max-age=604800": 38, 184 | "no-cache": 39, 185 | "no-store": 40, 186 | "public, max-age=31536000": 41, 187 | }}, 188 | "content-encoding": {42, map[string]uint8{ 189 | "br": 42, 190 | "gzip": 43, 191 | }}, 192 | "content-type": {44, map[string]uint8{ 193 | "application/dns-message": 44, 194 | "application/javascript": 45, 195 | "application/json": 46, 196 | "application/x-www-form-urlencoded": 47, 197 | "image/gif": 48, 198 | "image/jpeg": 49, 199 | "image/png": 50, 200 | "text/css": 51, 201 | "text/html; charset=utf-8": 52, 202 | "text/plain": 53, 203 | "text/plain;charset=utf-8": 54, 204 | }}, 205 | "range": {55, map[string]uint8{"bytes=0-": 55}}, 206 | "strict-transport-security": {56, map[string]uint8{ 207 | "max-age=31536000": 56, 208 | "max-age=31536000; includesubdomains": 57, 209 | "max-age=31536000; includesubdomains; preload": 58, 210 | }}, 211 | "vary": {59, map[string]uint8{ 212 | "accept-encoding": 59, 213 | "origin": 60, 214 | }}, 215 | "x-content-type-options": {61, map[string]uint8{"nosniff": 61}}, 216 | "x-xss-protection": {62, map[string]uint8{"1; mode=block": 62}}, 217 | // ":status" is duplicated and takes index 63 to 71 218 | "accept-language": {72, nil}, 219 | "access-control-allow-credentials": {73, map[string]uint8{ 220 | "FALSE": 73, 221 | "TRUE": 74, 222 | }}, 223 | // "access-control-allow-headers" is duplicated and takes index 75 224 | "access-control-allow-methods": {76, map[string]uint8{ 225 | "get": 76, 226 | "get, post, options": 77, 227 | "options": 78, 228 | }}, 229 | "access-control-expose-headers": {79, map[string]uint8{"content-length": 79}}, 230 | "access-control-request-headers": {80, map[string]uint8{"content-type": 80}}, 231 | "access-control-request-method": {81, map[string]uint8{ 232 | "get": 81, 233 | "post": 82, 234 | }}, 235 | "alt-svc": {83, map[string]uint8{"clear": 83}}, 236 | "authorization": {84, nil}, 237 | "content-security-policy": {85, map[string]uint8{ 238 | "script-src 'none'; object-src 'none'; base-uri 'none'": 85, 239 | }}, 240 | "early-data": {86, map[string]uint8{"1": 86}}, 241 | "expect-ct": {87, nil}, 242 | "forwarded": {88, nil}, 243 | "if-range": {89, nil}, 244 | "origin": {90, nil}, 245 | "purpose": {91, map[string]uint8{"prefetch": 91}}, 246 | "server": {92, nil}, 247 | "timing-allow-origin": {93, map[string]uint8{"*": 93}}, 248 | "upgrade-insecure-requests": {94, map[string]uint8{"1": 94}}, 249 | "user-agent": {95, nil}, 250 | "x-forwarded-for": {96, nil}, 251 | "x-frame-options": {97, map[string]uint8{ 252 | "deny": 97, 253 | "sameorigin": 98, 254 | }}, 255 | } 256 | -------------------------------------------------------------------------------- /static_table_test.go: -------------------------------------------------------------------------------- 1 | package qpack 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestEncoderMapHasValueForEveryStaticTableEntry(t *testing.T) { 10 | for idx, hf := range staticTableEntries { 11 | if len(hf.Value) == 0 { 12 | require.Equal(t, uint8(idx), encoderMap[hf.Name].idx) 13 | } else { 14 | require.Equal(t, uint8(idx), encoderMap[hf.Name].values[hf.Value]) 15 | } 16 | } 17 | } 18 | 19 | func TestStaticTableasValueForEveryEncoderMapEntry(t *testing.T) { 20 | for name, indexAndVal := range encoderMap { 21 | if len(indexAndVal.values) == 0 { 22 | id := indexAndVal.idx 23 | require.Equal(t, name, staticTableEntries[id].Name) 24 | require.Empty(t, staticTableEntries[id].Value) 25 | } else { 26 | for value, id := range indexAndVal.values { 27 | require.Equal(t, name, staticTableEntries[id].Name) 28 | require.Equal(t, value, staticTableEntries[id].Value) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /varint.go: -------------------------------------------------------------------------------- 1 | package qpack 2 | 3 | // copied from the Go standard library HPACK implementation 4 | 5 | import "errors" 6 | 7 | var errVarintOverflow = errors.New("varint integer overflow") 8 | 9 | // appendVarInt appends i, as encoded in variable integer form using n 10 | // bit prefix, to dst and returns the extended buffer. 11 | // 12 | // See 13 | // http://http2.github.io/http2-spec/compression.html#integer.representation 14 | func appendVarInt(dst []byte, n byte, i uint64) []byte { 15 | k := uint64((1 << n) - 1) 16 | if i < k { 17 | return append(dst, byte(i)) 18 | } 19 | dst = append(dst, byte(k)) 20 | i -= k 21 | for ; i >= 128; i >>= 7 { 22 | dst = append(dst, byte(0x80|(i&0x7f))) 23 | } 24 | return append(dst, byte(i)) 25 | } 26 | 27 | // readVarInt reads an unsigned variable length integer off the 28 | // beginning of p. n is the parameter as described in 29 | // http://http2.github.io/http2-spec/compression.html#rfc.section.5.1. 30 | // 31 | // n must always be between 1 and 8. 32 | // 33 | // The returned remain buffer is either a smaller suffix of p, or err != nil. 34 | // The error is errNeedMore if p doesn't contain a complete integer. 35 | func readVarInt(n byte, p []byte) (i uint64, remain []byte, err error) { 36 | if n < 1 || n > 8 { 37 | panic("bad n") 38 | } 39 | if len(p) == 0 { 40 | return 0, p, errNeedMore 41 | } 42 | i = uint64(p[0]) 43 | if n < 8 { 44 | i &= (1 << uint64(n)) - 1 45 | } 46 | if i < (1< 0 { 54 | b := p[0] 55 | p = p[1:] 56 | i += uint64(b&127) << m 57 | if b&128 == 0 { 58 | return i, p, nil 59 | } 60 | m += 7 61 | if m >= 63 { // TODO: proper overflow check. making this up. 62 | return 0, origP, errVarintOverflow 63 | } 64 | } 65 | return 0, origP, errNeedMore 66 | } 67 | --------------------------------------------------------------------------------