Browse Source

Merge pull request #15 from tatsuhiro-t/hpack-encoder

Implement HPACK encoder that actually compresses header block
Brad Fitzpatrick 11 years ago
parent
commit
2384117107
6 changed files with 704 additions and 36 deletions
  1. 225 21
      hpack/encode.go
  2. 308 15
      hpack/encode_test.go
  3. 43 0
      hpack/hpack.go
  4. 61 0
      hpack/hpack_test.go
  5. 66 0
      hpack/huffman.go
  6. 1 0
      server.go

+ 225 - 21
hpack/encode.go

@@ -6,43 +6,247 @@
 package hpack
 package hpack
 
 
 import (
 import (
-	"errors"
 	"io"
 	"io"
 )
 )
 
 
+const (
+	uint32Max              = ^uint32(0)
+	initialHeaderTableSize = 4096
+)
+
 type Encoder struct {
 type Encoder struct {
-	w   io.Writer
-	buf []byte
+	dynTab dynamicTable
+	// minSize is the minimum table size set by
+	// SetMaxDynamicTableSize after the previous Header Table Size
+	// Update.
+	minSize uint32
+	// maxSizeLimit is the maximum table size this encoder
+	// supports. This will protect the encoder from too large
+	// size.
+	maxSizeLimit uint32
+	// tableSizeUpdate indicates whether "Header Table Size
+	// Update" is required.
+	tableSizeUpdate bool
+	w               io.Writer
+	buf             []byte
 }
 }
 
 
+// NewEncoder returns a new Encoder which performs HPACK encoding. An
+// encoded data is written to w.
 func NewEncoder(w io.Writer) *Encoder {
 func NewEncoder(w io.Writer) *Encoder {
-	return &Encoder{w: w}
+	e := &Encoder{
+		minSize:         uint32Max,
+		maxSizeLimit:    initialHeaderTableSize,
+		tableSizeUpdate: false,
+		w:               w,
+	}
+	e.dynTab.setMaxSize(initialHeaderTableSize)
+	return e
 }
 }
 
 
 // WriteField encodes f into a single Write to e's underlying Writer.
 // WriteField encodes f into a single Write to e's underlying Writer.
+// This function may also produce bytes for "Header Table Size Update"
+// if necessary.  If produced, it is done before encoding f.
 func (e *Encoder) WriteField(f HeaderField) error {
 func (e *Encoder) WriteField(f HeaderField) error {
-	// TODO: write a decent Encoder. This is a quick hack job just to move on
-	// to other pieces. With this encoder, we only write literals.
-	// The form this writes is:
-	// http://http2.github.io/http2-spec/compression.html#rfc.section.6.2.2
-	// In particular, that second table. And no huffman
-	// compression.  Oh, and let's be even lazier for now and
-	// reject keys or values with lengths greather than 126 so we
-	// can save some time by not writing a varint encoder for
-	// now. But I'm wasting time writing this comment.
-	if len(f.Name) > 126 || len(f.Value) > 126 {
-		return errors.New("TODO: finish hpack encoder. names/values too long for current laziness, procrastination, impatience.")
-	}
 	e.buf = e.buf[:0]
 	e.buf = e.buf[:0]
-	e.buf = append(e.buf, 0)
-	e.buf = append(e.buf, byte(len(f.Name)))
-	e.buf = append(e.buf, f.Name...)
-	e.buf = append(e.buf, byte(len(f.Value)))
-	e.buf = append(e.buf, f.Value...)
 
 
+	if e.tableSizeUpdate {
+		e.tableSizeUpdate = false
+		if e.minSize < e.dynTab.maxSize {
+			e.buf = appendTableSize(e.buf, e.minSize)
+		}
+		e.minSize = uint32Max
+		e.buf = appendTableSize(e.buf, e.dynTab.maxSize)
+	}
+
+	idx, nameValueMatch := e.searchTable(f)
+	if nameValueMatch {
+		e.buf = appendIndexed(e.buf, idx)
+	} else {
+		indexing := e.shouldIndex(f)
+		if indexing {
+			e.dynTab.add(f)
+		}
+
+		if idx == 0 {
+			e.buf = appendNewName(e.buf, f, indexing)
+		} else {
+			e.buf = appendIndexedName(e.buf, f, idx, indexing)
+		}
+	}
 	n, err := e.w.Write(e.buf)
 	n, err := e.w.Write(e.buf)
 	if err == nil && n != len(e.buf) {
 	if err == nil && n != len(e.buf) {
 		err = io.ErrShortWrite
 		err = io.ErrShortWrite
 	}
 	}
 	return err
 	return err
 }
 }
