blob: 3dd7298d2a97bd952889be429b73e5bd170b9d5a [file] [log] [blame]
// Copyright 2019 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 "services/data_decoder/bundled_exchanges_parser.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/optional.h"
#include "base/path_service.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_task_environment.h"
#include "components/cbor/writer.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace data_decoder {
namespace {
std::string GetTestFileContents(const base::FilePath& path) {
base::FilePath test_data_dir;
base::PathService::Get(base::DIR_SOURCE_ROOT, &test_data_dir);
test_data_dir = test_data_dir.Append(base::FilePath(
FILE_PATH_LITERAL("services/test/data/bundled_exchanges")));
std::string contents;
EXPECT_TRUE(base::ReadFileToString(test_data_dir.Append(path), &contents));
return contents;
}
class TestDataSource : public mojom::BundleDataSource {
public:
explicit TestDataSource(const base::FilePath& path)
: data_(GetTestFileContents(path)) {}
explicit TestDataSource(const std::vector<uint8_t>& data)
: data_(reinterpret_cast<const char*>(data.data()), data.size()) {}
void GetSize(GetSizeCallback callback) override {
std::move(callback).Run(data_.size());
}
void Read(uint64_t offset, uint64_t length, ReadCallback callback) override {
if (offset + length > data_.size())
std::move(callback).Run(base::nullopt);
const uint8_t* start =
reinterpret_cast<const uint8_t*>(data_.data()) + offset;
std::move(callback).Run(std::vector<uint8_t>(start, start + length));
}
base::StringPiece GetPayload(const mojom::BundleResponsePtr& response) {
return base::StringPiece(data_).substr(response->payload_offset,
response->payload_length);
}
void AddReceiver(mojo::PendingReceiver<mojom::BundleDataSource> receiver) {
receivers_.Add(this, std::move(receiver));
}
private:
std::string data_;
mojo::ReceiverSet<mojom::BundleDataSource> receivers_;
};
mojom::BundleMetadataPtr ParseBundle(TestDataSource* data_source) {
mojo::PendingRemote<mojom::BundleDataSource> source_remote;
data_source->AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());
mojo::PendingRemote<mojom::BundledExchangesParser> parser_remote;
BundledExchangesParser parser_impl(
parser_remote.InitWithNewPipeAndPassReceiver(), std::move(source_remote));
data_decoder::mojom::BundledExchangesParser& parser = parser_impl;
base::RunLoop run_loop;
mojom::BundleMetadataPtr result;
parser.ParseMetadata(base::BindLambdaForTesting(
[&result, &run_loop](mojom::BundleMetadataPtr metadata,
const base::Optional<std::string>& error) {
result = std::move(metadata);
run_loop.QuitClosure().Run();
}));
run_loop.Run();
return result;
}
mojom::BundleResponsePtr ParseResponse(TestDataSource* data_source,
const mojom::BundleIndexItemPtr& item) {
mojo::PendingRemote<mojom::BundleDataSource> source_remote;
data_source->AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());
mojo::PendingRemote<mojom::BundledExchangesParser> parser_remote;
BundledExchangesParser parser_impl(
parser_remote.InitWithNewPipeAndPassReceiver(), std::move(source_remote));
data_decoder::mojom::BundledExchangesParser& parser = parser_impl;
base::RunLoop run_loop;
mojom::BundleResponsePtr result;
parser.ParseResponse(
item->response_offset, item->response_length,
base::BindLambdaForTesting(
[&result, &run_loop](mojom::BundleResponsePtr response,
const base::Optional<std::string>& error) {
result = std::move(response);
run_loop.QuitClosure().Run();
}));
run_loop.Run();
return result;
}
class BundleBuilder {
public:
using Headers = std::vector<std::pair<std::string, std::string>>;
void AddExchange(const Headers& request_headers,
const Headers& response_headers,
base::StringPiece payload) {
cbor::Value::ArrayValue response_array;
response_array.emplace_back(
*cbor::Writer::Write(CreateHeaderMap(response_headers)));
response_array.emplace_back(CreateByteString(payload));
cbor::Value response(response_array);
index_.emplace_back(CreateHeaderMap(request_headers));
index_.emplace_back(EncodedLength(response));
responses_.emplace_back(std::move(response));
}
void AddSection(base::StringPiece name, cbor::Value section) {
section_lengths_.emplace_back(name);
section_lengths_.emplace_back(EncodedLength(section));
sections_.emplace_back(std::move(section));
}
std::vector<uint8_t> CreateBundle() {
AddSection("index", cbor::Value(index_));
AddSection("responses", cbor::Value(responses_));
return *cbor::Writer::Write(CreateTopLevel());
}
private:
static cbor::Value CreateByteString(base::StringPiece s) {
return cbor::Value(base::as_bytes(base::make_span(s)));
}
static cbor::Value CreateHeaderMap(const Headers& headers) {
cbor::Value::MapValue map;
for (const auto& pair : headers)
map.insert({CreateByteString(pair.first), CreateByteString(pair.second)});
return cbor::Value(std::move(map));
}
cbor::Value CreateTopLevel() {
cbor::Value::ArrayValue toplevel_array;
toplevel_array.emplace_back(
CreateByteString(u8"\U0001F310\U0001F4E6")); // "🌐📦"
toplevel_array.emplace_back(
*cbor::Writer::Write(cbor::Value(section_lengths_)));
toplevel_array.emplace_back(sections_);
toplevel_array.emplace_back(CreateByteString("")); // length (ignored)
return cbor::Value(toplevel_array);
}
static int64_t EncodedLength(const cbor::Value& value) {
return cbor::Writer::Write(value)->size();
}
cbor::Value::ArrayValue section_lengths_;
cbor::Value::ArrayValue sections_;
cbor::Value::ArrayValue index_;
cbor::Value::ArrayValue responses_;
};
} // namespace
class BundledExchangeParserTest : public testing::Test {
private:
base::test::ScopedTaskEnvironment task_environment_;
};
TEST_F(BundledExchangeParserTest, EmptyBundle) {
BundleBuilder builder;
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_TRUE(metadata);
const auto& index = metadata->index;
ASSERT_EQ(index.size(), 0u);
}
TEST_F(BundledExchangeParserTest, WrongMagic) {
BundleBuilder builder;
std::vector<uint8_t> bundle = builder.CreateBundle();
bundle[3] ^= 1;
TestDataSource data_source(bundle);
ASSERT_FALSE(ParseBundle(&data_source));
}
TEST_F(BundledExchangeParserTest, SectionLengthsTooLarge) {
BundleBuilder builder;
std::string too_long_section_name(8192, 'x');
builder.AddSection(too_long_section_name, cbor::Value(0));
TestDataSource data_source(builder.CreateBundle());
ASSERT_FALSE(ParseBundle(&data_source));
}
TEST_F(BundledExchangeParserTest, DuplicateSectionName) {
BundleBuilder builder;
builder.AddSection("foo", cbor::Value(0));
builder.AddSection("foo", cbor::Value(0));
TestDataSource data_source(builder.CreateBundle());
ASSERT_FALSE(ParseBundle(&data_source));
}
TEST_F(BundledExchangeParserTest, SingleEntry) {
BundleBuilder builder;
builder.AddExchange(
{{":method", "GET"}, {":url", "https://test.example.com/"}},
{{":status", "200"}, {"content-type", "text/plain"}}, "payload");
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_TRUE(metadata);
const auto& index = metadata->index;
ASSERT_EQ(index.size(), 1u);
auto response = ParseResponse(&data_source, index[0]);
ASSERT_TRUE(response);
EXPECT_EQ(index[0]->request_url, "https://test.example.com/");
EXPECT_EQ(index[0]->request_method, "GET");
EXPECT_EQ(index[0]->request_headers.size(), 0u);
EXPECT_EQ(response->response_code, 200);
EXPECT_EQ(response->response_headers.size(), 1u);
EXPECT_EQ(response->response_headers["content-type"], "text/plain");
EXPECT_EQ(data_source.GetPayload(response), "payload");
}
TEST_F(BundledExchangeParserTest, InvalidRequestURL) {
BundleBuilder builder;
builder.AddExchange({{":method", "GET"}, {":url", ""}},
{{":status", "200"}, {"content-type", "text/plain"}},
"payload");
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_FALSE(metadata);
}
TEST_F(BundledExchangeParserTest, RequestURLHasCredentials) {
BundleBuilder builder;
builder.AddExchange(
{{":method", "GET"}, {":url", "https://user:passwd@test.example.com/"}},
{{":status", "200"}, {"content-type", "text/plain"}}, "payload");
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_FALSE(metadata);
}
TEST_F(BundledExchangeParserTest, RequestURLHasFragment) {
BundleBuilder builder;
builder.AddExchange(
{{":method", "GET"}, {":url", "https://test.example.com/#fragment"}},
{{":status", "200"}, {"content-type", "text/plain"}}, "payload");
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_FALSE(metadata);
}
TEST_F(BundledExchangeParserTest, NoMethodInRequestHeaders) {
BundleBuilder builder;
builder.AddExchange(
{{":url", "https://test.example.com/"}}, // ":method" is missing.
{{":status", "200"}, {"content-type", "text/plain"}}, "payload");
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_FALSE(metadata);
}
TEST_F(BundledExchangeParserTest, MethodIsNotGET) {
BundleBuilder builder;
builder.AddExchange(
{{":method", "POST"}, {":url", "https://test.example.com/"}},
{{":status", "200"}, {"content-type", "text/plain"}}, "payload");
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_FALSE(metadata);
}
TEST_F(BundledExchangeParserTest, ExtraPseudoInRequestHeaders) {
BundleBuilder builder;
builder.AddExchange({{":method", "GET"},
{":url", "https://test.example.com/"},
{":scheme", "https"}},
{{":status", "200"}, {"content-type", "text/plain"}},
"payload");
TestDataSource data_source(builder.CreateBundle());
ASSERT_FALSE(ParseBundle(&data_source));
}
TEST_F(BundledExchangeParserTest, NoStatusInResponseHeaders) {
BundleBuilder builder;
builder.AddExchange(
{{":method", "GET"}, {":url", "https://test.example.com/"}},
{{"content-type", "text/plain"}}, "payload"); // ":status" is missing.
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_TRUE(metadata);
ASSERT_EQ(metadata->index.size(), 1u);
ASSERT_FALSE(ParseResponse(&data_source, metadata->index[0]));
}
TEST_F(BundledExchangeParserTest, InvalidResponseStatus) {
BundleBuilder builder;
builder.AddExchange(
{{":method", "GET"}, {":url", "https://test.example.com/"}},
{{":status", "0200"}, {"content-type", "text/plain"}}, "payload");
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_TRUE(metadata);
ASSERT_EQ(metadata->index.size(), 1u);
ASSERT_FALSE(ParseResponse(&data_source, metadata->index[0]));
}
TEST_F(BundledExchangeParserTest, ExtraPseudoInResponseHeaders) {
BundleBuilder builder;
builder.AddExchange(
{{":method", "GET"}, {":url", "https://test.example.com/"}},
{{":status", "200"}, {":foo", ""}, {"content-type", "text/plain"}},
"payload");
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_TRUE(metadata);
ASSERT_EQ(metadata->index.size(), 1u);
ASSERT_FALSE(ParseResponse(&data_source, metadata->index[0]));
}
TEST_F(BundledExchangeParserTest, UpperCaseCharacterInHeaderName) {
BundleBuilder builder;
builder.AddExchange(
{{":method", "GET"}, {":url", "https://test.example.com/"}},
{{":status", "200"}, {"Content-Type", "text/plain"}}, "payload");
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_TRUE(metadata);
ASSERT_EQ(metadata->index.size(), 1u);
ASSERT_FALSE(ParseResponse(&data_source, metadata->index[0]));
}
TEST_F(BundledExchangeParserTest, InvalidHeaderValue) {
BundleBuilder builder;
builder.AddExchange(
{{":method", "GET"}, {":url", "https://test.example.com/"}},
{{":status", "200"}, {"content-type", "\n"}}, "payload");
TestDataSource data_source(builder.CreateBundle());
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_TRUE(metadata);
ASSERT_EQ(metadata->index.size(), 1u);
ASSERT_FALSE(ParseResponse(&data_source, metadata->index[0]));
}
TEST_F(BundledExchangeParserTest, ParseGoldenFile) {
TestDataSource data_source(base::FilePath(FILE_PATH_LITERAL("hello.wbn")));
mojom::BundleMetadataPtr metadata = ParseBundle(&data_source);
ASSERT_TRUE(metadata);
ASSERT_EQ(metadata->index.size(), 4u);
const auto& index = metadata->index;
std::vector<mojom::BundleResponsePtr> responses;
for (size_t i = 0; i < index.size(); i++) {
responses.push_back(ParseResponse(&data_source, index[i]));
ASSERT_TRUE(responses.back());
}
EXPECT_EQ(index[0]->request_url, "https://test.example.org/");
EXPECT_EQ(index[0]->request_method, "GET");
EXPECT_EQ(index[0]->request_headers.size(), 0u);
EXPECT_EQ(responses[0]->response_code, 200);
EXPECT_EQ(responses[0]->response_headers["content-type"],
"text/html; charset=utf-8");
EXPECT_EQ(data_source.GetPayload(responses[0]),
GetTestFileContents(
base::FilePath(FILE_PATH_LITERAL("hello/index.html"))));
EXPECT_EQ(index[1]->request_url, "https://test.example.org/index.html");
EXPECT_EQ(index[2]->request_url,
"https://test.example.org/manifest.webmanifest");
EXPECT_EQ(index[3]->request_url, "https://test.example.org/script.js");
}
} // namespace data_decoder