// 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()); } bool allow_huffman_compression() { return encoder_->allow_huffman_compression_; } 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(make_pair("", cookie), &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_) {} virtual void SetUp() { static_ = peer_.table()->GetByIndex(1); // 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(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(HpackEntry* entry) { return peer_.table()->IndexOf(entry); } HpackEncoder encoder_; test::HpackEncoderPeer peer_; HpackEntry* static_; HpackEntry* key_1_; HpackEntry* key_2_; HpackEntry* cookie_a_; HpackEntry* cookie_c_; HpackOutputStream expected_; }; TEST_F(HpackEncoderTest, SingleDynamicIndex) { ExpectIndex(IndexOf(key_2_)); map headers; headers[key_2_->name()] = key_2_->value(); CompareWithExpectedEncoding(headers); // |key_2_| was added to the reference set. EXPECT_THAT(peer_.table()->reference_set(), ElementsAre(key_2_)); } TEST_F(HpackEncoderTest, SingleStaticIndex) { ExpectIndex(IndexOf(static_)); map headers; headers[static_->name()] = static_->value(); CompareWithExpectedEncoding(headers); // A new entry copying |static_| was inserted and added to the reference set. HpackEntry* new_entry = &peer_.table_peer().dynamic_entries()->front(); EXPECT_NE(static_, new_entry); EXPECT_EQ(static_->name(), new_entry->name()); EXPECT_EQ(static_->value(), new_entry->value()); EXPECT_THAT(peer_.table()->reference_set(), ElementsAre(new_entry)); } 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()); EXPECT_EQ(0u, peer_.table()->reference_set().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"); EXPECT_THAT(peer_.table()->reference_set(), ElementsAre(new_entry)); } TEST_F(HpackEncoderTest, SingleLiteralWithLiteralName) { ExpectIndexedLiteral("key3", "value3"); map headers; headers["key3"] = "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(), "key3"); EXPECT_EQ(new_entry->value(), "value3"); EXPECT_THAT(peer_.table()->reference_set(), ElementsAre(new_entry)); } 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()); EXPECT_EQ(0u, peer_.table()->reference_set().size()); } TEST_F(HpackEncoderTest, SingleInReferenceSet) { peer_.table()->Toggle(key_2_); // Nothing is emitted. map headers; headers[key_2_->name()] = key_2_->value(); CompareWithExpectedEncoding(headers); } TEST_F(HpackEncoderTest, ExplicitToggleOff) { peer_.table()->Toggle(key_1_); peer_.table()->Toggle(key_2_); // |key_1_| is explicitly toggled off. ExpectIndex(IndexOf(key_1_)); map headers; headers[key_2_->name()] = key_2_->value(); CompareWithExpectedEncoding(headers); } TEST_F(HpackEncoderTest, ImplicitToggleOff) { peer_.table()->Toggle(key_1_); peer_.table()->Toggle(key_2_); // |key_1_| is evicted. No explicit toggle required. ExpectIndexedLiteral("key3", "value3"); map headers; headers[key_2_->name()] = key_2_->value(); headers["key3"] = "value3"; CompareWithExpectedEncoding(headers); } TEST_F(HpackEncoderTest, ExplicitDoubleToggle) { peer_.table()->Toggle(key_1_); // |key_1_| is double-toggled prior to being evicted. ExpectIndex(IndexOf(key_1_)); ExpectIndex(IndexOf(key_1_)); ExpectIndexedLiteral("key3", "value3"); map headers; headers[key_1_->name()] = key_1_->value(); headers["key3"] = "value3"; CompareWithExpectedEncoding(headers); } 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) { peer_.table()->Toggle(cookie_a_); // |cookie_a_| is already in the reference set. |cookie_c_| is // toggled, and "e=ff" is emitted with an indexed name. 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: key_1_ and cookie_a_ are toggled on. { map headers; headers["key1"] = "value1"; headers["cookie"] = "a=bb"; ExpectIndex(IndexOf(cookie_a_)); ExpectIndex(IndexOf(key_1_)); CompareWithExpectedEncoding(headers); } // Pass 2: |key_1_| is double-toggled and evicted. // |key_2_| & |cookie_c_| are toggled on. // |cookie_a_| is toggled off. // A new cookie entry is added. { map headers; headers["key1"] = "value1"; headers["key2"] = "value2"; headers["cookie"] = "c=dd; e=ff"; ExpectIndex(IndexOf(cookie_c_)); // Toggle on. ExpectIndex(IndexOf(key_1_)); // Double-toggle before eviction. ExpectIndex(IndexOf(key_1_)); ExpectIndexedLiteral(peer_.table()->GetByName("cookie"), "e=ff"); ExpectIndex(IndexOf(key_2_) + 1); // Toggle on. Add 1 to reflect insertion. ExpectIndex(IndexOf(cookie_a_) + 1); // Toggle off. CompareWithExpectedEncoding(headers); } // Pass 3: |key_2_| is evicted and implicitly toggled off. // |cookie_c_| is explicitly toggled off. // "key1" is re-inserted. { map headers; headers["key1"] = "value1"; headers["key3"] = "value3"; headers["cookie"] = "e=ff"; ExpectIndexedLiteral("key1", "value1"); ExpectIndexedLiteral("key3", "value3"); ExpectIndex(IndexOf(cookie_c_) + 2); // Toggle off. Add 1 for insertion. 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); } } // namespace } // namespace net