// Copyright (C) 2016 The Qt Company Ltd. // Copyright (C) 2014 Governikus GmbH & Co. KG. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include #include #include QT_USE_NAMESPACE using namespace HPack; class tst_Hpack: public QObject { Q_OBJECT public: tst_Hpack(); private Q_SLOTS: void bitstreamConstruction(); void bitstreamWrite(); void bitstreamReadWrite(); void bitstreamCompression(); void bitstreamErrors(); void lookupTableConstructor(); void lookupTableStatic(); void lookupTableDynamic(); void hpackEncodeRequest_data(); void hpackEncodeRequest(); void hpackDecodeRequest_data(); void hpackDecodeRequest(); void hpackEncodeResponse_data(); void hpackEncodeResponse(); void hpackDecodeResponse_data(); void hpackDecodeResponse(); // TODO: more-more-more tests needed! private: void hpackEncodeRequest(bool withHuffman); void hpackEncodeResponse(bool withHuffman); HttpHeader header1; std::vector buffer1; BitOStream request1; HttpHeader header2; std::vector buffer2; BitOStream request2; HttpHeader header3; std::vector buffer3; BitOStream request3; }; using StreamError = BitIStream::Error; tst_Hpack::tst_Hpack() : request1(buffer1), request2(buffer2), request3(buffer3) { } void tst_Hpack::bitstreamConstruction() { const uchar bytes[] = {0xDE, 0xAD, 0xBE, 0xEF}; const int size = int(sizeof bytes); // Default ctors: std::vector buffer; { const BitOStream out(buffer); QVERIFY(out.bitLength() == 0); QVERIFY(out.byteLength() == 0); const BitIStream in; QVERIFY(in.bitLength() == 0); QVERIFY(in.streamOffset() == 0); QVERIFY(in.error() == StreamError::NoError); } // Create istream with some data: { BitIStream in(bytes, bytes + size); QVERIFY(in.bitLength() == size * 8); QVERIFY(in.streamOffset() == 0); QVERIFY(in.error() == StreamError::NoError); // 'Read' some data back: for (int i = 0; i < size; ++i) { uchar bitPattern = 0; const auto bitsRead = in.peekBits(quint64(i * 8), 8, &bitPattern); QVERIFY(bitsRead == 8); QVERIFY(bitPattern == bytes[i]); } } // Copy ctors: { // Ostreams - copy is disabled. // Istreams: const BitIStream in1; const BitIStream in2(in1); QVERIFY(in2.bitLength() == in1.bitLength()); QVERIFY(in2.streamOffset() == in1.streamOffset()); QVERIFY(in2.error() == StreamError::NoError); const BitIStream in3(bytes, bytes + size); const BitIStream in4(in3); QVERIFY(in4.bitLength() == in3.bitLength()); QVERIFY(in4.streamOffset() == in3.streamOffset()); QVERIFY(in4.error() == StreamError::NoError); } } void tst_Hpack::bitstreamWrite() { // Known representations, // https://http2.github.io/http2-spec/compression.html. // 5.1 Integer Representation // Test bit/byte lengths of the // resulting data: std::vector buffer; BitOStream out(buffer); out.write(3); // 11, fits into 8-bit prefix: QVERIFY(out.bitLength() == 8); QVERIFY(out.byteLength() == 1); QVERIFY(out.begin()[0] == 3); out.clear(); QVERIFY(out.bitLength() == 0); QVERIFY(out.byteLength() == 0); // This number does not fit into 8-bit // prefix we'll need 2 bytes: out.write(256); QVERIFY(out.byteLength() == 2); QVERIFY(out.bitLength() == 16); QVERIFY(out.begin()[0] == 0xff); QVERIFY(out.begin()[1] == 1); out.clear(); // See 5.2 String Literal Representation. // We use Huffman code, // char 'a' has a prefix code 00011 (5 bits) out.write(QByteArray("aaa", 3), true); QVERIFY(out.byteLength() == 3); QVERIFY(out.bitLength() == 24); // Now we must have in our stream: // 10000010 | 00011000| 11000111 const uchar *encoded = out.begin(); QVERIFY(encoded[0] == 0x82); QVERIFY(encoded[1] == 0x18); QVERIFY(encoded[2] == 0xC7); // TODO: add more tests ... } void tst_Hpack::bitstreamReadWrite() { // We can write into the bit stream: // 1) bit patterns // 2) integers (see HPACK, 5.1) // 3) string (see HPACK, 5.2) std::vector buffer; BitOStream out(buffer); out.writeBits(0xf, 3); QVERIFY(out.byteLength() == 1); QVERIFY(out.bitLength() == 3); // Now, read it back: { BitIStream in(out.begin(), out.end()); uchar bitPattern = 0; const auto bitsRead = in.peekBits(0, 3, &bitPattern); // peekBits pack into the most significant byte/bit: QVERIFY(bitsRead == 3); QVERIFY((bitPattern >> 5) == 7); } const quint32 testInt = 133; out.write(testInt); // This integer does not fit into the current 5-bit prefix, // so byteLength == 2. QVERIFY(out.byteLength() == 2); const auto bitLength = out.bitLength(); QVERIFY(bitLength > 3); // Now, read it back: { BitIStream in(out.begin(), out.end()); in.skipBits(3); // Bit pattern quint32 value = 0; QVERIFY(in.read(&value)); QVERIFY(in.error() == StreamError::NoError); QCOMPARE(value, testInt); } const QByteArray testString("ABCDE", 5); out.write(testString, true); // Compressed out.write(testString, false); // Non-compressed QVERIFY(out.byteLength() > 2); QVERIFY(out.bitLength() > bitLength); // Now, read it back: { BitIStream in(out.begin(), out.end()); in.skipBits(bitLength); // Bit pattern and integer QByteArray value; // Read compressed string first ... QVERIFY(in.read(&value)); QCOMPARE(value, testString); QCOMPARE(in.error(), StreamError::NoError); // Now non-compressed ... QVERIFY(in.read(&value)); QCOMPARE(value, testString); QCOMPARE(in.error(), StreamError::NoError); } } void tst_Hpack::bitstreamCompression() { // Similar to bitstreamReadWrite but // writes/reads a lot of mixed strings/integers. std::vector strings; std::vector integers; std::vector isA; // integer or string. const std::string bytes("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()[]/*"); const unsigned nValues = 100000; quint64 totalStringBytes = 0; std::vector buffer; BitOStream out(buffer); for (unsigned i = 0; i < nValues; ++i) { const bool isString = QRandomGenerator::global()->bounded(1000) > 500; isA.push_back(isString); if (!isString) { integers.push_back(QRandomGenerator::global()->bounded(1000u)); out.write(integers.back()); } else { const auto start = QRandomGenerator::global()->bounded(uint(bytes.length()) / 2); auto end = start * 2; if (!end) end = unsigned(bytes.length() / 2); strings.push_back(bytes.substr(start, end - start)); const auto &s = strings.back(); totalStringBytes += s.size(); QByteArray data(s.c_str(), int(s.size())); const bool compressed(QRandomGenerator::global()->bounded(1000) > 500); out.write(data, compressed); } } qDebug() << "Compressed(?) byte length:" << out.byteLength() << "total string bytes:" << totalStringBytes; qDebug() << "total integer bytes (for quint32):" << integers.size() * sizeof(quint32); QVERIFY(out.byteLength() > 0); QVERIFY(out.bitLength() > 0); BitIStream in(out.begin(), out.end()); for (unsigned i = 0, iS = 0, iI = 0; i < nValues; ++i) { if (isA[i]) { QByteArray data; QVERIFY(in.read(&data)); QCOMPARE(in.error(), StreamError::NoError); QCOMPARE(data.toStdString(), strings[iS]); ++iS; } else { quint32 value = 0; QVERIFY(in.read(&value)); QCOMPARE(in.error(), StreamError::NoError); QCOMPARE(value, integers[iI]); ++iI; } } } void tst_Hpack::bitstreamErrors() { { BitIStream in; quint32 val = 0; QVERIFY(!in.read(&val)); QCOMPARE(in.error(), StreamError::NotEnoughData); } { // Integer in a stream, that does not fit into quint32. const uchar bytes[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; BitIStream in(bytes, bytes + sizeof bytes); quint32 val = 0; QVERIFY(!in.read(&val)); QCOMPARE(in.error(), StreamError::InvalidInteger); } { const uchar byte = 0x82; // 1 - Huffman compressed, 2 - the (fake) byte length. BitIStream in(&byte, &byte + 1); QByteArray val; QVERIFY(!in.read(&val)); QCOMPARE(in.error(), StreamError::NotEnoughData); } } void tst_Hpack::lookupTableConstructor() { { FieldLookupTable nonIndexed(4096, false); QVERIFY(nonIndexed.dynamicDataSize() == 0); QVERIFY(nonIndexed.numberOfDynamicEntries() == 0); QVERIFY(nonIndexed.numberOfStaticEntries() != 0); QVERIFY(nonIndexed.numberOfStaticEntries() == nonIndexed.numberOfEntries()); // Now we add some fake field and verify what 'non-indexed' means ... no search // by name. QVERIFY(nonIndexed.prependField("custom-key", "custom-value")); // 54: 10 + 12 in name/value pair above + 32 required by HPACK specs ... QVERIFY(nonIndexed.dynamicDataSize() == 54); QVERIFY(nonIndexed.numberOfDynamicEntries() == 1); QCOMPARE(nonIndexed.numberOfEntries(), nonIndexed.numberOfStaticEntries() + 1); // Should fail to find it (invalid index 0) - search is disabled. QVERIFY(nonIndexed.indexOf("custom-key", "custom-value") == 0); } { // "key" + "value" == 8 bytes, + 32 (HPACK's requirement) == 40. // Let's ask for a max-size 32 so that entry does not fit: FieldLookupTable nonIndexed(32, false); QVERIFY(nonIndexed.prependField("key", "value")); QVERIFY(nonIndexed.numberOfEntries() == nonIndexed.numberOfStaticEntries()); QVERIFY(nonIndexed.indexOf("key", "value") == 0); } { FieldLookupTable indexed(4096, true); QVERIFY(indexed.dynamicDataSize() == 0); QVERIFY(indexed.numberOfDynamicEntries() == 0); QVERIFY(indexed.numberOfStaticEntries() != 0); QVERIFY(indexed.numberOfStaticEntries() == indexed.numberOfEntries()); QVERIFY(indexed.prependField("custom-key", "custom-value")); QVERIFY(indexed.dynamicDataSize() == 54); QVERIFY(indexed.numberOfDynamicEntries() == 1); QVERIFY(indexed.numberOfEntries() == indexed.numberOfStaticEntries() + 1); QVERIFY(indexed.indexOf("custom-key") == indexed.numberOfStaticEntries() + 1); QVERIFY(indexed.indexOf("custom-key", "custom-value") == indexed.numberOfStaticEntries() + 1); } } void tst_Hpack::lookupTableStatic() { const FieldLookupTable table(0, false /*all static, no need in 'search index'*/); const auto &staticTable = FieldLookupTable::staticPart(); QByteArray name, value; quint32 currentIndex = 1; // HPACK is indexing starting from 1. for (const HeaderField &field : staticTable) { const quint32 index = table.indexOf(field.name, field.value); QVERIFY(index != 0); QCOMPARE(index, currentIndex); QVERIFY(table.field(index, &name, &value)); QCOMPARE(name, field.name); QCOMPARE(value, field.value); ++currentIndex; } } void tst_Hpack::lookupTableDynamic() { // HPACK's table size: // for every field -> size += field.name.length() + field.value.length() + 32. // Let's set some size limit and try to fill table with enough entries to have several // items evicted. const quint32 tableSize = 8192; const char stringData[] = "abcdefghijklmnopABCDEFGHIJKLMNOP0123456789()[]:"; const quint32 dataSize = sizeof stringData - 1; FieldLookupTable table(tableSize, true); std::vector fieldsToFind; quint32 evicted = 0; while (true) { // Strings are repeating way too often, I want to // have at least some items really evicted and not found, // therefore these weird dances with start/len. const quint32 start = QRandomGenerator::global()->bounded(dataSize - 10); quint32 len = QRandomGenerator::global()->bounded(dataSize - start); if (!len) len = 1; const QByteArray val(stringData + start, len); fieldsToFind.push_back(val); const quint32 entriesBefore = table.numberOfDynamicEntries(); QVERIFY(table.prependField(val, val)); QVERIFY(table.indexOf(val)); QVERIFY(table.indexOf(val) == table.indexOf(val, val)); QByteArray fieldName, fieldValue; table.field(table.indexOf(val), &fieldName, &fieldValue); QVERIFY(val == fieldName); QVERIFY(val == fieldValue); if (table.numberOfDynamicEntries() <= entriesBefore) { // We had to evict several items ... evicted += entriesBefore - table.numberOfDynamicEntries() + 1; if (evicted >= 200) break; } } QVERIFY(table.dynamicDataSize() <= tableSize); QVERIFY(table.numberOfDynamicEntries() > 0); QVERIFY(table.indexOf(fieldsToFind.back())); // We MUST have it in a table! using size_type = std::vector::size_type; for (size_type i = 0, e = fieldsToFind.size(); i < e; ++i) { const auto &val = fieldsToFind[i]; const quint32 index = table.indexOf(val); if (!index) { QVERIFY(i < size_type(evicted)); } else { QVERIFY(index == table.indexOf(val, val)); QByteArray fieldName, fieldValue; QVERIFY(table.field(index, &fieldName, &fieldValue)); QVERIFY(val == fieldName); QVERIFY(val == fieldValue); } } table.clearDynamicTable(); QVERIFY(table.numberOfDynamicEntries() == 0); QVERIFY(table.dynamicDataSize() == 0); QVERIFY(table.indexOf(fieldsToFind.back()) == 0); QVERIFY(table.prependField("name1", "value1")); QVERIFY(table.prependField("name2", "value2")); QVERIFY(table.indexOf("name1") == table.numberOfStaticEntries() + 2); QVERIFY(table.indexOf("name2", "value2") == table.numberOfStaticEntries() + 1); QVERIFY(table.indexOf("name1", "value2") == 0); QVERIFY(table.indexOf("name2", "value1") == 0); QVERIFY(table.indexOf("name3") == 0); QVERIFY(!table.indexIsValid(table.numberOfEntries() + 1)); QVERIFY(table.prependField("name1", "value1")); QVERIFY(table.numberOfDynamicEntries() == 3); table.evictEntry(); QVERIFY(table.indexOf("name1") != 0); table.evictEntry(); QVERIFY(table.indexOf("name2") == 0); QVERIFY(table.indexOf("name1") != 0); table.evictEntry(); QVERIFY(table.dynamicDataSize() == 0); QVERIFY(table.numberOfDynamicEntries() == 0); QVERIFY(table.indexOf("name1") == 0); } void tst_Hpack::hpackEncodeRequest_data() { QTest::addColumn("compression"); QTest::newRow("no-string-compression") << false; QTest::newRow("with-string-compression") << true; } void tst_Hpack::hpackEncodeRequest(bool withHuffman) { // This function uses examples from HPACK specs // (see appendix). Encoder encoder(4096, withHuffman); // HPACK, C.3.1 First Request /* :method: GET :scheme: http :path: / :authority: www.example.com Hex dump of encoded data (without Huffman): 8286 8441 0f77 7777 2e65 7861 6d70 6c65 | ...A.www.example 2e63 6f6d Hex dump of encoded data (with Huffman): 8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff */ request1.clear(); header1 = {{":method", "GET"}, {":scheme", "http"}, {":path", "/"}, {":authority", "www.example.com"}}; QVERIFY(encoder.encodeRequest(request1, header1)); QVERIFY(encoder.dynamicTableSize() == 57); // HPACK, C.3.2 Second Request /* Header list to encode: :method: GET :scheme: http :path: / :authority: www.example.com cache-control: no-cache Hex dump of encoded data (without Huffman): 8286 84be 5808 6e6f 2d63 6163 6865 Hex dump of encoded data (with Huffman): 8286 84be 5886 a8eb 1064 9cbf */ request2.clear(); header2 = {{":method", "GET"}, {":scheme", "http"}, {":path", "/"}, {":authority", "www.example.com"}, {"cache-control", "no-cache"}}; encoder.encodeRequest(request2, header2); QVERIFY(encoder.dynamicTableSize() == 110); // HPACK, C.3.3 Third Request /* Header list to encode: :method: GET :scheme: https :path: /index.html :authority: www.example.com custom-key: custom-value Hex dump of encoded data (without Huffman): 8287 85bf 400a 6375 7374 6f6d 2d6b 6579 0c63 7573 746f 6d2d 7661 6c75 65 Hex dump of encoded data (with Huffman): 8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925 a849 e95b b8e8 b4bf */ request3.clear(); header3 = {{":method", "GET"}, {":scheme", "https"}, {":path", "/index.html"}, {":authority", "www.example.com"}, {"custom-key", "custom-value"}}; encoder.encodeRequest(request3, header3); QVERIFY(encoder.dynamicTableSize() == 164); } void tst_Hpack::hpackEncodeRequest() { QFETCH(bool, compression); hpackEncodeRequest(compression); // See comments above about these hex dumps ... const uchar bytes1NH[] = {0x82, 0x86, 0x84, 0x41, 0x0f, 0x77, 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d}; const uchar bytes1WH[] = {0x82, 0x86, 0x84, 0x41, 0x8c, 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff}; const uchar *hexDump1 = compression ? bytes1WH : bytes1NH; const quint64 byteLength1 = compression ? sizeof bytes1WH : sizeof bytes1NH; QCOMPARE(request1.byteLength(), byteLength1); QCOMPARE(request1.bitLength(), byteLength1 * 8); for (quint32 i = 0, e = request1.byteLength(); i < e; ++i) QCOMPARE(hexDump1[i], request1.begin()[i]); const uchar bytes2NH[] = {0x82, 0x86, 0x84, 0xbe, 0x58, 0x08, 0x6e, 0x6f, 0x2d, 0x63, 0x61, 0x63, 0x68, 0x65}; const uchar bytes2WH[] = {0x82, 0x86, 0x84, 0xbe, 0x58, 0x86, 0xa8, 0xeb, 0x10, 0x64, 0x9c, 0xbf}; const uchar *hexDump2 = compression ? bytes2WH : bytes2NH; const auto byteLength2 = compression ? sizeof bytes2WH : sizeof bytes2NH; QVERIFY(request2.byteLength() == byteLength2); QVERIFY(request2.bitLength() == byteLength2 * 8); for (quint32 i = 0, e = request2.byteLength(); i < e; ++i) QCOMPARE(hexDump2[i], request2.begin()[i]); const uchar bytes3NH[] = {0x82, 0x87, 0x85, 0xbf, 0x40, 0x0a, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x76, 0x61, 0x6c, 0x75, 0x65}; const uchar bytes3WH[] = {0x82, 0x87, 0x85, 0xbf, 0x40, 0x88, 0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xa9, 0x7d, 0x7f, 0x89, 0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xb8, 0xe8, 0xb4, 0xbf}; const uchar *hexDump3 = compression ? bytes3WH : bytes3NH; const quint64 byteLength3 = compression ? sizeof bytes3WH : sizeof bytes3NH; QCOMPARE(request3.byteLength(), byteLength3); QCOMPARE(request3.bitLength(), byteLength3 * 8); for (quint32 i = 0, e = request3.byteLength(); i < e; ++i) QCOMPARE(hexDump3[i], request3.begin()[i]); } void tst_Hpack::hpackDecodeRequest_data() { QTest::addColumn("compression"); QTest::newRow("no-string-compression") << false; QTest::newRow("with-string-compression") << true; } void tst_Hpack::hpackDecodeRequest() { QFETCH(bool, compression); hpackEncodeRequest(compression); QVERIFY(request1.byteLength()); QVERIFY(request2.byteLength()); QVERIFY(request3.byteLength()); Decoder decoder(4096); BitIStream inputStream1(request1.begin(), request1.end()); QVERIFY(decoder.decodeHeaderFields(inputStream1)); QCOMPARE(decoder.dynamicTableSize(), quint32(57)); { const auto &decoded = decoder.decodedHeader(); QVERIFY(decoded == header1); } BitIStream inputStream2{request2.begin(), request2.end()}; QVERIFY(decoder.decodeHeaderFields(inputStream2)); QCOMPARE(decoder.dynamicTableSize(), quint32(110)); { const auto &decoded = decoder.decodedHeader(); QVERIFY(decoded == header2); } BitIStream inputStream3(request3.begin(), request3.end()); QVERIFY(decoder.decodeHeaderFields(inputStream3)); QCOMPARE(decoder.dynamicTableSize(), quint32(164)); { const auto &decoded = decoder.decodedHeader(); QVERIFY(decoded == header3); } } void tst_Hpack::hpackEncodeResponse_data() { hpackEncodeRequest_data(); } void tst_Hpack::hpackEncodeResponse() { QFETCH(bool, compression); hpackEncodeResponse(compression); // TODO: we can also test bytes - using hex dumps from HPACK's specs, // for now only test a table behavior/expected sizes. } void tst_Hpack::hpackEncodeResponse(bool withCompression) { Encoder encoder(256, withCompression); // 256 - this will result in entries evicted. // HPACK, C.5.1 First Response /* Header list to encode: :status: 302 cache-control: private date: Mon, 21 Oct 2013 20:13:21 GMT location: https://www.example.com */ request1.clear(); header1 = {{":status", "302"}, {"cache-control", "private"}, {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, {"location", "https://www.example.com"}}; QVERIFY(encoder.encodeResponse(request1, header1)); QCOMPARE(encoder.dynamicTableSize(), quint32(222)); // HPACK, C.5.2 Second Response /* The (":status", "302") header field is evicted from the dynamic table to free space to allow adding the (":status", "307") header field. Header list to encode: :status: 307 cache-control: private date: Mon, 21 Oct 2013 20:13:21 GMT location: https://www.example.com */ request2.clear(); header2 = {{":status", "307"}, {"cache-control", "private"}, {"date", "Mon, 21 Oct 2013 20:13:21 GMT"}, {"location", "https://www.example.com"}}; QVERIFY(encoder.encodeResponse(request2, header2)); QCOMPARE(encoder.dynamicTableSize(), quint32(222)); // HPACK, C.5.3 Third Response /* Several header fields are evicted from the dynamic table during the processing of this header list. Header list to encode: :status: 200 cache-control: private date: Mon, 21 Oct 2013 20:13:22 GMT location: https://www.example.com content-encoding: gzip set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1 */ request3.clear(); header3 = {{":status", "200"}, {"cache-control", "private"}, {"date", "Mon, 21 Oct 2013 20:13:22 GMT"}, {"location", "https://www.example.com"}, {"content-encoding", "gzip"}, {"set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}}; QVERIFY(encoder.encodeResponse(request3, header3)); QCOMPARE(encoder.dynamicTableSize(), quint32(215)); } void tst_Hpack::hpackDecodeResponse_data() { hpackEncodeRequest_data(); } void tst_Hpack::hpackDecodeResponse() { QFETCH(bool, compression); hpackEncodeResponse(compression); QVERIFY(request1.byteLength()); Decoder decoder(256); // This size will result in entries evicted. BitIStream inputStream1(request1.begin(), request1.end()); QVERIFY(decoder.decodeHeaderFields(inputStream1)); QCOMPARE(decoder.dynamicTableSize(), quint32(222)); { const auto &decoded = decoder.decodedHeader(); QVERIFY(decoded == header1); } QVERIFY(request2.byteLength()); BitIStream inputStream2(request2.begin(), request2.end()); QVERIFY(decoder.decodeHeaderFields(inputStream2)); QCOMPARE(decoder.dynamicTableSize(), quint32(222)); { const auto &decoded = decoder.decodedHeader(); QVERIFY(decoded == header2); } QVERIFY(request3.byteLength()); BitIStream inputStream3(request3.begin(), request3.end()); QVERIFY(decoder.decodeHeaderFields(inputStream3)); QCOMPARE(decoder.dynamicTableSize(), quint32(215)); { const auto &decoded = decoder.decodedHeader(); QVERIFY(decoded == header3); } } QTEST_MAIN(tst_Hpack) #include "tst_hpack.moc"