blob: 613e38555621c622fdcbadd197d5821755d2afa4 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/shared_dictionary/shared_dictionary_manager_on_disk.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/test/test_file_util.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "crypto/secure_hash.h"
#include "net/base/hash_value.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/base/network_isolation_key.h"
#include "net/base/schemeful_site.h"
#include "net/disk_cache/disk_cache.h"
#include "net/disk_cache/disk_cache_test_util.h"
#include "net/extras/shared_dictionary/shared_dictionary_info.h"
#include "net/http/http_response_headers.h"
#include "services/network/shared_dictionary/shared_dictionary.h"
#include "services/network/shared_dictionary/shared_dictionary_constants.h"
#include "services/network/shared_dictionary/shared_dictionary_disk_cache.h"
#include "services/network/shared_dictionary/shared_dictionary_manager_on_disk.h"
#include "services/network/shared_dictionary/shared_dictionary_storage.h"
#include "services/network/shared_dictionary/shared_dictionary_storage_on_disk.h"
#include "services/network/shared_dictionary/shared_dictionary_writer.h"
#include "sql/test/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace network {
namespace {
const GURL kUrl("https://origin.test/");
const net::SchemefulSite kSite(kUrl);
const std::string kTestData1 = "Hello world";
const std::string kTestData2 = "Bonjour le monde";
void WriteDictionary(SharedDictionaryStorage* storage,
const GURL& dictionary_url,
const std::string& match,
const std::string& data) {
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"/", match, "\"\n\n"}));
ASSERT_TRUE(headers);
scoped_refptr<SharedDictionaryWriter> writer =
storage->MaybeCreateWriter(dictionary_url, base::Time::Now(), *headers);
ASSERT_TRUE(writer);
writer->Append(data.c_str(), data.size());
writer->Finish();
}
bool DiskCacheEntryExists(SharedDictionaryManager* manager,
const base::UnguessableToken& disk_cache_key_token) {
TestEntryResultCompletionCallback open_callback;
disk_cache::EntryResult open_result = open_callback.GetResult(
static_cast<SharedDictionaryManagerOnDisk*>(manager)
->disk_cache()
.OpenOrCreateEntry(disk_cache_key_token.ToString(),
/*create=*/false, open_callback.callback()));
return open_result.net_error() == net::OK;
}
} // namespace
class SharedDictionaryManagerOnDiskTest : public ::testing::Test {
public:
SharedDictionaryManagerOnDiskTest() = default;
~SharedDictionaryManagerOnDiskTest() override = default;
SharedDictionaryManagerOnDiskTest(const SharedDictionaryManagerOnDiskTest&) =
delete;
SharedDictionaryManagerOnDiskTest& operator=(
const SharedDictionaryManagerOnDiskTest&) = delete;
void SetUp() override {
ASSERT_TRUE(tmp_directory_.CreateUniqueTempDir());
database_path_ = tmp_directory_.GetPath().Append(FILE_PATH_LITERAL("db"));
cache_directory_path_ =
tmp_directory_.GetPath().Append(FILE_PATH_LITERAL("cache"));
}
void TearDown() override { FlushCacheTasks(); }
protected:
std::unique_ptr<SharedDictionaryManager> CreateSharedDictionaryManager() {
return SharedDictionaryManager::CreateOnDisk(
database_path_, cache_directory_path_,
#if BUILDFLAG(IS_ANDROID)
/*app_status_listener=*/nullptr,
#endif // BUILDFLAG(IS_ANDROID)
/*file_operations_factory=*/nullptr);
}
const std::map<url::SchemeHostPort,
std::map<std::string, net::SharedDictionaryInfo>>&
GetOnDiskDictionaryMap(SharedDictionaryStorage* storage) {
return static_cast<SharedDictionaryStorageOnDisk*>(storage)
->GetDictionaryMapForTesting();
}
void FlushCacheTasks() {
disk_cache::FlushCacheThreadForTesting();
task_environment_.RunUntilIdle();
}
void CorruptDiskCache() {
// Corrupt the fake index file for the populated simple cache.
const base::FilePath index_file_path =
cache_directory_path_.Append(FILE_PATH_LITERAL("index"));
ASSERT_TRUE(base::WriteFile(index_file_path, "corrupted"));
file_permissions_restorer_ = std::make_unique<base::FilePermissionRestorer>(
tmp_directory_.GetPath());
// Mark the parent directory unwritable, so that we can't restore the dist
ASSERT_TRUE(base::MakeFileUnwritable(tmp_directory_.GetPath()));
}
void CorruptDatabase() {
CHECK(sql::test::CorruptSizeInHeader(database_path_));
}
private:
base::test::TaskEnvironment task_environment_;
base::ScopedTempDir tmp_directory_;
base::FilePath database_path_;
base::FilePath cache_directory_path_;
// `file_permissions_restorer_` must be below `tmp_directory_` to restore the
// file permission correctly.
std::unique_ptr<base::FilePermissionRestorer> file_permissions_restorer_;
};
TEST_F(SharedDictionaryManagerOnDiskTest, ReusingRefCountedSharedDictionary) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryStorageIsolationKey isolation_key(
url::Origin::Create(kUrl), kSite);
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
WriteDictionary(storage.get(), GURL("https://origin.test/dict"), "testfile*",
kTestData1);
FlushCacheTasks();
// Check the returned dictionary from GetDictionary().
std::unique_ptr<SharedDictionary> dict1 =
storage->GetDictionary(GURL("https://origin.test/testfile?1"));
ASSERT_TRUE(dict1);
{
base::RunLoop run_loop;
EXPECT_EQ(net::ERR_IO_PENDING,
dict1->ReadAll(base::BindLambdaForTesting([&](int rv) {
EXPECT_EQ(net::OK, rv);
run_loop.Quit();
})));
run_loop.Run();
}
std::unique_ptr<SharedDictionary> dict2 =
storage->GetDictionary(GURL("https://origin.test/testfile?2"));
ASSERT_TRUE(dict2);
// `dict2` shares the same RefCountedSharedDictionary with `dict1`. So
// ReadAll() must synchronously return OK.
EXPECT_EQ(net::OK, dict2->ReadAll(base::BindLambdaForTesting(
[&](int rv) { NOTREACHED(); })));
// `dict2` shares the same IOBuffer with `dict1`.
EXPECT_EQ(dict1->data(), dict2->data());
EXPECT_EQ(dict1->size(), dict2->size());
EXPECT_EQ(dict1->hash(), dict2->hash());
EXPECT_EQ(kTestData1,
std::string(reinterpret_cast<const char*>(dict1->data()->data()),
dict1->size()));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
MaybeCreateWriterAfterManagerDeleted) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryStorageIsolationKey isolation_key(
url::Origin::Create(kUrl), kSite);
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
manager.reset();
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"/testfile*\"\n\n"}));
ASSERT_TRUE(headers);
// MaybeCreateWriter() must return nullptr, after `manager` was deleted.
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
GURL("https://origin.test/dict"), base::Time::Now(), *headers);
EXPECT_FALSE(writer);
}
TEST_F(SharedDictionaryManagerOnDiskTest, GetDictionaryAfterManagerDeleted) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryStorageIsolationKey isolation_key(
url::Origin::Create(kUrl), kSite);
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
manager.reset();
// GetDictionary() must return nullptr, after `manager` was deleted.
std::unique_ptr<SharedDictionary> dict =
storage->GetDictionary(GURL("https://origin.test/testfile?1"));
EXPECT_FALSE(dict);
}
TEST_F(SharedDictionaryManagerOnDiskTest,
DictionaryWrittenInDiskCacheAfterManagerDeleted) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryStorageIsolationKey isolation_key(
url::Origin::Create(kUrl), kSite);
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin.test/dict"), "testfile*",
kTestData1);
// Test that deleting `manager` while writing the dictionary doesn't cause
// crash.
manager.reset();
FlushCacheTasks();
}
TEST_F(SharedDictionaryManagerOnDiskTest, OverridingDictionary) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryStorageIsolationKey isolation_key(
url::Origin::Create(kUrl), kSite);
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin.test/dict1"), "testfile*",
kTestData1);
FlushCacheTasks();
base::UnguessableToken disk_cache_key_token1;
{
const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get());
ASSERT_EQ(1u, dictionary_map.size());
ASSERT_EQ(1u, dictionary_map.begin()->second.size());
disk_cache_key_token1 =
dictionary_map.begin()->second.begin()->second.disk_cache_key_token();
}
// Check the returned dictionary from GetDictionary().
std::unique_ptr<SharedDictionary> dict1 =
storage->GetDictionary(GURL("https://origin.test/testfile"));
ASSERT_TRUE(dict1);
// The disk cache entry must exist.
EXPECT_TRUE(DiskCacheEntryExists(manager.get(), disk_cache_key_token1));
// Write different test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin.test/dict2"), "testfile*",
kTestData2);
FlushCacheTasks();
base::UnguessableToken disk_cache_key_token2;
{
const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get());
ASSERT_EQ(1u, dictionary_map.size());
ASSERT_EQ(1u, dictionary_map.begin()->second.size());
disk_cache_key_token2 =
dictionary_map.begin()->second.begin()->second.disk_cache_key_token();
}
EXPECT_NE(disk_cache_key_token1, disk_cache_key_token2);
// The disk cache entry should have been doomed.
EXPECT_FALSE(DiskCacheEntryExists(manager.get(), disk_cache_key_token1));
std::unique_ptr<SharedDictionary> dict2 =
storage->GetDictionary(GURL("https://origin.test/testfile"));
ASSERT_TRUE(dict2);
// We can read the new dictionary from `dict2`.
net::TestCompletionCallback read_callback2;
EXPECT_EQ(net::OK, read_callback2.GetResult(
dict2->ReadAll(read_callback2.callback())));
EXPECT_EQ(kTestData2,
std::string(reinterpret_cast<const char*>(dict2->data()->data()),
dict2->size()));
// We can still read the old dictionary from `dict1`.
net::TestCompletionCallback read_callback1;
EXPECT_EQ(net::OK, read_callback1.GetResult(
dict1->ReadAll(read_callback1.callback())));
EXPECT_EQ(kTestData1,
std::string(reinterpret_cast<const char*>(dict1->data()->data()),
dict1->size()));
}
TEST_F(SharedDictionaryManagerOnDiskTest, MultipleDictionaries) {
net::SharedDictionaryStorageIsolationKey isolation_key(
url::Origin::Create(kUrl), kSite);
{
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin.test/dict1"),
"testfile1*", kTestData1);
WriteDictionary(storage.get(), GURL("https://origin.test/dict2"),
"testfile2*", kTestData2);
FlushCacheTasks();
std::unique_ptr<SharedDictionary> dict1 =
storage->GetDictionary(GURL("https://origin.test/testfile1"));
ASSERT_TRUE(dict1);
std::unique_ptr<SharedDictionary> dict2 =
storage->GetDictionary(GURL("https://origin.test/testfile2"));
ASSERT_TRUE(dict2);
net::TestCompletionCallback read_callback1;
EXPECT_EQ(net::OK, read_callback1.GetResult(
dict1->ReadAll(read_callback1.callback())));
EXPECT_EQ(kTestData1,
std::string(reinterpret_cast<const char*>(dict1->data()->data()),
dict1->size()));
net::TestCompletionCallback read_callback2;
EXPECT_EQ(net::OK, read_callback2.GetResult(
dict2->ReadAll(read_callback2.callback())));
EXPECT_EQ(kTestData2,
std::string(reinterpret_cast<const char*>(dict2->data()->data()),
dict2->size()));
// Releasing `dict1`, `dict2`, `storage` and `manager`.
}
// The dictionaries must be available after recreating `manager`.
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
FlushCacheTasks();
const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get());
ASSERT_EQ(1u, dictionary_map.size());
ASSERT_EQ(2u, dictionary_map.begin()->second.size());
std::unique_ptr<SharedDictionary> dict1 =
storage->GetDictionary(GURL("https://origin.test/testfile1"));
ASSERT_TRUE(dict1);
std::unique_ptr<SharedDictionary> dict2 =
storage->GetDictionary(GURL("https://origin.test/testfile2"));
ASSERT_TRUE(dict2);
net::TestCompletionCallback read_callback1;
EXPECT_EQ(net::OK, read_callback1.GetResult(
dict1->ReadAll(read_callback1.callback())));
EXPECT_EQ(kTestData1,
std::string(reinterpret_cast<const char*>(dict1->data()->data()),
dict1->size()));
net::TestCompletionCallback read_callback2;
EXPECT_EQ(net::OK, read_callback2.GetResult(
dict2->ReadAll(read_callback2.callback())));
EXPECT_EQ(kTestData2,
std::string(reinterpret_cast<const char*>(dict2->data()->data()),
dict2->size()));
}
#if !BUILDFLAG(IS_FUCHSIA)
// Test that corruptted disk cache doesn't cause crash.
// CorruptDiskCache() doesn't work on Fuchsia. So disabling the following tests
// on Fuchsia.
TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDiskCache) {
net::SharedDictionaryStorageIsolationKey isolation_key(
url::Origin::Create(kUrl), kSite);
{
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin.test/dict1"),
"testfile1*", kTestData1);
FlushCacheTasks();
}
CorruptDiskCache();
{
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
FlushCacheTasks();
{
const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get());
ASSERT_EQ(1u, dictionary_map.size());
ASSERT_EQ(1u, dictionary_map.begin()->second.size());
}
WriteDictionary(storage.get(), GURL("https://origin.test/dict2"),
"testfile2*", kTestData2);
FlushCacheTasks();
// Currently, if the disk cache is corrupted, it just prevents adding new
// dictionaries.
// TODO(crbug.com/1413922): Implement a garbage collection logic to remove
// the entry in the database when its disk cache entry is unavailable.
{
const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get());
ASSERT_EQ(1u, dictionary_map.size());
ASSERT_EQ(1u, dictionary_map.begin()->second.size());
}
}
}
#endif // !BUILDFLAG(IS_FUCHSIA)
TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDatabase) {
net::SharedDictionaryStorageIsolationKey isolation_key(
url::Origin::Create(kUrl), kSite);
{
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin.test/dict"),
"testfile*", kTestData1);
FlushCacheTasks();
{
const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get());
ASSERT_EQ(1u, dictionary_map.size());
ASSERT_EQ(1u, dictionary_map.begin()->second.size());
}
}
CorruptDatabase();
{
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
FlushCacheTasks();
EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty());
WriteDictionary(storage.get(), GURL("https://origin.test/dict"),
"testfile*", kTestData1);
FlushCacheTasks();
// Can't add a new entry right after the databace corruption.
EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty());
}
// Test that database corruption can be recovered after reboot.
{
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
FlushCacheTasks();
EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty());
WriteDictionary(storage.get(), GURL("https://origin.test/dict"),
"testfile*", kTestData1);
FlushCacheTasks();
EXPECT_FALSE(GetOnDiskDictionaryMap(storage.get()).empty());
std::unique_ptr<SharedDictionary> dict =
storage->GetDictionary(GURL("https://origin.test/testfile"));
ASSERT_TRUE(dict);
// We can read the new dictionary.
net::TestCompletionCallback read_callback;
EXPECT_EQ(net::OK,
read_callback.GetResult(dict->ReadAll(read_callback.callback())));
EXPECT_EQ(kTestData1,
std::string(reinterpret_cast<const char*>(dict->data()->data()),
dict->size()));
// Currently the disk cache entries that were added before the database
// corruption will not be removed.
// TODO(crbug.com/1413922): Implement a garbage collection logic to remove
// the entry in the disk cache when its database entry is unavailable.
}
}
} // namespace network