1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Copyright (C) 2014 Governikus GmbH & Co. KG.
5** Contact: https://www.qt.io/licensing/
6**
7** This file is part of the test suite of the Qt Toolkit.
8**
9** $QT_BEGIN_LICENSE:GPL-EXCEPT$
10** Commercial License Usage
11** Licensees holding valid commercial Qt licenses may use this file in
12** accordance with the commercial license agreement provided with the
13** Software or, alternatively, in accordance with the terms contained in
14** a written agreement between you and The Qt Company. For licensing terms
15** and conditions see https://www.qt.io/terms-conditions. For further
16** information use the contact form at https://www.qt.io/contact-us.
17**
18** GNU General Public License Usage
19** Alternatively, this file may be used under the terms of the GNU
20** General Public License version 3 as published by the Free Software
21** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
22** included in the packaging of this file. Please review the following
23** information to ensure the GNU General Public License requirements will
24** be met: https://www.gnu.org/licenses/gpl-3.0.html.
25**
26** $QT_END_LICENSE$
27**
28****************************************************************************/
29
30#include <QtTest/QtTest>
31
32#include <QtNetwork/private/bitstreams_p.h>
33#include <QtNetwork/private/hpack_p.h>
34
35#include <QtCore/qbytearray.h>
36
37#include <cstdlib>
38#include <vector>
39#include <string>
40
41QT_USE_NAMESPACE
42
43using namespace HPack;
44
45class tst_Hpack: public QObject
46{
47 Q_OBJECT
48
49public:
50 tst_Hpack();
51private Q_SLOTS:
52 void bitstreamConstruction();
53 void bitstreamWrite();
54 void bitstreamReadWrite();
55 void bitstreamCompression();
56 void bitstreamErrors();
57
58 void lookupTableConstructor();
59
60 void lookupTableStatic();
61 void lookupTableDynamic();
62
63 void hpackEncodeRequest_data();
64 void hpackEncodeRequest();
65 void hpackDecodeRequest_data();
66 void hpackDecodeRequest();
67
68 void hpackEncodeResponse_data();
69 void hpackEncodeResponse();
70 void hpackDecodeResponse_data();
71 void hpackDecodeResponse();
72
73 // TODO: more-more-more tests needed!
74
75private:
76 void hpackEncodeRequest(bool withHuffman);
77 void hpackEncodeResponse(bool withHuffman);
78
79 HttpHeader header1;
80 std::vector<uchar> buffer1;
81 BitOStream request1;
82
83 HttpHeader header2;
84 std::vector<uchar> buffer2;
85 BitOStream request2;
86
87 HttpHeader header3;
88 std::vector<uchar> buffer3;
89 BitOStream request3;
90};
91
92using StreamError = BitIStream::Error;
93
94tst_Hpack::tst_Hpack()
95 : request1(buffer1),
96 request2(buffer2),
97 request3(buffer3)
98{
99}
100
101void tst_Hpack::bitstreamConstruction()
102{
103 const uchar bytes[] = {0xDE, 0xAD, 0xBE, 0xEF};
104 const int size = int(sizeof bytes);
105
106 // Default ctors:
107 std::vector<uchar> buffer;
108 {
109 const BitOStream out(buffer);
110 QVERIFY(out.bitLength() == 0);
111 QVERIFY(out.byteLength() == 0);
112
113 const BitIStream in;
114 QVERIFY(in.bitLength() == 0);
115 QVERIFY(in.streamOffset() == 0);
116 QVERIFY(in.error() == StreamError::NoError);
117 }
118
119 // Create istream with some data:
120 {
121 BitIStream in(bytes, bytes + size);
122 QVERIFY(in.bitLength() == size * 8);
123 QVERIFY(in.streamOffset() == 0);
124 QVERIFY(in.error() == StreamError::NoError);
125 // 'Read' some data back:
126 for (int i = 0; i < size; ++i) {
127 uchar bitPattern = 0;
128 const auto bitsRead = in.peekBits(from: quint64(i * 8), length: 8, dstPtr: &bitPattern);
129 QVERIFY(bitsRead == 8);
130 QVERIFY(bitPattern == bytes[i]);
131 }
132 }
133
134 // Copy ctors:
135 {
136 // Ostreams - copy is disabled.
137 // Istreams:
138 const BitIStream in1;
139 const BitIStream in2(in1);
140 QVERIFY(in2.bitLength() == in1.bitLength());
141 QVERIFY(in2.streamOffset() == in1.streamOffset());
142 QVERIFY(in2.error() == StreamError::NoError);
143
144 const BitIStream in3(bytes, bytes + size);
145 const BitIStream in4(in3);
146 QVERIFY(in4.bitLength() == in3.bitLength());
147 QVERIFY(in4.streamOffset() == in3.streamOffset());
148 QVERIFY(in4.error() == StreamError::NoError);
149 }
150}
151
152void tst_Hpack::bitstreamWrite()
153{
154 // Known representations,
155 // https://http2.github.io/http2-spec/compression.html.
156 // 5.1 Integer Representation
157
158 // Test bit/byte lengths of the
159 // resulting data:
160 std::vector<uchar> buffer;
161 BitOStream out(buffer);
162 out.write(src: 3);
163 // 11, fits into 8-bit prefix:
164 QVERIFY(out.bitLength() == 8);
165 QVERIFY(out.byteLength() == 1);
166 QVERIFY(out.begin()[0] == 3);
167
168 out.clear();
169 QVERIFY(out.bitLength() == 0);
170 QVERIFY(out.byteLength() == 0);
171
172 // This number does not fit into 8-bit
173 // prefix we'll need 2 bytes:
174 out.write(src: 256);
175 QVERIFY(out.byteLength() == 2);
176 QVERIFY(out.bitLength() == 16);
177 QVERIFY(out.begin()[0] == 0xff);
178 QVERIFY(out.begin()[1] == 1);
179
180 out.clear();
181
182 // See 5.2 String Literal Representation.
183
184 // We use Huffman code,
185 // char 'a' has a prefix code 00011 (5 bits)
186 out.write(src: QByteArray("aaa", 3), compressed: true);
187 QVERIFY(out.byteLength() == 3);
188 QVERIFY(out.bitLength() == 24);
189 // Now we must have in our stream:
190 // 10000010 | 00011000| 11000111
191 const uchar *encoded = out.begin();
192 QVERIFY(encoded[0] == 0x82);
193 QVERIFY(encoded[1] == 0x18);
194 QVERIFY(encoded[2] == 0xC7);
195 // TODO: add more tests ...
196}
197
198void tst_Hpack::bitstreamReadWrite()
199{
200 // We can write into the bit stream:
201 // 1) bit patterns
202 // 2) integers (see HPACK, 5.1)
203 // 3) string (see HPACK, 5.2)
204 std::vector<uchar> buffer;
205 BitOStream out(buffer);
206 out.writeBits(bits: 0xf, bitLength: 3);
207 QVERIFY(out.byteLength() == 1);
208 QVERIFY(out.bitLength() == 3);
209
210 // Now, read it back:
211 {
212 BitIStream in(out.begin(), out.end());
213 uchar bitPattern = 0;
214 const auto bitsRead = in.peekBits(from: 0, length: 3, dstPtr: &bitPattern);
215 // peekBits pack into the most significant byte/bit:
216 QVERIFY(bitsRead == 3);
217 QVERIFY((bitPattern >> 5) == 7);
218 }
219
220 const quint32 testInt = 133;
221 out.write(src: testInt);
222
223 // This integer does not fit into the current 5-bit prefix,
224 // so byteLength == 2.
225 QVERIFY(out.byteLength() == 2);
226 const auto bitLength = out.bitLength();
227 QVERIFY(bitLength > 3);
228
229 // Now, read it back:
230 {
231 BitIStream in(out.begin(), out.end());
232 in.skipBits(nBits: 3); // Bit pattern
233 quint32 value = 0;
234 QVERIFY(in.read(&value));
235 QVERIFY(in.error() == StreamError::NoError);
236 QCOMPARE(value, testInt);
237 }
238
239 const QByteArray testString("ABCDE", 5);
240 out.write(src: testString, compressed: true); // Compressed
241 out.write(src: testString, compressed: false); // Non-compressed
242 QVERIFY(out.byteLength() > 2);
243 QVERIFY(out.bitLength() > bitLength);
244
245 // Now, read it back:
246 {
247 BitIStream in(out.begin(), out.end());
248 in.skipBits(nBits: bitLength); // Bit pattern and integer
249 QByteArray value;
250 // Read compressed string first ...
251 QVERIFY(in.read(&value));
252 QCOMPARE(value, testString);
253 QCOMPARE(in.error(), StreamError::NoError);
254 // Now non-compressed ...
255 QVERIFY(in.read(&value));
256 QCOMPARE(value, testString);
257 QCOMPARE(in.error(), StreamError::NoError);
258 }
259}
260
261void tst_Hpack::bitstreamCompression()
262{
263 // Similar to bitstreamReadWrite but
264 // writes/reads a lot of mixed strings/integers.
265 std::vector<std::string> strings;
266 std::vector<quint32> integers;
267 std::vector<bool> isA; // integer or string.
268 const std::string bytes("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()[]/*");
269 const unsigned nValues = 100000;
270
271 quint64 totalStringBytes = 0;
272 std::vector<uchar> buffer;
273 BitOStream out(buffer);
274 for (unsigned i = 0; i < nValues; ++i) {
275 const bool isString = QRandomGenerator::global()->bounded(highest: 1000) > 500;
276 isA.push_back(x: isString);
277 if (!isString) {
278 integers.push_back(x: QRandomGenerator::global()->bounded(highest: 1000u));
279 out.write(src: integers.back());
280 } else {
281 const auto start = QRandomGenerator::global()->bounded(highest: uint(bytes.length()) / 2);
282 auto end = start * 2;
283 if (!end)
284 end = unsigned(bytes.length() / 2);
285 strings.push_back(x: bytes.substr(pos: start, n: end - start));
286 const auto &s = strings.back();
287 totalStringBytes += s.size();
288 QByteArray data(s.c_str(), int(s.size()));
289 const bool compressed(QRandomGenerator::global()->bounded(highest: 1000) > 500);
290 out.write(src: data, compressed);
291 }
292 }
293
294 qDebug() << "Compressed(?) byte length:" << out.byteLength()
295 << "total string bytes:" << totalStringBytes;
296 qDebug() << "total integer bytes (for quint32):" << integers.size() * sizeof(quint32);
297
298 QVERIFY(out.byteLength() > 0);
299 QVERIFY(out.bitLength() > 0);
300
301 BitIStream in(out.begin(), out.end());
302
303 for (unsigned i = 0, iS = 0, iI = 0; i < nValues; ++i) {
304 if (isA[i]) {
305 QByteArray data;
306 QVERIFY(in.read(&data));
307 QCOMPARE(in.error(), StreamError::NoError);
308 QCOMPARE(data.toStdString(), strings[iS]);
309 ++iS;
310 } else {
311 quint32 value = 0;
312 QVERIFY(in.read(&value));
313 QCOMPARE(in.error(), StreamError::NoError);
314 QCOMPARE(value, integers[iI]);
315 ++iI;
316 }
317 }
318}
319
320void tst_Hpack::bitstreamErrors()
321{
322 {
323 BitIStream in;
324 quint32 val = 0;
325 QVERIFY(!in.read(&val));
326 QCOMPARE(in.error(), StreamError::NotEnoughData);
327 }
328 {
329 // Integer in a stream, that does not fit into quint32.
330 const uchar bytes[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
331 BitIStream in(bytes, bytes + sizeof bytes);
332 quint32 val = 0;
333 QVERIFY(!in.read(&val));
334 QCOMPARE(in.error(), StreamError::InvalidInteger);
335 }
336 {
337 const uchar byte = 0x82; // 1 - Huffman compressed, 2 - the (fake) byte length.
338 BitIStream in(&byte, &byte + 1);
339 QByteArray val;
340 QVERIFY(!in.read(&val));
341 QCOMPARE(in.error(), StreamError::NotEnoughData);
342 }
343}
344
345void tst_Hpack::lookupTableConstructor()
346{
347 {
348 FieldLookupTable nonIndexed(4096, false);
349 QVERIFY(nonIndexed.dynamicDataSize() == 0);
350 QVERIFY(nonIndexed.numberOfDynamicEntries() == 0);
351 QVERIFY(nonIndexed.numberOfStaticEntries() != 0);
352 QVERIFY(nonIndexed.numberOfStaticEntries() == nonIndexed.numberOfEntries());
353 // Now we add some fake field and verify what 'non-indexed' means ... no search
354 // by name.
355 QVERIFY(nonIndexed.prependField("custom-key", "custom-value"));
356 // 54: 10 + 12 in name/value pair above + 32 required by HPACK specs ...
357 QVERIFY(nonIndexed.dynamicDataSize() == 54);
358 QVERIFY(nonIndexed.numberOfDynamicEntries() == 1);
359 QCOMPARE(nonIndexed.numberOfEntries(), nonIndexed.numberOfStaticEntries() + 1);
360 // Should fail to find it (invalid index 0) - search is disabled.
361 QVERIFY(nonIndexed.indexOf("custom-key", "custom-value") == 0);
362 }
363 {
364 // "key" + "value" == 8 bytes, + 32 (HPACK's requirement) == 40.
365 // Let's ask for a max-size 32 so that entry does not fit:
366 FieldLookupTable nonIndexed(32, false);
367 QVERIFY(nonIndexed.prependField("key", "value"));
368 QVERIFY(nonIndexed.numberOfEntries() == nonIndexed.numberOfStaticEntries());
369 QVERIFY(nonIndexed.indexOf("key", "value") == 0);
370 }
371 {
372 FieldLookupTable indexed(4096, true);
373 QVERIFY(indexed.dynamicDataSize() == 0);
374 QVERIFY(indexed.numberOfDynamicEntries() == 0);
375 QVERIFY(indexed.numberOfStaticEntries() != 0);
376 QVERIFY(indexed.numberOfStaticEntries() == indexed.numberOfEntries());
377 QVERIFY(indexed.prependField("custom-key", "custom-value"));
378 QVERIFY(indexed.dynamicDataSize() == 54);
379 QVERIFY(indexed.numberOfDynamicEntries() == 1);
380 QVERIFY(indexed.numberOfEntries() == indexed.numberOfStaticEntries() + 1);
381 QVERIFY(indexed.indexOf("custom-key") == indexed.numberOfStaticEntries() + 1);
382 QVERIFY(indexed.indexOf("custom-key", "custom-value") == indexed.numberOfStaticEntries() + 1);
383 }
384}
385
386void tst_Hpack::lookupTableStatic()
387{
388 const FieldLookupTable table(0, false /*all static, no need in 'search index'*/);
389 const auto &staticTable = FieldLookupTable::staticPart();
390 QByteArray name, value;
391 quint32 currentIndex = 1; // HPACK is indexing starting from 1.
392 for (const HeaderField &field : staticTable) {
393 const quint32 index = table.indexOf(name: field.name, value: field.value);
394 QVERIFY(index != 0);
395 QCOMPARE(index, currentIndex);
396 QVERIFY(table.field(index, &name, &value));
397 QCOMPARE(name, field.name);
398 QCOMPARE(value, field.value);
399 ++currentIndex;
400 }
401}
402
403void tst_Hpack::lookupTableDynamic()
404{
405 // HPACK's table size:
406 // for every field -> size += field.name.length() + field.value.length() + 32.
407 // Let's set some size limit and try to fill table with enough entries to have several
408 // items evicted.
409 const quint32 tableSize = 8192;
410 const char stringData[] = "abcdefghijklmnopABCDEFGHIJKLMNOP0123456789()[]:";
411 const quint32 dataSize = sizeof stringData - 1;
412
413 FieldLookupTable table(tableSize, true);
414
415 std::vector<QByteArray> fieldsToFind;
416 quint32 evicted = 0;
417
418 while (true) {
419 // Strings are repeating way too often, I want to
420 // have at least some items really evicted and not found,
421 // therefore these weird dances with start/len.
422 const quint32 start = QRandomGenerator::global()->bounded(highest: dataSize - 10);
423 quint32 len = QRandomGenerator::global()->bounded(highest: dataSize - start);
424 if (!len)
425 len = 1;
426
427 const QByteArray val(stringData + start, len);
428 fieldsToFind.push_back(x: val);
429 const quint32 entriesBefore = table.numberOfDynamicEntries();
430 QVERIFY(table.prependField(val, val));
431 QVERIFY(table.indexOf(val));
432 QVERIFY(table.indexOf(val) == table.indexOf(val, val));
433 QByteArray fieldName, fieldValue;
434 table.field(index: table.indexOf(name: val), name: &fieldName, value: &fieldValue);
435
436 QVERIFY(val == fieldName);
437 QVERIFY(val == fieldValue);
438
439 if (table.numberOfDynamicEntries() <= entriesBefore) {
440 // We had to evict several items ...
441 evicted += entriesBefore - table.numberOfDynamicEntries() + 1;
442 if (evicted >= 200)
443 break;
444 }
445 }
446
447 QVERIFY(table.dynamicDataSize() <= tableSize);
448 QVERIFY(table.numberOfDynamicEntries() > 0);
449 QVERIFY(table.indexOf(fieldsToFind.back())); // We MUST have it in a table!
450
451 using size_type = std::vector<QByteArray>::size_type;
452 for (size_type i = 0, e = fieldsToFind.size(); i < e; ++i) {
453 const auto &val = fieldsToFind[i];
454 const quint32 index = table.indexOf(name: val);
455 if (!index) {
456 QVERIFY(i < size_type(evicted));
457 } else {
458 QVERIFY(index == table.indexOf(val, val));
459 QByteArray fieldName, fieldValue;
460 QVERIFY(table.field(index, &fieldName, &fieldValue));
461 QVERIFY(val == fieldName);
462 QVERIFY(val == fieldValue);
463 }
464 }
465
466 table.clearDynamicTable();
467
468 QVERIFY(table.numberOfDynamicEntries() == 0);
469 QVERIFY(table.dynamicDataSize() == 0);
470 QVERIFY(table.indexOf(fieldsToFind.back()) == 0);
471
472 QVERIFY(table.prependField("name1", "value1"));
473 QVERIFY(table.prependField("name2", "value2"));
474
475 QVERIFY(table.indexOf("name1") == table.numberOfStaticEntries() + 2);
476 QVERIFY(table.indexOf("name2", "value2") == table.numberOfStaticEntries() + 1);
477 QVERIFY(table.indexOf("name1", "value2") == 0);
478 QVERIFY(table.indexOf("name2", "value1") == 0);
479 QVERIFY(table.indexOf("name3") == 0);
480
481 QVERIFY(!table.indexIsValid(table.numberOfEntries() + 1));
482
483 QVERIFY(table.prependField("name1", "value1"));
484 QVERIFY(table.numberOfDynamicEntries() == 3);
485 table.evictEntry();
486 QVERIFY(table.indexOf("name1") != 0);
487 table.evictEntry();
488 QVERIFY(table.indexOf("name2") == 0);
489 QVERIFY(table.indexOf("name1") != 0);
490 table.evictEntry();
491 QVERIFY(table.dynamicDataSize() == 0);
492 QVERIFY(table.numberOfDynamicEntries() == 0);
493 QVERIFY(table.indexOf("name1") == 0);
494}
495
496void tst_Hpack::hpackEncodeRequest_data()
497{
498 QTest::addColumn<bool>(name: "compression");
499 QTest::newRow(dataTag: "no-string-compression") << false;
500 QTest::newRow(dataTag: "with-string-compression") << true;
501}
502
503void tst_Hpack::hpackEncodeRequest(bool withHuffman)
504{
505 // This function uses examples from HPACK specs
506 // (see appendix).
507
508 Encoder encoder(4096, withHuffman);
509 // HPACK, C.3.1 First Request
510 /*
511 :method: GET
512 :scheme: http
513 :path: /
514 :authority: www.example.com
515
516 Hex dump of encoded data (without Huffman):
517
518 8286 8441 0f77 7777 2e65 7861 6d70 6c65 | ...A.www.example
519 2e63 6f6d
520
521 Hex dump of encoded data (with Huffman):
522
523 8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff
524 */
525 request1.clear();
526 header1 = {{":method", "GET"},
527 {":scheme", "http"},
528 {":path", "/"},
529 {":authority", "www.example.com"}};
530 QVERIFY(encoder.encodeRequest(request1, header1));
531 QVERIFY(encoder.dynamicTableSize() == 57);
532
533 // HPACK, C.3.2 Second Request
534 /*
535 Header list to encode:
536
537 :method: GET
538 :scheme: http
539 :path: /
540 :authority: www.example.com
541 cache-control: no-cache
542
543 Hex dump of encoded data (without Huffman):
544
545 8286 84be 5808 6e6f 2d63 6163 6865
546
547 Hex dump of encoded data (with Huffman):
548
549 8286 84be 5886 a8eb 1064 9cbf
550 */
551
552 request2.clear();
553 header2 = {{":method", "GET"},
554 {":scheme", "http"},
555 {":path", "/"},
556 {":authority", "www.example.com"},
557 {"cache-control", "no-cache"}};
558 encoder.encodeRequest(outputStream&: request2, header: header2);
559 QVERIFY(encoder.dynamicTableSize() == 110);
560
561 // HPACK, C.3.3 Third Request
562 /*
563 Header list to encode:
564
565 :method: GET
566 :scheme: https
567 :path: /index.html
568 :authority: www.example.com
569 custom-key: custom-value
570
571 Hex dump of encoded data (without Huffman):
572
573 8287 85bf 400a 6375 7374 6f6d 2d6b 6579
574 0c63 7573 746f 6d2d 7661 6c75 65
575
576 Hex dump of encoded data (with Huffman):
577
578 8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925
579 a849 e95b b8e8 b4bf
580 */
581 request3.clear();
582 header3 = {{":method", "GET"},
583 {":scheme", "https"},
584 {":path", "/index.html"},
585 {":authority", "www.example.com"},
586 {"custom-key", "custom-value"}};
587 encoder.encodeRequest(outputStream&: request3, header: header3);
588 QVERIFY(encoder.dynamicTableSize() == 164);
589}
590
591void tst_Hpack::hpackEncodeRequest()
592{
593 QFETCH(bool, compression);
594
595 hpackEncodeRequest(withHuffman: compression);
596
597 // See comments above about these hex dumps ...
598 const uchar bytes1NH[] = {0x82, 0x86, 0x84, 0x41,
599 0x0f, 0x77, 0x77, 0x77,
600 0x2e, 0x65, 0x78, 0x61,
601 0x6d, 0x70, 0x6c, 0x65,
602 0x2e, 0x63, 0x6f, 0x6d};
603
604 const uchar bytes1WH[] = {0x82, 0x86, 0x84, 0x41,
605 0x8c, 0xf1, 0xe3, 0xc2,
606 0xe5, 0xf2, 0x3a, 0x6b,
607 0xa0, 0xab, 0x90, 0xf4,
608 0xff};
609
610 const uchar *hexDump1 = compression ? bytes1WH : bytes1NH;
611 const quint64 byteLength1 = compression ? sizeof bytes1WH : sizeof bytes1NH;
612
613 QCOMPARE(request1.byteLength(), byteLength1);
614 QCOMPARE(request1.bitLength(), byteLength1 * 8);
615
616 for (quint32 i = 0, e = request1.byteLength(); i < e; ++i)
617 QCOMPARE(hexDump1[i], request1.begin()[i]);
618
619 const uchar bytes2NH[] = {0x82, 0x86, 0x84, 0xbe,
620 0x58, 0x08, 0x6e, 0x6f,
621 0x2d, 0x63, 0x61, 0x63,
622 0x68, 0x65};
623
624 const uchar bytes2WH[] = {0x82, 0x86, 0x84, 0xbe,
625 0x58, 0x86, 0xa8, 0xeb,
626 0x10, 0x64, 0x9c, 0xbf};
627
628 const uchar *hexDump2 = compression ? bytes2WH : bytes2NH;
629 const auto byteLength2 = compression ? sizeof bytes2WH : sizeof bytes2NH;
630 QVERIFY(request2.byteLength() == byteLength2);
631 QVERIFY(request2.bitLength() == byteLength2 * 8);
632 for (quint32 i = 0, e = request2.byteLength(); i < e; ++i)
633 QCOMPARE(hexDump2[i], request2.begin()[i]);
634
635 const uchar bytes3NH[] = {0x82, 0x87, 0x85, 0xbf,
636 0x40, 0x0a, 0x63, 0x75,
637 0x73, 0x74, 0x6f, 0x6d,
638 0x2d, 0x6b, 0x65, 0x79,
639 0x0c, 0x63, 0x75, 0x73,
640 0x74, 0x6f, 0x6d, 0x2d,
641 0x76, 0x61, 0x6c, 0x75,
642 0x65};
643 const uchar bytes3WH[] = {0x82, 0x87, 0x85, 0xbf,
644 0x40, 0x88, 0x25, 0xa8,
645 0x49, 0xe9, 0x5b, 0xa9,
646 0x7d, 0x7f, 0x89, 0x25,
647 0xa8, 0x49, 0xe9, 0x5b,
648 0xb8, 0xe8, 0xb4, 0xbf};
649
650 const uchar *hexDump3 = compression ? bytes3WH : bytes3NH;
651 const quint64 byteLength3 = compression ? sizeof bytes3WH : sizeof bytes3NH;
652 QCOMPARE(request3.byteLength(), byteLength3);
653 QCOMPARE(request3.bitLength(), byteLength3 * 8);
654 for (quint32 i = 0, e = request3.byteLength(); i < e; ++i)
655 QCOMPARE(hexDump3[i], request3.begin()[i]);
656}
657
658void tst_Hpack::hpackDecodeRequest_data()
659{
660 QTest::addColumn<bool>(name: "compression");
661 QTest::newRow(dataTag: "no-string-compression") << false;
662 QTest::newRow(dataTag: "with-string-compression") << true;
663}
664
665void tst_Hpack::hpackDecodeRequest()
666{
667 QFETCH(bool, compression);
668 hpackEncodeRequest(withHuffman: compression);
669
670 QVERIFY(request1.byteLength());
671 QVERIFY(request2.byteLength());
672 QVERIFY(request3.byteLength());
673
674 Decoder decoder(4096);
675 BitIStream inputStream1(request1.begin(), request1.end());
676 QVERIFY(decoder.decodeHeaderFields(inputStream1));
677 QCOMPARE(decoder.dynamicTableSize(), quint32(57));
678 {
679 const auto &decoded = decoder.decodedHeader();
680 QVERIFY(decoded == header1);
681 }
682
683 BitIStream inputStream2{request2.begin(), request2.end()};
684 QVERIFY(decoder.decodeHeaderFields(inputStream2));
685 QCOMPARE(decoder.dynamicTableSize(), quint32(110));
686 {
687 const auto &decoded = decoder.decodedHeader();
688 QVERIFY(decoded == header2);
689 }
690
691 BitIStream inputStream3(request3.begin(), request3.end());
692 QVERIFY(decoder.decodeHeaderFields(inputStream3));
693 QCOMPARE(decoder.dynamicTableSize(), quint32(164));
694 {
695 const auto &decoded = decoder.decodedHeader();
696 QVERIFY(decoded == header3);
697 }
698}
699
700void tst_Hpack::hpackEncodeResponse_data()
701{
702 hpackEncodeRequest_data();
703}
704
705void tst_Hpack::hpackEncodeResponse()
706{
707 QFETCH(bool, compression);
708
709 hpackEncodeResponse(withHuffman: compression);
710
711 // TODO: we can also test bytes - using hex dumps from HPACK's specs,
712 // for now only test a table behavior/expected sizes.
713}
714
715void tst_Hpack::hpackEncodeResponse(bool withCompression)
716{
717 Encoder encoder(256, withCompression); // 256 - this will result in entries evicted.
718
719 // HPACK, C.5.1 First Response
720 /*
721 Header list to encode:
722
723 :status: 302
724 cache-control: private
725 date: Mon, 21 Oct 2013 20:13:21 GMT
726 location: https://www.example.com
727 */
728 request1.clear();
729 header1 = {{":status", "302"},
730 {"cache-control", "private"},
731 {"date", "Mon, 21 Oct 2013 20:13:21 GMT"},
732 {"location", "https://www.example.com"}};
733
734 QVERIFY(encoder.encodeResponse(request1, header1));
735 QCOMPARE(encoder.dynamicTableSize(), quint32(222));
736
737 // HPACK, C.5.2 Second Response
738 /*
739
740
741 The (":status", "302") header field is evicted from the dynamic
742 table to free space to allow adding the (":status", "307") header field.
743
744 Header list to encode:
745
746 :status: 307
747 cache-control: private
748 date: Mon, 21 Oct 2013 20:13:21 GMT
749 location: https://www.example.com
750 */
751 request2.clear();
752 header2 = {{":status", "307"},
753 {"cache-control", "private"},
754 {"date", "Mon, 21 Oct 2013 20:13:21 GMT"},
755 {"location", "https://www.example.com"}};
756 QVERIFY(encoder.encodeResponse(request2, header2));
757 QCOMPARE(encoder.dynamicTableSize(), quint32(222));
758
759 // HPACK, C.5.3 Third Response
760 /*
761 Several header fields are evicted from the dynamic table
762 during the processing of this header list.
763
764 Header list to encode:
765
766 :status: 200
767 cache-control: private
768 date: Mon, 21 Oct 2013 20:13:22 GMT
769 location: https://www.example.com
770 content-encoding: gzip
771 set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1
772 */
773 request3.clear();
774 header3 = {{":status", "200"},
775 {"cache-control", "private"},
776 {"date", "Mon, 21 Oct 2013 20:13:22 GMT"},
777 {"location", "https://www.example.com"},
778 {"content-encoding", "gzip"},
779 {"set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}};
780 QVERIFY(encoder.encodeResponse(request3, header3));
781 QCOMPARE(encoder.dynamicTableSize(), quint32(215));
782}
783
784void tst_Hpack::hpackDecodeResponse_data()
785{
786 hpackEncodeRequest_data();
787}
788
789void tst_Hpack::hpackDecodeResponse()
790{
791 QFETCH(bool, compression);
792
793 hpackEncodeResponse(withCompression: compression);
794
795 QVERIFY(request1.byteLength());
796 Decoder decoder(256); // This size will result in entries evicted.
797 BitIStream inputStream1(request1.begin(), request1.end());
798 QVERIFY(decoder.decodeHeaderFields(inputStream1));
799 QCOMPARE(decoder.dynamicTableSize(), quint32(222));
800
801 {
802 const auto &decoded = decoder.decodedHeader();
803 QVERIFY(decoded == header1);
804 }
805
806 QVERIFY(request2.byteLength());
807 BitIStream inputStream2(request2.begin(), request2.end());
808 QVERIFY(decoder.decodeHeaderFields(inputStream2));
809 QCOMPARE(decoder.dynamicTableSize(), quint32(222));
810
811 {
812 const auto &decoded = decoder.decodedHeader();
813 QVERIFY(decoded == header2);
814 }
815
816 QVERIFY(request3.byteLength());
817 BitIStream inputStream3(request3.begin(), request3.end());
818 QVERIFY(decoder.decodeHeaderFields(inputStream3));
819 QCOMPARE(decoder.dynamicTableSize(), quint32(215));
820
821 {
822 const auto &decoded = decoder.decodedHeader();
823 QVERIFY(decoded == header3);
824 }
825}
826
827QTEST_MAIN(tst_Hpack)
828
829#include "tst_hpack.moc"
830

source code of qtbase/tests/auto/network/access/hpack/tst_hpack.cpp