v / vlib / net / http / h2_hpack_test.v
335 lines · 291 sloc · 11.23 KB · 95861b8bdeeddc71d79c3f09f56a66bf01106ecd
Raw
1module http
2
3// Tests for the HPACK implementation (RFC 7541). The byte sequences below are
4// the worked examples from RFC 7541 Appendix C; decoding them is the canonical
5// correctness check for an HPACK decoder.
6
7// hexb parses a hex string (spaces allowed) into bytes.
8fn hexb(s string) []u8 {
9 clean := s.replace(' ', '').replace('\n', '')
10 mut out := []u8{cap: clean.len / 2}
11 for i := 0; i + 1 < clean.len; i += 2 {
12 hi := hex_nibble(clean[i])
13 lo := hex_nibble(clean[i + 1])
14 out << u8((hi << 4) | lo)
15 }
16 return out
17}
18
19fn hex_nibble(c u8) u8 {
20 return match c {
21 `0`...`9` { c - `0` }
22 `a`...`f` { c - `a` + 10 }
23 `A`...`F` { c - `A` + 10 }
24 else { 0 }
25 }
26}
27
28fn assert_fields(got []H2HeaderField, want [][]string) {
29 assert got.len == want.len, 'field count: got ${got.len}, want ${want.len}'
30 for i, w in want {
31 assert got[i].name == w[0], 'field ${i} name: got "${got[i].name}", want "${w[0]}"'
32 assert got[i].value == w[1], 'field ${i} value: got "${got[i].value}", want "${w[1]}"'
33 }
34}
35
36// --- Integer representation (RFC 7541 Section 5.1) ---
37
38fn test_hpack_integer_examples() {
39 // C.1.1: encode 10 with a 5-bit prefix -> single byte 0x0a.
40 mut b := []u8{}
41 h2_hpack_write_int(mut b, 10, 5, 0)
42 assert b == [u8(0x0a)]
43 mut r := H2HpackReader{
44 buf: b
45 }
46 assert r.read_int(5)! == 10
47
48 // C.1.2: encode 1337 with a 5-bit prefix -> 0x1f 0x9a 0x0a.
49 b = []u8{}
50 h2_hpack_write_int(mut b, 1337, 5, 0)
51 assert b == [u8(0x1f), 0x9a, 0x0a]
52 r = H2HpackReader{
53 buf: b
54 }
55 assert r.read_int(5)! == 1337
56
57 // C.1.3: encode 42 with an 8-bit prefix -> single byte 0x2a.
58 b = []u8{}
59 h2_hpack_write_int(mut b, 42, 8, 0)
60 assert b == [u8(0x2a)]
61 r = H2HpackReader{
62 buf: b
63 }
64 assert r.read_int(8)! == 42
65}
66
67fn test_hpack_integer_overflow_rejected() {
68 // A run of continuation bytes with the high bit always set must not loop
69 // forever or overflow; it should error.
70 bad := [u8(0x1f), 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80]
71 mut r := H2HpackReader{
72 buf: bad
73 }
74 r.read_int(5) or { return }
75 assert false, 'expected integer overflow error'
76}
77
78// --- Huffman coding (RFC 7541 Section 5.2) ---
79
80fn test_huffman_roundtrip() {
81 samples := ['', 'www.example.com', 'no-cache', 'custom-value', '/sample/path',
82 'Mon, 21 Oct 2013 20:13:21 GMT', 'https://www.example.com', 'private']
83 for s in samples {
84 enc := h2_huffman_encode(s.bytes())
85 dec := h2_huffman_decode(enc)!
86 assert dec.bytestr() == s, 'huffman roundtrip failed for "${s}"'
87 }
88}
89
90fn test_huffman_known_vector() {
91 // "www.example.com" Huffman-encoded, from RFC 7541 C.4.1.
92 enc := hexb('f1e3c2e5f23a6ba0ab90f4ff')
93 dec := h2_huffman_decode(enc)!
94 assert dec.bytestr() == 'www.example.com'
95}
96
97fn test_huffman_codes_rebuilt_from_lengths() {
98 // The HPACK table now ships only the bit lengths; the canonical codes are
99 // rebuilt at startup via hash.huffman. Pin a few known codes from RFC 7541
100 // Appendix B so a bad rebuild (or a future builder change) is caught.
101 assert h2_huffman_table.codes.len == 257
102 assert h2_huffman_table.lengths.len == 257
103 // symbol 0 (NUL): 0x1ff8 / 13 bits
104 assert h2_huffman_table.codes[0] == 0x1ff8
105 assert h2_huffman_table.lengths[0] == 13
106 // '0' (0x30): 0x0 / 5 bits, '1': 0x1 / 5 bits, 'a' (0x61): 0x3 / 5 bits
107 assert h2_huffman_table.codes[0x30] == 0x0
108 assert h2_huffman_table.lengths[0x30] == 5
109 assert h2_huffman_table.codes[0x31] == 0x1
110 assert h2_huffman_table.codes[0x61] == 0x3
111 // EOS (256): 0x3fffffff / 30 bits
112 assert h2_huffman_table.codes[256] == 0x3fffffff
113 assert h2_huffman_table.lengths[256] == 30
114}
115
116fn test_huffman_rejects_padding_not_all_ones() {
117 // Valid encoding of "0" is 5 bits (00000); pad the rest of the byte with
118 // zeros instead of ones -> invalid per RFC 7541 Section 5.2.
119 bad := [u8(0x00)] // '0' code is 00000, then 000 padding (not all ones)
120 h2_huffman_decode(bad) or { return }
121 assert false, 'expected invalid huffman padding error'
122}
123
124// --- C.2: Header field representations ---
125
126fn test_hpack_c_2_1_literal_incremental() {
127 mut d := H2HpackDecoder{}
128 fields := d.decode(hexb('400a 6375 7374 6f6d 2d6b 6579 0d63 7573 746f 6d2d 6865 6164 6572'))!
129 assert_fields(fields, [['custom-key', 'custom-header']])
130 // Added to the dynamic table: size = 10 + 13 + 32 = 55.
131 assert d.dyn_table.entries.len == 1
132 assert d.dyn_table.cur_size == 55
133 assert d.dyn_table.entries[0].name == 'custom-key'
134}
135
136fn test_hpack_c_2_2_literal_without_indexing() {
137 mut d := H2HpackDecoder{}
138 fields := d.decode(hexb('040c 2f73 616d 706c 652f 7061 7468'))!
139 assert_fields(fields, [[':path', '/sample/path']])
140 assert d.dyn_table.entries.len == 0
141}
142
143fn test_hpack_c_2_3_never_indexed() {
144 mut d := H2HpackDecoder{}
145 fields := d.decode(hexb('1008 7061 7373 776f 7264 0673 6563 7265 74'))!
146 assert_fields(fields, [['password', 'secret']])
147 assert d.dyn_table.entries.len == 0
148}
149
150fn test_hpack_c_2_4_indexed() {
151 mut d := H2HpackDecoder{}
152 fields := d.decode(hexb('82'))!
153 assert_fields(fields, [[':method', 'GET']])
154}
155
156// --- C.3: Request sequence without Huffman, shared decoder ---
157
158fn test_hpack_c_3_request_sequence() {
159 mut d := H2HpackDecoder{}
160
161 f1 := d.decode(hexb('8286 8441 0f77 7777 2e65 7861 6d70 6c65 2e63 6f6d'))!
162 assert_fields(f1, [[':method', 'GET'], [':scheme', 'http'],
163 [':path', '/'], [':authority', 'www.example.com']])
164 assert d.dyn_table.cur_size == 57
165
166 f2 := d.decode(hexb('8286 84be 5808 6e6f 2d63 6163 6865'))!
167 assert_fields(f2, [[':method', 'GET'], [':scheme', 'http'],
168 [':path', '/'], [':authority', 'www.example.com'], ['cache-control', 'no-cache']])
169
170 f3 :=
171 d.decode(hexb('8287 85bf 400a 6375 7374 6f6d 2d6b 6579 0c63 7573 746f 6d2d 7661 6c75 65'))!
172 assert_fields(f3, [[':method', 'GET'], [':scheme', 'https'],
173 [':path', '/index.html'], [':authority', 'www.example.com'],
174 ['custom-key', 'custom-value']])
175}
176
177// --- C.4: Request sequence with Huffman, shared decoder ---
178
179fn test_hpack_c_4_request_sequence_huffman() {
180 mut d := H2HpackDecoder{}
181
182 f1 := d.decode(hexb('8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff'))!
183 assert_fields(f1, [[':method', 'GET'], [':scheme', 'http'],
184 [':path', '/'], [':authority', 'www.example.com']])
185
186 f2 := d.decode(hexb('8286 84be 5886 a8eb 1064 9cbf'))!
187 assert_fields(f2, [[':method', 'GET'], [':scheme', 'http'],
188 [':path', '/'], [':authority', 'www.example.com'], ['cache-control', 'no-cache']])
189
190 f3 := d.decode(hexb('8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925 a849 e95b b8e8 b4bf'))!
191 assert_fields(f3, [[':method', 'GET'], [':scheme', 'https'],
192 [':path', '/index.html'], [':authority', 'www.example.com'],
193 ['custom-key', 'custom-value']])
194}
195
196// --- Dynamic table eviction (RFC 7541 Sections 4.3, 4.4) ---
197
198fn test_dyn_table_eviction_on_add() {
199 // Mirrors python-hpack's eviction test: a 66-byte table holds only one of
200 // these two entries at a time.
201 mut t := H2DynTable{
202 max_size: 66
203 }
204 t.add('a', 'b') // size = 1 + 1 + 32 = 34
205 assert t.entries.len == 1
206 assert t.cur_size == 34
207 t.add('long-custom-header', 'longish value') // size = 18 + 13 + 32 = 63
208 assert t.entries.len == 1
209 assert t.entries[0].name == 'long-custom-header'
210 assert t.cur_size == 63
211}
212
213fn test_dyn_table_oversized_entry_empties_table() {
214 mut t := H2DynTable{
215 max_size: 64
216 }
217 t.add('a', 'b')
218 assert t.entries.len == 1
219 // An entry larger than the whole table empties it and is not added.
220 t.add('x'.repeat(100), '')
221 assert t.entries.len == 0
222 assert t.cur_size == 0
223}
224
225fn test_dyn_table_resize_evicts() {
226 mut t := H2DynTable{}
227 t.add('a', 'b')
228 t.add('c', 'd')
229 assert t.entries.len == 2
230 t.set_max_size(34) // room for exactly one 34-byte entry (the newest)
231 assert t.entries.len == 1
232 assert t.entries[0].name == 'c'
233 t.set_max_size(0)
234 assert t.entries.len == 0
235 assert t.cur_size == 0
236}
237
238// A "size update then re-add" sequence exercised through the decoder: an
239// indexed reference to an evicted entry must fail.
240fn test_decoder_dynamic_indexing_and_eviction() {
241 mut d := H2HpackDecoder{}
242 // Literal incremental indexing of custom-key: custom-header (size 55).
243 _ := d.decode(hexb('400a 6375 7374 6f6d 2d6b 6579 0d63 7573 746f 6d2d 6865 6164 6572'))!
244 assert d.dyn_table.entries.len == 1
245 // Index 62 now refers to that entry.
246 f := d.decode([u8(0xbe)])!
247 assert_fields(f, [['custom-key', 'custom-header']])
248 // Shrinking the table to 0 evicts it; index 62 is then out of range.
249 d.decode([u8(0x20)])! // dynamic table size update to 0
250 assert d.dyn_table.entries.len == 0
251 d.decode([u8(0xbe)]) or { return }
252 assert false, 'expected out-of-range error after eviction'
253}
254
255// --- Encoder + round-trip ---
256
257fn test_hpack_encode_indexed_static() {
258 mut e := H2HpackEncoder{}
259 // :method GET is static index 2 -> single byte 0x82.
260 out := e.encode([H2HeaderField{':method', 'GET'}])
261 assert out == [u8(0x82)]
262}
263
264fn test_hpack_roundtrip() {
265 fields := [
266 H2HeaderField{':method', 'GET'},
267 H2HeaderField{':scheme', 'https'},
268 H2HeaderField{':authority', 'example.com'},
269 H2HeaderField{':path', '/index.html'},
270 H2HeaderField{'user-agent', 'v.http/0.1'},
271 H2HeaderField{'accept', '*/*'},
272 H2HeaderField{'cookie', 'session=abc123'},
273 ]
274 mut e := H2HpackEncoder{}
275 mut d := H2HpackDecoder{}
276 encoded := e.encode(fields)
277 decoded := d.decode(encoded)!
278 assert_fields(decoded, [[':method', 'GET'], [':scheme', 'https'],
279 [':authority', 'example.com'], [':path', '/index.html'],
280 ['user-agent', 'v.http/0.1'], ['accept', '*/*'], ['cookie', 'session=abc123']])
281}
282
283// --- Decoder error handling ---
284
285fn test_hpack_rejects_zero_index() {
286 mut d := H2HpackDecoder{}
287 d.decode([u8(0x80)]) or { return } // indexed header field, index 0
288 assert false, 'expected error for index 0'
289}
290
291fn test_hpack_rejects_out_of_range_index() {
292 mut d := H2HpackDecoder{}
293 d.decode([u8(0xff), 0x00]) or { return } // index 62, dynamic table empty
294 assert false, 'expected error for out-of-range index'
295}
296
297fn test_hpack_rejects_size_update_after_field() {
298 mut d := H2HpackDecoder{}
299 // Indexed field (0x82) followed by a dynamic table size update (0x20).
300 d.decode([u8(0x82), 0x20]) or { return }
301 assert false, 'expected error for size update after field'
302}
303
304fn test_hpack_rejects_size_update_over_limit() {
305 mut d := H2HpackDecoder{}
306 d.set_max_dynamic_size(4096)
307 // 0x3f e0 0f = dynamic table size update to 4096+... well over 4096.
308 // Dynamic table size update to 8192 (> 4096 limit).
309 d.decode([u8(0x3f), 0xe1, 0x3f]) or { return }
310 assert false, 'expected error for size update over limit'
311}
312
313fn test_hpack_rejects_truncating_index() {
314 mut d := H2HpackDecoder{}
315 // Insert one dynamic entry, so dynamic index 1 (HPACK index 62) is valid.
316 _ := d.decode(hexb('400a 6375 7374 6f6d 2d6b 6579 0d63 7573 746f 6d2d 6865 6164 6572'))!
317 // Indexed representation with idx = 2^32 + 62: it truncates to 62 (a valid
318 // dynamic index) when narrowed to a 32-bit int, but must be rejected.
319 mut block := []u8{}
320 h2_hpack_write_int(mut block, u64(0x1_0000_0000) + 62, 7, 0x80)
321 d.decode(block) or { return }
322 assert false, 'expected out-of-range error for truncating index'
323}
324
325fn test_hpack_rejects_truncating_string_length() {
326 mut d := H2HpackDecoder{}
327 mut block := []u8{}
328 block << 0x00 // literal without indexing, name index 0
329 block << 0x00 // empty name (H=0, length 0)
330 // Value string length = 2^32 + 5, which truncates to 5 in a 32-bit int.
331 h2_hpack_write_int(mut block, u64(0x1_0000_0000) + 5, 7, 0x00)
332 // No value bytes follow; the oversized length must be rejected cleanly.
333 d.decode(block) or { return }
334 assert false, 'expected length-exceeds-buffer error for truncating string length'
335}
336