Bug 1482608 - Add basic Rust bindings for mozStorage. r=nika,asuth,mak

This commit wraps just enough of the mozStorage API to support the
bookmarks mirror. It's not complete: for example, there's no way
to open, clone, or close a connection, because the mirror handles
that from JS. The wrapper also omits shutdown blocking and retrying on
`SQLITE_BUSY`.

This commit also changes the behavior of sync and async mozStorage
connections. Async (`mozIStorageAsyncConnection`) methods may be called
from any thread on any connection. Sync (`mozIStorageConnection`)
methods may be called from any thread on a sync connection, and from
background threads on an async connection. All connections now QI
to `mozIStorageConnection`, but attempting to call a sync method on
an async connection from the main thread throws.

Finally, this commit exposes an `OpenedConnection::unsafeRawConnection`
getter in Sqlite.jsm, for JS code to access the underlying connection.

Differential Revision: https://phabricator.services.mozilla.com/D20073

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Lina Cambridge 2019-03-25 04:49:18 +00:00
parent bc72d9813e
commit 3e894ac30e
15 changed files with 625 additions and 78 deletions

12
Cargo.lock generated
View file

@ -1216,6 +1216,7 @@ dependencies = [
"profiler_helper 0.1.0",
"rsdparsa_capi 0.1.0",
"rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"storage 0.1.0",
"u2fhid 0.2.3",
"webrender_bindings 0.1.0",
"xpcom 0.1.0",
@ -2563,6 +2564,17 @@ name = "stable_deref_trait"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "storage"
version = "0.1.0"
dependencies = [
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
"nserror 0.1.0",
"nsstring 0.1.0",
"storage_variant 0.1.0",
"xpcom 0.1.0",
]
[[package]]
name = "storage_variant"
version = "0.1.0"

View file

@ -95,8 +95,8 @@ interface mozIStorageAsyncConnection : nsISupports {
*
* @note If your connection is already read-only, you will get a read-only
* clone.
* @note The resulting connection will NOT implement mozIStorageConnection,
* it will only implement mozIStorageAsyncConnection.
* @note The resulting connection will implement `mozIStorageConnection`, but
* all synchronous methods will throw if called from the main thread.
* @note Due to a bug in SQLite, if you use the shared cache
* (see mozIStorageService), you end up with the same privileges as the
* first connection opened regardless of what is specified in aReadOnly.

View file

@ -149,6 +149,16 @@ interface mozIStorageStatement : mozIStorageBaseStatement {
*/
long getTypeOfIndex(in unsigned long aIndex);
/**
* Retrieve the contents of a column from the current result row as a
* variant.
*
* @param aIndex
* 0-based colummn index.
* @return A variant with the type of the column value.
*/
nsIVariant getVariant(in unsigned long aIndex);
/**
* Retrieve the contents of a column from the current result row as an
* integer.

View file

@ -457,7 +457,8 @@ NS_IMPL_ISUPPORTS(CloseListener, mozIStorageCompletionCallback)
////////////////////////////////////////////////////////////////////////////////
//// Connection
Connection::Connection(Service *aService, int aFlags, bool aAsyncOnly,
Connection::Connection(Service *aService, int aFlags,
ConnectionOperation aSupportedOperations,
bool aIgnoreLockingMode)
: sharedAsyncExecutionMutex("Connection::sharedAsyncExecutionMutex"),
sharedDBMutex("Connection::sharedDBMutex"),
@ -472,7 +473,7 @@ Connection::Connection(Service *aService, int aFlags, bool aAsyncOnly,
mFlags(aFlags),
mIgnoreLockingMode(aIgnoreLockingMode),
mStorageService(aService),
mAsyncOnly(aAsyncOnly) {
mSupportedOperations(aSupportedOperations) {
MOZ_ASSERT(!mIgnoreLockingMode || mFlags & SQLITE_OPEN_READONLY,
"Can't ignore locking for a non-readonly connection!");
mStorageService->registerConnection(this);
@ -491,7 +492,7 @@ NS_IMPL_ADDREF(Connection)
NS_INTERFACE_MAP_BEGIN(Connection)
NS_INTERFACE_MAP_ENTRY(mozIStorageAsyncConnection)
NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor)
NS_INTERFACE_MAP_ENTRY_CONDITIONAL(mozIStorageConnection, !mAsyncOnly)
NS_INTERFACE_MAP_ENTRY(mozIStorageConnection)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIStorageConnection)
NS_INTERFACE_MAP_END
@ -528,10 +529,11 @@ NS_IMETHODIMP_(MozExternalRefCountType) Connection::Release(void) {
// This could cause SpinningSynchronousClose() to be invoked and AddRef
// triggered for AsyncCloseConnection's strong ref if the conn was ever
// use for async purposes. (Main-thread only, though.)
Unused << Close();
Unused << synchronousClose();
} else {
nsCOMPtr<nsIRunnable> event = NewRunnableMethod(
"storage::Connection::Close", this, &Connection::Close);
nsCOMPtr<nsIRunnable> event =
NewRunnableMethod("storage::Connection::synchronousClose", this,
&Connection::synchronousClose);
if (NS_FAILED(
threadOpenedOn->Dispatch(event.forget(), NS_DISPATCH_NORMAL))) {
// The target thread was dead and so we've just leaked our runnable.
@ -539,8 +541,9 @@ NS_IMETHODIMP_(MozExternalRefCountType) Connection::Release(void) {
// be explicitly closing their connections, not relying on us to close
// them for them. (It's okay to let a statement go out of scope for
// automatic cleanup, but not a Connection.)
MOZ_ASSERT(false, "Leaked Connection::Close(), ownership fail.");
Unused << Close();
MOZ_ASSERT(false,
"Leaked Connection::synchronousClose(), ownership fail.");
Unused << synchronousClose();
}
}
@ -823,7 +826,10 @@ void Connection::initializeFailed() {
nsresult Connection::databaseElementExists(
enum DatabaseElementType aElementType, const nsACString &aElementName,
bool *_exists) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
// When constructing the query, make sure to SELECT the correct db's
// sqlite_master if the user is prefixing the element with a specific db. ex:
@ -927,14 +933,25 @@ nsresult Connection::setClosedState() {
return NS_OK;
}
bool Connection::connectionReady() { return mDBConn != nullptr; }
nsresult Connection::connectionReady(ConnectionOperation aOperation) {
if (NS_WARN_IF(aOperation == SYNCHRONOUS &&
mSupportedOperations == ASYNCHRONOUS && NS_IsMainThread())) {
MOZ_ASSERT(false,
"Don't use async connections synchronously on the main thread");
return NS_ERROR_NOT_AVAILABLE;
}
if (!mDBConn) {
return NS_ERROR_NOT_INITIALIZED;
}
return NS_OK;
}
bool Connection::isConnectionReadyOnThisThread() {
MOZ_ASSERT_IF(mDBConn, !mConnectionClosed);
if (mAsyncExecutionThread && mAsyncExecutionThread->IsOnCurrentThread()) {
return true;
}
return connectionReady();
return mDBConn != nullptr;
}
bool Connection::isClosing() {
@ -1211,7 +1228,17 @@ Connection::GetInterface(const nsIID &aIID, void **_result) {
NS_IMETHODIMP
Connection::Close() {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
return synchronousClose();
}
nsresult Connection::synchronousClose() {
if (!mDBConn) {
return NS_ERROR_NOT_INITIALIZED;
}
#ifdef DEBUG
// Since we're accessing mAsyncExecutionThread, we need to be on the opener
@ -1259,13 +1286,14 @@ Connection::SpinningSynchronousClose() {
// As currently implemented, we can't spin to wait for an existing AsyncClose.
// Our only existing caller will never have called close; assert if misused
// so that no new callers assume this works after an AsyncClose.
MOZ_DIAGNOSTIC_ASSERT(connectionReady());
if (!connectionReady()) {
nsresult rv = connectionReady(SYNCHRONOUS);
MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
if (NS_FAILED(rv)) {
return NS_ERROR_UNEXPECTED;
}
RefPtr<CloseListener> listener = new CloseListener();
nsresult rv = AsyncClose(listener);
rv = AsyncClose(listener);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { return listener->mClosed; }));
MOZ_ASSERT(isClosed(), "The connection should be closed at this point");
@ -1277,8 +1305,9 @@ NS_IMETHODIMP
Connection::AsyncClose(mozIStorageCompletionCallback *aCallback) {
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD);
// Check if AsyncClose or Close were already invoked.
if (!mDBConn) {
return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(ASYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
// The two relevant factors at this point are whether we have a database
@ -1351,7 +1380,7 @@ Connection::AsyncClose(mozIStorageCompletionCallback *aCallback) {
// callers ignore our return value.
Unused << NS_DispatchToMainThread(completeEvent.forget());
}
MOZ_ALWAYS_SUCCEEDS(Close());
MOZ_ALWAYS_SUCCEEDS(synchronousClose());
// Return a success inconditionally here, since Close() is unlikely to fail
// and we want to reassure the consumer that its callback will be invoked.
return NS_OK;
@ -1360,7 +1389,7 @@ Connection::AsyncClose(mozIStorageCompletionCallback *aCallback) {
// setClosedState nullifies our connection pointer, so we take a raw pointer
// off it, to pass it through the close procedure.
sqlite3 *nativeConn = mDBConn;
nsresult rv = setClosedState();
rv = setClosedState();
NS_ENSURE_SUCCESS(rv, rv);
// Create and dispatch our close event to the background thread.
@ -1378,7 +1407,10 @@ Connection::AsyncClone(bool aReadOnly,
AUTO_PROFILER_LABEL("Connection::AsyncClone", OTHER);
NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD);
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(ASYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
if (!mDatabaseFile) return NS_ERROR_UNEXPECTED;
int flags = mFlags;
@ -1389,8 +1421,10 @@ Connection::AsyncClone(bool aReadOnly,
flags = (~SQLITE_OPEN_CREATE & flags);
}
// Force the cloned connection to only implement the async connection API.
RefPtr<Connection> clone = new Connection(mStorageService, flags, true);
// The cloned connection will still implement the synchronous API, but throw
// if any synchronous methods are called on the main thread.
RefPtr<Connection> clone =
new Connection(mStorageService, flags, ASYNCHRONOUS);
RefPtr<AsyncInitializeClone> initEvent =
new AsyncInitializeClone(this, clone, aReadOnly, aCallback);
@ -1553,7 +1587,10 @@ Connection::Clone(bool aReadOnly, mozIStorageConnection **_connection) {
AUTO_PROFILER_LABEL("Connection::Clone", OTHER);
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
if (!mDatabaseFile) return NS_ERROR_UNEXPECTED;
int flags = mFlags;
@ -1564,9 +1601,10 @@ Connection::Clone(bool aReadOnly, mozIStorageConnection **_connection) {
flags = (~SQLITE_OPEN_CREATE & flags);
}
RefPtr<Connection> clone = new Connection(mStorageService, flags, mAsyncOnly);
RefPtr<Connection> clone =
new Connection(mStorageService, flags, mSupportedOperations);
nsresult rv = initializeClone(clone, aReadOnly);
rv = initializeClone(clone, aReadOnly);
if (NS_FAILED(rv)) {
return rv;
}
@ -1581,7 +1619,7 @@ Connection::Interrupt() {
if (!mDBConn) {
return NS_ERROR_NOT_INITIALIZED;
}
if (!mAsyncOnly || !(mFlags & SQLITE_OPEN_READONLY)) {
if (mSupportedOperations == SYNCHRONOUS || !(mFlags & SQLITE_OPEN_READONLY)) {
return NS_ERROR_INVALID_ARG;
}
::sqlite3_interrupt(mDBConn);
@ -1597,13 +1635,16 @@ Connection::GetDefaultPageSize(int32_t *_defaultPageSize) {
NS_IMETHODIMP
Connection::GetConnectionReady(bool *_ready) {
MOZ_ASSERT(threadOpenedOn == NS_GetCurrentThread());
*_ready = connectionReady();
*_ready = !!mDBConn;
return NS_OK;
}
NS_IMETHODIMP
Connection::GetDatabaseFile(nsIFile **_dbFile) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(ASYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
NS_IF_ADDREF(*_dbFile = mDatabaseFile);
@ -1612,7 +1653,10 @@ Connection::GetDatabaseFile(nsIFile **_dbFile) {
NS_IMETHODIMP
Connection::GetLastInsertRowID(int64_t *_id) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
sqlite_int64 id = ::sqlite3_last_insert_rowid(mDBConn);
*_id = id;
@ -1622,7 +1666,10 @@ Connection::GetLastInsertRowID(int64_t *_id) {
NS_IMETHODIMP
Connection::GetAffectedRows(int32_t *_rows) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
*_rows = ::sqlite3_changes(mDBConn);
@ -1631,7 +1678,10 @@ Connection::GetAffectedRows(int32_t *_rows) {
NS_IMETHODIMP
Connection::GetLastError(int32_t *_error) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
*_error = ::sqlite3_errcode(mDBConn);
@ -1640,7 +1690,10 @@ Connection::GetLastError(int32_t *_error) {
NS_IMETHODIMP
Connection::GetLastErrorString(nsACString &_errorString) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
const char *serr = ::sqlite3_errmsg(mDBConn);
_errorString.Assign(serr);
@ -1650,7 +1703,10 @@ Connection::GetLastErrorString(nsACString &_errorString) {
NS_IMETHODIMP
Connection::GetSchemaVersion(int32_t *_version) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
nsCOMPtr<mozIStorageStatement> stmt;
(void)CreateStatement(NS_LITERAL_CSTRING("PRAGMA user_version"),
@ -1667,7 +1723,10 @@ Connection::GetSchemaVersion(int32_t *_version) {
NS_IMETHODIMP
Connection::SetSchemaVersion(int32_t aVersion) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
nsAutoCString stmt(NS_LITERAL_CSTRING("PRAGMA user_version = "));
stmt.AppendInt(aVersion);
@ -1679,12 +1738,15 @@ NS_IMETHODIMP
Connection::CreateStatement(const nsACString &aSQLStatement,
mozIStorageStatement **_stmt) {
NS_ENSURE_ARG_POINTER(_stmt);
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
RefPtr<Statement> statement(new Statement());
NS_ENSURE_TRUE(statement, NS_ERROR_OUT_OF_MEMORY);
nsresult rv = statement->initialize(this, mDBConn, aSQLStatement);
rv = statement->initialize(this, mDBConn, aSQLStatement);
NS_ENSURE_SUCCESS(rv, rv);
Statement *rawPtr;
@ -1697,12 +1759,15 @@ NS_IMETHODIMP
Connection::CreateAsyncStatement(const nsACString &aSQLStatement,
mozIStorageAsyncStatement **_stmt) {
NS_ENSURE_ARG_POINTER(_stmt);
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(ASYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
RefPtr<AsyncStatement> statement(new AsyncStatement());
NS_ENSURE_TRUE(statement, NS_ERROR_OUT_OF_MEMORY);
nsresult rv = statement->initialize(this, mDBConn, aSQLStatement);
rv = statement->initialize(this, mDBConn, aSQLStatement);
NS_ENSURE_SUCCESS(rv, rv);
AsyncStatement *rawPtr;
@ -1714,7 +1779,10 @@ Connection::CreateAsyncStatement(const nsACString &aSQLStatement,
NS_IMETHODIMP
Connection::ExecuteSimpleSQL(const nsACString &aSQLStatement) {
CHECK_MAINTHREAD_ABUSE();
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
int srv = executeSql(mDBConn, PromiseFlatCString(aSQLStatement).get());
return convertResultCode(srv);
@ -1781,7 +1849,10 @@ Connection::IndexExists(const nsACString &aIndexName, bool *_exists) {
NS_IMETHODIMP
Connection::GetTransactionInProgress(bool *_inProgress) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
SQLiteMutexAutoLock lockedScope(sharedDBMutex);
*_inProgress = mTransactionInProgress;
@ -1803,7 +1874,10 @@ Connection::SetDefaultTransactionType(int32_t aType) {
NS_IMETHODIMP
Connection::BeginTransaction() {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
return beginTransactionInternal(mDBConn, mDefaultTransactionType);
}
@ -1832,7 +1906,10 @@ nsresult Connection::beginTransactionInternal(sqlite3 *aNativeConnection,
NS_IMETHODIMP
Connection::CommitTransaction() {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
return commitTransactionInternal(mDBConn);
}
@ -1848,7 +1925,10 @@ nsresult Connection::commitTransactionInternal(sqlite3 *aNativeConnection) {
NS_IMETHODIMP
Connection::RollbackTransaction() {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
return rollbackTransactionInternal(mDBConn);
}
@ -1865,7 +1945,10 @@ nsresult Connection::rollbackTransactionInternal(sqlite3 *aNativeConnection) {
NS_IMETHODIMP
Connection::CreateTable(const char *aTableName, const char *aTableSchema) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
SmprintfPointer buf =
::mozilla::Smprintf("CREATE TABLE %s (%s)", aTableName, aTableSchema);
@ -1880,7 +1963,10 @@ NS_IMETHODIMP
Connection::CreateFunction(const nsACString &aFunctionName,
int32_t aNumArguments,
mozIStorageFunction *aFunction) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(ASYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
// Check to see if this function is already defined. We only check the name
// because a function can be defined with the same body but different names.
@ -1903,7 +1989,10 @@ NS_IMETHODIMP
Connection::CreateAggregateFunction(const nsACString &aFunctionName,
int32_t aNumArguments,
mozIStorageAggregateFunction *aFunction) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(ASYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
// Check to see if this function name is already defined.
SQLiteMutexAutoLock lockedScope(sharedDBMutex);
@ -1929,7 +2018,10 @@ Connection::CreateAggregateFunction(const nsACString &aFunctionName,
NS_IMETHODIMP
Connection::RemoveFunction(const nsACString &aFunctionName) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(ASYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
SQLiteMutexAutoLock lockedScope(sharedDBMutex);
NS_ENSURE_TRUE(mFunctions.Get(aFunctionName, nullptr), NS_ERROR_FAILURE);
@ -1948,7 +2040,10 @@ NS_IMETHODIMP
Connection::SetProgressHandler(int32_t aGranularity,
mozIStorageProgressHandler *aHandler,
mozIStorageProgressHandler **_oldHandler) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(ASYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
// Return previous one
SQLiteMutexAutoLock lockedScope(sharedDBMutex);
@ -1981,13 +2076,17 @@ Connection::RemoveProgressHandler(mozIStorageProgressHandler **_oldHandler) {
NS_IMETHODIMP
Connection::SetGrowthIncrement(int32_t aChunkSize,
const nsACString &aDatabaseName) {
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
// Bug 597215: Disk space is extremely limited on Android
// so don't preallocate space. This is also not effective
// on log structured file systems used by Android devices
#if !defined(ANDROID) && !defined(MOZ_PLATFORM_MAEMO)
// Don't preallocate if less than 500MiB is available.
int64_t bytesAvailable;
nsresult rv = mDatabaseFile->GetDiskSpaceAvailable(&bytesAvailable);
rv = mDatabaseFile->GetDiskSpaceAvailable(&bytesAvailable);
NS_ENSURE_SUCCESS(rv, rv);
if (bytesAvailable < MIN_AVAILABLE_BYTES_PER_CHUNKED_GROWTH) {
return NS_ERROR_FILE_TOO_BIG;
@ -2004,7 +2103,10 @@ Connection::SetGrowthIncrement(int32_t aChunkSize,
NS_IMETHODIMP
Connection::EnableModule(const nsACString &aModuleName) {
if (!mDBConn) return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
for (auto &gModule : gModules) {
struct Module *m = &gModule;
@ -2028,8 +2130,9 @@ Connection::GetQuotaObjects(QuotaObject **aDatabaseQuotaObject,
MOZ_ASSERT(aDatabaseQuotaObject);
MOZ_ASSERT(aJournalQuotaObject);
if (!mDBConn) {
return NS_ERROR_NOT_INITIALIZED;
nsresult rv = connectionReady(SYNCHRONOUS);
if (NS_FAILED(rv)) {
return rv;
}
sqlite3_file *file;

View file

@ -44,6 +44,15 @@ class Connection final : public mozIStorageConnection,
NS_DECL_MOZISTORAGECONNECTION
NS_DECL_NSIINTERFACEREQUESTOR
/**
* Indicates if a database operation is synchronous or asynchronous.
*
* - Async operations may be called from any thread for all connections.
* - Sync operations may be called from any thread for sync connections, and
* from background threads for async connections.
*/
enum ConnectionOperation { ASYNCHRONOUS, SYNCHRONOUS };
/**
* Structure used to describe user functions on the database connection.
*/
@ -61,11 +70,11 @@ class Connection final : public mozIStorageConnection,
* connection.
* @param aFlags
* The flags to pass to sqlite3_open_v2.
* @param aAsyncOnly
* If |true|, the Connection only implements asynchronous interface:
* - |mozIStorageAsyncConnection|;
* If |false|, the result also implements synchronous interface:
* - |mozIStorageConnection|.
* @param aSupportedOperations
* The operation types supported on this connection. All connections
* implement both the async (`mozIStorageAsyncConnection`) and sync
* (`mozIStorageConnection`) interfaces, but async connections may not
* call sync operations from the main thread.
* @param aIgnoreLockingMode
* If |true|, ignore locks in force on the file. Only usable with
* read-only connections. Defaults to false.
@ -74,7 +83,8 @@ class Connection final : public mozIStorageConnection,
* corrupt) or produce wrong results without any indication that has
* happened.
*/
Connection(Service *aService, int aFlags, bool aAsyncOnly,
Connection(Service *aService, int aFlags,
ConnectionOperation aSupportedOperations,
bool aIgnoreLockingMode = false);
/**
@ -225,7 +235,17 @@ class Connection final : public mozIStorageConnection,
nsresult commitTransactionInternal(sqlite3 *aNativeConnection);
nsresult rollbackTransactionInternal(sqlite3 *aNativeConnection);
bool connectionReady();
/**
* Indicates if this database connection is ready and supports the given
* operation.
*
* @param aOperationType
* The operation type, sync or async.
* @throws NS_ERROR_NOT_AVAILABLE if the operation isn't supported on this
* connection.
* @throws NS_ERROR_NOT_INITIALIZED if the connection isn't set up.
*/
nsresult connectionReady(ConnectionOperation aOperationType);
/**
* Thread-aware version of connectionReady, results per caller's thread are:
@ -418,10 +438,11 @@ class Connection final : public mozIStorageConnection,
RefPtr<Service> mStorageService;
/**
* If |false|, this instance supports synchronous operations
* and it can be cast to |mozIStorageConnection|.
* Indicates which operations are supported on this connection.
*/
const bool mAsyncOnly;
const ConnectionOperation mSupportedOperations;
nsresult synchronousClose();
};
/**

View file

@ -119,7 +119,8 @@ Service::CollectReports(nsIHandleReportCallback *aHandleReport,
// main-thread, like the DOM Cache and IndexedDB, and as such we must be
// sure that we have a connection.
MutexAutoLock lockedAsyncScope(conn->sharedAsyncExecutionMutex);
if (!conn->connectionReady()) {
nsresult rv = conn->connectionReady(Connection::ASYNCHRONOUS);
if (NS_FAILED(rv)) {
continue;
}
@ -303,7 +304,10 @@ void Service::minimizeMemory() {
RefPtr<Connection> conn = connections[i];
// For non-main-thread owning/opening threads, we may be racing against them
// closing their connection or their thread. That's okay, see below.
if (!conn->connectionReady()) continue;
nsresult rv = conn->connectionReady(Connection::ASYNCHRONOUS);
if (NS_FAILED(rv)) {
continue;
}
NS_NAMED_LITERAL_CSTRING(shrinkPragma, "PRAGMA shrink_memory");
nsCOMPtr<mozIStorageConnection> syncConn = do_QueryInterface(
@ -453,7 +457,8 @@ Service::OpenSpecialDatabase(const char *aStorageKey,
return NS_ERROR_INVALID_ARG;
}
RefPtr<Connection> msc = new Connection(this, SQLITE_OPEN_READWRITE, false);
RefPtr<Connection> msc =
new Connection(this, SQLITE_OPEN_READWRITE, Connection::SYNCHRONOUS);
rv = storageFile ? msc->initialize(storageFile) : msc->initialize();
NS_ENSURE_SUCCESS(rv, rv);
@ -604,7 +609,8 @@ Service::OpenAsyncDatabase(nsIVariant *aDatabaseStore,
}
// Create connection on this thread, but initialize it on its helper thread.
RefPtr<Connection> msc = new Connection(this, flags, true, ignoreLockingMode);
RefPtr<Connection> msc =
new Connection(this, flags, Connection::ASYNCHRONOUS, ignoreLockingMode);
nsCOMPtr<nsIEventTarget> target = msc->getAsyncExecutionTarget();
MOZ_ASSERT(target,
"Cannot initialize a connection that has been closed already");
@ -623,7 +629,7 @@ Service::OpenDatabase(nsIFile *aDatabaseFile,
// reasons.
int flags =
SQLITE_OPEN_READWRITE | SQLITE_OPEN_SHAREDCACHE | SQLITE_OPEN_CREATE;
RefPtr<Connection> msc = new Connection(this, flags, false);
RefPtr<Connection> msc = new Connection(this, flags, Connection::SYNCHRONOUS);
nsresult rv = msc->initialize(aDatabaseFile);
NS_ENSURE_SUCCESS(rv, rv);
@ -641,7 +647,7 @@ Service::OpenUnsharedDatabase(nsIFile *aDatabaseFile,
// reasons.
int flags =
SQLITE_OPEN_READWRITE | SQLITE_OPEN_PRIVATECACHE | SQLITE_OPEN_CREATE;
RefPtr<Connection> msc = new Connection(this, flags, false);
RefPtr<Connection> msc = new Connection(this, flags, Connection::SYNCHRONOUS);
nsresult rv = msc->initialize(aDatabaseFile);
NS_ENSURE_SUCCESS(rv, rv);
@ -659,7 +665,7 @@ Service::OpenDatabaseWithFileURL(nsIFileURL *aFileURL,
// reasons.
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_SHAREDCACHE |
SQLITE_OPEN_CREATE | SQLITE_OPEN_URI;
RefPtr<Connection> msc = new Connection(this, flags, false);
RefPtr<Connection> msc = new Connection(this, flags, Connection::SYNCHRONOUS);
nsresult rv = msc->initialize(aFileURL);
NS_ENSURE_SUCCESS(rv, rv);

View file

@ -675,6 +675,52 @@ Statement::GetString(uint32_t aIndex, nsAString &_value) {
return NS_OK;
}
NS_IMETHODIMP
Statement::GetVariant(uint32_t aIndex, nsIVariant **_value) {
if (!mDBStatement) {
return NS_ERROR_NOT_INITIALIZED;
}
ENSURE_INDEX_VALUE(aIndex, mResultColumnCount);
if (!mExecuting) {
return NS_ERROR_UNEXPECTED;
}
nsCOMPtr<nsIVariant> variant;
int type = ::sqlite3_column_type(mDBStatement, aIndex);
switch (type) {
case SQLITE_INTEGER:
variant =
new IntegerVariant(::sqlite3_column_int64(mDBStatement, aIndex));
break;
case SQLITE_FLOAT:
variant = new FloatVariant(::sqlite3_column_double(mDBStatement, aIndex));
break;
case SQLITE_TEXT: {
const char16_t *value = static_cast<const char16_t *>(
::sqlite3_column_text16(mDBStatement, aIndex));
nsDependentString str(value,
::sqlite3_column_bytes16(mDBStatement, aIndex) / 2);
variant = new TextVariant(str);
break;
}
case SQLITE_NULL:
variant = new NullVariant();
break;
case SQLITE_BLOB: {
int size = ::sqlite3_column_bytes(mDBStatement, aIndex);
const void *data = ::sqlite3_column_blob(mDBStatement, aIndex);
variant = new BlobVariant(std::pair<const void *, int>(data, size));
break;
}
}
NS_ENSURE_TRUE(variant, NS_ERROR_UNEXPECTED);
variant.forget(_value);
return NS_OK;
}
NS_IMETHODIMP
Statement::GetBlob(uint32_t aIndex, uint32_t *_size, uint8_t **_blob) {
if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED;

13
storage/rust/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "storage"
description = "Rust bindings for mozStorage."
version = "0.1.0"
authors = ["Lina Cambridge <lina@yakshaving.ninja>"]
edition = "2018"
[dependencies]
libc = "0.2"
nserror = { path = "../../xpcom/rust/nserror" }
nsstring = { path = "../../xpcom/rust/nsstring" }
storage_variant = { path = "../variant" }
xpcom = { path = "../../xpcom/rust/xpcom" }

281
storage/rust/src/lib.rs Normal file
View file

@ -0,0 +1,281 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
//! A Rust wrapper for mozStorage.
//!
//! mozStorage wraps the SQLite C API with support for XPCOM data structures,
//! asynchronous statement execution, cleanup on shutdown, and connection
//! cloning that propagates attached databases, pragmas, functions, and
//! temporary entities. It also collects timing and memory usage stats for
//! telemetry, and supports detailed statement logging. Additionally, mozStorage
//! makes it possible to use the same connection handle from JS and native
//! (C++ and Rust) code.
//!
//! Most mozStorage objects, like connections, statements, result rows,
//! and variants, are thread-safe. Each connection manages a background
//! thread that can be used to execute statements asynchronously, without
//! blocking the main thread.
//!
//! This crate provides a thin wrapper to make mozStorage easier to use
//! from Rust. It only wraps the synchronous API, so you can either manage
//! the entire connection from a background thread, or use the `moz_task`
//! crate to dispatch tasks to the connection's async thread. Executing
//! synchronous statements on the main thread is not supported, and will
//! assert in debug builds.
#![allow(non_snake_case)]
use std::{ops::Deref, result};
use nserror::{nsresult, NS_ERROR_NO_INTERFACE};
use nsstring::nsCString;
use storage_variant::VariantType;
use xpcom::{
getter_addrefs,
interfaces::{
mozIStorageAsyncConnection, mozIStorageConnection, mozIStorageStatement, nsIEventTarget,
nsIThread,
},
RefPtr, XpCom,
};
pub type Result<T> = result::Result<T, nsresult>;
/// `Conn` wraps a `mozIStorageConnection`.
#[derive(Clone)]
pub struct Conn {
handle: RefPtr<mozIStorageConnection>,
}
// This is safe as long as our `mozIStorageConnection` is an instance of
// `mozilla::storage::Connection`, which is atomically reference counted.
unsafe impl Send for Conn {}
unsafe impl Sync for Conn {}
impl Conn {
/// Wraps a `mozIStorageConnection` in a `Conn`.
#[inline]
pub fn wrap(connection: RefPtr<mozIStorageConnection>) -> Conn {
Conn { handle: connection }
}
/// Returns the wrapped `mozIStorageConnection`.
#[inline]
pub fn connection(&self) -> &mozIStorageConnection {
&self.handle
}
/// Returns the async thread for this connection. This can be used
/// with `moz_task` to run synchronous statements on the storage
/// thread, without blocking the main thread.
pub fn thread(&self) -> Result<RefPtr<nsIThread>> {
let target = self.handle.get_interface::<nsIEventTarget>();
target
.and_then(|t| t.query_interface::<nsIThread>())
.ok_or(NS_ERROR_NO_INTERFACE)
}
/// Prepares a SQL statement. `query` should only contain one SQL statement.
/// If `query` contains multiple statements, only the first will be prepared,
/// and the rest will be ignored.
pub fn prepare<Q: AsRef<str>>(&self, query: Q) -> Result<Statement> {
let statement = getter_addrefs(|p| unsafe {
self.handle
.CreateStatement(&*nsCString::from(query.as_ref()), p)
})?;
Ok(Statement { handle: statement })
}
/// Executes a SQL statement. `query` may contain one or more
/// semicolon-separated SQL statements.
pub fn exec<Q: AsRef<str>>(&self, query: Q) -> Result<()> {
unsafe {
self.handle
.ExecuteSimpleSQL(&*nsCString::from(query.as_ref()))
}
.to_result()
}
/// Opens a transaction with the default transaction behavior for this
/// connection. The transaction should be committed when done. Uncommitted
/// `Transaction`s will automatically roll back when they go out of scope.
pub fn transaction(&mut self) -> Result<Transaction> {
let behavior = self.get_default_transaction_behavior();
Transaction::new(self, behavior)
}
/// Opens a transaction with the requested behavior.
pub fn transaction_with_behavior(
&mut self,
behavior: TransactionBehavior,
) -> Result<Transaction> {
Transaction::new(self, behavior)
}
fn get_default_transaction_behavior(&self) -> TransactionBehavior {
let mut typ = 0i32;
let rv = unsafe { self.handle.GetDefaultTransactionType(&mut typ) };
if rv.failed() {
return TransactionBehavior::Deferred;
}
match typ as i64 {
mozIStorageAsyncConnection::TRANSACTION_IMMEDIATE => TransactionBehavior::Immediate,
mozIStorageAsyncConnection::TRANSACTION_EXCLUSIVE => TransactionBehavior::Exclusive,
_ => TransactionBehavior::Deferred,
}
}
}
pub enum TransactionBehavior {
Deferred,
Immediate,
Exclusive,
}
pub struct Transaction<'c> {
conn: &'c mut Conn,
active: bool,
}
impl<'c> Transaction<'c> {
/// Opens a transaction on `conn` with the given `behavior`.
fn new(conn: &'c mut Conn, behavior: TransactionBehavior) -> Result<Transaction<'c>> {
conn.exec(match behavior {
TransactionBehavior::Deferred => "BEGIN DEFERRED",
TransactionBehavior::Immediate => "BEGIN IMMEDIATE",
TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE",
})?;
Ok(Transaction { conn, active: true })
}
/// Commits the transaction.
pub fn commit(mut self) -> Result<()> {
if self.active {
self.conn.exec("COMMIT")?;
self.active = false;
}
Ok(())
}
/// Rolls the transaction back.
pub fn rollback(mut self) -> Result<()> {
self.abort()
}
fn abort(&mut self) -> Result<()> {
if self.active {
self.conn.exec("ROLLBACK")?;
self.active = false;
}
Ok(())
}
}
impl<'c> Deref for Transaction<'c> {
type Target = Conn;
fn deref(&self) -> &Conn {
self.conn
}
}
impl<'c> Drop for Transaction<'c> {
fn drop(&mut self) {
let _ = self.abort();
}
}
pub struct Statement {
handle: RefPtr<mozIStorageStatement>,
}
impl Statement {
/// Binds a parameter at the given `index` to the prepared statement.
/// `value` is any type that can be converted into a `Variant`.
pub fn bind_by_index<V: VariantType>(&mut self, index: u32, value: V) -> Result<()> {
let variant = value.into_variant();
unsafe { self.handle.BindByIndex(index as u32, variant.coerce()) }.to_result()
}
/// Binds a parameter with the given `name` to the prepared statement.
pub fn bind_by_name<N: AsRef<str>, V: VariantType>(&mut self, name: N, value: V) -> Result<()> {
let variant = value.into_variant();
unsafe {
self.handle
.BindByName(&*nsCString::from(name.as_ref()), variant.coerce())
}
.to_result()
}
/// Executes the statement and returns the next row of data.
pub fn step<'a>(&'a mut self) -> Result<Option<Step<'a>>> {
let mut has_more = false;
unsafe { self.handle.ExecuteStep(&mut has_more) }.to_result()?;
Ok(if has_more { Some(Step(self)) } else { None })
}
/// Executes the statement once, discards any data, and resets the
/// statement.
pub fn execute(&mut self) -> Result<()> {
unsafe { self.handle.Execute() }.to_result()
}
/// Resets the prepared statement so that it's ready to be executed
/// again, and clears any bound parameters.
pub fn reset(&mut self) -> Result<()> {
unsafe { self.handle.Reset() }.to_result()
}
fn get_column_index<N: AsRef<str>>(&self, name: N) -> Result<u32> {
let mut index = 0u32;
unsafe {
self.handle
.GetColumnIndex(&*nsCString::from(name.as_ref()), &mut index)
}
.to_result()
.map(|_| index)
}
fn get_variant<T: VariantType>(&self, index: u32) -> Result<T> {
let variant = getter_addrefs(|p| unsafe { self.handle.GetVariant(index, p) })?;
T::from_variant(variant.coerce())
}
}
impl Drop for Statement {
fn drop(&mut self) {
unsafe { self.handle.Finalize() };
}
}
/// A step is the next row in the result set for a statement.
pub struct Step<'a>(&'a mut Statement);
impl<'a> Step<'a> {
/// Returns the value of the column at `index` for the current row.
pub fn get_by_index<'s, T: VariantType>(&'s self, index: u32) -> Result<T> {
self.0.get_variant(index)
}
/// A convenience wrapper that returns the default value for the column
/// at `index` if `NULL`.
pub fn get_by_index_or_default<'s, T: VariantType + Default>(&'s self, index: u32) -> T {
self.get_by_index(index).unwrap_or_default()
}
/// Returns the value of the column specified by `name` for the current row.
pub fn get_by_name<'s, N: AsRef<str>, T: VariantType>(&'s self, name: N) -> Result<T> {
let index = self.0.get_column_index(name)?;
self.0.get_variant(index)
}
/// Returns the default value for the column with the given `name`, or the
/// default if the column is `NULL`.
pub fn get_by_name_or_default<'s, N: AsRef<str>, T: VariantType + Default>(
&'s self,
name: N,
) -> T {
self.get_by_name(name).unwrap_or_default()
}
}

View file

@ -314,7 +314,7 @@ async function standardAsyncTest(promisedDB, name, shouldInit = false) {
let adb = await promisedDB;
Assert.ok(adb instanceof Ci.mozIStorageAsyncConnection);
Assert.equal(false, adb instanceof Ci.mozIStorageConnection);
Assert.ok(adb instanceof Ci.mozIStorageConnection);
if (shouldInit) {
let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)");
@ -440,7 +440,7 @@ add_task(async function test_clone_trivial_async() {
info("AsyncClone connection");
let clone = await asyncClone(db, true);
Assert.ok(clone instanceof Ci.mozIStorageAsyncConnection);
Assert.equal(false, clone instanceof Ci.mozIStorageConnection);
Assert.ok(clone instanceof Ci.mozIStorageConnection);
info("Close connection");
await asyncClose(db);
info("Close clone");
@ -452,7 +452,7 @@ add_task(async function test_clone_no_optional_param_async() {
info("Testing async cloning");
let adb1 = await openAsyncDatabase(getTestDB(), null);
Assert.ok(adb1 instanceof Ci.mozIStorageAsyncConnection);
Assert.equal(false, adb1 instanceof Ci.mozIStorageConnection);
Assert.ok(adb1 instanceof Ci.mozIStorageConnection);
info("Cloning database");
@ -460,7 +460,7 @@ add_task(async function test_clone_no_optional_param_async() {
info("Testing that the cloned db is a mozIStorageAsyncConnection " +
"and not a mozIStorageConnection");
Assert.ok(adb2 instanceof Ci.mozIStorageAsyncConnection);
Assert.equal(false, adb2 instanceof Ci.mozIStorageConnection);
Assert.ok(adb2 instanceof Ci.mozIStorageConnection);
info("Inserting data into source db");
let stmt = adb1.

View file

@ -142,6 +142,21 @@ impl VariantType for () {
}
}
impl<T> VariantType for Option<T> where T: VariantType {
fn into_variant(self) -> RefPtr<nsIVariant> {
match self {
Some(v) => v.into_variant(),
None => ().into_variant(),
}
}
fn from_variant(variant: &nsIVariant) -> Result<Self, nsresult> {
match variant.get_data_type() {
DATA_TYPE_EMPTY => Ok(None),
_ => Ok(Some(VariantType::from_variant(variant)?)),
}
}
}
variant!(bool, NS_NewStorageBooleanVariant, GetAsBool);
variant!(i32, NS_NewStorageIntegerVariant, GetAsInt32);
variant!(i64, NS_NewStorageIntegerVariant, GetAsInt64);

View file

@ -34,6 +34,7 @@ jsrust_shared = { path = "../../../../js/src/rust/shared", optional = true }
arrayvec = "0.4"
cert_storage = { path = "../../../../security/manager/ssl/cert_storage" }
bitsdownload = { path = "../../../components/bitsdownload", optional = true }
storage = { path = "../../../../storage/rust" }
[build-dependencies]
rustc_version = "0.2"

View file

@ -38,6 +38,7 @@ extern crate rsdparsa_capi;
extern crate jsrust_shared;
#[cfg(feature = "bitsdownload")]
extern crate bitsdownload;
extern crate storage;
extern crate arrayvec;

View file

@ -1042,7 +1042,7 @@ function cloneStorageConnection(options) {
}
if (isClosed) {
throw new Error("Sqlite.jsm has been shutdown. Cannot clone connection to: " + source.database.path);
throw new Error("Sqlite.jsm has been shutdown. Cannot clone connection to: " + source.databaseFile.path);
}
let openedOptions = {};
@ -1107,7 +1107,7 @@ function wrapStorageConnection(options) {
}
if (isClosed) {
throw new Error("Sqlite.jsm has been shutdown. Cannot wrap connection to: " + connection.database.path);
throw new Error("Sqlite.jsm has been shutdown. Cannot wrap connection to: " + connection.databaseFile.path);
}
let identifier = getIdentifierByFileName(connection.databaseFile.leafName);
@ -1213,6 +1213,28 @@ OpenedConnection.prototype = Object.freeze({
TRANSACTION_IMMEDIATE: "IMMEDIATE",
TRANSACTION_EXCLUSIVE: "EXCLUSIVE",
/**
* Returns a handle to the underlying `mozIStorageAsyncConnection`. This is
* **extremely unsafe** because `Sqlite.jsm` continues to manage the
* connection's lifecycle, including transactions and shutdown blockers.
* Misusing the raw connection can easily lead to data loss, memory leaks,
* and errors.
*
* Consumers of the raw connection **must not** close or re-wrap it,
* and should not run statements concurrently with `Sqlite.jsm`.
*
* It's _much_ safer to open a `mozIStorage{Async}Connection` yourself,
* and access it from JavaScript via `Sqlite.wrapStorageConnection`.
* `unsafeRawConnection` is an escape hatch for cases where you can't
* do that.
*
* Please do _not_ add new uses of `unsafeRawConnection` without review
* from a storage peer.
*/
get unsafeRawConnection() {
return this._connectionData._dbConn;
},
/**
* The integer schema version of the database.
*

View file

@ -7,7 +7,7 @@ use {
RefPtr,
GetterAddrefs
};
use interfaces::nsISupports;
use interfaces::{nsIInterfaceRequestor, nsISupports};
#[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq)]
@ -44,4 +44,20 @@ pub unsafe trait XpCom : RefCounted {
}
}
}
/// Perform a `GetInterface` call on this object, returning `None` if the
/// object doesn't implement `nsIInterfaceRequestor`, or can't access the
/// interface `T`.
fn get_interface<T: XpCom>(&self) -> Option<RefPtr<T>> {
let ireq = self.query_interface::<nsIInterfaceRequestor>()?;
let mut ga = GetterAddrefs::<T>::new();
unsafe {
if ireq.GetInterface(&T::IID, ga.void_ptr()).succeeded() {
ga.refptr()
} else {
None
}
}
}
}