blob: 7efdf19feacaa1a8666b4dc6e9468b9974df82c2 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/indexed_db/instance/backing_store.h"
#include <stddef.h>
#include <stdint.h>
#include <array>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include "base/barrier_closure.h"
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/synchronization/waitable_event_watcher.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/uuid.h"
#include "components/services/storage/indexed_db/locks/partitioned_lock_manager.h"
#include "components/services/storage/indexed_db/scopes/varint_coding.h"
#include "components/services/storage/indexed_db/transactional_leveldb/leveldb_write_batch.h"
#include "components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_database.h"
#include "components/services/storage/privileged/mojom/indexed_db_control.mojom-test-utils.h"
#include "components/services/storage/public/cpp/buckets/bucket_locator.h"
#include "content/browser/indexed_db/indexed_db_leveldb_coding.h"
#include "content/browser/indexed_db/indexed_db_leveldb_operations.h"
#include "content/browser/indexed_db/indexed_db_value.h"
#include "content/browser/indexed_db/instance/bucket_context.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/base/features.h"
#include "net/base/schemeful_site.h"
#include "storage/browser/quota/special_storage_policy.h"
#include "storage/browser/test/fake_blob.h"
#include "storage/browser/test/mock_quota_manager.h"
#include "storage/browser/test/mock_quota_manager_proxy.h"
#include "storage/browser/test/mock_special_storage_policy.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom.h"
using blink::IndexedDBDatabaseMetadata;
using blink::IndexedDBIndexMetadata;
using blink::IndexedDBKey;
using blink::IndexedDBKeyPath;
using blink::IndexedDBKeyRange;
using blink::IndexedDBObjectStoreMetadata;
using blink::StorageKey;
using url::Origin;
namespace content::indexed_db {
struct BlobWrite {
BlobWrite() = default;
BlobWrite(BlobWrite&& other) {
blob = std::move(other.blob);
path = std::move(other.path);
}
BlobWrite(mojo::PendingRemote<::blink::mojom::Blob> blob, base::FilePath path)
: blob(std::move(blob)), path(path) {}
~BlobWrite() = default;
int64_t GetBlobNumber() const {
int64_t result;
EXPECT_TRUE(base::StringToInt64(path.BaseName().AsUTF8Unsafe(), &result));
return result;
}
mojo::Remote<::blink::mojom::Blob> blob;
base::FilePath path;
};
class MockBlobStorageContext : public ::storage::mojom::BlobStorageContext {
public:
~MockBlobStorageContext() override = default;
void RegisterFromDataItem(mojo::PendingReceiver<::blink::mojom::Blob> blob,
const std::string& uuid,
storage::mojom::BlobDataItemPtr item) override {
NOTREACHED();
}
void RegisterFromMemory(mojo::PendingReceiver<::blink::mojom::Blob> blob,
const std::string& uuid,
::mojo_base::BigBuffer data) override {
NOTREACHED();
}
void WriteBlobToFile(mojo::PendingRemote<::blink::mojom::Blob> blob,
const base::FilePath& path,
bool flush_on_write,
std::optional<base::Time> last_modified,
WriteBlobToFileCallback callback) override {
writes_.emplace_back(std::move(blob), path);
if (write_files_to_disk_) {
base::ImportantFileWriter::WriteFileAtomically(path, "fake contents");
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback),
storage::mojom::WriteBlobToFileResult::kSuccess));
}
void Clone(mojo::PendingReceiver<::storage::mojom::BlobStorageContext>
receiver) override {
receivers_.Add(this, std::move(receiver));
}
const std::vector<BlobWrite>& writes() { return writes_; }
void ClearWrites() { writes_.clear(); }
// If true, writes a fake file for each blob file to disk.
// The contents are bogus, but the files will exist.
void SetWriteFilesToDisk(bool write) { write_files_to_disk_ = write; }
private:
std::vector<BlobWrite> writes_;
bool write_files_to_disk_ = false;
mojo::ReceiverSet<::storage::mojom::BlobStorageContext> receivers_;
};
class FakeFileSystemAccessTransferToken
: public ::blink::mojom::FileSystemAccessTransferToken {
public:
explicit FakeFileSystemAccessTransferToken(const base::UnguessableToken& id)
: id_(id) {}
void GetInternalID(GetInternalIDCallback callback) override {
std::move(callback).Run(id_);
}
void Clone(mojo::PendingReceiver<blink::mojom::FileSystemAccessTransferToken>
clone_receiver) override {
mojo::MakeSelfOwnedReceiver(
std::make_unique<FakeFileSystemAccessTransferToken>(id_),
std::move(clone_receiver));
}
private:
base::UnguessableToken id_;
};
class MockFileSystemAccessContext
: public ::storage::mojom::FileSystemAccessContext {
public:
~MockFileSystemAccessContext() override = default;
void SerializeHandle(
mojo::PendingRemote<::blink::mojom::FileSystemAccessTransferToken>
pending_token,
SerializeHandleCallback callback) override {
writes_.emplace_back(std::move(pending_token));
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
std::move(callback),
std::vector<uint8_t>{static_cast<uint8_t>(writes_.size() - 1)}));
}
void DeserializeHandle(
const blink::StorageKey& storage_key,
const std::vector<uint8_t>& bits,
mojo::PendingReceiver<::blink::mojom::FileSystemAccessTransferToken>
token) override {
NOTREACHED();
}
void Clone(mojo::PendingReceiver<::storage::mojom::FileSystemAccessContext>
receiver) override {
receivers_.Add(this, std::move(receiver));
}
const std::vector<
mojo::Remote<::blink::mojom::FileSystemAccessTransferToken>>&
writes() {
return writes_;
}
void ClearWrites() { writes_.clear(); }
private:
std::vector<mojo::Remote<::blink::mojom::FileSystemAccessTransferToken>>
writes_;
mojo::ReceiverSet<::storage::mojom::FileSystemAccessContext> receivers_;
};
class BackingStoreTest : public testing::Test {
public:
BackingStoreTest() = default;
BackingStoreTest(const BackingStoreTest&) = delete;
BackingStoreTest& operator=(const BackingStoreTest&) = delete;
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
blob_context_ = std::make_unique<MockBlobStorageContext>();
file_system_access_context_ =
std::make_unique<MockFileSystemAccessContext>();
quota_manager_ = base::MakeRefCounted<storage::MockQuotaManager>(
/*is_incognito=*/false, temp_dir_.GetPath(),
base::SingleThreadTaskRunner::GetCurrentDefault(), nullptr);
quota_manager_proxy_ = base::MakeRefCounted<storage::MockQuotaManagerProxy>(
quota_manager_.get(),
base::SingleThreadTaskRunner::GetCurrentDefault());
CreateFactoryAndBackingStore();
// useful keys and values during tests
value1_ = IndexedDBValue("value1", {});
value2_ = IndexedDBValue("value2", {});
key1_ = IndexedDBKey(99, blink::mojom::IDBKeyType::Number);
key2_ = IndexedDBKey(u"key2");
}
void CreateFactoryAndBackingStore() {
const blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting("http://localhost:81");
auto bucket_info = storage::BucketInfo();
bucket_info.id = storage::BucketId::FromUnsafeValue(1);
bucket_info.storage_key = storage_key;
bucket_info.name = storage::kDefaultBucketName;
auto bucket_locator = bucket_info.ToBucketLocator();
mojo::PendingRemote<storage::mojom::BlobStorageContext>
blob_storage_context;
blob_context_->Clone(blob_storage_context.InitWithNewPipeAndPassReceiver());
mojo::PendingRemote<storage::mojom::FileSystemAccessContext> fsa_context;
file_system_access_context_->Clone(
fsa_context.InitWithNewPipeAndPassReceiver());
bucket_context_ = std::make_unique<BucketContext>(
bucket_info, temp_dir_.GetPath(), BucketContext::Delegate(),
scoped_refptr<base::UpdateableSequencedTaskRunner>(),
quota_manager_proxy_, std::move(blob_storage_context),
std::move(fsa_context));
std::tie(std::ignore, std::ignore, data_loss_info_) =
bucket_context_->InitBackingStoreIfNeeded(/*create_if_missing=*/true);
backing_store_ = bucket_context_->backing_store();
}
std::vector<PartitionedLock> CreateDummyLock() {
base::RunLoop loop;
PartitionedLockHolder locks_receiver;
bucket_context_->lock_manager().AcquireLocks(
{{{0, "01"}, PartitionedLockManager::LockType::kShared}},
locks_receiver, base::BindLambdaForTesting([&loop]() { loop.Quit(); }));
loop.Run();
return std::move(locks_receiver.locks);
}
void DestroyFactoryAndBackingStore() {
backing_store_ = nullptr;
bucket_context_ = nullptr;
}
void TearDown() override {
DestroyFactoryAndBackingStore();
if (temp_dir_.IsValid()) {
ASSERT_TRUE(temp_dir_.Delete());
}
}
BackingStore* backing_store() { return backing_store_; }
protected:
base::test::TaskEnvironment task_environment_;
base::ScopedTempDir temp_dir_;
std::unique_ptr<MockBlobStorageContext> blob_context_;
std::unique_ptr<MockFileSystemAccessContext> file_system_access_context_;
scoped_refptr<storage::MockQuotaManager> quota_manager_;
scoped_refptr<storage::MockQuotaManagerProxy> quota_manager_proxy_;
std::unique_ptr<BucketContext> bucket_context_;
raw_ptr<BackingStore> backing_store_ = nullptr;
IndexedDBDataLossInfo data_loss_info_;
// Sample keys and values that are consistent.
IndexedDBKey key1_;
IndexedDBKey key2_;
IndexedDBValue value1_;
IndexedDBValue value2_;
};
class BackingStoreTestForThirdPartyStoragePartitioning
: public testing::WithParamInterface<bool>,
public BackingStoreTest {
public:
BackingStoreTestForThirdPartyStoragePartitioning() {
scoped_feature_list_.InitWithFeatureState(
net::features::kThirdPartyStoragePartitioning,
IsThirdPartyStoragePartitioningEnabled());
}
bool IsThirdPartyStoragePartitioningEnabled() { return GetParam(); }
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
BackingStoreTestForThirdPartyStoragePartitioning,
testing::Bool());
enum class ExternalObjectTestType {
kOnlyBlobs,
kOnlyFileSystemAccessHandles,
kBlobsAndFileSystemAccessHandles
};
class BackingStoreTestWithExternalObjects
: public testing::WithParamInterface<ExternalObjectTestType>,
public BackingStoreTest {
public:
BackingStoreTestWithExternalObjects() = default;
BackingStoreTestWithExternalObjects(
const BackingStoreTestWithExternalObjects&) = delete;
BackingStoreTestWithExternalObjects& operator=(
const BackingStoreTestWithExternalObjects&) = delete;
virtual ExternalObjectTestType TestType() { return GetParam(); }
bool IncludesBlobs() {
return TestType() != ExternalObjectTestType::kOnlyFileSystemAccessHandles;
}
bool IncludesFileSystemAccessHandles() {
return TestType() != ExternalObjectTestType::kOnlyBlobs;
}
void SetUp() override {
BackingStoreTest::SetUp();
const int64_t kTime1 = 13255919133000000ll;
const int64_t kTime2 = 13287455133000000ll;
// useful keys and values during tests
if (IncludesBlobs()) {
external_objects_.push_back(CreateBlobInfo(u"blob type", 1));
external_objects_.push_back(CreateBlobInfo(
u"file name", u"file type",
base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(kTime1)),
kBlobFileData1.size()));
external_objects_.push_back(CreateBlobInfo(
u"file name", u"file type",
base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(kTime2)),
kBlobFileData2.size()));
}
if (IncludesFileSystemAccessHandles()) {
external_objects_.push_back(CreateFileSystemAccessHandle());
external_objects_.push_back(CreateFileSystemAccessHandle());
}
value3_ = IndexedDBValue("value3", external_objects_);
key3_ = IndexedDBKey(u"key3");
}
IndexedDBExternalObject CreateBlobInfo(const std::u16string& file_name,
const std::u16string& type,
base::Time last_modified,
int64_t size) {
auto uuid = base::Uuid::GenerateRandomV4().AsLowercaseString();
mojo::PendingRemote<blink::mojom::Blob> remote;
base::ThreadPool::CreateSequencedTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
[](std::string uuid,
mojo::PendingReceiver<blink::mojom::Blob> pending_receiver) {
mojo::MakeSelfOwnedReceiver(
std::make_unique<storage::FakeBlob>(uuid),
std::move(pending_receiver));
},
uuid, remote.InitWithNewPipeAndPassReceiver()));
IndexedDBExternalObject info(std::move(remote), file_name, type,
last_modified, size);
return info;
}
IndexedDBExternalObject CreateBlobInfo(const std::u16string& type,
int64_t size) {
auto uuid = base::Uuid::GenerateRandomV4().AsLowercaseString();
mojo::PendingRemote<blink::mojom::Blob> remote;
base::ThreadPool::CreateSequencedTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
[](std::string uuid,
mojo::PendingReceiver<blink::mojom::Blob> pending_receiver) {
mojo::MakeSelfOwnedReceiver(
std::make_unique<storage::FakeBlob>(uuid),
std::move(pending_receiver));
},
uuid, remote.InitWithNewPipeAndPassReceiver()));
IndexedDBExternalObject info(std::move(remote), type, size);
return info;
}
IndexedDBExternalObject CreateFileSystemAccessHandle() {
auto id = base::UnguessableToken::Create();
mojo::PendingRemote<blink::mojom::FileSystemAccessTransferToken> remote;
base::ThreadPool::CreateSequencedTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
[](base::UnguessableToken id,
mojo::PendingReceiver<
blink::mojom::FileSystemAccessTransferToken>
pending_receiver) {
mojo::MakeSelfOwnedReceiver(
std::make_unique<FakeFileSystemAccessTransferToken>(id),
std::move(pending_receiver));
},
id, remote.InitWithNewPipeAndPassReceiver()));
IndexedDBExternalObject info(std::move(remote));
return info;
}
// This just checks the data that survive getting stored and recalled, e.g.
// the file path and UUID will change and thus aren't verified.
bool CheckBlobInfoMatches(
const std::vector<IndexedDBExternalObject>& reads) const {
if (external_objects_.size() != reads.size()) {
EXPECT_EQ(external_objects_.size(), reads.size());
return false;
}
for (size_t i = 0; i < external_objects_.size(); ++i) {
const IndexedDBExternalObject& a = external_objects_[i];
const IndexedDBExternalObject& b = reads[i];
if (a.object_type() != b.object_type()) {
EXPECT_EQ(a.object_type(), b.object_type());
return false;
}
switch (a.object_type()) {
case IndexedDBExternalObject::ObjectType::kFile:
if (a.file_name() != b.file_name()) {
EXPECT_EQ(a.file_name(), b.file_name());
return false;
}
if (a.last_modified() != b.last_modified()) {
EXPECT_EQ(a.last_modified(), b.last_modified());
return false;
}
[[fallthrough]];
case IndexedDBExternalObject::ObjectType::kBlob:
if (a.type() != b.type()) {
EXPECT_EQ(a.type(), b.type());
return false;
}
if (a.size() != b.size()) {
EXPECT_EQ(a.size(), b.size());
return false;
}
break;
case IndexedDBExternalObject::ObjectType::kFileSystemAccessHandle:
if (b.serialized_file_system_access_handle().empty()) {
EXPECT_FALSE(b.serialized_file_system_access_handle().empty());
return false;
}
break;
}
}
return true;
}
bool CheckBlobReadsMatchWrites(
const std::vector<IndexedDBExternalObject>& reads) const {
if (blob_context_->writes().size() +
file_system_access_context_->writes().size() !=
reads.size()) {
return false;
}
std::set<base::FilePath> ids;
for (const auto& write : blob_context_->writes()) {
ids.insert(write.path);
}
if (ids.size() != blob_context_->writes().size()) {
return false;
}
for (const auto& read : reads) {
switch (read.object_type()) {
case IndexedDBExternalObject::ObjectType::kBlob:
case IndexedDBExternalObject::ObjectType::kFile:
if (ids.count(read.indexed_db_file_path()) != 1) {
return false;
}
break;
case IndexedDBExternalObject::ObjectType::kFileSystemAccessHandle:
if (read.serialized_file_system_access_handle().size() != 1 ||
read.serialized_file_system_access_handle()[0] >
file_system_access_context_->writes().size()) {
return false;
}
break;
}
}
return true;
}
bool CheckBlobWrites() {
size_t num_empty_blobs = 0;
for (const auto& info : external_objects_) {
if (info.object_type() == IndexedDBExternalObject::ObjectType::kFile &&
!info.size()) {
num_empty_blobs++;
}
}
size_t num_written = blob_context_->writes().size() +
file_system_access_context_->writes().size();
if (num_written != external_objects_.size() - num_empty_blobs) {
return false;
}
for (size_t i = 0; i < blob_context_->writes().size(); ++i) {
const BlobWrite& desc = blob_context_->writes()[i];
const IndexedDBExternalObject& info = external_objects_[i];
if (!info.size()) {
continue;
}
DCHECK(desc.blob.is_bound());
DCHECK(desc.blob.is_connected());
}
for (size_t i = 0; i < file_system_access_context_->writes().size(); ++i) {
const IndexedDBExternalObject& info =
external_objects_[blob_context_->writes().size() + i];
base::UnguessableToken info_token;
{
base::RunLoop loop;
info.file_system_access_token_remote()->GetInternalID(
base::BindLambdaForTesting(
[&](const base::UnguessableToken& token) {
info_token = token;
loop.Quit();
}));
loop.Run();
}
base::UnguessableToken written_token;
{
base::RunLoop loop;
file_system_access_context_->writes()[i]->GetInternalID(
base::BindLambdaForTesting(
[&](const base::UnguessableToken& token) {
written_token = token;
loop.Quit();
}));
loop.Run();
}
if (info_token != written_token) {
EXPECT_EQ(info_token, written_token);
return false;
}
}
return true;
}
void VerifyNumBlobsRemoved(int deleted_count) {
#if DCHECK_IS_ON()
EXPECT_EQ(deleted_count + removed_blobs_count_,
backing_store()->NumBlobFilesDeletedForTesting());
removed_blobs_count_ += deleted_count;
#endif
}
void CheckFirstNBlobsRemoved(size_t deleted_count) {
VerifyNumBlobsRemoved(deleted_count);
for (size_t i = 0; i < deleted_count; ++i) {
EXPECT_FALSE(base::PathExists(blob_context_->writes()[i].path));
}
}
std::vector<IndexedDBExternalObject>& external_objects() {
return external_objects_;
}
// Sample keys and values that are consistent. Public so that posted
// lambdas passed `this` can access them.
IndexedDBKey key3_;
IndexedDBValue value3_;
protected:
const std::string kBlobFileData1 = "asdfgasdf";
const std::string kBlobFileData2 = "aaaaaa";
private:
// Blob details referenced by `value3_`. The various CheckBlob*() methods
// can be used to verify the state as a test progresses.
std::vector<IndexedDBExternalObject> external_objects_;
std::vector<std::string> blob_remote_uuids_;
#if DCHECK_IS_ON()
// Number of blob deletions previously counted by a call to
// `VerifyNumBlobsRemoved()`.
int removed_blobs_count_ = 0;
#endif
};
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
BackingStoreTestWithExternalObjects,
::testing::Values(
ExternalObjectTestType::kOnlyBlobs,
ExternalObjectTestType::kOnlyFileSystemAccessHandles,
ExternalObjectTestType::kBlobsAndFileSystemAccessHandles));
class BackingStoreTestWithBlobs : public BackingStoreTestWithExternalObjects {
public:
ExternalObjectTestType TestType() override {
return ExternalObjectTestType::kOnlyBlobs;
}
};
BlobWriteCallback CreateBlobWriteCallback(
bool* succeeded,
base::OnceClosure on_done = base::OnceClosure()) {
*succeeded = false;
return base::BindOnce(
[](bool* succeeded, base::OnceClosure on_done, BlobWriteResult result,
storage::mojom::WriteBlobToFileResult error) {
switch (result) {
case BlobWriteResult::kFailure:
NOTREACHED();
case BlobWriteResult::kRunPhaseTwoAsync:
case BlobWriteResult::kRunPhaseTwoAndReturnResult:
DCHECK_EQ(error, storage::mojom::WriteBlobToFileResult::kSuccess);
*succeeded = true;
break;
}
if (!on_done.is_null()) {
std::move(on_done).Run();
}
return Status::OK();
},
succeeded, std::move(on_done));
}
TEST_F(BackingStoreTest, PutGetConsistency) {
base::RunLoop loop;
const IndexedDBKey key = key1_;
IndexedDBValue value = value1_;
{
BackingStore::Transaction transaction1(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1.Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
Status s =
backing_store()->PutRecord(&transaction1, 1, 1, key, &value, &record);
EXPECT_TRUE(s.ok());
bool succeeded = false;
EXPECT_TRUE(
transaction1.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction1.CommitPhaseTwo().ok());
}
{
BackingStore::Transaction transaction2(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction2.Begin(CreateDummyLock());
IndexedDBValue result_value;
EXPECT_TRUE(backing_store()
->GetRecord(&transaction2, 1, 1, key, &result_value)
.ok());
bool succeeded = false;
EXPECT_TRUE(
transaction2.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction2.CommitPhaseTwo().ok());
EXPECT_EQ(value.bits, result_value.bits);
}
}
TEST_P(BackingStoreTestWithExternalObjects, PutGetConsistency) {
// Initiate transaction1 - writing blobs.
std::unique_ptr<BackingStore::Transaction> transaction1 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1->Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
EXPECT_TRUE(
backing_store()
->PutRecord(transaction1.get(), 1, 1, key3_, &value3_, &record)
.ok());
bool succeeded = false;
base::RunLoop phase_one_wait;
EXPECT_TRUE(transaction1
->CommitPhaseOne(CreateBlobWriteCallback(
&succeeded, phase_one_wait.QuitClosure()))
.ok());
EXPECT_FALSE(succeeded);
task_environment_.RunUntilIdle();
phase_one_wait.Run();
// Finish up transaction1, verifying blob writes.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(CheckBlobWrites());
EXPECT_TRUE(transaction1->CommitPhaseTwo().ok());
// Initiate transaction2, reading blobs.
BackingStore::Transaction transaction2(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction2.Begin(CreateDummyLock());
IndexedDBValue result_value;
EXPECT_TRUE(backing_store()
->GetRecord(&transaction2, 1, 1, key3_, &result_value)
.ok());
// Finish up transaction2, verifying blob reads.
succeeded = false;
EXPECT_TRUE(
transaction2.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction2.CommitPhaseTwo().ok());
EXPECT_EQ(value3_.bits, result_value.bits);
task_environment_.RunUntilIdle();
EXPECT_TRUE(CheckBlobInfoMatches(result_value.external_objects));
EXPECT_TRUE(CheckBlobReadsMatchWrites(result_value.external_objects));
// Initiate transaction3, deleting blobs.
std::unique_ptr<BackingStore::Transaction> transaction3 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction3->Begin(CreateDummyLock());
EXPECT_TRUE(
backing_store()
->DeleteRange(transaction3.get(), 1, 1, IndexedDBKeyRange(key3_))
.ok());
succeeded = false;
EXPECT_TRUE(
transaction3->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
task_environment_.RunUntilIdle();
EXPECT_TRUE(succeeded);
// Finish up transaction 3, verifying blob deletes.
EXPECT_TRUE(transaction3->CommitPhaseTwo().ok());
CheckFirstNBlobsRemoved(blob_context_->writes().size());
// Clean up on the IDB sequence.
transaction1.reset();
transaction3.reset();
task_environment_.RunUntilIdle();
}
// http://crbug.com/1131151
// Validate that recovery journal cleanup during a transaction does
// not delete blobs that were just written.
TEST_P(BackingStoreTestWithExternalObjects, BlobWriteCleanup) {
const std::vector<IndexedDBKey> keys = {
IndexedDBKey(u"key0"), IndexedDBKey(u"key1"), IndexedDBKey(u"key2"),
IndexedDBKey(u"key3")};
const int64_t database_id = 1;
const int64_t object_store_id = 1;
external_objects().clear();
for (size_t j = 0; j < 4; ++j) {
std::string type = "type " + base::NumberToString(j);
external_objects().push_back(CreateBlobInfo(base::UTF8ToUTF16(type), 1));
}
std::vector<IndexedDBValue> values = {
IndexedDBValue("value0", {external_objects()[0]}),
IndexedDBValue("value1", {external_objects()[1]}),
IndexedDBValue("value2", {external_objects()[2]}),
IndexedDBValue("value3", {external_objects()[3]}),
};
ASSERT_GE(keys.size(), values.size());
// Validate that cleaning up after writing blobs does not delete those
// blobs.
backing_store()->SetExecuteJournalCleaningOnNoTransactionsForTesting();
std::unique_ptr<BackingStore::Transaction> transaction1 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1->Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
for (size_t i = 0; i < values.size(); ++i) {
EXPECT_TRUE(backing_store()
->PutRecord(transaction1.get(), database_id,
object_store_id, keys[i], &values[i], &record)
.ok());
}
// Start committing transaction1.
bool succeeded = false;
EXPECT_TRUE(
transaction1->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
EXPECT_TRUE(CheckBlobWrites());
// Finish committing transaction1.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction1->CommitPhaseTwo().ok());
// Verify lack of blob removals.
VerifyNumBlobsRemoved(0);
// Clean up on the IDB sequence.
transaction1.reset();
task_environment_.RunUntilIdle();
}
TEST_P(BackingStoreTestWithExternalObjects, DeleteRange) {
const std::vector<IndexedDBKey> keys = {
IndexedDBKey(u"key0"), IndexedDBKey(u"key1"), IndexedDBKey(u"key2"),
IndexedDBKey(u"key3")};
const auto ranges = std::to_array({
IndexedDBKeyRange(keys[1], keys[2], false, false),
IndexedDBKeyRange(keys[1], keys[2], false, false),
IndexedDBKeyRange(keys[0], keys[2], true, false),
IndexedDBKeyRange(keys[1], keys[3], false, true),
IndexedDBKeyRange(keys[0], keys[3], true, true),
});
for (size_t i = 0; i < std::size(ranges); ++i) {
const int64_t database_id = 1;
const int64_t object_store_id = i + 1;
const IndexedDBKeyRange& range = ranges[i];
std::vector<IndexedDBExternalObject> external_objects;
for (size_t j = 0; j < 4; ++j) {
std::string type = "type " + base::NumberToString(j);
external_objects.push_back(CreateBlobInfo(base::UTF8ToUTF16(type), 1));
}
// Reset from previous iteration.
blob_context_->ClearWrites();
file_system_access_context_->ClearWrites();
std::vector<IndexedDBValue> values = {
IndexedDBValue("value0", {external_objects[0]}),
IndexedDBValue("value1", {external_objects[1]}),
IndexedDBValue("value2", {external_objects[2]}),
IndexedDBValue("value3", {external_objects[3]}),
};
ASSERT_GE(keys.size(), values.size());
// Initiate transaction1 - write records.
std::unique_ptr<BackingStore::Transaction> transaction1 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1->Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
for (size_t j = 0; j < values.size(); ++j) {
EXPECT_TRUE(backing_store()
->PutRecord(transaction1.get(), database_id,
object_store_id, keys[j], &values[j], &record)
.ok());
}
// Start committing transaction1.
bool succeeded = false;
EXPECT_TRUE(
transaction1->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
// Finish committing transaction1.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction1->CommitPhaseTwo().ok());
// Initiate transaction 2 - delete range.
std::unique_ptr<BackingStore::Transaction> transaction2 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction2->Begin(CreateDummyLock());
IndexedDBValue result_value;
EXPECT_TRUE(backing_store()
->DeleteRange(transaction2.get(), database_id,
object_store_id, range)
.ok());
// Start committing transaction2.
succeeded = false;
EXPECT_TRUE(
transaction2->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
// Finish committing transaction2.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction2->CommitPhaseTwo().ok());
// Verify blob removals.
CheckFirstNBlobsRemoved(2U);
// Clean up on the IDB sequence.
transaction1.reset();
transaction2.reset();
task_environment_.RunUntilIdle();
}
}
TEST_P(BackingStoreTestWithExternalObjects, DeleteRangeEmptyRange) {
const std::vector<IndexedDBKey> keys = {
IndexedDBKey(u"key0"), IndexedDBKey(u"key1"), IndexedDBKey(u"key2"),
IndexedDBKey(u"key3"), IndexedDBKey(u"key4"),
};
const std::vector<IndexedDBKeyRange> ranges = {
IndexedDBKeyRange(keys[3], keys[4], true, false),
IndexedDBKeyRange(keys[2], keys[1], false, false),
IndexedDBKeyRange(keys[2], keys[1], true, true),
};
for (size_t i = 0; i < std::size(ranges); ++i) {
const int64_t database_id = 1;
const int64_t object_store_id = i + 1;
const IndexedDBKeyRange& range = ranges[i];
std::vector<IndexedDBExternalObject> external_objects;
for (size_t j = 0; j < 4; ++j) {
std::string type = "type " + base::NumberToString(j);
external_objects.push_back(CreateBlobInfo(base::UTF8ToUTF16(type), 1));
}
// Reset from previous iteration.
blob_context_->ClearWrites();
file_system_access_context_->ClearWrites();
std::vector<IndexedDBValue> values = {
IndexedDBValue("value0", {external_objects[0]}),
IndexedDBValue("value1", {external_objects[1]}),
IndexedDBValue("value2", {external_objects[2]}),
IndexedDBValue("value3", {external_objects[3]}),
};
ASSERT_GE(keys.size(), values.size());
// Initiate transaction1 - write records.
std::unique_ptr<BackingStore::Transaction> transaction1 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1->Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
for (size_t j = 0; j < values.size(); ++j) {
EXPECT_TRUE(backing_store()
->PutRecord(transaction1.get(), database_id,
object_store_id, keys[j], &values[j], &record)
.ok());
}
// Start committing transaction1.
bool succeeded = false;
EXPECT_TRUE(
transaction1->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
// Finish committing transaction1.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction1->CommitPhaseTwo().ok());
// Initiate transaction 2 - delete range.
std::unique_ptr<BackingStore::Transaction> transaction2 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction2->Begin(CreateDummyLock());
IndexedDBValue result_value;
EXPECT_TRUE(backing_store()
->DeleteRange(transaction2.get(), database_id,
object_store_id, range)
.ok());
// Start committing transaction2.
succeeded = false;
EXPECT_TRUE(
transaction2->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
// Finish committing transaction2.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction2->CommitPhaseTwo().ok());
// Verify blob removals.
VerifyNumBlobsRemoved(0U);
// Clean on the IDB sequence.
transaction1.reset();
transaction2.reset();
task_environment_.RunUntilIdle();
}
}
TEST_P(BackingStoreTestWithExternalObjects,
BlobJournalInterleavedTransactions) {
// Initiate transaction1.
std::unique_ptr<BackingStore::Transaction> transaction1 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1->Begin(CreateDummyLock());
BackingStore::RecordIdentifier record1;
EXPECT_TRUE(
backing_store()
->PutRecord(transaction1.get(), 1, 1, key3_, &value3_, &record1)
.ok());
bool succeeded = false;
EXPECT_TRUE(
transaction1->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
// Verify transaction1 phase one completed.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(CheckBlobWrites());
VerifyNumBlobsRemoved(0);
// Initiate transaction2.
std::unique_ptr<BackingStore::Transaction> transaction2 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction2->Begin(CreateDummyLock());
BackingStore::RecordIdentifier record2;
EXPECT_TRUE(
backing_store()
->PutRecord(transaction2.get(), 1, 1, key1_, &value1_, &record2)
.ok());
succeeded = false;
EXPECT_TRUE(
transaction2->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
// Verify transaction2 phase one completed.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(CheckBlobWrites());
VerifyNumBlobsRemoved(0);
// Finalize both transactions.
EXPECT_TRUE(transaction1->CommitPhaseTwo().ok());
VerifyNumBlobsRemoved(0);
EXPECT_TRUE(transaction2->CommitPhaseTwo().ok());
VerifyNumBlobsRemoved(0);
// Clean up on the IDB sequence.
transaction1.reset();
transaction2.reset();
task_environment_.RunUntilIdle();
}
TEST_P(BackingStoreTestWithExternalObjects, ActiveBlobJournal) {
std::unique_ptr<BackingStore::Transaction> transaction1 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1->Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
EXPECT_TRUE(
backing_store()
->PutRecord(transaction1.get(), 1, 1, key3_, &value3_, &record)
.ok());
bool succeeded = false;
EXPECT_TRUE(
transaction1->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
EXPECT_TRUE(succeeded);
EXPECT_TRUE(CheckBlobWrites());
EXPECT_TRUE(transaction1->CommitPhaseTwo().ok());
BackingStore::Transaction transaction2(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction2.Begin(CreateDummyLock());
IndexedDBValue read_result_value;
EXPECT_TRUE(backing_store()
->GetRecord(&transaction2, 1, 1, key3_, &read_result_value)
.ok());
succeeded = false;
EXPECT_TRUE(
transaction2.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction2.CommitPhaseTwo().ok());
EXPECT_EQ(value3_.bits, read_result_value.bits);
EXPECT_TRUE(CheckBlobInfoMatches(read_result_value.external_objects));
EXPECT_TRUE(CheckBlobReadsMatchWrites(read_result_value.external_objects));
for (const IndexedDBExternalObject& external_object :
read_result_value.external_objects) {
if (external_object.mark_used_callback()) {
external_object.mark_used_callback().Run();
}
}
std::unique_ptr<BackingStore::Transaction> transaction3 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction3->Begin(CreateDummyLock());
EXPECT_TRUE(
backing_store()
->DeleteRange(transaction3.get(), 1, 1, IndexedDBKeyRange(key3_))
.ok());
succeeded = false;
EXPECT_TRUE(
transaction3->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction3->CommitPhaseTwo().ok());
VerifyNumBlobsRemoved(0);
for (const IndexedDBExternalObject& external_object :
read_result_value.external_objects) {
if (external_object.release_callback()) {
external_object.release_callback().Run();
}
}
task_environment_.RunUntilIdle();
if (TestType() != ExternalObjectTestType::kOnlyFileSystemAccessHandles) {
EXPECT_TRUE(backing_store()->IsBlobCleanupPending());
EXPECT_EQ(
3, backing_store()->NumAggregatedJournalCleaningRequestsForTesting());
backing_store()->SetNumAggregatedJournalCleaningRequestsForTesting(
BackingStore::kMaxJournalCleanRequests - 1);
backing_store()->StartJournalCleaningTimer();
CheckFirstNBlobsRemoved(3);
#if DCHECK_IS_ON()
EXPECT_EQ(3, backing_store()->NumBlobFilesDeletedForTesting());
#endif
}
EXPECT_FALSE(backing_store()->IsBlobCleanupPending());
// Clean on the IDB sequence.
transaction1.reset();
transaction3.reset();
task_environment_.RunUntilIdle();
}
// Make sure that using very high ( more than 32 bit ) values for
// database_id and object_store_id still work.
TEST_F(BackingStoreTest, HighIds) {
IndexedDBKey key1 = key1_;
IndexedDBKey key2 = key2_;
IndexedDBValue value1 = value1_;
const int64_t high_database_id = 1ULL << 35;
const int64_t high_object_store_id = 1ULL << 39;
// index_ids are capped at 32 bits for storage purposes.
const int64_t high_index_id = 1ULL << 29;
const int64_t invalid_high_index_id = 1ULL << 37;
const IndexedDBKey& index_key = key2;
std::string index_key_raw;
EncodeIDBKey(index_key, &index_key_raw);
{
BackingStore::Transaction transaction1(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1.Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
Status s = backing_store()->PutRecord(&transaction1, high_database_id,
high_object_store_id, key1, &value1,
&record);
EXPECT_TRUE(s.ok());
s = backing_store()->PutIndexDataForRecord(
&transaction1, high_database_id, high_object_store_id,
invalid_high_index_id, index_key, record);
EXPECT_FALSE(s.ok());
s = backing_store()->PutIndexDataForRecord(
&transaction1, high_database_id, high_object_store_id, high_index_id,
index_key, record);
EXPECT_TRUE(s.ok());
bool succeeded = false;
EXPECT_TRUE(
transaction1.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction1.CommitPhaseTwo().ok());
}
{
BackingStore::Transaction transaction2(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction2.Begin(CreateDummyLock());
IndexedDBValue result_value;
Status s =
backing_store()->GetRecord(&transaction2, high_database_id,
high_object_store_id, key1, &result_value);
EXPECT_TRUE(s.ok());
EXPECT_EQ(value1.bits, result_value.bits);
std::unique_ptr<IndexedDBKey> new_primary_key;
s = backing_store()->GetPrimaryKeyViaIndex(
&transaction2, high_database_id, high_object_store_id,
invalid_high_index_id, index_key, &new_primary_key);
EXPECT_FALSE(s.ok());
s = backing_store()->GetPrimaryKeyViaIndex(
&transaction2, high_database_id, high_object_store_id, high_index_id,
index_key, &new_primary_key);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(new_primary_key->Equals(key1));
bool succeeded = false;
EXPECT_TRUE(
transaction2.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction2.CommitPhaseTwo().ok());
}
}
// Make sure that other invalid ids do not crash.
TEST_F(BackingStoreTest, InvalidIds) {
const IndexedDBKey key = key1_;
IndexedDBValue value = value1_;
// valid ids for use when testing invalid ids
const int64_t database_id = 1;
const int64_t object_store_id = 1;
const int64_t index_id = kMinimumIndexId;
// index_ids must be > kMinimumIndexId
const int64_t invalid_low_index_id = 19;
IndexedDBValue result_value;
BackingStore::Transaction transaction1(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1.Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
Status s = backing_store()->PutRecord(
&transaction1, database_id, KeyPrefix::kInvalidId, key, &value, &record);
EXPECT_FALSE(s.ok());
s = backing_store()->PutRecord(&transaction1, database_id, 0, key, &value,
&record);
EXPECT_FALSE(s.ok());
s = backing_store()->PutRecord(&transaction1, KeyPrefix::kInvalidId,
object_store_id, key, &value, &record);
EXPECT_FALSE(s.ok());
s = backing_store()->PutRecord(&transaction1, 0, object_store_id, key, &value,
&record);
EXPECT_FALSE(s.ok());
s = backing_store()->GetRecord(&transaction1, database_id,
KeyPrefix::kInvalidId, key, &result_value);
EXPECT_FALSE(s.ok());
s = backing_store()->GetRecord(&transaction1, database_id, 0, key,
&result_value);
EXPECT_FALSE(s.ok());
s = backing_store()->GetRecord(&transaction1, KeyPrefix::kInvalidId,
object_store_id, key, &result_value);
EXPECT_FALSE(s.ok());
s = backing_store()->GetRecord(&transaction1, 0, object_store_id, key,
&result_value);
EXPECT_FALSE(s.ok());
std::unique_ptr<IndexedDBKey> new_primary_key;
s = backing_store()->GetPrimaryKeyViaIndex(
&transaction1, database_id, object_store_id, KeyPrefix::kInvalidId, key,
&new_primary_key);
EXPECT_FALSE(s.ok());
s = backing_store()->GetPrimaryKeyViaIndex(
&transaction1, database_id, object_store_id, invalid_low_index_id, key,
&new_primary_key);
EXPECT_FALSE(s.ok());
s = backing_store()->GetPrimaryKeyViaIndex(
&transaction1, database_id, object_store_id, 0, key, &new_primary_key);
EXPECT_FALSE(s.ok());
s = backing_store()->GetPrimaryKeyViaIndex(
&transaction1, KeyPrefix::kInvalidId, object_store_id, index_id, key,
&new_primary_key);
EXPECT_FALSE(s.ok());
s = backing_store()->GetPrimaryKeyViaIndex(&transaction1, database_id,
KeyPrefix::kInvalidId, index_id,
key, &new_primary_key);
EXPECT_FALSE(s.ok());
}
TEST_F(BackingStoreTest, CreateDatabase) {
const std::u16string database_name(u"db1");
int64_t database_id;
const int64_t version = 9;
const int64_t object_store_id = 99;
const std::u16string object_store_name(u"object_store1");
const bool auto_increment = true;
const IndexedDBKeyPath object_store_key_path(u"object_store_key");
const int64_t index_id = 999;
const std::u16string index_name(u"index1");
const bool unique = true;
const bool multi_entry = true;
const IndexedDBKeyPath index_key_path(u"index_key");
{
IndexedDBDatabaseMetadata database;
database.name = database_name;
database.version = version;
Status s = backing_store()->CreateDatabase(database);
EXPECT_TRUE(s.ok());
EXPECT_GT(database.id, 0);
database_id = database.id;
BackingStore::Transaction transaction(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction.Begin(CreateDummyLock());
IndexedDBObjectStoreMetadata object_store;
s = backing_store()->CreateObjectStore(
&transaction, database.id, object_store_id, object_store_name,
object_store_key_path, auto_increment, &object_store);
EXPECT_TRUE(s.ok());
IndexedDBIndexMetadata index;
s = backing_store()->CreateIndex(&transaction, database.id, object_store.id,
index_id, index_name, index_key_path,
unique, multi_entry, &index);
EXPECT_TRUE(s.ok());
bool succeeded = false;
EXPECT_TRUE(
transaction.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction.CommitPhaseTwo().ok());
}
{
IndexedDBDatabaseMetadata database;
bool found;
Status s = backing_store()->ReadMetadataForDatabaseName(database_name,
&database, &found);
EXPECT_TRUE(s.ok());
EXPECT_TRUE(found);
// database.name is not filled in by the implementation.
EXPECT_EQ(version, database.version);
EXPECT_EQ(database_id, database.id);
EXPECT_EQ(1UL, database.object_stores.size());
IndexedDBObjectStoreMetadata object_store =
database.object_stores[object_store_id];
EXPECT_EQ(object_store_name, object_store.name);
EXPECT_EQ(object_store_key_path, object_store.key_path);
EXPECT_EQ(auto_increment, object_store.auto_increment);
EXPECT_EQ(1UL, object_store.indexes.size());
IndexedDBIndexMetadata index = object_store.indexes[index_id];
EXPECT_EQ(index_name, index.name);
EXPECT_EQ(index_key_path, index.key_path);
EXPECT_EQ(unique, index.unique);
EXPECT_EQ(multi_entry, index.multi_entry);
}
}
TEST_F(BackingStoreTest, GetDatabaseNames) {
const std::u16string db1_name(u"db1");
const int64_t db1_version = 1LL;
// Database records with DEFAULT_VERSION represent
// stale data, and should not be enumerated.
const std::u16string db2_name(u"db2");
const int64_t db2_version = IndexedDBDatabaseMetadata::DEFAULT_VERSION;
IndexedDBDatabaseMetadata db1;
db1.name = db1_name;
db1.version = db1_version;
Status s = backing_store()->CreateDatabase(db1);
EXPECT_TRUE(s.ok());
EXPECT_GT(db1.id, 0LL);
IndexedDBDatabaseMetadata db2;
db2.name = db2_name;
db2.version = db2_version;
s = backing_store()->CreateDatabase(db2);
EXPECT_TRUE(s.ok());
EXPECT_GT(db2.id, db1.id);
std::vector<std::u16string> names;
s = backing_store()->GetDatabaseNames(&names);
EXPECT_TRUE(s.ok());
ASSERT_EQ(1U, names.size());
EXPECT_EQ(db1_name, names[0]);
}
TEST_P(BackingStoreTestForThirdPartyStoragePartitioning,
ReadCorruptionInfoForOpaqueStorageKey) {
storage::BucketLocator bucket_locator;
bucket_locator.storage_key =
blink::StorageKey::CreateFirstParty(url::Origin());
bucket_locator.is_default = true;
// No `path_base`.
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(base::FilePath(), bucket_locator).empty());
}
TEST_P(BackingStoreTestForThirdPartyStoragePartitioning,
ReadCorruptionInfoForFirstPartyStorageKey) {
storage::BucketLocator bucket_locator;
const base::FilePath path_base = temp_dir_.GetPath();
bucket_locator.storage_key =
blink::StorageKey::CreateFromStringForTesting("http://www.google.com/");
bucket_locator.id = storage::BucketId::FromUnsafeValue(1);
bucket_locator.is_default = true;
ASSERT_FALSE(path_base.empty());
// File not found.
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
const base::FilePath info_path =
path_base.AppendASCII("http_www.google.com_0.indexeddb.leveldb")
.AppendASCII("corruption_info.json");
ASSERT_TRUE(CreateDirectory(info_path.DirName()));
// Empty file.
std::string dummy_data;
ASSERT_TRUE(base::WriteFile(info_path, dummy_data));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// File size > 4 KB.
dummy_data.resize(5000, 'c');
ASSERT_TRUE(base::WriteFile(info_path, dummy_data));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// Random string.
ASSERT_TRUE(base::WriteFile(info_path, "foo bar"));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// Not a dictionary.
ASSERT_TRUE(base::WriteFile(info_path, "[]"));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// Empty dictionary.
ASSERT_TRUE(base::WriteFile(info_path, "{}"));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// Dictionary, no message key.
ASSERT_TRUE(base::WriteFile(info_path, "{\"foo\":\"bar\"}"));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// Dictionary, message key.
ASSERT_TRUE(base::WriteFile(info_path, "{\"message\":\"bar\"}"));
std::string message =
indexed_db::ReadCorruptionInfo(path_base, bucket_locator);
EXPECT_FALSE(message.empty());
EXPECT_FALSE(PathExists(info_path));
EXPECT_EQ("bar", message);
// Dictionary, message key and more.
ASSERT_TRUE(base::WriteFile(info_path, "{\"message\":\"foo\",\"bar\":5}"));
message = indexed_db::ReadCorruptionInfo(path_base, bucket_locator);
EXPECT_FALSE(message.empty());
EXPECT_FALSE(PathExists(info_path));
EXPECT_EQ("foo", message);
}
TEST_P(BackingStoreTestForThirdPartyStoragePartitioning,
ReadCorruptionInfoForThirdPartyStorageKey) {
storage::BucketLocator bucket_locator;
bucket_locator.storage_key = blink::StorageKey::Create(
url::Origin::Create(GURL("http://www.google.com/")),
net::SchemefulSite(GURL("http://www.youtube.com/")),
blink::mojom::AncestorChainBit::kCrossSite);
bucket_locator.id = storage::BucketId::FromUnsafeValue(1);
bucket_locator.is_default = true;
const base::FilePath path_base = temp_dir_.GetPath();
ASSERT_FALSE(path_base.empty());
// File not found.
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
base::FilePath info_path =
path_base.AppendASCII("http_www.google.com_0.indexeddb.leveldb")
.AppendASCII("corruption_info.json");
if (IsThirdPartyStoragePartitioningEnabled()) {
info_path = path_base.AppendASCII("indexeddb.leveldb")
.AppendASCII("corruption_info.json");
}
ASSERT_TRUE(CreateDirectory(info_path.DirName()));
// Empty file.
std::string dummy_data;
ASSERT_TRUE(base::WriteFile(info_path, dummy_data));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// File size > 4 KB.
dummy_data.resize(5000, 'c');
ASSERT_TRUE(base::WriteFile(info_path, dummy_data));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// Random string.
ASSERT_TRUE(base::WriteFile(info_path, "foo bar"));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// Not a dictionary.
ASSERT_TRUE(base::WriteFile(info_path, "[]"));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// Empty dictionary.
ASSERT_TRUE(base::WriteFile(info_path, "{}"));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// Dictionary, no message key.
ASSERT_TRUE(base::WriteFile(info_path, "{\"foo\":\"bar\"}"));
EXPECT_TRUE(
indexed_db::ReadCorruptionInfo(path_base, bucket_locator).empty());
EXPECT_FALSE(PathExists(info_path));
// Dictionary, message key.
ASSERT_TRUE(base::WriteFile(info_path, "{\"message\":\"bar\"}"));
std::string message =
indexed_db::ReadCorruptionInfo(path_base, bucket_locator);
EXPECT_FALSE(message.empty());
EXPECT_FALSE(PathExists(info_path));
EXPECT_EQ("bar", message);
// Dictionary, message key and more.
ASSERT_TRUE(base::WriteFile(info_path, "{\"message\":\"foo\",\"bar\":5}"));
message = indexed_db::ReadCorruptionInfo(path_base, bucket_locator);
EXPECT_FALSE(message.empty());
EXPECT_FALSE(PathExists(info_path));
EXPECT_EQ("foo", message);
}
namespace {
// v3 Blob Data is encoded as a series of:
// { is_file [bool], blob_number [int64_t as varInt],
// type [string-with-length, may be empty],
// (for Blobs only) size [int64_t as varInt]
// (for Files only) fileName [string-with-length]
// }
// There is no length field; just read until you run out of data.
std::string EncodeV3BlobInfos(
const std::vector<IndexedDBExternalObject>& blob_info) {
std::string ret;
for (const auto& info : blob_info) {
DCHECK(info.object_type() == IndexedDBExternalObject::ObjectType::kFile ||
info.object_type() == IndexedDBExternalObject::ObjectType::kBlob);
bool is_file =
info.object_type() == IndexedDBExternalObject::ObjectType::kFile;
EncodeBool(is_file, &ret);
EncodeVarInt(info.blob_number(), &ret);
EncodeStringWithLength(info.type(), &ret);
if (is_file) {
EncodeStringWithLength(info.file_name(), &ret);
} else {
EncodeVarInt(info.size(), &ret);
}
}
return ret;
}
int64_t GetTotalBlobSize(MockBlobStorageContext* blob_context) {
int64_t space_used = 0;
for (const BlobWrite& write : blob_context->writes()) {
space_used += base::GetFileSize(write.path).value_or(0);
}
return space_used;
}
} // namespace
// This test ensures that the rollback process correctly handles blob cleanup
// in abort scenarios, specifically verifying that blob writes are rolled back
// and no orphaned blobs are left on disk after an abort.
// For more details, refer to https://crbug.com/41460842
TEST_P(BackingStoreTestWithExternalObjects, RollbackClearsDiskSpace) {
// Enable writing files to disk for the blob context.
blob_context_->SetWriteFilesToDisk(true);
// Ensure no disk space is used initially.
int64_t initial_disk_space = GetTotalBlobSize(blob_context_.get());
ASSERT_EQ(initial_disk_space, 0);
// The initial transaction is necessary to establish a baseline blob on disk.
// This ensures that the rollback process only affects the blobs inserted in
// the second transaction, allowing us to verify that the rollback correctly
// handles blob cleanup without impacting pre-existing blobs.
BackingStore::Transaction initial_transaction(
BackingStore::Transaction(backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite));
initial_transaction.Begin(CreateDummyLock());
// Insert an initial blob.
std::string initial_blob_name = "initial_blob";
IndexedDBExternalObject initial_blob =
CreateBlobInfo(base::UTF8ToUTF16(initial_blob_name), /*size=*/100);
IndexedDBValue initial_value("initial_value", {initial_blob});
IndexedDBKey initial_key(u"initial_key");
BackingStore::RecordIdentifier initial_record;
EXPECT_TRUE(backing_store()
->PutRecord(&initial_transaction, /*database_id=*/1,
/*object_store_id=*/1, initial_key,
&initial_value, &initial_record)
.ok());
// Commit the initial transaction (Phase 1 and Phase 2).
bool initial_succeeded = false;
base::RunLoop initial_write_blobs_loop;
EXPECT_TRUE(
initial_transaction
.CommitPhaseOne(CreateBlobWriteCallback(
&initial_succeeded, initial_write_blobs_loop.QuitClosure()))
.ok());
initial_write_blobs_loop.Run();
EXPECT_TRUE(initial_succeeded);
EXPECT_TRUE(initial_transaction.CommitPhaseTwo().ok());
// Track the path of the initially written blob.
ASSERT_GT(blob_context_->writes().size(), 0u);
base::FilePath initial_blob_path = blob_context_->writes()[0].path;
// Ensure disk space is used after committing the initial transaction.
int64_t disk_space_after_committed_transaction =
GetTotalBlobSize(blob_context_.get());
ASSERT_GT(disk_space_after_committed_transaction, 0);
// Start a new transaction for the test scenario (rollback).
BackingStore::Transaction transaction(
BackingStore::Transaction(backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite));
transaction.Begin(CreateDummyLock());
// Prepare test data for second transaction.
IndexedDBKey key = IndexedDBKey(u"key0");
std::string name = "name0";
IndexedDBExternalObject test_blob =
CreateBlobInfo(base::UTF8ToUTF16(name), 100);
IndexedDBValue value = IndexedDBValue("value0", {test_blob});
// Insert additional blob that will be rolled back.
BackingStore::RecordIdentifier record;
EXPECT_TRUE(backing_store()
->PutRecord(&transaction, /*database_id=*/1,
/*object_store_id=*/1, key, &value, &record)
.ok());
// Simulate commit phase 1 to ensure that the blob is written to disk.
bool succeeded = false;
base::RunLoop write_blobs_loop;
EXPECT_TRUE(transaction
.CommitPhaseOne(CreateBlobWriteCallback(
&succeeded, write_blobs_loop.QuitClosure()))
.ok());
write_blobs_loop.Run();
EXPECT_TRUE(succeeded);
// Verify that disk space has increased after commit phase 1.
int64_t disk_space_after_commit_phase_one =
GetTotalBlobSize(blob_context_.get());
ASSERT_GT(disk_space_after_commit_phase_one,
disk_space_after_committed_transaction);
// Set the condition to trigger immediate journal cleaning.
backing_store_->SetNumAggregatedJournalCleaningRequestsForTesting(
BackingStore::kMaxJournalCleanRequests - 1);
// Introduce the rollback: Simulate a failure or abort in the transaction.
transaction.Rollback();
// Wait for the cleanup process to complete.
base::RunLoop cleanup_loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, cleanup_loop.QuitClosure());
cleanup_loop.Run();
// Verify that the number of writes is as expected.
EXPECT_EQ(blob_context_->writes().size(), 2u);
// After the rollback, verify that the blobs inserted in the transaction are
// cleaned up.
for (const BlobWrite& write : blob_context_->writes()) {
if (write.path == initial_blob_path) {
// Ensure initial blob still exists.
EXPECT_TRUE(base::PathExists(write.path));
} else {
// Ensure rolled-back blobs are deleted.
EXPECT_FALSE(base::PathExists(write.path));
}
}
// Finally, verify that the disk space is back to its original state after the
// rollback.
int64_t disk_space_after_rollback = GetTotalBlobSize(blob_context_.get());
ASSERT_EQ(disk_space_after_committed_transaction, disk_space_after_rollback);
}
TEST_F(BackingStoreTestWithBlobs, SchemaUpgradeV3ToV4) {
int64_t database_id;
const int64_t object_store_id = 99;
const std::u16string database_name(u"db1");
const int64_t version = 9;
const std::u16string object_store_name(u"object_store1");
const bool auto_increment = true;
const IndexedDBKeyPath object_store_key_path(u"object_store_key");
{
IndexedDBDatabaseMetadata database;
database.name = database_name;
database.version = version;
Status s = backing_store()->CreateDatabase(database);
EXPECT_TRUE(s.ok());
EXPECT_GT(database.id, 0);
database_id = database.id;
BackingStore::Transaction transaction(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction.Begin(CreateDummyLock());
IndexedDBObjectStoreMetadata object_store;
s = backing_store()->CreateObjectStore(
&transaction, database.id, object_store_id, object_store_name,
object_store_key_path, auto_increment, &object_store);
EXPECT_TRUE(s.ok());
bool succeeded = false;
EXPECT_TRUE(
transaction.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction.CommitPhaseTwo().ok());
}
task_environment_.RunUntilIdle();
// Initiate transaction1 - writing blobs.
std::unique_ptr<BackingStore::Transaction> transaction1 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1->Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
EXPECT_TRUE(backing_store()
->PutRecord(transaction1.get(), database_id, object_store_id,
key3_, &value3_, &record)
.ok());
bool succeeded = false;
base::RunLoop write_blobs_loop;
EXPECT_TRUE(transaction1
->CommitPhaseOne(CreateBlobWriteCallback(
&succeeded, write_blobs_loop.QuitClosure()))
.ok());
write_blobs_loop.Run();
task_environment_.RunUntilIdle();
// Finish up transaction1, verifying blob writes.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(CheckBlobWrites());
ASSERT_TRUE(transaction1->CommitPhaseTwo().ok());
transaction1.reset();
task_environment_.RunUntilIdle();
// Change entries to be v3, and change the schema to be v3.
std::unique_ptr<LevelDBWriteBatch> write_batch = LevelDBWriteBatch::Create();
const std::string schema_version_key = SchemaVersionKey::Encode();
ASSERT_TRUE(
indexed_db::PutInt(write_batch.get(), schema_version_key, 3).ok());
const std::string object_store_data_key =
ObjectStoreDataKey::Encode(database_id, object_store_id, key3_);
std::string_view leveldb_key_piece(object_store_data_key);
BlobEntryKey blob_entry_key;
ASSERT_TRUE(BlobEntryKey::FromObjectStoreDataKey(&leveldb_key_piece,
&blob_entry_key));
ASSERT_EQ(blob_context_->writes().size(), 3u);
auto& writes = blob_context_->writes();
external_objects()[0].set_blob_number(writes[0].GetBlobNumber());
external_objects()[1].set_blob_number(writes[1].GetBlobNumber());
external_objects()[2].set_blob_number(writes[2].GetBlobNumber());
std::string v3_blob_data = EncodeV3BlobInfos(external_objects());
write_batch->Put(std::string_view(blob_entry_key.Encode()),
std::string_view(v3_blob_data));
ASSERT_TRUE(backing_store()->db()->Write(write_batch.get()).ok());
// The migration code uses the physical files on disk, so those need to be
// written with the correct size & timestamp.
base::FilePath file1_path = writes[1].path;
base::FilePath file2_path = writes[2].path;
ASSERT_TRUE(CreateDirectory(file1_path.DirName()));
ASSERT_TRUE(CreateDirectory(file2_path.DirName()));
base::File file1(file1_path,
base::File::FLAG_WRITE | base::File::FLAG_CREATE_ALWAYS);
ASSERT_TRUE(file1.IsValid());
ASSERT_TRUE(
file1.WriteAtCurrentPosAndCheck(base::as_byte_span(kBlobFileData1)));
ASSERT_TRUE(file1.SetTimes(external_objects()[1].last_modified(),
external_objects()[1].last_modified()));
file1.Close();
base::File file2(file2_path,
base::File::FLAG_WRITE | base::File::FLAG_CREATE_ALWAYS);
ASSERT_TRUE(file2.IsValid());
ASSERT_TRUE(
file2.WriteAtCurrentPosAndCheck(base::as_byte_span(kBlobFileData2)));
ASSERT_TRUE(file2.SetTimes(external_objects()[2].last_modified(),
external_objects()[2].last_modified()));
file2.Close();
DestroyFactoryAndBackingStore();
CreateFactoryAndBackingStore();
// There should be no corruption.
ASSERT_TRUE(data_loss_info_.status == blink::mojom::IDBDataLoss::None);
// Initiate transaction2, reading blobs.
BackingStore::Transaction transaction2(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction2.Begin(CreateDummyLock());
IndexedDBValue result_value;
EXPECT_TRUE(backing_store()
->GetRecord(&transaction2, database_id, object_store_id,
key3_, &result_value)
.ok());
// Finish up transaction2, verifying blob reads.
succeeded = false;
EXPECT_TRUE(
transaction2.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction2.CommitPhaseTwo().ok());
EXPECT_EQ(value3_.bits, result_value.bits);
EXPECT_TRUE(CheckBlobInfoMatches(result_value.external_objects));
}
TEST_F(BackingStoreTestWithBlobs, SchemaUpgradeV4ToV5) {
int64_t database_id;
const int64_t object_store_id = 99;
const std::u16string database_name(u"db1");
const int64_t version = 9;
const std::u16string object_store_name(u"object_store1");
const bool auto_increment = true;
const IndexedDBKeyPath object_store_key_path(u"object_store_key");
// Add an empty blob here to test with. Empty blobs are not written
// to disk, so it's important to verify that a database with empty blobs
// should be considered still valid.
external_objects().push_back(
CreateBlobInfo(u"empty blob", u"file type", base::Time::Now(), 0u));
// The V5 migration checks files on disk, so make sure our fake blob
// context writes something there to check.
blob_context_->SetWriteFilesToDisk(true);
{
IndexedDBDatabaseMetadata database;
database.name = database_name;
database.version = version;
Status s = backing_store()->CreateDatabase(database);
EXPECT_TRUE(s.ok());
EXPECT_GT(database.id, 0);
database_id = database.id;
BackingStore::Transaction transaction(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction.Begin(CreateDummyLock());
IndexedDBObjectStoreMetadata object_store;
s = backing_store()->CreateObjectStore(
&transaction, database.id, object_store_id, object_store_name,
object_store_key_path, auto_increment, &object_store);
EXPECT_TRUE(s.ok());
bool succeeded = false;
EXPECT_TRUE(
transaction.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction.CommitPhaseTwo().ok());
}
task_environment_.RunUntilIdle();
// Initiate transaction - writing blobs.
std::unique_ptr<BackingStore::Transaction> transaction =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction->Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
IndexedDBKey key = IndexedDBKey(u"key");
IndexedDBValue value = IndexedDBValue("value3", external_objects());
EXPECT_TRUE(backing_store()
->PutRecord(transaction.get(), database_id, object_store_id,
key, &value, &record)
.ok());
bool succeeded = false;
base::RunLoop write_blobs_loop;
EXPECT_TRUE(transaction
->CommitPhaseOne(CreateBlobWriteCallback(
&succeeded, write_blobs_loop.QuitClosure()))
.ok());
write_blobs_loop.Run();
task_environment_.RunUntilIdle();
// Finish up transaction, verifying blob writes.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(CheckBlobWrites());
ASSERT_TRUE(transaction->CommitPhaseTwo().ok());
transaction.reset();
task_environment_.RunUntilIdle();
ASSERT_EQ(blob_context_->writes().size(), 3u);
// Verify V4 to V5 conversion with all blobs intact has no data loss.
{
// Change the schema to be v4.
const int64_t old_version = 4;
std::unique_ptr<LevelDBWriteBatch> write_batch =
LevelDBWriteBatch::Create();
const std::string schema_version_key = SchemaVersionKey::Encode();
ASSERT_TRUE(
indexed_db::PutInt(write_batch.get(), schema_version_key, old_version)
.ok());
ASSERT_TRUE(backing_store()->db()->Write(write_batch.get()).ok());
DestroyFactoryAndBackingStore();
CreateFactoryAndBackingStore();
// There should be no corruption here.
ASSERT_EQ(data_loss_info_.status, blink::mojom::IDBDataLoss::None);
}
// Verify V4 to V5 conversion with missing blobs has data loss.
{
// Change the schema to be v4.
const int64_t old_version = 4;
std::unique_ptr<LevelDBWriteBatch> write_batch =
LevelDBWriteBatch::Create();
const std::string schema_version_key = SchemaVersionKey::Encode();
ASSERT_TRUE(
indexed_db::PutInt(write_batch.get(), schema_version_key, old_version)
.ok());
ASSERT_TRUE(backing_store()->db()->Write(write_batch.get()).ok());
// Pick a blob we wrote arbitrarily and delete it.
auto path = blob_context_->writes()[1].path;
base::DeleteFile(path);
DestroyFactoryAndBackingStore();
CreateFactoryAndBackingStore();
// This should be corrupted.
ASSERT_NE(data_loss_info_.status, blink::mojom::IDBDataLoss::None);
DestroyFactoryAndBackingStore();
}
}
// This tests that external objects are deleted when ClearObjectStore is called.
// See: http://crbug.com/488851
// TODO(enne): we could use more comprehensive testing for ClearObjectStore.
TEST_P(BackingStoreTestWithExternalObjects, ClearObjectStoreObjects) {
const std::vector<IndexedDBKey> keys = {
IndexedDBKey(u"key0"), IndexedDBKey(u"key1"), IndexedDBKey(u"key2"),
IndexedDBKey(u"key3")};
const int64_t database_id = 777;
const int64_t object_store_id = 999;
// Create two object stores, to verify that only one gets deleted.
for (size_t i = 0; i < 2; ++i) {
const int64_t write_object_store_id = object_store_id + i;
std::vector<IndexedDBExternalObject> external_objects;
for (size_t j = 0; j < 4; ++j) {
std::string type = "type " + base::NumberToString(j);
external_objects.push_back(CreateBlobInfo(base::UTF8ToUTF16(type), 1));
}
std::vector<IndexedDBValue> values = {
IndexedDBValue("value0", {external_objects[0]}),
IndexedDBValue("value1", {external_objects[1]}),
IndexedDBValue("value2", {external_objects[2]}),
IndexedDBValue("value3", {external_objects[3]}),
};
ASSERT_GE(keys.size(), values.size());
// Initiate transaction1 - write records.
std::unique_ptr<BackingStore::Transaction> transaction1 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction1->Begin(CreateDummyLock());
BackingStore::RecordIdentifier record;
for (size_t j = 0; j < values.size(); ++j) {
EXPECT_TRUE(backing_store()
->PutRecord(transaction1.get(), database_id,
write_object_store_id, keys[j], &values[j],
&record)
.ok());
}
// Start committing transaction1.
bool succeeded = false;
EXPECT_TRUE(
transaction1->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
// Finish committing transaction1.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction1->CommitPhaseTwo().ok());
}
// Initiate transaction 2 - delete object store
std::unique_ptr<BackingStore::Transaction> transaction2 =
std::make_unique<BackingStore::Transaction>(
backing_store()->AsWeakPtr(),
blink::mojom::IDBTransactionDurability::Relaxed,
blink::mojom::IDBTransactionMode::ReadWrite);
transaction2->Begin(CreateDummyLock());
IndexedDBValue result_value;
EXPECT_TRUE(
backing_store()
->ClearObjectStore(transaction2.get(), database_id, object_store_id)
.ok());
// Start committing transaction2.
bool succeeded = false;
EXPECT_TRUE(
transaction2->CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok());
task_environment_.RunUntilIdle();
// Finish committing transaction2.
EXPECT_TRUE(succeeded);
EXPECT_TRUE(transaction2->CommitPhaseTwo().ok());
// Verify blob removals.
CheckFirstNBlobsRemoved(4);
// Clean up on the IDB sequence.
transaction2.reset();
task_environment_.RunUntilIdle();
}
class BackingStoreTestForCleanupScheduler : public BackingStoreTest {
public:
BackingStoreTestForCleanupScheduler() {
scoped_feature_list_.InitAndEnableFeature(kIdbInSessionDbCleanup);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(BackingStoreTestForCleanupScheduler,
SchedulerInitializedIfTombstoneThresholdExceeded) {
backing_store_->OnTransactionComplete(false);
EXPECT_FALSE(backing_store_->GetLevelDBCleanupSchedulerForTesting()
.GetRunningStateForTesting()
.has_value());
backing_store_->OnTransactionComplete(true);
EXPECT_TRUE(backing_store_->GetLevelDBCleanupSchedulerForTesting()
.GetRunningStateForTesting()
.has_value());
}
} // namespace content::indexed_db