diff options
Diffstat (limited to 'sql/mojo')
-rw-r--r-- | sql/mojo/BUILD.gn | 62 | ||||
-rw-r--r-- | sql/mojo/DEPS | 6 | ||||
-rw-r--r-- | sql/mojo/mojo_vfs.cc | 413 | ||||
-rw-r--r-- | sql/mojo/mojo_vfs.h | 45 | ||||
-rw-r--r-- | sql/mojo/sql_test_base.cc | 156 | ||||
-rw-r--r-- | sql/mojo/sql_test_base.h | 85 | ||||
-rw-r--r-- | sql/mojo/vfs_unittest.cc | 317 |
7 files changed, 1084 insertions, 0 deletions
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<ScopedMojoFilesystemVFS*>(mojo_vfs->pAppData)->parent_; +} + +filesystem::DirectoryPtr& GetRootDirectory(sqlite3_vfs* mojo_vfs) { + return static_cast<ScopedMojoFilesystemVFS*>(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<MojoVFSFile*>(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<uint8_t> 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_t>(size)) + return SQLITE_OK; + + // We didn't read the entire buffer. Fill the rest of the buffer with zeros. + memset(reinterpret_cast<char*>(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<uint8_t> 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<uint32_t>(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<uint8_t> 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<uint8_t> 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<ScopedMojoFilesystemVFS> 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<sqlite3_file> { + 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<uint8_t*>(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<sqlite3_file> MakeFile() { + return scoped_ptr<sqlite3_file>(reinterpret_cast<sqlite3_file*>( + 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<ScopedMojoFilesystemVFS> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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<sqlite3_file> 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 |