|  | // Copyright 2014 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. | 
|  |  | 
|  | package hpack | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "encoding/hex" | 
|  | "fmt" | 
|  | "math/rand" | 
|  | "reflect" | 
|  | "strings" | 
|  | "testing" | 
|  | ) | 
|  |  | 
|  | func TestEncoderTableSizeUpdate(t *testing.T) { | 
|  | tests := []struct { | 
|  | size1, size2 uint32 | 
|  | wantHex      string | 
|  | }{ | 
|  | // Should emit 2 table size updates (2048 and 4096) | 
|  | {2048, 4096, "3fe10f 3fe11f 82"}, | 
|  |  | 
|  | // Should emit 1 table size update (2048) | 
|  | {16384, 2048, "3fe10f 82"}, | 
|  | } | 
|  | for _, tt := range tests { | 
|  | var buf bytes.Buffer | 
|  | e := NewEncoder(&buf) | 
|  | e.SetMaxDynamicTableSize(tt.size1) | 
|  | e.SetMaxDynamicTableSize(tt.size2) | 
|  | if err := e.WriteField(pair(":method", "GET")); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | want := removeSpace(tt.wantHex) | 
|  | if got := hex.EncodeToString(buf.Bytes()); got != want { | 
|  | t.Errorf("e.SetDynamicTableSize %v, %v = %q; want %q", tt.size1, tt.size2, got, want) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestEncoderWriteField(t *testing.T) { | 
|  | var buf bytes.Buffer | 
|  | e := NewEncoder(&buf) | 
|  | var got []HeaderField | 
|  | d := NewDecoder(4<<10, func(f HeaderField) { | 
|  | got = append(got, f) | 
|  | }) | 
|  |  | 
|  | tests := []struct { | 
|  | hdrs []HeaderField | 
|  | }{ | 
|  | {[]HeaderField{ | 
|  | pair(":method", "GET"), | 
|  | pair(":scheme", "http"), | 
|  | pair(":path", "/"), | 
|  | pair(":authority", "www.example.com"), | 
|  | }}, | 
|  | {[]HeaderField{ | 
|  | pair(":method", "GET"), | 
|  | pair(":scheme", "http"), | 
|  | pair(":path", "/"), | 
|  | pair(":authority", "www.example.com"), | 
|  | pair("cache-control", "no-cache"), | 
|  | }}, | 
|  | {[]HeaderField{ | 
|  | pair(":method", "GET"), | 
|  | pair(":scheme", "https"), | 
|  | pair(":path", "/index.html"), | 
|  | pair(":authority", "www.example.com"), | 
|  | pair("custom-key", "custom-value"), | 
|  | }}, | 
|  | } | 
|  | for i, tt := range tests { | 
|  | buf.Reset() | 
|  | got = got[:0] | 
|  | for _, hf := range tt.hdrs { | 
|  | if err := e.WriteField(hf); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | } | 
|  | _, err := d.Write(buf.Bytes()) | 
|  | if err != nil { | 
|  | t.Errorf("%d. Decoder Write = %v", i, err) | 
|  | } | 
|  | if !reflect.DeepEqual(got, tt.hdrs) { | 
|  | t.Errorf("%d. Decoded %+v; want %+v", i, got, tt.hdrs) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestEncoderSearchTable(t *testing.T) { | 
|  | e := NewEncoder(nil) | 
|  |  | 
|  | e.dynTab.add(pair("foo", "bar")) | 
|  | e.dynTab.add(pair("blake", "miz")) | 
|  | e.dynTab.add(pair(":method", "GET")) | 
|  |  | 
|  | tests := []struct { | 
|  | hf        HeaderField | 
|  | wantI     uint64 | 
|  | wantMatch bool | 
|  | }{ | 
|  | // Name and Value match | 
|  | {pair("foo", "bar"), uint64(staticTable.len()) + 3, true}, | 
|  | {pair("blake", "miz"), uint64(staticTable.len()) + 2, true}, | 
|  | {pair(":method", "GET"), 2, true}, | 
|  |  | 
|  | // Only name match because Sensitive == true. This is allowed to match | 
|  | // any ":method" entry. The current implementation uses the last entry | 
|  | // added in newStaticTable. | 
|  | {HeaderField{":method", "GET", true}, 3, false}, | 
|  |  | 
|  | // Only Name matches | 
|  | {pair("foo", "..."), uint64(staticTable.len()) + 3, false}, | 
|  | {pair("blake", "..."), uint64(staticTable.len()) + 2, false}, | 
|  | // As before, this is allowed to match any ":method" entry. | 
|  | {pair(":method", "..."), 3, false}, | 
|  |  | 
|  | // None match | 
|  | {pair("foo-", "bar"), 0, false}, | 
|  | } | 
|  | for _, tt := range tests { | 
|  | if gotI, gotMatch := e.searchTable(tt.hf); gotI != tt.wantI || gotMatch != tt.wantMatch { | 
|  | t.Errorf("d.search(%+v) = %v, %v; want %v, %v", tt.hf, gotI, gotMatch, tt.wantI, tt.wantMatch) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestAppendVarInt(t *testing.T) { | 
|  | tests := []struct { | 
|  | n    byte | 
|  | i    uint64 | 
|  | want []byte | 
|  | }{ | 
|  | // Fits in a byte: | 
|  | {1, 0, []byte{0}}, | 
|  | {2, 2, []byte{2}}, | 
|  | {3, 6, []byte{6}}, | 
|  | {4, 14, []byte{14}}, | 
|  | {5, 30, []byte{30}}, | 
|  | {6, 62, []byte{62}}, | 
|  | {7, 126, []byte{126}}, | 
|  | {8, 254, []byte{254}}, | 
|  |  | 
|  | // Multiple bytes: | 
|  | {5, 1337, []byte{31, 154, 10}}, | 
|  | } | 
|  | for _, tt := range tests { | 
|  | got := appendVarInt(nil, tt.n, tt.i) | 
|  | if !bytes.Equal(got, tt.want) { | 
|  | t.Errorf("appendVarInt(nil, %v, %v) = %v; want %v", tt.n, tt.i, got, tt.want) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestAppendHpackString(t *testing.T) { | 
|  | tests := []struct { | 
|  | s, wantHex string | 
|  | }{ | 
|  | // Huffman encoded | 
|  | {"www.example.com", "8c f1e3 c2e5 f23a 6ba0 ab90 f4ff"}, | 
|  |  | 
|  | // Not Huffman encoded | 
|  | {"a", "01 61"}, | 
|  |  | 
|  | // zero length | 
|  | {"", "00"}, | 
|  | } | 
|  | for _, tt := range tests { | 
|  | want := removeSpace(tt.wantHex) | 
|  | buf := appendHpackString(nil, tt.s) | 
|  | if got := hex.EncodeToString(buf); want != got { | 
|  | t.Errorf("appendHpackString(nil, %q) = %q; want %q", tt.s, got, want) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestAppendIndexed(t *testing.T) { | 
|  | tests := []struct { | 
|  | i       uint64 | 
|  | wantHex string | 
|  | }{ | 
|  | // 1 byte | 
|  | {1, "81"}, | 
|  | {126, "fe"}, | 
|  |  | 
|  | // 2 bytes | 
|  | {127, "ff00"}, | 
|  | {128, "ff01"}, | 
|  | } | 
|  | for _, tt := range tests { | 
|  | want := removeSpace(tt.wantHex) | 
|  | buf := appendIndexed(nil, tt.i) | 
|  | if got := hex.EncodeToString(buf); want != got { | 
|  | t.Errorf("appendIndex(nil, %v) = %q; want %q", tt.i, got, want) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestAppendNewName(t *testing.T) { | 
|  | tests := []struct { | 
|  | f        HeaderField | 
|  | indexing bool | 
|  | wantHex  string | 
|  | }{ | 
|  | // Incremental indexing | 
|  | {HeaderField{"custom-key", "custom-value", false}, true, "40 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"}, | 
|  |  | 
|  | // Without indexing | 
|  | {HeaderField{"custom-key", "custom-value", false}, false, "00 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"}, | 
|  |  | 
|  | // Never indexed | 
|  | {HeaderField{"custom-key", "custom-value", true}, true, "10 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"}, | 
|  | {HeaderField{"custom-key", "custom-value", true}, false, "10 88 25a8 49e9 5ba9 7d7f 89 25a8 49e9 5bb8 e8b4 bf"}, | 
|  | } | 
|  | for _, tt := range tests { | 
|  | want := removeSpace(tt.wantHex) | 
|  | buf := appendNewName(nil, tt.f, tt.indexing) | 
|  | if got := hex.EncodeToString(buf); want != got { | 
|  | t.Errorf("appendNewName(nil, %+v, %v) = %q; want %q", tt.f, tt.indexing, got, want) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestAppendIndexedName(t *testing.T) { | 
|  | tests := []struct { | 
|  | f        HeaderField | 
|  | i        uint64 | 
|  | indexing bool | 
|  | wantHex  string | 
|  | }{ | 
|  | // Incremental indexing | 
|  | {HeaderField{":status", "302", false}, 8, true, "48 82 6402"}, | 
|  |  | 
|  | // Without indexing | 
|  | {HeaderField{":status", "302", false}, 8, false, "08 82 6402"}, | 
|  |  | 
|  | // Never indexed | 
|  | {HeaderField{":status", "302", true}, 8, true, "18 82 6402"}, | 
|  | {HeaderField{":status", "302", true}, 8, false, "18 82 6402"}, | 
|  | } | 
|  | for _, tt := range tests { | 
|  | want := removeSpace(tt.wantHex) | 
|  | buf := appendIndexedName(nil, tt.f, tt.i, tt.indexing) | 
|  | if got := hex.EncodeToString(buf); want != got { | 
|  | t.Errorf("appendIndexedName(nil, %+v, %v) = %q; want %q", tt.f, tt.indexing, got, want) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestAppendTableSize(t *testing.T) { | 
|  | tests := []struct { | 
|  | i       uint32 | 
|  | wantHex string | 
|  | }{ | 
|  | // Fits into 1 byte | 
|  | {30, "3e"}, | 
|  |  | 
|  | // Extra byte | 
|  | {31, "3f00"}, | 
|  | {32, "3f01"}, | 
|  | } | 
|  | for _, tt := range tests { | 
|  | want := removeSpace(tt.wantHex) | 
|  | buf := appendTableSize(nil, tt.i) | 
|  | if got := hex.EncodeToString(buf); want != got { | 
|  | t.Errorf("appendTableSize(nil, %v) = %q; want %q", tt.i, got, want) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestEncoderSetMaxDynamicTableSize(t *testing.T) { | 
|  | var buf bytes.Buffer | 
|  | e := NewEncoder(&buf) | 
|  | tests := []struct { | 
|  | v           uint32 | 
|  | wantUpdate  bool | 
|  | wantMinSize uint32 | 
|  | wantMaxSize uint32 | 
|  | }{ | 
|  | // Set new table size to 2048 | 
|  | {2048, true, 2048, 2048}, | 
|  |  | 
|  | // Set new table size to 16384, but still limited to | 
|  | // 4096 | 
|  | {16384, true, 2048, 4096}, | 
|  | } | 
|  | for _, tt := range tests { | 
|  | e.SetMaxDynamicTableSize(tt.v) | 
|  | if got := e.tableSizeUpdate; tt.wantUpdate != got { | 
|  | t.Errorf("e.tableSizeUpdate = %v; want %v", got, tt.wantUpdate) | 
|  | } | 
|  | if got := e.minSize; tt.wantMinSize != got { | 
|  | t.Errorf("e.minSize = %v; want %v", got, tt.wantMinSize) | 
|  | } | 
|  | if got := e.dynTab.maxSize; tt.wantMaxSize != got { | 
|  | t.Errorf("e.maxSize = %v; want %v", got, tt.wantMaxSize) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestEncoderSetMaxDynamicTableSizeLimit(t *testing.T) { | 
|  | e := NewEncoder(nil) | 
|  | // 4095 < initialHeaderTableSize means maxSize is truncated to | 
|  | // 4095. | 
|  | e.SetMaxDynamicTableSizeLimit(4095) | 
|  | if got, want := e.dynTab.maxSize, uint32(4095); got != want { | 
|  | t.Errorf("e.dynTab.maxSize = %v; want %v", got, want) | 
|  | } | 
|  | if got, want := e.maxSizeLimit, uint32(4095); got != want { | 
|  | t.Errorf("e.maxSizeLimit = %v; want %v", got, want) | 
|  | } | 
|  | if got, want := e.tableSizeUpdate, true; got != want { | 
|  | t.Errorf("e.tableSizeUpdate = %v; want %v", got, want) | 
|  | } | 
|  | // maxSize will be truncated to maxSizeLimit | 
|  | e.SetMaxDynamicTableSize(16384) | 
|  | if got, want := e.dynTab.maxSize, uint32(4095); got != want { | 
|  | t.Errorf("e.dynTab.maxSize = %v; want %v", got, want) | 
|  | } | 
|  | // 8192 > current maxSizeLimit, so maxSize does not change. | 
|  | e.SetMaxDynamicTableSizeLimit(8192) | 
|  | if got, want := e.dynTab.maxSize, uint32(4095); got != want { | 
|  | t.Errorf("e.dynTab.maxSize = %v; want %v", got, want) | 
|  | } | 
|  | if got, want := e.maxSizeLimit, uint32(8192); got != want { | 
|  | t.Errorf("e.maxSizeLimit = %v; want %v", got, want) | 
|  | } | 
|  | } | 
|  |  | 
|  | func removeSpace(s string) string { | 
|  | return strings.Replace(s, " ", "", -1) | 
|  | } | 
|  |  | 
|  | func BenchmarkEncoderSearchTable(b *testing.B) { | 
|  | e := NewEncoder(nil) | 
|  |  | 
|  | // A sample of possible header fields. | 
|  | // This is not based on any actual data from HTTP/2 traces. | 
|  | var possible []HeaderField | 
|  | for _, f := range staticTable.ents { | 
|  | if f.Value == "" { | 
|  | possible = append(possible, f) | 
|  | continue | 
|  | } | 
|  | // Generate 5 random values, except for cookie and set-cookie, | 
|  | // which we know can have many values in practice. | 
|  | num := 5 | 
|  | if f.Name == "cookie" || f.Name == "set-cookie" { | 
|  | num = 25 | 
|  | } | 
|  | for i := 0; i < num; i++ { | 
|  | f.Value = fmt.Sprintf("%s-%d", f.Name, i) | 
|  | possible = append(possible, f) | 
|  | } | 
|  | } | 
|  | for k := 0; k < 10; k++ { | 
|  | f := HeaderField{ | 
|  | Name:      fmt.Sprintf("x-header-%d", k), | 
|  | Sensitive: rand.Int()%2 == 0, | 
|  | } | 
|  | for i := 0; i < 5; i++ { | 
|  | f.Value = fmt.Sprintf("%s-%d", f.Name, i) | 
|  | possible = append(possible, f) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Add a random sample to the dynamic table. This very loosely simulates | 
|  | // a history of 100 requests with 20 header fields per request. | 
|  | for r := 0; r < 100*20; r++ { | 
|  | f := possible[rand.Int31n(int32(len(possible)))] | 
|  | // Skip if this is in the staticTable verbatim. | 
|  | if _, has := staticTable.search(f); !has { | 
|  | e.dynTab.add(f) | 
|  | } | 
|  | } | 
|  |  | 
|  | b.ResetTimer() | 
|  | for n := 0; n < b.N; n++ { | 
|  | for _, f := range possible { | 
|  | e.searchTable(f) | 
|  | } | 
|  | } | 
|  | } |