blob: 79ea606ccb74c2eef02f6043e009e65e0bf009b0 [file] [log] [blame]
// Copyright 2016 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 <string>
#include "base/bind.h"
#include "base/bit_cast.h"
#include "base/callback.h"
#include "base/memory/ptr_util.h"
#include "net/base/io_buffer.h"
#include "net/base/test_completion_callback.h"
#include "net/filter/gzip_source_stream.h"
#include "net/filter/mock_source_stream.h"
#include "net/filter/sdch_source_stream.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/zlib/zlib.h"
namespace net {
namespace {
const size_t kBufferSize = 4096;
const size_t kSmallBufferSize = 1;
// Provide sample data and compression results with a sample VCDIFF dictionary.
// Note an SDCH dictionary has extra meta-data before the VCDIFF dictionary.
static const char kTestVcdiffDictionary[] =
"DictionaryFor"
"SdchCompression1SdchCompression2SdchCompression3SdchCompression\n";
// Pre-compression test data. Note that we pad with a lot of highly gzip
// compressible content to help to exercise the chaining pipeline. That is why
// there are a PILE of zeros at the start and end.
static const char kTestData[] =
"0000000000000000000000000000000000000000000000"
"0000000000000000000000000000TestData "
"SdchCompression1SdchCompression2SdchCompression3SdchCompression"
"00000000000000000000000000000000000000000000000000000000000000000000000000"
"000000000000000000000000000000000000000\n";
static const char kSdchCompressedTestData[] =
"\326\303\304\0\0\001M\0\201S\202\004\0\201E\006\001"
"00000000000000000000000000000000000000000000000000000000000000000000000000"
"TestData 00000000000000000000000000000000000000000000000000000000000000000"
"000000000000000000000000000000000000000000000000\n\001S\023\077\001r\r";
// Helper function to perform gzip compression of data.
static std::string gzip_compress(const std::string& input,
size_t input_size,
size_t* output_size) {
z_stream zlib_stream;
memset(&zlib_stream, 0, sizeof(zlib_stream));
int code;
// Initialize zlib
code =
deflateInit2(&zlib_stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS,
8, // DEF_MEM_LEVEL
Z_DEFAULT_STRATEGY);
CHECK_EQ(Z_OK, code);
// Fill in zlib control block
zlib_stream.next_in = bit_cast<Bytef*>(input.data());
zlib_stream.avail_in = input_size;
// Assume we can compress into similar buffer (add 100 bytes to be sure).
size_t gzip_compressed_length = zlib_stream.avail_in + 100;
std::unique_ptr<char[]> gzip_compressed(new char[gzip_compressed_length]);
zlib_stream.next_out = bit_cast<Bytef*>(gzip_compressed.get());
zlib_stream.avail_out = gzip_compressed_length;
// The GZIP header (see RFC 1952):
// +---+---+---+---+---+---+---+---+---+---+
// |ID1|ID2|CM |FLG| MTIME |XFL|OS |
// +---+---+---+---+---+---+---+---+---+---+
// ID1 \037
// ID2 \213
// CM \010 (compression method == DEFLATE)
// FLG \000 (special flags that we do not support)
// MTIME Unix format modification time (0 means not available)
// XFL 2-4? DEFLATE flags
// OS ???? Operating system indicator (255 means unknown)
//
// Header value we generate:
const char kGZipHeader[] = {'\037', '\213', '\010', '\000', '\000',
'\000', '\000', '\000', '\002', '\377'};
CHECK_GT(zlib_stream.avail_out, sizeof(kGZipHeader));
memcpy(zlib_stream.next_out, kGZipHeader, sizeof(kGZipHeader));
zlib_stream.next_out += sizeof(kGZipHeader);
zlib_stream.avail_out -= sizeof(kGZipHeader);
// Do deflate
code = deflate(&zlib_stream, Z_FINISH);
gzip_compressed_length -= zlib_stream.avail_out;
std::string compressed(gzip_compressed.get(), gzip_compressed_length);
deflateEnd(&zlib_stream);
*output_size = gzip_compressed_length;
return compressed;
}
class MockDelegate : public SdchSourceStream::Delegate {
public:
MockDelegate(const std::string& test_dictionary_id,
const std::string& test_dictionary_text,
SdchSourceStream::Delegate::ErrorRecovery error_recover,
const std::string& replace_output)
: dictionary_id_error_handled_(false),
get_dictionary_error_handled_(false),
decoding_error_handled_(false),
test_dictionary_id_(test_dictionary_id),
test_dictionary_text_(test_dictionary_text),
error_recover_(error_recover),
replace_output_(replace_output) {}
// SdchSourceStream::Delegate implementation.
SdchSourceStream::Delegate::ErrorRecovery OnDictionaryIdError(
std::string* replace_output) override {
dictionary_id_error_handled_ = true;
if (error_recover_ == REPLACE_OUTPUT)
*replace_output = replace_output_;
return error_recover_;
}
SdchSourceStream::Delegate::ErrorRecovery OnGetDictionaryError(
std::string* replace_output) override {
get_dictionary_error_handled_ = true;
if (error_recover_ == REPLACE_OUTPUT)
*replace_output = replace_output_;
return error_recover_;
}
SdchSourceStream::Delegate::ErrorRecovery OnDecodingError(
std::string* replace_output) override {
decoding_error_handled_ = true;
if (error_recover_ == REPLACE_OUTPUT)
*replace_output = replace_output_;
return error_recover_;
}
bool OnGetDictionary(const std::string& server_id,
const std::string** text) override {
last_get_dictionary_id_ = server_id;
if (server_id == test_dictionary_id_) {
*text = &test_dictionary_text_;
return true;
}
return false;
}
void OnStreamDestroyed(SdchSourceStream::InputState input_state,
bool buffered_output_present,
bool decoding_not_finished) override {}
bool dictionary_id_error_handled() { return dictionary_id_error_handled_; }
bool get_dictionary_error_handled() { return get_dictionary_error_handled_; }
bool decoding_error_handled() { return decoding_error_handled_; }
std::string last_get_dictionary_id() { return last_get_dictionary_id_; }
private:
std::string last_get_dictionary_id_;
bool dictionary_id_error_handled_;
bool get_dictionary_error_handled_;
bool decoding_error_handled_;
std::string test_dictionary_id_;
std::string test_dictionary_text_;
SdchSourceStream::Delegate::ErrorRecovery error_recover_;
std::string replace_output_;
DISALLOW_COPY_AND_ASSIGN(MockDelegate);
};
} // namespace
class SdchSourceStreamTest
: public ::testing::TestWithParam<MockSourceStream::Mode> {
public:
SdchSourceStreamTest() : out_buffer_size_(kBufferSize) {}
void Init() {
out_buffer_ = new IOBufferWithSize(out_buffer_size_);
std::unique_ptr<MockSourceStream> source(new MockSourceStream);
source_ = source.get();
std::unique_ptr<MockDelegate> delegate(GetNewDelegate());
delegate_ = delegate.get();
sdch_source_.reset(new SdchSourceStream(
std::move(source), std::move(delegate), SourceStream::TYPE_SDCH));
}
// If MockSourceStream::Mode is ASYNC, completes 1 read from
// |mock_stream| and wait for |callback| to complete. If Mode is not ASYNC,
// does nothing and returns |previous_result|.
int CompleteReadIfAsync(int previous_result,
TestCompletionCallback* callback,
MockSourceStream* mock_stream) {
if (GetParam() == MockSourceStream::ASYNC) {
EXPECT_EQ(ERR_IO_PENDING, previous_result);
mock_stream->CompleteNextRead();
return callback->WaitForResult();
}
return previous_result;
}
int CompleteReadIfAsync(int previous_result,
TestCompletionCallback* callback,
MockSourceStream* mock_stream,
int num_reads) {
if (GetParam() == MockSourceStream::ASYNC) {
EXPECT_EQ(ERR_IO_PENDING, previous_result);
while (num_reads > 0) {
mock_stream->CompleteNextRead();
num_reads--;
}
return callback->WaitForResult();
}
return previous_result;
}
IOBuffer* out_buffer() { return out_buffer_.get(); }
char* out_data() { return out_buffer_->data(); }
size_t out_buffer_size() { return out_buffer_size_; }
MockSourceStream* mock_source() { return source_; }
SdchSourceStream* sdch_source() { return sdch_source_.get(); }
int ReadStream(const TestCompletionCallback& callback) {
return sdch_source()->Read(out_buffer(), out_buffer_size(),
callback.callback());
}
void set_out_buffer_size(int out_buffer_size) {
out_buffer_size_ = out_buffer_size;
}
void SetTestDictionary(const std::string& dictionary_id,
const std::string& dictionary_text) {
test_dictionary_id_ = dictionary_id;
test_dictionary_text_ = dictionary_text;
}
void SetErrorRecovery(SdchSourceStream::Delegate::ErrorRecovery error_recover,
const std::string& replace_output) {
error_recover_ = error_recover;
replace_output_ = replace_output;
}
void AppendDictionaryIdTo(std::string* resp, std::string* server_id) {
std::string client_id;
SdchManager::GenerateHash(kTestVcdiffDictionary, &client_id, server_id);
SetTestDictionary(*server_id, kTestVcdiffDictionary);
std::string response(server_id->data(), server_id->size());
response.append("\0");
resp->append(response.data(), server_id->size() + 1);
}
MockDelegate* delegate() { return delegate_; }
// Gets a new MockDelegate and take ownership of it.
std::unique_ptr<MockDelegate> GetNewDelegate() {
return base::WrapUnique(new MockDelegate(test_dictionary_id_,
test_dictionary_text_,
error_recover_, replace_output_));
}
private:
// Owned by |sdch_source_|.
MockSourceStream* source_;
MockDelegate* delegate_;
std::unique_ptr<SdchSourceStream> sdch_source_;
scoped_refptr<IOBufferWithSize> out_buffer_;
int out_buffer_size_;
std::string test_dictionary_id_;
std::string test_dictionary_text_;
SdchSourceStream::Delegate::ErrorRecovery error_recover_;
std::string replace_output_;
DISALLOW_COPY_AND_ASSIGN(SdchSourceStreamTest);
};
TEST(SdchSourceStreamTest, GetTypeAsString) {
SourceStream::SourceType types[] = {SourceStream::TYPE_SDCH_POSSIBLE,
SourceStream::TYPE_SDCH};
for (auto type : types) {
std::unique_ptr<MockSourceStream> mock_source(new MockSourceStream());
std::unique_ptr<MockDelegate> dummy_delegate(
new MockDelegate("", "", SdchSourceStream::Delegate::NONE, ""));
SdchSourceStream stream(std::move(mock_source), std::move(dummy_delegate),
type);
EXPECT_EQ(
type == SourceStream::TYPE_SDCH_POSSIBLE ? "SDCH_POSSIBLE" : "SDCH",
stream.Description());
}
}
INSTANTIATE_TEST_CASE_P(SdchSourceStreamTests,
SdchSourceStreamTest,
::testing::Values(MockSourceStream::SYNC,
MockSourceStream::ASYNC));
TEST_P(SdchSourceStreamTest, EmptyStream) {
Init();
mock_source()->AddReadResult("", 0, OK, GetParam());
TestCompletionCallback callback;
int result = ReadStream(callback);
result = CompleteReadIfAsync(result, &callback, mock_source());
EXPECT_EQ(OK, result);
EXPECT_EQ("SDCH", sdch_source()->Description());
}
// Ensure that GetDictionary() is not called at all if the SDCH dictionary ID is
// malformed.
TEST_P(SdchSourceStreamTest, BogusDictionaryId) {
char id[] = {0x1f, '0', '0', '0', '0', '0', '0', '0', 0x0};
SetTestDictionary(id, "...");
SetErrorRecovery(SdchSourceStream::Delegate::PASS_THROUGH, std::string());
Init();
mock_source()->AddReadResult(id, sizeof(id), OK, GetParam());
TestCompletionCallback callback;
int result = ReadStream(callback);
result = CompleteReadIfAsync(result, &callback, mock_source());
EXPECT_TRUE(delegate()->dictionary_id_error_handled());
EXPECT_EQ("", delegate()->last_get_dictionary_id());
EXPECT_EQ(9, result);
EXPECT_EQ(0, memcmp(id, out_data(), result));
EXPECT_EQ("SDCH", sdch_source()->Description());
}
// When encounter a dictionary error, delegate returns ErrorRecovery NONE.
TEST_P(SdchSourceStreamTest, BogusDictionaryIdNoRecover) {
char id[] = {0x1f, '0', '0', '0', '0', '0', '0', '0', 0x0};
SetTestDictionary(id, "...");
SetErrorRecovery(SdchSourceStream::Delegate::NONE, std::string());
Init();
mock_source()->AddReadResult(id, sizeof(id), OK, GetParam());
TestCompletionCallback callback;
int result = ReadStream(callback);
result = CompleteReadIfAsync(result, &callback, mock_source());
EXPECT_TRUE(delegate()->dictionary_id_error_handled());
EXPECT_EQ("", delegate()->last_get_dictionary_id());
EXPECT_EQ(ERR_CONTENT_DECODING_FAILED, result);
EXPECT_EQ("SDCH", sdch_source()->Description());
}
// Ensure that the stream's dictionary error handler is called if GetDictionary
// returns no dictionary.
TEST_P(SdchSourceStreamTest, NoDictionaryError) {
char id[] = "00000000";
SetErrorRecovery(SdchSourceStream::Delegate::PASS_THROUGH, std::string());
Init();
mock_source()->AddReadResult(id, sizeof(id), OK, GetParam());
TestCompletionCallback callback;
int result = ReadStream(callback);
result = CompleteReadIfAsync(result, &callback, mock_source());
EXPECT_EQ(9, result);
EXPECT_TRUE(delegate()->get_dictionary_error_handled());
EXPECT_EQ(id, delegate()->last_get_dictionary_id());
EXPECT_EQ(0, memcmp(id, out_data(), result));
EXPECT_EQ("SDCH", sdch_source()->Description());
}
TEST_P(SdchSourceStreamTest, DictionaryLoaded) {
std::string response;
std::string server_id;
AppendDictionaryIdTo(&response, &server_id);
Init();
mock_source()->AddReadResult(response.data(), response.size(), OK,
GetParam());
mock_source()->AddReadResult(response.data(), 0, OK, MockSourceStream::SYNC);
TestCompletionCallback callback;
int rv = ReadStream(callback);
rv = CompleteReadIfAsync(rv, &callback, mock_source());
// Decoded response should be empty.
EXPECT_EQ(0, rv);
EXPECT_FALSE(delegate()->get_dictionary_error_handled());
EXPECT_EQ(server_id, delegate()->last_get_dictionary_id());
EXPECT_EQ("SDCH", sdch_source()->Description());
}
TEST_P(SdchSourceStreamTest, DecompressOneBlock) {
std::string response;
std::string server_id;
AppendDictionaryIdTo(&response, &server_id);
Init();
response.append(kSdchCompressedTestData, sizeof(kSdchCompressedTestData) - 1);
mock_source()->AddReadResult(response.data(), response.size(), OK,
GetParam());
TestCompletionCallback callback;
int rv = ReadStream(callback);
rv = CompleteReadIfAsync(rv, &callback, mock_source());
EXPECT_FALSE(delegate()->decoding_error_handled());
EXPECT_EQ(server_id, delegate()->last_get_dictionary_id());
EXPECT_EQ(static_cast<int>(sizeof(kTestData) - 1), rv);
EXPECT_EQ(0, memcmp(kTestData, out_data(), rv));
EXPECT_EQ("SDCH", sdch_source()->Description());
}
TEST_P(SdchSourceStreamTest, DecompressWithSmallOutputBuffer) {
set_out_buffer_size(kSmallBufferSize);
std::string response;
std::string server_id;
AppendDictionaryIdTo(&response, &server_id);
Init();
response.append(kSdchCompressedTestData, sizeof(kSdchCompressedTestData) - 1);
mock_source()->AddReadResult(response.data(), response.size(), OK,
GetParam());
// Add a 0 byte read to signal EOF.
mock_source()->AddReadResult(kSdchCompressedTestData, 0, OK,
MockSourceStream::SYNC);
std::string actual_output;
while (true) {
TestCompletionCallback callback;
int rv = ReadStream(callback);
if (rv == ERR_IO_PENDING)
rv = CompleteReadIfAsync(rv, &callback, mock_source());
if (rv == OK)
break;
ASSERT_GT(rv, OK);
EXPECT_GE(kSmallBufferSize, static_cast<size_t>(rv));
actual_output.append(out_data(), rv);
}
EXPECT_FALSE(delegate()->decoding_error_handled());
EXPECT_EQ(server_id, delegate()->last_get_dictionary_id());
EXPECT_EQ(sizeof(kTestData) - 1, actual_output.size());
EXPECT_EQ(kTestData, actual_output);
EXPECT_EQ("SDCH", sdch_source()->Description());
}
TEST_P(SdchSourceStreamTest, DecompressWithSmallInputBuffer) {
std::string response;
std::string server_id;
AppendDictionaryIdTo(&response, &server_id);
Init();
response.append(kSdchCompressedTestData, sizeof(kSdchCompressedTestData) - 1);
// Add a sequence of small reads.
for (size_t i = 0; i < response.size(); i++) {
mock_source()->AddReadResult(response.data() + i, 1, OK,
MockSourceStream::SYNC);
}
// Add a 0 byte read to signal EOF.
mock_source()->AddReadResult(kSdchCompressedTestData, 0, OK,
MockSourceStream::SYNC);
std::string actual_output;
while (true) {
TestCompletionCallback callback;
int rv = ReadStream(callback);
if (rv == ERR_IO_PENDING)
rv = CompleteReadIfAsync(rv, &callback, mock_source());
if (rv == OK)
break;
actual_output.append(out_data(), rv);
}
EXPECT_FALSE(delegate()->decoding_error_handled());
EXPECT_EQ(server_id, delegate()->last_get_dictionary_id());
EXPECT_EQ(sizeof(kTestData) - 1, actual_output.size());
EXPECT_EQ(kTestData, actual_output);
EXPECT_EQ("SDCH", sdch_source()->Description());
}
TEST_P(SdchSourceStreamTest, DecompressTwoBlocks) {
std::string response;
std::string server_id;
AppendDictionaryIdTo(&response, &server_id);
Init();
response.append(kSdchCompressedTestData, sizeof(kSdchCompressedTestData) - 1);
mock_source()->AddReadResult(response.data(), 32, OK, GetParam());
mock_source()->AddReadResult(response.data() + 32, response.size() - 32, OK,
GetParam());
mock_source()->AddReadResult(kSdchCompressedTestData, 0, OK,
MockSourceStream::SYNC);
std::string actual_output;
while (true) {
TestCompletionCallback callback;
int rv = ReadStream(callback);
if (rv == ERR_IO_PENDING)
rv = CompleteReadIfAsync(rv, &callback, mock_source(), 2);
if (rv == OK)
break;
ASSERT_GT(rv, OK);
actual_output.append(out_data(), rv);
}
EXPECT_FALSE(delegate()->decoding_error_handled());
EXPECT_EQ(server_id, delegate()->last_get_dictionary_id());
EXPECT_EQ(sizeof(kTestData) - 1, actual_output.size());
EXPECT_EQ(kTestData, actual_output);
EXPECT_EQ("SDCH", sdch_source()->Description());
}
// Test that filters can be cascaded (chained) so that the output of one filter
// is processed by the next one. This is most critical for SDCH, which is
// routinely followed by gzip (during encoding). The filter we'll test for will
// do the gzip decoding first, and then decode the SDCH content.
TEST_P(SdchSourceStreamTest, FilterChaining) {
int out_buffer_sizes[] = {kBufferSize, kSmallBufferSize};
for (auto out_buffer_size : out_buffer_sizes) {
set_out_buffer_size(out_buffer_size);
std::string sdch_response;
std::string server_id;
AppendDictionaryIdTo(&sdch_response, &server_id);
Init();
sdch_response.append(kSdchCompressedTestData,
sizeof(kSdchCompressedTestData) - 1);
size_t expected_length =
server_id.length() + sizeof(kSdchCompressedTestData);
size_t gzip_length;
std::string gzip_compressed_sdch =
gzip_compress(sdch_response, expected_length, &gzip_length);
MockSourceStream* source = new MockSourceStream;
source->AddReadResult(gzip_compressed_sdch.data(), gzip_length, OK,
GetParam());
// Add a 0 byte read to signal EOF.
source->AddReadResult(gzip_compressed_sdch.data(), 0, OK,
MockSourceStream::SYNC);
std::unique_ptr<GzipSourceStream> gzip_source = GzipSourceStream::Create(
base::WrapUnique(source), SourceStream::TYPE_GZIP);
std::unique_ptr<MockDelegate> delegate = GetNewDelegate();
MockDelegate* raw_delegate_pointer = delegate.get();
std::unique_ptr<SdchSourceStream> sdch_source(new SdchSourceStream(
std::move(gzip_source), std::move(delegate), SourceStream::TYPE_SDCH));
std::string actual_output;
while (true) {
TestCompletionCallback callback;
int rv =
sdch_source->Read(out_buffer(), out_buffer_size, callback.callback());
if (rv == ERR_IO_PENDING)
rv = CompleteReadIfAsync(rv, &callback, source);
if (rv == OK)
break;
ASSERT_GT(rv, OK);
if (out_buffer_size == kSmallBufferSize)
EXPECT_GE(kSmallBufferSize, static_cast<size_t>(rv));
actual_output.append(out_data(), rv);
}
EXPECT_FALSE(raw_delegate_pointer->decoding_error_handled());
EXPECT_EQ(server_id, raw_delegate_pointer->last_get_dictionary_id());
EXPECT_EQ(sizeof(kTestData) - 1, actual_output.size());
EXPECT_EQ(kTestData, actual_output);
EXPECT_EQ("GZIP,SDCH", sdch_source->Description());
}
}
// Test that if TYPE_SDCH_POSSIBLE and TYPE_GZIP_FALLBACK are added to a
// gzipped content, the content can be decoded without problem.
TEST_P(SdchSourceStreamTest, PossibleSdchActuallyGzip) {
int out_buffer_sizes[] = {kBufferSize, kSmallBufferSize};
for (auto out_buffer_size : out_buffer_sizes) {
char plain_data[] = "Hello, World!";
unsigned char gzip_data[] = {
// From:
// echo -n 'Hello, World!' | gzip | xxd -i | sed -e 's/^/ /'
// with the 8 footer bytes removed.
0x1f, 0x8b, 0x08, 0x00, 0x2b, 0x02, 0x84, 0x55, 0x00,
0x03, 0xf3, 0x48, 0xcd, 0xc9, 0xc9, 0xd7, 0x51, 0x08,
0xcf, 0x2f, 0xca, 0x49, 0x51, 0x04, 0x00};
SetErrorRecovery(SdchSourceStream::Delegate::PASS_THROUGH, std::string());
Init();
std::unique_ptr<MockDelegate> delegate = GetNewDelegate();
MockDelegate* raw_delegate_pointer = delegate.get();
MockSourceStream* source = new MockSourceStream();
source->AddReadResult(reinterpret_cast<char*>(gzip_data), sizeof(gzip_data),
OK, GetParam());
source->AddReadResult(
reinterpret_cast<char*>(gzip_data) + sizeof(gzip_data), 0, OK,
MockSourceStream::SYNC);
std::unique_ptr<GzipSourceStream> gzip_source = GzipSourceStream::Create(
base::WrapUnique(source), SourceStream::TYPE_GZIP);
std::unique_ptr<GzipSourceStream> gzip_fallback_source =
GzipSourceStream::Create(std::move(gzip_source),
SourceStream::TYPE_GZIP_FALLBACK);
std::unique_ptr<SdchSourceStream> sdch_possible(new SdchSourceStream(
std::move(gzip_fallback_source), std::move(delegate),
SourceStream::TYPE_SDCH_POSSIBLE));
std::string actual_output;
while (true) {
TestCompletionCallback callback;
int rv = sdch_possible->Read(out_buffer(), out_buffer_size,
callback.callback());
if (rv == ERR_IO_PENDING)
rv = CompleteReadIfAsync(rv, &callback, source);
if (rv == OK)
break;
ASSERT_GT(rv, OK);
if (out_buffer_size == kSmallBufferSize)
EXPECT_GE(kSmallBufferSize, static_cast<size_t>(rv));
actual_output.append(out_data(), rv);
}
EXPECT_TRUE(raw_delegate_pointer->dictionary_id_error_handled());
EXPECT_EQ(plain_data, actual_output);
EXPECT_EQ("GZIP,GZIP_FALLBACK,SDCH_POSSIBLE", sdch_possible->Description());
}
}
} // namespace net