golang.org/x/net@v0.25.1-0.20240516223405-c87a5b62e243/http2/hpack/encode_test.go (about) 1 // Copyright 2014 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package hpack 6 7 import ( 8 "bytes" 9 "encoding/hex" 10 "fmt" 11 "math/rand" 12 "reflect" 13 "strings" 14 "testing" 15 ) 16 17 func TestEncoderTableSizeUpdate(t *testing.T) { 18 tests := []struct { 19 size1, size2 uint32 20 wantHex string 21 }{ 22 // Should emit 2 table size updates (2048 and 4096) 23 {2048, 4096, "3fe10f 3fe11f 82"}, 24 25 // Should emit 1 table size update (2048) 26 {16384, 2048, "3fe10f 82"}, 27 } 28 for _, tt := range tests { 29 var buf bytes.Buffer 30 e := NewEncoder(&buf) 31 e.SetMaxDynamicTableSize(tt.size1) 32 e.SetMaxDynamicTableSize(tt.size2) 33 if err := e.WriteField(pair(":method", "GET")); err != nil { 34 t.Fatal(err) 35 } 36 want := removeSpace(tt.wantHex) 37 if got := hex.EncodeToString(buf.Bytes()); got != want { 38 t.Errorf("e.SetDynamicTableSize %v, %v = %q; want %q", tt.size1, tt.size2, got, want) 39 } 40 } 41 } 42 43 func TestEncoderWriteField(t *testing.T) { 44 var buf bytes.Buffer 45 e := NewEncoder(&buf) 46 var got []HeaderField 47 d := NewDecoder(4<<10, func(f HeaderField) { 48 got = append(got, f) 49 }) 50 51 tests := []struct { 52 hdrs []HeaderField 53 }{ 54 {[]HeaderField{ 55 pair(":method", "GET"), 56 pair(":scheme", "http"), 57 pair(":path", "/"), 58 pair(":authority", "www.example.com"), 59 }}, 60 {[]HeaderField{ 61 pair(":method", "GET"), 62 pair(":scheme", "http"), 63 pair(":path", "/"), 64 pair(":authority", "www.example.com"), 65 pair("cache-control", "no-cache"), 66 }}, 67 {[]HeaderField{ 68 pair(":method", "GET"), 69 pair(":scheme", "https"), 70 pair(":path", "/index.html"), 71 pair(":authority", "www.example.com"), 72 pair("custom-key", "custom-value"), 73 }}, 74 } 75 for i, tt := range tests { 76 buf.Reset() 77 got = got[:0] 78 for _, hf := range tt.hdrs { 79 if err := e.WriteField(hf); err != nil { 80 t.Fatal(err) 81 } 82 } 83 _, err := d.Write(buf.Bytes()) 84 if err != nil { 85 t.Errorf("%d. Decoder Write = %v", i, err) 86 } 87 if !reflect.DeepEqual(got, tt.hdrs) { 88 t.Errorf("%d. Decoded %+v; want %+v", i, got, tt.hdrs) 89 } 90 } 91 } 92 93 func TestEncoderSearchTable(t *testing.T) { 94 e := NewEncoder(nil) 95 96 e.dynTab.add(pair("foo", "bar")) 97 e.dynTab.add(pair("blake", "miz")) 98 e.dynTab.add(pair(":method", "GET")) 99 100 tests := []struct { 101 hf HeaderField 102 wantI uint64 103 wantMatch bool 104 }{ 105 // Name and Value match 106 {pair("foo", "bar"), uint64(staticTable.len()) + 3, true}, 107 {pair("blake", "miz"), uint64(staticTable.len()) + 2, true}, 108 {pair(":method", "GET"), 2, true}, 109 110 // Only name match because Sensitive == true. This is allowed to match 111 // any ":method" entry. The current implementation uses the last entry 112 // added in newStaticTable. 113 {HeaderField{":method", "GET", true}, 3, false}, 114 115 // Only Name matches 116 {pair("foo", "..."), uint64(staticTable.len()) + 3, false}, 117 {pair("blake", "..."), uint64(staticTable.len()) + 2, false}, 118 // As before, this is allowed to match any ":method" entry. 119 {pair(":method", "..."), 3, false}, 120 121 // None match 122 {pair("foo-", "bar"), 0, false}, 123 } 124 for _, tt := range tests { 125 if gotI, gotMatch := e.searchTable(tt.hf); gotI != tt.wantI || gotMatch != tt.wantMatch { 126 t.Errorf("d.search(%+v) = %v, %v; want %v, %v", tt.hf, gotI, gotMatch, tt.wantI, tt.wantMatch) 127 } 128 } 129 } 130 131 func TestAppendVarInt(t *testing.T) { 132 tests := []struct { 133 n byte 134 i uint64 135 want []byte 136 }{ 137 // Fits in a byte: 138 {1, 0, []byte{0}}, 139 {2, 2, []byte{2}}, 140 {3, 6, []byte{6}}, 141 {4, 14, []byte{14}}, 142 {5, 30, []byte{30}}, 143 {6, 62, []byte{62}}, 144 {7, 126, []byte{126}}, 145 {8, 254, []byte{254}}, 146 147 // Multiple bytes: 148 {5, 1337, []byte{31, 154, 10}}, 149 } 150 for _, tt := range tests { 151 got := appendVarInt(nil, tt.n, tt.i) 152 if !bytes.Equal(got, tt.want) { 153 t.Errorf("appendVarInt(nil, %v, %v) = %v; want %v", tt.n, tt.i, got, tt.want) 154 } 155 } 156 } 157 158 func TestAppendHpackString(t *testing.T) { 159 tests := []struct { 160 s, wantHex string 161 }{ 162 // Huffman encoded 163 {"www.example.com", "8c f1e3 c2e5 f23a 6ba0 ab90 f4ff"}, 164 165 // Not Huffman encoded 166 {"a", "01 61"}, 167 168 // zero length 169 {"", "00"}, 170 } 171 for _, tt := range tests { 172 want := removeSpace(tt.wantHex) 173 buf := appendHpackString(nil, tt.s) 174 if got := hex.EncodeToString(buf); want != got { 175 t.Errorf("appendHpackString(nil, %q) = %q; want %q", tt.s, got, want) 176 } 177 } 178 } 179 180 func TestAppendIndexed(t *testing.T) { 181 tests := []struct { 182 i uint64 183 wantHex string 184 }{ 185 // 1 byte 186 {1, "81"}, 187 {126, "fe"}, 188 189 // 2 bytes 190 {127, "ff00"}, 191 {128, "ff01"}, 192 } 193 for _, tt := range tests { 194 want := removeSpace(tt.wantHex) 195 buf := appendIndexed(nil, tt.i) 196 if got := hex.EncodeToString(buf); want != got { 197 t.Errorf("appendIndex(nil, %v) = %q; want %q", tt.i, got, want) 198 } 199 } 200 } 201 202 func TestAppendNewName(t *testing.T) { 203 tests := []struct { 204 f HeaderField 205 indexing bool 206 wantHex string 207 }{ 208 // Incremental indexing 209 {HeaderField{"custom-key", "custom-value", false}, true, "40 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"}, 210 211 // Without indexing 212 {HeaderField{"custom-key", "custom-value", false}, false, "00 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"}, 213 214 // Never indexed 215 {HeaderField{"custom-key", "custom-value", true}, true, "10 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"}, 216 {HeaderField{"custom-key", "custom-value", true}, false, "10 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"}, 217 } 218 for _, tt := range tests { 219 want := removeSpace(tt.wantHex) 220 buf := appendNewName(nil, tt.f, tt.indexing) 221 if got := hex.EncodeToString(buf); want != got { 222 t.Errorf("appendNewName(nil, %+v, %v) = %q; want %q", tt.f, tt.indexing, got, want) 223 } 224 } 225 } 226 227 func TestAppendIndexedName(t *testing.T) { 228 tests := []struct { 229 f HeaderField 230 i uint64 231 indexing bool 232 wantHex string 233 }{ 234 // Incremental indexing 235 {HeaderField{":status", "302", false}, 8, true, "48 82 6402"}, 236 237 // Without indexing 238 {HeaderField{":status", "302", false}, 8, false, "08 82 6402"}, 239 240 // Never indexed 241 {HeaderField{":status", "302", true}, 8, true, "18 82 6402"}, 242 {HeaderField{":status", "302", true}, 8, false, "18 82 6402"}, 243 } 244 for _, tt := range tests { 245 want := removeSpace(tt.wantHex) 246 buf := appendIndexedName(nil, tt.f, tt.i, tt.indexing) 247 if got := hex.EncodeToString(buf); want != got { 248 t.Errorf("appendIndexedName(nil, %+v, %v) = %q; want %q", tt.f, tt.indexing, got, want) 249 } 250 } 251 } 252 253 func TestAppendTableSize(t *testing.T) { 254 tests := []struct { 255 i uint32 256 wantHex string 257 }{ 258 // Fits into 1 byte 259 {30, "3e"}, 260 261 // Extra byte 262 {31, "3f00"}, 263 {32, "3f01"}, 264 } 265 for _, tt := range tests { 266 want := removeSpace(tt.wantHex) 267 buf := appendTableSize(nil, tt.i) 268 if got := hex.EncodeToString(buf); want != got { 269 t.Errorf("appendTableSize(nil, %v) = %q; want %q", tt.i, got, want) 270 } 271 } 272 } 273 274 func TestEncoderSetMaxDynamicTableSize(t *testing.T) { 275 var buf bytes.Buffer 276 e := NewEncoder(&buf) 277 tests := []struct { 278 v uint32 279 wantUpdate bool 280 wantMinSize uint32 281 wantMaxSize uint32 282 }{ 283 // Set new table size to 2048 284 {2048, true, 2048, 2048}, 285 286 // Set new table size to 16384, but still limited to 287 // 4096 288 {16384, true, 2048, 4096}, 289 } 290 for _, tt := range tests { 291 e.SetMaxDynamicTableSize(tt.v) 292 if got := e.tableSizeUpdate; tt.wantUpdate != got { 293 t.Errorf("e.tableSizeUpdate = %v; want %v", got, tt.wantUpdate) 294 } 295 if got := e.minSize; tt.wantMinSize != got { 296 t.Errorf("e.minSize = %v; want %v", got, tt.wantMinSize) 297 } 298 if got := e.dynTab.maxSize; tt.wantMaxSize != got { 299 t.Errorf("e.maxSize = %v; want %v", got, tt.wantMaxSize) 300 } 301 } 302 } 303 304 func TestEncoderSetMaxDynamicTableSizeLimit(t *testing.T) { 305 e := NewEncoder(nil) 306 // 4095 < initialHeaderTableSize means maxSize is truncated to 307 // 4095. 308 e.SetMaxDynamicTableSizeLimit(4095) 309 if got, want := e.dynTab.maxSize, uint32(4095); got != want { 310 t.Errorf("e.dynTab.maxSize = %v; want %v", got, want) 311 } 312 if got, want := e.maxSizeLimit, uint32(4095); got != want { 313 t.Errorf("e.maxSizeLimit = %v; want %v", got, want) 314 } 315 if got, want := e.tableSizeUpdate, true; got != want { 316 t.Errorf("e.tableSizeUpdate = %v; want %v", got, want) 317 } 318 // maxSize will be truncated to maxSizeLimit 319 e.SetMaxDynamicTableSize(16384) 320 if got, want := e.dynTab.maxSize, uint32(4095); got != want { 321 t.Errorf("e.dynTab.maxSize = %v; want %v", got, want) 322 } 323 // 8192 > current maxSizeLimit, so maxSize does not change. 324 e.SetMaxDynamicTableSizeLimit(8192) 325 if got, want := e.dynTab.maxSize, uint32(4095); got != want { 326 t.Errorf("e.dynTab.maxSize = %v; want %v", got, want) 327 } 328 if got, want := e.maxSizeLimit, uint32(8192); got != want { 329 t.Errorf("e.maxSizeLimit = %v; want %v", got, want) 330 } 331 } 332 333 func removeSpace(s string) string { 334 return strings.Replace(s, " ", "", -1) 335 } 336 337 func BenchmarkEncoderSearchTable(b *testing.B) { 338 e := NewEncoder(nil) 339 340 // A sample of possible header fields. 341 // This is not based on any actual data from HTTP/2 traces. 342 var possible []HeaderField 343 for _, f := range staticTable.ents { 344 if f.Value == "" { 345 possible = append(possible, f) 346 continue 347 } 348 // Generate 5 random values, except for cookie and set-cookie, 349 // which we know can have many values in practice. 350 num := 5 351 if f.Name == "cookie" || f.Name == "set-cookie" { 352 num = 25 353 } 354 for i := 0; i < num; i++ { 355 f.Value = fmt.Sprintf("%s-%d", f.Name, i) 356 possible = append(possible, f) 357 } 358 } 359 for k := 0; k < 10; k++ { 360 f := HeaderField{ 361 Name: fmt.Sprintf("x-header-%d", k), 362 Sensitive: rand.Int()%2 == 0, 363 } 364 for i := 0; i < 5; i++ { 365 f.Value = fmt.Sprintf("%s-%d", f.Name, i) 366 possible = append(possible, f) 367 } 368 } 369 370 // Add a random sample to the dynamic table. This very loosely simulates 371 // a history of 100 requests with 20 header fields per request. 372 for r := 0; r < 100*20; r++ { 373 f := possible[rand.Int31n(int32(len(possible)))] 374 // Skip if this is in the staticTable verbatim. 375 if _, has := staticTable.search(f); !has { 376 e.dynTab.add(f) 377 } 378 } 379 380 b.ResetTimer() 381 for n := 0; n < b.N; n++ { 382 for _, f := range possible { 383 e.searchTable(f) 384 } 385 } 386 }