| // Copyright 2025 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/vfs/sqlite_sandboxed_vfs.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/types/expected_macros.h" |
| #include "components/persistent_cache/pending_backend.h" |
| #include "components/persistent_cache/sqlite/backend_storage_delegate.h" |
| #include "components/persistent_cache/sqlite/sqlite_backend_impl.h" |
| #include "components/persistent_cache/sqlite/vfs/sqlite_database_vfs_file_set.h" |
| #include "sql/database.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/sqlite/sqlite3.h" |
| |
| namespace persistent_cache { |
| |
| namespace { |
| |
| const base::FilePath kNonExistentVirtualFilePath = |
| base::FilePath::FromASCII("NotFound"); |
| |
| } // namespace |
| |
| class SqliteSandboxedVfsTest : public testing::Test { |
| protected: |
| static constexpr int kOpenFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_MAIN_DB; |
| |
| void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); } |
| |
| std::optional<SqliteVfsFileSet> CreateFilesAndBuildVfsFileSet() { |
| std::optional<SqliteVfsFileSet> file_set; |
| if (auto pending_backend = backend_storage_delegate_.MakePendingBackend( |
| temp_dir_.GetPath(), base::FilePath(kTestBaseName), |
| /*single_connection=*/false, /*journal_mode_wal=*/false); |
| !pending_backend.has_value()) { |
| ADD_FAILURE() << "Failed creating pending backend"; |
| } else { |
| file_set = SqliteBackendImpl::BindToFileSet(*std::move(pending_backend)); |
| EXPECT_NE(file_set, std::nullopt) << "Failed binding to pending backend"; |
| } |
| return file_set; |
| } |
| |
| // Returns a read-only view of the same database as `file_set`. |
| std::optional<SqliteVfsFileSet> GetReadOnlyVfsFileSet( |
| const SqliteVfsFileSet& file_set) { |
| std::optional<SqliteVfsFileSet> read_only_file_set; |
| if (auto pending_backend = backend_storage_delegate_.ShareConnection( |
| temp_dir_.GetPath(), base::FilePath(kTestBaseName), file_set, |
| /*read_write=*/false); |
| !pending_backend.has_value()) { |
| ADD_FAILURE() << "Failed sharing file set"; |
| } else { |
| read_only_file_set = |
| SqliteBackendImpl::BindToFileSet(*std::move(pending_backend)); |
| EXPECT_NE(read_only_file_set, std::nullopt) |
| << "Failed binding to pending backend"; |
| } |
| return read_only_file_set; |
| } |
| |
| private: |
| static constexpr base::FilePath::StringViewType kTestBaseName = |
| FILE_PATH_LITERAL("TEST"); |
| |
| base::ScopedTempDir temp_dir_; |
| sqlite::BackendStorageDelegate backend_storage_delegate_; |
| }; |
| |
| TEST_F(SqliteSandboxedVfsTest, NoAccessWithoutRegistering) { |
| ASSERT_OK_AND_ASSIGN(SqliteVfsFileSet vfs_file_set, |
| CreateFilesAndBuildVfsFileSet()); |
| EXPECT_FALSE(SqliteSandboxedVfsDelegate::GetInstance() |
| ->OpenFile(kNonExistentVirtualFilePath, kOpenFlags) |
| .IsValid()); |
| } |
| |
| TEST_F(SqliteSandboxedVfsTest, AccessAfterRegistering) { |
| ASSERT_OK_AND_ASSIGN(SqliteVfsFileSet vfs_file_set, |
| CreateFilesAndBuildVfsFileSet()); |
| |
| SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner = |
| SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles( |
| vfs_file_set); |
| |
| EXPECT_TRUE(SqliteSandboxedVfsDelegate::GetInstance() |
| ->OpenFile(vfs_file_set.GetDbVirtualFilePath(), |
| SQLITE_OPEN_READWRITE | SQLITE_OPEN_MAIN_DB) |
| .IsValid()); |
| EXPECT_TRUE(SqliteSandboxedVfsDelegate::GetInstance() |
| ->OpenFile(vfs_file_set.GetJournalVirtualFilePath(), |
| SQLITE_OPEN_READWRITE | SQLITE_OPEN_MAIN_JOURNAL) |
| .IsValid()); |
| } |
| |
| TEST_F(SqliteSandboxedVfsTest, NoAccessAfterUnregistering) { |
| ASSERT_OK_AND_ASSIGN(SqliteVfsFileSet vfs_file_set, |
| CreateFilesAndBuildVfsFileSet()); |
| |
| // Register and immediately unregister. |
| { |
| SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner = |
| SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles( |
| vfs_file_set); |
| } |
| |
| EXPECT_FALSE(SqliteSandboxedVfsDelegate::GetInstance() |
| ->OpenFile(vfs_file_set.GetDbVirtualFilePath(), |
| SQLITE_OPEN_READWRITE | SQLITE_OPEN_MAIN_DB) |
| .IsValid()); |
| EXPECT_FALSE(SqliteSandboxedVfsDelegate::GetInstance() |
| ->OpenFile(vfs_file_set.GetJournalVirtualFilePath(), |
| SQLITE_OPEN_READWRITE | SQLITE_OPEN_MAIN_JOURNAL) |
| .IsValid()); |
| } |
| |
| TEST_F(SqliteSandboxedVfsTest, AccessAfterReRegistering) { |
| ASSERT_OK_AND_ASSIGN(SqliteVfsFileSet vfs_file_set, |
| CreateFilesAndBuildVfsFileSet()); |
| |
| // Register and immediately unregister. |
| { |
| SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner = |
| SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles( |
| vfs_file_set); |
| } |
| |
| // Register again. |
| SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner = |
| SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles( |
| vfs_file_set); |
| |
| EXPECT_TRUE(SqliteSandboxedVfsDelegate::GetInstance() |
| ->OpenFile(vfs_file_set.GetDbVirtualFilePath(), |
| SQLITE_OPEN_READWRITE | SQLITE_OPEN_MAIN_DB) |
| .IsValid()); |
| EXPECT_TRUE(SqliteSandboxedVfsDelegate::GetInstance() |
| ->OpenFile(vfs_file_set.GetJournalVirtualFilePath(), |
| SQLITE_OPEN_READWRITE | SQLITE_OPEN_MAIN_JOURNAL) |
| .IsValid()); |
| } |
| |
| TEST_F(SqliteSandboxedVfsTest, DeleteFile) { |
| ASSERT_OK_AND_ASSIGN(SqliteVfsFileSet vfs_file_set, |
| CreateFilesAndBuildVfsFileSet()); |
| |
| // Impossible to delete non-registered files. |
| EXPECT_EQ(SqliteSandboxedVfsDelegate::GetInstance()->DeleteFile( |
| vfs_file_set.GetDbVirtualFilePath(), /*sync_dir=*/true), |
| SQLITE_NOTFOUND); |
| EXPECT_EQ(SqliteSandboxedVfsDelegate::GetInstance()->DeleteFile( |
| vfs_file_set.GetJournalVirtualFilePath(), /*sync_dir=*/true), |
| SQLITE_NOTFOUND); |
| |
| SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner = |
| SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles( |
| vfs_file_set); |
| |
| // Get a file to test with and open it. |
| const base::FilePath file_path_to_delete = |
| vfs_file_set.GetDbVirtualFilePath(); |
| SandboxedFile* file_to_delete = vfs_file_set.GetSandboxedDbFile(); |
| file_to_delete->OnFileOpened( |
| file_to_delete->TakeUnderlyingFile(SandboxedFile::FileType::kMainDb)); |
| EXPECT_TRUE(file_to_delete->IsValid()); |
| |
| // Write something to the file. |
| const std::string content = "hello"; |
| EXPECT_EQ(file_to_delete->Write(content.data(), content.size(), 0), |
| SQLITE_OK); |
| |
| // Deleting the open file succeeds and empties the file. |
| EXPECT_EQ(SqliteSandboxedVfsDelegate::GetInstance()->DeleteFile( |
| file_path_to_delete, true), |
| SQLITE_OK); |
| EXPECT_EQ(file_to_delete->OpenedFileForTesting().GetLength(), 0); |
| |
| // Write to the file again, then close it. |
| EXPECT_EQ(file_to_delete->Write(content.data(), content.size(), 0), |
| SQLITE_OK); |
| file_to_delete->Close(); |
| EXPECT_FALSE(file_to_delete->IsValid()); |
| |
| // Deleting the closed file succeeds and empties the file. |
| EXPECT_EQ(SqliteSandboxedVfsDelegate::GetInstance()->DeleteFile( |
| file_path_to_delete, true), |
| SQLITE_OK); |
| EXPECT_EQ(file_to_delete->UnderlyingFileForTesting().GetLength(), 0); |
| } |
| |
| TEST_F(SqliteSandboxedVfsTest, DeleteFileFails) { |
| // Get read/write and read-only views of a db and register them. |
| ASSERT_OK_AND_ASSIGN(SqliteVfsFileSet vfs_file_set, |
| CreateFilesAndBuildVfsFileSet()); |
| SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner = |
| SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles( |
| vfs_file_set); |
| |
| ASSERT_OK_AND_ASSIGN(SqliteVfsFileSet read_only_vfs_file_set, |
| GetReadOnlyVfsFileSet(vfs_file_set)); |
| SqliteSandboxedVfsDelegate::UnregisterRunner read_only_unregister_runner = |
| SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles( |
| read_only_vfs_file_set); |
| |
| // Write some data to the db file and close it. |
| { |
| const base::FilePath file_path_to_delete = |
| vfs_file_set.GetDbVirtualFilePath(); |
| SandboxedFile* file_to_delete = vfs_file_set.GetSandboxedDbFile(); |
| file_to_delete->OnFileOpened( |
| file_to_delete->TakeUnderlyingFile(SandboxedFile::FileType::kMainDb)); |
| EXPECT_TRUE(file_to_delete->IsValid()); |
| const std::string_view content = "hello"; |
| EXPECT_EQ(file_to_delete->Write(content.data(), content.size(), 0), |
| SQLITE_OK); |
| file_to_delete->Close(); |
| } |
| |
| // Deleting from the read-only view should fail. |
| base::HistogramTester histogram_tester; |
| { |
| const base::FilePath file_path_to_delete = |
| read_only_vfs_file_set.GetDbVirtualFilePath(); |
| EXPECT_EQ(SqliteSandboxedVfsDelegate::GetInstance()->DeleteFile( |
| file_path_to_delete, true), |
| SQLITE_IOERR_DELETE); |
| } |
| // The exact error reported differs by platform, so accept any value. |
| histogram_tester.ExpectTotalCount( |
| "PersistentCache.Sqlite.DbFile.SetLengthResult", 1); |
| } |
| |
| TEST_F(SqliteSandboxedVfsTest, OpenFile) { |
| ASSERT_OK_AND_ASSIGN(SqliteVfsFileSet vfs_file_set, |
| CreateFilesAndBuildVfsFileSet()); |
| |
| int64_t length = 0; |
| vfs_file_set.GetSandboxedDbFile()->UnderlyingFileForTesting().SetLength( |
| length += 100); |
| vfs_file_set.GetSandboxedJournalFile()->UnderlyingFileForTesting().SetLength( |
| length += 100); |
| |
| SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner = |
| SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles( |
| vfs_file_set); |
| |
| const struct { |
| SandboxedFile* (SqliteVfsFileSet::*get_sandboxed_file_fn)() const; |
| base::FilePath (SqliteVfsFileSet::*get_sandboxed_file_path_fn)() const; |
| int open_flags; |
| } kAccessors[] = { |
| {&SqliteVfsFileSet::GetSandboxedDbFile, |
| &SqliteVfsFileSet::GetDbVirtualFilePath, |
| SQLITE_OPEN_READWRITE | SQLITE_OPEN_MAIN_DB}, |
| {&SqliteVfsFileSet::GetSandboxedJournalFile, |
| &SqliteVfsFileSet::GetJournalVirtualFilePath, |
| SQLITE_OPEN_READWRITE | SQLITE_OPEN_MAIN_JOURNAL}, |
| }; |
| |
| for (const auto& accessors : kAccessors) { |
| SandboxedFile* const sandboxed_file = |
| (vfs_file_set.*accessors.get_sandboxed_file_fn)(); |
| const base::FilePath sandboxed_file_path = |
| (vfs_file_set.*accessors.get_sandboxed_file_path_fn)(); |
| int open_flags = accessors.open_flags; |
| base::File::Info info_from_map; |
| sandboxed_file->UnderlyingFileForTesting().GetInfo(&info_from_map); |
| |
| // Simulate an open from the VFS. |
| base::File from_delegate = |
| SqliteSandboxedVfsDelegate::GetInstance()->OpenFile(sandboxed_file_path, |
| open_flags); |
| |
| base::File::Info info_from_delegate; |
| from_delegate.GetInfo(&info_from_delegate); |
| EXPECT_EQ(info_from_delegate.size, info_from_map.size); |
| |
| // Simulate the binding done by the VFS. |
| sandboxed_file->OnFileOpened(std::move(from_delegate)); |
| |
| base::File::Info info_from_opened_file; |
| sandboxed_file->OpenedFileForTesting().GetInfo(&info_from_opened_file); |
| EXPECT_EQ(info_from_opened_file.size, info_from_map.size); |
| } |
| } |
| |
| TEST_F(SqliteSandboxedVfsTest, SqliteIntegration) { |
| ASSERT_OK_AND_ASSIGN(SqliteVfsFileSet vfs_file_set, |
| CreateFilesAndBuildVfsFileSet()); |
| SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner = |
| SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles( |
| vfs_file_set); |
| |
| sql::Database db(sql::DatabaseOptions().set_vfs_name_discouraged( |
| SqliteSandboxedVfsDelegate::kSqliteVfsName), |
| "Test"); |
| EXPECT_TRUE(db.Open(vfs_file_set.GetDbVirtualFilePath())); |
| } |
| |
| } // namespace persistent_cache |