+
+// searchTable searches f in both stable and dynamic header tables.
+// The static header table is searched first. Only when there is no
+// exact match for both name and value, the dynamic header table is
+// then searched. If there is no match, i is 0. If both name and value
+// match, i is the matched index and nameValueMatch becomes true. If
+// only name matches, i points to that index and nameValueMatch
+// becomes false.
+func (e *Encoder) searchTable(f HeaderField) (i uint64, nameValueMatch bool) {
+	for idx, hf := range staticTable {
+		if !constantTimeStringCompare(hf.Name, f.Name) {
+			continue
+		}
+		if i == 0 {
+			i = uint64(idx + 1)
+		}
+		if f.Sensitive {
+			continue
+		}
+		if !constantTimeStringCompare(hf.Value, f.Value) {
+			continue
+		}
+		i = uint64(idx + 1)
+		nameValueMatch = true
+		return
+	}
+
+	j, nameValueMatch := e.dynTab.search(f)
+	if nameValueMatch || (i == 0 && j != 0) {
+		i = j + uint64(len(staticTable))
+	}
+	return
+}
+
+// SetMaxDynamicTableSize changes the dynamic header table size to v.
+// The actual size is bounded by maxSizeLimit parameter passed to
+// NewEncoder.
+func (e *Encoder) SetMaxDynamicTableSize(v uint32) {
+	if v > e.maxSizeLimit {
+		v = e.maxSizeLimit
+	}
+	if v < e.minSize {
+		e.minSize = v
+	}
+	e.tableSizeUpdate = true
+	e.dynTab.setMaxSize(v)
+}
+
+// SetMaxDynamicTableSizeLimit changes the maximum value that can be
+// specified in SetMaxDynamicTableSize to v. By default, it is set to
+// 4096, which is the same size of the default dynamic header table
+// size described in HPACK specification. If the current maximum
+// dynamic header table size is strictly greater than v, "Header Table
+// Size Update" will be done in the next WriteField call and the
+// maximum dynamic header table size is truncated to v.
+func (e *Encoder) SetMaxDynamicTableSizeLimit(v uint32) {
+	e.maxSizeLimit = v
+	if e.dynTab.maxSize > v {
+		e.tableSizeUpdate = true
+		e.dynTab.setMaxSize(v)
+	}
+}
+
+// shouldIndex reports whether f should be indexed.
+func (e *Encoder) shouldIndex(f HeaderField) bool {
+	return !f.Sensitive && f.size() <= e.dynTab.maxSize
+}
+
+// appendIndexed appends index i, as encoded in "Indexed Header Field"
+// representation, to dst and returns the extended buffer.
+func appendIndexed(dst []byte, i uint64) []byte {
+	first := len(dst)
+	dst = appendVarInt(dst, 7, i)
+	dst[first] |= 0x80
+	return dst
+}
+
+// appendNewName appends f, as encoded in one of "Literal Header field
+// - New Name" representation variants, to dst and returns the
+// extended buffer.
+//
+// If f.Sensitive is true, "Never Indexed" representation is used. If
+// f.Sensitive is false and indexing is true, "Inremental Indexing"
+// representation is used.
+func appendNewName(dst []byte, f HeaderField, indexing bool) []byte {
+	dst = append(dst, encodeTypeByte(indexing, f.Sensitive))
+	dst = appendHpackString(dst, f.Name)
+	return appendHpackString(dst, f.Value)
+}
+
+// appendIndexedName appends f and index i referring indexed name
+// entry, as encoded in one of "Literal Header field - Indexed Name"
+// representation variants, to dst and returns the extended buffer.
+//
+// If f.Sensitive is true, "Never Indexed" representation is used. If
+// f.Sensitive is false and indexing is true, "Incremental Indexing"
+// representation is used.
+func appendIndexedName(dst []byte, f HeaderField, i uint64, indexing bool) []byte {
+	first := len(dst)
+	var n byte
+	if indexing {
+		n = 6
+	} else {
+		n = 4
+	}
+	dst = appendVarInt(dst, n, i)
+	dst[first] |= encodeTypeByte(indexing, f.Sensitive)
+	return appendHpackString(dst, f.Value)
+}
+
+// appendTableSize appends v, as encoded in "Header Table Size Update"
+// representation, to dst and returns the extended buffer.
+func appendTableSize(dst []byte, v uint32) []byte {
+	first := len(dst)
+	dst = appendVarInt(dst, 5, uint64(v))
+	dst[first] |= 0x20
+	return dst
+}
+
+// appendVarInt appends i, as encoded in variable integer form using n
+// bit prefix, to dst and returns the extended buffer.
+//
+// See
+// http://http2.github.io/http2-spec/compression.html#integer.representation
+func appendVarInt(dst []byte, n byte, i uint64) []byte {
+	k := uint64((1 << n) - 1)
+	if i < k {
+		return append(dst, byte(i))
+	}
+	dst = append(dst, byte(k))
+	i -= k
+	for ; i >= 128; i >>= 7 {
+		dst = append(dst, byte(0x80|(i&0x7f)))
+	}
+	return append(dst, byte(i))
+}
+
+// appendHpackString appends s, as encoded in "String Literal"
+// representation, to dst and returns the the extended buffer.
+//
+// s will be encoded in Huffman codes only when it produces strictly
+// shorter byte string.
+func appendHpackString(dst []byte, s string) []byte {
+	huffmanLength := HuffmanEncodeLength(s)
+	if huffmanLength < uint64(len(s)) {
+		first := len(dst)
+		dst = appendVarInt(dst, 7, huffmanLength)
+		dst = AppendHuffmanString(dst, s)
+		dst[first] |= 0x80
+	} else {
+		dst = appendVarInt(dst, 7, uint64(len(s)))
+		dst = append(dst, s...)
+	}
+	return dst
+}
+
+// encodeTypeByte returns type byte. If sensitive is true, type byte
+// for "Never Indexed" representation is returned. If sensitive is
+// false and indexing is true, type byte for "Incremental Indexing"
+// representation is returned. Otherwise, type byte for "Without
+// Indexing" is returned.
+func encodeTypeByte(indexing, sensitive bool) byte {
+	if sensitive {
+		return 0x10
+	}
+	if indexing {
+		return 0x40
+	}
+	return 0
+}

