summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorerg <erg@chromium.org>2015-06-19 18:38:13 -0700
committerCommit bot <commit-bot@chromium.org>2015-06-20 01:38:55 +0000
commit102ceb4133685db70ac15cb5bcb134a85736fe86 (patch)
treeb902e100529fe3675e293c49c70eed32c430daf7
parent55002d65c4a31e6b386cbe9dc02dfaa24d3fb42d (diff)
downloadchromium_src-102ceb4133685db70ac15cb5bcb134a85736fe86.zip
chromium_src-102ceb4133685db70ac15cb5bcb134a85736fe86.tar.gz
chromium_src-102ceb4133685db70ac15cb5bcb134a85736fe86.tar.bz2
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}
-rw-r--r--components/filesystem/DEPS1
-rw-r--r--components/filesystem/directory_impl.cc38
-rw-r--r--components/filesystem/directory_impl.h5
-rw-r--r--components/filesystem/directory_impl_unittest.cc3
-rw-r--r--components/filesystem/file_impl.cc10
-rw-r--r--components/filesystem/file_impl.h1
-rw-r--r--components/filesystem/file_impl_unittest.cc3
-rw-r--r--components/filesystem/files_test_base.cc3
-rw-r--r--components/filesystem/files_test_base.h67
-rw-r--r--components/filesystem/public/interfaces/directory.mojom10
-rw-r--r--components/filesystem/public/interfaces/file.mojom3
-rw-r--r--components/filesystem/public/interfaces/types.mojom2
-rw-r--r--mandoline/BUILD.gn1
-rw-r--r--mojo/services/network/BUILD.gn1
-rw-r--r--mojo/tools/data/apptests8
-rw-r--r--mojo/util/capture_util.h36
-rw-r--r--sql/BUILD.gn14
-rw-r--r--sql/connection.cc51
-rw-r--r--sql/connection_unittest.cc86
-rw-r--r--sql/correct_sql_test_base.h27
-rw-r--r--sql/meta_table_unittest.cc17
-rw-r--r--sql/mojo/BUILD.gn62
-rw-r--r--sql/mojo/DEPS6
-rw-r--r--sql/mojo/mojo_vfs.cc413
-rw-r--r--sql/mojo/mojo_vfs.h45
-rw-r--r--sql/mojo/sql_test_base.cc156
-rw-r--r--sql/mojo/sql_test_base.h85
-rw-r--r--sql/mojo/vfs_unittest.cc317
-rw-r--r--sql/recovery_unittest.cc28
-rw-r--r--sql/sql.gyp2
-rw-r--r--sql/sqlite_features_unittest.cc17
-rw-r--r--sql/statement_unittest.cc17
-rw-r--r--sql/test/sql_test_base.cc66
-rw-r--r--sql/test/sql_test_base.h78
-rw-r--r--sql/test/test_helpers.cc27
-rw-r--r--sql/test/test_helpers.h5
-rw-r--r--sql/transaction_unittest.cc15
37 files changed, 1501 insertions, 225 deletions
diff --git a/components/filesystem/DEPS b/components/filesystem/DEPS
index 40d8075..b3412d0 100644
--- a/components/filesystem/DEPS
+++ b/components/filesystem/DEPS
@@ -2,4 +2,5 @@ include_rules = [
"+mojo/application",
"+mojo/platform_handle",
"+mojo/public",
+ "+mojo/util",
]
diff --git a/components/filesystem/directory_impl.cc b/components/filesystem/directory_impl.cc
index 4a6caac..3d63f0e 100644
--- a/components/filesystem/directory_impl.cc
+++ b/components/filesystem/directory_impl.cc
@@ -171,4 +171,42 @@ void DirectoryImpl::Delete(const mojo::String& raw_path,
callback.Run(FILE_ERROR_OK);
}
+void DirectoryImpl::Exists(const mojo::String& raw_path,
+ const ExistsCallback& callback) {
+ base::FilePath path;
+ if (FileError error = ValidatePath(raw_path, directory_path_, &path)) {
+ callback.Run(error, false);
+ return;
+ }
+
+ bool exists = base::PathExists(path);
+ callback.Run(FILE_ERROR_OK, exists);
+}
+
+void DirectoryImpl::IsWritable(const mojo::String& raw_path,
+ const IsWritableCallback& callback) {
+ base::FilePath path;
+ if (FileError error = ValidatePath(raw_path, directory_path_, &path)) {
+ callback.Run(error, false);
+ return;
+ }
+
+ callback.Run(FILE_ERROR_OK, base::PathIsWritable(path));
+}
+
+void DirectoryImpl::Flush(const FlushCallback& callback) {
+ base::File file(directory_path_, base::File::FLAG_READ);
+ if (!file.IsValid()) {
+ callback.Run(FILE_ERROR_FAILED);
+ return;
+ }
+
+ if (!file.Flush()) {
+ callback.Run(FILE_ERROR_FAILED);
+ return;
+ }
+
+ callback.Run(FILE_ERROR_OK);
+}
+
} // namespace filesystem
diff --git a/components/filesystem/directory_impl.h b/components/filesystem/directory_impl.h
index 17c752f..ed19d00 100644
--- a/components/filesystem/directory_impl.h
+++ b/components/filesystem/directory_impl.h
@@ -44,6 +44,11 @@ class DirectoryImpl : public Directory {
void Delete(const mojo::String& path,
uint32_t delete_flags,
const DeleteCallback& callback) override;
+ void Exists(const mojo::String& path,
+ const ExistsCallback& callback) override;
+ void IsWritable(const mojo::String& path,
+ const IsWritableCallback& callback) override;
+ void Flush(const FlushCallback& callback) override;
private:
mojo::StrongBinding<Directory> binding_;
diff --git a/components/filesystem/directory_impl_unittest.cc b/components/filesystem/directory_impl_unittest.cc
index a5759b5..983302f 100644
--- a/components/filesystem/directory_impl_unittest.cc
+++ b/components/filesystem/directory_impl_unittest.cc
@@ -6,6 +6,9 @@
#include <string>
#include "components/filesystem/files_test_base.h"
+#include "mojo/util/capture_util.h"
+
+using mojo::Capture;
namespace filesystem {
namespace {
diff --git a/components/filesystem/file_impl.cc b/components/filesystem/file_impl.cc
index f378432..2539498 100644
--- a/components/filesystem/file_impl.cc
+++ b/components/filesystem/file_impl.cc
@@ -260,6 +260,16 @@ void FileImpl::Dup(mojo::InterfaceRequest<File> file,
callback.Run(FILE_ERROR_OK);
}
+void FileImpl::Flush(const FlushCallback& callback) {
+ if (!file_.IsValid()) {
+ callback.Run(GetError(file_));
+ return;
+ }
+
+ bool ret = file_.Flush();
+ callback.Run(ret ? FILE_ERROR_OK : FILE_ERROR_FAILED);
+}
+
void FileImpl::AsHandle(const AsHandleCallback& callback) {
if (!file_.IsValid()) {
callback.Run(GetError(file_), ScopedHandle());
diff --git a/components/filesystem/file_impl.h b/components/filesystem/file_impl.h
index 469e89d..0ca3501 100644
--- a/components/filesystem/file_impl.h
+++ b/components/filesystem/file_impl.h
@@ -47,6 +47,7 @@ class FileImpl : public File {
const TouchCallback& callback) override;
void Dup(mojo::InterfaceRequest<File> file,
const DupCallback& callback) override;
+ void Flush(const FlushCallback& callback) override;
void AsHandle(const AsHandleCallback& callback) override;
private:
diff --git a/components/filesystem/file_impl_unittest.cc b/components/filesystem/file_impl_unittest.cc
index c55e516..86ec3ed 100644
--- a/components/filesystem/file_impl_unittest.cc
+++ b/components/filesystem/file_impl_unittest.cc
@@ -9,6 +9,9 @@
#include "mojo/platform_handle/platform_handle_functions.h"
#include "mojo/public/cpp/bindings/interface_request.h"
#include "mojo/public/cpp/bindings/type_converter.h"
+#include "mojo/util/capture_util.h"
+
+using mojo::Capture;
namespace filesystem {
namespace {
diff --git a/components/filesystem/files_test_base.cc b/components/filesystem/files_test_base.cc
index 5bd477a..47f7d61 100644
--- a/components/filesystem/files_test_base.cc
+++ b/components/filesystem/files_test_base.cc
@@ -7,6 +7,7 @@
#include "components/filesystem/public/interfaces/directory.mojom.h"
#include "components/filesystem/public/interfaces/types.mojom.h"
#include "mojo/application/public/cpp/application_impl.h"
+#include "mojo/util/capture_util.h"
namespace filesystem {
@@ -26,7 +27,7 @@ void FilesTestBase::SetUp() {
void FilesTestBase::GetTemporaryRoot(DirectoryPtr* directory) {
FileError error = FILE_ERROR_FAILED;
- files()->OpenFileSystem("temp", GetProxy(directory), Capture(&error));
+ files()->OpenFileSystem("temp", GetProxy(directory), mojo::Capture(&error));
ASSERT_TRUE(files().WaitForIncomingResponse());
ASSERT_EQ(FILE_ERROR_OK, error);
}
diff --git a/components/filesystem/files_test_base.h b/components/filesystem/files_test_base.h
index 36e15d5..bc9a9a3 100644
--- a/components/filesystem/files_test_base.h
+++ b/components/filesystem/files_test_base.h
@@ -11,73 +11,6 @@
namespace filesystem {
-// TODO(vtl): Stuff copied from mojo/public/cpp/bindings/lib/template_util.h.
-template <class T, T v>
-struct IntegralConstant {
- static const T value = v;
-};
-
-template <class T, T v>
-const T IntegralConstant<T, v>::value;
-
-typedef IntegralConstant<bool, true> TrueType;
-typedef IntegralConstant<bool, false> FalseType;
-
-template <class T>
-struct IsConst : FalseType {};
-template <class T>
-struct IsConst<const T> : TrueType {};
-
-template <bool B, typename T = void>
-struct EnableIf {};
-
-template <typename T>
-struct EnableIf<true, T> {
- typedef T type;
-};
-
-typedef char YesType;
-
-struct NoType {
- YesType dummy[2];
-};
-
-template <typename T>
-struct IsMoveOnlyType {
- template <typename U>
- static YesType Test(const typename U::MoveOnlyTypeForCPP03*);
-
- template <typename U>
- static NoType Test(...);
-
- static const bool value =
- sizeof(Test<T>(0)) == sizeof(YesType) && !IsConst<T>::value;
-};
-
-template <typename T>
-typename EnableIf<!IsMoveOnlyType<T>::value, T>::type& Forward(T& t) {
- return t;
-}
-
-template <typename T>
-typename EnableIf<IsMoveOnlyType<T>::value, T>::type Forward(T& t) {
- return t.Pass();
-}
-// TODO(vtl): (End of stuff copied from template_util.h.)
-
-template <typename T1>
-mojo::Callback<void(T1)> Capture(T1* t1) {
- return [t1](T1 got_t1) { *t1 = Forward(got_t1); };
-}
-
-template <typename T1, typename T2>
-mojo::Callback<void(T1, T2)> Capture(T1* t1, T2* t2) {
- return [t1, t2](T1 got_t1, T2 got_t2) {
- *t1 = Forward(got_t1);
- *t2 = Forward(got_t2);
- };
-}
-
class FilesTestBase : public mojo::test::ApplicationTestBase {
public:
FilesTestBase();
diff --git a/components/filesystem/public/interfaces/directory.mojom b/components/filesystem/public/interfaces/directory.mojom
index 5d3a1db..bd5aba4 100644
--- a/components/filesystem/public/interfaces/directory.mojom
+++ b/components/filesystem/public/interfaces/directory.mojom
@@ -42,6 +42,16 @@ interface Directory {
// |kDeleteFlag...| for details).
Delete(string path, uint32 delete_flags) => (FileError error);
+ // Returns true if |path| exists.
+ Exists(string path) => (FileError error, bool exists);
+
+ // Returns true if |path| is writable.
+ IsWritable(string path) => (FileError error, bool is_writable);
+
+ // Opens a file descriptor on this directory and calls
+ // fsync()/FlushFileBuffers().
+ Flush() => (FileError error);
+
// TODO(vtl): directory "streaming"?
// TODO(vtl): "make root" (i.e., prevent cd-ing, etc., to parent); note that
// this would require a much more complicated implementation (e.g., it needs
diff --git a/components/filesystem/public/interfaces/file.mojom b/components/filesystem/public/interfaces/file.mojom
index 306b9f7..d64c0f1 100644
--- a/components/filesystem/public/interfaces/file.mojom
+++ b/components/filesystem/public/interfaces/file.mojom
@@ -59,6 +59,9 @@ interface File {
// |open_flags| argument) as well as file position.
Dup(File& file) => (FileError error);
+ // Syncs data to disk.
+ Flush() => (FileError error);
+
// Returns a handle to the file for raw access.
AsHandle() => (FileError error, handle file_handle);
};
diff --git a/components/filesystem/public/interfaces/types.mojom b/components/filesystem/public/interfaces/types.mojom
index 97988f6..6a2ba49b 100644
--- a/components/filesystem/public/interfaces/types.mojom
+++ b/components/filesystem/public/interfaces/types.mojom
@@ -77,6 +77,8 @@ const uint32 kFlagOpenTruncated = 0x10;
const uint32 kFlagRead = 0x20;
const uint32 kFlagWrite = 0x40;
const uint32 kFlagAppend = 0x80;
+// Deletes the file when closed.
+const uint32 kDeleteOnClose = 0x2000;
// File types.
//
diff --git a/mandoline/BUILD.gn b/mandoline/BUILD.gn
index d76d752..7a35fce 100644
--- a/mandoline/BUILD.gn
+++ b/mandoline/BUILD.gn
@@ -35,6 +35,7 @@ group("tests") {
"//components/resource_provider:apptests",
"//components/resource_provider:resource_provider_unittests",
"//components/view_manager:tests",
+ "//sql/mojo:apptests",
"//mandoline/tab:mandoline_frame_apptests",
"//media/mojo/services:media_apptests",
]
diff --git a/mojo/services/network/BUILD.gn b/mojo/services/network/BUILD.gn
index 02ef3f0..2296d44 100644
--- a/mojo/services/network/BUILD.gn
+++ b/mojo/services/network/BUILD.gn
@@ -92,6 +92,7 @@ source_set("lib") {
"//net",
"//net:http_server",
"//url",
+ "//sql/mojo",
]
}
diff --git a/mojo/tools/data/apptests b/mojo/tools/data/apptests
index 50b47e4..1b8bee8 100644
--- a/mojo/tools/data/apptests
+++ b/mojo/tools/data/apptests
@@ -55,13 +55,17 @@ if config.target_os != config.OS_ANDROID:
"args": ["--use-test-config"]
},
{
+ "test": "mojo:mandoline_frame_apptests",
+ "type": "gtest_isolated",
+ "args": ["--use-headless-config"]
+ },
+ {
"test": "mojo:resource_provider_apptests",
"type": "gtest_isolated",
},
{
- "test": "mojo:mandoline_frame_apptests",
+ "test": "mojo:sql_apptests",
"type": "gtest_isolated",
- "args": ["--use-headless-config"]
},
# TODO(xhwang): Fix and enable the media_apptests. http://crbug.com/501417
#{
diff --git a/mojo/util/capture_util.h b/mojo/util/capture_util.h
new file mode 100644
index 0000000..4b89eb7
--- /dev/null
+++ b/mojo/util/capture_util.h
@@ -0,0 +1,36 @@
+// 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 MOJO_UTIL_CAPTURE_UTIL_H_
+#define MOJO_UTIL_CAPTURE_UTIL_H_
+
+#include "mojo/public/cpp/bindings/lib/template_util.h"
+
+namespace mojo {
+
+template <typename T1>
+mojo::Callback<void(T1)> Capture(T1* t1) {
+ return [t1](T1 got_t1) { *t1 = mojo::internal::Forward(got_t1); };
+}
+
+template <typename T1, typename T2>
+mojo::Callback<void(T1, T2)> Capture(T1* t1, T2* t2) {
+ return [t1, t2](T1 got_t1, T2 got_t2) {
+ *t1 = mojo::internal::Forward(got_t1);
+ *t2 = mojo::internal::Forward(got_t2);
+ };
+}
+
+template <typename T1, typename T2, typename T3>
+mojo::Callback<void(T1, T2, T3)> Capture(T1* t1, T2* t2, T3* t3) {
+ return [t1, t2, t3](T1 got_t1, T2 got_t2, T3 got_t3) {
+ *t1 = mojo::internal::Forward(got_t1);
+ *t2 = mojo::internal::Forward(got_t2);
+ *t3 = mojo::internal::Forward(got_t3);
+ };
+}
+
+} // namespace mojo
+
+#endif // MOJO_UTIL_CAPTURE_UTIL_H_
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<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
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) {