From 102ceb4133685db70ac15cb5bcb134a85736fe86 Mon Sep 17 00:00:00 2001 From: erg Date: Fri, 19 Jun 2015 18:38:13 -0700 Subject: mandoline filesystem: add a sqlite3 vfs to proxy filesystem usage. This adds a //sql/mojo library which can be linked into preexisting sqlite3 code which adds a new VFS which transparently proxies filesystem usage to the mojo:filesystem application. We create a new sql_apptests.mojo target, which currently has all the sql connection_unittests.cc (minus 2 hard ones), all statement and transaction unit tests and refactor the sql testing stuff so that we have two implementations of an SQLTestBase class: one that uses files raw and one that proxies to the filesystem process. Notably, this patch does not implement file locking, which will have to be implemented before we can safely use this, but will be a large enough patch in and of itself that I'm punting on it for this patch. BUG=493311 Review URL: https://codereview.chromium.org/1176653002 Cr-Commit-Position: refs/heads/master@{#335415} --- sql/BUILD.gn | 14 ++ sql/connection.cc | 51 +++-- sql/connection_unittest.cc | 86 +++------ sql/correct_sql_test_base.h | 27 +++ sql/meta_table_unittest.cc | 17 +- sql/mojo/BUILD.gn | 62 ++++++ sql/mojo/DEPS | 6 + sql/mojo/mojo_vfs.cc | 413 ++++++++++++++++++++++++++++++++++++++++ sql/mojo/mojo_vfs.h | 45 +++++ sql/mojo/sql_test_base.cc | 156 +++++++++++++++ sql/mojo/sql_test_base.h | 85 +++++++++ sql/mojo/vfs_unittest.cc | 317 ++++++++++++++++++++++++++++++ sql/recovery_unittest.cc | 28 +-- sql/sql.gyp | 2 + sql/sqlite_features_unittest.cc | 17 +- sql/statement_unittest.cc | 17 +- sql/test/sql_test_base.cc | 66 +++++++ sql/test/sql_test_base.h | 78 ++++++++ sql/test/test_helpers.cc | 27 +-- sql/test/test_helpers.h | 5 + sql/transaction_unittest.cc | 15 +- 21 files changed, 1379 insertions(+), 155 deletions(-) create mode 100644 sql/correct_sql_test_base.h create mode 100644 sql/mojo/BUILD.gn create mode 100644 sql/mojo/DEPS create mode 100644 sql/mojo/mojo_vfs.cc create mode 100644 sql/mojo/mojo_vfs.h create mode 100644 sql/mojo/sql_test_base.cc create mode 100644 sql/mojo/sql_test_base.h create mode 100644 sql/mojo/vfs_unittest.cc create mode 100644 sql/test/sql_test_base.cc create mode 100644 sql/test/sql_test_base.h (limited to 'sql') diff --git a/sql/BUILD.gn b/sql/BUILD.gn index f3c44be..fdf8c7b 100644 --- a/sql/BUILD.gn +++ b/sql/BUILD.gn @@ -54,6 +54,17 @@ source_set("test_support") { ] } +source_set("redirection_header") { + # This target exists because we need a way to switch between + # "test/sql_test_base.h" and "mojo/sql_test_base.h" at compile time, to allow + # us to switch out the gtest vs mojo:apptest frameworks. + check_includes = false + + sources = [ + "correct_sql_test_base.h", + ] +} + test("sql_unittests") { sources = [ "connection_unittest.cc", @@ -64,6 +75,8 @@ test("sql_unittests") { "test/paths.cc", "test/paths.h", "test/run_all_unittests.cc", + "test/sql_test_base.cc", + "test/sql_test_base.h", "test/sql_test_suite.cc", "test/sql_test_suite.h", "transaction_unittest.cc", @@ -74,6 +87,7 @@ test("sql_unittests") { deps = [ ":sql", + ":redirection_header", ":test_support", "//base/allocator", "//base/test:test_support", diff --git a/sql/connection.cc b/sql/connection.cc index c196073..b17457b 100644 --- a/sql/connection.cc +++ b/sql/connection.cc @@ -151,6 +151,14 @@ base::HistogramBase* GetMediumTimeHistogram(const std::string& name) { base::HistogramBase::kUmaTargetedHistogramFlag); } +std::string AsUTF8ForSQL(const base::FilePath& path) { +#if defined(OS_WIN) + return base::WideToUTF8(path.value()); +#elif defined(OS_POSIX) + return path.value(); +#endif +} + } // namespace namespace sql { @@ -312,11 +320,7 @@ bool Connection::Open(const base::FilePath& path) { } } -#if defined(OS_WIN) - return OpenInternal(base::WideToUTF8(path.value()), RETRY_ON_POISON); -#elif defined(OS_POSIX) - return OpenInternal(path.value(), RETRY_ON_POISON); -#endif + return OpenInternal(AsUTF8ForSQL(path), RETRY_ON_POISON); } bool Connection::OpenInMemory() { @@ -624,13 +628,38 @@ bool Connection::Delete(const base::FilePath& path) { base::FilePath journal_path(path.value() + FILE_PATH_LITERAL("-journal")); base::FilePath wal_path(path.value() + FILE_PATH_LITERAL("-wal")); - base::DeleteFile(journal_path, false); - base::DeleteFile(wal_path, false); - base::DeleteFile(path, false); + std::string journal_str = AsUTF8ForSQL(journal_path); + std::string wal_str = AsUTF8ForSQL(wal_path); + std::string path_str = AsUTF8ForSQL(path); + + sqlite3_vfs* vfs = sqlite3_vfs_find(NULL); + CHECK(vfs); + CHECK(vfs->xDelete); + CHECK(vfs->xAccess); + + // We only work with unix, win32 and mojo filesystems. If you're trying to + // use this code with any other VFS, you're not in a good place. + CHECK(strncmp(vfs->zName, "unix", 4) == 0 || + strncmp(vfs->zName, "win32", 5) == 0 || + strcmp(vfs->zName, "mojo") == 0); + + vfs->xDelete(vfs, journal_str.c_str(), 0); + vfs->xDelete(vfs, wal_str.c_str(), 0); + vfs->xDelete(vfs, path_str.c_str(), 0); + + int journal_exists = 0; + vfs->xAccess(vfs, journal_str.c_str(), SQLITE_ACCESS_EXISTS, + &journal_exists); + + int wal_exists = 0; + vfs->xAccess(vfs, wal_str.c_str(), SQLITE_ACCESS_EXISTS, + &wal_exists); + + int path_exists = 0; + vfs->xAccess(vfs, path_str.c_str(), SQLITE_ACCESS_EXISTS, + &path_exists); - return !base::PathExists(journal_path) && - !base::PathExists(wal_path) && - !base::PathExists(path); + return !journal_exists && !wal_exists && !path_exists; } bool Connection::BeginTransaction() { diff --git a/sql/connection_unittest.cc b/sql/connection_unittest.cc index a09721a..c7d9080 100644 --- a/sql/connection_unittest.cc +++ b/sql/connection_unittest.cc @@ -10,6 +10,7 @@ #include "base/metrics/statistics_recorder.h" #include "base/test/histogram_tester.h" #include "sql/connection.h" +#include "sql/correct_sql_test_base.h" #include "sql/meta_table.h" #include "sql/proxy.h" #include "sql/statement.h" @@ -207,33 +208,21 @@ class ScopedUmaskSetter { }; #endif -class SQLConnectionTest : public testing::Test { +class SQLConnectionTest : public sql::SQLTestBase { public: void SetUp() override { // Any macro histograms which fire before the recorder is initialized cannot // be tested. So this needs to be ahead of Open(). base::StatisticsRecorder::Initialize(); - ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); - db_path_ = temp_dir_.path().AppendASCII("SQLConnectionTest.db"); - ASSERT_TRUE(db_.Open(db_path_)); + SQLTestBase::SetUp(); } - void TearDown() override { db_.Close(); } - - sql::Connection& db() { return db_; } - const base::FilePath& db_path() { return db_path_; } - // Handle errors by blowing away the database. void RazeErrorCallback(int expected_error, int error, sql::Statement* stmt) { EXPECT_EQ(expected_error, error); - db_.RazeAndClose(); + db().RazeAndClose(); } - - private: - sql::Connection db_; - base::FilePath db_path_; - base::ScopedTempDir temp_dir_; }; TEST_F(SQLConnectionTest, Execute) { @@ -366,7 +355,7 @@ TEST_F(SQLConnectionTest, ScopedIgnoreUntracked) { db().Close(); // Corrupt the database so that nothing works, including PRAGMAs. - ASSERT_TRUE(sql::test::CorruptSizeInHeader(db_path())); + ASSERT_TRUE(CorruptSizeInHeaderOfDB()); { sql::ScopedErrorIgnorer ignore_errors; @@ -534,6 +523,8 @@ TEST_F(SQLConnectionTest, RazeMultiple) { ASSERT_EQ(0, SqliteMasterCount(&other_db)); } +// TODO(erg): Enable this in the next patch once I add locking. +#if !defined(MOJO_APPTEST_IMPL) TEST_F(SQLConnectionTest, RazeLocked) { const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)"; ASSERT_TRUE(db().Execute(kCreateSql)); @@ -568,6 +559,7 @@ TEST_F(SQLConnectionTest, RazeLocked) { ASSERT_FALSE(s.Step()); ASSERT_TRUE(db().Raze()); } +#endif // Verify that Raze() can handle an empty file. SQLite should treat // this as an empty database. @@ -576,12 +568,7 @@ TEST_F(SQLConnectionTest, RazeEmptyDB) { ASSERT_TRUE(db().Execute(kCreateSql)); db().Close(); - { - base::ScopedFILE file(base::OpenFile(db_path(), "rb+")); - ASSERT_TRUE(file.get() != NULL); - ASSERT_EQ(0, fseek(file.get(), 0, SEEK_SET)); - ASSERT_TRUE(base::TruncateFile(file.get())); - } + TruncateDatabase(); ASSERT_TRUE(db().Open(db_path())); ASSERT_TRUE(db().Raze()); @@ -592,16 +579,10 @@ TEST_F(SQLConnectionTest, RazeEmptyDB) { TEST_F(SQLConnectionTest, RazeNOTADB) { db().Close(); sql::Connection::Delete(db_path()); - ASSERT_FALSE(base::PathExists(db_path())); + ASSERT_FALSE(GetPathExists(db_path())); - { - base::ScopedFILE file(base::OpenFile(db_path(), "wb")); - ASSERT_TRUE(file.get() != NULL); - - const char* kJunk = "This is the hour of our discontent."; - fputs(kJunk, file.get()); - } - ASSERT_TRUE(base::PathExists(db_path())); + WriteJunkToDatabase(SQLTestBase::TYPE_OVERWRITE_AND_TRUNCATE); + ASSERT_TRUE(GetPathExists(db_path())); // SQLite will successfully open the handle, but fail when running PRAGMA // statements that access the database. @@ -635,14 +616,7 @@ TEST_F(SQLConnectionTest, RazeNOTADB2) { ASSERT_EQ(1, SqliteMasterCount(&db())); db().Close(); - { - base::ScopedFILE file(base::OpenFile(db_path(), "rb+")); - ASSERT_TRUE(file.get() != NULL); - ASSERT_EQ(0, fseek(file.get(), 0, SEEK_SET)); - - const char* kJunk = "This is the hour of our discontent."; - fputs(kJunk, file.get()); - } + WriteJunkToDatabase(SQLTestBase::TYPE_OVERWRITE); // SQLite will successfully open the handle, but will fail with // SQLITE_NOTADB on pragma statemenets which attempt to read the @@ -672,7 +646,7 @@ TEST_F(SQLConnectionTest, RazeCallbackReopen) { db().Close(); // Corrupt the database so that nothing works, including PRAGMAs. - ASSERT_TRUE(sql::test::CorruptSizeInHeader(db_path())); + ASSERT_TRUE(CorruptSizeInHeaderOfDB()); // Open() will succeed, even though the PRAGMA calls within will // fail with SQLITE_CORRUPT, as will this PRAGMA. @@ -817,15 +791,17 @@ TEST_F(SQLConnectionTest, Delete) { // Should have both a main database file and a journal file because // of journal_mode TRUNCATE. base::FilePath journal(db_path().value() + FILE_PATH_LITERAL("-journal")); - ASSERT_TRUE(base::PathExists(db_path())); - ASSERT_TRUE(base::PathExists(journal)); + ASSERT_TRUE(GetPathExists(db_path())); + ASSERT_TRUE(GetPathExists(journal)); sql::Connection::Delete(db_path()); - EXPECT_FALSE(base::PathExists(db_path())); - EXPECT_FALSE(base::PathExists(journal)); + EXPECT_FALSE(GetPathExists(db_path())); + EXPECT_FALSE(GetPathExists(journal)); } -#if defined(OS_POSIX) +// This test manually sets on disk permissions; this doesn't apply to the mojo +// fork. +#if defined(OS_POSIX) && !defined(MOJO_APPTEST_IMPL) // Test that set_restrict_to_user() trims database permissions so that // only the owner (and root) can read. TEST_F(SQLConnectionTest, UserPermission) { @@ -835,7 +811,7 @@ TEST_F(SQLConnectionTest, UserPermission) { // Temporarily provide a more permissive umask. db().Close(); sql::Connection::Delete(db_path()); - ASSERT_FALSE(base::PathExists(db_path())); + ASSERT_FALSE(GetPathExists(db_path())); ScopedUmaskSetter permissive_umask(S_IWGRP | S_IWOTH); ASSERT_TRUE(db().Open(db_path())); @@ -849,8 +825,8 @@ TEST_F(SQLConnectionTest, UserPermission) { // Given a permissive umask, the database is created with permissive // read access for the database and journal. - ASSERT_TRUE(base::PathExists(db_path())); - ASSERT_TRUE(base::PathExists(journal)); + ASSERT_TRUE(GetPathExists(db_path())); + ASSERT_TRUE(GetPathExists(journal)); mode = base::FILE_PERMISSION_MASK; EXPECT_TRUE(base::GetPosixFilePermissions(db_path(), &mode)); ASSERT_NE((mode & base::FILE_PERMISSION_USER_MASK), mode); @@ -863,8 +839,8 @@ TEST_F(SQLConnectionTest, UserPermission) { db().Close(); db().set_restrict_to_user(); ASSERT_TRUE(db().Open(db_path())); - ASSERT_TRUE(base::PathExists(db_path())); - ASSERT_TRUE(base::PathExists(journal)); + ASSERT_TRUE(GetPathExists(db_path())); + ASSERT_TRUE(GetPathExists(journal)); mode = base::FILE_PERMISSION_MASK; EXPECT_TRUE(base::GetPosixFilePermissions(db_path(), &mode)); ASSERT_EQ((mode & base::FILE_PERMISSION_USER_MASK), mode); @@ -876,15 +852,15 @@ TEST_F(SQLConnectionTest, UserPermission) { db().Close(); sql::Connection::Delete(db_path()); ASSERT_TRUE(db().Open(db_path())); - ASSERT_TRUE(base::PathExists(db_path())); - ASSERT_FALSE(base::PathExists(journal)); + ASSERT_TRUE(GetPathExists(db_path())); + ASSERT_FALSE(GetPathExists(journal)); mode = base::FILE_PERMISSION_MASK; EXPECT_TRUE(base::GetPosixFilePermissions(db_path(), &mode)); ASSERT_EQ((mode & base::FILE_PERMISSION_USER_MASK), mode); // Verify that journal creation inherits the restriction. EXPECT_TRUE(db().Execute("CREATE TABLE x (x)")); - ASSERT_TRUE(base::PathExists(journal)); + ASSERT_TRUE(GetPathExists(journal)); mode = base::FILE_PERMISSION_MASK; EXPECT_TRUE(base::GetPosixFilePermissions(journal, &mode)); ASSERT_EQ((mode & base::FILE_PERMISSION_USER_MASK), mode); @@ -990,7 +966,7 @@ TEST_F(SQLConnectionTest, Basic_QuickIntegrityCheck) { EXPECT_TRUE(db().QuickIntegrityCheck()); db().Close(); - ASSERT_TRUE(sql::test::CorruptSizeInHeader(db_path())); + ASSERT_TRUE(CorruptSizeInHeaderOfDB()); { sql::ScopedErrorIgnorer ignore_errors; @@ -1012,7 +988,7 @@ TEST_F(SQLConnectionTest, Basic_FullIntegrityCheck) { EXPECT_EQ(kOk, messages[0]); db().Close(); - ASSERT_TRUE(sql::test::CorruptSizeInHeader(db_path())); + ASSERT_TRUE(CorruptSizeInHeaderOfDB()); { sql::ScopedErrorIgnorer ignore_errors; diff --git a/sql/correct_sql_test_base.h b/sql/correct_sql_test_base.h new file mode 100644 index 0000000..7056dea --- /dev/null +++ b/sql/correct_sql_test_base.h @@ -0,0 +1,27 @@ +// Copyright 2015 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. + +#ifndef SQL_CORRECT_SQL_TEST_BASE_H_ +#define SQL_CORRECT_SQL_TEST_BASE_H_ + +// This header exists to get around gn check. We want to use the same testing +// code in both the sql_unittests target (which uses gtest and targets the +// filesystem directly) and sql_apptests.mojo (which uses mojo:apptest and +// proxies the additional filesystem access to mojo:filesystem). Both of these +// files define a class named sql::SQLTestBase and have the same interface. +// +// Unfortunately, gn check does not understand preprocessor directives. If it +// did, the following code would be gn check clean, but since it isn't, we +// stuff this redirection header in its own file, give it its own source_set +// target, and then set check_includes to false. +// +// This work around was suggested by brettw@. +#if defined(MOJO_APPTEST_IMPL) +#include "sql/mojo/sql_test_base.h" +#else +#include "sql/test/sql_test_base.h" +#endif + +#endif // SQL_CORRECT_SQL_TEST_BASE_H_ + diff --git a/sql/meta_table_unittest.cc b/sql/meta_table_unittest.cc index 1412392..13d0b5d 100644 --- a/sql/meta_table_unittest.cc +++ b/sql/meta_table_unittest.cc @@ -8,25 +8,12 @@ #include "base/files/scoped_temp_dir.h" #include "sql/connection.h" #include "sql/statement.h" +#include "sql/test/sql_test_base.h" #include "testing/gtest/include/gtest/gtest.h" namespace { -class SQLMetaTableTest : public testing::Test { - public: - void SetUp() override { - ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); - ASSERT_TRUE(db_.Open(temp_dir_.path().AppendASCII("SQLMetaTableTest.db"))); - } - - void TearDown() override { db_.Close(); } - - sql::Connection& db() { return db_; } - - private: - base::ScopedTempDir temp_dir_; - sql::Connection db_; -}; +using SQLMetaTableTest = sql::SQLTestBase; TEST_F(SQLMetaTableTest, DoesTableExist) { EXPECT_FALSE(sql::MetaTable::DoesTableExist(&db())); diff --git a/sql/mojo/BUILD.gn b/sql/mojo/BUILD.gn new file mode 100644 index 0000000..18feaec --- /dev/null +++ b/sql/mojo/BUILD.gn @@ -0,0 +1,62 @@ +# Copyright 2015 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. + +import("//mojo/public/mojo_application.gni") + +source_set("mojo") { + sources = [ + "mojo_vfs.cc", + "mojo_vfs.h", + ] + + # TODO(jschuh): crbug.com/167187 fix size_t to int truncations. + configs += [ "//build/config/compiler:no_size_t_to_int_warning" ] + + defines = [ "SQL_IMPLEMENTATION" ] + + deps = [ + "//base", + "//base/third_party/dynamic_annotations", + "//components/filesystem/public/interfaces", + "//mojo/application/public/cpp", + "//mojo/common", + "//mojo/platform_handle", + "//third_party/sqlite", + ] +} + +mojo_native_application("apptests") { + output_name = "sql_apptests" + + testonly = true + + # Instead of using the code in //sql/test/sql_test_base.h, we should use the + # mojo test base class. + defines = [ "MOJO_APPTEST_IMPL" ] + + sources = [ + "../connection_unittest.cc", + "../statement_unittest.cc", + "../test/paths.cc", + "../test/paths.h", + "../transaction_unittest.cc", + "sql_test_base.cc", + "sql_test_base.h", + "vfs_unittest.cc", + ] + + deps = [ + ":mojo", + "//base", + "//base/test:test_support", + "//components/filesystem/public/interfaces", + "//mojo/application/public/cpp:sources", + "//mojo/application/public/cpp:test_support", + "//sql", + "//sql:redirection_header", + "//sql:test_support", + "//testing/gtest:gtest", + "//third_party/mojo/src/mojo/public/cpp/bindings", + ] +} diff --git a/sql/mojo/DEPS b/sql/mojo/DEPS new file mode 100644 index 0000000..be63411 --- /dev/null +++ b/sql/mojo/DEPS @@ -0,0 +1,6 @@ +include_rules = [ + "+components/filesystem", + "+mojo/application", + "+mojo/public", + "+mojo/util", +] diff --git a/sql/mojo/mojo_vfs.cc b/sql/mojo/mojo_vfs.cc new file mode 100644 index 0000000..6e38af9 --- /dev/null +++ b/sql/mojo/mojo_vfs.cc @@ -0,0 +1,413 @@ +// Copyright 2015 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 "sql/mojo/mojo_vfs.h" + +#include "base/logging.h" +#include "base/rand_util.h" +#include "components/filesystem/public/interfaces/file.mojom.h" +#include "components/filesystem/public/interfaces/file_system.mojom.h" +#include "components/filesystem/public/interfaces/types.mojom.h" +#include "mojo/public/cpp/bindings/lib/template_util.h" +#include "mojo/util/capture_util.h" +#include "third_party/sqlite/sqlite3.h" + +using mojo::Capture; + +namespace sql { + +sqlite3_vfs* GetParentVFS(sqlite3_vfs* mojo_vfs) { + return static_cast(mojo_vfs->pAppData)->parent_; +} + +filesystem::DirectoryPtr& GetRootDirectory(sqlite3_vfs* mojo_vfs) { + return static_cast(mojo_vfs->pAppData)-> + root_directory_; +} + +namespace { + +// Implementation of the sqlite3 Mojo proxying vfs. +// +// This is a bunch of C callback objects which transparently proxy sqlite3's +// filesystem reads/writes over the mojo:filesystem service. The main +// entrypoint is sqlite3_mojovfs(), which proxies all the file open/delete/etc +// operations. mojo:filesystem has support for passing a raw file descriptor +// over the IPC barrier, and most of the implementation of sqlite3_io_methods +// is derived from the default sqlite3 unix VFS and operates on the raw file +// descriptors. + +const int kMaxPathName = 512; + +// A struct which extends the base sqlite3_file to also hold on to a file +// pipe. We reinterpret_cast our sqlite3_file structs to this struct +// instead. This is "safe" because this struct is really just a slab of +// malloced memory, of which we tell sqlite how large we want with szOsFile. +struct MojoVFSFile { + // The "vtable" of our sqlite3_file "subclass". + sqlite3_file base; + + // We keep an open pipe to the File object to keep it from cleaning itself + // up. + filesystem::FilePtr file_ptr; +}; + +filesystem::FilePtr& GetFSFile(sqlite3_file* vfs_file) { + return reinterpret_cast(vfs_file)->file_ptr; +} + +int MojoVFSClose(sqlite3_file* file) { + DVLOG(1) << "MojoVFSClose(*)"; + using filesystem::FilePtr; + GetFSFile(file).~FilePtr(); + return SQLITE_OK; +} + +int MojoVFSRead(sqlite3_file* sql_file, + void* buffer, + int size, + sqlite3_int64 offset) { + DVLOG(1) << "MojoVFSRead (" << size << " @ " << offset << ")"; + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + mojo::Array mojo_data; + GetFSFile(sql_file)->Read(size, offset, filesystem::WHENCE_FROM_BEGIN, + Capture(&error, &mojo_data)); + GetFSFile(sql_file).WaitForIncomingResponse(); + + if (error != filesystem::FILE_ERROR_OK) { + // TODO(erg): Better implementation here. + NOTIMPLEMENTED(); + return SQLITE_IOERR_READ; + } + + if (mojo_data.size()) + memcpy(buffer, &mojo_data.front(), mojo_data.size()); + if (mojo_data.size() == static_cast(size)) + return SQLITE_OK; + + // We didn't read the entire buffer. Fill the rest of the buffer with zeros. + memset(reinterpret_cast(buffer) + mojo_data.size(), 0, + size - mojo_data.size()); + + return SQLITE_IOERR_SHORT_READ; +} + +int MojoVFSWrite(sqlite3_file* sql_file, + const void* buffer, + int size, + sqlite_int64 offset) { + DVLOG(1) << "MojoVFSWrite(*, " << size << ", " << offset << ")"; + mojo::Array mojo_data(size); + memcpy(&mojo_data.front(), buffer, size); + + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + uint32_t num_bytes_written = 0; + GetFSFile(sql_file)->Write(mojo_data.Pass(), offset, + filesystem::WHENCE_FROM_BEGIN, + Capture(&error, &num_bytes_written)); + GetFSFile(sql_file).WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) { + // TODO(erg): Better implementation here. + NOTIMPLEMENTED(); + return SQLITE_IOERR_WRITE; + } + if (num_bytes_written != static_cast(size)) { + NOTIMPLEMENTED(); + return SQLITE_IOERR_WRITE; + } + + return SQLITE_OK; +} + +int MojoVFSTruncate(sqlite3_file* sql_file, sqlite_int64 size) { + DVLOG(1) << "MojoVFSTruncate(*, " << size << ")"; + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + GetFSFile(sql_file)->Truncate(size, Capture(&error)); + GetFSFile(sql_file).WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) { + // TODO(erg): Better implementation here. + NOTIMPLEMENTED(); + return SQLITE_IOERR_TRUNCATE; + } + + return SQLITE_OK; +} + +int MojoVFSSync(sqlite3_file* sql_file, int flags) { + DVLOG(1) << "MojoVFSSync(*, " << flags << ")"; + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + GetFSFile(sql_file)->Flush(Capture(&error)); + GetFSFile(sql_file).WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) { + // TODO(erg): Better implementation here. + NOTIMPLEMENTED(); + return SQLITE_IOERR_FSYNC; + } + + return SQLITE_OK; +} + +int MojoVFSFileSize(sqlite3_file* sql_file, sqlite_int64* size) { + DVLOG(1) << "MojoVFSFileSize(*)"; + + filesystem::FileError err = filesystem::FILE_ERROR_FAILED; + filesystem::FileInformationPtr file_info; + GetFSFile(sql_file)->Stat(Capture(&err, &file_info)); + GetFSFile(sql_file).WaitForIncomingResponse(); + + if (err != filesystem::FILE_ERROR_OK) { + // TODO(erg): Better implementation here. + NOTIMPLEMENTED(); + return SQLITE_IOERR_FSTAT; + } + + *size = file_info->size; + return SQLITE_OK; +} + +// TODO(erg): The current base::File interface isn't sufficient to handle +// sqlite's locking primitives, which are done on byte ranges in the file. (See +// "File Locking Notes" in sqlite3.c.) +int MojoVFSLock(sqlite3_file* pFile, int eLock) { + DVLOG(1) << "MojoVFSLock(*, " << eLock << ")"; + return SQLITE_OK; +} +int MojoVFSUnlock(sqlite3_file* pFile, int eLock) { + DVLOG(1) << "MojoVFSUnlock(*, " << eLock << ")"; + return SQLITE_OK; +} +int MojoVFSCheckReservedLock(sqlite3_file* pFile, int* pResOut) { + DVLOG(1) << "MojoVFSCheckReservedLock(*)"; + *pResOut = 0; + return SQLITE_OK; +} + +// TODO(erg): This is the minimal implementation to get a few tests passing; +// lots more needs to be done here. +int MojoVFSFileControl(sqlite3_file* pFile, int op, void* pArg) { + DVLOG(1) << "MojoVFSFileControl(*, " << op << ", *)"; + if (op == SQLITE_FCNTL_PRAGMA) { + // Returning NOTFOUND tells sqlite that we aren't doing any processing. + return SQLITE_NOTFOUND; + } + + return SQLITE_OK; +} + +int MojoVFSSectorSize(sqlite3_file* pFile) { + DVLOG(1) << "MojoVFSSectorSize(*)"; + // Use the default sector size. + return 0; +} + +int MojoVFSDeviceCharacteristics(sqlite3_file* pFile) { + DVLOG(1) << "MojoVFSDeviceCharacteristics(*)"; + NOTIMPLEMENTED(); + return 0; +} + +static sqlite3_io_methods mojo_vfs_io_methods = { + 1, /* iVersion */ + MojoVFSClose, /* xClose */ + MojoVFSRead, /* xRead */ + MojoVFSWrite, /* xWrite */ + MojoVFSTruncate, /* xTruncate */ + MojoVFSSync, /* xSync */ + MojoVFSFileSize, /* xFileSize */ + MojoVFSLock, /* xLock */ + MojoVFSUnlock, /* xUnlock */ + MojoVFSCheckReservedLock, /* xCheckReservedLock */ + MojoVFSFileControl, /* xFileControl */ + MojoVFSSectorSize, /* xSectorSize */ + MojoVFSDeviceCharacteristics, /* xDeviceCharacteristics */ +}; + +int MojoVFSOpen(sqlite3_vfs* mojo_vfs, + const char* name, + sqlite3_file* file, + int flags, + int* pOutFlags) { + DVLOG(1) << "MojoVFSOpen(*, " << name << ", *, " << flags << ")"; + int open_flags = 0; + if (flags & SQLITE_OPEN_EXCLUSIVE) { + DCHECK(flags & SQLITE_OPEN_CREATE); + open_flags = filesystem::kFlagCreate; + } else if (flags & SQLITE_OPEN_CREATE) { + DCHECK(flags & SQLITE_OPEN_READWRITE); + open_flags = filesystem::kFlagOpenAlways; + } else { + open_flags = filesystem::kFlagOpen; + } + open_flags |= filesystem::kFlagRead; + if (flags & SQLITE_OPEN_READWRITE) + open_flags |= filesystem::kFlagWrite; + if (flags & SQLITE_OPEN_DELETEONCLOSE) + open_flags |= filesystem::kDeleteOnClose; + + // Grab the incoming file + filesystem::FilePtr file_ptr; + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + GetRootDirectory(mojo_vfs)->OpenFile(mojo::String(name), GetProxy(&file_ptr), + open_flags, Capture(&error)); + GetRootDirectory(mojo_vfs).WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) { + // TODO(erg): Translate more of the mojo error codes into sqlite error + // codes. + return SQLITE_CANTOPEN; + } + + // Set the method table so we can be closed (and run the manual dtor call to + // match the following placement news). + file->pMethods = &mojo_vfs_io_methods; + + // |file| is actually a malloced buffer of size szOsFile. This means that we + // need to manually use placement new to construct the C++ object which owns + // the pipe to our file. + new (&GetFSFile(file)) filesystem::FilePtr(file_ptr.Pass()); + + return SQLITE_OK; +} + +int MojoVFSDelete(sqlite3_vfs* mojo_vfs, const char* filename, int sync_dir) { + DVLOG(1) << "MojoVFSDelete(*, " << filename << ", " << sync_dir << ")"; + // TODO(erg): The default windows sqlite VFS has retry code to work around + // antivirus software keeping files open. We'll probably have to do something + // like that in the far future if we ever support Windows. + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + GetRootDirectory(mojo_vfs)->Delete(filename, 0, Capture(&error)); + GetRootDirectory(mojo_vfs).WaitForIncomingResponse(); + + if (error == filesystem::FILE_ERROR_OK && sync_dir) { + GetRootDirectory(mojo_vfs)->Flush(Capture(&error)); + GetRootDirectory(mojo_vfs).WaitForIncomingResponse(); + } + + return error == filesystem::FILE_ERROR_OK ? SQLITE_OK : SQLITE_IOERR_DELETE; +} + +int MojoVFSAccess(sqlite3_vfs* mojo_vfs, + const char* filename, + int flags, + int* result) { + DVLOG(1) << "MojoVFSAccess(*, " << filename << ", " << flags << ", *)"; + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + + if (flags == SQLITE_ACCESS_READWRITE || flags == SQLITE_ACCESS_READ) { + bool is_writable = false; + GetRootDirectory(mojo_vfs) + ->IsWritable(filename, Capture(&error, &is_writable)); + GetRootDirectory(mojo_vfs).WaitForIncomingResponse(); + *result = is_writable; + return SQLITE_OK; + } + + if (flags == SQLITE_ACCESS_EXISTS) { + bool exists = false; + GetRootDirectory(mojo_vfs)->Exists(filename, Capture(&error, &exists)); + GetRootDirectory(mojo_vfs).WaitForIncomingResponse(); + *result = exists; + return SQLITE_OK; + } + + return SQLITE_IOERR; +} + +int MojoVFSFullPathname(sqlite3_vfs* mojo_vfs, + const char* relative_path, + int absolute_path_size, + char* absolute_path) { + // The sandboxed process doesn't need to know the absolute path of the file. + sqlite3_snprintf(absolute_path_size, absolute_path, "%s", relative_path); + return SQLITE_OK; +} + +// Don't let SQLite dynamically load things. (If we are using the +// mojo:filesystem proxying VFS, then it's highly likely that we are sandboxed +// and that any attempt to dlopen() a shared object is folly.) +void* MojoVFSDlOpen(sqlite3_vfs*, const char*) { + return 0; +} + +void MojoVFSDlError(sqlite3_vfs*, int buf_size, char* error_msg) { + sqlite3_snprintf(buf_size, error_msg, "Dynamic loading not supported"); +} + +void (*MojoVFSDlSym(sqlite3_vfs*, void*, const char*))(void) { + return 0; +} + +void MojoVFSDlClose(sqlite3_vfs*, void*) { + return; +} + +int MojoVFSRandomness(sqlite3_vfs* mojo_vfs, int size, char* out) { + base::RandBytes(out, size); + return size; +} + +// Proxy the rest of the calls down to the OS specific handler. +int MojoVFSSleep(sqlite3_vfs* mojo_vfs, int micro) { + return GetParentVFS(mojo_vfs)->xSleep(GetParentVFS(mojo_vfs), micro); +} + +int MojoVFSCurrentTime(sqlite3_vfs* mojo_vfs, double* time) { + return GetParentVFS(mojo_vfs)->xCurrentTime(GetParentVFS(mojo_vfs), time); +} + +int MojoVFSGetLastError(sqlite3_vfs* mojo_vfs, int a, char* b) { + return 0; +} + +int MojoVFSCurrentTimeInt64(sqlite3_vfs* mojo_vfs, sqlite3_int64* out) { + return GetParentVFS(mojo_vfs)->xCurrentTimeInt64(GetParentVFS(mojo_vfs), out); +} + +static sqlite3_vfs mojo_vfs = { + 1, /* iVersion */ + sizeof(MojoVFSFile), /* szOsFile */ + kMaxPathName, /* mxPathname */ + 0, /* pNext */ + "mojo", /* zName */ + 0, /* pAppData */ + MojoVFSOpen, /* xOpen */ + MojoVFSDelete, /* xDelete */ + MojoVFSAccess, /* xAccess */ + MojoVFSFullPathname, /* xFullPathname */ + MojoVFSDlOpen, /* xDlOpen */ + MojoVFSDlError, /* xDlError */ + MojoVFSDlSym, /* xDlSym */ + MojoVFSDlClose, /* xDlClose */ + MojoVFSRandomness, /* xRandomness */ + MojoVFSSleep, /* xSleep */ + MojoVFSCurrentTime, /* xCurrentTime */ + MojoVFSGetLastError, /* xGetLastError */ + MojoVFSCurrentTimeInt64 /* xCurrentTimeInt64 */ +}; + +} // namespace + +ScopedMojoFilesystemVFS::ScopedMojoFilesystemVFS( + filesystem::DirectoryPtr root_directory) + : parent_(sqlite3_vfs_find(NULL)), + root_directory_(root_directory.Pass()) { + CHECK(!mojo_vfs.pAppData); + mojo_vfs.pAppData = this; + mojo_vfs.mxPathname = parent_->mxPathname; + + CHECK(sqlite3_vfs_register(&mojo_vfs, 1) == SQLITE_OK); +} + +ScopedMojoFilesystemVFS::~ScopedMojoFilesystemVFS() { + CHECK(mojo_vfs.pAppData); + mojo_vfs.pAppData = nullptr; + + CHECK(sqlite3_vfs_register(parent_, 1) == SQLITE_OK); + CHECK(sqlite3_vfs_unregister(&mojo_vfs) == SQLITE_OK); +} + +filesystem::DirectoryPtr& ScopedMojoFilesystemVFS::GetDirectory() { + return root_directory_; +} + +} // namespace sql diff --git a/sql/mojo/mojo_vfs.h b/sql/mojo/mojo_vfs.h new file mode 100644 index 0000000..dc83593 --- /dev/null +++ b/sql/mojo/mojo_vfs.h @@ -0,0 +1,45 @@ +// Copyright 2015 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. + +#ifndef SQL_MOJO_MOJO_VFS_H_ +#define SQL_MOJO_MOJO_VFS_H_ + +#include "base/macros.h" +#include "components/filesystem/public/interfaces/directory.mojom.h" + +typedef struct sqlite3_vfs sqlite3_vfs; + +namespace sql { + +// Changes the default sqlite3 vfs to a vfs that uses proxies calls to the +// mojo:filesystem service. Instantiating this object transparently changes how +// the entire //sql/ subsystem works in the process of the caller; all paths +// are treated as relative to |directory|. +class ScopedMojoFilesystemVFS { + public: + explicit ScopedMojoFilesystemVFS(filesystem::DirectoryPtr directory); + ~ScopedMojoFilesystemVFS(); + + // Returns the directory of the current VFS. + filesystem::DirectoryPtr& GetDirectory(); + + private: + friend sqlite3_vfs* GetParentVFS(sqlite3_vfs* mojo_vfs); + friend filesystem::DirectoryPtr& GetRootDirectory(sqlite3_vfs* mojo_vfs); + + // The default vfs at the time MojoVFS was installed. We use the to pass + // through things like randomness requests and per-platform sleep calls. + sqlite3_vfs* parent_; + + // When we initialize the subsystem, we are given a filesystem::Directory + // object, which is the root directory of a mojo:filesystem. All access to + // various files are specified from this root directory. + filesystem::DirectoryPtr root_directory_; + + DISALLOW_COPY_AND_ASSIGN(ScopedMojoFilesystemVFS); +}; + +} // namespace sql + +#endif // SQL_MOJO_MOJO_VFS_H_ diff --git a/sql/mojo/sql_test_base.cc b/sql/mojo/sql_test_base.cc new file mode 100644 index 0000000..57645dd --- /dev/null +++ b/sql/mojo/sql_test_base.cc @@ -0,0 +1,156 @@ +// Copyright 2015 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 "sql/mojo/sql_test_base.h" + +#include "mojo/application/public/cpp/application_impl.h" +#include "mojo/util/capture_util.h" +#include "sql/mojo/mojo_vfs.h" +#include "sql/test/test_helpers.h" + +using mojo::Capture; + +namespace sql { + +SQLTestBase::SQLTestBase() { +} + +SQLTestBase::~SQLTestBase() { +} + +base::FilePath SQLTestBase::db_path() { + return base::FilePath(FILE_PATH_LITERAL("SQLTest.db")); +} + +sql::Connection& SQLTestBase::db() { + return db_; +} + +bool SQLTestBase::Reopen() { + db_.Close(); + return db_.Open(db_path()); +} + +bool SQLTestBase::GetPathExists(const base::FilePath& path) { + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + bool exists = false; + vfs_->GetDirectory()->Exists(path.AsUTF8Unsafe(), Capture(&error, &exists)); + vfs_->GetDirectory().WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) + return false; + return exists; +} + +bool SQLTestBase::CorruptSizeInHeaderOfDB() { + // See http://www.sqlite.org/fileformat.html#database_header + const size_t kHeaderSize = 100; + + mojo::Array header; + + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + filesystem::FilePtr file_ptr; + vfs_->GetDirectory()->OpenFile( + mojo::String(db_path().AsUTF8Unsafe()), GetProxy(&file_ptr), + filesystem::kFlagRead | filesystem::kFlagWrite | + filesystem::kFlagOpenAlways, + Capture(&error)); + vfs_->GetDirectory().WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) + return false; + + file_ptr->Read(kHeaderSize, 0, filesystem::WHENCE_FROM_BEGIN, + Capture(&error, &header)); + file_ptr.WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) + return false; + + filesystem::FileInformationPtr info; + file_ptr->Stat(Capture(&error, &info)); + file_ptr.WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) + return false; + int64_t db_size = info->size; + + test::CorruptSizeInHeaderMemory(&header.front(), db_size); + + uint32_t num_bytes_written = 0; + file_ptr->Write(header.Pass(), 0, filesystem::WHENCE_FROM_BEGIN, + Capture(&error, &num_bytes_written)); + file_ptr.WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) + return false; + if (num_bytes_written != kHeaderSize) + return false; + + return true; +} + +void SQLTestBase::WriteJunkToDatabase(WriteJunkType type) { + uint32_t flags = 0; + if (type == TYPE_OVERWRITE_AND_TRUNCATE) + flags = filesystem::kFlagWrite | filesystem::kFlagCreate; + else + flags = filesystem::kFlagWrite | filesystem::kFlagOpen; + + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + filesystem::FilePtr file_ptr; + vfs_->GetDirectory()->OpenFile( + mojo::String(db_path().AsUTF8Unsafe()), GetProxy(&file_ptr), + flags, + Capture(&error)); + vfs_->GetDirectory().WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) + return; + + const char* kJunk = "Now is the winter of our discontent."; + mojo::Array data(strlen(kJunk)); + memcpy(&data.front(), kJunk, strlen(kJunk)); + + uint32_t num_bytes_written = 0; + file_ptr->Write(data.Pass(), 0, filesystem::WHENCE_FROM_BEGIN, + Capture(&error, &num_bytes_written)); + file_ptr.WaitForIncomingResponse(); +} + +void SQLTestBase::TruncateDatabase() { + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + filesystem::FilePtr file_ptr; + vfs_->GetDirectory()->OpenFile( + mojo::String(db_path().AsUTF8Unsafe()), GetProxy(&file_ptr), + filesystem::kFlagWrite | filesystem::kFlagOpen, + Capture(&error)); + vfs_->GetDirectory().WaitForIncomingResponse(); + if (error != filesystem::FILE_ERROR_OK) + return; + + file_ptr->Truncate(0, Capture(&error)); + file_ptr.WaitForIncomingResponse(); + ASSERT_EQ(filesystem::FILE_ERROR_OK, error); +} + +void SQLTestBase::SetUp() { + ApplicationTestBase::SetUp(); + + mojo::URLRequestPtr request(mojo::URLRequest::New()); + request->url = mojo::String::From("mojo:filesystem"); + application_impl()->ConnectToService(request.Pass(), &files_); + + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + filesystem::DirectoryPtr directory; + files()->OpenFileSystem("temp", GetProxy(&directory), Capture(&error)); + ASSERT_TRUE(files().WaitForIncomingResponse()); + ASSERT_EQ(filesystem::FILE_ERROR_OK, error); + + vfs_.reset(new ScopedMojoFilesystemVFS(directory.Pass())); + ASSERT_TRUE(db_.Open(db_path())); +} + +void SQLTestBase::TearDown() { + db_.Close(); + vfs_.reset(); + + ApplicationTestBase::TearDown(); +} + +} // namespace sql diff --git a/sql/mojo/sql_test_base.h b/sql/mojo/sql_test_base.h new file mode 100644 index 0000000..f2bfb5d4 --- /dev/null +++ b/sql/mojo/sql_test_base.h @@ -0,0 +1,85 @@ +// Copyright 2015 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. + +#ifndef SQL_MOJO_SQL_TEST_BASE_H_ +#define SQL_MOJO_SQL_TEST_BASE_H_ + +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.h" +#include "components/filesystem/public/interfaces/file_system.mojom.h" +#include "mojo/application/public/cpp/application_test_base.h" +#include "sql/connection.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace sql { + +class Connection; +class ScopedMojoFilesystemVFS; + +// Base class for SQL tests. +// +// WARNING: We want to run the same gtest based unit test code both against +// chromium (which uses this implementation here), and the mojo code (which +// uses a different class named SQLTestBase). These two classes need to have +// the same interface because we compile time switch them based on a +// #define. We need to have two different implementations because the mojo +// version derives from mojo::test::ApplicationTestBase instead of +// testing::Test. +class SQLTestBase : public mojo::test::ApplicationTestBase { + public: + SQLTestBase(); + ~SQLTestBase() override; + + enum WriteJunkType { + TYPE_OVERWRITE_AND_TRUNCATE, + TYPE_OVERWRITE + }; + + // Returns the path to the database. + base::FilePath db_path(); + + // Returns a connection to the database at db_path(). + sql::Connection& db(); + + // Closes the current connection to the database and reopens it. + bool Reopen(); + + // Proxying method around base::PathExists. + bool GetPathExists(const base::FilePath& path); + + // SQLite stores the database size in the header, and if the actual + // OS-derived size is smaller, the database is considered corrupt. + // [This case is actually a common form of corruption in the wild.] + // This helper sets the in-header size to one page larger than the + // actual file size. The resulting file will return SQLITE_CORRUPT + // for most operations unless PRAGMA writable_schema is turned ON. + // + // Returns false if any error occurs accessing the file. + bool CorruptSizeInHeaderOfDB(); + + // Writes junk to the start of the file. + void WriteJunkToDatabase(WriteJunkType type); + + // Sets the database file size to 0. + void TruncateDatabase(); + + // Overridden from testing::Test: + void SetUp() override; + void TearDown() override; + + protected: + filesystem::FileSystemPtr& files() { return files_; } + + private: + filesystem::FileSystemPtr files_; + + scoped_ptr vfs_; + sql::Connection db_; + + DISALLOW_COPY_AND_ASSIGN(SQLTestBase); +}; + +} // namespace sql + +#endif // SQL_MOJO_SQL_TEST_BASE_H_ diff --git a/sql/mojo/vfs_unittest.cc b/sql/mojo/vfs_unittest.cc new file mode 100644 index 0000000..8ca7c5c --- /dev/null +++ b/sql/mojo/vfs_unittest.cc @@ -0,0 +1,317 @@ +// Copyright 2015 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 "components/filesystem/public/interfaces/file_system.mojom.h" +#include "mojo/application/public/cpp/application_impl.h" +#include "mojo/application/public/cpp/application_test_base.h" +#include "mojo/util/capture_util.h" +#include "sql/mojo/mojo_vfs.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/sqlite/sqlite3.h" + +namespace base { + +// This deleter lets us be safe with sqlite3 objects, which aren't really the +// structs, but slabs of new uint8_t[size]. +template <> +struct DefaultDeleter { + inline void operator()(sqlite3_file* ptr) const { + // Why don't we call file->pMethods->xClose() here? Because it's not + // guaranteed to be valid. sqlite3_file "objects" can be in partially + // initialized states. + delete [] reinterpret_cast(ptr); + } +}; + +} // namespace base + +namespace sql { + +const char kFileName[] = "TestingDatabase.db"; + +class VFSTest : public mojo::test::ApplicationTestBase { + public: + VFSTest() {} + ~VFSTest() override {} + + sqlite3_vfs* vfs() { + return sqlite3_vfs_find(NULL); + } + + scoped_ptr MakeFile() { + return scoped_ptr(reinterpret_cast( + new uint8_t[vfs()->szOsFile])); + } + + void SetUp() override { + mojo::test::ApplicationTestBase::SetUp(); + + mojo::URLRequestPtr request(mojo::URLRequest::New()); + request->url = mojo::String::From("mojo:filesystem"); + application_impl()->ConnectToService(request.Pass(), &files_); + + filesystem::FileError error = filesystem::FILE_ERROR_FAILED; + filesystem::DirectoryPtr directory; + files_->OpenFileSystem("temp", GetProxy(&directory), mojo::Capture(&error)); + ASSERT_TRUE(files_.WaitForIncomingResponse()); + ASSERT_EQ(filesystem::FILE_ERROR_OK, error); + + vfs_.reset(new ScopedMojoFilesystemVFS(directory.Pass())); + } + + void TearDown() override { + vfs_.reset(); + mojo::test::ApplicationTestBase::TearDown(); + } + + private: + filesystem::FileSystemPtr files_; + scoped_ptr vfs_; + + DISALLOW_COPY_AND_ASSIGN(VFSTest); +}; + +TEST_F(VFSTest, TestInstalled) { + EXPECT_EQ("mojo", vfs()->zName); +} + +TEST_F(VFSTest, ExclusiveOpen) { + // Opening a non-existent file exclusively should work. + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen(vfs(), kFileName, file.get(), + SQLITE_OPEN_EXCLUSIVE | SQLITE_OPEN_CREATE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + // Opening it a second time exclusively shouldn't. + scoped_ptr file2(MakeFile()); + rc = vfs()->xOpen(vfs(), kFileName, file2.get(), + SQLITE_OPEN_EXCLUSIVE | SQLITE_OPEN_CREATE, + &out_flags); + EXPECT_NE(SQLITE_OK, rc); + + file->pMethods->xClose(file.get()); +} + +TEST_F(VFSTest, NonexclusiveOpen) { + // Opening a non-existent file should work. + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen(vfs(), kFileName, file.get(), + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + // Opening it a second time should work. + scoped_ptr file2(MakeFile()); + rc = vfs()->xOpen(vfs(), kFileName, file2.get(), + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + file->pMethods->xClose(file.get()); + file->pMethods->xClose(file2.get()); +} + +TEST_F(VFSTest, DeleteOnClose) { + { + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen( + vfs(), kFileName, file.get(), + SQLITE_OPEN_DELETEONCLOSE | SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + file->pMethods->xClose(file.get()); + } + + // The file shouldn't exist now. + int result = 0; + vfs()->xAccess(vfs(), kFileName, SQLITE_ACCESS_EXISTS, &result); + EXPECT_FALSE(result); +} + +TEST_F(VFSTest, TestNonExistence) { + // We shouldn't have a file exist yet in a fresh directory. + int result = 0; + vfs()->xAccess(vfs(), kFileName, SQLITE_ACCESS_EXISTS, &result); + EXPECT_FALSE(result); +} + +TEST_F(VFSTest, TestExistence) { + { + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen(vfs(), kFileName, file.get(), + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + file->pMethods->xClose(file.get()); + } + + int result = 0; + vfs()->xAccess(vfs(), kFileName, SQLITE_ACCESS_EXISTS, &result); + EXPECT_TRUE(result); +} + +TEST_F(VFSTest, TestDelete) { + { + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen(vfs(), kFileName, file.get(), + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + file->pMethods->xClose(file.get()); + } + + int result = 0; + vfs()->xAccess(vfs(), kFileName, SQLITE_ACCESS_EXISTS, &result); + EXPECT_TRUE(result); + + vfs()->xDelete(vfs(), kFileName, 0); + + vfs()->xAccess(vfs(), kFileName, SQLITE_ACCESS_EXISTS, &result); + EXPECT_FALSE(result); +} + +TEST_F(VFSTest, TestWriteAndRead) { + const char kBuffer[] = "One Two Three Four Five Six Seven"; + const int kBufferSize = arraysize(kBuffer); + + { + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen(vfs(), kFileName, file.get(), + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + for (int i = 0; i < 10; ++i) { + rc = file->pMethods->xWrite(file.get(), kBuffer, kBufferSize, + i * kBufferSize); + EXPECT_EQ(SQLITE_OK, rc); + } + + file->pMethods->xClose(file.get()); + } + + // Expect that the size of the file is 10 * arraysize(kBuffer); + { + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen(vfs(), kFileName, file.get(), + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + sqlite_int64 size; + rc = file->pMethods->xFileSize(file.get(), &size); + EXPECT_EQ(SQLITE_OK, rc); + EXPECT_EQ(10 * kBufferSize, size); + + file->pMethods->xClose(file.get()); + } + + // We should be able to read things back. + { + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen(vfs(), kFileName, file.get(), + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + char data_buffer[kBufferSize]; + memset(data_buffer, '8', kBufferSize); + for (int i = 0; i < 10; ++i) { + rc = file->pMethods->xRead(file.get(), data_buffer, kBufferSize, + i * kBufferSize); + EXPECT_EQ(SQLITE_OK, rc); + EXPECT_TRUE(strncmp(kBuffer, &data_buffer[0], kBufferSize) == 0); + } + + file->pMethods->xClose(file.get()); + } +} + +TEST_F(VFSTest, PartialRead) { + const char kBuffer[] = "One Two Three Four Five Six Seven"; + const int kBufferSize = arraysize(kBuffer); + + // Write the data once. + { + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen(vfs(), kFileName, file.get(), + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + rc = file->pMethods->xWrite(file.get(), kBuffer, kBufferSize, 0); + EXPECT_EQ(SQLITE_OK, rc); + + file->pMethods->xClose(file.get()); + } + + // Now attempt to read kBufferSize + 5 from a file sized to kBufferSize. + { + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen(vfs(), kFileName, file.get(), + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + const char kBufferWithFiveNulls[] = + "One Two Three Four Five Six Seven\0\0\0\0\0"; + const int kBufferWithFiveNullsSize = arraysize(kBufferWithFiveNulls); + + char data_buffer[kBufferWithFiveNullsSize]; + memset(data_buffer, '8', kBufferWithFiveNullsSize); + rc = file->pMethods->xRead(file.get(), data_buffer, + kBufferWithFiveNullsSize, 0); + EXPECT_EQ(SQLITE_IOERR_SHORT_READ, rc); + + EXPECT_TRUE(strncmp(kBufferWithFiveNulls, &data_buffer[0], + kBufferWithFiveNullsSize) == 0); + + file->pMethods->xClose(file.get()); + } +} + +TEST_F(VFSTest, Truncate) { + const char kBuffer[] = "One Two Three Four Five Six Seven"; + const int kBufferSize = arraysize(kBuffer); + const int kCharsToThree = 13; + + scoped_ptr file(MakeFile()); + int out_flags; + int rc = vfs()->xOpen(vfs(), kFileName, file.get(), + SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, + &out_flags); + EXPECT_EQ(SQLITE_OK, rc); + + rc = file->pMethods->xWrite(file.get(), kBuffer, kBufferSize, 0); + EXPECT_EQ(SQLITE_OK, rc); + + sqlite_int64 size; + rc = file->pMethods->xFileSize(file.get(), &size); + EXPECT_EQ(SQLITE_OK, rc); + EXPECT_EQ(kBufferSize, size); + + rc = file->pMethods->xTruncate(file.get(), kCharsToThree); + EXPECT_EQ(SQLITE_OK, rc); + + rc = file->pMethods->xFileSize(file.get(), &size); + EXPECT_EQ(SQLITE_OK, rc); + EXPECT_EQ(kCharsToThree, size); + + file->pMethods->xClose(file.get()); +} + +} // namespace sql diff --git a/sql/recovery_unittest.cc b/sql/recovery_unittest.cc index 78a1478..1f930cb 100644 --- a/sql/recovery_unittest.cc +++ b/sql/recovery_unittest.cc @@ -16,6 +16,7 @@ #include "sql/statement.h" #include "sql/test/paths.h" #include "sql/test/scoped_error_ignorer.h" +#include "sql/test/sql_test_base.h" #include "sql/test/test_helpers.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/sqlite/sqlite3.h" @@ -61,32 +62,7 @@ std::string GetSchema(sql::Connection* db) { return ExecuteWithResults(db, kSql, "|", "\n"); } -class SQLRecoveryTest : public testing::Test { - public: - SQLRecoveryTest() {} - - void SetUp() override { - ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); - ASSERT_TRUE(db_.Open(db_path())); - } - - void TearDown() override { db_.Close(); } - - sql::Connection& db() { return db_; } - - base::FilePath db_path() { - return temp_dir_.path().AppendASCII("SQLRecoveryTest.db"); - } - - bool Reopen() { - db_.Close(); - return db_.Open(db_path()); - } - - private: - base::ScopedTempDir temp_dir_; - sql::Connection db_; -}; +using SQLRecoveryTest = sql::SQLTestBase; TEST_F(SQLRecoveryTest, RecoverBasic) { const char kCreateSql[] = "CREATE TABLE x (t TEXT)"; diff --git a/sql/sql.gyp b/sql/sql.gyp index 5345791..d983a45 100644 --- a/sql/sql.gyp +++ b/sql/sql.gyp @@ -96,6 +96,8 @@ 'test/paths.cc', 'test/paths.h', 'test/run_all_unittests.cc', + 'test/sql_test_base.cc', + 'test/sql_test_base.h', 'test/sql_test_suite.cc', 'test/sql_test_suite.h', 'transaction_unittest.cc', diff --git a/sql/sqlite_features_unittest.cc b/sql/sqlite_features_unittest.cc index 2b95bb8..20e002d 100644 --- a/sql/sqlite_features_unittest.cc +++ b/sql/sqlite_features_unittest.cc @@ -9,6 +9,7 @@ #include "base/files/scoped_temp_dir.h" #include "sql/connection.h" #include "sql/statement.h" +#include "sql/test/sql_test_base.h" #include "sql/test/test_helpers.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/sqlite/sqlite3.h" @@ -24,34 +25,30 @@ void CaptureErrorCallback(int* error_pointer, std::string* sql_text, *sql_text = text ? text : "no statement available"; } -class SQLiteFeaturesTest : public testing::Test { +class SQLiteFeaturesTest : public sql::SQLTestBase { public: SQLiteFeaturesTest() : error_(SQLITE_OK) {} void SetUp() override { - ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); - ASSERT_TRUE(db_.Open(temp_dir_.path().AppendASCII("SQLStatementTest.db"))); + SQLTestBase::SetUp(); // The error delegate will set |error_| and |sql_text_| when any sqlite // statement operation returns an error code. - db_.set_error_callback(base::Bind(&CaptureErrorCallback, - &error_, &sql_text_)); + db().set_error_callback( + base::Bind(&CaptureErrorCallback, &error_, &sql_text_)); } void TearDown() override { // If any error happened the original sql statement can be found in // |sql_text_|. EXPECT_EQ(SQLITE_OK, error_) << sql_text_; - db_.Close(); + + SQLTestBase::TearDown(); } - sql::Connection& db() { return db_; } int error() { return error_; } private: - base::ScopedTempDir temp_dir_; - sql::Connection db_; - // The error code of the most recent error. int error_; // Original statement which has caused the error. diff --git a/sql/statement_unittest.cc b/sql/statement_unittest.cc index 38f1778..1565b3e 100644 --- a/sql/statement_unittest.cc +++ b/sql/statement_unittest.cc @@ -8,6 +8,7 @@ #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "sql/connection.h" +#include "sql/correct_sql_test_base.h" #include "sql/statement.h" #include "sql/test/error_callback_support.h" #include "sql/test/scoped_error_ignorer.h" @@ -16,21 +17,7 @@ namespace { -class SQLStatementTest : public testing::Test { - public: - void SetUp() override { - ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); - ASSERT_TRUE(db_.Open(temp_dir_.path().AppendASCII("SQLStatementTest.db"))); - } - - void TearDown() override { db_.Close(); } - - sql::Connection& db() { return db_; } - - private: - base::ScopedTempDir temp_dir_; - sql::Connection db_; -}; +using SQLStatementTest = sql::SQLTestBase; } // namespace diff --git a/sql/test/sql_test_base.cc b/sql/test/sql_test_base.cc new file mode 100644 index 0000000..bdd427f --- /dev/null +++ b/sql/test/sql_test_base.cc @@ -0,0 +1,66 @@ +// Copyright 2015 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 "sql/test/sql_test_base.h" + +#include "base/files/file_util.h" +#include "sql/test/test_helpers.h" + +namespace sql { + +SQLTestBase::SQLTestBase() { +} + +SQLTestBase::~SQLTestBase() { +} + +base::FilePath SQLTestBase::db_path() { + return temp_dir_.path().AppendASCII("SQLTest.db"); +} + +sql::Connection& SQLTestBase::db() { + return db_; +} + +bool SQLTestBase::Reopen() { + db_.Close(); + return db_.Open(db_path()); +} + +bool SQLTestBase::GetPathExists(const base::FilePath& path) { + return base::PathExists(path); +} + +bool SQLTestBase::CorruptSizeInHeaderOfDB() { + return sql::test::CorruptSizeInHeader(db_path()); +} + +void SQLTestBase::WriteJunkToDatabase(WriteJunkType type) { + base::ScopedFILE file(base::OpenFile( + db_path(), + type == TYPE_OVERWRITE_AND_TRUNCATE ? "wb" : "rb+")); + ASSERT_TRUE(file.get() != NULL); + ASSERT_EQ(0, fseek(file.get(), 0, SEEK_SET)); + + const char* kJunk = "Now is the winter of our discontent."; + fputs(kJunk, file.get()); +} + +void SQLTestBase::TruncateDatabase() { + base::ScopedFILE file(base::OpenFile(db_path(), "rb+")); + ASSERT_TRUE(file.get() != NULL); + ASSERT_EQ(0, fseek(file.get(), 0, SEEK_SET)); + ASSERT_TRUE(base::TruncateFile(file.get())); +} + +void SQLTestBase::SetUp() { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + ASSERT_TRUE(db_.Open(db_path())); +} + +void SQLTestBase::TearDown() { + db_.Close(); +} + +} // namespace sql diff --git a/sql/test/sql_test_base.h b/sql/test/sql_test_base.h new file mode 100644 index 0000000..ebe9048 --- /dev/null +++ b/sql/test/sql_test_base.h @@ -0,0 +1,78 @@ +// Copyright 2015 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. + +#ifndef SQL_TEST_SQL_TEST_BASE_H_ +#define SQL_TEST_SQL_TEST_BASE_H_ + +#include "base/files/file_path.h" +#include "base/files/scoped_temp_dir.h" +#include "base/memory/scoped_ptr.h" +#include "sql/connection.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace sql { + +class Connection; + +// Base class for SQL tests. +// +// WARNING: We want to run the same gtest based unit test code both against +// chromium (which uses this implementation here), and the mojo code (which +// uses a different class named SQLTestBase). These two classes need to have +// the same interface because we compile time switch them based on a +// #define. We need to have two different implementations because the mojo +// version derives from mojo::test::ApplicationTestBase instead of +// testing::Test. +class SQLTestBase : public testing::Test { + public: + SQLTestBase(); + ~SQLTestBase() override; + + enum WriteJunkType { + TYPE_OVERWRITE_AND_TRUNCATE, + TYPE_OVERWRITE + }; + + // Returns the path to the database. + base::FilePath db_path(); + + // Returns a connection to the database at db_path(). + sql::Connection& db(); + + // Closes the current connection to the database and reopens it. + bool Reopen(); + + // Proxying method around base::PathExists. + bool GetPathExists(const base::FilePath& path); + + // SQLite stores the database size in the header, and if the actual + // OS-derived size is smaller, the database is considered corrupt. + // [This case is actually a common form of corruption in the wild.] + // This helper sets the in-header size to one page larger than the + // actual file size. The resulting file will return SQLITE_CORRUPT + // for most operations unless PRAGMA writable_schema is turned ON. + // + // Returns false if any error occurs accessing the file. + bool CorruptSizeInHeaderOfDB(); + + // Writes junk to the start of the file. + void WriteJunkToDatabase(WriteJunkType type); + + // Sets the database file size to 0. + void TruncateDatabase(); + + // Overridden from testing::Test: + void SetUp() override; + void TearDown() override; + + private: + base::ScopedTempDir temp_dir_; + sql::Connection db_; + + DISALLOW_COPY_AND_ASSIGN(SQLTestBase); +}; + +} // namespace sql + +#endif // SQL_TEST_SQL_TEST_BASE_H_ diff --git a/sql/test/test_helpers.cc b/sql/test/test_helpers.cc index 361b336..16f2a7e 100644 --- a/sql/test/test_helpers.cc +++ b/sql/test/test_helpers.cc @@ -69,10 +69,6 @@ namespace test { bool CorruptSizeInHeader(const base::FilePath& db_path) { // See http://www.sqlite.org/fileformat.html#database_header const size_t kHeaderSize = 100; - const size_t kPageSizeOffset = 16; - const size_t kFileChangeCountOffset = 24; - const size_t kPageCountOffset = 28; - const size_t kVersionValidForOffset = 92; // duplicate kFileChangeCountOffset unsigned char header[kHeaderSize]; @@ -89,6 +85,22 @@ bool CorruptSizeInHeader(const base::FilePath& db_path) { if (!base::GetFileSize(db_path, &db_size)) return false; + CorruptSizeInHeaderMemory(header, db_size); + + if (0 != fseek(file.get(), 0, SEEK_SET)) + return false; + if (1u != fwrite(header, sizeof(header), 1, file.get())) + return false; + + return true; +} + +void CorruptSizeInHeaderMemory(unsigned char* header, int64_t db_size) { + const size_t kPageSizeOffset = 16; + const size_t kFileChangeCountOffset = 24; + const size_t kPageCountOffset = 28; + const size_t kVersionValidForOffset = 92; // duplicate kFileChangeCountOffset + const unsigned page_size = ReadBigEndian(header + kPageSizeOffset, 2); // One larger than the expected size. @@ -101,13 +113,6 @@ bool CorruptSizeInHeader(const base::FilePath& db_path) { unsigned change_count = ReadBigEndian(header + kFileChangeCountOffset, 4); WriteBigEndian(change_count + 1, header + kFileChangeCountOffset, 4); WriteBigEndian(change_count + 1, header + kVersionValidForOffset, 4); - - if (0 != fseek(file.get(), 0, SEEK_SET)) - return false; - if (1u != fwrite(header, sizeof(header), 1, file.get())) - return false; - - return true; } bool CorruptTableOrIndex(const base::FilePath& db_path, diff --git a/sql/test/test_helpers.h b/sql/test/test_helpers.h index e93b207..b2cecb0 100644 --- a/sql/test/test_helpers.h +++ b/sql/test/test_helpers.h @@ -33,6 +33,11 @@ namespace test { // Returns false if any error occurs accessing the file. bool CorruptSizeInHeader(const base::FilePath& db_path) WARN_UNUSED_RESULT; +// Common implementation of CorruptSizeInHeader() which operates on loaded +// memory. Shared between CorruptSizeInHeader() and the the mojo proxy testing +// code. +void CorruptSizeInHeaderMemory(unsigned char* header, int64_t db_size); + // Frequently corruption is a result of failure to atomically update // pages in different structures. For instance, if an index update // takes effect but the corresponding table update does not. This diff --git a/sql/transaction_unittest.cc b/sql/transaction_unittest.cc index 83d4125..179adcf 100644 --- a/sql/transaction_unittest.cc +++ b/sql/transaction_unittest.cc @@ -5,35 +5,26 @@ #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "sql/connection.h" +#include "sql/correct_sql_test_base.h" #include "sql/statement.h" #include "sql/transaction.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/sqlite/sqlite3.h" -class SQLTransactionTest : public testing::Test { +class SQLTransactionTest : public sql::SQLTestBase { public: void SetUp() override { - ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); - ASSERT_TRUE(db_.Open( - temp_dir_.path().AppendASCII("SQLTransactionTest.db"))); + SQLTestBase::SetUp(); ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); } - void TearDown() override { db_.Close(); } - - sql::Connection& db() { return db_; } - // Returns the number of rows in table "foo". int CountFoo() { sql::Statement count(db().GetUniqueStatement("SELECT count(*) FROM foo")); count.Step(); return count.ColumnInt(0); } - - private: - base::ScopedTempDir temp_dir_; - sql::Connection db_; }; TEST_F(SQLTransactionTest, Commit) { -- cgit v1.1