+ 308 - 15
hpack/encode_test.go

@@ -7,33 +7,326 @@ package hpack
 
 
 import (
 import (
 	"bytes"
 	"bytes"
+	"encoding/hex"
 	"reflect"
 	"reflect"
+	"strings"
 	"testing"
 	"testing"
 )
 )
 
 
-func TestEncoder(t *testing.T) {
-	headers := []HeaderField{
-		HeaderField{Name: "content-type", Value: "text/html"},
-		HeaderField{Name: "x-foo", Value: "x-bar"},
-	}
+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"},
 
 
-	var buf bytes.Buffer
-	e := NewEncoder(&buf)
-	for _, hf := range headers {
-		if err := e.WriteField(hf); err != nil {
+		// 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)
 			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
 	var got []HeaderField
-	_, err := NewDecoder(4<<10, func(f HeaderField) {
+	d := NewDecoder(4<<10, func(f HeaderField) {
 		got = append(got, f)
 		got = append(got, f)
-	}).Write(buf.Bytes())
-	if err != nil {
-		t.Error("Decoder Write = %v", err)
+	})
+
+	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.Error("%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(len(staticTable) + 3), true},
+		{pair("blake", "miz"), uint64(len(staticTable) + 2), true},
+		{pair(":method", "GET"), 2, true},
+
+		// Only name match because Sensitive == true
+		{HeaderField{":method", "GET", true}, 2, false},
+
+		// Only Name matches
+		{pair("foo", "..."), uint64(len(staticTable) + 3), false},
+		{pair("blake", "..."), uint64(len(staticTable) + 2), false},
+		{pair(":method", "..."), 2, false},
+
+		// None match
+		{pair("foo-", "bar"), 0, false},
 	}
 	}
-	if !reflect.DeepEqual(got, headers) {
-		t.Errorf("Decoded %+v; want %+v", got, headers)
+	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)
 }
 }

+ 43 - 0
hpack/hpack.go

@@ -141,6 +141,49 @@ func (dt *dynamicTable) evict() {
 	}
 	}
 }
 }
 
 
