Skip to content

Commit

Permalink
Merge pull request #10 from jstordeur/master
Browse files Browse the repository at this point in the history
Encoder uses the static table.
  • Loading branch information
marten-seemann authored Aug 8, 2020
2 parents 3b7462f + 59d364f commit 8ef5426
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 2 deletions.
44 changes: 43 additions & 1 deletion encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,27 @@ func (e *Encoder) WriteField(f HeaderField) error {
e.wrotePrefix = true
}

e.writeLiteralFieldWithoutNameReference(f)
idxAndVals, nameFound := encoderMap[f.Name]
if nameFound {
if idxAndVals.values == nil {
if len(f.Value) == 0 {
e.writeIndexedField(idxAndVals.idx)
} else {
e.writeLiteralFieldWithNameReference(&f, idxAndVals.idx)
}
} else {
valIdx, valueFound := idxAndVals.values[f.Value]
if valueFound {
e.writeIndexedField(valIdx)
} else {
e.writeLiteralFieldWithNameReference(&f, idxAndVals.idx)
}
}

} else {
e.writeLiteralFieldWithoutNameReference(f)
}

e.w.Write(e.buf)
e.buf = e.buf[:0]
return nil
Expand All @@ -50,3 +70,25 @@ func (e *Encoder) writeLiteralFieldWithoutNameReference(f HeaderField) {
e.buf = appendVarInt(e.buf, 7, uint64(len(f.Value)))
e.buf = append(e.buf, []byte(f.Value)...)
}

// Encodes a header field whose name is present in one of the
// tables.
func (e *Encoder) writeLiteralFieldWithNameReference(
f *HeaderField, idx uint8) {
offset := len(e.buf)
e.buf = appendVarInt(e.buf, 4, uint64(idx))
// Set the 01NTxxxx pattern, forcing N to 0 and T to 1
e.buf[offset] ^= 0x50

e.buf = appendVarInt(e.buf, 7, uint64(len(f.Value)))
e.buf = append(e.buf, []byte(f.Value)...)
}

