blob: c46ba13dea8b5ab113fa6632430f1406788ac62b [file] [log] [blame]
// 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 <map>
#include <string>
#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<StringPiece>* 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<string, string>& 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<StringPiece> 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<size_t> 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<size_t> expect(256, 0);
expect[static_cast<uint8>('f')] = 1;
expect[static_cast<uint8>('o')] = 4;
expect[static_cast<uint8>('\0')] = 1;
expect[static_cast<uint8>('\1')] = 1;
expect[static_cast<uint8>('\xff')] = 1;
expect[static_cast<uint8>('b')] = 1;
EXPECT_EQ(expect, counts);
EXPECT_EQ(9u, total_counts);
}
} // namespace
} // namespace net