+// constantTimeStringCompare compares string a and b in a constant
+// time manner.
+func constantTimeStringCompare(a, b string) bool {
+	if len(a) != len(b) {
+		return false
+	}
+
+	c := byte(0)
+
+	for i := 0; i < len(a); i++ {
+		c |= a[i] ^ b[i]
+	}
+
+	return c == 0
+}
+
+// Search searches f in the table. The return value i is 0 if there is
+// no name match. If there is name match or name/value match, i is the
+// index of that entry (1-based). If both name and value match,
+// nameValueMatch becomes true.
+func (dt *dynamicTable) search(f HeaderField) (i uint64, nameValueMatch bool) {
+	l := len(dt.ents)
+	for j := l - 1; j >= 0; j-- {
+		ent := dt.ents[j]
+		if !constantTimeStringCompare(ent.Name, f.Name) {
+			continue
+		}
+		if i == 0 {
+			i = uint64(l - j)
+		}
+		if f.Sensitive {
+			continue
+		}
+		if !constantTimeStringCompare(ent.Value, f.Value) {
+			continue
+		}
+		i = uint64(l - j)
+		nameValueMatch = true
+		return
+	}
+	return
+}
+
 func (d *Decoder) maxTableIndex() int {
 func (d *Decoder) maxTableIndex() int {
 	return len(d.dynTab.ents) + len(staticTable)
 	return len(d.dynTab.ents) + len(staticTable)
 }
 }

+ 61 - 0
hpack/hpack_test.go

