| // Copyright (c) 2013 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 "content/browser/indexed_db/indexed_db_backing_store.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <string> |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/barrier_closure.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/check_op.h" |
| #include "base/containers/span.h" |
| #include "base/cxx17_backports.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/guid.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/post_task.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/test/bind.h" |
| #include "base/test/task_environment.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/default_clock.h" |
| #include "components/services/storage/indexed_db/scopes/disjoint_range_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/public/mojom/indexed_db_control.mojom-test-utils.h" |
| #include "content/browser/indexed_db/indexed_db_class_factory.h" |
| #include "content/browser/indexed_db/indexed_db_context_impl.h" |
| #include "content/browser/indexed_db/indexed_db_factory_impl.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_metadata_coding.h" |
| #include "content/browser/indexed_db/indexed_db_storage_key_state.h" |
| #include "content/browser/indexed_db/indexed_db_value.h" |
| #include "mojo/public/cpp/bindings/self_owned_receiver.h" |
| #include "storage/browser/quota/special_storage_policy.h" |
| #include "storage/browser/test/fake_blob.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/indexeddb/web_idb_types.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 { |
| namespace indexed_db_backing_store_unittest { |
| |
| class TestableIndexedDBBackingStore : public IndexedDBBackingStore { |
| public: |
| TestableIndexedDBBackingStore( |
| IndexedDBBackingStore::Mode backing_store_mode, |
| TransactionalLevelDBFactory* leveldb_factory, |
| const blink::StorageKey& storage_key, |
| const base::FilePath& blob_path, |
| std::unique_ptr<TransactionalLevelDBDatabase> db, |
| storage::mojom::BlobStorageContext* blob_storage_context, |
| storage::mojom::FileSystemAccessContext* file_system_access_context, |
| std::unique_ptr<storage::FilesystemProxy> filesystem_proxy, |
| BlobFilesCleanedCallback blob_files_cleaned, |
| ReportOutstandingBlobsCallback report_outstanding_blobs, |
| scoped_refptr<base::SequencedTaskRunner> idb_task_runner) |
| : IndexedDBBackingStore(backing_store_mode, |
| leveldb_factory, |
| storage_key, |
| blob_path, |
| std::move(db), |
| blob_storage_context, |
| file_system_access_context, |
| std::move(filesystem_proxy), |
| std::move(blob_files_cleaned), |
| std::move(report_outstanding_blobs), |
| std::move(idb_task_runner)) {} |
| |
| TestableIndexedDBBackingStore(const TestableIndexedDBBackingStore&) = delete; |
| TestableIndexedDBBackingStore& operator=( |
| const TestableIndexedDBBackingStore&) = delete; |
| |
| ~TestableIndexedDBBackingStore() override = default; |
| |
| const std::vector<base::FilePath>& removals() const { return removals_; } |
| void ClearRemovals() { removals_.clear(); } |
| |
| void StartJournalCleaningTimer() override { |
| IndexedDBBackingStore::StartJournalCleaningTimer(); |
| } |
| |
| protected: |
| bool RemoveBlobFile(int64_t database_id, int64_t blob_number) const override { |
| removals_.push_back(GetBlobFileName(database_id, blob_number)); |
| return IndexedDBBackingStore::RemoveBlobFile(database_id, blob_number); |
| } |
| |
| private: |
| // This is modified in an overridden virtual function that is properly const |
| // in the real implementation, therefore must be mutable here. |
| mutable std::vector<base::FilePath> removals_; |
| }; |
| |
| // Factory subclass to allow the test to use the |
| // TestableIndexedDBBackingStore subclass. |
| class TestIDBFactory : public IndexedDBFactoryImpl { |
| public: |
| explicit TestIDBFactory( |
| IndexedDBContextImpl* idb_context, |
| storage::mojom::BlobStorageContext* blob_storage_context, |
| storage::mojom::FileSystemAccessContext* file_system_access_context) |
| : IndexedDBFactoryImpl(idb_context, |
| IndexedDBClassFactory::Get(), |
| base::DefaultClock::GetInstance()), |
| blob_storage_context_(blob_storage_context), |
| file_system_access_context_(file_system_access_context) {} |
| |
| TestIDBFactory(const TestIDBFactory&) = delete; |
| TestIDBFactory& operator=(const TestIDBFactory&) = delete; |
| |
| ~TestIDBFactory() override = default; |
| |
| protected: |
| std::unique_ptr<IndexedDBBackingStore> CreateBackingStore( |
| IndexedDBBackingStore::Mode backing_store_mode, |
| TransactionalLevelDBFactory* leveldb_factory, |
| const blink::StorageKey& storage_key, |
| const base::FilePath& blob_path, |
| std::unique_ptr<TransactionalLevelDBDatabase> db, |
| storage::mojom::BlobStorageContext*, |
| storage::mojom::FileSystemAccessContext*, |
| std::unique_ptr<storage::FilesystemProxy> filesystem_proxy, |
| IndexedDBBackingStore::BlobFilesCleanedCallback blob_files_cleaned, |
| IndexedDBBackingStore::ReportOutstandingBlobsCallback |
| report_outstanding_blobs, |
| scoped_refptr<base::SequencedTaskRunner> idb_task_runner) override { |
| // Use the overridden blob storage and File System Access contexts rather |
| // than the versions that were passed in to this method. This way tests can |
| // use a different context from what is stored in the IndexedDBContext. |
| return std::make_unique<TestableIndexedDBBackingStore>( |
| backing_store_mode, leveldb_factory, storage_key, blob_path, |
| std::move(db), blob_storage_context_, file_system_access_context_, |
| std::move(filesystem_proxy), std::move(blob_files_cleaned), |
| std::move(report_outstanding_blobs), std::move(idb_task_runner)); |
| } |
| |
| private: |
| raw_ptr<storage::mojom::BlobStorageContext> blob_storage_context_; |
| raw_ptr<storage::mojom::FileSystemAccessContext> file_system_access_context_; |
| }; |
| |
| 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, |
| absl::optional<base::Time> last_modified, |
| WriteBlobToFileCallback callback) override { |
| writes_.emplace_back(std::move(blob), path); |
| |
| if (write_files_to_disk_) { |
| auto filesystem_proxy = std::make_unique<storage::FilesystemProxy>( |
| storage::FilesystemProxy::UNRESTRICTED, base::FilePath()); |
| filesystem_proxy->WriteFileAtomically(path, "fake contents"); |
| } |
| |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), |
| storage::mojom::WriteBlobToFileResult::kSuccess)); |
| } |
| |
| 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; |
| }; |
| |
| 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::SequencedTaskRunnerHandle::Get()->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(); |
| } |
| |
| const std::vector< |
| mojo::Remote<::blink::mojom::FileSystemAccessTransferToken>>& |
| writes() { |
| return writes_; |
| } |
| void ClearWrites() { writes_.clear(); } |
| |
| private: |
| std::vector<mojo::Remote<::blink::mojom::FileSystemAccessTransferToken>> |
| writes_; |
| }; |
| |
| class IndexedDBBackingStoreTest : public testing::Test { |
| public: |
| IndexedDBBackingStoreTest() |
| : quota_manager_proxy_( |
| base::MakeRefCounted<storage::MockQuotaManagerProxy>( |
| nullptr, |
| base::ThreadTaskRunnerHandle::Get())) {} |
| |
| IndexedDBBackingStoreTest(const IndexedDBBackingStoreTest&) = delete; |
| IndexedDBBackingStoreTest& operator=(const IndexedDBBackingStoreTest&) = |
| delete; |
| |
| void SetUp() override { |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| |
| blob_context_ = std::make_unique<MockBlobStorageContext>(); |
| file_system_access_context_ = |
| std::make_unique<MockFileSystemAccessContext>(); |
| |
| idb_context_ = base::MakeRefCounted<IndexedDBContextImpl>( |
| temp_dir_.GetPath(), quota_manager_proxy_, |
| base::DefaultClock::GetInstance(), |
| /*blob_storage_context=*/mojo::NullRemote(), |
| /*file_system_access_context=*/mojo::NullRemote(), |
| base::SequencedTaskRunnerHandle::Get(), |
| base::SequencedTaskRunnerHandle::Get()); |
| |
| // Needed to get the QuotaClient bound. |
| { |
| base::RunLoop run_loop; |
| idb_context_->IDBTaskRunner()->PostTask(FROM_HERE, |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| 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"); |
| idb_factory_ = std::make_unique<TestIDBFactory>( |
| idb_context_.get(), blob_context_.get(), |
| file_system_access_context_.get()); |
| |
| leveldb::Status s; |
| std::tie(storage_key_state_handle_, s, std::ignore, data_loss_info_, |
| std::ignore) = |
| idb_factory_->GetOrOpenStorageKeyFactory(storage_key, |
| idb_context_->data_path(), |
| /*create_if_missing=*/true); |
| if (!storage_key_state_handle_.IsHeld()) { |
| backing_store_ = nullptr; |
| return; |
| } |
| backing_store_ = static_cast<TestableIndexedDBBackingStore*>( |
| storage_key_state_handle_.storage_key_state()->backing_store()); |
| lock_manager_ = |
| storage_key_state_handle_.storage_key_state()->lock_manager(); |
| } |
| |
| std::vector<ScopeLock> CreateDummyLock() { |
| base::RunLoop loop; |
| ScopesLocksHolder locks_receiver; |
| bool success = lock_manager_->AcquireLocks( |
| {{0, {"01", "11"}, ScopesLockManager::LockType::kShared}}, |
| locks_receiver.AsWeakPtr(), |
| base::BindLambdaForTesting([&loop]() { loop.Quit(); })); |
| EXPECT_TRUE(success); |
| if (success) |
| loop.Run(); |
| return std::move(locks_receiver.locks); |
| } |
| |
| void DestroyFactoryAndBackingStore() { |
| storage_key_state_handle_.Release(); |
| idb_factory_.reset(); |
| backing_store_ = nullptr; |
| } |
| |
| void TearDown() override { |
| DestroyFactoryAndBackingStore(); |
| if (idb_context_ && !idb_context_->IsInMemoryContext()) { |
| IndexedDBFactoryImpl* factory = idb_context_->GetIDBFactory(); |
| |
| // Loop through all open origins, and force close them, and request the |
| // deletion of the leveldb state. Once the states are no longer around, |
| // delete all of the databases on disk. |
| auto open_factory_storage_keys = factory->GetOpenStorageKeys(); |
| |
| for (const auto& storage_key : open_factory_storage_keys) { |
| base::RunLoop loop; |
| IndexedDBStorageKeyState* per_storage_key_factory = |
| factory->GetStorageKeyFactory(storage_key); |
| |
| auto* leveldb_state = |
| per_storage_key_factory->backing_store()->db()->leveldb_state(); |
| |
| base::WaitableEvent leveldb_close_event; |
| base::WaitableEventWatcher event_watcher; |
| leveldb_state->RequestDestruction(&leveldb_close_event); |
| event_watcher.StartWatching( |
| &leveldb_close_event, |
| base::BindLambdaForTesting( |
| [&](base::WaitableEvent*) { loop.Quit(); }), |
| base::SequencedTaskRunnerHandle::Get()); |
| |
| idb_context_->ForceCloseSync( |
| storage_key, |
| storage::mojom::ForceCloseReason::FORCE_CLOSE_DELETE_ORIGIN); |
| loop.Run(); |
| // There is a possible race in |leveldb_close_event| where the signaling |
| // thread is still in the WaitableEvent::Signal() method. To ensure that |
| // the other thread exits their Signal method, any method on the |
| // WaitableEvent can be called to acquire the internal lock (which will |
| // subsequently wait for the other thread to exit the Signal method). |
| EXPECT_TRUE(leveldb_close_event.IsSignaled()); |
| } |
| // All leveldb databases are closed, and they can be deleted. |
| for (auto storage_key : idb_context_->GetAllStorageKeys()) { |
| bool success = false; |
| storage::mojom::IndexedDBControlAsyncWaiter waiter(idb_context_.get()); |
| waiter.DeleteForStorageKey(storage_key, &success); |
| EXPECT_TRUE(success); |
| } |
| } |
| if (temp_dir_.IsValid()) |
| ASSERT_TRUE(temp_dir_.Delete()); |
| |
| // Wait until the context has fully destroyed. |
| scoped_refptr<base::SequencedTaskRunner> task_runner = |
| idb_context_->IDBTaskRunner(); |
| idb_context_.reset(); |
| { |
| base::RunLoop loop; |
| task_runner->PostTask(FROM_HERE, loop.QuitClosure()); |
| loop.Run(); |
| } |
| } |
| |
| TestableIndexedDBBackingStore* backing_store() { return backing_store_; } |
| |
| // Cycle the idb runner to help clean up tasks, which allows for a clean |
| // shutdown of the leveldb database. This ensures that all file handles are |
| // released and the folder can be deleted on windows (which doesn't allow |
| // folders to be deleted when inside files are in use/exist). |
| void CycleIDBTaskRunner() { |
| base::RunLoop cycle_loop; |
| idb_context_->IDBTaskRunner()->PostTask(FROM_HERE, |
| cycle_loop.QuitClosure()); |
| cycle_loop.Run(); |
| } |
| |
| 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::MockQuotaManagerProxy> quota_manager_proxy_; |
| scoped_refptr<IndexedDBContextImpl> idb_context_; |
| std::unique_ptr<TestIDBFactory> idb_factory_; |
| raw_ptr<DisjointRangeLockManager> lock_manager_; |
| |
| IndexedDBStorageKeyStateHandle storage_key_state_handle_; |
| raw_ptr<TestableIndexedDBBackingStore> backing_store_ = nullptr; |
| IndexedDBDataLossInfo data_loss_info_; |
| |
| // Sample keys and values that are consistent. |
| IndexedDBKey key1_; |
| IndexedDBKey key2_; |
| IndexedDBValue value1_; |
| IndexedDBValue value2_; |
| }; |
| |
| enum class ExternalObjectTestType { |
| kOnlyBlobs, |
| kOnlyFileSystemAccessHandles, |
| kBlobsAndFileSystemAccessHandles |
| }; |
| |
| class IndexedDBBackingStoreTestWithExternalObjects |
| : public testing::WithParamInterface<ExternalObjectTestType>, |
| public IndexedDBBackingStoreTest { |
| public: |
| IndexedDBBackingStoreTestWithExternalObjects() = default; |
| |
| IndexedDBBackingStoreTestWithExternalObjects( |
| const IndexedDBBackingStoreTestWithExternalObjects&) = delete; |
| IndexedDBBackingStoreTestWithExternalObjects& operator=( |
| const IndexedDBBackingStoreTestWithExternalObjects&) = delete; |
| |
| virtual ExternalObjectTestType TestType() { return GetParam(); } |
| |
| bool IncludesBlobs() { |
| return TestType() != ExternalObjectTestType::kOnlyFileSystemAccessHandles; |
| } |
| |
| bool IncludesFileSystemAccessHandles() { |
| return TestType() != ExternalObjectTestType::kOnlyBlobs; |
| } |
| |
| void SetUp() override { |
| IndexedDBBackingStoreTest::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::GenerateGUID(); |
| 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), uuid, file_name, type, |
| last_modified, size); |
| return info; |
| } |
| |
| IndexedDBExternalObject CreateBlobInfo(const std::u16string& type, |
| int64_t size) { |
| auto uuid = base::GenerateGUID(); |
| 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), uuid, 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 { |
| DCHECK(idb_context_->IDBTaskRunner()->RunsTasksInCurrentSequence()); |
| |
| 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.file_system_access_token().empty()) { |
| EXPECT_FALSE(b.file_system_access_token().empty()); |
| return false; |
| } |
| break; |
| } |
| } |
| return true; |
| } |
| |
| bool CheckBlobReadsMatchWrites( |
| const std::vector<IndexedDBExternalObject>& reads) const { |
| DCHECK(idb_context_->IDBTaskRunner()->RunsTasksInCurrentSequence()); |
| |
| 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.file_system_access_token().size() != 1 || |
| read.file_system_access_token()[0] > |
| file_system_access_context_->writes().size()) { |
| return false; |
| } |
| break; |
| } |
| } |
| return true; |
| } |
| |
| bool CheckBlobWrites() { |
| DCHECK(idb_context_->IDBTaskRunner()->RunsTasksInCurrentSequence()); |
| |
| 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; |
| |
| base::RunLoop uuid_loop; |
| std::string uuid_out; |
| DCHECK(desc.blob.is_bound()); |
| DCHECK(desc.blob.is_connected()); |
| desc.blob->GetInternalUUID( |
| base::BindLambdaForTesting([&](const std::string& uuid) { |
| uuid_out = uuid; |
| uuid_loop.Quit(); |
| })); |
| uuid_loop.Run(); |
| if (uuid_out != info.uuid()) |
| return false; |
| } |
| 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; |
| } |
| |
| bool CheckBlobRemovals() const { |
| DCHECK(idb_context_->IDBTaskRunner()->RunsTasksInCurrentSequence()); |
| |
| if (backing_store_->removals().size() != blob_context_->writes().size()) |
| return false; |
| for (size_t i = 0; i < blob_context_->writes().size(); ++i) { |
| if (blob_context_->writes()[i].path != backing_store_->removals()[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| 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_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* no prefix */, |
| IndexedDBBackingStoreTestWithExternalObjects, |
| ::testing::Values( |
| ExternalObjectTestType::kOnlyBlobs, |
| ExternalObjectTestType::kOnlyFileSystemAccessHandles, |
| ExternalObjectTestType::kBlobsAndFileSystemAccessHandles)); |
| |
| class IndexedDBBackingStoreTestWithBlobs |
| : public IndexedDBBackingStoreTestWithExternalObjects { |
| 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(); |
| break; |
| 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 leveldb::Status::OK(); |
| }, |
| succeeded, std::move(on_done)); |
| } |
| |
| TEST_F(IndexedDBBackingStoreTest, PutGetConsistency) { |
| base::RunLoop loop; |
| idb_context_->IDBTaskRunner()->PostTask( |
| FROM_HERE, base::BindLambdaForTesting([&]() { |
| const IndexedDBKey key = key1_; |
| IndexedDBValue value = value1_; |
| { |
| IndexedDBBackingStore::Transaction transaction1( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1.Begin(CreateDummyLock()); |
| IndexedDBBackingStore::RecordIdentifier record; |
| leveldb::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()); |
| } |
| |
| { |
| IndexedDBBackingStore::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); |
| } |
| loop.Quit(); |
| })); |
| loop.Run(); |
| |
| CycleIDBTaskRunner(); |
| } |
| |
| TEST_P(IndexedDBBackingStoreTestWithExternalObjects, PutGetConsistency) { |
| // Initiate transaction1 - writing blobs. |
| std::unique_ptr<IndexedDBBackingStore::Transaction> transaction1 = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1->Begin(CreateDummyLock()); |
| IndexedDBBackingStore::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. |
| IndexedDBBackingStore::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<IndexedDBBackingStore::Transaction> transaction3 = |
| std::make_unique<IndexedDBBackingStore::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()); |
| EXPECT_TRUE(CheckBlobRemovals()); |
| |
| // 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(IndexedDBBackingStoreTestWithExternalObjects, 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<IndexedDBBackingStore::Transaction> transaction1 = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1->Begin(CreateDummyLock()); |
| IndexedDBBackingStore::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. |
| ASSERT_EQ(0UL, backing_store()->removals().size()); |
| |
| // Clean up on the IDB sequence. |
| transaction1.reset(); |
| task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_P(IndexedDBBackingStoreTestWithExternalObjects, DeleteRange) { |
| const std::vector<IndexedDBKey> keys = { |
| IndexedDBKey(u"key0"), IndexedDBKey(u"key1"), IndexedDBKey(u"key2"), |
| IndexedDBKey(u"key3")}; |
| const IndexedDBKeyRange ranges[] = { |
| 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 < base::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(); |
| backing_store()->ClearRemovals(); |
| |
| 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<IndexedDBBackingStore::Transaction> transaction1 = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1->Begin(CreateDummyLock()); |
| IndexedDBBackingStore::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<IndexedDBBackingStore::Transaction> transaction2 = |
| std::make_unique<IndexedDBBackingStore::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. |
| ASSERT_EQ(2UL, backing_store()->removals().size()); |
| EXPECT_EQ(blob_context_->writes()[1].path, backing_store()->removals()[0]); |
| EXPECT_EQ(blob_context_->writes()[2].path, backing_store()->removals()[1]); |
| |
| // Clean up on the IDB sequence. |
| transaction1.reset(); |
| transaction2.reset(); |
| task_environment_.RunUntilIdle(); |
| } |
| } |
| |
| TEST_P(IndexedDBBackingStoreTestWithExternalObjects, DeleteRangeEmptyRange) { |
| const std::vector<IndexedDBKey> keys = { |
| IndexedDBKey(u"key0"), IndexedDBKey(u"key1"), IndexedDBKey(u"key2"), |
| IndexedDBKey(u"key3"), IndexedDBKey(u"key4")}; |
| const 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 < base::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(); |
| backing_store()->ClearRemovals(); |
| |
| 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<IndexedDBBackingStore::Transaction> transaction1 = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1->Begin(CreateDummyLock()); |
| |
| IndexedDBBackingStore::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<IndexedDBBackingStore::Transaction> transaction2 = |
| std::make_unique<IndexedDBBackingStore::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. |
| EXPECT_EQ(0UL, backing_store()->removals().size()); |
| |
| // Clean on the IDB sequence. |
| transaction1.reset(); |
| transaction2.reset(); |
| task_environment_.RunUntilIdle(); |
| } |
| } |
| |
| TEST_P(IndexedDBBackingStoreTestWithExternalObjects, |
| BlobJournalInterleavedTransactions) { |
| // Initiate transaction1. |
| std::unique_ptr<IndexedDBBackingStore::Transaction> transaction1 = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1->Begin(CreateDummyLock()); |
| IndexedDBBackingStore::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()); |
| EXPECT_EQ(0U, backing_store()->removals().size()); |
| |
| // Initiate transaction2. |
| std::unique_ptr<IndexedDBBackingStore::Transaction> transaction2 = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction2->Begin(CreateDummyLock()); |
| IndexedDBBackingStore::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()); |
| EXPECT_EQ(0U, backing_store()->removals().size()); |
| |
| // Finalize both transactions. |
| EXPECT_TRUE(transaction1->CommitPhaseTwo().ok()); |
| EXPECT_EQ(0U, backing_store()->removals().size()); |
| |
| EXPECT_TRUE(transaction2->CommitPhaseTwo().ok()); |
| EXPECT_EQ(0U, backing_store()->removals().size()); |
| |
| // Clean up on the IDB sequence. |
| transaction1.reset(); |
| transaction2.reset(); |
| task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_P(IndexedDBBackingStoreTestWithExternalObjects, ActiveBlobJournal) { |
| std::unique_ptr<IndexedDBBackingStore::Transaction> transaction1 = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1->Begin(CreateDummyLock()); |
| IndexedDBBackingStore::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()); |
| |
| IndexedDBBackingStore::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<IndexedDBBackingStore::Transaction> transaction3 = |
| std::make_unique<IndexedDBBackingStore::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()); |
| EXPECT_EQ(0U, backing_store()->removals().size()); |
| 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()); |
| #if DCHECK_IS_ON() |
| EXPECT_EQ(3, |
| backing_store()->NumAggregatedJournalCleaningRequestsForTesting()); |
| #endif |
| for (int i = 3; i < IndexedDBBackingStore::kMaxJournalCleanRequests; ++i) { |
| backing_store()->StartJournalCleaningTimer(); |
| } |
| EXPECT_NE(0U, backing_store()->removals().size()); |
| EXPECT_TRUE(CheckBlobRemovals()); |
| #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(IndexedDBBackingStoreTest, 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); |
| { |
| IndexedDBBackingStore::Transaction transaction1( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1.Begin(CreateDummyLock()); |
| IndexedDBBackingStore::RecordIdentifier record; |
| leveldb::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()); |
| } |
| |
| { |
| IndexedDBBackingStore::Transaction transaction2( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction2.Begin(CreateDummyLock()); |
| IndexedDBValue result_value; |
| leveldb::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()); |
| } |
| |
| CycleIDBTaskRunner(); |
| } |
| |
| // Make sure that other invalid ids do not crash. |
| TEST_F(IndexedDBBackingStoreTest, InvalidIds) { |
| base::RunLoop loop; |
| idb_context_->IDBTaskRunner()->PostTask( |
| FROM_HERE, base::BindLambdaForTesting([&]() { |
| 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; |
| |
| IndexedDBBackingStore::Transaction transaction1( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1.Begin(CreateDummyLock()); |
| |
| IndexedDBBackingStore::RecordIdentifier record; |
| leveldb::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()); |
| loop.Quit(); |
| })); |
| loop.Run(); |
| } |
| |
| TEST_F(IndexedDBBackingStoreTest, CreateDatabase) { |
| base::RunLoop loop; |
| idb_context_->IDBTaskRunner()->PostTask( |
| FROM_HERE, base::BindLambdaForTesting([&]() { |
| 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"); |
| |
| IndexedDBMetadataCoding metadata_coding; |
| |
| { |
| IndexedDBDatabaseMetadata database; |
| leveldb::Status s = metadata_coding.CreateDatabase( |
| backing_store()->db(), backing_store()->origin_identifier(), |
| database_name, version, &database); |
| EXPECT_TRUE(s.ok()); |
| EXPECT_GT(database.id, 0); |
| database_id = database.id; |
| |
| IndexedDBBackingStore::Transaction transaction( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction.Begin(CreateDummyLock()); |
| |
| IndexedDBObjectStoreMetadata object_store; |
| s = metadata_coding.CreateObjectStore( |
| transaction.transaction(), database.id, object_store_id, |
| object_store_name, object_store_key_path, auto_increment, |
| &object_store); |
| EXPECT_TRUE(s.ok()); |
| |
| IndexedDBIndexMetadata index; |
| s = metadata_coding.CreateIndex( |
| transaction.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; |
| leveldb::Status s = metadata_coding.ReadMetadataForDatabaseName( |
| backing_store()->db(), backing_store()->origin_identifier(), |
| 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); |
| } |
| loop.Quit(); |
| })); |
| loop.Run(); |
| |
| { |
| // Cycle the idb runner to help clean up tasks for the Windows tests. |
| base::RunLoop cycle_loop; |
| idb_context_->IDBTaskRunner()->PostTask(FROM_HERE, |
| cycle_loop.QuitClosure()); |
| cycle_loop.Run(); |
| } |
| } |
| |
| TEST_F(IndexedDBBackingStoreTest, 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; |
| IndexedDBMetadataCoding metadata_coding; |
| |
| IndexedDBDatabaseMetadata db1; |
| leveldb::Status s = metadata_coding.CreateDatabase( |
| backing_store()->db(), backing_store()->origin_identifier(), db1_name, |
| db1_version, &db1); |
| EXPECT_TRUE(s.ok()); |
| EXPECT_GT(db1.id, 0LL); |
| |
| IndexedDBDatabaseMetadata db2; |
| s = metadata_coding.CreateDatabase(backing_store()->db(), |
| backing_store()->origin_identifier(), |
| db2_name, db2_version, &db2); |
| EXPECT_TRUE(s.ok()); |
| EXPECT_GT(db2.id, db1.id); |
| |
| std::vector<std::u16string> names; |
| s = metadata_coding.ReadDatabaseNames( |
| backing_store()->db(), backing_store()->origin_identifier(), &names); |
| EXPECT_TRUE(s.ok()); |
| ASSERT_EQ(1U, names.size()); |
| EXPECT_EQ(db1_name, names[0]); |
| } |
| |
| TEST_F(IndexedDBBackingStoreTest, ReadCorruptionInfo) { |
| auto filesystem_proxy = std::make_unique<storage::FilesystemProxy>( |
| storage::FilesystemProxy::UNRESTRICTED, base::FilePath()); |
| |
| // No |path_base|. |
| EXPECT_TRUE(indexed_db::ReadCorruptionInfo(filesystem_proxy.get(), |
| base::FilePath(), |
| StorageKey(Origin())) |
| .empty()); |
| |
| const base::FilePath path_base = temp_dir_.GetPath(); |
| const StorageKey storage_key = |
| StorageKey::CreateFromStringForTesting("http://www.google.com/"); |
| ASSERT_FALSE(path_base.empty()); |
| ASSERT_TRUE(PathIsWritable(path_base)); |
| |
| // File not found. |
| EXPECT_TRUE(indexed_db::ReadCorruptionInfo(filesystem_proxy.get(), path_base, |
| storage_key) |
| .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(filesystem_proxy.get(), path_base, |
| storage_key) |
| .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(filesystem_proxy.get(), path_base, |
| storage_key) |
| .empty()); |
| EXPECT_FALSE(PathExists(info_path)); |
| |
| // Random string. |
| ASSERT_TRUE(base::WriteFile(info_path, "foo bar")); |
| EXPECT_TRUE(indexed_db::ReadCorruptionInfo(filesystem_proxy.get(), path_base, |
| storage_key) |
| .empty()); |
| EXPECT_FALSE(PathExists(info_path)); |
| |
| // Not a dictionary. |
| ASSERT_TRUE(base::WriteFile(info_path, "[]")); |
| EXPECT_TRUE(indexed_db::ReadCorruptionInfo(filesystem_proxy.get(), path_base, |
| storage_key) |
| .empty()); |
| EXPECT_FALSE(PathExists(info_path)); |
| |
| // Empty dictionary. |
| ASSERT_TRUE(base::WriteFile(info_path, "{}")); |
| EXPECT_TRUE(indexed_db::ReadCorruptionInfo(filesystem_proxy.get(), path_base, |
| storage_key) |
| .empty()); |
| EXPECT_FALSE(PathExists(info_path)); |
| |
| // Dictionary, no message key. |
| ASSERT_TRUE(base::WriteFile(info_path, "{\"foo\":\"bar\"}")); |
| EXPECT_TRUE(indexed_db::ReadCorruptionInfo(filesystem_proxy.get(), path_base, |
| storage_key) |
| .empty()); |
| EXPECT_FALSE(PathExists(info_path)); |
| |
| // Dictionary, message key. |
| ASSERT_TRUE(base::WriteFile(info_path, "{\"message\":\"bar\"}")); |
| std::string message = indexed_db::ReadCorruptionInfo(filesystem_proxy.get(), |
| path_base, storage_key); |
| 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(filesystem_proxy.get(), path_base, |
| storage_key); |
| EXPECT_FALSE(message.empty()); |
| EXPECT_FALSE(PathExists(info_path)); |
| EXPECT_EQ("foo", message); |
| } |
| |
| // There was a wrong migration from schema 2 to 3, which always delete IDB |
| // blobs and doesn't actually write the new schema version. This tests the |
| // upgrade path where the database doesn't have blob entries, so it' safe to |
| // keep the database. |
| // https://crbug.com/756447, https://crbug.com/829125, https://crbug.com/829141 |
| TEST_F(IndexedDBBackingStoreTest, SchemaUpgradeWithoutBlobsSurvives) { |
| int64_t database_id; |
| const int64_t object_store_id = 99; |
| |
| // The database metadata needs to be written so we can verify the blob entry |
| // keys are not detected. |
| 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"); |
| |
| IndexedDBMetadataCoding metadata_coding; |
| |
| { |
| IndexedDBDatabaseMetadata database; |
| leveldb::Status s = metadata_coding.CreateDatabase( |
| backing_store()->db(), backing_store()->origin_identifier(), |
| database_name, version, &database); |
| EXPECT_TRUE(s.ok()); |
| EXPECT_GT(database.id, 0); |
| database_id = database.id; |
| |
| IndexedDBBackingStore::Transaction transaction( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction.Begin(CreateDummyLock()); |
| |
| IndexedDBObjectStoreMetadata object_store; |
| s = metadata_coding.CreateObjectStore( |
| transaction.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(); |
| |
| // Save a value. |
| IndexedDBBackingStore::Transaction transaction1( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1.Begin(CreateDummyLock()); |
| IndexedDBBackingStore::RecordIdentifier record; |
| leveldb::Status s = backing_store()->PutRecord( |
| &transaction1, database_id, object_store_id, key1_, &value1_, &record); |
| EXPECT_TRUE(s.ok()); |
| bool succeeded = false; |
| EXPECT_TRUE( |
| transaction1.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok()); |
| EXPECT_TRUE(succeeded); |
| EXPECT_TRUE(transaction1.CommitPhaseTwo().ok()); |
| |
| // Set the schema to 2, which was before blob support. |
| std::unique_ptr<LevelDBWriteBatch> write_batch = LevelDBWriteBatch::Create(); |
| const std::string schema_version_key = SchemaVersionKey::Encode(); |
| std::ignore = indexed_db::PutInt(write_batch.get(), schema_version_key, 2); |
| ASSERT_TRUE(backing_store()->db()->Write(write_batch.get()).ok()); |
| task_environment_.RunUntilIdle(); |
| |
| DestroyFactoryAndBackingStore(); |
| CreateFactoryAndBackingStore(); |
| |
| IndexedDBBackingStore::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, |
| key1_, &result_value) |
| .ok()); |
| succeeded = false; |
| EXPECT_TRUE( |
| transaction2.CommitPhaseOne(CreateBlobWriteCallback(&succeeded)).ok()); |
| EXPECT_TRUE(succeeded); |
| EXPECT_TRUE(transaction2.CommitPhaseTwo().ok()); |
| EXPECT_EQ(value1_.bits, result_value.bits); |
| |
| // Test that we upgraded. |
| int64_t found_int = 0; |
| bool found = false; |
| bool success = indexed_db::GetInt(backing_store()->db(), schema_version_key, |
| &found_int, &found) |
| .ok(); |
| ASSERT_TRUE(success); |
| |
| EXPECT_TRUE(found); |
| EXPECT_EQ(indexed_db::kLatestKnownSchemaVersion, found_int); |
| task_environment_.RunUntilIdle(); |
| } |
| |
| // Our v2->v3 schema migration code forgot to bump the on-disk version number. |
| // This test covers migrating a v3 database mislabeled as v2 to a properly |
| // labeled v3 database. When the mislabeled database has blob entries, we must |
| // treat it as corrupt and delete it. |
| // https://crbug.com/756447, https://crbug.com/829125, https://crbug.com/829141 |
| TEST_F(IndexedDBBackingStoreTestWithBlobs, SchemaUpgradeWithBlobsCorrupt) { |
| int64_t database_id; |
| const int64_t object_store_id = 99; |
| |
| // The database metadata needs to be written so the blob entry keys can |
| // be detected. |
| 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"); |
| |
| IndexedDBMetadataCoding metadata_coding; |
| |
| { |
| IndexedDBDatabaseMetadata database; |
| leveldb::Status s = metadata_coding.CreateDatabase( |
| backing_store()->db(), backing_store()->origin_identifier(), |
| database_name, version, &database); |
| EXPECT_TRUE(s.ok()); |
| EXPECT_GT(database.id, 0); |
| database_id = database.id; |
| |
| IndexedDBBackingStore::Transaction transaction( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction.Begin(CreateDummyLock()); |
| |
| IndexedDBObjectStoreMetadata object_store; |
| s = metadata_coding.CreateObjectStore( |
| transaction.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(); |
| |
| base::RunLoop write_blobs_loop; |
| // Initiate transaction1 - writing blobs. |
| std::unique_ptr<IndexedDBBackingStore::Transaction> transaction1 = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1->Begin(CreateDummyLock()); |
| IndexedDBBackingStore::RecordIdentifier record; |
| EXPECT_TRUE(backing_store() |
| ->PutRecord(transaction1.get(), database_id, object_store_id, |
| key3_, &value3_, &record) |
| .ok()); |
| bool succeeded = false; |
| EXPECT_TRUE(transaction1 |
| ->CommitPhaseOne(CreateBlobWriteCallback( |
| &succeeded, write_blobs_loop.QuitClosure())) |
| .ok()); |
| task_environment_.RunUntilIdle(); |
| write_blobs_loop.Run(); |
| |
| // Finish up transaction1, verifying blob writes. |
| EXPECT_TRUE(succeeded); |
| EXPECT_TRUE(CheckBlobWrites()); |
| EXPECT_TRUE(transaction1->CommitPhaseTwo().ok()); |
| |
| // Set the schema to 2, which was before blob support. |
| std::unique_ptr<LevelDBWriteBatch> write_batch = LevelDBWriteBatch::Create(); |
| const std::string schema_version_key = SchemaVersionKey::Encode(); |
| std::ignore = indexed_db::PutInt(write_batch.get(), schema_version_key, 2); |
| ASSERT_TRUE(backing_store()->db()->Write(write_batch.get()).ok()); |
| |
| // Clean up on the IDB sequence. |
| transaction1.reset(); |
| task_environment_.RunUntilIdle(); |
| |
| DestroyFactoryAndBackingStore(); |
| CreateFactoryAndBackingStore(); |
| |
| // The factory returns a null backing store pointer when there is a corrupt |
| // database. |
| EXPECT_TRUE(data_loss_info_.status == blink::mojom::IDBDataLoss::Total); |
| } |
| |
| 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; |
| } |
| |
| } // namespace |
| |
| TEST_F(IndexedDBBackingStoreTestWithBlobs, 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"); |
| |
| IndexedDBMetadataCoding metadata_coding; |
| |
| { |
| IndexedDBDatabaseMetadata database; |
| leveldb::Status s = metadata_coding.CreateDatabase( |
| backing_store()->db(), backing_store()->origin_identifier(), |
| database_name, version, &database); |
| EXPECT_TRUE(s.ok()); |
| EXPECT_GT(database.id, 0); |
| database_id = database.id; |
| |
| IndexedDBBackingStore::Transaction transaction( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction.Begin(CreateDummyLock()); |
| |
| IndexedDBObjectStoreMetadata object_store; |
| s = metadata_coding.CreateObjectStore( |
| transaction.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<IndexedDBBackingStore::Transaction> transaction1 = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1->Begin(CreateDummyLock()); |
| IndexedDBBackingStore::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_); |
| base::StringPiece 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(base::StringPiece(blob_entry_key.Encode()), |
| base::StringPiece(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_bytes(base::make_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_bytes(base::make_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. |
| IndexedDBBackingStore::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(IndexedDBBackingStoreTestWithBlobs, 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); |
| |
| IndexedDBMetadataCoding metadata_coding; |
| |
| { |
| IndexedDBDatabaseMetadata database; |
| leveldb::Status s = metadata_coding.CreateDatabase( |
| backing_store()->db(), backing_store()->origin_identifier(), |
| database_name, version, &database); |
| EXPECT_TRUE(s.ok()); |
| EXPECT_GT(database.id, 0); |
| database_id = database.id; |
| |
| IndexedDBBackingStore::Transaction transaction( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction.Begin(CreateDummyLock()); |
| |
| IndexedDBObjectStoreMetadata object_store; |
| s = metadata_coding.CreateObjectStore( |
| transaction.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<IndexedDBBackingStore::Transaction> transaction = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction->Begin(CreateDummyLock()); |
| IndexedDBBackingStore::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; |
| auto filesystem_proxy = std::make_unique<storage::FilesystemProxy>( |
| storage::FilesystemProxy::UNRESTRICTED, base::FilePath()); |
| filesystem_proxy->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(IndexedDBBackingStoreTestWithExternalObjects, 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<IndexedDBBackingStore::Transaction> transaction1 = |
| std::make_unique<IndexedDBBackingStore::Transaction>( |
| backing_store()->AsWeakPtr(), |
| blink::mojom::IDBTransactionDurability::Relaxed, |
| blink::mojom::IDBTransactionMode::ReadWrite); |
| transaction1->Begin(CreateDummyLock()); |
| IndexedDBBackingStore::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<IndexedDBBackingStore::Transaction> transaction2 = |
| std::make_unique<IndexedDBBackingStore::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. |
| ASSERT_EQ(4UL, backing_store()->removals().size()); |
| EXPECT_EQ(blob_context_->writes()[0].path, backing_store()->removals()[0]); |
| EXPECT_EQ(blob_context_->writes()[1].path, backing_store()->removals()[1]); |
| EXPECT_EQ(blob_context_->writes()[2].path, backing_store()->removals()[2]); |
| EXPECT_EQ(blob_context_->writes()[3].path, backing_store()->removals()[3]); |
| |
| // Clean up on the IDB sequence. |
| transaction2.reset(); |
| task_environment_.RunUntilIdle(); |
| } |
| |
| } // namespace indexed_db_backing_store_unittest |
| } // namespace content |