| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "components/persistent_cache/sqlite/sqlite_backend_impl.h" | |
| #include <stdint.h> | |
| #include <array> | |
| #include <optional> | |
| #include "base/containers/span.h" | |
| #include "base/files/scoped_temp_dir.h" | |
| #include "base/numerics/byte_conversions.h" | |
| #include "base/test/gmock_expected_support.h" | |
| #include "components/persistent_cache/backend_storage.h" | |
| #include "components/persistent_cache/backend_type.h" | |
| #include "components/persistent_cache/client.h" | |
| #include "components/persistent_cache/persistent_cache.h" | |
| #include "testing/gmock/include/gmock/gmock.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| namespace persistent_cache { | |
| namespace { | |
| using ::base::test::ErrorIs; | |
| using ::base::test::ValueIs; | |
| using ::testing::Eq; | |
| using ::testing::Ne; | |
| using ::testing::Optional; | |
| // See https://sqlite.org/fileformat2.html#user_version_number | |
| constexpr int64_t kUserVersionOffset = 60; | |
| // Make sure the files are initialized with an invalid version. | |
| // 0 is chosen for two reasons: It's a value this is not equal to | |
| // `kCurrentUserVersion` so validates that mismatches are not tolerated. It's | |
| // also the default value present prior to the use of user_version and real | |
| // files exist in clients with no specific user versions and tables which need | |
| // to be discarded. | |
| constexpr int kInvalidVersion = 0; | |
| } // namespace | |
| class SQLiteBackendImplTest : public testing::Test { | |
| protected: | |
| void SetUp() override { | |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); | |
| backend_storage_.emplace(Client::kTest, BackendType::kSqlite, | |
| temp_dir_.GetPath()); | |
| } | |
| base::ScopedTempDir temp_dir_; | |
| std::optional<BackendStorage> backend_storage_; | |
| }; | |
| TEST_F(SQLiteBackendImplTest, ReopeningFilesWithSameUserVersionWorks) { | |
| const base::FilePath db_basename = base::FilePath::FromASCII("Cache"); | |
| ASSERT_OK_AND_ASSIGN( | |
| auto pending_backend, | |
| backend_storage_->MakePendingBackend(db_basename, false, false)); | |
| // Initialize the backend and close it. | |
| ASSERT_THAT( | |
| SqliteBackendImpl::Bind(std::move(pending_backend), Client::kTest), | |
| ValueIs(Ne(nullptr))); | |
| ASSERT_OK_AND_ASSIGN(pending_backend, backend_storage_->MakePendingBackend( | |
| db_basename, false, false)); | |
| // NOTE: This does not respect the database locks and so is done when there | |
| // are no live backends. | |
| std::array<uint8_t, sizeof(int)> data; | |
| ASSERT_THAT( | |
| pending_backend.pending_file_set.db_file.Read(kUserVersionOffset, data), | |
| Optional(sizeof(int))); | |
| ASSERT_EQ(base::I32FromBigEndian(data), | |
| SqliteBackendImpl::kCurrentUserVersion); | |
| ASSERT_THAT( | |
| SqliteBackendImpl::Bind(std::move(pending_backend), Client::kTest), | |
| ValueIs(Ne(nullptr))); | |
| } | |
| TEST_F(SQLiteBackendImplTest, | |
| VersionMismatchLeadsToFailedInitializeWhenReadOnly) { | |
| const base::FilePath db_path = base::FilePath::FromASCII("Cache"); | |
| ASSERT_OK_AND_ASSIGN( | |
| auto pending_backend, | |
| backend_storage_->MakePendingBackend(db_path, false, false)); | |
| base::File db_file = pending_backend.pending_file_set.db_file.Duplicate(); | |
| ASSERT_OK_AND_ASSIGN( | |
| auto cache, | |
| PersistentCache::Bind(Client::kTest, std::move(pending_backend))); | |
| ASSERT_NE(cache, nullptr); | |
| ASSERT_OK_AND_ASSIGN( | |
| pending_backend, | |
| backend_storage_->ShareReadOnlyConnection(db_path, *cache)); | |
| // Manually set the user_version in the db file to an invalid value. | |
| // NOTE: This does not respect the database locks and so is done when there | |
| // are no live backends. | |
| cache.reset(); | |
| ASSERT_THAT( | |
| db_file.Write(kUserVersionOffset, base::I32ToBigEndian(kInvalidVersion)), | |
| Optional(sizeof(int))); | |
| // Mismatched version numbers makes Bind() fail. | |
| ASSERT_THAT( | |
| SqliteBackendImpl::Bind(std::move(pending_backend), Client::kTest), | |
| ErrorIs(TransactionError::kConnectionError)); | |
| } | |
| TEST_F(SQLiteBackendImplTest, | |
| VersionMismatchDropsTablesWithReadWriteConnection) { | |
| const base::FilePath db_basename = base::FilePath::FromASCII("Cache"); | |
| ASSERT_OK_AND_ASSIGN( | |
| auto pending_backend, | |
| backend_storage_->MakePendingBackend(db_basename, false, false)); | |
| ASSERT_OK_AND_ASSIGN( | |
| auto cache, | |
| PersistentCache::Bind(Client::kTest, std::move(pending_backend))); | |
| ASSERT_NE(cache, nullptr); | |
| const base::span<const uint8_t> kKey = base::byte_span_from_cstring("key"); | |
| EXPECT_OK(cache->Insert(kKey, base::byte_span_from_cstring("1"))); | |
| EXPECT_THAT(cache->Find(kKey, [](size_t) { return base::span<uint8_t>(); }), | |
| ValueIs(Ne(std::nullopt))); | |
| ASSERT_OK_AND_ASSIGN( | |
| pending_backend, | |
| backend_storage_->ShareReadWriteConnection(db_basename, *cache)); | |
| // Manually set the user_version in the db file to an invalid value. | |
| // NOTE: This does not respect the database locks and so is done when there | |
| // are no live backends. | |
| cache.reset(); | |
| ASSERT_THAT(pending_backend.pending_file_set.db_file.Write( | |
| kUserVersionOffset, base::I32ToBigEndian(kInvalidVersion)), | |
| Optional(sizeof(int))); | |
| // Bind succeeds. | |
| ASSERT_OK_AND_ASSIGN( | |
| auto backend, | |
| SqliteBackendImpl::Bind(std::move(pending_backend), Client::kTest)); | |
| ASSERT_NE(backend, nullptr); | |
| // The files were wiped so the value inserted is no longer present. | |
| EXPECT_THAT(backend->Find(kKey, [](size_t) { return base::span<uint8_t>(); }), | |
| ValueIs(Eq(std::nullopt))); | |
| } | |
| TEST_F(SQLiteBackendImplTest, BindFailsWhenDatabaseFileIsInvalid) { | |
| const base::FilePath db_path = base::FilePath::FromASCII("Cache"); | |
| ASSERT_OK_AND_ASSIGN( | |
| auto pending_backend, | |
| backend_storage_->MakePendingBackend(db_path, false, false)); | |
| // Invalidate the database file handle. | |
| pending_backend.pending_file_set.db_file = base::File(); | |
| // Bind should fail because the database file is invalid. | |
| // With the fix, we should get kConnectionError (due to SQLITE_CANTOPEN). | |
| // Without the fix, we would get kPermanent (due to SQLITE_ERROR fallback). | |
| ASSERT_THAT( | |
| SqliteBackendImpl::Bind(std::move(pending_backend), Client::kTest), | |
| ErrorIs(TransactionError::kConnectionError)); | |
| } | |
| } // namespace persistent_cache |