@@ -142,6 +142,41 @@ func TestDynamicTableAt(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestDynamicTableSearch(t *testing.T) {
+	dt := dynamicTable{}
+	dt.setMaxSize(4096)
+
+	dt.add(pair("foo", "bar"))
+	dt.add(pair("blake", "miz"))
+	dt.add(pair(":method", "GET"))
+
+	tests := []struct {
+		hf        HeaderField
+		wantI     uint64
+		wantMatch bool
+	}{
+		// Name and Value match
+		{pair("foo", "bar"), 3, true},
+		{pair(":method", "GET"), 1, true},
+
+		// Only name match because of Sensitive == true
+		{HeaderField{"blake", "miz", true}, 2, false},
+
+		// Only Name matches
+		{pair("foo", "..."), 3, false},
+		{pair("blake", "..."), 2, false},
+		{pair(":method", "..."), 1, false},
+
+		// None match
+		{pair("foo-", "bar"), 0, false},
+	}
+	for _, tt := range tests {
+		if gotI, gotMatch := dt.search(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 TestDynamicTableSizeEvict(t *testing.T) {
 func TestDynamicTableSizeEvict(t *testing.T) {
 	d := NewDecoder(4096, nil)
 	d := NewDecoder(4096, nil)
 	if want := uint32(0); d.dynTab.size != want {
 	if want := uint32(0); d.dynTab.size != want {
@@ -521,6 +556,32 @@ func TestHuffmanDecode(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestAppendHuffmanString(t *testing.T) {
+	tests := []struct {
+		in, want string
+	}{
+		{"www.example.com", "f1e3 c2e5 f23a 6ba0 ab90 f4ff"},
+		{"no-cache", "a8eb 1064 9cbf"},
+		{"custom-key", "25a8 49e9 5ba9 7d7f"},
+		{"custom-value", "25a8 49e9 5bb8 e8b4 bf"},
+		{"302", "6402"},
+		{"private", "aec3 771a 4b"},
+		{"Mon, 21 Oct 2013 20:13:21 GMT", "d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff"},
+		{"https://www.example.com", "9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3"},
+		{"gzip", "9bd9 ab"},
+		{"foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1",
+			"94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 3160 65c0 03ed 4ee5 b106 3d50 07"},
+	}
+	for i, tt := range tests {
+		buf := []byte{}
+		want := strings.Replace(tt.want, " ", "", -1)
+		buf = AppendHuffmanString(buf, tt.in)
+		if got := hex.EncodeToString(buf); want != got {
+			t.Errorf("%d. encode = %q; want %q", i, got, want)
+		}
+	}
+}
+
 func TestReadVarInt(t *testing.T) {
 func TestReadVarInt(t *testing.T) {
 	type res struct {
 	type res struct {
 		i        uint64
 		i        uint64

+ 66 - 0
hpack/huffman.go

@@ -91,3 +91,69 @@ func addDecoderNode(sym byte, code uint32, codeLen uint8) {
 		cur.children[i] = &node{sym: sym, codeLen: codeLen}
 		cur.children[i] = &node{sym: sym, codeLen: codeLen}
 	}
 	}
 }
 }
+
+// AppendHuffmanString appends s, as encoded in Huffman codes, to dst
+// and returns the extended buffer.
+func AppendHuffmanString(dst []byte, s string) []byte {
+	rembits := uint8(8)
+
+	for i := 0; i < len(s); i++ {
+		if rembits == 8 {
+			dst = append(dst, 0)
+		}
+		dst, rembits = appendByteToHuffmanCode(dst, rembits, s[i])
+	}
+
+	if rembits < 8 {
+		// special EOS symbol
+		code := uint32(0x3fffffff)
+		nbits := uint8(30)
+
+		t := uint8(code >> (nbits - rembits))
+		dst[len(dst)-1] |= t
+	}
+
+	return dst
+}
+
+// HuffmanEncodeLength returns the number of bytes required to encode
+// s in Huffman codes. The result is round up to byte boundary.
+func HuffmanEncodeLength(s string) uint64 {
+	n := uint64(0)
+	for i := 0; i < len(s); i++ {
+		n += uint64(huffmanCodeLen[s[i]])
+	}
+	return (n + 7) / 8
+}
+
+// appendByteToHuffmanCode appends Huffman code for c to dst and
+// returns the extended buffer and the remaining bits in the last
+// element. The appending is not byte aligned and the remaining bits
+// in the last element of dst is given in rembits.
+func appendByteToHuffmanCode(dst []byte, rembits uint8, c byte) ([]byte, uint8) {
+	code := huffmanCodes[c]
+	nbits := huffmanCodeLen[c]
+
+	for {
+		if rembits > nbits {
+			t := uint8(code << (rembits - nbits))
+			dst[len(dst)-1] |= t
+			rembits -= nbits
+			break
+		}
+
+		t := uint8(code >> (nbits - rembits))
+		dst[len(dst)-1] |= t
+
+		nbits -= rembits
+		rembits = 8
+
+		if nbits == 0 {
+			break
+		}
+
+		dst = append(dst, 0)
+	}
+
+	return dst, rembits
+}

+ 1 - 0
server.go

@@ -954,6 +954,7 @@ func (sc *serverConn) processSetting(s Setting) error {
 	switch s.ID {
 	switch s.ID {
 	case SettingHeaderTableSize:
 	case SettingHeaderTableSize:
 		sc.headerTableSize = s.Val
 		sc.headerTableSize = s.Val
+		sc.hpackEncoder.SetMaxDynamicTableSize(s.Val)
 		return nil
 		return nil
 	case SettingEnablePush:
 	case SettingEnablePush:
 		sc.pushEnabled = s.Val != 0
 		sc.pushEnabled = s.Val != 0