1package self
2
3import (
4	"bytes"
5	"fmt"
6	"math/rand"
7
8	"github.com/marten-seemann/qpack"
9
10	. "github.com/onsi/ginkgo"
11	. "github.com/onsi/gomega"
12)
13
14var _ = Describe("Self Tests", func() {
15	getEncoder := func() (*qpack.Encoder, *bytes.Buffer) {
16		output := &bytes.Buffer{}
17		return qpack.NewEncoder(output), output
18	}
19
20	randomString := func(l int) string {
21		const charset = "abcdefghijklmnopqrstuvwxyz" +
22			"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
23		s := make([]byte, l)
24		for i := range s {
25			s[i] = charset[rand.Intn(len(charset))]
26		}
27		return string(s)
28	}
29
30	It("encodes and decodes a single header field", func() {
31		hf := qpack.HeaderField{
32			Name:  randomString(15),
33			Value: randomString(15),
34		}
35		encoder, output := getEncoder()
36		Expect(encoder.WriteField(hf)).To(Succeed())
37		headerFields, err := qpack.NewDecoder(nil).DecodeFull(output.Bytes())
38		Expect(err).ToNot(HaveOccurred())
39		Expect(headerFields).To(Equal([]qpack.HeaderField{hf}))
40	})
41
42	It("encodes and decodes multiple header fields", func() {
43		hfs := []qpack.HeaderField{
44			{Name: "foo", Value: "bar"},
45			{Name: "lorem", Value: "ipsum"},
46			{Name: randomString(15), Value: randomString(20)},
47		}
48		encoder, output := getEncoder()
49		for _, hf := range hfs {
50			Expect(encoder.WriteField(hf)).To(Succeed())
51		}
52		headerFields, err := qpack.NewDecoder(nil).DecodeFull(output.Bytes())
53		Expect(err).ToNot(HaveOccurred())
54		Expect(headerFields).To(Equal(hfs))
55	})
56
57	It("encodes and decodes multiple requests", func() {
58		hfs1 := []qpack.HeaderField{{Name: "foo", Value: "bar"}}
59		hfs2 := []qpack.HeaderField{
60			{Name: "lorem", Value: "ipsum"},
61			{Name: randomString(15), Value: randomString(20)},
62		}
63		encoder, output := getEncoder()
64		for _, hf := range hfs1 {
65			Expect(encoder.WriteField(hf)).To(Succeed())
66		}
67		req1 := append([]byte{}, output.Bytes()...)
68		output.Reset()
69		for _, hf := range hfs2 {
70			Expect(encoder.WriteField(hf)).To(Succeed())
71		}
72		req2 := append([]byte{}, output.Bytes()...)
73
74		var headerFields []qpack.HeaderField
75		decoder := qpack.NewDecoder(func(hf qpack.HeaderField) { headerFields = append(headerFields, hf) })
76		_, err := decoder.Write(req1)
77		Expect(err).ToNot(HaveOccurred())
78		Expect(headerFields).To(Equal(hfs1))
79		headerFields = nil
80		_, err = decoder.Write(req2)
81		Expect(err).ToNot(HaveOccurred())
82		Expect(headerFields).To(Equal(hfs2))
83	})
84
85	// replace one character by a random character at a random position
86	replaceRandomCharacter := func(s string) string {
87		pos := rand.Intn(len(s))
88		new := s[:pos]
89		for {
90			if c := randomString(1); c != string(s[pos]) {
91				new += c
92				break
93			}
94		}
95		new += s[pos+1:]
96		return new
97	}
98
99	check := func(encoded []byte, hf qpack.HeaderField) {
100		headerFields, err := qpack.NewDecoder(nil).DecodeFull(encoded)
101		ExpectWithOffset(1, err).ToNot(HaveOccurred())
102		ExpectWithOffset(1, headerFields).To(HaveLen(1))
103		ExpectWithOffset(1, headerFields[0]).To(Equal(hf))
104	}
105
106	// use an entry with a value, for example "set-cookie"
107	It("uses the static table for field names, for fields without values", func() {
108		var hf qpack.HeaderField
109		for {
110			if entry := staticTable[rand.Intn(len(staticTable))]; len(entry.Value) == 0 {
111				hf = qpack.HeaderField{Name: entry.Name}
112				break
113			}
114		}
115		encoder, output := getEncoder()
116		Expect(encoder.WriteField(hf)).To(Succeed())
117		encodedLen := output.Len()
118		check(output.Bytes(), hf)
119		encoder, output = getEncoder()
120		oldName := hf.Name
121		hf.Name = replaceRandomCharacter(hf.Name)
122		Expect(encoder.WriteField(hf)).To(Succeed())
123		fmt.Fprintf(GinkgoWriter, "Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes\n", oldName, encodedLen, hf.Name, output.Len())
124		Expect(output.Len()).To(BeNumerically(">", encodedLen))
125	})
126
127	// use an entry with a value, for example "set-cookie",
128	// but now use a custom value
129	It("uses the static table for field names, for fields without values", func() {
130		var hf qpack.HeaderField
131		for {
132			if entry := staticTable[rand.Intn(len(staticTable))]; len(entry.Value) == 0 {
133				hf = qpack.HeaderField{
134					Name:  entry.Name,
135					Value: randomString(5),
136				}
137				break
138			}
139		}
140		encoder, output := getEncoder()
141		Expect(encoder.WriteField(hf)).To(Succeed())
142		encodedLen := output.Len()
143		check(output.Bytes(), hf)
144		encoder, output = getEncoder()
145		oldName := hf.Name
146		hf.Name = replaceRandomCharacter(hf.Name)
147		Expect(encoder.WriteField(hf)).To(Succeed())
148		fmt.Fprintf(GinkgoWriter, "Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes\n", oldName, encodedLen, hf.Name, output.Len())
149		Expect(output.Len()).To(BeNumerically(">", encodedLen))
150	})
151
152	// use an entry with a value, for example
153	//   cache-control -> Value: "max-age=0"
154	// but encode a different value
155	//   cache-control -> xyz
156	It("uses the static table for field names, for fields with values", func() {
157		var hf qpack.HeaderField
158		for {
159			// Only use values with at least 2 characters.
160			// This makes sure that Huffman enocding doesn't compress them as much as encoding it using the static table would.
161			if entry := staticTable[rand.Intn(len(staticTable))]; len(entry.Value) > 1 {
162				hf = qpack.HeaderField{
163					Name:  entry.Name,
164					Value: randomString(20),
165				}
166				break
167			}
168		}
169		encoder, output := getEncoder()
170		Expect(encoder.WriteField(hf)).To(Succeed())
171		encodedLen := output.Len()
172		check(output.Bytes(), hf)
173		encoder, output = getEncoder()
174		oldName := hf.Name
175		hf.Name = replaceRandomCharacter(hf.Name)
176		Expect(encoder.WriteField(hf)).To(Succeed())
177		fmt.Fprintf(GinkgoWriter, "Encoding field name:\n\t%s: %d bytes\n\t%s: %d bytes\n", oldName, encodedLen, hf.Name, output.Len())
178		Expect(output.Len()).To(BeNumerically(">", encodedLen))
179	})
180
181	It("uses the static table for field values", func() {
182		var hf qpack.HeaderField
183		for {
184			// Only use values with at least 2 characters.
185			// This makes sure that Huffman enocding doesn't compress them as much as encoding it using the static table would.
186			if entry := staticTable[rand.Intn(len(staticTable))]; len(entry.Value) > 1 {
187				hf = qpack.HeaderField{
188					Name:  entry.Name,
189					Value: entry.Value,
190				}
191				break
192			}
193		}
194		encoder, output := getEncoder()
195		Expect(encoder.WriteField(hf)).To(Succeed())
196		encodedLen := output.Len()
197		check(output.Bytes(), hf)
198		encoder, output = getEncoder()
199		oldValue := hf.Value
200		hf.Value = replaceRandomCharacter(hf.Value)
201		Expect(encoder.WriteField(hf)).To(Succeed())
202		fmt.Fprintf(GinkgoWriter,
203			"Encoding field value:\n\t%s: %s -> %d bytes\n\t%s: %s -> %d bytes\n",
204			hf.Name, oldValue, encodedLen,
205			hf.Name, hf.Value, output.Len(),
206		)
207		Expect(output.Len()).To(BeNumerically(">", encodedLen))
208	})
209})
210