// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/spdy/hpack_encoder.h" #include #include #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { using base::StringPiece; using std::string; using testing::ElementsAre; namespace test { class HpackHeaderTablePeer { public: explicit HpackHeaderTablePeer(HpackHeaderTable* table) : table_(table) {} HpackHeaderTable::EntryTable* dynamic_entries() { return &table_->dynamic_entries_; } private: HpackHeaderTable* table_; }; class HpackEncoderPeer { public: typedef HpackEncoder::Representation Representation; typedef HpackEncoder::Representations Representations; explicit HpackEncoderPeer(HpackEncoder* encoder) : encoder_(encoder) {} HpackHeaderTable* table() { return &encoder_->header_table_; } HpackHeaderTablePeer table_peer() { return HpackHeaderTablePeer(table()); } void set_allow_huffman_compression(bool allow) { encoder_->allow_huffman_compression_ = allow; } void EmitString(StringPiece str) { encoder_->EmitString(str); } void TakeString(string* out) { encoder_->output_stream_.TakeString(out); } void UpdateCharacterCounts(StringPiece str) { encoder_->UpdateCharacterCounts(str); } static void CookieToCrumbs(StringPiece cookie, std::vector* out) { Representations tmp; HpackEncoder::CookieToCrumbs(std::make_pair("", cookie), &tmp); out->clear(); for (size_t i = 0; i != tmp.size(); ++i) { out->push_back(tmp[i].second); } } static void DecomposeRepresentation(StringPiece value, std::vector* out) { Representations tmp; HpackEncoder::DecomposeRepresentation(std::make_pair("foobar", value), &tmp); out->clear(); for (size_t i = 0; i != tmp.size(); ++i) { out->push_back(tmp[i].second); } } private: HpackEncoder* encoder_; }; } // namespace test namespace { using std::map; using testing::ElementsAre; class HpackEncoderTest : public ::testing::Test { protected: typedef test::HpackEncoderPeer::Representations Representations; HpackEncoderTest() : encoder_(ObtainHpackHuffmanTable()), peer_(&encoder_), static_(peer_.table()->GetByIndex(1)) {} void SetUp() override { // Populate dynamic entries into the table fixture. For simplicity each // entry has name.size() + value.size() == 10. key_1_ = peer_.table()->TryAddEntry("key1", "value1"); key_2_ = peer_.table()->TryAddEntry("key2", "value2"); cookie_a_ = peer_.table()->TryAddEntry("cookie", "a=bb"); cookie_c_ = peer_.table()->TryAddEntry("cookie", "c=dd"); // No further insertions may occur without evictions. peer_.table()->SetMaxSize(peer_.table()->size()); // Disable Huffman coding by default. Most tests don't care about it. peer_.set_allow_huffman_compression(false); } void ExpectIndex(size_t index) { expected_.AppendPrefix(kIndexedOpcode); expected_.AppendUint32(index); } void ExpectIndexedLiteral(const HpackEntry* key_entry, StringPiece value) { expected_.AppendPrefix(kLiteralIncrementalIndexOpcode); expected_.AppendUint32(IndexOf(key_entry)); expected_.AppendPrefix(kStringLiteralIdentityEncoded); expected_.AppendUint32(value.size()); expected_.AppendBytes(value); } void ExpectIndexedLiteral(StringPiece name, StringPiece value) { expected_.AppendPrefix(kLiteralIncrementalIndexOpcode); expected_.AppendUint32(0); expected_.AppendPrefix(kStringLiteralIdentityEncoded); expected_.AppendUint32(name.size()); expected_.AppendBytes(name); expected_.AppendPrefix(kStringLiteralIdentityEncoded); expected_.AppendUint32(value.size()); expected_.AppendBytes(value); } void ExpectNonIndexedLiteral(StringPiece name, StringPiece value) { expected_.AppendPrefix(kLiteralNoIndexOpcode); expected_.AppendUint32(0); expected_.AppendPrefix(kStringLiteralIdentityEncoded); expected_.AppendUint32(name.size()); expected_.AppendBytes(name); expected_.AppendPrefix(kStringLiteralIdentityEncoded); expected_.AppendUint32(value.size()); expected_.AppendBytes(value); } void CompareWithExpectedEncoding(const map& header_set) { string expected_out, actual_out; expected_.TakeString(&expected_out); EXPECT_TRUE(encoder_.EncodeHeaderSet(header_set, &actual_out)); EXPECT_EQ(expected_out, actual_out); } size_t IndexOf(const HpackEntry* entry) { return peer_.table()->IndexOf(entry); } HpackEncoder encoder_; test::HpackEncoderPeer peer_; const HpackEntry* static_; const HpackEntry* key_1_; const HpackEntry* key_2_; const HpackEntry* cookie_a_; const HpackEntry* cookie_c_; HpackOutputStream expected_; }; TEST_F(HpackEncoderTest, SingleDynamicIndex) { ExpectIndex(IndexOf(key_2_)); map headers; headers[key_2_->name()] = key_2_->value(); CompareWithExpectedEncoding(headers); } TEST_F(HpackEncoderTest, SingleStaticIndex) { ExpectIndex(IndexOf(static_)); map headers; headers[static_->name()] = static_->value(); CompareWithExpectedEncoding(headers); } TEST_F(HpackEncoderTest, SingleStaticIndexTooLarge) { peer_.table()->SetMaxSize(1); // Also evicts all fixtures. ExpectIndex(IndexOf(static_)); map headers; headers[static_->name()] = static_->value(); CompareWithExpectedEncoding(headers); EXPECT_EQ(0u, peer_.table_peer().dynamic_entries()->size()); } TEST_F(HpackEncoderTest, SingleLiteralWithIndexName) { ExpectIndexedLiteral(key_2_, "value3"); map headers; headers[key_2_->name()] = "value3"; CompareWithExpectedEncoding(headers); // A new entry was inserted and added to the reference set. HpackEntry* new_entry = &peer_.table_peer().dynamic_entries()->front(); EXPECT_EQ(new_entry->name(), key_2_->name()); EXPECT_EQ(new_entry->value(), "value3"); } TEST_F(HpackEncoderTest, SingleLiteralWithLiteralName) { ExpectIndexedLiteral("key3", "value3"); map headers; headers["key3"] = "value3"; CompareWithExpectedEncoding(headers); HpackEntry* new_entry = &peer_.table_peer().dynamic_entries()->front(); EXPECT_EQ(new_entry->name(), "key3"); EXPECT_EQ(new_entry->value(), "value3"); } TEST_F(HpackEncoderTest, SingleLiteralTooLarge) { peer_.table()->SetMaxSize(1); // Also evicts all fixtures. ExpectIndexedLiteral("key3", "value3"); // A header overflowing the header table is still emitted. // The header table is empty. map headers; headers["key3"] = "value3"; CompareWithExpectedEncoding(headers); EXPECT_EQ(0u, peer_.table_peer().dynamic_entries()->size()); } TEST_F(HpackEncoderTest, EmitThanEvict) { // |key_1_| is toggled and placed into the reference set, // and then immediately evicted by "key3". ExpectIndex(IndexOf(key_1_)); ExpectIndexedLiteral("key3", "value3"); map headers; headers[key_1_->name()] = key_1_->value(); headers["key3"] = "value3"; CompareWithExpectedEncoding(headers); } TEST_F(HpackEncoderTest, CookieHeaderIsCrumbled) { ExpectIndex(IndexOf(cookie_a_)); ExpectIndex(IndexOf(cookie_c_)); ExpectIndexedLiteral(peer_.table()->GetByName("cookie"), "e=ff"); map headers; headers["cookie"] = "e=ff; a=bb; c=dd"; CompareWithExpectedEncoding(headers); } TEST_F(HpackEncoderTest, StringsDynamicallySelectHuffmanCoding) { peer_.set_allow_huffman_compression(true); // Compactable string. Uses Huffman coding. peer_.EmitString("feedbeef"); expected_.AppendPrefix(kStringLiteralHuffmanEncoded); expected_.AppendUint32(6); expected_.AppendBytes("\x94\xA5\x92""2\x96_"); // Non-compactable. Uses identity coding. peer_.EmitString("@@@@@@"); expected_.AppendPrefix(kStringLiteralIdentityEncoded); expected_.AppendUint32(6); expected_.AppendBytes("@@@@@@"); string expected_out, actual_out; expected_.TakeString(&expected_out); peer_.TakeString(&actual_out); EXPECT_EQ(expected_out, actual_out); } TEST_F(HpackEncoderTest, EncodingWithoutCompression) { // Implementation should internally disable. peer_.set_allow_huffman_compression(true); ExpectNonIndexedLiteral(":path", "/index.html"); ExpectNonIndexedLiteral("cookie", "foo=bar; baz=bing"); ExpectNonIndexedLiteral("hello", "goodbye"); map headers; headers[":path"] = "/index.html"; headers["cookie"] = "foo=bar; baz=bing"; headers["hello"] = "goodbye"; string expected_out, actual_out; expected_.TakeString(&expected_out); encoder_.EncodeHeaderSetWithoutCompression(headers, &actual_out); EXPECT_EQ(expected_out, actual_out); } TEST_F(HpackEncoderTest, MultipleEncodingPasses) { // Pass 1. { map headers; headers["key1"] = "value1"; headers["cookie"] = "a=bb"; ExpectIndex(IndexOf(cookie_a_)); ExpectIndex(IndexOf(key_1_)); CompareWithExpectedEncoding(headers); } // Header table is: // 65: key1: value1 // 64: key2: value2 // 63: cookie: a=bb // 62: cookie: c=dd // Pass 2. { map headers; headers["key2"] = "value2"; headers["cookie"] = "c=dd; e=ff"; ExpectIndex(IndexOf(cookie_c_)); // This cookie evicts |key1| from the dynamic table. ExpectIndexedLiteral(peer_.table()->GetByName("cookie"), "e=ff"); // "key2: value2" ExpectIndex(65); CompareWithExpectedEncoding(headers); } // Header table is: // 65: key2: value2 // 64: cookie: a=bb // 63: cookie: c=dd // 62: cookie: e=ff // Pass 3. { map headers; headers["key2"] = "value2"; headers["cookie"] = "a=bb; b=cc; c=dd"; // "cookie: a=bb" ExpectIndex(64); // This cookie evicts |key2| from the dynamic table. ExpectIndexedLiteral(peer_.table()->GetByName("cookie"), "b=cc"); // "cookie: c=dd" ExpectIndex(64); // "key2: value2" ExpectIndexedLiteral("key2", "value2"); CompareWithExpectedEncoding(headers); } } TEST_F(HpackEncoderTest, PseudoHeadersFirst) { map headers; // A pseudo-header to be indexed. headers[":authority"] = "www.example.com"; // A pseudo-header that should not be indexed. headers[":path"] = "/spam/eggs.html"; // A regular header which precedes ":" alphabetically, should still be encoded // after pseudo-headers. headers["-foo"] = "bar"; headers["foo"] = "bar"; headers["cookie"] = "c=dd"; // Pseudo-headers are encoded in alphabetical order. // This entry pushes "cookie: a=bb" back to 63. ExpectIndexedLiteral(peer_.table()->GetByName(":authority"), "www.example.com"); ExpectNonIndexedLiteral(":path", "/spam/eggs.html"); // Regular headers are endoded in alphabetical order. // This entry pushes "cookie: a=bb" back to 64. ExpectIndexedLiteral("-foo", "bar"); ExpectIndex(64); ExpectIndexedLiteral("foo", "bar"); CompareWithExpectedEncoding(headers); } TEST_F(HpackEncoderTest, CookieToCrumbs) { test::HpackEncoderPeer peer(NULL); std::vector out; // A space after ';' is consumed. All other spaces remain. ';' at beginning // and end of string produce empty crumbs. Duplicate crumbs are removed. // See section 8.1.3.4 "Compressing the Cookie Header Field" in the HTTP/2 // specification at http://tools.ietf.org/html/draft-ietf-httpbis-http2-11 peer.CookieToCrumbs(" foo=1;bar=2 ; bar=3; bing=4; ", &out); EXPECT_THAT(out, ElementsAre("", " bing=4", " foo=1", "bar=2 ", "bar=3")); peer.CookieToCrumbs(";;foo = bar ;; ;baz =bing", &out); EXPECT_THAT(out, ElementsAre("", "baz =bing", "foo = bar ")); peer.CookieToCrumbs("baz=bing; foo=bar; baz=bing", &out); EXPECT_THAT(out, ElementsAre("baz=bing", "foo=bar")); peer.CookieToCrumbs("baz=bing", &out); EXPECT_THAT(out, ElementsAre("baz=bing")); peer.CookieToCrumbs("", &out); EXPECT_THAT(out, ElementsAre("")); peer.CookieToCrumbs("foo;bar; baz;baz;bing;", &out); EXPECT_THAT(out, ElementsAre("", "bar", "baz", "bing", "foo")); } TEST_F(HpackEncoderTest, UpdateCharacterCounts) { std::vector counts(256, 0); size_t total_counts = 0; encoder_.SetCharCountsStorage(&counts, &total_counts); char kTestString[] = "foo\0\1\xff""boo"; peer_.UpdateCharacterCounts( StringPiece(kTestString, arraysize(kTestString) - 1)); std::vector expect(256, 0); expect[static_cast('f')] = 1; expect[static_cast('o')] = 4; expect[static_cast('\0')] = 1; expect[static_cast('\1')] = 1; expect[static_cast('\xff')] = 1; expect[static_cast('b')] = 1; EXPECT_EQ(expect, counts); EXPECT_EQ(9u, total_counts); } TEST_F(HpackEncoderTest, DecomposeRepresentation) { test::HpackEncoderPeer peer(NULL); std::vector out; peer.DecomposeRepresentation("", &out); EXPECT_THAT(out, ElementsAre("")); peer.DecomposeRepresentation("foobar", &out); EXPECT_THAT(out, ElementsAre("foobar")); peer.DecomposeRepresentation(StringPiece("foo\0bar", 7), &out); EXPECT_THAT(out, ElementsAre("foo", "bar")); peer.DecomposeRepresentation(StringPiece("\0foo\0bar", 8), &out); EXPECT_THAT(out, ElementsAre("", "foo", "bar")); peer.DecomposeRepresentation(StringPiece("foo\0bar\0", 8), &out); EXPECT_THAT(out, ElementsAre("foo", "bar", "")); peer.DecomposeRepresentation(StringPiece("\0foo\0bar\0", 9), &out); EXPECT_THAT(out, ElementsAre("", "foo", "bar", "")); } // Test that encoded headers do not have \0-delimited multiple values, as this // became disallowed in HTTP/2 draft-14. TEST_F(HpackEncoderTest, CrumbleNullByteDelimitedValue) { map headers; // A header field to be crumbled: "spam: foo\0bar". headers["spam"] = string("foo\0bar", 7); ExpectIndexedLiteral("spam", "foo"); expected_.AppendPrefix(kLiteralIncrementalIndexOpcode); expected_.AppendUint32(62); expected_.AppendPrefix(kStringLiteralIdentityEncoded); expected_.AppendUint32(3); expected_.AppendBytes("bar"); CompareWithExpectedEncoding(headers); } } // namespace } // namespace net