#include "content/browser/indexed_db/instance/sqlite/database_connection.h"
#include <memory>
#include <string>
#include <string_view>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/browser/indexed_db/file_path_util.h"
#include "content/browser/indexed_db/indexed_db_data_loss_info.h"
#include "content/browser/indexed_db/indexed_db_value.h"
#include "content/browser/indexed_db/instance/backing_store_util.h"
#include "content/browser/indexed_db/instance/sqlite/backing_store_impl.h"
#include "content/browser/indexed_db/status.h"
#include "sql/meta_table.h"
#include "sql/test/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom-data-view.h"
namespace content::indexed_db::sqlite {
namespace {
BlobWriteCallback CreateBlobWriteCallback(
bool* succeeded,
base::OnceClosure on_done = base::DoNothing()) {
*succeeded = false;
return base::BindOnce(
[](bool* succeeded, base::OnceClosure on_done,
StatusOr<BlobWriteResult> result) {
*succeeded = result.has_value();
std::move(on_done).Run();
return Status::OK();
},
succeeded, std::move(on_done));
}
}
class MockBlobStorageContext : public ::storage::mojom::BlobStorageContext {
public:
MockBlobStorageContext() = default;
~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 {
NOTREACHED();
}
void Clone(mojo::PendingReceiver<::storage::mojom::BlobStorageContext>
receiver) override {
NOTREACHED();
}
};
class DatabaseConnectionTest : public testing::Test {
public:
static constexpr int kObjectStoreId = 42;
const blink::IndexedDBKey kKey;
const IndexedDBValue kValue;
DatabaseConnectionTest() : kKey("key"), kValue("deadbeef", {}) {}
~DatabaseConnectionTest() override = default;
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
backing_store_ =
std::make_unique<BackingStoreImpl>(temp_dir_.GetPath(), blob_context_);
}
void TearDown() override { backing_store_.reset(); }
DatabaseConnection* GetDatabaseConnection(const std::u16string& name) {
auto it = backing_store()->open_connections_.find(name);
return it == backing_store()->open_connections_.end() ? nullptr
: it->second.get();
}
protected:
BackingStoreImpl* backing_store() {
return reinterpret_cast<BackingStoreImpl*>(backing_store_.get());
}
std::unique_ptr<BackingStore::Database> OpenDb(std::u16string_view name) {
StatusOr<std::unique_ptr<BackingStore::Database>> db =
backing_store()->CreateOrOpenDatabase(std::u16string(name));
EXPECT_TRUE(db.has_value());
EXPECT_TRUE(db.value().get());
return std::move(db.value());
}
base::FilePath GetDatabasePath(std::u16string_view name) {
return temp_dir_.GetPath().Append(DatabaseNameToFileName(name));
}
void InitializeDbWithOneRecord(BackingStore::Database& db) {
auto vc =
db.CreateTransaction(blink::mojom::IDBTransactionDurability::Default,
blink::mojom::IDBTransactionMode::VersionChange);
vc->Begin({});
ASSERT_TRUE(
vc->CreateObjectStore(kObjectStoreId, u"object store name", {}, true)
.ok());
ASSERT_TRUE(vc->PutRecord(kObjectStoreId, kKey.Clone(), kValue.Clone())
.has_value());
ASSERT_TRUE(vc->SetDatabaseVersion(1).ok());
bool succeeded = false;
ASSERT_TRUE(
vc->CommitPhaseOne(CreateBlobWriteCallback(&succeeded), {}).ok());
EXPECT_TRUE(succeeded);
ASSERT_TRUE(vc->CommitPhaseTwo().ok());
}
base::test::TaskEnvironment task_environment_;
MockBlobStorageContext blob_context_;
base::ScopedTempDir temp_dir_;
std::unique_ptr<BackingStore> backing_store_;
};
TEST_F(DatabaseConnectionTest, TooNew) {
base::HistogramTester histograms;
const std::u16string_view kDbName{u"test db"};
auto connection = OpenDb(kDbName);
ASSERT_NO_FATAL_FAILURE(InitializeDbWithOneRecord(*connection));
connection.reset();
const base::FilePath db_path = GetDatabasePath(kDbName);
ASSERT_TRUE(base::PathExists(db_path));
histograms.ExpectUniqueSample(
"IndexedDB.SQLite.SpecificEvent.OnDisk",
DatabaseConnection::SpecificEvent::kDatabaseOpenAttempt, 1);
auto sql_db = std::make_unique<sql::Database>(sql::DatabaseOptions()
.set_exclusive_locking(true)
.set_wal_mode(true)
.set_enable_triggers(true),
sql::test::kTestTag);
ASSERT_TRUE(sql_db->Open(db_path));
ASSERT_TRUE(sql::MetaTable::DoesTableExist(sql_db.get()));
int original_version, original_compat_version;
{
sql::MetaTable meta_table;
EXPECT_TRUE(meta_table.Init(sql_db.get(), 42,
42));
original_version = meta_table.GetVersionNumber();
original_compat_version = meta_table.GetCompatibleVersionNumber();
EXPECT_TRUE(meta_table.SetVersionNumber(1000));
EXPECT_TRUE(meta_table.SetCompatibleVersionNumber(1000));
}
sql_db->Close();
connection = OpenDb(kDbName);
ASSERT_NO_FATAL_FAILURE(InitializeDbWithOneRecord(*connection));
connection.reset();
histograms.ExpectBucketCount(
"IndexedDB.SQLite.SpecificEvent.OnDisk",
DatabaseConnection::SpecificEvent::kDatabaseOpenAttempt, 3);
histograms.ExpectBucketCount(
"IndexedDB.SQLite.SpecificEvent.OnDisk",
DatabaseConnection::SpecificEvent::kDatabaseTooNew, 1);
histograms.ExpectBucketCount(
"IndexedDB.SQLite.SpecificEvent.OnDisk",
DatabaseConnection::SpecificEvent::kDatabaseHadSqlError, 0);
ASSERT_TRUE(sql_db->Open(db_path));
ASSERT_TRUE(sql::MetaTable::DoesTableExist(sql_db.get()));
{
sql::MetaTable meta_table;
EXPECT_TRUE(meta_table.Init(sql_db.get(), 42,
42));
EXPECT_EQ(original_version, meta_table.GetVersionNumber());
EXPECT_EQ(original_compat_version, meta_table.GetCompatibleVersionNumber());
}
}
class DatabaseConnectionCorruptionTest : public DatabaseConnectionTest {
public:
DatabaseConnectionCorruptionTest() = default;
void VerifyCorruptionHandling(
base::RepeatingCallback<StatusOr<IndexedDBValue>(
BackingStore::Transaction&)> read_value_callback) {
const std::u16string_view kDbName{u"test db"};
auto db = OpenDb(kDbName);
ASSERT_NO_FATAL_FAILURE(InitializeDbWithOneRecord(*db));
auto read_value = [&]() {
auto ro =
db->CreateTransaction(blink::mojom::IDBTransactionDurability::Default,
blink::mojom::IDBTransactionMode::ReadOnly);
ro->Begin({});
StatusOr<IndexedDBValue> value = read_value_callback.Run(*ro);
ro->Rollback();
return value;
};
StatusOr<IndexedDBValue> value = read_value();
ASSERT_TRUE(value.has_value());
EXPECT_EQ(base::span(value.value().bits), base::span(kValue.bits));
StatusOr<base::DictValue> contents_before_corruption =
SnapshotDatabase(*db);
ASSERT_TRUE(contents_before_corruption.has_value());
db.reset();
const base::FilePath db_path = GetDatabasePath(kDbName);
ASSERT_TRUE(sql::test::CorruptIndexRootPage(db_path, "records_by_key"));
db = OpenDb(kDbName);
value = read_value();
ASSERT_FALSE(value.has_value());
EXPECT_TRUE(value.error().IsCorruption());
db.reset();
db = OpenDb(kDbName);
auto verify_recovery = [&]() {
StatusOr<IndexedDBValue> recovered_value = read_value();
#if BUILDFLAG(IS_FUCHSIA)
ASSERT_TRUE(recovered_value.has_value());
EXPECT_TRUE(recovered_value.value().empty());
ASSERT_NO_FATAL_FAILURE(InitializeDbWithOneRecord(*db));
recovered_value = read_value();
#else
StatusOr<base::DictValue> contents_after_recovery = SnapshotDatabase(*db);
ASSERT_TRUE(contents_after_recovery.has_value());
EXPECT_EQ(*contents_after_recovery, *contents_before_corruption);
#endif
ASSERT_TRUE(recovered_value.has_value());
EXPECT_EQ(base::span(recovered_value.value().bits),
base::span(kValue.bits));
};
verify_recovery();
db.reset();
ASSERT_TRUE(sql::test::CorruptSizeInHeader(db_path));
db = OpenDb(kDbName);
verify_recovery();
}
};
TEST_F(DatabaseConnectionCorruptionTest, Get) {
VerifyCorruptionHandling(
base::BindLambdaForTesting([&](BackingStore::Transaction& ro) {
return ro.GetRecord(kObjectStoreId, kKey);
}));
}
TEST_F(DatabaseConnectionCorruptionTest, ObjectStoreCursor) {
VerifyCorruptionHandling(
base::BindLambdaForTesting([&](BackingStore::Transaction& ro) {
return ro
.OpenObjectStoreCursor(kObjectStoreId, blink::IndexedDBKeyRange(),
blink::mojom::IDBCursorDirection::Next)
.transform([](std::unique_ptr<BackingStore::Cursor> cursor)
-> IndexedDBValue {
if (!cursor) {
return {};
}
return cursor->GetValue().Clone();
});
}));
}
}