// Encodes an indexed field, meaning it's entirely defined in one of the
// tables.
func (e *Encoder) writeIndexedField(idx uint8) {
offset := len(e.buf)
e.buf = appendVarInt(e.buf, 6, uint64(idx))
// Set the 1Txxxxxx pattern, forcing T to 1
e.buf[offset] ^= 0xc0
}
58 changes: 58 additions & 0 deletions encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,31 @@ var _ = Describe("Encoder", func() {
return data[len(hf.Value):]
}

// Reads one indexed field line representation from data and verifies it
// matches expected_hf.
// Returns the leftover bytes from data.
checkIndexedHeaderField := func(data []byte, expected_hf HeaderField) []byte {
Expect(data[0] >> 7).To(Equal(uint8(1))) // 1Txxxxxx
index, data, err := readVarInt(6, data)
Expect(err).ToNot(HaveOccurred())
Expect(staticTableEntries[index]).To(Equal(expected_hf))
return data
}

checkHeaderFieldWithNameRef := func(data []byte, expected_hf HeaderField) []byte {
// read name reference
Expect(data[0] >> 6).To(Equal(uint8(1))) // 01NTxxxx
index, data, err := readVarInt(4, data)
Expect(err).ToNot(HaveOccurred())
Expect(staticTableEntries[index].Name).To(Equal(expected_hf.Name))
// read literal value
valueLen, data, err := readVarInt(7, data)
Expect(err).ToNot(HaveOccurred())
Expect(valueLen).To(BeEquivalentTo(len(expected_hf.Value)))
Expect(string(data[:len(expected_hf.Value)])).To(Equal(expected_hf.Value))
return data[len(expected_hf.Value):]
}

It("encodes a single field", func() {
hf := HeaderField{Name: "foobar", Value: "lorem ipsum"}
Expect(encoder.WriteField(hf)).To(Succeed())
Expand Down Expand Up @@ -68,6 +93,39 @@ var _ = Describe("Encoder", func() {
Expect(data).To(BeEmpty())
})

It("encodes all the fields of the static table", func() {
for _, hf := range staticTableEntries {
Expect(encoder.WriteField(hf)).To(Succeed())
}

data, requiredInsertCount, deltaBase := readPrefix(output.Bytes())
Expect(requiredInsertCount).To(BeZero())
Expect(deltaBase).To(BeZero())

for _, hf := range staticTableEntries {
data = checkIndexedHeaderField(data, hf)
}
Expect(data).To(BeEmpty())
})

It("encodes fields with name reference in the static table", func() {
hf1 := HeaderField{Name: ":status", Value: "666"}
hf2 := HeaderField{Name: "server", Value: "lorem ipsum"}
hf3 := HeaderField{Name: ":method", Value: ""}
Expect(encoder.WriteField(hf1)).To(Succeed())
Expect(encoder.WriteField(hf2)).To(Succeed())
Expect(encoder.WriteField(hf3)).To(Succeed())

data, requiredInsertCount, deltaBase := readPrefix(output.Bytes())
Expect(requiredInsertCount).To(BeZero())
Expect(deltaBase).To(BeZero())

data = checkHeaderFieldWithNameRef(data, hf1)
data = checkHeaderFieldWithNameRef(data, hf2)
data = checkHeaderFieldWithNameRef(data, hf3)
Expect(data).To(BeEmpty())
})

It("encodes multiple requests", func() {
hf1 := HeaderField{Name: "foobar", Value: "lorem ipsum"}
Expect(encoder.WriteField(hf1)).To(Succeed())
Expand Down
2 changes: 1 addition & 1 deletion integrationtests/interop/qifs
Submodule qifs updated 136 files
130 changes: 130 additions & 0 deletions static_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,133 @@ var staticTableEntries = [...]HeaderField{
{Name: "x-frame-options", Value: "deny"},
{Name: "x-frame-options", Value: "sameorigin"},
}

type indexAndValues struct {
idx uint8
values map[string]uint8
}

// A map of the header names from the static table to their index in the table.
// This is used by the encoder to quickly find if a header is in the static table
// and what value should be used to encode it.
// There's a second level of mapping for the headers that have some predefined
// values in the static table.
var encoderMap = map[string]indexAndValues{
":authority": {0, nil},
":path": {1, map[string]uint8{"/": 1}},
"age": {2, map[string]uint8{"0": 2}},
"content-disposition": {3, nil},
"content-length": {4, map[string]uint8{"0": 4}},
"cookie": {5, nil},
"date": {6, nil},
"etag": {7, nil},
"if-modified-since": {8, nil},
"if-none-match": {9, nil},
"last-modified": {10, nil},
"link": {11, nil},
"location": {12, nil},
"referer": {13, nil},
"set-cookie": {14, nil},
":method": {15, map[string]uint8{
"CONNECT": 15,
"DELETE": 16,
"GET": 17,
"HEAD": 18,
"OPTIONS": 19,
"POST": 20,
"PUT": 21}},
":scheme": {22, map[string]uint8{
"http": 22,
"https": 23}},
":status": {24, map[string]uint8{
"103": 24,
"200": 25,
"304": 26,
"404": 27,
"503": 28,
"100": 63,
"204": 64,
"206": 65,
"302": 66,
"400": 67,
"403": 68,
"421": 69,
"425": 70,
"500": 71}},
"accept": {29, map[string]uint8{
"*/*": 29,
"application/dns-message": 30}},
"accept-encoding": {31, map[string]uint8{"gzip, deflate, br": 31}},
"accept-ranges": {32, map[string]uint8{"bytes": 32}},
"access-control-allow-headers": {33, map[string]uint8{
"cache-control": 33,
"content-type": 34,
"*": 75}},
"access-control-allow-origin": {35, map[string]uint8{"*": 35}},
"cache-control": {36, map[string]uint8{
"max-age=0": 36,
"max-age=2592000": 37,
"max-age=604800": 38,
"no-cache": 39,
"no-store": 40,
"public, max-age=31536000": 41}},
"content-encoding": {42, map[string]uint8{
"br": 42,
"gzip": 43}},
"content-type": {44, map[string]uint8{
"application/dns-message": 44,
"application/javascript": 45,
"application/json": 46,
"application/x-www-form-urlencoded": 47,
"image/gif": 48,
"image/jpeg": 49,
"image/png": 50,
"text/css": 51,
"text/html; charset=utf-8": 52,
"text/plain": 53,
"text/plain;charset=utf-8": 54}},
"range": {55, map[string]uint8{"bytes=0-": 55}},
"strict-transport-security": {56, map[string]uint8{
"max-age=31536000": 56,
"max-age=31536000; includesubdomains": 57,
"max-age=31536000; includesubdomains; preload": 58}},
"vary": {59, map[string]uint8{
"accept-encoding": 59,
"origin": 60}},
"x-content-type-options": {61, map[string]uint8{"nosniff": 61}},
"x-xss-protection": {62, map[string]uint8{"1; mode=block": 62}},
// ":status" is duplicated and takes index 63 to 71
"accept-language": {72, nil},
"access-control-allow-credentials": {73, map[string]uint8{
"FALSE": 73,
"TRUE": 74}},
// "access-control-allow-headers" is duplicated and takes index 75
"access-control-allow-methods": {76, map[string]uint8{
"get": 76,
"get, post, options": 77,
"options": 78}},
"access-control-expose-headers": {79, map[string]uint8{
"content-length": 79,
"content-type": 80}},
"access-control-request-method": {81, map[string]uint8{
"get": 81,
"post": 82}},
"alt-svc": {83, map[string]uint8{"clear": 83}},
"authorization": {84, nil},
"content-security-policy": {85, map[string]uint8{
"script-src 'none'; object-src 'none'; base-uri 'none'": 85}},
"early-data": {86, map[string]uint8{"1": 86}},
"expect-ct": {87, nil},
"forwarded": {88, nil},
"if-range": {89, nil},
"origin": {90, nil},
"purpose": {91, map[string]uint8{"prefetch": 91}},
"server": {92, nil},
"timing-allow-origin": {93, map[string]uint8{"*": 93}},
"upgrade-insecure-requests": {94, map[string]uint8{"1": 94}},
"user-agent": {95, nil},
"x-forwarded-for": {96, nil},
"x-frame-options": {97, map[string]uint8{
"deny": 97,
"sameorigin": 98}},
}
20 changes: 20 additions & 0 deletions static_table_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package qpack

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("StaticTable", func() {

It("verifies that encoderMap and staticTableEntries are coherent", func() {
for idx, hf := range staticTableEntries {
if len(hf.Value) == 0 {
Expect(encoderMap[hf.Name].idx).To(Equal(uint8(idx)))
} else {
Expect(encoderMap[hf.Name].values[hf.Value]).To(Equal(uint8(idx)))
}
}
})

})

0 comments on commit 8ef5426

Please sign in to comment.